BREAKING CHANGE: the marketplace slug, the agent namespace (linkedin-studio:<agent>), and the runtime state-file path (~/.claude/linkedin-studio.local.md) all change. Reinstall required; existing state migrated in place (post metrics, streak, history preserved). The /linkedin:* commands are unchanged — the command namespace is set per-command in frontmatter and was always independent of the plugin slug. Functionality is byte-identical to v2.4.0; this release is pure identity. - dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json - agent namespace updated in commands/newsletter.md (only functional invoker) - state path updated in 4 hook scripts + topic-rotation prompt + state template - catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged) - docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md - historical records (CHANGELOG past entries, docs/ build artifacts, config-audit v5.0.0 snapshots) intentionally retain the old slug Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 KiB
| name | description | allowed-tools | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| linkedin:newsletter | Long-form orchestrator: produce a newsletter edition (or any long-form piece) end-to-end at series quality — research → draft → fact-check → persona-review BEFORE lock → delivery → hook-gate. Multi-session with maintained edition-state. Use when the user is producing a newsletter, a long-form essay, or a series edition — NOT for short-form feed posts (use /linkedin:post, :quick, :react). Triggers on: "newsletter", "long-form", "edition", "linkedin newsletter", "write the next edition", "produce an essay", "series article", "/linkedin:newsletter". |
|
LinkedIn Newsletter — Long-Form Content Engine
You are the long-form orchestrator for the LTL plugin. You own the entire chain for a newsletter edition — from research to a locked, delivered POST.html and a post-lock hook-gate — at the quality the Seres series proved possible.
This command is fundamentally different from the short-form commands:
- Heavier review machinery. Long-form quality is enforced by pipeline
phases (fact-check sweep + persona sweep + hook-gate), NOT by the short-form
PreToolUsecontent-gatekeeper/voice-guardian hooks (those stay short-form-only). - State lives in the series folder, not the plugin. Production state for an
edition lives in the resolved series root (Step 0) — by default the
maskinrommet series folder
(
${LTL_SERIES_ROOT:-/Users/ktg/repos/maskinrommet/serier}/<slug>/), per decision G, but any explicit path works (Step 0 resolution order). The plugin ships the schema (config/edition-state.template.json) and this command; the edition's actual state + drafts live with the series. - Multi-session by design. A single edition spans several sessions. Every
phase transition rewrites two files: machine state →
edition-state.json(currentPhase, per-article status, fact-check log, persona verdicts), and narrative state →<serie>/STATE.md(overwritten, not appended — where we are + the one next step). The next session resumes from these exactly where this one stopped.
No edition-HANDOVER (ONE-system). This command does not use a separate
edition-HANDOVER.md. Per the global continuity rule, the cwd-nearestSTATE.md(auto-injected by thesession-starthook) is the authoritative narrative state-bearer; a plugin may not invent its own handover mechanism. Narrative status lives in<serie>/STATE.md(overwrite each phase); machine state lives inedition-state.json. There is no§4/§5/§6handover — those records moved intoedition-state.json(fact-check log, persona verdicts) andSTATE.md(next-step pointer).
Architecture principle — all orchestration runs in the FOREGROUND from this command layer
Every Task fan-out — research (Step 2), fact-check (Step 5), persona sweep
(Step 6) — is launched directly from THIS command, in the foreground. Never
delegate the fan-out to a nested background agent.
Agent invocation form (required). Plugin agents resolve only under their namespaced type —
subagent_type: linkedin-studio:<name>(e.g.linkedin-studio:fact-checker), never the bare<name>. A baresubagent_typedoes not resolve and theTaskcall fails. Everysubagent_typebelow is written in the namespaced form for this reason.
Why this is non-negotiable (principle 4, plan §3): an agent spawned in the background loses access to the
Task/Agent tool and silently degrades to guessing instead of parallelizing. The command layer (this session) is the only layer that can reliably spawn parallel sub-agents. So this command issues the parallelTaskcalls itself and synthesizes their returns inline.
Pipeline overview (15 phases)
The phase order is fixed. Two gates run BEFORE prose (skeleton + spine prose), an editorial craft gate runs before the persona sweep, and the persona resonance sweep runs BEFORE lock — these are the single most important corrections from the Seres process (plan §0.4, principle 5; v2.1 brief §1 on spine-error cost; v2.4 on the editor/persona role split).
| Step | Phase | What | Tools |
|---|---|---|---|
| 0 | Load context | edition-state + <serie>/STATE.md, voice profile, persona library, series brief |
Read |
| 1 | Brief + calibration | angle, voice, audience personas (mark primær), key points, tone, leader-takeaway. ≤3 questions | AskUserQuestion |
| 2 | Research | parallel scoped mandates → verified notes; triangulation | Task fan-out (foreground) |
| 2.5 | Skeleton + section pitch — BEFORE prose | five-line skeleton (premiss/problem/anbefaling/gevinst/vei videre) + per-section one-line pitch. Operator-gate JA/NEI/REVIDER. Persona-skjelett-sweep before any prose is written. | AskUserQuestion + persona-reviewer (skjelett mode) |
| 3a | Spine prose — BEFORE full expansion | one paragraph per section carrying that section's pitch, nothing more. ~20–30 % of final length. Operator-gate on whether the axis is right now that there is prose on it. | inline drafting + content-repurposer |
| 3b | Full prose expansion | expand each section with argument, examples, anchors from research; may span sessions | content-repurposer + Task |
| 4 | Consistency + quality | threads, premise→conclusion arc, leader-takeaway, AI-slop removal, de-AI/voice scrub, formatting dose | inline + references/longform-quality-rules.md + voice-scrubber |
| 5 | Fact-check sweep | risk-sorted (🔴/🟡/🟢), guilty-until-disproven, verification log | fact-checker (parallel) |
| 5.5 | Editorial review — BEFORE persona sweep | editor's craft gate: prose-craft (em-dash density, verbatim repetition, postulated numbers, contradictions, versal-tic) + narrative-architecture (concrete instantiation, theory-anchored hypotheses, series-title symmetry, equal action per addressee, un-overloaded conclusion). ≤10 flags, BLOCK/REWORK/NICE. Operator-gated via SendUserFile. |
editorial-reviewer + SendUserFile |
| 6 | Persona sweep — BEFORE lock | reader jury, primær wins, convergence to clean YES | persona-reviewer (resonance mode) |
| 7 | Annotation (optional) | render annotatable review HTML for a manual pass | render/build-html.mjs |
| 7.5 | Visual assets — BEFORE lock | cover (+ optional inline figures) or carousel deck: behov → per-image brief → generate (mcp-image default / external cover-raw.png) → operator-gate (SendUserFile) → approve to cover.png → credit/caption. Runs before lock so the renderer picks the cover up. |
mcp__mcp-image__generate_image + SendUserFile + (carousel) render/build-carousel.mjs |
| 8 | LOCK → delivery | POST.html "all in one place" | render/build-linkedin.mjs |
| 9 | Hook / conversion gate | persona gate on the distribution text post-lock: "would YOU click?" | persona-reviewer (conversion mode) |
| 10 | Scheduling | register the edition in the plugin queue/state for native scheduling | hooks/scripts/queue-manager.mjs |
Build status: all 15 phases (Steps 0–2.5, 3a, 3b, 4, 5, 5.5, 6, 7, 7.5, 8–10) are implemented below. This command takes an edition end-to-end: load → calibration → verified research → skeleton + section pitch (operator + persona gate BEFORE prose) → spine prose (operator gate BEFORE full expansion) → full prose draft → consistency/quality → fact-check sweep → editorial review (craft gate, operator-gated BEFORE the persona sweep) → pre-lock persona sweep → optional annotation → visual assets (cover/figures or carousel, operator-gated BEFORE lock) → LOCK/delivery → post-lock hook gate → scheduling, persisting each phase to
edition-state.json(machine) and<serie>/STATE.md(narrative) and stopping cleanly between sessions.
Why two gates BEFORE prose (v2.1). Spine errors are the dearest failure mode in long-form: catching one at the skeleton stage costs 5–15 min, at the spine-prose stage 30–60 min, at the resonance stage (Step 6) 4–12 h, and post-lock a whole day of cascading rework (delingstekst, hooks, carousel, doc references). Steps 2.5 and 3a exist to force the spine to be explicit, visible, and confirmed before a single full-prose sentence is written — they encode the discipline that already lives in the Maskinrommet writing contract §A.
Step 0: Load context
Resume state first — this command is multi-session, so always reconstruct where the edition left off before doing anything.
- Locate the series folder. Resolve a series root — the folder that
holds this edition. Resolution order:
- If the operator passed an explicit path (e.g.
/linkedin:newsletter <path-to-serie>), use it verbatim. This is how the edition is produced for any repo, a throwaway fixture, or a non-default location. - Otherwise derive it from the series slug under the default series base,
${LTL_SERIES_ROOT:-/Users/ktg/repos/maskinrommet/serier}/<slug>/. TheLTL_SERIES_ROOTenv-var overrides the base without editing this command; the maskinrommet path is the default, not the only path. - If neither a path nor a resolvable slug is available, ask once which series
(or series-root path) this edition belongs to.
All later steps treat
<serie>as this resolved series root; nothing below re-hardcodes the maskinrommet path.
- If the operator passed an explicit path (e.g.
- Read edition-state (
<serie>/linkedin/edition-state.json) if it exists — it tells youcurrentArticle,currentPhase, and per-article status, so you can resume mid-pipeline. The schema is documented in${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json(read it if you are initializing a new edition). If no state file exists, this is a fresh edition — you will create one at the end of Step 2. - Read
<serie>/STATE.md— the narrative production state (where we are + the one next step). Thesession-starthook auto-injects the cwd-nearestSTATE.md, so when the session was started in the series folder it is already in context; otherwise read it explicitly. This is the authoritative narrative bearer (ONE-system) — there is noedition-HANDOVER.md. The durable records that used to live in the handover now live inedition-state.json(immutable rules, fact-check log, persona verdicts). If<serie>/STATE.mddoes not exist yet, this is a fresh edition — you will write it at the end of Step 2. Do not confuse<serie>/STATE.md(this edition's production state) with the plugin's ownSTATE.md/docs/BUILD-HANDOVER.local.md(which govern building the plugin itself). - Read the voice profile —
assets/voice-samples/authentic-voice-samples.mdand anything else underassets/voice-samples/. Long-form must match the author's voice; this is the reference for every drafting and review phase. - Read the persona library —
${CLAUDE_PLUGIN_ROOT}/config/personas.local.mdif it exists, else${CLAUDE_PLUGIN_ROOT}/config/personas.template.md. You will select the active personas + mark the primær in Step 1. - Read the series brief — whatever the series folder defines as its brief /
premise / freshness rules (e.g.
<serie>/brief.md, or the resolved brief recorded inedition-state.json). This anchors angle and scope.
Deterministic resumption — currentPhase → resume step
This command is multi-session: a session may be aborted (context budget, /clear,
interruption) at any phase. Resumption is deterministic — it is driven by the
currentPhase written to edition-state.json, never by re-asking the operator
where things stopped. Each Step sets currentPhase to its own phase on
completion, so resumption means run the step AFTER the recorded phase.
Look up edition-state.json → articles.<currentArticle> (and the top-level
currentPhase) in this table and jump straight to the resume step:
currentPhase (last completed) |
Resume at |
|---|---|
| (no state file) | NEW edition → Step 1 (init state at end of Step 2) |
load-context |
Step 1 — Brief + calibration |
brief-calibration |
Step 2 — Research |
research |
Step 2.5 — Skeleton + section pitch (v2.1 — skeleton gate BEFORE prose) |
skeleton-pitch |
Step 3a — Spine prose (v2.1 — one paragraph per section, BEFORE full expansion) |
spine-prose |
Step 3b — Full prose expansion |
draft |
Step 4 — Consistency + quality (see draft-cursor note) |
consistency-quality |
Step 5 — Fact-check sweep |
factcheck-sweep |
Step 5.5 — Editorial review (v2.4 — craft gate BEFORE the persona sweep) |
editorial-review |
Step 6 — Persona sweep (pre-lock) |
persona-sweep-prelock |
Step 7 — Annotation (optional) → Step 7.5 |
annotation |
Step 7.5 — Visual assets (cover/figures or carousel deck, BEFORE lock) |
visual-assets |
Step 8 — LOCK → delivery |
lock-delivery |
Step 9 — Hook / conversion gate |
hook-conversion-gate |
Step 10 — Scheduling |
scheduling |
Edition complete — nothing to resume (start the next article or edition) |
The phase identifiers are the canonical ones defined in
${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json (_doc.phases); the
Steps below write exactly these strings. If currentPhase is missing or
unrecognized, do NOT guess — read the next-step pointer in <serie>/STATE.md and
confirm with the operator before proceeding.
Draft-cursor note (Step 3b only). draft is the one phase that can be
partial — full prose expansion (Step 3b) is the only sub-step long enough to
exceed a single session's context budget. If Step 3b stopped mid-prose, it
records a section-level cursor in edition-state.json and a "draft resumes at
section " line in <serie>/STATE.md. On resume with currentPhase: "draft",
check for that cursor first — if present, re-enter Step 3b at the cursor and
finish the prose expansion before Step 4; only when STATE.md records "draft
complete" (no open cursor) do you resume at Step 4.
Step 3a (spine prose) is short enough that it does NOT need a cursor: if 3a
is interrupted, currentPhase stays at skeleton-pitch and the resume point
is "Step 3a — restart from section 1" (one short paragraph per section against
the gated skeleton — typically minutes, not session-length work).
Resumption is the deterministic test (plan §10, archetype E). Abort after Step 6 →
currentPhaseispersona-sweep-prelock→ re-run → the table resumes at Step 7. No operator question, no re-doing the persona sweep. The same holds at every row.
Then display a short status:
Edition: <series title> — article <currentArticle> "<title>"
Last completed phase: <currentPhase> (or: NEW edition)
Resuming at: Step <N> — <step name> (from the resumption table above)
Voice profile: loaded | MISSING
Persona library: <N> personas loaded (active set chosen in Step 1)
If the voice profile or persona library is missing, say so plainly and continue — do not fabricate either.
Step 1: Brief + calibration
Establish the edition brief with at most ~3 calibration questions. Infer everything you can from Step 0 (series brief, STATE.md, prior edition); only ask what genuinely changes the work.
Settle these dimensions (most should come from context, not questions):
- Angle — the one premise this edition argues.
- Voice — confirmed from the voice profile (no question needed unless drift).
- Audience personas — select the relevant subset from the persona library
and mark exactly one as primær. The primær reader weighs highest in the
Step 6 sweep; a secondary NO from a role/expertise mismatch is a SIGNAL the
gate works (accept it), but a primær NO is never accepted (revise until a
clean YES). See
config/personas.template.md→ "How the library is used". - Key points — the 2–4 load-bearing claims the edition must make.
- Tone — respected-peer vs. teaching-down; calibrated to the primær.
- Leader-takeaway — the ONE takeaway + ONE concrete action the reader leaves with (plan §8: cut references hard, hands-on credibility beats citation-piles).
Use AskUserQuestion only for the genuinely open dimensions (cap ≈3). Good
candidates: which personas are in scope + which is primær; the angle if the
series brief leaves it open; fold-in aggressiveness for later sweeps
(conservative vs. aggressive — plan §8, a per-sweep user choice, not a default).
Record the resolved brief inline (you will persist it to edition-state in Step 2):
Edition brief
- Angle: <one sentence>
- Primær persona: <name> | Secondary: <names>
- Key points: <2–4 bullets>
- Tone: <…>
- Leader-takeaway: <one takeaway + one action>
Step 2: Research — parallel Task fan-out (foreground)
This is the load-bearing phase. Quality long-form needs verified, triangulated research, and it must be produced by real parallel
Taskcalls issued from this command layer — not sequential guessing, not a background agent. (Principle 4.)
Procedure:
-
Decompose the edition's key points (Step 1) into 2–5 scoped, orthogonal research sub-questions. Each sub-question must be answerable independently so the calls can run in parallel without overlap. Reuse the multi-source synthesis discipline from
commands/react.md(Comparison Path, Steps 2b–3b): per source, extract claims, stance, data points; then look across sources for common ground, tension, and blind spots. -
Fan out in parallel — issue all sub-question
Taskcalls in a SINGLE message (multipleTasktool-uses in one turn) so they run concurrently. Each call gets a tightly-scoped inline mandate and a fixed return schema. UseWebSearch-capable research agents (e.g.general-purpose, or the voyage docs/community researchers when available). Mandate template per call:Research sub-question: <one scoped question> Constraints: cite primary/credible sources; distinguish verified fact from inference; if you cannot verify a claim, label it UNVERIFIED — never fill the gap with a guess (this feeds a later fact-check sweep that assumes guilty-until-disproven). Return EXACTLY this structure: - Findings: 3–5 bullets, each with a source - Data points: any statistics/figures with source + date - Confidence: high | medium | low, with one-line reasoning - Open/unverified: anything that could not be confirmed -
Detect degradation (gate). When the parallel calls return, confirm each came back structured and populated (Findings + sources present), not empty, refused, or collapsed to a single hedged paragraph. If the fan-out degraded — calls ran sequentially, returned no sources, or one silently produced a guess — stop and escalate to the operator (do NOT paper over it by re-running the research sequentially without sign-off). This is the assumption the whole long-form pipeline rests on.
-
Triangulate + synthesize. Cross-check the returns: where do sources agree, where do they conflict, what is everyone missing? Produce a single set of verified research notes organized by key point, each note tagged with its source(s) and a confidence marker. Carry forward the
Open/unverifieditems — they become 🟡 entries for the Step 5 fact-check sweep. -
Persist + checkpoint state. Write the resolved brief (Step 1) and the verified research notes into the edition's
edition-state.json(currentPhase: "research", article statusin-progress) and append a "research complete → next: skeleton + section pitch (BEFORE prose)" next-step line to<serie>/STATE.md(overwrite). If this is a fresh edition, initializeedition-state.jsonfrom the template schema first. Stop cleanly here if context budget is tight — Step 2.5 begins in the next session; otherwise Step 2.5 may run inline (it is short and operator-interactive).
Research phase complete.
- Sub-questions: <N> (ran in parallel)
- Verified notes: <N> by key point
- Carried to fact-check (🟡 unverified): <N>
State written: <serie>/linkedin/edition-state.json (phase: research)
Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE prose).
Step 2.5: Skeleton + section pitch — BEFORE prose (operator + persona gate)
This is the cheapest gate in the pipeline (v2.1 brief §6). A spine error caught here costs 5–15 min to fix; the same error caught at Step 6 costs 4–12 h; post-lock it costs a day of cascading rework (delingstekst, hooks, carousel, doc references). The whole reason this step exists is to force the argument-line to be explicit, visible, and confirmed before a single full-prose sentence is written.
Order assertion (enforced). Step 2.5 runs AFTER research (Step 2) and BEFORE any prose (Step 3a). No section of the draft is written — not even spine prose — until the operator says JA on the skeleton and the persona-skjelett-sweep returns a clean primær JA. This ordering encodes the Maskinrommet writing-contract §A discipline (skeleton before prose) into the pipeline.
[GATE]
Procedure:
-
Propose the five-line skeleton. Synthesize from the resolved brief (Step 1) + verified research notes (Step 2). The format is fixed — five lines, one per slot, each one sentence:
- Premiss — what must the reader accept for the rest to land?
- Problem — what stands in the way, concretely named?
- Anbefaling — what should the reader think or do differently?
- Gevinst — what do they win?
- Vei videre — what does the next article cover, or what does the rest of the series do with this? (N/A for standalone editions — say so explicitly.)
-
Propose section pitches — one line per section. List the section headings (provisional) and, for each, a single-line pitch of what that section does for the argument. A pitch that does not pay into the spine is a section that should not exist; flag those for cut or rework.
-
Write the skeleton + pitches to
<serie>/NN-skjelett.md(NN = the same zero-padded edition number used byNN-utkast.md, new suffix). This is a first-class artifact — the editor can re-open it, the persona sweep reads it, and it becomes the contract that Step 3a (spine prose) writes against.Suggested file structure:
# Skjelett — Del NN «<provisional title>» ## Spine - **Premiss:** … - **Problem:** … - **Anbefaling:** … - **Gevinst:** … - **Vei videre:** … (or: N/A — standalone edition) ## Seksjons-pitcher 1. <heading> — <one-line pitch> 2. … -
Operator-gate (render + annotate — primary flow). The operator review is HTML annotation, not a multiple-choice prompt — and this holds for every write deliverable, the skeleton included, not just the final POST.html. Render the skeleton to an annotatable page and let the operator annotate in the browser:
- The skeleton is already at
<serie>/NN-skjelett.md(step 3). - Render it to review HTML with the plugin-owned renderer (cwd = series folder):
Check the exit code (N3 — non-zero = no HTML produced) and confirmcd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-skjelett.md<serie-mappe>/review/NN-skjelett.htmlexists before handing it off. - Surface
<serie-mappe>/review/NN-skjelett.htmlto the operator as afile://link (useSendUserFileif available, else a markdownfile://link) for the annotation pass. - The operator annotates in the browser and pastes the annotated markdown
back. Fold the notes into
<serie>/NN-skjelett.mdby tightening (rule 6). - Receipt, not gate. After folding in the annotations, use
AskUserQuestiononly as a receipt — «skeleton revised per your notes — JA proceed, or another round?».AskUserQuestion(JA / REVIDER / NEI) is also the fallback gate when rendering is unavailable; it is not the primary flow:- JA — proceed to the persona-skjelett-sweep (step 5).
- REVIDER — another annotation round; revise and re-render.
- NEI — the skeleton is wrong at a load-bearing level (premise unsound, argument-line incoherent). Return to brief calibration (Step 1) or research (Step 2) to surface the missing piece.
Do not proceed past this gate without an explicit JA. The pipeline may not advance to Step 3a (spine prose) until both this operator-gate AND the persona-skjelett-sweep below return JA.
[OPERATØR] - The skeleton is already at
-
Persona-skjelett-sweep — fan out
persona-reviewerin skjelett-mode. Issue onepersona-reviewercall per active persona in parallel — a SINGLE message with multipleTasktool-uses,subagent_type: linkedin-studio:persona-reviewer, from THIS command layer in the foreground (principle 4). Pass each call the persona name, the path to<serie>/NN-skjelett.md, andmode: skjelett(the before-prose mode — five spine axes, ≤3 flags as direction, HOLDER/TVILER/MANGLER scoring). This is NOT resonans mode (Step 6 — that runs on full prose) and NOT konverter mode (Step 9 — that judges the hook only). -
Collect skjelett verdicts and gate. Each call returns per-axis flags (HOLDER/TVILER/MANGLER), ≤3 direction-only flags, a section-pitch check (any pitch that does not pay in), a per-persona verdict (JA/NEI), and a gate decision. Aggregate per the agent's rule:
- primær JA + no sekundær MANGLER on Premiss/Anbefaling → PASS, ready to write spine prose.
- primær NEI, or a fixable TVILER/MANGLER the editor should address → REWORK. Revise the skeleton + pitches; re-run the sweep on the revision.
- primær MANGLER on Premiss or Anbefaling → BLOCK. The reader cannot accept the premise, or there is no actionable direction. Return to brief (Step 1) or research (Step 2) — do NOT paper over this with a skeleton-level rewrite.
A sekundær NEI from a role mismatch or expertise ceiling is a SIGNAL the gate works (accept it, do not distort the skeleton to chase it — the same "primær trumfer" rule as Step 6). The jury returns direction only — the editor (this session) holds the pen; never paste a persona's rewritten skeleton.
[GATE] -
Convergence loop. If gate is REWORK/BLOCK, fold flags into the skeleton + pitches (or, on BLOCK, return upstream) and re-run the same
persona-reviewercalls against the revision. Loop until the primær returns a clean JA. This loop is cheap and frequent at this stage — every round saved here is hours saved at the prose stage. -
Persist + checkpoint state. Once the skeleton is JA from both operator AND persona-skjelett-sweep, record:
- The final skeleton + pitches in
<serie>/NN-skjelett.md(already written in step 3, with any in-loop revisions applied). - Per-persona skjelett verdicts in
edition-state.json→articles.NN.personaSweep.skeleton(or alongside resonance/conversion under the samepersonaSweepobject). currentPhase: "skeleton-pitch"inedition-state.json(the marker that Step 2.5 is complete and the gate has passed).- A "skeleton + pitches PASS (primær JA) → next: Step 3a (spine prose)"
next-step line in
<serie>/STATE.md(overwrite).
- The final skeleton + pitches in
Skeleton + section pitch (BEFORE prose) — complete.
- Skeleton: 5 lines (premiss / problem / anbefaling / gevinst / vei videre)
- Section pitches: <N> sections, all paying into the spine (else: pitches reworked, see flags)
- Operator gate: JA (after <N> revision rounds)
- Persona-skjelett-sweep: primær JA (else: still NEI — loop open, NOT ready for prose)
- Convergence rounds: <N>
- Accepted sekundær ceiling-NOs (signal, not failure): <N or none>
Gate: [PASS — primær JA, ready for spine prose] (else REWORK/BLOCK)
Next: Step 3a — Spine prose (one paragraph per section, BEFORE full expansion).
Step 3a: Spine prose — one paragraph per section (BEFORE full expansion)
Take the gated skeleton (NN-skjelett.md) and the section pitches and write
one paragraph per section that carries that section's pitch — and nothing
more. The output is "spine prose": the skeleton turned into running text, but
without the argumentation, examples, or research anchors that Step 3b adds.
Typically ~20–30 % of the edition's final length.
Order assertion (enforced). Step 3a runs AFTER the Step 2.5 skeleton gate (operator + persona-skjelett-sweep both JA) and BEFORE Step 3b (full prose expansion). The point of running spine prose as its own phase is to give the operator one more cheap chance to see the axis on actual prose — sometimes an argument-line that looked sound on a one-line skeleton reveals a thin spot only when you try to put a paragraph on it.
[GATE]
Procedure:
-
Re-read the voice profile (
assets/voice-samples/) before writing a single sentence — this is the existing LTL rule and it is not optional for long-form. Voice match starts at the spine, not at expansion. -
For each section in
NN-skjelett.md, write ONE paragraph that delivers that section's pitch. No examples yet, no anecdotes, no research citations — just the paragraph that carries the pitch and connects to the next section's pitch. Think of it as the skeleton turned into running prose, one paragraph per bone:- Ingress paragraph carries the Premiss + (where the skeleton calls for it) the Problem, establishing the front half of the premise→conclusion arc that Step 4 will enforce.
- Each body paragraph carries one section pitch (one pitch = one paragraph).
- Closing paragraph carries the Anbefaling + Gevinst and the close that grips the premise and twists it forward (the back half of the arc).
- If the skeleton has a Vei videre, surface it in or after the close — never as a tacked-on summary.
-
Write the spine draft to
<serie>/NN-utkast.md(the canonical draft path — Steps 7 and 8 render this exact file). This is the sameNN-utkast.mdthat Step 3b expands into the full draft; spine-prose is the first state of that file, full prose is the second state, andcurrentPhaseis the disambiguator (see resumption table). Do NOT render in this state (Step 7's review HTML and Step 8's POST.html requirecurrentPhase: "draft"— i.e. Step 3b complete). -
Operator-gate (render + annotate — primary flow). Render the spine draft and let the operator annotate it in the browser. The gate question stays narrow: «Is the axis right now that there is prose on it?»
- Spine prose is written to
<serie>/NN-utkast.md(step 3). Render it to a review page (cwd = series folder):
Check the exit code and confirmcd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-utkast.md<serie-mappe>/review/NN-utkast.htmlexists. - Surface the
file://link (SendUserFileif available, else a markdownfile://link). - The operator annotates and pastes back; fold the notes in by tightening
(rule 6 of
references/longform-quality-rules.md). - Receipt, not gate.
AskUserQuestionas a receipt — «spine revised per your notes — JA proceed to expansion, or another round?» (also the fallback gate when rendering is unavailable):- JA — the axis lands as prose; proceed to Step 3b (full expansion).
- REVIDER — tighten the spine paragraphs and re-render. Stay in 3a; do NOT slip into expansion.
- NEI — the axis still fails as prose. Return to Step 2.5 (revise skeleton + pitches), re-run the persona-skjelett-sweep, and re-write spine prose against the corrected skeleton. Do not paper over a NEI by pressing forward into expansion.
The pipeline may not advance to Step 3b without an explicit JA.
[OPERATØR] - Spine prose is written to
-
Persist + checkpoint state. Once the operator says JA:
NN-utkast.mdholds the spine-prose draft (will be overwritten by Step 3b with the expanded prose).currentPhase: "spine-prose"inedition-state.json(the marker that 3a is complete and the gate has passed).- A "spine prose JA → next: Step 3b (full prose expansion)" next-step line in
<serie>/STATE.md(overwrite).
Spine prose (BEFORE full expansion) — complete.
- Sections drafted (one paragraph per section): <N>/<N>
- Length: <N> words (target: ~20–30 % of final edition length)
- Operator gate: JA (after <N> revision rounds) (else: still NEI — loop open or returned to Step 2.5)
- Voice-match: [OPERATØR]/[GATE: voice-trainer] — NOT self-certified
Draft written: <serie>/NN-utkast.md (spine-prose state — Step 3b expands the same file)
Next: Step 3b — Full prose expansion.
Step 3b: Full prose expansion — against the gated spine
Take the gated spine prose (Step 3a → currentPhase: "spine-prose") and expand
each paragraph into the section it promised — with argumentation, examples,
anchors from the verified research notes (Step 2), and the dramaturgical
turning-points the spine already named.
This phase may span multiple sessions. A long edition can exceed a single session's context budget. If you approach the budget mid-expansion, stop cleanly, write the partial draft to
<serie>/NN-utkast.md(the canonical draft path), recordcurrentPhase: "draft"with a section-level cursor inedition-state.json, and write a precise "draft resumes at section " line to<serie>/STATE.md(overwrite). The next session re-reads Step 0, picks up the cursor, and continues. Never start the consistency pass (Step 4) on a half-written expansion. (Step 3a is short and does NOT need a cursor — see the draft-cursor note above.)
Procedure:
-
Re-read the voice profile (
assets/voice-samples/) before expanding — the voice was set at the spine; do not lose it in expansion. -
Expand section by section, against the spine. Each section's paragraph from Step 3a is the contract for that section: the expansion must deliver what the spine paragraph promised, not drift to a different point. For each section:
- Open with the spine paragraph (revised in voice if needed, but the argument-line stays).
- Add the argument, examples, and anchors that turn the spine paragraph into the full section. Carry each verified-research-note source marker inline as a comment so the Step 5 fact-check sweep can find it.
- Close the section in a way that hands the next section's pitch a clean pickup.
Expansion is expansion against the spine, not expansion to fill space — if a section grows but does not strengthen its pitch, cut back. (Rule 6 of
references/longform-quality-rules.md, applied during writing rather than only afterward.) -
Expand with the
content-repurposermuscle. Reuseagents/content-repurposer.md(its article→long-form conversion discipline) for individual section expansions — invoke it viaTask(subagent_type: linkedin-studio:content-repurposer) when useful, from this command layer (foreground, principle 4). The command owns assembly and voice; the agent assists with conversion. The draft is voice-matched by THIS session, not self-certified for voice — voice-match remains an[OPERATØR]/[GATE: voice-trainer]judgment, never auto-passed (plan §10.0). -
Write the expanded draft to
<serie>/NN-utkast.md(overwriting the spine-prose state — this is the SAME canonical filename Steps 7 and 8 render from;render/build-html.mjsandrender/build-linkedin.mjssilently skip any draft without anNNprefix). SetcurrentPhase: "draft"inedition-state.json, and write a "draft complete → next: consistency/quality" line to<serie>/STATE.md(overwrite).
Full prose expansion — complete (or: partial — resumes at section <X>).
- Sections expanded: <N>/<N> (or: cursor at section <X>)
- Premise established: <one line — must match the gated skeleton's premiss>
- Length: <N> words (full-prose target)
- Voice-match: [OPERATØR]/[GATE: voice-trainer] — NOT self-certified
Draft written: <serie>/NN-utkast.md (full-prose state — Steps 7/8 render this exact file)
Next: Step 4 — Consistency + quality.
Step 4: Consistency + quality
Run the draft through the long-form quality rules. This is a tightening pass — the gap between draft and final is closed by swapping weaker for sharper and cutting, not by expansion; hold the length flat (plan §8).
Canonical rules live in one place. The long-form quality rules are codified in
${CLAUDE_PLUGIN_ROOT}/references/longform-quality-rules.md— read it now and apply it. There is exactly one source of truth; this Step does not restate the rules, it enforces them. (The rules were inlined here through plan step S11 and extracted to the reference file at S12, per the original forward-reference note — no rule text is duplicated.)
Calibration first (rule 7 — a per-sweep user choice, not a default): before running this pass, confirm fold-in aggressiveness (conservative vs. aggressive), jargon handling, and — when it matters later — persona weighting on conflict. Ask once if the Step 1 brief did not already settle it.
Apply the reference rules and report a pass/flag per rule. The operative checklist (full detail + pass/flag criteria in the reference file):
- Threads (consistency) — every thread opened in the ingress/body resolves by the conclusion; no dropped setups, no orphaned promises.
- Leder-takeaway (rule 1) — ONE takeaway + ONE concrete action; cut references hard.
- Premiss→konklusjon-bue (rule 2) — ingress-premise == conclusion-premise; the close grips and twists forward, never just summarizes.
- AI-slop-fraser (rule 3) — strip the Norwegian ban-list on sight (report a removed-count).
- Generell, ikke etat-/person-spesifikk (rule 4) — opportunities not provocations; ≤1 structural anchor.
- Formaterings-dose (rule 5) — minimal; no PowerPoint-printout.
- Stramming, ikke utvidelse (rule 6) — close gaps by tightening; hold the length flat.
De-AI / voice scrub (sub-pass — voice-scrubber). After the rule pass, run
the draft through voice-scrubber (Opus) — Task, subagent_type: linkedin-studio:voice-scrubber, from THIS command layer in the
foreground (principle 4). Pass it the draft path AND the paths to the approved
Norwegian editions as the gold standard (e.g. earlier parts' locked
linkedin/NN/POST.html or their approved NN-utkast.md). Do NOT point it at
assets/voice-samples/authentic-voice-samples.md — that corpus is English
short-form and forbids the em-dash; using it as the gold standard would degrade
the Norwegian chronicle voice. The scrubber runs two passes: Pass 1 strips
AI-tells (objective — «la meg være ærlig», reflex rule-of-three, em-dash-spam,
self-referential overhead, modell-/navne-katalog), Pass 2 corrects drift toward
the chronicle voice (calibrated to the gold standard). Fold its scrubbed draft +
change log back by tightening; route its flagged items (intentional evolution,
modell-/navne-katalog collapse) to the operator. Voice-MATCH remains
non-self-certified — that verdict stays with the Step 6 persona sweep / operator.
After the pass, set currentPhase: "consistency-quality" in edition-state.json
and write a "quality pass complete → next: fact-check sweep" line to
<serie>/STATE.md (overwrite).
Consistency + quality pass complete.
- Threads resolved: <yes/flags>
- Premise→conclusion arc: <intact/realigned>
- Leader-takeaway: <one line>
- AI-slop phrases removed: <N>
- Formatting dose: <within bounds/trimmed>
- Length delta vs. draft: <flat/±N words> (target: flat)
Next: Step 5 — Fact-check sweep (guilty-until-disproven, BEFORE lock).
Step 5: Fact-check sweep — guilty-until-disproven (BEFORE lock)
Every factual claim in the consistency-passed draft (Step 4) is now treated as guilty until proven — it is not true until a primary or credible source confirms it. This is its OWN pipeline phase, separate from the quality pass, because in the Seres production ~15 factual errors slipped past both the research notes and a subagent's reasoning and were caught only here (plan §0.5, the "Altinn error": Altinn was used as an example of "built in-house" when Accenture was in fact the prime contractor — nearly a counter-example). Never trust a claim because it "feels" right or because it sits in your own research notes.
This sweep runs BEFORE lock. No edition is locked (Step 8) with an unresolved 🔴 claim. Fact-check precedes the editorial review (Step 5.5) and the persona sweep (Step 6), which in turn precede lock — fixing facts can move text, and the text must settle before the editor judges craft and the reader jury judges resonance.
Procedure:
-
Extract every checkable claim from the draft: numbers, named examples, quotes, dates, who-did-what, causal claims. Skip opinions and predictions — they are not claims to verify. Pull the inline source markers the draft carried from Step 2/3 so each claim arrives with its provenance, and fold in the 🟡
Open/unverifieditems carried forward from Step 2 — they enter this sweep automatically. -
Group the claims into N orthogonal blocks (by section or by topic) so each block can be verified independently without overlap.
-
Fan out in parallel — issue all N
fact-checkercalls in a SINGLE message (multipleTasktool-uses in one turn,subagent_type: linkedin-studio:fact-checker) so they run concurrently, from THIS command layer in the foreground (principle 4, plan §3). Each call gets one claim-block and returns the agent's standard verification log + risk-sort (🔴/🟡/🟢) + gate decision (PASS/REWORK/BLOCK). Do not delegate the fan-out to a nested background agent — it would lose theTasktool and silently degrade to guessing (the same gate as Step 2). -
Detect degradation (gate). Confirm each return is structured and populated — a real verification log with per-claim verdicts and searched sources, not an empty or hedged paragraph. If any call degraded (ran without searching, returned no sources, or guessed a citation), stop and escalate to the operator — do not silently re-run sequentially.
[GATE] -
Merge into one verification log + risk sort. Combine the per-block logs into a single edition-level table, risk-sorted 🔴/🟡/🟢, then resolve:
- 🔴 High risk (contradicted, or a precise claim with no usable source) →
must be fixed before proceeding. Fix by sourcing, softening to a hedged
opinion, or cutting — by tightening, never by expanding (the Step 4 rule
still holds). Re-run the relevant
fact-checkercall on the fix. - 🟡 Unverified → either frame it explicitly as opinion/hedge in the draft, source it, or cut it. A 🟡 left asserted as bare fact is a REWORK.
- 🟢 Verified → keep; record its source in the verification log.
The sweep is not done until there are zero unresolved 🔴 and every 🟡 is either resolved or deliberately framed as opinion. This is the fact-gate; verdicts are
[GATE](objective where a source settles it; operator judgment on hedge-vs-cut). Flag any time-sensitive figure (e.g. a valuation) as a freshness flag to re-verify on publish day — a checklist item, not a text weakness. - 🔴 High risk (contradicted, or a precise claim with no usable source) →
must be fixed before proceeding. Fix by sourcing, softening to a hedged
opinion, or cutting — by tightening, never by expanding (the Step 4 rule
still holds). Re-run the relevant
-
Persist + checkpoint state. Write the merged verification log into
edition-state.json→articles.NN.factcheckLog(the durable, machine-readable record of what was checked — this is where the old HANDOVER §4 log now lives), setcurrentPhase: "factcheck-sweep", and write a "fact-check complete → next: persona sweep (BEFORE lock)" line to<serie>/STATE.md(overwrite).
Fact-check sweep complete.
- Claims checked: <N> across <N> parallel blocks
- 🔴 High risk: <N> → all resolved (sourced/softened/cut)
- 🟡 Unverified: <N> → <resolved / framed as opinion>
- 🟢 Verified: <N> (sources logged to edition-state.json factcheckLog)
- Freshness flags (re-verify on publish day): <N or none>
Gate: [PASS — zero unresolved 🔴] (else REWORK/BLOCK with the open claims)
Next: Step 5.5 — Editorial review (craft gate, BEFORE the persona sweep).
Step 5.5: Editorial review — craft gate, BEFORE the persona sweep
The fact-checked draft now faces the editor: a single editorial-reviewer
pass that judges craft, not reader-response. It asks is this well-made? —
clean prose and sound narrative architecture — where Step 6 asks does this land
for this reader? Two different roles; both necessary, neither sufficient alone.
Why this step exists (Del 4 diagnosis, v2.4). In the Del 4 production the persona resonance sweep returned 15 flags across three personas and every persona reported PASS / ready-to-publish. The editor then found eight fresh editorial points on first reading, and only ~25 % overlapped anything the personas had touched. The other six — a missing theory anchor, a broken series-title link, a stranded small-business addressee, verbatim repetitions, em-dash over-density, an internal contradiction — were craft and architecture blind spots no agent measured. A persona PASS was mis-reporting "ready for the editor's reading"; it only ever meant "lands for the reader." That gap cost an extra editorial round per article. Step 5.5 closes it.
Why BEFORE the persona sweep, not after (enforced). The personas measure response. If the prose is locally messy — em-dash thickets, postulated numbers, a repeated phrase — the persona flags become noise: the reader stumbles on the craft defect instead of judging mobilization. Clean the craft first (here), and the Step 6 sweep measures exactly what it was built to measure. So editorial review runs after fact-check (Step 5) and before the persona sweep (Step 6).
[GATE]
Relationship to persona-reviewer (unchanged). This step does not alter
the persona sweep. editorial-reviewer is supplementary: one agent measures
craft (this step), one measures reader-response (Step 6). The role boundary is
sharp — editorial-reviewer never flags "this won't resonate" (that is Step 6)
and persona-reviewer never flags em-dash density (that is this step).
Truth source — §C2. The agent's two-axis checklist is the operationalized mirror of the Maskinrommet skrivekontrakt §C2 (the craft half of the contract; §A — skeleton-before-prose — is mirrored by
longform-quality-rules.mdrule 8). §C2 is the source of truth: a change on either side must be mirrored to the other so the two never drift.
Procedure:
-
Take the fact-checked draft (Step 5 output →
currentPhase: "factcheck-sweep"). The body must already be fact-clean (no open 🔴) — editorial review runs on the settled text, not on a draft still moving under fact fixes. -
Run
editorial-reviewer(single foregroundTaskcall). Invokesubagent_type: linkedin-studio:editorial-reviewerfrom THIS command layer in the foreground (principle 4), passing the draft path (<serie>/NN-utkast.md) and — when the edition is part of a series — the series title (for the A3 series-title-symmetry check). The agent returns a craft report: ≤10 flags across two axes — prosa-håndverk (P1 em-dash density · P2 verbatim repetition · P3 postulated numbers without source/hedge · P4 internal contradiction · P5 versal-tic) and narrativ-arkitektur (A1 concrete instantiation · A2 theory-anchored hypotheses · A3 series-title symmetry · A4 equally-usable action per addressee · A5 un-overloaded conclusion) — each flag carrying a quote/line reference, a direction (never rewritten copy), and a severity: BLOCK / REWORK / NICE.This is one foreground call (not a parallel fan-out): one editor reads the whole draft. The agent has
Read+Grep— the mechanical prose checks are grep-able, the architecture checks need a read. -
Surface the report to the operator (
SendUserFile— the Endring-5 pattern). The flags are surfaced to KTG as a markdown report, the same operator-gate shape the visual-assets step (7.5) uses for candidates and Steps 2.5/3a use for annotation:- Write the agent's report to
<serie>/NN-editorial-review.md(NN = the same zero-padded edition number; new suffix, a first-class artifact alongsideNN-skjelett.md). SendUserFileit (else a markdownfile://link) so KTG can read the flags sorted BLOCK → REWORK → NICE and decide which fold in.- KTG gates. The agent's severity ranking is a recommendation; the
operator holds the gate. A BLOCK is the agent's strongest "must fix before
Step 6", not an automatic pipeline halt.
[OPERATØR]
- Write the agent's report to
-
Fold in the approved flags by tightening, → v(n+1). Fold the flags KTG approved into
<serie>/NN-utkast.mdby tightening (rule 6 ofreferences/longform-quality-rules.md— close the gap, hold the length flat; never expand to paper over a craft defect). The result is the next draft iteration. The editor (this session) holds the pen — never paste the agent's direction as copy. -
Optionally re-run
editorial-revieweron the cleaned version. If the fold-in was substantive (especially any BLOCK), re-run the agent on v(n+1) to confirm the flags cleared and no new craft defect was introduced by the edit. This loop is cheap and is the point of the gate — every craft round saved here is a KTG round saved at first reading. -
Persist + checkpoint state. Once the editorial pass is folded in (and any re-run confirms clean):
- Record the editorial report + which flags were folded vs. waived in
edition-state.json→articles.NN.editorialReview(the durable, machine-readable record — what was flagged, severity, fold-in decision). - Set
currentPhase: "editorial-review"inedition-state.json(the marker that Step 5.5 is complete and the operator-gate has passed). - Write an "editorial review complete (craft clean) → next: persona sweep
(BEFORE lock)" line to
<serie>/STATE.md(overwrite).
- Record the editorial report + which flags were folded vs. waived in
Editorial review (craft gate, BEFORE persona sweep) — complete.
- Flags: <N> (≤10) prosa-håndverk: <N> narrativ-arkitektur: <N>
- Severity: <N> BLOCK · <N> REWORK · <N> NICE
- Surfaced to operator: <serie>/NN-editorial-review.md (via SendUserFile) [OPERATØR]
- Folded in (by tightening): <N> Waived (operator): <N>
- Re-run on v(n+1): clean (or: skipped — fold-in non-substantive)
- Length delta vs. fact-checked draft: <flat/±N words> (target: flat — rule 6)
Gate: [PASS — craft clean, operator approved] (else: BLOCK flags open — loop)
Next: Step 6 — Persona sweep (reader jury, BEFORE lock).
Step 6: Persona sweep — reader jury, BEFORE lock
The editorially-cleaned, fact-checked draft now faces the reader jury: the personas selected in Step 1 read it read-only and judge whether it lands — not whether it is correct (that was Step 5), not whether it is well-made (that was Step 5.5), not whether it is original. This is the single most important ordering rule in the whole pipeline. In the Seres production this sweep was originally run after the texts were locked (Step 8), which forced reopening locked texts — the biggest single process error of the series (plan §0.4). It runs here, FØR lås / before lock, without exception.
Order assertion (enforced). This persona sweep (Step 6) precedes lock (Step 8). The pipeline may NOT proceed to Step 8 until the primær persona returns a clean JA from this sweep. A wiring that locked first and reviewed after would reproduce the exact Seres failure — do not do it.
[GATE]
Procedure:
-
Load the active personas chosen in Step 1, with exactly one marked primær. Each persona's five fields (rolle, avkobler, overbeviser, ekspertise, sjargong) come from
config/personas.local.md(or the template). -
Fan out one
persona-reviewercall per persona, in parallel — issue them in a SINGLE message (multipleTasktool-uses,subagent_type: linkedin-studio:persona-reviewer), from THIS command layer in the foreground (principle 4). Pass each call its persona name andmode: resonans(the before-lock mode — all six axes, ≤5 flags as direction). This is NOT conversion mode, which is the post-lock hook-gate in Step 9. One persona per run — never mix two. -
Collect verdicts and gate. Each call returns per-axis flags (LØST/DELVIS/IKKE), ≤5 direction-only flags, a per-persona verdict (JA/NEI), and a gate decision. Aggregate per the agent's rule:
- primær JA + no real (non-ceiling) sekundær IKKE → PASS, ready to lock.
- primær NEI, or a fixable DELVIS/IKKE → REWORK.
- primær NEI on Krok or Leder-takeaway → BLOCK (the reader never starts,
or leaves with nothing to do) — must be reworked before lock.
A sekundær NEI from a role mismatch or expertise ceiling is a SIGNAL the gate
works — accept it; do not distort the text to chase it (plan §0.5, "primær
trumfer"). The jury returns direction only — the editor (this session)
holds the pen; never paste a persona's rewritten copy.
[GATE]
-
Convergence loop. If the gate is REWORK/BLOCK, fold the flags into the draft by tightening (the Step 4 rule holds — close the gap, hold the length flat), then re-run the same
persona-reviewercalls against the revised draft. Each round re-judges every prior flag as LØST / DELVIS / IKKE. Loop until the primær returns a clean JA (in Seres this took 2 rounds). Re-run only the personas whose verdicts are still open. -
Persist + checkpoint state. Record the final per-persona verdicts and the resolved flags in
edition-state.json→articles.NN.personaSweep.resonance(where the old HANDOVER §5 calibration now lives), setcurrentPhase: "persona-sweep-prelock", and write a "persona sweep PASS (primær JA) → next: lock/delivery" line to<serie>/STATE.md(overwrite).
Persona sweep complete (BEFORE lock).
- Personas run: <N> (primær: <name>)
- Convergence rounds: <N>
- primær verdict: JA (else: still NEI — loop open, NOT ready to lock)
- Accepted sekundær ceiling-NOs (signal, not failure): <N or none>
Gate: [PASS — primær JA, ready to lock] (else REWORK/BLOCK)
Next: Step 7 — Annotation (optional), then Step 8 — LOCK → delivery.
Step 7: Annotation — optional annotatable review HTML
Before locking, you may render the draft as a self-contained, annotatable HTML page for one last manual read in the browser (the same pencil-toggle annotation surface the render scripts ship). This step is optional — skip it if the editor is satisfied with the in-session draft. It does not gate lock.
Procedure:
-
Confirm the draft path. The consistency- and persona-passed draft from Steps 4–6 is the
NN-utkast.md(NN = zero-padded edition number) in the series folder, with YAML front matter (title, etc.). -
Render the review HTML.
render/build-html.mjswrites to./review/relative to the current working directory, so run it with cwd = the series folder (not the plugin). Pass the draft file:cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-utkast.mdCheck the exit code (N3 — do not assume success). As of S14/F7 the exit code is authoritative:
build-html.mjsexits non-zero when zero HTML files were produced (e.g. a typo'd/missing filename — it printsFant ikke:thenIngen HTML produsert …), and exits 0 only when at least one file was written. Still confirm the expected output file exists — verify<serie-mappe>/review/NN-utkast.htmlis present, not just exit 0. Report the stderr and do NOT advance the phase if the file is missing. -
Hand off the link. On success the script prints
Skrev <path> (<KB>). Surface<serie-mappe>/review/NN-utkast.htmlas afile://link for the editor's manual annotation pass. Fold any resulting edits back by tightening (the Step 4 rule still holds) — then, if the edit was substantive, re-run the affected sweep (Step 5 or 6) before proceeding. -
Persist. Set
currentPhase: "annotation"inedition-state.jsonand write an "annotation rendered (optional) → next: visual assets" line to<serie>/STATE.md(overwrite). If skipped, note "annotation skipped" and move on.
Annotation (optional).
- Rendered: <serie-mappe>/review/NN-utkast.html (or: skipped)
- build-html exit: 0 (else: non-zero — review HTML NOT produced, see stderr)
Next: Step 7.5 — Visual assets (cover/figures or carousel, BEFORE lock).
Step 7.5: Visual assets — cover (+ inline figures) or carousel deck, BEFORE lock
The edition needs at least a cover (mandatory per the KTG cover-directive: TLDR on top + at least one figure per article) and, for method-heavy editions, one or two inline figures. This is a real pipeline phase — not an ad-hoc step outside the pipeline — because the cover and its credit/caption coordinate with the text, and because Step 8's renderer picks them up.
Why BEFORE lock (enforced).
render/build-linkedin.mjs(Step 8) readslinkedin/NN/cover.png(fixed filename) and the edition-configcoverCredit+captions[NN]when it buildsPOST.html. If images were generated after lock,POST.htmlwould have to be re-rendered — so the edition would no longer be locked in practice. Visual assets are therefore resolved here, between the pre-lock persona sweep (Step 6) / optional annotation (Step 7) and the lock (Step 8).[GATE]
What the renderer does and does NOT do.
build-linkedin.mjsembeds the cover by filename reference in the POST.html cover field (linkedin/NN/cover.png
- credit + caption) — it does not inline the image bytes, and it does not embed
fig<N>.pngat all. Inline figures are referenced in the draft markdown () for the author's reference and uploaded manually in the LinkedIn editor. So "visual assets" here means: produce and approve the image files + record their credit/caption, not auto-embed them.
Format branch (decide first). An edition is either standard (cover +
optional inline figures) or carousel (a typografisk slide-deck instead of
cover+inline). It is carousel when its NN is in edition-config.json
→ carousel (the list of editions that ship a document post), or when the
operator declares carousel format for it. Branch accordingly:
standard→ run steps 1–5 below (cover, optional figures, credit/caption).carousel→ skip cover/figures; jump to step 6 (carousel branch) below.
Procedure (standard format):
-
Decide image needs from the article type. Use a light heuristic on the article's skeleton (Step 2.5) + writing contract, or just ask the operator:
- Cover — always mandatory: one hero illustration for the edition.
- Inline figures — article-dependent. Method-heavy articles (a model diagram, a relationship map, before/after) usually want 1–2 figures; diagnosis-heavy articles often need only the cover. Propose a count and let the operator confirm or override.
-
Write a brief per image. For each image (cover + any figures): a short text brief — motif, mood, format, aspect ratio (cover target is 1920×1080, per the renderer's cover block). Generate a first proposal from the skeleton + writing contract; the operator overrides freely. Record each per-image brief in
edition-state.json→articles.NN.visualAssets.cover.briefand…figures[].brief. -
Generate — two routes, no lock-in. The interface is pluggable (path-in / path-out);
mcp-imageis the default, not a hard dependency:- Default route —
mcp__mcp-image__generate_image(Nano Banana Pro / Gemini 3 Pro Image). Write candidates tolinkedin/NN/cover-v<N>-kandidat.png(andfig<N>-kandidat.pngfor figures). Candidate naming lets several attempts sit side by side without overwriting an approved file. Record route"mcp-image". - External route — DALL·E, Midjourney, a photographer, a hand-built SVG.
The plugin accepts a
linkedin/NN/cover-raw.pngthe operator drops in; no tool is mandated. Record route"external". (The raw file may then be cropped/retouched into a candidate, or approved directly.)
- Default route —
-
Operator-gate (render + approve — the Step 2.5/3a pattern, for images). Surface every candidate to the operator with
SendUserFile(the image equivalent of the render+annotate gate the write deliverables use):- Collect the candidate paths (
cover-v<N>-kandidat.png, any externalcover-raw.png). SendUserFilethem with a one-line caption tying each to its brief, so the operator can compare side by side.- The operator either approves one or asks for more attempts (loop
back to step 3 — generate the next
cover-v<N+1>-kandidat.png). - On approval, copy the approved candidate to the fixed name that
build-linkedin.mjsreads —cover.png:
(Same pattern for figures: approved →cd <serie-mappe> && cp "linkedin/NN/cover-v<N>-kandidat.png" "linkedin/NN/cover.png"linkedin/NN/fig<N>.png.) Confirmlinkedin/NN/cover.pngexists before advancing.
Do not advance to Step 8 without an approved
cover.png.[OPERATØR] - Collect the candidate paths (
-
Credit + caption (prep for Step 8). Read
<serie>/linkedin/image-credit-caption.md(template:${CLAUDE_PLUGIN_ROOT}/config/image-credit-caption.template.md— copy it in for a new series) and add/update the row for this edition: motif (one line) + caption (one line, encoding the article's signal). The credit must be honest about AI generation when the image is AI-made (verification duty). Then fold these into<serie>/linkedin/edition-config.json:coverCredit— the global cover credit line (one value for the edition/series).captions[NN]— this article's cover caption / alt text. These are exactly the fieldsbuild-linkedin.mjsalready reads in Step 8.
Procedure (carousel format — branch):
- Render the carousel deck instead of cover+inline. A carousel edition's
visual asset is the slide-deck, not a hero cover. Author the slides in
<serie>/linkedin/NN/carousel.md(the## SLIDE N — …grammarbuild-carousel.mjsparses), then render with the plugin-owned renderer (cwd = series folder):cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-carousel.mjs" linkedin/NN/carousel.mdbuild-carousel.mjswriteslinkedin/NN/carousel.htmlalways andlinkedin/NN/carousel.pdfwhenweasyprintis on PATH (it degrades with an install hint instead of throwing — check the output and surface the hint if the PDF was skipped). Surface the rendered deck (carousel.pdf, elsecarousel.html) to the operator viaSendUserFilefor the same approve / regenerate gate as step 4. Record this underarticles.NN.visualAssets.carousel = { source, pdf, status }and setformat: "carousel". Nocover.pngis required for a pure carousel edition (itsPOST.htmlcarousel block referenceslinkedin/NN/carousel.pdf); a carousel edition that also posts a feed cover runs both branches.[OPERATØR]
Naming convention (documented — consistent with existing series use):
| File | Meaning |
|---|---|
cover.png |
Approved, fixed name — the only cover filename build-linkedin.mjs reads. |
cover-v<N>-kandidat.png |
Generation attempts (mcp-image or post-processed). Several may coexist. |
cover-raw.png |
Optional external pre-edit source (DALL·E / Midjourney / photographer). |
fig<N>.png |
Inline figure (fig1.png, fig2.png, …), referenced from the draft markdown, uploaded manually. |
carousel.md / carousel.pdf |
Carousel deck source + rendered PDF (carousel-format editions). |
Descriptive variant suffixes (e.g. cover-foto-kandidat-v2.png) are fine for
parallel exploration as long as the approved image always lands at the fixed
cover.png name.
Persist + checkpoint state. Once the cover is approved (or, for carousel, the deck is approved) and credit/caption are recorded:
- Set
articles.NN.visualAssets(format, cover.statusapproved, cover.approved"cover.png", candidates list, figures, carousel) inedition-state.json. - Set
currentPhase: "visual-assets"inedition-state.json(the marker that Step 7.5 is complete and the gate has passed). - Write a "visual assets approved (cover/figures or carousel) → next:
lock/delivery" line to
<serie>/STATE.md(overwrite).
Visual assets (BEFORE lock).
- Format: standard (cover + <N> figures) (or: carousel deck)
- Cover: linkedin/NN/cover.png approved (after <N> candidates) (or: N/A — carousel)
- Figures: <N> approved → linkedin/NN/figN.png (or: none)
- Carousel deck: linkedin/NN/carousel.pdf rendered + approved (or: N/A — standard)
- Route: mcp-image | external Credit/caption: recorded in image-credit-caption.md + edition-config.json
- Operator gate: approved (candidates surfaced via SendUserFile) [OPERATØR]
Next: Step 8 — LOCK → delivery.
Step 8: LOCK → delivery — POST.html "all in one place"
This is the lock. Only enter it once the Step 6 persona sweep returned a
clean primær JA and the Step 5 fact-check has no unresolved 🔴. Locking
produces the editor's single delivery artifact — POST.html, the
"all-in-one-place" page that carries the edition text plus its distribution
(delingstekst) copy, ready to paste into LinkedIn.
Order assertion (enforced). Lock (Step 8) runs AFTER the pre-lock persona sweep (Step 6) and BEFORE the hook/conversion gate (Step 9). Reversing lock and the pre-lock sweep reproduces the exact Seres failure (reopening locked texts) — see Step 6. The post-lock hook-gate (Step 9) judges only the distribution hook and never reopens the locked body.
[GATE]
Procedure:
-
Confirm lock preconditions. In
edition-state.json: the article'sfactcheckLoghas no open 🔴,personaSweep.resonancerecorded a primær JA, andvisualAssetsis gated — forstandardformat the approvedlinkedin/NN/cover.pngexists (Step 7.5); forcarouselformat the approvedcarousel.pdf/carousel.htmlexists. If any is missing, STOP — return to the relevant step (5/6/7.5). Do not lock past an open gate. -
Confirm the delivery inputs in the series folder.
render/build-linkedin.mjsreads, relative to cwd (<serie>/linkedin/):linkedin/edition-config.json— calendar, freshness, cover credit/caption. Template + schema:${CLAUDE_PLUGIN_ROOT}/config/edition-config.template.json.linkedin/edition-delingstekst.md— the per-edition distribution text (and thesamlepost). Template:${CLAUDE_PLUGIN_ROOT}/config/edition-delingstekst.template.md, whose header documents the exact## Del N —/## Samlesection grammar the renderer parses.
Both inputs are optional and graceful (renderer degrades, does not throw): a missing or malformed
edition-config.jsonfalls back to empty defaults, and a missingedition-delingstekst.mdyields no distribution copy while the articlePOST.htmlstill builds. Provide both for a complete delivery — the distribution hook is what Step 9 gates. -
Render POST.html. Run with cwd = the series folder (the script resolves
linkedin/from cwd and writeslinkedin/NN/POST.html). The draft filename MUST keep its two-digitNNprefix or the script skips it (↷ hopper over … (ikke NN-prefiks)):cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs" NN-utkast.mdCheck the exit code (N3 — do not assume success). Confirm exit 0 AND that
linkedin/NN/POST.htmlnow exists (the✓ linkedin/NN/POST.htmlline). A skip-warning with exit 0 but no NN file means the prefix was wrong — treat as failure, rename, re-run. Surface any throw (missing config / delingstekst) with its stderr. -
Lock the article. Set
locked: true,status: "locked", andcurrentPhase: "lock-delivery"inedition-state.json. Surface<serie-mappe>/linkedin/NN/POST.htmlas afile://link. From here the body is frozen — Step 9 may only adjust the distribution hook, never the locked edition text.
LOCK → delivery.
- Preconditions: factcheck 🔴 = none, persona resonans primær = JA (else: STOP)
- Delivered: <serie-mappe>/linkedin/NN/POST.html
- build-linkedin exit: 0 + POST.html present (else: non-zero / skip — NOT locked)
- Article status: locked
Next: Step 9 — Hook / conversion gate (post-lock).
Step 9: Hook / conversion gate — "would YOU click?" (AFTER lock)
The locked edition still has to earn the click. The distribution text — the delingstekst hook that fronts the post in the feed — now faces a final binary gate: would the primær persona, scrolling, actually stop and open this? This is the post-lock conversion sweep, distinct from the pre-lock resonance sweep (Step 6): it judges the hook only, binary, never the body.
Order assertion (enforced). This hook-gate (Step 9) runs AFTER lock (Step 8) — it operates on the distribution text of an already-locked edition and must never reopen the locked body. If the hook fails, you revise the delingstekst (and re-render via Step 8's
build-linkedin.mjs), not the edition text. This ordering is the inverse of the resonance sweep, which runs BEFORE lock — keeping the two apart is what fixed the Seres process.[GATE]
Procedure:
-
Take the distribution hook — the first two lines of the edition's entry in
linkedin/edition-delingstekst.md(and thesamlehook, if shipping the collected post). This is what the reader sees before "…see more". -
Run
persona-reviewerin conversion mode (subagent_type: linkedin-studio:persona-reviewer) for the primær persona only, from THIS command layer in the foreground. Passmode: konverter(the after-lock, hook-only mode — NOT resonans). The agent returns a single binary verdict, JA / NEI, on «would YOU click?» — no axis scoring, no flags, no rewritten copy (principle: the jury judges, the editor writes). -
Gate on the binary.
- JA → the hook converts. Proceed to Step 10.
- NEI → the hook does not earn the click. Revise the delingstekst hook
only (sharpen the krok by tightening — close the gap, hold the body
frozen), re-render POST.html via Step 8's
build-linkedin.mjs(the body stays locked; only the distribution copy changed), and re-run the conversion check. Loop until JA.[GATE]
-
Persist. Record the conversion verdict in
edition-state.json→articles.NN.personaSweep.conversion, and setcurrentPhase: "hook-conversion-gate".
Hook / conversion gate (post-lock).
- Primær persona: <name> mode: konverter (hook only)
- Verdict: JA — hook converts (else: NEI — revise delingstekst hook, re-render, re-check)
- Body: untouched (locked in Step 8)
Gate: [PASS — JA] (else loop on the distribution hook)
Next: Step 10 — Scheduling.
Step 10: Scheduling — register the edition in the plugin queue
The locked, conversion-passed edition is ready to ship. Register it in the
plugin's native scheduling queue so it shows up in /linkedin:calendar
(both for viewing and for the publish action) and the posting-time
reminders — the same queue the short-form pipeline uses. The edition is
now a first-class scheduled post.
Procedure:
-
Pick the slot. Confirm the target
scheduled_date(YYYY-MM-DD) andscheduled_timefrom the edition-config calendar / the series brief. If the slot is unset, ask once (Step 1's calibration may already have settled it). -
Register via
queue-manager.mjs. Add one queue entry for the edition, reusing the short-form queue contract —queueAdd(id, draftPath, schedDate, schedTime, pillar, format, hookPreview, charCount):id— stable edition id (e.g.<series-slug>-NN)draftPath— the lockedlinkedin/NN/POST.htmlformat—newsletter(so calendar/reporting can distinguish long-form)hookPreview— the conversion-passed distribution hook (first ~80 chars)
node -e 'import("'"${CLAUDE_PLUGIN_ROOT}"'/hooks/scripts/queue-manager.mjs").then(q => q.queueAdd("<series-slug>-NN","<serie-mappe>/linkedin/NN/POST.html","YYYY-MM-DD","HH:MM","<pillar>","newsletter","<hook ~80c>",<charCount>))'The function appends to
assets/drafts/queue.jsonwithstatus: "scheduled"and returns the new entry. -
Persist + close the edition. Set the article's
status: "scheduled",scheduled: "<YYYY-MM-DD HH:MM>", andcurrentPhase: "scheduling"inedition-state.json. Write a closing "edition scheduled → mark live via/linkedin:calendaron " line to<serie>/STATE.md(overwrite). The pipeline is complete.
Scheduling.
- Queue entry: <series-slug>-NN → assets/drafts/queue.json (status: scheduled)
- Slot: YYYY-MM-DD HH:MM format: newsletter
- Article status: scheduled
Edition complete. Visible in /linkedin:calendar; mark live via /linkedin:calendar (publish action).
Reference Files
${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json— edition-state schema (15 phases including v2.1 skeleton + spine-prose gates, v2.3 visual-assets, and v2.4 editorial-review)${CLAUDE_PLUGIN_ROOT}/config/edition-config.template.json— static delivery metadata schema (calendar, freshness, credit, captions) — Step 8${CLAUDE_PLUGIN_ROOT}/config/image-credit-caption.template.md— cover motif + credit + caption table (honest-about-AI credit) — Step 7.5${CLAUDE_PLUGIN_ROOT}/config/edition-delingstekst.template.md— distribution-copy grammar (## Del N —/## Samle) — Steps 8/9${CLAUDE_PLUGIN_ROOT}/config/personas.template.md— reusable reader personas + "primær trumfer" rule${CLAUDE_PLUGIN_ROOT}/agents/fact-checker.md— Step 5 fact-check agent (risk-sorted, guilty-until-disproven)${CLAUDE_PLUGIN_ROOT}/agents/editorial-reviewer.md— Step 5.5 editor's craft gate (prose-craft + narrative-architecture, BLOCK/REWORK/NICE; mirrors Maskinrommet §C2)${CLAUDE_PLUGIN_ROOT}/agents/persona-reviewer.md— Step 2.5/6/9 reader jury (skeleton + resonance + conversion modes)${CLAUDE_PLUGIN_ROOT}/agents/voice-scrubber.md— Step 4 de-AI / Norwegian-chronicle voice scrub (gold standard = approved Norwegian editions, NOT the English post corpus)${CLAUDE_PLUGIN_ROOT}/commands/react.md— multi-source synthesis discipline (reused in Step 2)${CLAUDE_PLUGIN_ROOT}/assets/voice-samples/authentic-voice-samples.md— voice matching${CLAUDE_PLUGIN_ROOT}/references/longform-quality-rules.md— canonical long-form rules (Steps 2.5, 3a, 3b, 4–9 all reference)${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs— POST.html delivery; readslinkedin/NN/cover.png+ credit/caption (Step 8)${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs— annotatable review renderer (Step 7)${CLAUDE_PLUGIN_ROOT}/render/build-carousel.mjs— carousel deck renderer (## SLIDE N —→ PDF via weasyprint) — Step 7.5 carousel branch