---
type: cold-review
batch: R2a
journey: "Create — atomic emitters"
scope: "FROZEN committed files vs HEAD 9567689 (no pending diff; post-hardening cold pass)"
method: "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"
surfaces: [post, react, carousel, video, multiplatform]
reviewers:
  - "intent-lens (conformance: intent delivery + cross-ref resolution + class predicates + terminology)"
  - "correctness-lens (internal consistency + bound-vs-canonical + checklist arithmetic + structure)"
status: "COMPLETE — all 5 surfaces reviewed (post, react, carousel, video, multiplatform)"
verdict: REWORK
counts: { BLOCKER: 0, MAJOR_systemic: 1, MAJOR_surface: 1, MINOR: 13, SUGGESTION: 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
(S2–S26, `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,000–1,400) contradicts the file's own Step 5 gate + canonical SSOT (1,200–1,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 150–500).
- **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,200–1,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)".

---

## carousel.md — VERDICT: ALLOW (0 BLOCKER/MAJOR · 5 MINOR · 4 SUGGESTION)

Class: guided/stateful (content-emitting). Intent (5–8 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").

### MINOR — no-external-link rule (Content Quality Rule #3) absent from caption guidance
- `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 200–400 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` "~80–85% watch muted" — same claim, two figures in one frozen file.
- **Both lenses flagged this.** 85% sits inside 80–85% so not a hard contradiction, but reads as unreviewed precision in a quality-gate checklist.
- **disposition:** pick one figure (recommend "~80–85%") in both places.

### MINOR — post caption (200–400) is a third length band vs canonical quick (150–500)
- `video.md:121,158` specify "200-400 chars"; `content-quality-gate.md:18` quick = 150–500. 200–400 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 200–400 is intentional or drift.
- **disposition:** accept if intentional (captions deliberately shorter), but add a one-line note that 200–400 is a deliberate sub-band of the 150–500 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 ~110–140 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 110–140) 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 110–140 / 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,:132` → `commands/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.
