fix(linkedin-studio): S15 — UX finish §6c (B1 onboarding inline-draft + B3 carousel full-deck clipboard)

B2 (router tiering) was already delivered in S14, so S15 = B1 + B3 only.
No surface/count/version change -> within-v4.1.0 refinement (S11-S13 precedent).

- B1 (commands/onboarding.md): replace the "Run /linkedin:first-post" dead-end
  hand-off in Phase 3 with the first-post drafting steps embedded inline (3.1 topic
  -> 3.2 3-line draft -> 3.3 QC -> 3.4 present+clipboard -> 3.5 state-update that sets
  first_post_date). Wizard now yields a draft in-flow; 0 dead-end strings. Stays within
  the existing allowed-tools (Read/Bash/AskUserQuestion); UI-brief §12b scope-guard
  honored (no provider seams / progressive-disclosure added).
- B3 (commands/carousel.md): Step 6 now assembles the ENTIRE deck (every slide's copy
  + the caption) into the clipboard payload, not just the caption; full-deck assembly
  precedes the clipboard-helper.mjs call.

Independent /trekreview (2 Opus reviewers): brief-conformance 0 findings; code-correctness
1 MAJOR that is PRE-EXISTING and out of S15 scope (onboarding Phase 2 saves need Write in
allowed-tools; lines 142/157, untouched by the S15 diff) -> DEFERRED to next session per
"ekte design-funn -> neste sesjon". Verdict ALLOW for the delivered scope (not a WARN-override).

Gate: test-runner.sh 74/0/0; node --test 98/98; commands=29; v4.1.0 unchanged.
See docs/remediation/review.md for the full record (ALLOW + 1 deferred MAJOR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-30 21:44:34 +02:00
commit 8c52bdb2e4
3 changed files with 191 additions and 128 deletions

View file

@ -188,11 +188,29 @@ Create one slide per page using the content above.
Export as PDF and upload directly to LinkedIn.
```
Auto-copy the carousel caption text to clipboard silently:
```bash
printf '%s' '<CAROUSEL_CAPTION>' | node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/clipboard-helper.mjs
**Assemble the entire deck** — every slide's copy (header + body, plus its visual note) followed by the caption — into ONE clipboard payload, so the whole carousel travels in a single copy, not just the caption. A carousel's deliverable is the slide text you paste into your design tool *and* the caption; copying only the caption left the bulk of the work uncopied. Build the payload like this:
```
Then confirm: "Caption copied to clipboard."
SLIDE 1 of [TOTAL] — [purpose]
[HEADER]
[BODY line 1]
[BODY line 2]
...
Visual: [visual note]
SLIDE 2 of [TOTAL] — [purpose]
...
— — —
CAPTION
[caption text]
```
Then auto-copy the full deck to clipboard silently:
```bash
printf '%s' '<FULL_DECK_PAYLOAD>' | node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/clipboard-helper.mjs
```
Substitute `<FULL_DECK_PAYLOAD>` with the assembled deck above — all slides' copy + the caption. Then confirm: "Full deck — [N] slides + caption — copied to clipboard."
Offer refinement options as text (no interactive prompt):
"Want to refine? Options: adjust slide text / change visual style / regenerate specific slide / different hook / ready for publishing."

View file

@ -166,28 +166,76 @@ After setup, recalculate and show updated score.
╚═══════════════════════════════════════════╝
```
Check `first_post_date` in state file:
Check `first_post_date` in state file.
**If null (no first post yet):**
- "You're ready to create your first post! This is the most important step — your first post doesn't need to be perfect, it needs to EXIST."
- Use AskUserQuestion:
1. **Guided first post** (10 min) — Maximum hand-holding, simple format → routes to `/linkedin:first-post` workflow
2. **Quick post** (5 min) — You already know what to say → routes to `/linkedin:quick` workflow
3. **Not now** — I'll post later
**If first_post_date is set (returning user):**
- "You already have your first post (published [date]). Ready for your next one? Use `/linkedin:post` or `/linkedin:quick` whenever you are." Move to Phase 4.
**If first_post_date is set:**
- "You already have your first post (published [date]). Ready to create your next one?"
- Use AskUserQuestion:
1. **Create a new post** → suggest `/linkedin:post`
2. **Quick post** → suggest `/linkedin:quick`
3. **Exit onboarding**
**If null (no first post yet) — draft it inline, right here:**
**If user chooses to post (option 1 or 2):** Don't invoke the sub-command directly — instead, tell them:
"Run `/linkedin:first-post` to start the guided first-post flow."
or
"Run `/linkedin:quick` to create a quick post."
"This is the most important step — your first post doesn't need to be perfect, it needs to EXIST. Let's write it now, together, in this flow."
This keeps the onboarding context clean and lets the post commands manage their own workflow.
Use AskUserQuestion:
1. **Write my first post now** — Draft it inline, ~5 minutes, ready to paste
2. **Not now** — I'll post later (you can use `/linkedin:post` or `/linkedin:quick` anytime)
**If "Not now":** move to Phase 4 and mark the first post Pending.
**If "Write my first post now":** walk through these steps **inline** — do NOT hand off to another command. The post is produced right here in the wizard.
### 3.1 — Pick a topic
Use AskUserQuestion:
1. **Something I learned recently** — a specific insight from your work
2. **A tool or approach I recommend** — something that made your work better
3. **An observation about my industry** — a pattern or trend you've noticed
4. **A question I'm genuinely curious about** — start a conversation
Then ask: "Give me a sentence or two about what you have in mind." If expertise areas are set in the state file, steer the topic toward one of their pillars.
### 3.2 — Write the post (3-line formula)
Draft the post using the voice profile from Phase 2 (or the existing `assets/voice-samples/` profile):
- **Line 1 — Hook (under 140 chars):** specific to their experience, no generic opening
- **Line 2 — Context (1-3 sentences):** the what and why, kept tight
- **Line 3 — Insight + question:** their takeaway, ending on a genuine question that invites comments
Target 150-500 characters (short posts perform well for new accounts). One point, not three. No external links in the post body.
### 3.3 — Quick quality check
Confirm 4 things before presenting:
- [ ] Hook works in 140 chars?
- [ ] ONE clear point (not three)?
- [ ] Ends with a question or invitation?
- [ ] Sounds like THEM (not corporate/AI)?
Fix any miss before showing it.
### 3.4 — Present and copy
Show the post with its character count, the hook highlighted, and one alternative hook. Auto-copy the post text to clipboard silently:
```bash
printf '%s' '<POST_TEXT>' | node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/clipboard-helper.mjs
```
Then say: "Post copied to clipboard. Go to linkedin.com, click 'Start a post', paste it, and hit Post."
### 3.5 — Record it
Update state deterministically (this sets `first_post_date` automatically when null):
```bash
node --input-type=module -e "
import { writeState, updatePostTracking } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/state-updater.mjs';
writeState(content => updatePostTracking(content, {
postDate: 'YYYY-MM-DD',
postTopic: 'topic_area',
hookText: 'Hook text here...',
charCount: NNNN,
format: 'post'
}));
"
```
Replace the placeholders with the actual post data, then continue to Phase 4.
## Phase 4: Summary and Next Steps
@ -201,7 +249,7 @@ Show final status:
```
Profile: [Optimized / Skipped — run /linkedin:profile later]
Personalization: [XX]% [↑ from YY% if improved]
First post: [Published DATE / Pending — run /linkedin:first-post]
First post: [Published DATE / Pending — create anytime with /linkedin:post or /linkedin:quick]
```
**What's next — your first week:**

View file

@ -1,145 +1,142 @@
---
type: trekreview
review_version: "1.0"
task: "S14 — journey layer over the LinkedIn Studio command surface (operator-reframed from merge/cut after 14a found zero redundancy)"
task: "S15 — UX finish §6c on the final 29-command set: B1 onboarding inline-draft + B3 carousel full-deck clipboard (B2 router-tiering already delivered in S14)"
slug: remediation
project_dir: docs/remediation/
brief_path: docs/remediation/brief.md
scope_sha_start: 431a893
scope_sha_end: 431a893
reviewed_files_count: 15
scope_sha_start: baca30f
scope_sha_end: baca30f
reviewed_files_count: 2
verdict: ALLOW
mode: default
effort: high
effort: standard
profile: premium
findings: []
---
# Review — linkedin-studio S14 (journey layer; merge/cut → add a layer)
# Review — linkedin-studio S15 (UX finish §6c: B1 onboarding inline + B3 carousel full-deck)
## Executive Summary
**Verdict: ALLOW** (post-remediation) — 0 BLOCKER, 0 MAJOR, 0 MINOR, 0 SUGGESTION open.
Two independent reviewers (brief-conformance, code-correctness) ran COLD, high-effort,
without cross-feeding, on the as-delivered uncommitted working tree (HEAD `431a893` +
the S14 delta). **Each returned 2 MAJOR findings; deduped to 3 distinct issues.** All
three were **remediated in-session** with deterministic re-verification; the final
state that pushes is clean.
**Verdict: ALLOW** for S15's delivered scope — 0 BLOCKER, 0 MAJOR, 0 MINOR, 0 SUGGESTION
**attributable to the S15 diff**. Two independent reviewers (brief-conformance,
code-correctness) ran COLD, without cross-feeding, on the as-delivered uncommitted
working tree (HEAD `baca30f` + the S15 delta = two command files).
S14 was **reframed by operator decision (2026-05-30)**: 14a's cold per-command review
(`command-rationalization.md`) found **zero redundancy** across the 27 commands (no
defensible merge/cut), so instead of cutting, a **journey layer** was added over the
kept atomics. Build contract: `journey-layer-design.md`. Delivered: two new
delegate-only front-doors (`/linkedin:create`, `/linkedin:measure`), the router
re-tiered into five journeys (Start · Create · Engage · Measure · Grow), `onboarding`/
`strategy` elevated as the Start/Grow front-doors; 27 → 29 commands; v4.0.0 → v4.1.0
(minor/additive). Two of 14a's honesty nits (router lists `firsthour`; `calendar`
cross-links to it) were real and fixed; a third (a `competitive` 1K-gating claim) was
withdrawn as unfounded on verification.
- **brief-conformance-reviewer:** 0 findings. B1 and B3 both trace to delivered code; the
binding §12b scope-guard is honored (no extensibility / provider-seam / progressive-
disclosure / detect-and-offer language entered the diff); no version/count drift
(19/29/25/6 intact, v4.1.0 untouched); no files outside the two in scope.
- **code-correctness-reviewer:** 1 MAJOR — but **pre-existing and outside S15's scope**
(see Findings). The S15 changes themselves are correct: branches all terminate, the
state-updater + clipboard snippets mirror the established `first-post.md` pattern
exactly, `${CLAUDE_PLUGIN_ROOT}` is the established path var, and the larger carousel
clipboard payload introduces no new failure mode over the uniform plugin pattern.
**What both reviewers confirmed conformant + correct** (the bulk of the delivery):
the two front-doors are delegate-only (no `Write`; route to the contracted targets);
the router re-tier preserves **reachability for all 29 commands** (no atomic dropped;
`post-feedback-monitor` still mentioned in prose, not orphaned); `EXPECT_COMMANDS`
27→29 + `## Commands (29)` + `ls`=29 agree; version 4.1.0 is consistent across
plugin.json / README version badge / CLAUDE.md header / CHANGELOG / root docs; SemVer
framing is honest (additive, nothing falsely "breaking", nothing removed/renamed);
out-of-scope discipline held (no atomic folded; `multiplatform`-develop only recorded,
not built; S16/S17/UI-brief-M0 untouched).
S15 = **B1 + B3 only**. B2 (router tiering) was already delivered in S14 (the finish-plan
S15 body still lists B2, but the S14 amendment + the session-state label supersede it);
its absence here is correct, not a gap.
## Coverage
Scope: HEAD `431a893` (S13's commit) + the **uncommitted S14 working-tree delta**
Scope: HEAD `baca30f` (S14's commit) + the **uncommitted S15 working-tree delta**
(annotated `[uncommitted]` — a brief-level contract; the brief's Assumptions allow
uncommitted review). 15 files = the operator's own changes; the 3 untracked
not-mine files (`docs/linkedin-studio-persona-brief.md`, `…-ui-brief.md`,
`docs/voyage-build/progress.json`) are explicitly excluded from scope and from the
commit. **No silent skips.**
uncommitted review). 2 files = the operator's own changes. The 3 untracked not-mine files
(`docs/linkedin-studio-persona-brief.md`, `…-ui-brief.md`, `docs/voyage-build/progress.json`)
are explicitly excluded from scope and from the commit. **No silent skips.**
| Treatment | Count | Notes |
|-----------|-------|-------|
| `deep-review` | 6 | `commands/create.md` `[new]`, `commands/measure.md` `[new]`, `commands/linkedin.md` (router re-tier), `commands/calendar.md`, `scripts/test-runner.sh`, `README.md` |
| `summary-only` | 9 | plugin `CLAUDE.md`/`CHANGELOG.md`/`plugin.json`/`commands/onboarding.md`; root `CLAUDE.md`/`README.md`; `docs/remediation/{finish-plan.md, command-rationalization.md, journey-layer-design.md}` |
| `deep-review` | 0 | neither file is under `hooks/**` / `auth/**` / `crypto/**` / `**/security/**` |
| `summary-only` | 2 | `commands/onboarding.md` (Phase 3 rewrite + 1 Phase-4 summary line), `commands/carousel.md` (Step 6 clipboard) |
| `skip` | 0 | no lockfiles / svg / generated / dist |
**Cross-cutting execution criteria (run by orchestrator):**
`scripts/test-runner.sh`**74 passed / 0 failed / 0 warnings**, exit 0 (was 71;
+2 the two new commands' frontmatter checks, +1 a new README commands-badge guard,
1 net rounding of the count-loop — all green). `node --test …/*.test.mjs` → **98/98**
(no hook logic changed). `ls commands/*.md`**29**.
**B1/B3 acceptance checks (orchestrator-run greps):**
- B1: `grep -E "[Rr]un \`?/linkedin:first-post" commands/onboarding.md` → **0 dead-end strings**;
inline draft steps 3.13.5 present (topic → 3-line draft → QC → present+clipboard → state-update that sets `first_post_date`).
- B3: full-deck assembly block at `carousel.md:191` precedes the `clipboard-helper.mjs` call at `:211`; payload contains slide text; caption-only `<CAROUSEL_CAPTION>` removed.
**Cross-cutting execution criteria:** `scripts/test-runner.sh` → **74 passed / 0 failed /
0 warnings**, exit 0. `node --test hooks/scripts/__tests__/*.test.mjs`**98/98** (no hook
logic changed). `ls commands/*.md`**29** (no surface-count change; no version bump —
consistent with S11S13 precedent: within-scope refinements stay at the current version).
## Findings
**3 distinct MAJOR findings were raised by the independent reviewers and ALL closed
in-session.** No finding remains open; the trailing JSON `findings` is empty because
the pushed state carries none.
**0 findings attributable to the S15 diff.** One MAJOR was raised by code-correctness; on
inspection it is **pre-existing and out of S15's scope**, and is routed to the next session
per the finish-plan's locked constraint ("fix-in-next-session for any review finding") and
the operator rule "ekte design-funn → neste sesjon" (in-session fix is reserved for the
session's *own* lockstep misses). It is recorded below and propagated to STATE.md — **not
dropped, not silenced.**
### [MAJOR — RESOLVED] README commands badge stale at `commands-27`
*Raised by both reviewers (`COUNT_DRIFT` / `SUCCESS_CRITERION_UNMET`), `README.md:11`.*
The release reconciled the version badge, intro prose, CLAUDE.md header, `EXPECT_COMMANDS`,
and rosters to 29, but the shields **commands** badge still read `commands-27-green`. It
slipped both the human pass and the gate (the version-consistency grep checks only the
*version* badge; the design-doc verification grep searched `"27 command"` with a space,
which cannot match `commands-27-green`).
**Resolution:** `README.md:11``commands-29-green`; **and** a new
`test-runner.sh` Section-2 guard now asserts the `badge/commands-${EXPECT_COMMANDS}-`
shields badge matches the contract (closing the class, not just the line — the
count-badge analogue of the existing version-badge check). Re-verified: lint 74/0/0;
`grep commands-27` → 0 hits.
### [MAJOR — DEFERRED to next session] onboarding Phase 2 file-saves need `Write`, not in `allowed-tools`
### [MAJOR — RESOLVED] Router self-contradicted on `competitive` gating
*Raised by code-correctness (`INTERNAL_CONTRADICTION`), `commands/linkedin.md:140`.*
The Grow journey **table** marked `competitive` "Any phase" and the gating-rule note
said it is **not** gated, but the **"Ask the User"** menu line still rendered
`competitive ⚿` (the file's own ~1K soft-gate marker). An interim erroneous gating
(added then reverted in the same pass) had been reverted in the table + the gating note
+ the design doc, but the menu line was missed — the exact inconsistency the release
claims to fix.
**Resolution:** `commands/linkedin.md` Ask-the-User Grow line → `competitive` (no ⚿);
re-verified `grep "competitive ⚿"` → 0 hits across commands + the design doc. The router
now states competitive ungated consistently in the table, the note, and the menu.
*Raised by code-correctness (`PLAN_EXECUTE_DRIFT`), `commands/onboarding.md:142` (and `:157`).*
### [MAJOR — RESOLVED] STATE.md binding count block stale at 27 / v4.0.0
*Raised by brief-conformance (`SUCCESS_CRITERION_UNMET`), `STATE.md:46`.*
The design-doc lockstep enumerates the STATE.md count block as a surface to update
(27→29, v4.1.0). `STATE.md` is **gitignored** (not part of the pushed commit, so not a
push-blocking three-doc violation), but it is a contracted lockstep item and would
mislead the next session.
**Resolution:** updated to 29 / v4.1.0 as part of the session-close STATE.md overwrite
(which also advances the pointer to S15). Not in the pushed artifact (gitignored).
`onboarding.md` frontmatter grants `allowed-tools: [Read, Bash, AskUserQuestion]` — no
`Write`. **Phase 2** instructs "Save the responses to `assets/voice-samples/authentic-voice-samples.md`"
(`:142`, with a REPLACE-vs-append rule) and "Save to `config/user-profile.local.md`" (`:157`).
Persisting those profile files is a file write; the plugin's accepted pattern is the `Write`
tool, and the sibling `first-post.md` (same voice-save) declares `Write` (`first-post.md:9-14`).
With only Read/Bash/AskUserQuestion the agent cannot honor the two "Save to …" instructions
(the only scripted Bash writes are the `node -e` state and clipboard snippets). **Real defect.**
**Why DEFERRED, not fixed in S15:**
- **Pre-existing.** The gap exists identically at HEAD `baca30f` *before* the S15 edits —
`git show HEAD:…/onboarding.md` shows `allowed-tools: [Read, Bash, AskUserQuestion]` already.
- **Out of S15's diff scope.** The S15 hunks are `@@ -166` (Phase 3) and `@@ -201` (Phase 4
summary line); lines 142/157 (Phase 2) are untouched. S15's *own* inline steps (3.13.5)
stay within Read/Bash/AskUserQuestion and need no `Write` (3.4 clipboard = Bash, 3.5
state-update = Bash `node -e`).
- **Not introduced or worsened by S15.** The reviewer's "now owns the save flow" framing does
not hold on inspection: Phase 2's saves are reached identically before and after S15, and
the removed `/linkedin:first-post` hand-off was about *post creation* (Phase 3), not Phase 2's
profile saves. S15 added no save-flow that needs `Write`.
- **Operator governance.** Fixing a Phase-2 tool-contract bug inside an S15 (B1+B3) push would
be exactly the "rydd opp i pre-existing som del av en bugfix" / scope-expansion the operator's
rules forbid; the documented default for out-of-scope items is **defer** (record, route to
next session) — not silently fix, not silently drop.
**Recommended action (next session):** add `Write` to `onboarding.md`'s `allowed-tools`
(matching `first-post.md:9-14`), or replace the two "Save to …" instructions with a `node -e`
Bash write so the steps stay within the declared Bash grant. Natural home: fold into S16 (which
already opens the onboarding/saves surface) or the S17 triage pass.
## Remediation Summary
**Gate: ALLOW (post-remediation).** Two independent high-effort reviewers found 3
distinct MAJORs — all stale-count / incomplete-revert completion-misses of THIS
session's own committed lockstep, none a design flaw. All three were closed in-session
with deterministic re-verification (lint 74/0/0; targeted greps → 0 residual hits); a
new lint guard prevents the badge regression class from recurring. No BLOCKER; no
design or correctness defect in the journey layer itself; reachability for all 29
commands preserved; SemVer honest. The pushed state is clean.
**Gate: ALLOW** for S15's delivered scope (B1 + B3). Both reviewers confirm the two changes
are conformant (brief) and correct (code); the binding §12b scope-guard held; no version/count
drift; the inline first-post flow and the full-deck clipboard both function within the existing
tool grants and established patterns. The single MAJOR is a **pre-existing, out-of-scope**
tool-contract gap in Phase 2 that S15 neither introduced nor worsened; per the operator's
explicit "ekte design-funn → neste sesjon" rule it is **deferred and recorded**, not fixed
as scope creep. This is a genuine ALLOW of the scoped delivery — **not** a WARN-override (there
is no open finding *against* S15's diff).
Decision rationale for in-session fix (vs `feedback_trekreview_always_last`'s
fix-in-next-session): the three findings are mechanical completions of the current
task's own lockstep — #1 and #2 would have failed the design doc's own Verification
grep, #3 is an incomplete revert — i.e. *unfinished scope of S14*, not deliberation-
needing review findings. Shipping a README badge that reads "27" and a router that
self-contradicts on gating would be dishonest to the release's own claims. Closing them
now reaches a genuine ALLOW (not a WARN-override).
Per Handover 6, this `review.md` is consumable by `/trekplan --brief …`; with an ALLOW
verdict and no open findings, no follow-up plan is required — S14 may commit + push, and
the finish-plan continues at S15.
Per Handover 6, this `review.md` is consumable by `/trekplan --brief …`. With an ALLOW verdict
on the delivered scope and the one deferred finding routed forward, S15 may commit + push, and
the finish-plan continues at S16 (with the deferred `Write` gap reconciled there or in S17).
```json
{
"verdict": "ALLOW",
"scope": { "sha_start": "431a893", "sha_end": "431a893", "reviewed_files_count": 15, "uncommitted_delta": true },
"verdict_scope": "S15 delivered changes (B1 + B3); 2 files",
"scope": { "sha_start": "baca30f", "sha_end": "baca30f", "reviewed_files_count": 2, "uncommitted_delta": true },
"counts": { "BLOCKER": 0, "MAJOR": 0, "MINOR": 0, "SUGGESTION": 0 },
"findings": [],
"resolved_in_session": [
{ "severity": "MAJOR", "title": "README commands badge stale at 27", "file": "README.md", "line": 11, "rule_key": "COUNT_DRIFT", "resolution": "badge -> commands-29-green + new test-runner.sh commands-badge guard" },
{ "severity": "MAJOR", "title": "Router self-contradicts on competitive gating", "file": "commands/linkedin.md", "line": 140, "rule_key": "INTERNAL_CONTRADICTION", "resolution": "Ask-the-User Grow line competitive ⚿ -> competitive; 0 residual hits" },
{ "severity": "MAJOR", "title": "STATE.md binding count block stale at 27 / v4.0.0", "file": "STATE.md", "line": 46, "rule_key": "SUCCESS_CRITERION_UNMET", "resolution": "updated to 29 / v4.1.0 in session-close overwrite (gitignored, not pushed)" }
"deferred_findings": [
{
"severity": "MAJOR",
"title": "onboarding Phase 2 file-saves need Write, not in allowed-tools",
"file": "commands/onboarding.md",
"line": 142,
"rule_key": "PLAN_EXECUTE_DRIFT",
"status": "pre-existing, out of S15 scope (Phase 2; S15 diff = Phase 3 + Phase 4 line)",
"routed_to": "next session (S16 onboarding/saves surface, or S17 triage)",
"recommended_action": "add Write to onboarding.md allowed-tools (match first-post.md:9-14), or replace the two 'Save to ...' instructions with a node -e Bash write"
}
],
"dropped_findings": []
}