---
name: linkedin:newsletter
description: |
  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".
allowed-tools:
  - Read
  - Glob
  - Grep
  - WebFetch
  - Bash
  - AskUserQuestion
  - Task
  - Write
---

# LinkedIn Newsletter — Long-Form Content Engine

> **⏱ Before you start — this is a campaign, not a post.** Producing one edition is
> a **multi-session, multi-gate process (18 phases)**: lived-specifics extraction →
> research → skeleton gate →
> spine gate → full draft → contract-gate → fact-check → editorial craft gate →
> persona sweep → cold headless review → visual assets → lock → hook-conversion
> gate → schedule.
> Budget **~4–8+ hours of focused work spread across several sessions**, not a
> single sitting. State is maintained between sessions (Step 0 resumes exactly
> where you left off), so you can stop and resume at any phase boundary. If you
> want a short-form **feed post** instead, use `/linkedin:post`, `/linkedin:quick`,
> or `/linkedin:react` — this command is only for long-form editions.

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
  `PreToolUse` content-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 a per-slug
  folder under your series base
  (`${LTL_SERIES_ROOT:-$HOME/linkedin-series}/<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-nearest
> `STATE.md` (auto-injected by the `session-start` hook) 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 in `edition-state.json`. There is no `§4`/`§5`/`§6` handover —
> those records moved into `edition-state.json` (fact-check log, persona
> verdicts) and `STATE.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 bare
> `subagent_type` does not resolve and the `Task` call fails. Every
> `subagent_type` below 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 parallel `Task` calls itself and synthesizes their returns inline.

## Pipeline overview (19 phases)

The phase order is fixed. Two gates run **BEFORE prose** (skeleton + spine
prose), an **editorial craft gate** runs before the persona sweep, the persona
resonance sweep runs **BEFORE lock**, and a **cold adversarial review package**
(Step 6.5) runs after the in-session persona sweep and 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; v3.1 / Endring 9 on adversarial independence + framing-bias).

| 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` |
| 1.5 | **Lived-specifics extraction — BEFORE research** | elicitation interview (3–5 sharp questions, **vagueness refused**) → `add` lived material to the specifics-bank + populate `articles.NN.livedSpecifics` slot-map (each load-bearing key-point bound specific/abstrakt/ekstern/unresolved) → render `NN-kilder.md`. The grounded-sourcing layer (Fix #2, kilde-så-draft; never fabricated — invariant 3). | `AskUserQuestion` + `scripts/specifics-bank` (`node --import tsx src/cli.ts`) |
| 2 | **Research — scoped by the binding** | parallel scoped mandates → verified notes; triangulation. Fills the `ekstern`/`unresolved` slots + verifies your own numbers (regel 6/7) | **`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. **Serie-destillat-sjekk** (has the reader heard this anecdote/argument/hook before? advisory, into the annotation gate). Operator-gate JA/NEI/REVIDER. Persona-skjelett-sweep before any prose is written. | `AskUserQuestion` + `scripts/editions` (`distil-check`) + **`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`** |
| 4.5 | **Contract-gate — BEFORE the AI sweeps** | deterministic §B/§C1 rule-gate on the draft: ratify `rules.ts` ↔ §E-manifest (KTG invariant) → gate the draft (BLOCK = fix by tightening + re-run; WARN = direction). Mechanical violations die here, before the costly AI gates. | `scripts/contract-gate` (`node --import tsx src/cli.ts`) |
| 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) |
| 6.5 | **Headless adversarial review — BEFORE lock** | COLD review package on a frozen draft, no drafting-session context: content-reviewer (argument) + language-reviewer (Norwegian) + fact-reviewer (cold re-verification incl. pivot premises) + persona-reviewer resonance/conversion. Consolidated, operator-gated via `SendUserFile`. The independence layer the in-session gates can't be. | **`content-reviewer` + `language-reviewer` + `fact-reviewer` + `persona-reviewer`** (parallel) + `SendUserFile` |
| 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 (coded `build-figur.mjs` primary for data figures / mcp-image for illustrative / external `cover-raw.png`) → operator-gate (`SendUserFile`) → approve to `cover.png` → credit/caption. Runs before lock so the renderer picks the cover up. | `render/build-figur.mjs` (data figures) + `mcp__mcp-image__generate_image` + `SendUserFile` + (carousel) `render/build-carousel.mjs` |
| 8 | **LOCK → delivery** | POST.html "all in one place"; auto-gull (gold corpus) + **serie-minne** (series distillate + specifics-bank usage log) | `render/build-linkedin.mjs` + `scripts/editions` (`distil-append`) + `scripts/specifics-bank` (`record-usage`) |
| 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; close the editions-register row (measured lead time) | `hooks/scripts/queue-manager.mjs` + `scripts/editions` (`register-complete`) |
| 11 | **Retro — make the corrections stick** | ≤5 min AFTER delivery: promote the pending `foldIns` (mechanical → a contract-gate rule; judgment → the language-rules file), summarize the MEASURED effort from `phaseLog`, ask ONE friction question. The loop that makes edition N's correction enforced in N+1 | `scripts/contract-gate` (`--ratify`) + `scripts/editions` (`register-list`) + `references/fold-in-loop.md` |

> **Build status:** all 19 phases (Steps 0, 1, 1.5, 2, 2.5, 3a, 3b, 4, 4.5, 5,
> 5.5, 6, 6.5, 7, 7.5, 8–11) are implemented below. This command takes an edition
> end-to-end:
> load → calibration → **lived-specifics extraction (elicitation + bank binding,
> BEFORE research)** → verified research (scoped by the binding) → **skeleton + section pitch (operator +
> persona gate BEFORE prose)** → **spine prose (operator gate BEFORE full
> expansion)** → full prose draft → consistency/quality → **contract-gate (deterministic §B/§C1)** → fact-check sweep →
> **editorial review (craft gate, operator-gated BEFORE the persona sweep)** →
> pre-lock persona sweep → **headless adversarial review (cold review package,
> operator-gated BEFORE lock)** → optional annotation → **visual assets
> (cover/figures or carousel, operator-gated BEFORE lock)** → LOCK/delivery →
> post-lock hook gate → scheduling → **retro (promote this edition's corrections
> so the next one starts with them enforced)**, 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.

1. **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:-$HOME/linkedin-series}/<slug>/`. The
     `LTL_SERIES_ROOT` env-var overrides the base without editing this command
     (and `LTL_BRAND` re-brands the rendered output — empty by default); the
     default base is a 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 a specific series path.
2. **Read edition-state** (`<serie>/linkedin/edition-state.json`) if it exists —
   it tells you `currentArticle`, `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.
3. **Read `<serie>/STATE.md`** — the narrative production state (where we are +
   the one next step). The `session-start` hook auto-injects the cwd-nearest
   `STATE.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 no `edition-HANDOVER.md`. The durable
   records that used to live in the handover now live in `edition-state.json`
   (immutable rules, fact-check log, persona verdicts). If `<serie>/STATE.md`
   does 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 own `STATE.md` / `docs/BUILD-HANDOVER.local.md` (which govern
   building the plugin itself).
4. **Read the long-form voice profile** — the **chronicle-voice-profile**
   (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/chronicle-voice-profile.md`),
   the distilled chronicle voice for THIS edition's language/format, built by
   `voice-trainer` from the published gold corpus. This is the **primary reference
   for every long-form drafting and review phase**. Short-form feed posts and
   long-form editions are different registers of the same author, so long-form must
   **not** be drafted against the English short-form corpus
   (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/authentic-voice-samples.md`) —
   that corpus encodes rules (e.g. no em-dash) that are wrong for chronicle voice
   and would degrade it. Read the `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`
   corpus as a **secondary tributary** (register cues), never as the primary
   long-form reference.
   **Fallback (never silent).** If `chronicle-voice-profile.md` is absent — or has
   no section for this edition's language/format — say so plainly, note the
   chronicle voice is uncalibrated, and fall back to today's behavior (draft against
   `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`), while
   flagging the gap so the operator can seed the gold corpus (Step 8 auto-gull grows
   it from each locked edition).
5. **Resolve the active personas (per-artifact).** Personas are configured **per
   edition**, not from one fixed global file. Resolve the set for
   `articles.<currentArticle>` in this order (Step 1 finalizes + persists it):
   1. **`edition-state.json` → `articles.NN.personas`** — if already populated
      (a resumed edition), use it as-is.
   2. **`<serie>/linkedin/personas.md`** — a per-series persona file, if present.
   3. **`${CLAUDE_PLUGIN_ROOT}/config/personas.local.md`** (else
      `personas.template.md`) — the plugin library; select a subset.
   4. **None / insufficient** — Step 1 will **define personas interactively**.
   Exactly one persona is the **primær**. The resolved set feeds BOTH the Step 6
   resonance sweep AND the Step 6.5 headless package; see
   `config/personas.template.md` → "Per-artifact personas".
6. **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 in `edition-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 1.5 — Lived-specifics extraction *(Fix #2 — elicitation + bank binding BEFORE research)* |
| `lived-specifics`               | Step 2 — Research *(scoped by the binding)* |
| `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 6.5 — Headless adversarial review *(v3.1 — cold review package, BEFORE lock)* |
| `headless-review`               | 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`                    | Step 11 — Retro *(N14 — promote the fold-in queue; the edition is delivered but the loop is still open)* |
| `retro`                         | **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 <X>" 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 → `currentPhase` is `persona-sweep-prelock` → re-run → the table resumes
> at Step 6.5 (headless adversarial review). 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)
Chronicle voice profile: loaded (<language>/<format>) | MISSING → fallback to short-form corpus
Persona library: <N> personas loaded (active set chosen in Step 1)
```

If the chronicle voice profile or persona library is missing, say so plainly and
continue on the Step-0 fallback — do not fabricate either.

### Phase-transition protocol — one deterministic call per transition

Every Step below ends by writing `currentPhase` to `edition-state.json`. That
write is a **phase transition**, and every transition runs exactly one
deterministic command — no per-step variation, no hand-written telemetry:

```bash
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-upsert \
  --edition-state "<serie>/linkedin/edition-state.json" \
  --next "<the one next concrete step>"
```

Run it **after** `currentPhase` has been written, never before — the command
reads the phase out of the state file. It does two things in one call:

1. **appends** `{phase, completedAt}` to `articles.NN.phaseLog` — the lead-time
   telemetry (A1-12); and
2. **mirrors** the edition into the editions register at
   `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/editions/register.json`
   — series, edition, title, series path, phase, next action, slot (A1-11).

One call, both writes: a log the command layer had to remember to append
separately would be incomplete within a week, and an incomplete log measures
nothing. Add `--slot "<YYYY-MM-DD HH:MM>"` once a publishing slot is claimed, and
`--path <series-root>` when the state file is not at the conventional
`<serie>/linkedin/edition-state.json` location.

The week's work-in-progress is then one command away, with no series folder
opened:

```bash
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-list
```

**At Step 10 only**, once the edition is scheduled, close its row (this is what
turns `startedAt` into a measured lead time):

```bash
node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-complete \
  --series "<slug>" --edition "<NN>"
```

> **The register is a MIRROR, never a source of truth.** Resumption reads
> `edition-state.json` and nothing else (the table above). The register is a
> read-only view for humans; if it is deleted or drifts, the next transition
> repairs it and nothing about the edition is lost. So a failing `register-upsert`
> is a telemetry problem to report plainly — never a reason to stop the pipeline
> or to re-do the phase.

Each Step below marks its transition **↳ Phase transition** — that marker means
"run the protocol above for this phase".

## 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.

> **Trend intake (optional bridge — N7).** If this edition is being started from a
> `/linkedin:trends` candidate — the operator supplies a **trend id** (shown in the
> trends brief and via `CLI list --json`, where
> `CLI` = `cd "${CLAUDE_PLUGIN_ROOT}/scripts/trends" && node --import tsx src/cli.ts`)
> — read that record and **pre-fill the brief from it** instead of asking from
> scratch. Without a trend id, skip this block entirely: the flow below is
> unchanged (the bridge is an offer, never a requirement).
>
> 1. **Read the candidate.** Run `CLI list --json` (or `CLI query --topics <edition
>    topics> --json`) and select the entry whose `id` equals the supplied trend id.
> 2. **Pre-fill from the record's proposal layer (N6 fields)** — each is a *starting
>    point the operator confirms or edits* in the dimensions below, not a bypass of
>    calibration:
>    - `angle` → the edition **Angle**.
>    - `targetLevel` → the audience/tone calibration (**Audience** / **Tone**); the
>      resolved value is also persisted to `articles.NN.targetLevel` at the Step 1.5
>      checkpoint (below) so persona-reviewer can place the personas on the configured
>      target-level span (which end is primær, which is the mandatory secondary).
>    - `summary` + `rationale` + `readerQuestion` / `painPoint` → seed the **Key
>      points** and the **Leader-takeaway** (the reader-side framing this bridge
>      exists to carry: what the reader would DO with it).
>    - `url` (+ any URLs in `rationale`) → seed **source URLs** for Step 2 research /
>      the `NN-kilder.md` ledger (they land as `ekstern` slots at Step 1.5).
>    - `actionability` / `verdict` (BÆRENDE/STØTTE/NYHET) → frame the takeaway; a
>      candidate the operator selected already cleared the F7 reader-grip gate.
> 3. **Bind the provenance.** Note the trend id — it is persisted as
>    `articles.NN.sourceTrendId` at the **Step 1.5 checkpoint** (below) and closes the
>    discovery→production loop automatically at Step 10 (scheduling flips the trend to
>    `acted`).

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 (per-artifact)** — finalize the **one or more personas for
  THIS edition** from the Step 0 resolution, and **mark exactly one as primær**.
  If the resolution found a set (edition-state / series file / plugin library),
  confirm or trim it; if it found none — or the operator wants a reader the
  library does not cover — **define personas interactively** here (name + the
  five fields: rolle, avkobler, overbeviser, ekspertise, sjargong). The primær
  reader weighs highest in the Step 6 sweep AND the Step 6.5 headless package; 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).
  **Persist** the resolved set to `edition-state.json` →
  `articles.NN.personas` (each entry: name, tier, the five fields, source) at the
  **Step 1.5 checkpoint** (the first machine-state write; Step 2 then adds the
  research notes) — it is then stable across sessions and is the single source
  every later sweep reads. See `config/personas.template.md` →
  "Per-artifact personas".
- **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).
- **Language** — **Resolve the review language** for this edition: detect it from
  the series brief / prior editions / the drafting language, else ask once. Persist
  it to `edition-state.json` → `language` (e.g. `"no"` for Norwegian, `"en"` for
  English) at the **Step 1.5 checkpoint**. This is not cosmetic — it gates the
  language-dependent review agents: `language-reviewer`'s Norwegian-idiom /
  «kanselli-stil» checks and `voice-scrubber`'s gold standard fire against THIS
  language, and never grade prose against Norwegian idiom when the edition is not
  Norwegian. Left unresolved, the template default `"en"` silently disables the
  Norwegian axis even for a Norwegian edition (the C-3 bug this closes) — so resolve
  it explicitly, do not assume the default.

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 1.5: Lived-specifics extraction — BEFORE research

> **The grounded-sourcing layer (Fix #2, kilde-så-draft).** Sourcing, not styling,
> is where authenticity is won. A draft that starts from your real number / named
> case / what-actually-broke is forwardable; a draft that invents plausible filler
> is hollow, and every downstream voice gate then only polishes hollow content. So
> BEFORE research and BEFORE prose, each load-bearing claim is bound to real
> material — or you *consciously* decide it does not need a concrete instance. This
> step is that binding; **vagueness is refused** (drømme-spec G4). The plugin never
> invents lived experience (invariant 3).

> **Order assertion (enforced).** Step 1.5 runs AFTER the brief (Step 1) and BEFORE
> research (Step 2). It populates the slot-map and renders the `NN-kilder.md`
> ledger; the BLOCKing «vaghet avvises» gate (`validate-binding`) is enforced at
> the **Step 2.5 skeleton-gate**, once the spine is visible. Research (Step 2) is
> then scoped BY this binding — it fills the `ekstern`/`unresolved` gaps and
> verifies your own numbers, instead of re-sourcing what is already grounded. `[GATE]`

> **Adopter note.** This step uses the KTG-internal specifics-bank
> (`${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank` — run `npm install` there once).
> If its deps are absent (a plain adopter ships none), this Step is a **no-op**:
> skip to Step 2 and supply specifics inline during drafting. It never blocks an
> adopter.

**Procedure:**

1. **Surface inventory you already have.** Derive the edition's topic tags from the
   brief (Step 1 angle + key points), and query the bank for lived material that
   already matches — so the interview asks only for what is genuinely missing. The
   CLI runs from its own dir (where `tsx` lives):

   ```bash
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
     node --import tsx src/cli.ts query --tags "<tag1,tag2,…>"
   ```

   Show the hits to the operator; each is a candidate binding for a load-bearing
   key-point.

2. **Elicitation interview — 3–5 sharp questions, vagueness refused.** For each
   load-bearing key-point (Step 1) NOT already covered by inventory, ask one tight
   question that demands a *concrete instance*, not a generality. Pick the shape the
   claim needs (one per question):
   - a **number** you measured ("how many / how much / how long — your figure"),
   - a **named case** ("which project / tool / team — name it"),
   - a **what-broke** ("what actually failed, concretely"),
   - a **contrarian** point ("where do you disagree with the consensus, and why"),
   - a **mind-change** ("what did you used to believe and stop — what flipped it").

   **Refuse vagueness.** If the answer is a generality ("it's generally faster",
   "people struggle with this"), push back once for the concrete instance. If the
   operator genuinely has none, that key-point becomes an `abstrakt` slot (with a
   stated rationale) or an `ekstern` slot (research will source it) — **never a
   fabricated specific** (invariant 3). The plugin elicits your raw material; it
   does not invent it.

3. **Fold elicited material into the bank.** For each concrete answer, `add` it so
   it is reusable across editions (content-hash dedupe + tag-union are automatic):

   ```bash
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
     node --import tsx src/cli.ts add \
       --type <number|named-case|what-broke|contrarian|mind-change|other> \
       --content "<verbatim — the operator's own words>" --tags "<tags>" \
       [--source "<where it came from>"] [--verification <verified|unverified|n/a>]
   ```

   A figure the operator states from memory is `--verification unverified` — it
   stays guilty-until-checked, and Step 2 verifies it (regel 6/7).

4. **Populate the slot-map.** Write `articles.NN.livedSpecifics` in
   `edition-state.json` — one `kind: "key-point"` slot per load-bearing claim, each
   bound to exactly one of:
   - `{ type: "specific", specificId: "<bank id>" }` — grounded in lived material;
   - `{ type: "abstrakt", rationale: "<why no concrete instance is needed here>" }`;
   - `{ type: "ekstern", source?: "<research ref — Step 2 fills it>" }` — to be
     sourced, not lived;
   - `{ type: "unresolved" }` — a temporary placeholder only; this is the vagueness
     the Step 2.5 gate BLOCKs, so resolve it before then.

   Set `status: "pending"` (Step 2.5 flips it to `"bound"` when the gate passes).
   The shape is documented in `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json`
   (`_doc.livedSpecifics`).

5. **Render the `NN-kilder.md` ledger.** Deterministically render the read-only
   sources artifact from the slot-map + bank (NN = the same zero-padded edition
   number as `NN-utkast.md`). Pass absolute paths:

   ```bash
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
     node --import tsx src/cli.ts render-kilder \
       --edition "<abs>/linkedin/edition-state.json" \
       --out     "<abs>/linkedin/NN-kilder.md"
   ```

   It is regenerated like POST.html, never hand-edited. Surface it to the operator
   so the binding is visible (coverage by type + the numbers-to-verify line).

6. **Persist + checkpoint state.** Step 1.5 is the **first phase that writes machine
   state**, so:
   - If `edition-state.json` does not exist yet (fresh edition), initialize it from
     the template schema (`${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json`).
   - Persist the resolved **brief** (Step 1) and **personas** (the set + primær)
     into `articles.NN` — this is the first durable write of both (Step 2 then adds
     the research notes).
   - If Step 1 ran the **trend-intake bridge**, persist `articles.NN.sourceTrendId`
     (the trend id) here too — the durable provenance link Step 10 reads to auto-`act`
     the trend. Omit it (leave `null`) for a manually-started edition.
   - Persist the resolved **`targetLevel`** (Step 1's Audience/Tone calibration; the
     trend bridge pre-fills it from the trend record) to `articles.NN.targetLevel` — the
     per-article position on the configured target-level span that persona-reviewer reads
     to place the personas (which end is primær, which is the mandatory secondary). Leave
     `null` if the operator did not resolve a level; the reader agents then fall back to
     each persona's own `ekspertise` field.
   - Persist the resolved **`language`** (Step 1) — write it to the **top-level**
     `language` (the series/edition-level review language; there is no per-article
     `language` field) so the language-dependent review agents grade against the
     right language from here on. This is the write that makes the template
     doc-string's "Resolved at Step 1" claim true.
   - Write `articles.NN.livedSpecifics` (the slot-map above, `status: "pending"`).
   - Set `currentPhase: "lived-specifics"` in `edition-state.json`.
   - **↳ Phase transition** — run the protocol (Step 0) for `lived-specifics`,
     `--next "Step 2 — research (scoped by the binding)"`.
   - Write a "lived-specifics bound → next: research (scoped by the binding)" line
     to `<serie>/STATE.md` (overwrite).

```
Lived-specifics extraction (BEFORE research) — complete.
- Inventory hits reused: <N>   New material elicited + added to bank: <N>
- Slot-map: <total> slots — levd <N> · abstrakt <N> · ekstern <N> · uavklart <N>
- Numbers to verify (regel 6/7): <N> (carried to Step 2)
- Ledger rendered: <serie>/linkedin/NN-kilder.md
- Vagueness refused: <N or none>   (never fabricated — invariant 3)
Next: Step 2 — Research (scoped by the binding: fill ekstern/unresolved, verify numbers).
```

---

## 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 `Task` calls issued from this
> command layer** — not sequential guessing, not a background agent. (Principle 4.)

**Procedure:**

0. **Scope by the binding, not the raw key points (Fix #2 re-scope).** Read
   `articles.NN.livedSpecifics` (Step 1.5). Research targets the **gaps the binding
   names** — slots bound `ekstern` (and any still `unresolved` at entry): the claims
   that explicitly need an external source. Slots bound to a `specific` are already
   grounded in lived material; do **not** re-source their substance (you may
   corroborate, but it is not the gap). Every **`unverified-number`** warning from
   the Step 1.5 binding is also a research target: confirm the operator's own figure
   against a primary source (regel 6/7). This is what makes research *fill* the
   draft instead of re-deriving what the operator already knows.

0.5. **External fact-package intake (optional — kilde-så-draft with finished
   research).** If the operator already has a **deep-research report** — a file the
   operator supplies by path (e.g. a Gemini Deep Research export, a consultant brief,
   a prior research pass; markdown / text / PDF) — ingest it as a research input
   instead of researching those slots from zero:
   - **Read the file** and extract the same structure the fan-out returns: findings
     (each with its source), data points (figures + source + date), and anything the
     report itself flags as uncertain.
   - **Map onto the binding.** Attribute each finding to the `ekstern`/`unresolved`
     slot (or unverified number) it resolves; its sources become the `source` on
     those slots and land in `NN-kilder.md` at persist (step 5).
   - **Same guilty-until-disproven discipline.** A packaged claim is NOT
     pre-verified: label anything you cannot corroborate `UNVERIFIED` and carry it to
     the Step 5 fact-check sweep as 🟡 — a polished report is not a primary source.
   - **Then scope the fan-out to the GAP only.** Slots the package covers need no
     `Task` call; run steps 1–3 below **only for the slots the package leaves open**.
     If the package covers every `ekstern`/`unresolved` slot, the fan-out may reduce
     to zero calls — note that explicitly (don't fabricate a fan-out that added
     nothing). With no package supplied, this sub-step is a no-op: proceed to step 1.

1. **Decompose** the binding's `ekstern`/`unresolved` slots + unverified numbers
   (step 0) 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.

2. **Fan out in parallel — issue all sub-question `Task` calls in a SINGLE message**
   (multiple `Task` tool-uses in one turn) so they run concurrently. Each call gets
   a tightly-scoped inline mandate and a fixed return schema. Use `WebSearch`-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
   ```

3. **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.

4. **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/unverified` items —
   they become 🟡 entries for the Step 5 fact-check sweep.

5. **Persist + checkpoint state.** Write the verified research notes into
   `edition-state.json` (the brief + personas were first persisted at the Step 1.5
   checkpoint — re-affirm them here if calibration changed). **Update the binding
   from what research resolved:** when a sub-question sources an `ekstern` slot, set
   its `source`; when it confirms an operator figure, flip the bank specific
   `unverified → verified` (the bank's `add`/verify path), then **re-render
   `NN-kilder.md`** so the ledger stays current (a number that could not be verified
   stays `unverified` and is carried to the Step 5 fact-check sweep as 🟡 — never
   silently asserted). Set `currentPhase: "research"`, article status
   `in-progress`, and append a
   "research complete → next: skeleton + section pitch (BEFORE prose)" next-step
   line to `<serie>/STATE.md` (overwrite). If somehow no `edition-state.json` exists
   yet (Step 1.5 was a no-op for an adopter), initialize it from the template schema
   first. **↳ Phase transition** — run the protocol (Step 0) for `research`,
   `--next "Step 2.5 — skeleton + section pitch"`. 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; scoped to ekstern/unresolved slots + numbers)
- Verified notes: <N> by key point
- Binding updated: ekstern slots sourced <N> · numbers verified <N> → NN-kilder.md re-rendered
- 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, the **deterministic
> binding-gate** passes (every load-bearing claim grounded — Fix #2), and the
> persona-skjelett-sweep returns a clean primær JA. This ordering encodes the
> Maskinrommet writing-contract §A discipline (skeleton + sources before prose)
> into the pipeline. `[GATE]`

**Procedure:**

1. **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.)

2. **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.

3. **Write the skeleton + pitches to `<serie>/NN-skjelett.md`** (NN = the same
   zero-padded edition number used by `NN-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:

   ```markdown
   # 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. …
   ```

4. **Serie-destillat-sjekk — «har leseren hørt dette før?» (deterministic).**
   Every other gate in this pipeline sees ONE edition. At series cadence the
   fastest-growing defect is not a bad edition but a **repeated** one — the
   anecdote, the argument or the hook the reader already met three editions ago.
   Check the proposed skeleton against everything the series has locked, while
   changing course is still cheap. Write the units the spine + pitches actually
   propose to a small JSON file (`{anecdotes, arguments, hooks}` — the operator's
   phrasing, not a summary), then run it from the package dir with **absolute**
   paths:

   ```bash
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/editions" && \
     node --import tsx src/cli.ts distil-check \
       --distillate "<abs>/linkedin/series-distillate.json" \
       --skeleton "<abs>/skjelett-units.json"
   ```

   - **CLEAR** → nothing here echoes an earlier edition. Carry the one-liner into
     the annotation page as a receipt.
   - **REUSE** → the output names each unit, the edition it echoes, and how close
     it is. This is **advisory, never a block** (exit 0 either way): a deliberate
     callback is a legitimate move — an *unnoticed* retread is not. Fold the
     finding into the annotation page (step 5) so the operator decides with the
     re-use in view: keep it, sharpen it into an explicit callback, or replace
     the unit.
   - No distillate yet (a series' first edition, or an adopter who has not locked
     one since this gate existed) → CLEAR against 0 editions; say so and move on.
     Deps absent → skip with a one-line note, like the binding-gate.

5. **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:

   1. The skeleton is already at `<serie>/NN-skjelett.md` (step 3).
   2. Render it to review HTML with the plugin-owned renderer (cwd = series folder):
      ```bash
      cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-skjelett.md
      ```
      Check the exit code (N3 — non-zero = no HTML produced) and confirm
      `<serie-mappe>/review/NN-skjelett.html` exists before handing it off.
   3. Surface `<serie-mappe>/review/NN-skjelett.html` to the operator as a
      `file://` link (use `SendUserFile` if available, else a markdown `file://`
      link) for the annotation pass. If the destillat-sjekk (step 4) returned
      **REUSE**, state each echoed unit + the edition it came from alongside the
      link — the operator annotates with the re-use visible, not after the fact.
   4. The operator annotates in the browser and pastes the annotated markdown
      back. Fold the notes into `<serie>/NN-skjelett.md` by tightening (rule 6).

      **↳ Fold-in capture.** Any note here that would be just as true of the
      *next* edition — a rule, not a fact about this one — appends **one row** to
      `articles.NN.foldIns[]`: `correction` (near-verbatim), `trigger` (where it
      surfaced), `classification: null`, `decision: "pending"`. Capture is
      unconditional and costs seconds; **Step 11** classifies and promotes it. An
      edition-specific fix ("that number is 4.2") is not a rule — leave it out.
      Full loop: `${CLAUDE_PLUGIN_ROOT}/references/fold-in-loop.md`.
   5. **Receipt, not gate.** After folding in the annotations, use
      `AskUserQuestion` only 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 binding-gate (step 6), then the
        persona-skjelett-sweep (step 7).
      - **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 this operator-gate, the deterministic
   binding-gate, AND the persona-skjelett-sweep below all pass. `[OPERATØR]`

6. **Binding-gate — deterministic «vaghet avvises» (Fix #2, BEFORE the persona
   sweep).** Now that the spine is visible, enforce the Step 1.5 binding: every
   load-bearing slot must resolve to a real specific, a justified `abstrakt`, or an
   `ekstern` source — no `unresolved` slot, no `specific` pointing at a missing bank
   id. It is pure/deterministic (no model in the loop), so the same vagueness is
   caught the same way every time. Run it from the package dir; pass the edition
   state's **absolute** path:

   ```bash
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
     node --import tsx src/cli.ts validate-binding \
       --edition "<abs>/linkedin/edition-state.json"
   ```

   - **exit 0** → PASS. Surface any **WARN** (`unverified-number`, regel 6/7) as
     direction — a number still unverified after Step 2 research is carried to the
     Step 5 fact-check sweep as 🟡, never silently asserted.
   - **exit 1** → BLOCKED. The output names each blocking slot (`unresolved` /
     `dangling-specific` / `unjustified-abstrakt`). Resolve it — elicit the missing
     specific (loop back to Step 1.5 step 2), fix the dangling id, or give the
     `abstrakt` escape a real rationale — then **re-render `NN-kilder.md`** and
     re-run until exit 0. No prose is written on an unresolved load-bearing
     claim. `[GATE]`
   - deps/contract absent (adopter — Step 1.5 was a no-op) → skip with a one-line
     note; the persona-skjelett-sweep below still runs.

   When the gate passes, set `articles.NN.livedSpecifics.status: "bound"` in
   `edition-state.json`.

7. **Persona-skjelett-sweep — fan out `persona-reviewer` in skjelett-mode.**
   Issue one `persona-reviewer` call per active persona in parallel — a SINGLE
   message with multiple `Task` tool-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`, and **`mode: 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).

8. **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]`

9. **Convergence loop.** If gate is REWORK/BLOCK, fold flags into the
   skeleton + pitches (or, on BLOCK, return upstream) and re-run the same
   `persona-reviewer` calls 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.

10. **Persist + checkpoint state.** Once the skeleton is JA from the operator, the
   binding-gate passed, AND the persona-skjelett-sweep returns a clean primær JA,
   record:

   - The final skeleton + pitches in `<serie>/NN-skjelett.md` (already written
     in step 3, with any in-loop revisions applied).
   - `articles.NN.livedSpecifics.status: "bound"` (set by the binding-gate, step 6).
   - Per-persona skjelett verdicts in
     `edition-state.json` → `articles.NN.personaSweep.skeleton` (or alongside
     resonance/conversion under the same `personaSweep` object).
   - `currentPhase: "skeleton-pitch"` in `edition-state.json` (the marker that
     Step 2.5 is complete and the gate has passed).
   - **↳ Phase transition** — run the protocol (Step 0) for `skeleton-pitch`,
     `--next "Step 3a — spine prose"`.
   - A "skeleton + pitches PASS (primær JA) → next: Step 3a (spine prose)"
     next-step line in `<serie>/STATE.md` (overwrite).

```
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)
- Series distillate: CLEAR against <N> locked edition(s)   (else REUSE: <N> unit(s) — kept as explicit callback / replaced)
- Operator gate: JA (after <N> revision rounds)
- Binding-gate (vaghet avvises): PASS — all load-bearing slots bound; livedSpecifics → "bound"   (else BLOCK: <N> unresolved/dangling)
- 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 + binding bound, 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:**

1. **Pass 0 — load the voice reference + the drift negatives** before writing a
   single sentence (voice match starts at the spine, not at expansion — the
   existing LTL rule, not optional for long-form):
   - Re-read the **chronicle-voice-profile**
     (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/chronicle-voice-profile.md`)
     for this edition's language/format — the reference the spine must match
     (fall back to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`
     per Step 0 if it is missing).
   - Read the **chronicle-voice-drift-log**
     (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/chronicle-voice-drift-log.md`)
     as a **negative list**: the recurring AI-tells and voice drifts `voice-scrubber`
     logged on prior editions. Do NOT reproduce them in the spine — the log is no
     longer write-only; it steers drafting BEFORE the scrub, so the Step-4/persona
     passes and the scrubber find less each edition. If the log is absent, note it
     and proceed.

2. **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.

3. **Write the spine draft** to `<serie>/NN-utkast.md` (the canonical draft
   path — Steps 7 and 8 render this exact file). This is the same `NN-utkast.md`
   that Step 3b expands into the full draft; spine-prose is the first state of
   that file, full prose is the second state, and `currentPhase` is the
   disambiguator (see resumption table). Do NOT render in this state (Step 7's
   review HTML and Step 8's POST.html require `currentPhase: "draft"` — i.e.
   Step 3b complete).

4. **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?»

   1. Spine prose is written to `<serie>/NN-utkast.md` (step 3). Render it to a
      review page (cwd = series folder):
      ```bash
      cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-utkast.md
      ```
      Check the exit code and confirm `<serie-mappe>/review/NN-utkast.html` exists.
   2. Surface the `file://` link (`SendUserFile` if available, else a markdown
      `file://` link).
   3. The operator annotates and pastes back; fold the notes in by tightening
      (rule 6 of `references/longform-quality-rules.md`).

      **↳ Fold-in capture.** Same rule as Step 2.5: a note that generalizes past
      this edition appends one `articles.NN.foldIns[]` row (`decision:
      "pending"`, `classification: null`) with the correction near-verbatim and
      the trigger. **Step 11** empties the queue; nothing here decides whether a
      correction "deserves" a rule.
   4. **Receipt, not gate.** `AskUserQuestion` as 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]`

5. **Persist + checkpoint state.** Once the operator says JA:

   - `NN-utkast.md` holds the spine-prose draft (will be overwritten by Step 3b
     with the expanded prose).
   - `currentPhase: "spine-prose"` in `edition-state.json` (the marker that 3a
     is complete and the gate has passed).
   - **↳ Phase transition** — run the protocol (Step 0) for `spine-prose`,
     `--next "Step 3b — full prose expansion"`.
   - 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), record `currentPhase: "draft"` **with a section-level
> cursor** in `edition-state.json`, and write a precise "draft resumes at
> section <X>" 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:**

1. **Re-read the chronicle-voice-profile**
   (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/chronicle-voice-profile.md`,
   fallback `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/`)
   and the drift-log negatives (Step 3a Pass 0) before expanding — the voice was set
   at the spine; do not lose it, and do not let expansion reintroduce a logged drift.

2. **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.)

3. **Expand with the `content-repurposer` muscle.** Reuse
   `agents/content-repurposer.md` (its article→long-form conversion discipline)
   for individual section expansions — invoke it via `Task`
   (`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).

4. **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.mjs` and `render/build-linkedin.mjs`
   silently skip any draft without an `NN` prefix). Set
   `currentPhase: "draft"` in `edition-state.json`, and write a "draft
   complete → next: consistency/quality" line to `<serie>/STATE.md` (overwrite).
   **↳ Phase transition** — run the protocol (Step 0) for `draft`,
   `--next "Step 4 — consistency + quality"`. Run it only when the draft is
   COMPLETE, not at an interim cursor save: the log measures finished phases.

```
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.)

> **Plus the rules THIS operator has accumulated (N14 / C-10).** Read
> `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules/<lang>.md`
> (`<lang>` = `edition-state.language`) and apply it alongside the reference file.
> It holds the judgment-class corrections the operator confirmed in earlier
> editions and Step 11 promoted — reading it *here*, while the draft is still
> being cleaned, is what makes a rule **prevent** the defect instead of merely
> catching it at Step 6.5. The shipped ban-list (rule 3) is the baseline and the
> user file extends it; where they disagree, the stricter wins. Absent the file
> (the default — it is opt-in, template at
> `${CLAUDE_PLUGIN_ROOT}/config/language-rules.template.md`), apply the baseline
> alone and say nothing.

**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):

1. **Threads** *(consistency)* — every thread opened in the ingress/body resolves
   by the conclusion; no dropped setups, no orphaned promises.
2. **Leder-takeaway** (rule 1) — ONE takeaway + ONE concrete action; cut references
   hard.
3. **Premiss→konklusjon-bue** (rule 2) — ingress-premise == conclusion-premise; the
   close grips and twists forward, never just summarizes.
4. **AI-slop-fraser** (rule 3) — strip the Norwegian ban-list on sight (report a
   removed-count).
5. **Generell, ikke etat-/person-spesifikk** (rule 4) — opportunities not
   provocations; ≤1 structural anchor.
6. **Formaterings-dose** (rule 5) — minimal; no PowerPoint-printout.
7. **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
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/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: contract-gate" line to
`<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the protocol
(Step 0) for `consistency-quality`, `--next "Step 4.5 — contract-gate"`.

```
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 4.5 — Contract-gate (deterministic §B/§C1 binding, BEFORE the AI sweeps).
```

## Step 4.5: Contract-gate — deterministic §B/§C1 binding (BEFORE the AI sweeps)

Before any AI gate touches the draft — fact-check (Step 5), editorial craft (Step
5.5), persona sweep (Step 6), headless review (Step 6.5) — run the **deterministic
contract-gate** over the consistency-passed draft. It is pure regex/literal matching
(no model in the loop): the same mechanical §B/§C1 violation is caught and fixed the
same way every time, so it **never reaches the operator at a judgment gate**. This is
the machine half of the "corrections that stick" goal — the cheap, deterministic pass
runs first so the costly AI gates spend their judgment on resonance and truth, not on
mechanical noise.

> **Truth source — §B/§C1 vs §C2.** The gate's ruleset is `scripts/contract-gate/
> src/rules.ts` (data, not code), bound **1:1** to the Maskinrommet skrivekontrakt
> **§E gate-bindings-manifest** by `ratify` (id ↔ ref ↔ severity bijection). It is the
> *mechanical* half of the contract (§B structure + §C1 mechanical gate); the *craft*
> half (§C2) is the `editorial-reviewer` at Step 5.5. This Step does not restate the
> rules — `rules.ts` is the single source of truth, and ratify proves it stays bound
> to the contract. Distinct from Step 4's `references/longform-quality-rules.md`,
> which is the §A/quality reference applied by reasoning.

> **Adopter note.** The gate is a KTG-internal tool: it needs the contract-gate deps
> (`scripts/contract-gate/node_modules` — run `npm install` there once) and, for
> ratify, the maskinrommet contract. If either is absent (a plain adopter ships
> neither), this Step is a **no-op** and the §B/§C1 discipline falls back to the Step 4
> quality-pass reasoning. It never blocks an adopter.

**Procedure:**

1. **Ratify the gate (invariant — confirm it is still bound to the contract).** Before
   trusting any verdict, confirm the ruleset is in 1:1 sync with the contract
   §E-manifest:
   ```
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate" && \
     node --import tsx src/cli.ts --ratify
   ```
   - **exit 0** → `rules.ts` bound 1:1 to §E → proceed.
   - **exit 1 (DRIFT)** → a **tooling/contract inconsistency** (a rule changed on one
     side only). This is NOT a draft fix — **escalate to the operator**; the §E
     accumulation procedure (promote → ratify) reconciles it. `[GATE]`
   - contract or deps absent → skip with a one-line note (adopter); the draft-gate
     below still runs if the deps are present.

2. **Gate the draft.** Run the gate over the edition draft. The gate runs from its own
   dir (where `tsx` lives), so pass the draft's **absolute** path:
   ```
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate" && \
     node --import tsx src/cli.ts "<absolutt sti til serie-mappe>/NN-utkast.md"
   ```
   - **exit 0** → PASS. Surface any **WARN** violations as direction (operator
     judgment — WARN never blocks).
   - **exit 1** → BLOCKED. Show each BLOCK violation (ruleId · ref · line · message),
     fix **by tightening** (the Step 4 rule still holds — swap weaker for sharper, do
     not expand), and **re-run until exit 0**. `[GATE]`
   - **exit 2** → usage error (e.g. wrong path) — diagnose; do not treat as a content
     flag.

3. **Persist + checkpoint state.** Set `currentPhase: "contract-gate"` in
   `edition-state.json` and write a "contract-gate passed → next: fact-check sweep"
   line to `<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the
   protocol (Step 0) for `contract-gate`, `--next "Step 5 — fact-check sweep"`.

```
Contract-gate complete.
- Ratify: <in sync (10 rules 1:1) / DRIFT escalated / skipped (adopter)>
- BLOCK violations: <0 / fixed N by tightening>
- WARN violations: <N surfaced as direction>
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:**

1. **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/unverified` items carried forward from Step 2 — they enter this
   sweep automatically.

2. **Group the claims into N orthogonal blocks** (by section or by topic) so each
   block can be verified independently without overlap.

3. **Fan out in parallel — issue all N `fact-checker` calls in a SINGLE message**
   (multiple `Task` tool-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 the
   `Task` tool and silently degrade to guessing (the same gate as Step 2).

4. **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]`

5. **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-checker` call 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.

6. **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),
   set `currentPhase: "factcheck-sweep"`, and write a "fact-check complete → next:
   persona sweep (BEFORE lock)" line to `<serie>/STATE.md` (overwrite).
   **↳ Phase transition** — run the protocol (Step 0) for `factcheck-sweep`,
   `--next "Step 5.5 — editorial review"`.

```
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).
```

> **Post-factcheck fact-invariant (Endring N9 / C-6) — deterministic recheck.**
> Once this sweep clears, the article's facts are gated — but every later edit can
> silently reintroduce an unchecked fact: an editorial fold-in (5.5), a persona
> tightening (6), a headless fold-in (6.5), an annotation pass (7). So the
> re-verification trigger is **deterministic — not a judgment call:** if a fold-in's
> diff touches any sentence containing **numbers, names, or quotes**, it fires a
> mandatory **scoped fact-reviewer** mini-recheck of *exactly those changed
> sentences* (`subagent_type: linkedin-studio:fact-reviewer`, inputs limited to the
> touched sentences + the sources they cite) before the phase advances. This
> replaces the old "re-run only if it felt substantive" discretion and closes the
> pivot-heuristic blind spot: a swapped statistic that adds no section and stays
> under the +20 % word-count threshold never trips `/linkedin:pivot`, but it *does*
> trip this trigger. A verified fact stays verified only if every number, name, or
> quote that changes after the gate is re-checked. `[GATE]`

## 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.md`
> rule 8). §C2 is the source of truth: a change on either side must be mirrored to
> the other so the two never drift.

**Procedure:**

1. **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.

2. **Run `editorial-reviewer` (single foreground `Task` call).** Invoke
   `subagent_type: linkedin-studio:editorial-reviewer` from 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.

3. **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:
   1. Write the agent's report to `<serie>/NN-editorial-review.md` (NN = the same
      zero-padded edition number; new suffix, a first-class artifact alongside
      `NN-skjelett.md`).
   2. `SendUserFile` it (else a markdown `file://` link) so KTG can read the flags
      sorted BLOCK → REWORK → NICE and **decide which fold in**.
   3. **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]`

4. **Fold in the approved flags by tightening, → v(n+1).** Fold the flags KTG
   approved into `<serie>/NN-utkast.md` **by tightening** (rule 6 of
   `references/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.

   **↳ Fold-in capture.** A flag the operator approved that names a *recurring*
   craft defect (an em-dash habit, a phrase that keeps returning, a conclusion
   shape that keeps overloading) appends one `articles.NN.foldIns[]` row —
   correction near-verbatim, `trigger` = the flag's category (P1–P5 / A1–A5),
   `decision: "pending"`. This is the queue **Step 11** promotes: mechanical
   flags become deterministic gate rules that die at Step 4.5 next edition,
   judgment flags become checklist lines. A flag about *this draft's* specific
   sentence is not a rule.

5. **Optionally re-run `editorial-reviewer` on 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.

6. **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"` in `edition-state.json` (the marker
     that Step 5.5 is complete and the operator-gate has passed).
   - **↳ Phase transition** — run the protocol (Step 0) for `editorial-review`,
     `--next "Step 6 — persona sweep (pre-lock)"`.
   - Write an "editorial review complete (craft clean) → next: persona sweep
     (BEFORE lock)" line to `<serie>/STATE.md` (overwrite).

```
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:**

1. **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).

2. **Fan out one `persona-reviewer` call per persona, in parallel** — issue them
   in a SINGLE message (multiple `Task` tool-uses, `subagent_type:
   linkedin-studio:persona-reviewer`), from THIS command layer in the
   foreground (principle 4).
   Pass each call its persona name and **`mode: 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.

3. **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]`

4. **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-reviewer` calls** 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.

5. **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). **Also record the cleared
   draft's word count** as `articles.NN.personaSweep.resonance.wordCount`
   (`wc -w <serie>/NN-utkast.md`) — this is the **baseline** the pivot-detection
   heuristic (Step 8 / `/linkedin:pivot`) compares against to catch a late pivot.
   Set `currentPhase: "persona-sweep-prelock"`, and write a "persona sweep
   PASS (primær JA) → next: headless adversarial review (Step 6.5, BEFORE lock)"
   line to `<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the
   protocol (Step 0) for `persona-sweep-prelock`,
   `--next "Step 6.5 — headless adversarial review"`.

```
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>
- Cleared word count recorded: <N> (pivot-detection baseline)
Gate: [PASS — primær JA, ready to lock]   (else REWORK/BLOCK)
Next: Step 6.5 — Headless adversarial review (cold review package, BEFORE lock).
```

---

## Step 6.5: Headless adversarial review — cold review package, BEFORE lock

The persona-passed draft now faces a **cold, adversarial review package**: five
independent archetypes read the *finished* text with **none of this session's
context** — no version history, no "deliberately omitted" list, no pivot
narrative, no record of who approved what. They are the independence layer the
in-session gates (`fact-checker` Step 5, `editorial-reviewer` Step 5.5,
`persona-reviewer` Step 6) structurally cannot be, because those share the
drafting session's framing-bias.

> **Why this step exists (Del 4 diagnosis, Endring 9).** In Del 4 the editor and
> the persona sweep ran in the same session as drafting. They shared the
> conversation history, so they carried framing-bias and were not adversarial:
> the resonance sweep effectively judged an early version, editor-approval was
> single-source, and the fact-check was post-hoc relative to a late pivot. This
> step answers KTG's question — *how do I start sessions with no context from the
> main session, to review both content and language?* — by running the review on
> a **frozen** draft through agents that refuse session framing.

> **Order assertion (enforced).** Step 6.5 runs AFTER the in-session persona
> sweep (Step 6) and BEFORE lock (Step 8), on a **frozen snapshot** of the
> publish-ready draft. Any flag the operator folds in is re-touched **before**
> lock — never reopen a locked text (the cardinal Seres lesson). If a pivot
> changes the draft after this gate, `/linkedin:pivot` re-opens the pipeline and
> this package re-runs on the pivoted version. `[GATE]`

**Relationship to the in-session gates (deliberate redundancy).** `fact-reviewer`
overlaps `fact-checker` and `language-reviewer` overlaps `editorial-reviewer`'s
prose axis **on purpose** — the cold re-read catches what the framing-biased
in-session pass hid. `content-reviewer` is genuinely new (argument integrity,
which no in-session gate measures). Do NOT collapse the pairs.

**Procedure** (this is the same package the standalone `/linkedin:headless-review`
command runs — see `commands/headless-review.md` for the full cold contract):

1. **Freeze the draft.** Snapshot the persona-passed `NN-utkast.md` so the
   reviewers judge a stable artifact and the report names exactly what was read:
   ```bash
   cd <serie-mappe> && cp NN-utkast.md "review/NN-frozen-$(date +%Y%m%d-%H%M).md"
   ```
   Record the frozen path; pass *that* path (not the live draft) to every reviewer.

2. **Resolve the cold inputs.** The **review language** (`edition-state.language`,
   default `en` — tells `language-reviewer` and `voice-scrubber` which language's
   rules to grade against; Norwegian-specific checks fire only when `language: no`),
   the writing contract *if it ships* (`<serie>/../../docs/skrivekontrakt.md` →
   plugin mirror → `references/longform-quality-rules.md`; absent for an adopter,
   the craft agents' in-tree checklists are self-contained), and the active
   personas (`articles.NN.personas`, primær identified). Nothing else.

3. **Fan out the five archetypes in parallel** — issue them in a SINGLE message
   (multiple `Task` tool-uses) from THIS command layer in the foreground
   (principle 4), `subagent_type` namespaced:
   - `linkedin-studio:content-reviewer` — argument integrity (C1–C5)
   - `linkedin-studio:language-reviewer` — language quality (L1–L5; grades against `edition-state.language`, Norwegian-specific rules when `language: no`)
   - `linkedin-studio:fact-reviewer` — cold re-verification (F1–F4, 🔴/🟡/🟢, incl. pivot premises)
   - `linkedin-studio:persona-reviewer` `mode: resonans` — **one call per active persona**
   - `linkedin-studio:persona-reviewer` `mode: konverter` — **primær only** (hook)

   Each call's prompt carries ONLY the cold-contract inputs (frozen draft path,
   `language`, contract path if it ships, persona for the persona modes) + the
   instruction to ignore any
   framing about prior versions / cuts / pivots. **Never** paste history or
   summarize "what we changed" into a reviewer prompt — that is the context
   pollution the package exists to eliminate.

   > **Maximum-independence path — no session switch, no paste-back (N14).** The
   > strongest isolation that costs the operator nothing is the **background
   > run**: `/linkedin:headless-review --draft <frozen> --article NN --background`
   > spawns the package in a background agent with its own context window, which
   > writes the consolidated report to disk. This session then **reads that file**
   > — it never sees the reviewers' reasoning, and the reviewers never see this
   > session's drafting history. Same isolation as a fresh session, without the
   > manual copy-paste seam that used to sit here. Run it, then `Read` the report
   > path and continue at step 5 below. The inline fan-out above stays the
   > fallback when background execution is unavailable; both use the same agents.

4. **Degradation gate.** Confirm each call returned structured, populated output
   (real flags / a verification log), not empty or a hedged paragraph. Re-run any
   degraded archetype — do not proceed with a missing reviewer. `[GATE]`

5. **Consolidate + surface (`SendUserFile`).** Merge the returns into one report
   at `<serie>/review/NN-headless-<stamp>.md`, grouped by archetype → severity,
   with a cross-archetype signal line. **Mark ⚑ converged** any passage two
   independent cold reviewers flag — independent agreement with no shared session
   is the package's strongest signal. `SendUserFile` it (else a `file://` link)
   so KTG decides which flags fold in. You do not resolve flags or pick winners;
   the operator gates. `[OPERATØR]`

6. **Fold in by tightening, → v(n+1).** Fold the flags KTG approved into
   `NN-utkast.md` **by tightening** (rule 6 — close the gap, hold the length flat).
   The editor (this session) holds the pen; never paste a reviewer's direction as
   copy. If the fold-in was substantive, re-run the affected archetype on v(n+1).
   All of this happens **before** lock, so the body is never reopened post-lock.
   **Post-factcheck fact-invariant applies (C-6):** independently of the substantive
   judgment, if any folded edit's diff touches a sentence with **numbers, names, or
   quotes**, fire the deterministic **scoped fact-reviewer** mini-recheck on those
   sentences (see the invariant after Step 5) before advancing.

   **↳ Fold-in capture.** The cold reviewers are the richest source of rules the
   in-session gates never see — a language flag two independent readers converge
   on is almost always a standing rule, not a one-off. Append one
   `articles.NN.foldIns[]` row per approved flag that generalizes (`decision:
   "pending"`, `trigger` = the C/L/F category, ⚑ converged noted), and let
   **Step 11** decide mechanical vs judgment. Do not classify here.

7. **Persist + checkpoint state.** Record the run in `edition-state.json` →
   `articles.NN.headlessReview` (`frozenDraft`, per-reviewer `{reportPath,
   summary, status}`, `consolidatedReport`, `foldedIn`/`waived`, `status:
   "folded"`), set `currentPhase: "headless-review"`, and write a "headless review
   complete (cold, converged flags folded) → next: annotation/lock" line to
   `<serie>/STATE.md` (overwrite). **↳ Phase transition** — run the protocol
   (Step 0) for `headless-review`, `--next "Step 7/7.5 — annotation + visual assets"`.

```
Headless adversarial review (cold, BEFORE lock) — complete.
- Frozen draft: <serie>/review/NN-frozen-<stamp>.md
- Archetypes: content · language · fact · persona-resonance (<N> personas) · persona-conversion (primær)
- Converged flags (independent agreement): <N>
- BLOCK/🔴: <N> → folded/​waived   REWORK: <N>   primær resonance: JA   conversion: JA
- Surfaced to operator: <serie>/review/NN-headless-<stamp>.md (via SendUserFile)   [OPERATØR]
- Folded in (by tightening, pre-lock): <N>   Waived: <N>
Gate: [PASS — operator approved, body re-touched pre-lock]
Next: Step 7 — Annotation (optional), then Step 7.5 — Visual assets, then Step 8 — LOCK.
```

---

## 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:**

1. **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.).

2. **Render the review HTML.** `render/build-html.mjs` writes to `./review/`
   relative to the *current working directory*, so run it **with cwd = the
   series folder** (not the plugin). Pass the draft file:

   ```bash
   cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-utkast.md
   ```

   **Check the exit code (N3 — do not assume success).** As of S14/F7 the exit
   code is authoritative: `build-html.mjs` exits **non-zero when zero HTML files
   were produced** (e.g. a typo'd/missing filename — it prints `Fant ikke:` then
   `Ingen 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.html` is present, not just exit 0. Report the
   stderr and do NOT advance the phase if the file is missing.

3. **Hand off the link.** On success the script prints `Skrev <path> (<KB>)`.
   Surface `<serie-mappe>/review/NN-utkast.html` as a `file://` 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.
   **Post-factcheck fact-invariant applies (C-6):** any annotation edit whose diff
   touches a sentence with **numbers, names, or quotes** fires the deterministic
   **scoped fact-reviewer** mini-recheck on those sentences (see the invariant after
   Step 5), regardless of whether the edit felt substantive.

4. **Persist.** Set `currentPhase: "annotation"` in `edition-state.json` and
   write an "annotation rendered (optional) → next: visual assets" line to
   `<serie>/STATE.md` (overwrite). If skipped, note "annotation skipped" and move on.
   **↳ Phase transition** — run the protocol (Step 0) for `annotation`,
   `--next "Step 7.5 — visual assets"`. If the step was skipped, skip the
   transition too: an unrun phase must not appear in the lead-time log.

```
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) reads
> `linkedin/NN/cover.png` (fixed filename) and the edition-config
> `coverCredit` + `captions[NN]` when it builds `POST.html`. If images were
> generated *after* lock, `POST.html` would 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.mjs` embeds 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>.png` at all. Inline figures are referenced in the draft markdown
> (`![alt](linkedin/NN/figN.png)`) 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):**

1. **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.

2. **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.brief` and
   `…figures[].brief`.

3. **Generate — three routes, no lock-in.** The interface is pluggable (path-in /
   path-out). Route by what the image *is*:
   - **Coded route — PRIMARY for data figures** (charts, diagrams, before/after
     comparisons — anything whose content is real numbers or real structure).
     Author the figure as SVG/HTML per
     `${CLAUDE_PLUGIN_ROOT}/references/figure-design-guidelines.md` (design
     rules + brand-token convention), then render to a candidate:
     ```bash
     node "${CLAUDE_PLUGIN_ROOT}/render/build-figur.mjs" linkedin/NN/fig<N>.svg --target article --out linkedin/NN/fig<N>-kandidat.png
     ```
     Precision and reproducibility beat generative output for data — numbers,
     labels, and proportions are exact, and the figure re-renders identically
     after a correction. Record route `"coded"`.
   - **Generative route — `mcp__mcp-image__generate_image`** (Nano Banana Pro /
     Gemini 3 Pro Image) for **illustrative** images (cover art, mood,
     metaphor). Write candidates to `linkedin/NN/cover-v<N>-kandidat.png` (and
     `fig<N>-kandidat.png` for 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.png` the operator drops in; no
     tool is mandated. Record route `"external"`. (The raw file may then be
     cropped/retouched into a candidate, or approved directly.)

4. **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):
   1. Collect the candidate paths (`cover-v<N>-kandidat.png`, any external
      `cover-raw.png`).
   2. `SendUserFile` them with a one-line caption tying each to its brief, so
      the operator can compare side by side.
   3. The operator either **approves one** or **asks for more attempts** (loop
      back to step 3 — generate the next `cover-v<N+1>-kandidat.png`).
   4. On approval, **copy the approved candidate to the fixed name** that
      `build-linkedin.mjs` reads — `cover.png`:
      ```bash
      cd <serie-mappe> && cp "linkedin/NN/cover-v<N>-kandidat.png" "linkedin/NN/cover.png"
      ```
      (Same pattern for figures: approved → `linkedin/NN/fig<N>.png`.) Confirm
      `linkedin/NN/cover.png` exists before advancing.

   Do not advance to Step 8 without an approved `cover.png`. `[OPERATØR]`

5. **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 fields `build-linkedin.mjs` already reads in Step 8.

**Procedure (`carousel` format — branch):**

6. **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 — …` grammar
   `build-carousel.mjs` parses), then render with the plugin-owned renderer
   (cwd = series folder):
   ```bash
   cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-carousel.mjs" linkedin/NN/carousel.md
   ```
   `build-carousel.mjs` writes `linkedin/NN/carousel.html` always and
   `linkedin/NN/carousel.pdf` when `weasyprint` is 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`, else
   `carousel.html`) to the operator via `SendUserFile` for the same approve /
   regenerate gate as step 4. Record this under
   `articles.NN.visualAssets.carousel = { source, pdf, status }` and set
   `format: "carousel"`. No `cover.png` is required for a pure carousel edition
   (its `POST.html` carousel block references `linkedin/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.status `approved`,
  cover.approved `"cover.png"`, candidates list, figures, carousel) in
  `edition-state.json`.
- Set `currentPhase: "visual-assets"` in `edition-state.json` (the marker that
  Step 7.5 is complete and the gate has passed).
- **↳ Phase transition** — run the protocol (Step 0) for `visual-assets`,
  `--next "Step 8 — LOCK + delivery"`.
- 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: coded | 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 the headless adversarial review (Step 6.5), and BEFORE the
> hook/conversion gate (Step 9). Reversing lock and the pre-lock sweeps
> 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:**

1. **Confirm lock preconditions.** In `edition-state.json`: the article's
   `factcheckLog` has no open 🔴, `personaSweep.resonance` recorded a primær JA,
   `headlessReview.status` is `folded` (or `run` with no open BLOCK/🔴 the
   operator left unaddressed — Step 6.5), and `visualAssets` is gated — for
   `standard` format the approved `linkedin/NN/cover.png` exists (Step 7.5); for
   `carousel` format the approved `carousel.pdf`/`carousel.html` exists. If any is
   missing, STOP — return to the relevant step (5/6/6.5/7.5). Do not lock past an
   open gate.

   **Pivot-detection gate (Endring 9c — enforced).** Before locking, compare the
   current draft against the version that last cleared Step 6:
   - **word count:** `wc -w <serie>/NN-utkast.md` vs
     `articles.NN.personaSweep.resonance.wordCount` (the recorded baseline);
   - **new sections:** top-level headings now present that were absent then
     (`grep -c '^## '` delta is a fair proxy).

   If the draft has drifted **> 20 % in word count OR gained > 2 sections** since
   Step 6 cleared, the text pivoted after its gates — **STOP, do not lock.** Run
   `/linkedin:pivot --article NN --reason "<what changed>"`, which re-opens the
   pipeline so fact-check (5) → editorial (5.5) → persona (6) → headless (6.5)
   re-run on the pivoted version. Likewise, if `articles.NN.pivots[]` has an entry
   whose `gatesToRerun` gates have not since re-passed, STOP — the pivot's
   re-review is incomplete. (This is exactly the Del 4 v8→v11 case: +42 %, 2 new
   sections → the gate would have fired and forced the re-sweep.) `[GATE]`

2. **Confirm the delivery inputs in the series folder.**
   `render/build-linkedin.mjs` reads, 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
     the `samle` post). Template: `${CLAUDE_PLUGIN_ROOT}/config/edition-delingstekst.template.md`,
     whose header documents the exact `## Del N —` / `## Samle` section grammar the
     renderer parses.

   Both inputs are **optional and graceful** (renderer degrades, does not throw):
   a missing or malformed `edition-config.json` falls back to empty defaults, and a
   missing `edition-delingstekst.md` yields no distribution copy while the article
   `POST.html` still builds. Provide both for a complete delivery — the
   distribution hook is what Step 9 gates.

3. **Render POST.html.** Run with **cwd = the series folder** (the script
   resolves `linkedin/` from cwd and writes `linkedin/NN/POST.html`). The draft
   filename MUST keep its two-digit `NN` prefix or the script skips it
   (`↷ hopper over … (ikke NN-prefiks)`):

   ```bash
   cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs" NN-utkast.md
   ```

   **Check the exit code (N3 — do not assume success).** Confirm exit 0 AND
   that `linkedin/NN/POST.html` now exists (the `✓ linkedin/NN/POST.html`
   line). 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.

4. **Lock the article.** Set `locked: true`, `status: "locked"`, and
   `currentPhase: "lock-delivery"` in `edition-state.json`. Surface
   `<serie-mappe>/linkedin/NN/POST.html` as a `file://` link. From here the
   body is frozen — Step 9 may only adjust the distribution hook, never the
   locked edition text.
   **↳ Phase transition** — run the protocol (Step 0) for `lock-delivery`,
   `--next "Step 9 — hook/conversion gate"`.

5. **Auto-gull — grow the gold corpus (A2-F2).** A locked edition is now
   published-grade voice signal, so append it to the gold corpus the chronicle
   voice profile is distilled from:
   - Record the locked body (`NN-utkast.md`) as a new record under
     `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/ingest/published/`,
     tagged **`provenance=published`** (never `ai-draft`) with this edition's
     `language` and format in its frontmatter, so `voice-trainer`'s
     chronicle-voice-profile distillation (per language/format) picks it up.
   - This closes the loop the model-collapse fence protects: the corpus the
     chronicle voice is learned from is the author's OWN locked editions, growing
     one edition at a time — never the engine's drafts. The next edition's Step 0
     therefore binds to a sharper profile without any manual corpus curation.

6. **Serie-minne — record what this edition spent.** The reader-facing twin of
   auto-gull: auto-gull grows the *voice* corpus, this grows the *series memory*
   that the next edition's Step 2.5 destillat-sjekk is checked against. Both
   writes happen at LOCK, so they mean **published**, never merely considered —
   an abandoned draft leaves no trace.

   - **Series distillate.** Extract the narrative units this edition actually
     spent — the anecdotes told, the arguments argued, the hooks used — **in the
     operator's own phrasing**, not as a summary (the check compares text, so a
     paraphrased summary would compare the summariser's words, not the reader's
     experience). Write them to a JSON extract and fold it in:

     ```bash
     cd "${CLAUDE_PLUGIN_ROOT}/scripts/editions" && \
       node --import tsx src/cli.ts distil-append \
         --distillate "<abs>/linkedin/series-distillate.json" \
         --series "<slug>" --extract "<abs>/NN-destillat.json"
     ```

     Extract shape: `{editionId, title, lockedAt, anecdotes[], arguments[],
     hooks[]}`. A re-lock after `/linkedin:pivot` **replaces** that edition's
     entry rather than duplicating it — the reader still only met it once.

   - **Specifics-bank usage log.** Stamp the lived material this edition consumed
     with «used in edition NN», so the next Step 1.5 query surfaces re-use
     instead of silently encouraging it (the bank's dedupe unions tags across
     editions by design — without this log, re-surfacing the same specific is
     invisible):

     ```bash
     cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
       node --import tsx src/cli.ts record-usage \
         --edition "<abs>/linkedin/edition-state.json" --edition-id "<slug>/NN"
     ```

     The ids are read from the article's bound slot-map, so only material the
     edition actually leaned on is stamped; `abstrakt`/`ekstern` slots consume no
     bank material. Idempotent — a pivot re-lock does not double-count.

   - Deps/contract absent (adopter — Step 1.5 was a no-op, or `npm install` never
     run) → skip both with a one-line note, like the Step 2.5 gates. The lock
     itself never depends on this bookkeeping.

```
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
- Serie-minne: distillate +1 edition (<N> units) · bank usage stamped on <N> specific(s)   (else: skipped — deps absent)
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 feed. Before it ships, the
**feed-facing surfaces** the reader actually meets — the **delingstekst hook**,
the **samle post**, the **carousel slide text**, and the **edition title** —
face two final gates: a **randsone fact + language gate** (so nothing false or
clumsy reaches the feed in the most-read, most-requoted copy — C-4/C-9) and the
**hook conversion binary** (would the primær persona, scrolling, actually stop
and open this?). Both operate on **distribution copy only** — the post-lock
sweep, distinct from the pre-lock resonance sweep (Step 6), never the locked
body. Until N9 only the hook's click-worth was judged; the hook's *truth and
language*, the samle post, the carousel slides and the title reached the feed
ungated.

> **Order assertion (enforced).** This gate (Step 9) runs AFTER lock (Step 8) — it
> operates on the **distribution copy** of an already-locked edition and must never
> reopen the locked body. Both the randsone fact/language gate and the hook binary
> revise only distribution surfaces (delingstekst, samle, carousel text,
> distribution title) and re-render via Step 8's `build-linkedin.mjs` /
> `build-carousel.mjs` — never the edition text. A defect that traces into the
> **locked article body or its canonical title** is a pivot (STOP →
> `/linkedin:pivot`), not an in-place edit. This ordering is the inverse of the
> resonance sweep, which runs BEFORE lock — keeping the two apart is what fixed the
> Seres process. `[GATE]`

**Why Step 9 (placement, documented per plan N9).** The randsone gate lives here,
not earlier, because Step 9 is the **first phase where all four surfaces exist**:
the carousel slide text is authored in Step 7.5 (after the last content gate 6.5),
and the delingstekst + samle post exist only from Step 8. Step 9 already operates on
post-lock distribution copy, so gating and revising these surfaces here respects the
lock invariant without breaking phase order.

**Procedure:**

1. **Collect the four distribution surfaces.** These are what the reader meets in
   the feed — and until N9 only the first was judged, and only for click-worth:
   - the **delingstekst hook** — the first two lines of the edition's entry in
     `linkedin/edition-delingstekst.md` (what the reader sees before "…see more");
   - the **samle post** — the `## Samle` section, if shipping the collected post;
   - the **carousel slide text** — `linkedin/NN/carousel.md`, for a carousel-format
     edition (authored in Step 7.5, after the last content gate — so it has never
     been fact- or language-checked);
   - the **edition title** — the edition's own headline (its distribution / feed
     title), never strength-gated before now.

2. **Randsone fact + language gate (scoped — C-4/C-9).** Run two scoped reviewers
   over *only* these surfaces (never the locked body), issued from THIS command
   layer in the foreground, `subagent_type` namespaced:
   - `linkedin-studio:fact-reviewer` — **F1** (verifiable claims) + **F3** (number
     attribution) on any figure, named case, or quotation appearing in the four
     surfaces. A hook or slide that asserts a number the body never sourced is a 🔴.
     (WebFetch the source for any quoted or exact figure — C-7.)
   - `linkedin-studio:language-reviewer` — **L1** (repetition) + **L2** (anglicisms
     / stiff register), graded against `edition-state.language`. The hook is the
     most-requoted line the edition has; a clumsy or anglicised hook is an L-BLOCK.
   Pass each reviewer only the four surfaces + the sources they cite + `language`.
   This is a **scoped mini-pass**, not a re-review of the article. `[GATE]`

3. **Title-strength gate (C-9).** The edition's own title was never strength-gated —
   only the hook's two lines were. Judge the **title-strength** of the **edition
   title** here: does it earn the open on its own, or is it a generic label? The
   strongest candidate becomes the distribution title. The verdict is **binding on
   the distribution title** (sharpen a weak one in place) and **advisory on the
   canonical (locked) title** — changing that is a body edit → STOP, run
   `/linkedin:pivot`; never edit the locked title in place.

4. **Resolve the randsone gate.**
   - **Fact 🔴 / L-BLOCK on a distribution surface** → revise that surface **by
     tightening** (hook, samle, carousel text, or distribution title — all
     distribution copy), **re-render** the affected artifact (POST.html via Step 8's
     `build-linkedin.mjs`; carousel via `build-carousel.mjs`), and re-run the scoped
     pass on what changed. The locked body stays frozen. Loop until clean.
   - **A defect that traces into the locked body or canonical title** → it is a
     pivot: STOP, run `/linkedin:pivot`. The randsone gate never reopens the locked
     body in place — the same invariant as the conversion loop. `[GATE]`

5. **Run the hook conversion binary** (`subagent_type:
   linkedin-studio:persona-reviewer`, **`mode: konverter`** — the after-lock,
   hook-only mode, NOT resonans) for the **primær** persona only. The agent returns
   a single binary verdict, **JA / NEI**, on «would YOU click?» — no axis scoring,
   no flags, no rewritten copy (the jury judges, the editor writes).
   - **JA** → the hook converts. Proceed to Step 10.
   - **NEI** → revise the **delingstekst hook only** (sharpen the krok by tightening,
     body frozen), **re-render POST.html** via `build-linkedin.mjs`, and re-run the
     conversion check. Loop until JA. `[GATE]`

6. **Persist.** Record the randsone-gate result in `edition-state.json` →
   `articles.NN.randsoneGate` (per-surface fact/language verdicts + title-strength +
   what was revised) and the conversion verdict in
   `articles.NN.personaSweep.conversion`; set `currentPhase: "hook-conversion-gate"`.
   **↳ Phase transition** — run the protocol (Step 0) for `hook-conversion-gate`,
   `--next "Step 10 — scheduling"`.

```
Randsone + conversion gate (post-lock, distribution copy only).
- Surfaces gated: delingstekst hook · samle post · carousel slide text · edition title
- Fact (F1/F3): <N> 🔴 → resolved (or pivot if it traces to the body)
- Language (L1/L2): <N> BLOCK → tightened
- Title-strength: <verdict> (distribution title sharpened; canonical-title weakness → pivot)
- Conversion (primær, konverter): JA   (else: NEI — revise hook, re-render, re-check)
- Body: untouched (locked in Step 8)
Gate: [PASS — randsone clean + JA]   (else loop on distribution copy, or pivot)
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:**

1. **Pick the slot — default to the next open one (N13).** Read the operator's
   publishing grid and take the first slot nothing covers yet, so scheduling is
   one confirmation rather than an interview:

   ```bash
   node --input-type=module -e "
   import { nextFreeSlot, formatSlot, readSlots } from '${CLAUDE_PLUGIN_ROOT}/hooks/scripts/slots.mjs';
   const s = readSlots().slots.length ? nextFreeSlot({ days: 28 }) : null;
   console.log(s ? s.date + ' ' + s.time + '  (' + formatSlot(s) + ')' : 'NO SLOT');
   "
   ```

   Propose that date/time and ask **once** for a yes/no. `NO SLOT` means the grid
   is unconfigured or every slot in the next 28 days is taken — then fall back to
   the edition-config calendar / series brief, and ask for the slot outright
   (Step 1's calibration may already have settled it). A slot the operator
   overrides is simply used as given; the default is a starting point, never a
   constraint.

2. **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 locked `linkedin/NN/POST.html`
   - `format` — `newsletter` (so calendar/reporting can distinguish long-form)
   - `hookPreview` — the conversion-passed distribution hook (first ~80 chars)

   ```bash
   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 `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/drafts/queue.json` with `status:
   "scheduled"` and returns the new entry.

3. **Close the discovery loop (auto-`act` — N7).** If `articles.NN.sourceTrendId`
   is set (this edition was started from a `/linkedin:trends` candidate), flip that
   trend's store status to `acted` so it drops out of future discovery triage — the
   discovery→production loop closes deterministically, not from memory:

   ```bash
   cd "${CLAUDE_PLUGIN_ROOT}/scripts/trends" && node --import tsx src/cli.ts act --id "<sourceTrendId>"
   ```

   A present id flips to `acted` (exit 0). If the id is no longer in the store the
   call errors (all-miss → exit 2, store untouched) — that is **not fatal** to
   scheduling (the edition is already registered in step 2): note it and proceed.
   Skip this step entirely when `sourceTrendId` is `null` (a manually-started
   edition has no trend to close).

4. **Persist + close the edition.** Set the article's `status: "scheduled"`,
   `scheduled: "<YYYY-MM-DD HH:MM>"`, and `currentPhase: "scheduling"` in
   `edition-state.json`. Write a closing "edition scheduled → mark live via
   `/linkedin:calendar` on <date>" line to `<serie>/STATE.md` (overwrite). The
   pipeline is complete.
   **↳ Phase transition** — run the protocol (Step 0) for `scheduling`, this time
   **with the slot** (`--slot "<YYYY-MM-DD HH:MM>"`, the one confirmed in step 1)
   and `--next "published — mark live via /linkedin:calendar"`. The slot on the row
   is what makes the edition visible as *coverage* of that publishing slot, so the
   next session stops offering it as open. Then **close the register row** (this
   step alone does):

   ```bash
   node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-complete \
     --series "<slug>" --edition "<NN>"
   ```

   It prints the measured lead time (first transition → completion) — the A1-12
   number the cadence work is calibrated on. An edition re-opened later by
   `/linkedin:pivot` simply re-activates the same row on its next transition; the
   original `startedAt` is kept, so the clock reflects the real elapsed production
   time rather than restarting.

```
Scheduling.
- Queue entry: <series-slug>-NN → ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/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).
```

---

## Step 11: Retro — make this edition's corrections stick (≤5 min, AFTER scheduling)

The edition is delivered. This step is not about it — it is about **the next
one.** Every correction the operator made during production is sitting in
`articles.NN.foldIns[]` marked `pending`; if the session ends here, those
corrections live only in a transcript and get re-discovered in three editions'
time. Step 11 empties that queue into permanent, enforced homes.

> **Why AFTER scheduling, and why capped.** The retro is cheap only if it is
> short: three fixed jobs, one operator pass, no open-ended reflection. It runs
> after Step 10 so nothing about shipping waits on it, and it is the last thing
> the edition does — the pipeline is not complete until the loop is closed.
> The full model (capture → classify → promote → enforce) is documented
> domain-generally in `${CLAUDE_PLUGIN_ROOT}/references/fold-in-loop.md`. `[OPERATØR]`

**Procedure — three jobs, in order:**

1. **Promote the fold-in queue (the operator gates each row).** Read every
   `articles.NN.foldIns[]` row with `decision: "pending"`. For each, propose a
   `classification` and ask for a **JA / NEI** — one compact list, not one
   question per row:

   - **`mechanical-block` / `mechanical-warn`** — provable by string, regex, or
     count with ~zero false positives. **JA →** promote **atomically**: add the
     rule to `scripts/contract-gate/src/rules.ts`, the matching row to the
     writing contract, and the manifest row that binds them; set the fold-in's
     `decision: "promoted"` and `ruleId`. Then prove it:

     ```bash
     cd "${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate" && node --import tsx src/cli.ts --ratify
     ```

     **Red ratify means the promotion did not happen** — rules and contract are
     out of sync, and a gate enforcing something undocumented is exactly the
     drift `ratify` exists to refuse. Fix it before recording the promotion.
     From the next edition, that correction dies at **Step 4.5**, before any
     human reads the draft.
   - **`judgment`** — real editorial discretion, no gate can prove it. **JA →**
     append it to the operator's accumulating language file,
     `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules/<lang>.md`
     (`<lang>` = `edition-state.language`; template
     `${CLAUDE_PLUGIN_ROOT}/config/language-rules.template.md`), under `## Rules`,
     with the date and this edition number. That file is read by **Step 4** while
     the next draft is being cleaned *and* by `language-reviewer` at Step 6.5 — so
     the rule prevents the defect as well as catching it. If the file does not
     exist, say so once and offer the one-line copy from the template; never
     create it silently.
   - **NEI** → `decision: "rejected"`, **kept** — "we considered this and said
     no" is itself worth not re-discovering. Never delete a fold-in row.

   **When unsure, classify as `judgment`.** A mechanical rule with false
   positives trains the operator to ignore the gate, and a gate nobody trusts
   enforces nothing.

2. **Effort summary — read the measurement, do not re-estimate it.** The numbers
   already exist in `articles.NN.phaseLog` (written deterministically at every
   phase transition since N12). Report exactly three things:

   ```bash
   node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-list --all
   ```

   - **transitions** — how many phases this edition logged.
   - **recurred phases** — any phase appearing more than once in `phaseLog`: a
     gate that had to re-run, i.e. where the rounds actually went. This is the
     signal worth reading; a phase that recurs across several editions is a
     process problem, not an edition problem.
   - **lead time** — first transition → `scheduling`, straight from the log.

   No new measuring apparatus, no stopwatch, no asking the operator how long it
   felt. If `phaseLog` is absent (an edition started before N12), say
   "not measured" and move on — never estimate a number and present it as data.

3. **One process-friction question — asked once, answered by them.** Ask exactly
   one: *"Any process friction from this edition worth recording?"* Take the
   answer, hand it back in a form they can paste, and stop. **The plugin must
   never write into the operator's own notes, register, or repo** — their record
   is theirs, and a tool that files entries in someone else's log on their behalf
   is a tool they stop trusting. One question, no follow-ups, no second round.

   While the queue is in view, name the **other accumulation silos** so the
   operator can see the whole picture in one place — specifics-bank (Step 1.5 /
   Step 8), brain-consolidate (Step 8), the voice drift log (`voice-scrubber`),
   and A/B learnings (`/linkedin:ab-test`). A retro that empties one queue while
   the others fill silently teaches the operator that everything is empty.

4. **Persist + close the loop.** Record the retro in `edition-state.json` →
   `articles.NN.retro` (`promoted`, `rejected`, `ruleIds`,
   `languageRulesAppended`, `effort`, `frictionNoted`, `completedAt`) and set
   `currentPhase: "retro"`.
   **↳ Phase transition** — run the protocol (Step 0) for `retro`,
   `--next "edition closed — corrections promoted"`. This appends the `retro`
   entry to `phaseLog` **and re-activates the register row** (the same
   re-activation `/linkedin:pivot` relies on), so close it again:

   ```bash
   node --import tsx "${CLAUDE_PLUGIN_ROOT}/scripts/editions/src/cli.ts" register-complete \
     --series "<slug>" --edition "<NN>"
   ```

   > **Which lead time is which.** The register row's `completedAt` now marks the
   > end of the *retro*, not of delivery. The **delivery** lead time — the number
   > the cadence work is calibrated on — is always readable exactly from
   > `phaseLog` (first entry → the `scheduling` entry), which is why the register
   > is a mirror and `edition-state.json` the source of truth. Report the
   > `phaseLog` number, not the row's.

   Write a "retro complete — <N> promoted, <N> rejected → next edition starts
   with them enforced" line to `<serie>/STATE.md` (overwrite). The edition is
   now done.

```
Retro (loop closed) — complete.
- Fold-ins reviewed: <N>   promoted: <N> (<N> mechanical → rules.ts · <N> judgment → language-rules/<lang>.md)   rejected (kept): <N>
- Ratify after promotion: ✓ in sync   (or: not run — no mechanical promotion this edition)
- Effort (measured, phaseLog): <N> transitions · recurred: <phase list or none> · lead time <N> days
- Process friction: noted and handed back   (or: none)   — the operator's own register is untouched
- Other silos in view: specifics-bank · brain · voice drift log · A/B learnings
Edition CLOSED. The next edition starts with these corrections already enforced.
```

---

## Distribution channel — native LinkedIn Newsletter vs a long-form post (honest mechanics)

A long edition can ship two ways: as a **regular long-form post** (what Steps 8–10
deliver — pasted into the feed, ranked organically), or published through
LinkedIn's **native Newsletter feature** (subscribe-able, with its own notification
channel). They are not the same distribution, and the native newsletter is **not a
strict upgrade** — it earns its place only above a follower floor. This section is
the honest decision surface; it sells nothing.

**What the native newsletter actually does (and doesn't):**

- **It bypasses organic feed ranking** for the subscriber notification — that is the
  real, defensible benefit. When you publish an edition, each subscriber gets **one
  notification** routed to their preferred channel (in-app / push / email). The
  channels are **deduplicated** — LinkedIn's own FAQ states that if you get an in-app
  or push notification you should **not** also expect an email for the same event. So
  it is **one deduplicated notification per subscriber per edition — not three
  guaranteed touchpoints.** Treat any "3× notification" / "hits everyone on every
  channel" framing as oversold. (The edition is still **also** posted to your feed and
  can resurface via engagement and interest sections.)
- **The mass invite fires once.** On your **first** edition LinkedIn auto-invites all
  your current connections/followers to subscribe (and invites each new follower once,
  thereafter). That one-time "invite all followers" blast **spends at the size you
  launch at** — launch with a small base and you permanently burn the blast on a tiny
  audience. This is why there is a **~1–2K follower floor**: below it, **wait** — you
  are not yet able to spend the launch blast well. (The floor aligns with the existing
  ~1K `/linkedin:monetize` and `/linkedin:outreach` unlocks.)
- **Cold-start is slow — don't inflate it.** A genuine zero-audience start is roughly
  **0–100 subscribers in months 1–3.** The viral "0→9K in 7 days" / "0→10K" newsletter
  case studies were **not** cold starts — they leveraged existing audiences or long
  grinds. Plan for the slow floor, not the screenshot. Cadence: **weekly** is common
  among top performers; **biweekly** is a safe default for original analysis.

**Honest downsides to disclose before you commit:**

- **Subscribers are non-exportable** — pure platform lock-in. If LinkedIn ever sunsets
  the feature, you lose the whole list with no portability.
- **No canonical control** — LinkedIn outranks your **own site** for the same article.
  Harmful if you are trying to build an owned property (your domain/email list).
- **No read/open/unsubscribe analytics** — you cannot see opens, click detail, or who
  left. (Consistent with the boundary the plugin states elsewhere: there is no
  self-serve post-analytics API for a personal profile.)
- **Per-subscriber reach decays** as the list grows — a bigger list does not mean every
  edition reaches everyone; notification delivery is not guaranteed.

**Decision rule:**

- **Below ~1–2K followers** → ship the edition as a normal long-form post (Steps 8–10)
  and **keep building the base** with short-form / documents. Do **not** spend the
  one-time launch blast yet.
- **At/above ~1–2K followers, posting regularly** → the native newsletter is worth it:
  publish the **first** edition deliberately (that is when the blast fires), then hold a
  steady weekly/biweekly cadence. Frame it as bypass-the-feed reach **with** the
  lock-in / no-canonical / no-analytics / decay caveats above understood — not as a
  guaranteed multi-touch megaphone.

> **Sourcing.** Mechanics (5-newsletter max, 2-week cooldown, auto-invite, feed
> resurfacing, notification dedup) are from LinkedIn's own help docs; the follower
> floor and cold-start ranges are practitioner-sourced (medium confidence) — see
> `docs/remediation/research/03-coverage-gap-specs.md` §D5. Date every figure when you
> repeat it; the email-delivery behavior (dedup vs "always emailed") is genuinely
> contested and a one-edition live test would resolve it.

---

## Reference Files

- `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json` — edition-state schema (18 phases including Fix #2 lived-specifics extraction (Step 1.5) + `articles.NN.livedSpecifics`, the deterministic §B/§C1 contract-gate (Step 4.5), v2.1 skeleton + spine-prose gates, v2.3 visual-assets, v2.4 editorial-review, and v3.1 headless-review + per-article `personas` + `pivots`)
- `${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank/` — lived-specifics store + per-edition binding (Fix #2, kilde-så-draft): `query`/`add` (Step 1.5), `validate-binding` (Step 2.5 gate), `render-kilder` (`NN-kilder.md` ledger), `record-usage` (Step 8 lock — «used in edition NN»). Design record: `docs/fix2/slice2-binding.md`
- `${CLAUDE_PLUGIN_ROOT}/scripts/editions/` — series-level memory (N11): `distil-append` writes what a locked edition spent to `<serie>/linkedin/series-distillate.json` (Step 8), `distil-check` gates the next skeleton against it (Step 2.5). Deterministic character-trigram similarity; advisory, never blocking. **Editions register (N12):** `register-upsert` at every phase transition (appends `articles.NN.phaseLog` + mirrors the row into `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/editions/register.json`), `register-list` for the work-in-progress view, `register-complete` at Step 10 (measured lead time). The register mirrors state — resumption never reads it
- `${CLAUDE_PLUGIN_ROOT}/hooks/scripts/slots.mjs` — the publishing grid (N13): `nextFreeSlot` gives Step 10 its default slot (queue + register coverage), and session start reports open slots from the same functions. Zero-dep, so hook and command can never disagree. The grid itself is operator config (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/publishing-slots.json`); unconfigured means Step 10 simply asks
- `${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); also resonance + conversion in the Step 6.5 headless package
- `${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}/agents/content-reviewer.md` — Step 6.5 cold argument-integrity review (C1–C5; headless, no session context)
- `${CLAUDE_PLUGIN_ROOT}/agents/language-reviewer.md` — Step 6.5 cold Norwegian-language review (L1–L5; headless)
- `${CLAUDE_PLUGIN_ROOT}/agents/fact-reviewer.md` — Step 6.5 cold re-verification (F1–F4, 🔴/🟡/🟢; catches pivot premises Step 5 missed)
- `${CLAUDE_PLUGIN_ROOT}/commands/headless-review.md` — the Step 6.5 cold review package as a standalone command (run in a fresh session for maximum isolation)
- `${CLAUDE_PLUGIN_ROOT}/commands/pivot.md` — re-opens the pipeline after a late pivot so Steps 5–6.5 re-run on the changed version before lock
- `${CLAUDE_PLUGIN_ROOT}/commands/react.md` — multi-source synthesis discipline (reused in Step 2)
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/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}/references/fold-in-loop.md` — the capture → classify → promote → enforce loop (N14): Steps 2.5/3a/5.5/6.5 capture into `articles.NN.foldIns`, Step 11 promotes. Domain-general; needs no external writing contract
- `${CLAUDE_PLUGIN_ROOT}/config/language-rules.template.md` — the operator's accumulating per-language rules (opt-in; read by Step 4 + `language-reviewer`, appended by Step 11)
- `${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs` — POST.html delivery; reads `linkedin/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
- `${CLAUDE_PLUGIN_ROOT}/render/build-figur.mjs` — coded data-figure renderer (SVG/HTML → PNG via headless Chrome; targets article/carousel/single) — Step 7.5 coded route
- `${CLAUDE_PLUGIN_ROOT}/references/figure-design-guidelines.md` — coded-figure design rules + brand-token convention + render targets — Step 7.5
