linkedin-studio/docs/hardening/review-R2b.md
Kjell Tore Guttormsen 2b706609bd docs(linkedin-studio): cold-review R2b (Create orchestrators) — independent kald-review of 4 surfaces
Reproduces the S1/R2a non-fabricating method: 2 blind cold Opus lenses per
surface (intent + correctness), no cross-feed, every mechanical claim
tool-grounded. Surfaces: create, batch, pipeline, newsletter.

Verdict REWORK — 3 of 4 surfaces, each 1 MAJOR; 0 BLOCKER:
- batch: 3a/3b component scaffold (format-blind) contradicts the format-aware
  band gate
- pipeline: Step 2 scaffold (960-1640) cannot satisfy Step 3 total band
  (1200-1800)
- newsletter: resumption table omits the contract-gate phase (Step 4.5) ->
  breaks deterministic resume between Step 4 and Step 5
- create: ALLOW (1 MINOR, 8-option AskUserQuestion vs documented 2-4 cap)

Two systemic patterns surfaced (main-grounded, not single-reviewer):
- 5-component draft scaffold 960-1640 != 1200-1800 band, in exactly 3 files
  (post.md [R2a], batch.md, pipeline.md)
- bare reference paths vs ${CLAUDE_PLUGIN_ROOT}/ in batch + pipeline

Independence axis earned its keep: on newsletter the intent-lens asserted the
resumption table complete; the correctness-lens counted the gap; main confirmed
the correctness-lens (a divergence resolved by grounding, not just convergence).

Review finds; changes no code. Fixes are separate operator-gated decisions.
test-runner 138/0 unchanged. Hardening-class artifact.

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

21 KiB
Raw Permalink Blame History

type batch journey scope method surfaces reviewers status verdict counts
cold-review R2b Create — orchestrators & front-door FROZEN committed files at HEAD 5474df5 (clean tree; 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
create
batch
pipeline
newsletter
intent-lens (conformance: intent delivery + cross-ref resolution + class predicates + terminology)
correctness-lens (internal consistency + bound-vs-canonical + checklist arithmetic + structure)
COMPLETE — all 4 surfaces reviewed (create, batch, pipeline, newsletter) REWORK
BLOCKER MAJOR MINOR SUGGESTION systemic_patterns
0 3 10 6 2

Cold review — R2b (Create · orchestrators & front-door)

Independent post-hoc cold review of the 4 Create-journey orchestrators/front-door, on the FROZEN committed files (HEAD 5474df5). Mirrors the S1 review.md + R2a model (the cold-review method that did not fabricate): read-and-show before assert, every file:line tool-confirmed, reviewers carry no drafting-session context. The per-command interactive gate (log.md) already passed these; this pass adds the independent axis that gate never had.

Independence cross-check — two outcomes worth noting this batch:

  1. Convergence (the R2a pattern): both blind lenses independently surfaced the same real defect on several surfaces — batch's bare-vs-prefixed reference path, batch's 3a/3b component-band tension, newsletter's allowed-tools omission, create's 8-option AskUserQuestion.
  2. Divergence resolved by grounding (new this batch, the strongest argument FOR the two-lens method): on newsletter the intent-lens asserted the resumption table "maps every currentPhase to the correct resume step"; the correctness-lens counted the rows (17) against the canonical _doc.phases (18) and found the contract-gate row missing. Main re-grounded it independently (below) → the correctness-lens is right. The independence axis caught a real MAJOR that one lens had asserted away.

★ Cross-cutting finding #1 (systemic) — 5-component draft scaffold (9601,640) cannot satisfy the 1,2001,800 standard band it is gated against

  • Pattern (verified by main, independent of reviewers — grep blast radius): the identical 5-component "standard post" breakdown — Hook 110-140 + Context 200-300 + Insight 400-800 + Implication 200-300 + CTA 50-100 — appears in exactly 3 files: post.md:100-104, batch.md:88-92, pipeline.md:58-62. (grep -rln "Insight.*400" commands/ → those three only; quick/first-post/react carry the lone Hook 110-140 line, NOT the full scaffold, so are unaffected.)
  • Grounded arithmetic: component min-sum = 110+200+400+200+50 = 960 · max-sum = 140+300+800+300+100 = 1,640. Each file then gates the SAME post against the canonical standard band 1,2001,800 (content-quality-gate.md:17; post.md:128, batch.md:95, pipeline.md:77).
  • Mechanism (two-sided): (a) a draft built at the component minimums is 960 chars — 240 below the 1,200 floor enforced by the next step AND the live PreToolUse content-quality-gate hook; (b) the component maximums sum to 1,640 — 160 below the 1,800 ceiling, so the scaffold literally cannot reach the upper half of its own target band.
  • Why it matters: on the most common path (drafting a standard text post) the drafting recipe and the acceptance test are mutually incompatible, with no transition/connective slack to close the 960→1,200 gap. In batch the defect is sharper: the 3a scaffold is format-blind (always the 5-component standard layout) while the 3b gate is format-aware (batch.md:114 format: [text/carousel/video], rotation :76), so a quick-format post built from the scaffold (9601,640) also blows the quick band (150500). In pipeline the contradiction is between two adjacent steps (Step 2 draft → Step 3 scorecard).
  • Severity: recorded MAJOR in batch and pipeline (real adjacent-step / gate contradiction on every standard post). Reconciliation note for R2a: R2a recorded the same scaffold in post.md at MINOR ("component minimums sum below the 1,200 optimal floor") and flagged only the min-side. The batch/pipeline analysis shows it is a two-sided, gate-hitting contradiction, not merely loose guidance — the post.md instance is arguably under-rated and should be reconciled in the same fix.
  • Disposition (NOT fixed here — review finds, operator decides; 3-file change = own go): one consolidated fix across all 3 files — raise the component floors so min-sum ≥ 1,200 (e.g. widen Context/Insight) AND either lift the ceiling or accept ~1,640, AND scope the standard 5-component scaffold explicitly to standard-format posts where the command is format-aware (batch), adding per-format draft guidance for quick/carousel/video. Recommend treating this as the second consolidated fix out of the sweep, after the R2a clipboard fix.

★ Cross-cutting finding #2 (recurring) — bare relative reference paths vs ${CLAUDE_PLUGIN_ROOT}/

  • Pattern: several Read/reference paths are written bare (resolved against the runtime cwd) while the same file's appendix and most other sites prefix ${CLAUDE_PLUGIN_ROOT}/. batch.md:43 (references/content-angles.md) vs batch.md:206 (prefixed, same file) — flagged by both batch lenses. pipeline.md:31,55,70,75 bare vs pipeline.md:28,64,107 + appendix :204-209 prefixed — content-angles.md is bare at :55 but prefixed at :204.
  • Impact: the files exist (not dead refs), but a bare path fails the Read when cwd ≠ plugin root, on real paths run every invocation (angle-select, optimize). Self-recoverable via Glob, latent.
  • Disposition: normalize all bare reference/asset paths to ${CLAUDE_PLUGIN_ROOT}/. Cheap, mechanical; fold into the consolidated fix pass. (Worth a repo-wide grep for the same pattern in the other 25 commands during the eventual fix.)

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

Class: routing (pure delegating front-door). Both lenses confirm intent delivered: Step 0 context → Step 1 intent-ID → Step 2 route, with explicit "you do not draft here / do NOT inline the target's steps" (create.md:49-51,64-65) — delegation purity intact, single source of truth preserved. All 8 routed targets resolve (post/quick/react/carousel/video/multiplatform/batch/newsletter, :37-44 + :55-62, ls-confirmed); the three enumerations (description :7, menu :37-44, route table :55-62) are mutually consistent (8/8/8, same order); no subagent_type refs (correct — it routes to commands); no "thought leadership". Correctness-lens: 0 findings.

MINOR — Step 1 directs one AskUserQuestion carrying 8 options; documented support is 24

  • create.md:35-44 — "use AskUserQuestion" immediately followed by 8 numbered options (grep -cE "^[0-9]+\. \*\*" → 8). Grounded against the plugin-dev reference command-development/.../interactive-commands.md:469 ("2-4 options per question") + :906.
  • impact: on the PRIMARY interactive path (user names no format) the front-door instructs a single question with double the documented option range.
  • anti-fabrication caveat (carried from the reviewer, honestly): grounded = (a) 8 options instructed, (b) the documented 24 range. NOT grounded = whether the live AskUserQuestion runtime hard-rejects >4 vs silently truncates/degrades. If the runtime hard-rejects, this escalates to MAJOR/BLOCKER on the no-format-named path; if it only degrades, the picker is over-long. Worth a runtime check before the fix.
  • disposition: group the 8 intents into ≤4 options (e.g. Short-form / Reaction / Visual / Long-form & batch) with a drill-down, or split into two questions.

batch.md — VERDICT: REWORK (0 BLOCKER · 1 MAJOR · 3 MINOR · 2 SUGGESTION)

Class: guided/stateful + routing ("create a full week of content"). Intent delivered: the Step 0→5 flow traces the frontmatter promise; all cross-refs resolve (trend-spotter, content-planneragents/; /linkedin:calendarcommands/; queue-manager.mjs + ical-generator.mjs exports + the 8-arg queueAdd call/signature match; all 6 reference/asset paths + SKILL.md); graceful degradation present; no "thought leadership".

MAJOR — Step 3a component scaffold contradicts the Step 3b band gate → see ★ cross-cutting #1 (batch.md:88-92 vs :95)

Sharper here than elsewhere: 3a is format-blind (always the 5-component standard layout, 9601,640) while 3b is format-aware (:114 format:[text/carousel/video], rotation :76), so a quick-format post built from 3a also blows the quick band 150500. Both lenses flagged this (intent-lens MINOR, correctness-lens MAJOR — recorded at the higher severity: it hits a real gate on every standard post).

MINOR — bare reference path → see ★ cross-cutting #2 (batch.md:43 vs :206). Both lenses.

MINOR — weekly_goal cadence decoupled from the fixed "35 posts" headline

  • batch.md:5,65 fix the output at "35 posts"; batch.md:52 schedules against weekly_goal slot templates (2x/3x/4x/5x). At weekly_goal=2x, 35 posts against 2 weekly slots overflow into the next week (:53 "next available slot after today") — ~2.5 weeks of content under a "full week" label.
  • impact: non-breaking (scheduling rolls forward), but "full week" + "35" is internally inconsistent with the 2x cadence.
  • disposition: tie post count to weekly_goal, or note that overflow rolls into following weeks.

MINOR — orphan sub-step marker 5b with no 5a

  • batch.md:172 ### 5b. Generate Calendar File; grep -n "5a" batch.md → no match (exit 1). Step 3 has 3a/3b/3c/3d; Step 5 jumps straight to 5b.
  • disposition: renumber to 5a, or drop the letter.

SUGGESTION — weekly_goal default (3x) lives only in the referenced scheduling-strategy.md:15, never stated in batch.md. Optional one-line "default 3x" for self-evident degradation.

SUGGESTION — planned_date metadata never computed

  • batch.md:109 writes planned_date: YYYY-MM-DD into each draft header, but Step 2 only computes scheduled_date/scheduled_time (:54); planned_date is introduced nowhere upstream.
  • disposition: drop planned_date, or define where it is derived.

pipeline.md — VERDICT: REWORK (0 BLOCKER · 1 MAJOR · 4 MINOR · 2 SUGGESTION + clipboard pointer)

Class: post-emitting + guided/stateful + routing ("full end-to-end pipeline"). Intent delivered: Steps 08 map to every named stage. All post-emitting predicates present (hook 110140 :58,:76 · length band :77 · no-body-link :78 · buzzword check :79 · topic→expertise :49,:55,:80); all cross-refs + function signatures resolve (content-planner, trend-spotter; /linkedin:calendar, /linkedin:analyze; queueAdd 8-arg call/signature; writeState/updatePostTracking); no "thought leadership".

MAJOR — Step 2 component scaffold cannot satisfy the Step 3 total-length gate → see ★ cross-cutting #1 (pipeline.md:58-62 vs :77)

Adjacent-step contradiction: Step 2 partitions into 9601,640; the very next step's scorecard asserts "Total 1,2001,800". Correctness-lens, grounded arithmetic.

MINOR — inline buzzword checklist enumerates 8, canonical gate enumerates 10

  • pipeline.md:79 lists 8 terms (= CLAUDE.md rule 4); SSOT content-quality-gate.md:13 adds 'actionable insights' + 'best practices' = 10. A draft passing the inline list can still trip the Write hook. (Gate-vs-rule divergence, not unique to this file — also noted on carousel in R2a.)
  • disposition: align to the 10-term canonical list, or reference the gate instead of duplicating.

MINOR — bare reference paths → see ★ cross-cutting #2 (pipeline.md:31,55,70,75). Intent-lens.

MINOR — Step 4 deferred/queued path falls through into the immediate Publish steps

  • pipeline.md:97-101 offers "Schedule / Add to queue / Save as draft (no schedule)"; Steps 58 (:120 "15-20 min BEFORE posting", :135 Publish, :158 first-hour, :171 post-analysis) then run with no branch. A user who queued/deferred is marched through Pre-Engagement → Publish → Monitoring, contradicting the just-made defer decision.
  • disposition: add an early-exit after Step 4 for options 24 ("if scheduled/queued, end here; Steps 58 run at publish time").

MINOR — Step 7 inlines a static first-hour checklist instead of routing to the stateful surface

  • pipeline.md:158-169 inlines a 5-item plan; the dedicated /linkedin:firsthour delegates to engagement-coach, persists via recordFirstHourPlan, hands off to post-feedback-monitor — strictly richer (Step 8 already routes to /linkedin:analyze, so the inline first-hour is the inconsistent one).
  • disposition: route to /linkedin:firsthour.

SUGGESTION — over-provisioned allowed-tools: :13 declares WebFetch but no body step fetches (trend-spotter does its own). Drop unless a URL-ingest step is intended.

SUGGESTION — hardcoded Norwegian peak times: :92-95 bakes "European/Norwegian audience" peak windows into the body while scheduling-strategy.md (read at :107) is the SSOT for slots; conflicts with the domain/audience-general principle. Source from the reference/config.

(pointer) clipboard printf '%s' systemic bug — pipeline.md:139 confirmed present (the only R2b surface in the 10-content-command set). Folds into the R2a ★ cross-cutting clipboard finding; no new derivation.


newsletter.md — VERDICT: REWORK (0 BLOCKER · 1 MAJOR · 2 MINOR · 2 SUGGESTION)

Class: guided/stateful + routing + heavy subagent orchestration (long-form 18-phase pipeline, ~110 KB). Intent delivered: all 18 phases present, ordered, [GATE]/[OPERATØR]-marked. 18-phase count confirmed by both lenses (0,1,1.5,2,2.5,3a,3b,4,4.5,5,5.5,6,6.5,7,7.5,8,9,10; headline :25 matches body + template _doc.phases + build-status). All 7 longform agents (fact-checker, editorial-reviewer, persona-reviewer, voice-scrubber, content-reviewer, language-reviewer, fact-reviewer) present in agents/ AND invoked; gate sequence ordered before lock (:1570): skeleton 2.5 → spine 3a → fact-check 5 → editorial 5.5 → persona 6 → headless 6.5 → visual 7.5 → LOCK 8 → hook 9. All subagent_type carry the linkedin-studio: namespace (the 5 prefix-less grep hits are line-wraps). All ~25 cross-refs (agents, commands, scripts, configs, render, docs) resolve. Pivot heuristic, flag caps, step-label 5.5/6.5 consistency all clean. No "thought leadership".

MAJOR — deterministic resumption table omits the contract-gate phase (Step 4.5) → breaks resume between Step 4 and Step 5

  • Verified by main (independent re-grounding of a lens disagreement): the resumption table newsletter.md:209-228 has no contract-gate row (grep contract-gate over the table region → none). The canonical _doc.phases it claims to mirror (:230-231) does define it — config/edition-state.template.json lists "contract-gate — … (Step 4.5)" between consistency-quality (Step 4) and factcheck-sweep (Step 5). Step 4.5 actually writes it: newsletter.md:988 "Set currentPhase: "contract-gate"".
  • Two concrete breakages on the multi-session resume path (the file's core premise, :200-204):
    1. Gate skipped on resume. The rule (:203-204) is "run the step AFTER the recorded phase." Row :219 maps consistency-quality → Step 5 (Fact-check), but the step after Step 4 is Step 4.5 (contract-gate), not Step 5. A session aborting after Step 4 resumes past the deterministic contract-gate, never running it.
    2. Unrecognized phase on resume. A session aborting after Step 4.5 has currentPhase: "contract-gate", absent from the table → falls into the :232-234 fallback ("missing or unrecognized → do NOT guess … confirm with the operator"), defeating the deterministic-resumption guarantee the section is built on.
  • Note: the linear next-pointers are correct (:918 "next: contract-gate", :989 "next: fact-check"); only the resume table is short one row — the defect surfaces solely on abort/resume between Step 4 and Step 5.
  • Independence note: the intent-lens asserted this table "maps every currentPhase to the correct resume step"; the correctness-lens counted (17 rows vs 18 phases) and found the gap. Main confirmed the correctness-lens. Two-lens method earned its keep here.
  • disposition: insert a contract-gate → Step 5 — Fact-check sweep row, and repoint consistency-quality → Step 4.5 — Contract-gate.

MINOR — Step 1 says the brief is first persisted "in Step 2"; the rest of the file says Step 1.5

  • newsletter.md:303 "Record the resolved brief inline (you will persist it to edition-state in Step 2)" contradicts :287-289, :412-418, :494-496 (all: first durable write is the Step 1.5 checkpoint). Stale "Step 2" — almost certainly predates the Fix #2 Step 1.5 insertion; non-breaking (Step 1 only records inline either way).
  • disposition: change :303 "in Step 2" → "at the Step 1.5 checkpoint".

MINOR — allowed-tools omits SendUserFile (body-primary operator gate) + mcp__mcp-image__generate_image (default image route)

  • newsletter.md:11-19 declares Read, Glob, Grep, WebFetch, Bash, AskUserQuestion, Task, Write. The body names SendUserFile as the primary operator gate at Steps 5.5/6.5/7.5 (13 uses, e.g. :1138) and mcp-image as the default image route (:1471); neither is declared. Both lenses flagged this (intent-lens MINOR, correctness-lens SUGGESTION — recorded at the higher: the declared "default" path can't execute under the frontmatter as written). Every use guards with a fallback ("SendUserFile if available, else a markdown file:// link"), so it degrades gracefully → not load-bearing.
  • disposition: add SendUserFile (+ optionally mcp-image) to allowed-tools, or downgrade the body wording from "default/primary" to "if permitted".

SUGGESTION — undefined "LTL plugin" acronym: newsletter.md:36,725 ("the LTL plugin" / "the LTL rule"); repo-wide the bare phrase appears only here, no expansion; the plugin is canonically "LinkedIn Studio". (The env vars LTL_SERIES_ROOT/LTL_BRAND :48,154-156 ARE a legit convention consumed by render/build-*.mjs — not a defect.) Rename to "LinkedIn Studio plugin", or define once.

SUGGESTION — "leveraged" in doc prose (newsletter.md:1785, note-only): ordinary verb in the command's own explanatory prose, not generated post content; CLAUDE.md rule 4 targets generated posts. Not a real violation; optionally swap to "drew on / built on".


Gate decision — R2b COMPLETE (4 surfaces)

Surface Verdict BLOCKER MAJOR MINOR SUGGESTION
create ALLOW 0 0 1 0
batch REWORK 0 1 3 2
pipeline REWORK 0 1 4 2
newsletter REWORK 0 1 2 2
★ cross-cutting #1 (scaffold, 3 files) 0 (counted in batch + pipeline; spans post.md from R2a)
★ cross-cutting #2 (bare paths) 0 0 (counted in batch + pipeline)

Batch verdict: REWORK — 3 of 4 surfaces (batch, pipeline, newsletter), each with 1 MAJOR: (1) the systemic 5-component scaffold contradicting the 1,2001,800 band (batch + pipeline; spans post.md from R2a), and (2) newsletter's resumption table missing the contract-gate phase. 0 BLOCKER anywhere. create is ALLOW (one option-count MINOR with a runtime caveat).

Independence verdict: convergence on batch path-prefix / batch 3a-3b band / newsletter allowed-tools / create 8-option (both lenses) — plus one divergence resolved by main's grounding (newsletter resumption table: intent-lens asserted complete, correctness-lens counted the gap, main confirmed). Both the convergence and the resolved divergence are signals these are real defects, not single-reviewer framing.

Systemic findings now span R2a+R2b: clipboard printf (R2a, 10 files) · component scaffold (R2b, 3 files incl. post.md from R2a) · bare reference paths (R2b, 2 files, worth a repo-wide grep). Cold review finds; it changes no code. Each fix is its own operator-gated decision. Recommended consolidated-fix order out of the sweep so far: (1) clipboard printf [R2a, 10 files, highest blast radius], (2) component scaffold [3 files], (3) bare reference paths [grep-driven], then the per-surface items. Local-only (hardening-class), not pushed.