linkedin-studio/docs/hardening/review-R2a.md
Kjell Tore Guttormsen 5474df50e6 docs(linkedin-studio): cold-review R2a (Create emitters) — independent kald-review of 5 surfaces
R2a of the cold-review sweep: post/react/carousel/video/multiplatform, 2 independent cold
Opus reviewers per surface (intent + correctness lenses), no cross-feed, every mechanical
claim tool-grounded (anti-fabrication mandate). Verdict REWORK — 2 MAJOR (systemic
clipboard-printf apostrophe corruption across all 10 content commands; post personal-stories
band 1,000-1,400 vs its own Step 5 gate + canonical SSOT 1,200-1,800), 0 BLOCKER. Findings
only — no code changed. Local-only hardening artifact, not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GqEHp4uDiivfrAUjw4BdE
2026-06-30 07:18:43 +02:00

17 KiB
Raw Blame History

type batch journey scope method surfaces reviewers status verdict counts
cold-review R2a Create — atomic emitters FROZEN committed files vs HEAD 9567689 (no pending diff; post-hardening cold pass) 2 independent cold Opus reviewers per surface (intent + correctness), no cross-feed; every mechanical claim tool-grounded (anti-fabrication mandate); reviewers carry NO drafting-session context
post
react
carousel
video
multiplatform
intent-lens (conformance: intent delivery + cross-ref resolution + class predicates + terminology)
correctness-lens (internal consistency + bound-vs-canonical + checklist arithmetic + structure)
COMPLETE — all 5 surfaces reviewed (post, react, carousel, video, multiplatform) REWORK
BLOCKER MAJOR_systemic MAJOR_surface MINOR SUGGESTION
0 1 1 13 8

Cold review — R2a (Create · atomic emitters)

Independent post-hoc cold review of the 5 atomic Create-journey emitters, on the FROZEN committed files. Mirrors the S1 review.md model (the one cold-review method that did not fabricate): read-and-show before assert, every file:line tool-confirmed. The per-command interactive gate (S2S26, log.md) already passed these; this pass adds the independent axis that gate never had.

Independence cross-check worked: on every WAVE-1 surface the two blind lenses converged on the same real defects (personal-stories band in post; "full angle set below" in react; slide-scaffold + slide-count in carousel) — convergence from two no-cross-feed reviewers is the signal that a finding is real, not an artifact of one reviewer's framing.


★ Cross-cutting finding (systemic — spans all 10 content commands)

MAJOR (systemic) — printf '%s' '<TEXT>' clipboard pattern silently corrupts content containing an apostrophe

  • Pattern (verified by main, independent of reviewers): printf '%s' '<PLACEHOLDER>' | node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/clipboard-helper.mjs, followed by an unconditional Then confirm: "Copied to clipboard."
  • Blast radius — all 10 content-emitting commands (grep-confirmed): post.md:153, quick.md:162, react.md:149, carousel.md:211, video.md:175, multiplatform.md:121, pipeline.md:139, first-post.md:141, firsthour.md:72, onboarding.md:221.
  • Mechanism: the executing model substitutes the draft into the single-quoted bash argument. English LinkedIn drafts almost always contain an apostrophe ("it's", "don't", "here's"); a literal ' terminates the bash string → printf receives word-split fragments → clipboard gets garbled/truncated text (printf '%s' 'It's a test'Itsatest). The command gives no escaping guidance.
  • Why it matters (trust-breaking): the step runs "silently" then unconditionally reports success, so on the most common content shape the clipboard is corrupt while the user is told the copy worked. It is the headline convenience feature of every content command.
  • Severity note: flagged MAJOR (not BLOCKER) — it doesn't crash the session, and a careful executing model might escape; but the instruction's default path fails. Independently surfaced by rev-react-intent (MAJOR) and rev-post-intent (SUGGESTION, "convention-level").
  • Disposition (NOT fixed here — review finds, operator decides; 10-file change = own go): switch the convention to a no-in-content-quoting form — write the draft to a temp file and feed via stdin (node …/clipboard-helper.mjs < "$tmp"), or a quoted heredoc. Fix once, consistently, across all 10. Recommend treating this as the first fix that comes out of R2a.

post.md — VERDICT: REWORK (1 MAJOR · 2 MINOR · 1 SUGGESTION)

Class: post-emitting (primary) + guided/stateful (8-step). Both lenses confirm intent delivered; all cross-refs resolve (2 agents, 2 routes, 2 scripts, 7 assets — tool-confirmed); no "thought leadership".

MAJOR — Step 3 "Personal stories" band (1,0001,400) contradicts the file's own Step 5 gate + canonical SSOT (1,2001,800)

  • post.md:87 — Step 3 assigns "Personal stories | Medium text post (1,000-1,400 chars)" — a third band present nowhere else.
  • post.md:128 — Step 5 quality checklist requires "Character count: 1,200-1,800 (optimal range)" for the same post.
  • hooks/prompts/content-quality-gate.md:17 — canonical: "Standard posts: 1,200-1,800". A personal story is a standard text post (not quick 150500).
  • impact: a personal-story draft written to Step 3 at ~1,050 chars passes Step 3 but FAILS the Step 5 checklist and the save-time quality-gate hook. The command self-contradicts.
  • Both lenses flagged this (rev-post-correct MAJOR, rev-post-intent MINOR). Recorded at the higher severity: it hits a real gate path, not just advisory text.
  • disposition: raise line 87 floor to 1,2001,800, OR (if shorter personal stories are intentional) push the sub-band to the canonical SSOT first and reconcile :128 + content-quality-gate.md:17 — never leave a divergent band only in this file.

MINOR — Step 4 component minimums sum below the 1,200 optimal floor

  • post.md:100-104 — Hook 110-140 + Context 200-300 + Insight 400-800 + Implication 200-300 + CTA 50-100. Minimums sum to 960 (110+200+400+200+50); maximums to 1,640.
  • impact: following every section at its minimum yields a 960-char post, below the 1,200 floor asserted at :7/:86/:128 and canonical :17. Loose guidance, not a hard gate, but a writer hugging the low end lands under-length.
  • disposition: accept as-is, or lift Insight/Context minimums so the component floor reaches ~1,200.

SUGGESTION — clipboard apostrophe breakage → see ★ cross-cutting finding (post.md:153).


react.md — VERDICT: REWORK (1 MAJOR · 3 MINOR · 1 SUGGESTION)

Class: post-emitting + graceful-degradation on bad/empty URL (delivered, react.md:60,194). Intent (URL→post pipeline) delivered; all 7 cross-refs resolve; no "thought leadership".

MAJOR — clipboard apostrophe breakage → see ★ cross-cutting finding (react.md:148-151).

(Originally surfaced here by rev-react-intent; promoted to the cross-cutting section.)

MINOR — /linkedin:summarize trigger has no backing command

  • react.md:9 — the frontmatter description lists "/linkedin:summarize" among triggers. ls commands/summarize.md → does not exist; not among the 29 commands.
  • impact: advertises a slash-command alias that resolves to nothing; a user typing it gets no command. Dead/aspirational trigger.
  • disposition: remove /linkedin:summarize from the trigger list, or add a summarize alias command.

MINOR — "the full angle set below" is a dead in-file locator

  • react.md:95 — "present 2-3 alternatives from the full angle set below." No enumerated full angle set appears below this line; Step 4's table (:81-86) lists only 4 preferred + 4 fallback; the 8 universal angles live in references/content-angles.md (:273), not "below."
  • Both lenses flagged this (rev-react-intent + rev-react-correct).
  • impact: dangling locator on the "try a different angle" path; the model must guess "below" means the reference file.
  • disposition: change "the full angle set below" → "the 8 universal angles in references/content-angles.md".

MINOR — "medium post" label diverges from canonical "standard" band

  • react.md:119 — "Character target: 1,200-1,800 chars (medium post)". The number is correct (matches canonical standard band content-quality-gate.md:17 + CLAUDE.md rule 2), but canonical/CLAUDE.md label it "standard"; no "medium" tier is defined anywhere.
  • impact: cosmetic; the active length gate is unaffected. Risk is reader confusion that a separate "medium" tier exists.
  • disposition: accept as-is, or relabel "(standard post)".

Class: guided/stateful (content-emitting). Intent (58 slide deck + caption, optional image render, text-only degradation) delivered; all cross-refs resolve (differentiation-checker, templates, algorithm-signals, clipboard + state scripts, mcp-image params valid); hook bound :97 matches SSOT; no "thought leadership". Clean on all blocking dimensions — findings are polish.

MINOR — slide-count minimum stated two ways (6 vs 5)

  • carousel.md:41-45 — Step 1 offers all 5 templates as "(6-8 slides)"; carousel.md:114 — Step 5 gate checks "5-8 slides total (7 is optimal)". Minimum disagrees (6 vs 5).
  • Both lenses flagged this. Mirrors the upstream split in assets/templates/carousel-templates.md:11 ("5-8") vs per-template headers ("6-8").
  • impact: a 5-slide deck passes Step 5 but was never offered in Step 1. Cosmetic guidance, not a hard break.
  • disposition: align the floor (pick 5 or 6 across Step 1 + Step 5); ideally fix the source too.

MINOR — inlined Step 5 checklist drops an item vs the source it cites (7 vs 8)

  • carousel.md:109 says "Run against the Carousel Quality Checklist from carousel-templates.md", then inlines 7 items (:111-117, grep -c = 7). The cited source has 8 (carousel-templates.md:276-283); the dropped one is :283 "Exported as PDF, under 100 MB".
  • impact: the export/size check only resurfaces in Step 6's text-only branch (:184); when image generation succeeds, the PDF/100 MB constraint is never surfaced in the gate.
  • disposition: add the PDF/under-100 MB item to the Step 5 list, or stop claiming verbatim fidelity to the source.

MINOR — slide body scaffold provides 5 line-slots but the rule permits up to 7

  • carousel.md:73-77 models 5 BODY lines (4-5 optional); carousel.md:87 + carousel-templates.md:10 permit "5-7 lines".
  • Both lenses flagged this.
  • impact: a slide legitimately needing 6-7 lines has no scaffold slot; the literal template caps generation at 5.
  • disposition: extend the scaffold to 7 optional lines, or tighten the rule/template to "max 5".

MINOR — caption voice-guardian safety-net claim doesn't engage in this flow

  • carousel.md:123 — "(The voice-guardian hook scores the caption on save.)" The PreToolUse gate fires only on Write|Edit of LinkedIn content, but this command never Writes the caption — Step 6 pipes it to clipboard (:210-212), Step 7 mutates state via node -e (:222-231). No save → hook never scores the caption.
  • impact: overstates a backstop that doesn't fire here; could justify under-doing the in-command De-AI check (Step 5).
  • disposition: drop the parenthetical or qualify it ("only if you later save the caption to a file").
  • carousel.md:93-105, 121-125 — the caption is feed text on the same reach mechanics, but neither Step 4 nor the De-AI gate mentions the no-body-link rule.
  • impact: a caption with an inline link incurs the documented reach penalty with nothing in this surface catching it.
  • disposition: add "no links in the caption body (put links in first comment)" to the De-AI gate or Step 4.

SUGGESTION — orphan provenance comment for an unused capability

  • carousel.md:18<!-- MERMAID_CHART_RESOLUTION: UNTESTED -->. Mermaid is never referenced anywhere in the file (only mcp-image is used).
  • disposition: remove the vestigial marker (or move the note to a design doc).

SUGGESTION — dual slide-count framing (6-8 vs 5-8); locally-defined per-slide bounds (header "max 8 words" :70, body "max 50 chars" :73-77, no canonical SSOT — no overlap with post bounds, noted for completeness); buzzword list 8 words :123 faithful to CLAUDE.md Rule #4 while canonical hook flags 10 (gap lives between CLAUDE.md + hook, not in this file).


video.md — VERDICT: ALLOW (0 BLOCKER/MAJOR · 3 MINOR · 1 SUGGESTION)

Class: guided/stateful (8-step script build) + post-emitting sub-surface (the 200400 char caption). Intent (paced 30s/60s/90s/2min video script + on-video captions + thumbnail + post caption + first comment, delegated to video-scripter) delivered; video-scripter + differentiation-checker resolve; word-budget math internally consistent (30/60/90/120s × 2.5 wps = 75/150/225/300, :64-67); no "thought leadership".

MINOR — muted-watch statistic stated two ways

  • video.md:100 "~85% watch without sound" vs video.md:120 "~8085% watch muted" — same claim, two figures in one frozen file.
  • Both lenses flagged this. 85% sits inside 8085% so not a hard contradiction, but reads as unreviewed precision in a quality-gate checklist.
  • disposition: pick one figure (recommend "~8085%") in both places.

MINOR — post caption (200400) is a third length band vs canonical quick (150500)

  • video.md:121,158 specify "200-400 chars"; content-quality-gate.md:18 quick = 150500. 200400 is a narrower subset (no hard conflict) but a band not present in the SSOT.
  • impact: a reader can't tell from video.md whether 200400 is intentional or drift.
  • disposition: accept if intentional (captions deliberately shorter), but add a one-line note that 200400 is a deliberate sub-band of the 150500 quick range.

MINOR — caption has no mobile-cutoff / first-line hook discipline

  • video.md:122,129,156-158 — the caption (feed text, truncated at the same "...see more" cutoff as any post) gets a length band + De-AI + no-body-link + buzzword strip, but NO instruction to front-load value within the ~110140 cutoff. (:101 "first line reads on-screen" is the muted-autoplay test on the VIDEO's on-screen text, not the feed caption.)
  • impact: vs text-post parity (SSOT hook 110140) the caption's truncation is unguarded; a buried lede underperforms in-feed.
  • disposition: defensible to accept (video is primary content), or add "caption's first line should land value before the mobile cutoff".

SUGGESTION — clipboard apostrophe breakage → see ★ cross-cutting finding (video.md:175).


multiplatform.md — VERDICT: ALLOW (0 BLOCKER/MAJOR · 1 MINOR · 3 SUGGESTION)

Class: routing + guided/stateful (content-adaptation). post-emitting predicate does NOT apply — this command consumes a LinkedIn post and emits adaptations for OTHER platforms (Twitter/slides/ YouTube), so the LinkedIn quality-gate (hook 110140 / length band / no-body-link / topic→pillars) is out of scope; the buzzword check IS carried (:46-48, mirrors CLAUDE.md rule #4 exactly). Intent delivered (3 promised platforms = 3 AskUserQuestion options = 3 templates); routing resolves (/linkedin:newsletter at :6,:34,:36,:132commands/newsletter.md exists); no subagent refs; graceful degradation present (:27-29, forbids fabricating source); no "thought leadership".

MINOR — clipboard apostrophe breakage → see ★ cross-cutting finding (multiplatform.md:121)

  • Elevated exposure noted: Twitter threads + YouTube CTAs are contraction-heavy (don't/it's/I'll), so this surface is more exposed to the systemic bug than most.

SUGGESTION — Twitter "280 chars max" is locally-defined (no LinkedIn SSOT)

  • multiplatform.md:53 — the only numeric bound in the file; an X/Twitter limit, correct value, cannot diverge from the LinkedIn SSOT. Noted for completeness.

SUGGESTION — "write once, publish everywhere" tagline overstates the command

  • multiplatform.md:4-5 — the tagline implies publishing; the command only adapts + saves to drafts (:118) + copies to clipboard. No publish action exists.
  • disposition: accept, or soften to "adapt once, post everywhere" / "draft for every platform".

SUGGESTION — Step 1 always asks platform even when the trigger already names it

  • multiplatform.md:38-41 unconditionally invokes AskUserQuestion, yet triggers include "adapt for twitter"/"turn into thread" (:7) that already pin the platform — against the commands-section principle to minimize interactive steps / infer from context.
  • disposition: accept, or "skip if the platform is already evident from the user's request".

Gate decision — R2a COMPLETE (5 surfaces)

Surface Verdict BLOCKER MAJOR MINOR SUGGESTION
post REWORK 0 1 (+systemic) 1
react REWORK 0 (systemic) 3
carousel ALLOW 0 0 5 4
video ALLOW 0 0 3 1
multiplatform ALLOW 0 0 1 3
★ cross-cutting 0 1 (systemic, 10 files)

Batch verdict: REWORK — 2 of 5 surfaces (post, react), driven by 2 distinct MAJORs: (1) the systemic clipboard-printf corruption (10 content commands), and (2) post's personal-stories band contradicting its own Step 5 gate + the canonical SSOT. 0 BLOCKER anywhere. carousel / video / multiplatform are ALLOW (polish only).

Independence verdict: every WAVE-1 REWORK/MINOR was independently surfaced by both blind lenses (clipboard, personal-stories band, "full angle set below", slide-scaffold, slide-count, muted-stat) — the convergence signal that these are real defects, not single-reviewer framing.

Cold review finds; it changes no code. Each fix is its own operator-gated decision. Recommended first fix out of R2a: the systemic clipboard pattern (one change, 10 files, highest blast radius). Local-only (hardening-class), not pushed.