Måling som ikke endrer neste utgave er teater: report/analyze/ab-test/48h-monitor endte alle i chat (grep do.next = 0 i hele repoet). N15 gjør kanalen til en kontrakt. - recordDoNext i hooks/scripts/state-updater.mjs (TDD, 17 nye tester): seksjonen Do-Next Directives, rader "[dato] (kilde) direktiv — evidence: peker". Levetid = erstatt-per-kilde + 60-dagers aldersgulv, maks 3 per skriv, additiv scalar last_donext_date, $-sikre replacement-funksjoner. CLI: --record-do-next. - Skrivere: report Step 7b, analyze Step 6b, ab-test 2c.6 (kun Adopt), post-feedback-monitor (kun 48h-sjekken); analytics-interpreter emitterer nå anbefalinger på do-next-form (imperativ + evidens). - Lesere: post/quick/batch/create Step 0 + newsletter Step 1, som i tillegg kalibrerer på forrige utgaves MÅLTE tall via queue-id-match (queue.json → analytics/posts) med eksplisitt honest-miss — aldri estimert. - Guards: test-runner Section 16v (15 ubetingede sjekker, floor 198 → 213), $-safety-batteriet dekker recordDoNext (coverage-gate fanget den nye eksporten). Suiter: test-runner 232/0, hooks 191/0, trends 300/0, brain 134/0, editions 72/0, specifics-bank 45/0, contract-gate 33/0, tests 35/0, render 60/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2464 lines
148 KiB
Markdown
2464 lines
148 KiB
Markdown
---
|
||
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`).
|
||
|
||
> **Calibration from measurement (A2-F14 — the do-next contract).** Two reads, both
|
||
> cheap, both BEFORE the dimensions below. Neither asks the operator anything.
|
||
>
|
||
> 1. **`## Do-Next Directives` in `~/.claude/linkedin-studio.local.md`.** What the last
|
||
> measurement pass (`/linkedin:report`, `/linkedin:analyze`, an A/B Adopt verdict, the
|
||
> 48h monitor) decided the next piece should do differently, each row carrying its
|
||
> evidence pointer. Apply the ones that apply to long-form and **name them in the brief**
|
||
> — a directive that shaped this edition must be visible in the brief, not silently
|
||
> absorbed. Never edit or clear the section here (each source manages its own rows).
|
||
> 2. **The previous edition's MEASURED numbers — queue-id match.** The prior edition of
|
||
> this series was registered at its Step 10 under the stable queue id
|
||
> `<series-slug>-<NN−1>`. Resolve its numbers:
|
||
> - read `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/drafts/queue.json`,
|
||
> take the entry whose `id` is `<series-slug>-<NN−1>` → its `scheduled_date`,
|
||
> `hook_preview` and `status`;
|
||
> - read the analytics batches in
|
||
> `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/*.json` and
|
||
> select the `.posts[]` row whose `publishedDate` equals that `scheduled_date`
|
||
> (a rescheduled edition may be a day or two off — widen to ±2 days and disambiguate
|
||
> on a `title` that matches `hook_preview`);
|
||
> - carry forward `impressions`, `engagementRate`, `comments` and `saves` (when present).
|
||
>
|
||
> Read them as calibration, not as a verdict: **low impressions** points at hook /
|
||
> topic-relevance (a Step 9 problem for this edition), **decent impressions with weak
|
||
> comments** points at resonance (a Step 6 persona problem), and a strong prior edition
|
||
> says keep the structure that worked. State the comparison in one line in the brief.
|
||
>
|
||
> **Honest miss.** No queue entry, no analytics match, or the edition simply is not measured
|
||
> yet (`status: scheduled`, or the import has not been run) → say so in one line — *"forrige
|
||
> utgave er ikke målt ennå; kalibrerer på skjelett og persona alene"* — and continue.
|
||
> **Never estimate** the previous edition's numbers, and never let a missing measurement
|
||
> block the brief. This is edition NN=1 of a new series' normal state.
|
||
|
||
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
|
||
> (``) 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
|