feat(linkedin-studio): N14 — foldIns-fangst + Step 11 retro + background-headless + språkregel-akkumulering [skip-docs]

Lukker sløyfen pluginen manglet: en rettelse operatøren gjør i utgave N
håndheves i N+1 i stedet for å bli gjenoppdaget. Maskineriet fantes
(foldIns-skjema + promote→ratify siden fix #1); det som manglet var wiring —
ingenting skrev til køen og ingen fase tømte den.

- Fangst (A2-F7): Steps 2.5/3a/5.5/6.5 appender rettelsen ordrett med trigger,
  decision "pending". Ubetinget og bevisst dum — å avgjøre ved fangst om noe
  "fortjener" en regel er nettopp slik køen holder seg tom og sløyfen dør.
- Step 11 retro (A2-F8), ≤5 min ETTER scheduling: promoter køen med eksplisitt
  JA/NEI (mekanisk → atomisk contract-gate-promotering, teller kun med grønn
  --ratify; dømmekraft → operatørens språkregelfil; NEI → rejected, beholdes),
  effort-oppsummering fra MÅLT phaseLog (aldri re-estimert), og ÉN
  friksjons-spørring som besvares tilbake til operatøren. Faser 18 → 19;
  resumption-tabellen ruter scheduling → Step 11, retro → complete.
- articles.NN.retro: additiv-valgfri (default null), schemaVersion forblir 1.
- Background-headless (A2-F6): --background kjører pakken i en bakgrunnsagent
  som skriver rapporten til disk; drafting-sesjonen leser fila. Samme isolasjon
  som fersk sesjon, uten copy-paste-sømmen. Inline fan-out = eksplisitt fallback.
- Språkregler (C-10): ${DATA}/language-rules/<lang>.md (opt-in, template).
  Leses TO ganger — av language-reviewer (fanger) og av Step 4 (forebygger).
  Shippet banliste forblir baseline; brukerfila utvider.
- references/fold-in-loop.md (A2-F9): loopen dokumentert domene-generelt in-tree,
  så en adopter uten ekstern skrivekontrakt har hele sløyfen. Refs 28 → 29.

Suiter (alle grønne): test-runner 197 → 217 (Section 16u: 18 ubetingede greps +
non-vacuity self-test; fase-sveip 16 → 17 faser; floor 179 → 198) · hooks 174 ·
trends 300 · brain 134 · editions 72 · specifics-bank 45 · contract-gate 33 ·
tests 35 · render 60.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 12:39:16 +02:00
commit f0532dce3f
9 changed files with 711 additions and 30 deletions

View file

@ -61,12 +61,19 @@ They get **NOTHING** about: prior versions or version numbers, what was
deliberately omitted, the pivot narrative, who has read it, what an editor said,
how a persona voted, or what the author intended. Two layers enforce this:
1. **Layer 1 — fresh session (strongest).** For the publish-ready gate, the
operator runs *this command in a brand-new Claude Code session*. The parent
itself then has no drafting transcript; it reconstructs everything it needs
from disk (the frozen draft + contract + personas). This is the recommended
path and the one KTG asked for.
2. **Layer 2 — subagent isolation.** Each archetype is a `Task` subagent, which
1. **Layer 1 — fresh session (strongest, manual).** The operator runs *this
command in a brand-new Claude Code session*. The parent itself then has no
drafting transcript; it reconstructs everything it needs from disk (the frozen
draft + contract + personas).
2. **Layer 1b — background run (`--background`, equal isolation, no seam).** The
drafting session spawns this package as a **background agent** with its own
context window, which writes the consolidated report to a file; the drafting
session then reads that file from disk. The background agent gets only the
cold-contract inputs — it cannot see the parent's transcript — so the
isolation matches a fresh session, with **no copy-paste** of the report back
into the drafting session and no manual session switch. See "Background run"
below. This is the default recommendation for the Step 6.5 gate.
3. **Layer 2 — subagent isolation.** Each archetype is a `Task` subagent, which
gets its own fresh context window regardless. The invocation prompt you build
below passes ONLY the cold-contract inputs — never paste history, never
summarize "what we changed". If you find yourself about to tell a reviewer
@ -91,11 +98,33 @@ how a persona voted, or what the author intended. Two layers enforce this:
--persona <name> (persona modes only; default: the primær)
--article NN (optional; persist into edition-state.json)
--output <path> (default: <serie>/review/NN-headless-<stamp>.md)
--background (run the package in a background agent
that writes --output; caller reads the file)
```
No `--type` (or `--type all`) runs the **whole package in parallel**. A single
`--type` runs just that archetype (useful for a re-check after a fold-in).
### Background run (`--background`)
With `--background`, do **not** fan the archetypes out here. Spawn **one**
background agent that runs Steps 15 of this command itself, and let it write the
report:
- Launch it with `run_in_background: true` and a prompt carrying **only** the
cold-contract inputs — draft path, contract path, persona names + block path,
`--type`, and the absolute `--output` path — plus the instruction to write the
consolidated report to `--output` and return that path.
- **Never** put drafting history, version notes, or "what we changed" in that
prompt. The background agent has its own context window; the only way session
framing reaches it is if you type it in.
- When it completes, `Read` the report at `--output` and continue at Step 5
(surface + persist). The drafting session sees the *report*, not the reviewers'
reasoning, and the reviewers never see the drafting session — the same
independence a fresh session gives, with **no copy-paste seam** between them.
- If background execution is unavailable, say so plainly and fall back to the
inline fan-out (Step 3) — never silently downgrade the isolation.
## Step 1 — Resolve inputs (from disk, not from memory)
1. **Draft.** Use `--draft`. It must be a long-form draft `.md` (the same
@ -219,18 +248,20 @@ Headless review complete (COLD).
- Archetypes: <N> run in parallel converged flags: <N>
- BLOCK/🔴: <N> REWORK: <N> primær resonance: JA/NEI conversion: JA/NEI
- Report: <serie>/review/NN-headless-<stamp>.md (surfaced via SendUserFile)
- Isolation: background agent (report read from disk) | fresh session | inline fan-out (fallback)
- Persisted: edition-state.json articles.NN.headlessReview (or: standalone, not persisted)
Operator gates the fold-in. For maximum independence, run this command in a FRESH session.
Operator gates the fold-in. For maximum independence with no manual seam, run with --background.
```
## Relationship to the pipeline + the in-session gates
- **Step 6.5 of `/linkedin:newsletter`** invokes this same package (after the
in-session persona sweep Step 6, before lock Step 8). The pipeline may fan the
reviewers out inline, but the **strongest** isolation is the operator running
`/linkedin:headless-review` in a fresh session and pasting the consolidated
report back. Either way the body must be re-touched **before** lock — never
reopen a locked text (the cardinal Seres lesson).
reviewers out inline, but the recommended path is `--background`: full
isolation, the report read from disk, and no manual step between the review and
the fold-in. A fresh session remains available and is equally cold. Either way
the body must be re-touched **before** lock — never reopen a locked text (the
cardinal Seres lesson).
- **Deliberate redundancy.** `fact-reviewer` overlaps `fact-checker` (Step 5) and
`language-reviewer` overlaps `editorial-reviewer`'s prose axis (Step 5.5) **on
purpose**. The in-session gates ran with framing-bias; the cold re-read catches

View file

@ -83,7 +83,7 @@ delegate the fan-out to a nested background agent.
> 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 (18 phases)
## 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
@ -113,9 +113,10 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
| 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 18 phases (Steps 0, 1, 1.5, 2, 2.5, 3a, 3b, 4, 4.5, 5,
> 5.5, 6, 6.5, 7, 7.5, 810) are implemented below. This command takes an edition
> **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, 811) 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 +
@ -125,7 +126,8 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
> 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, persisting each phase to `edition-state.json`
> 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.
@ -240,7 +242,8 @@ Look up `edition-state.json` → `articles.<currentArticle>` (and the top-level
| `visual-assets` | Step 8 — LOCK → delivery |
| `lock-delivery` | Step 9 — Hook / conversion gate |
| `hook-conversion-gate` | Step 10 — Scheduling |
| `scheduling` | **Edition complete** — nothing to resume (start the next article or edition) |
| `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
@ -770,6 +773,14 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
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
@ -953,6 +964,12 @@ Typically ~2030 % of the edition's final length.
`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):
@ -1075,6 +1092,18 @@ cutting, not by expansion**; hold the length flat (plan §8).
> 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
@ -1373,6 +1402,15 @@ and `persona-reviewer` never flags em-dash density (that is this step).
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 (P1P5 / A1A5),
`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.
@ -1545,11 +1583,16 @@ command runs — see `commands/headless-review.md` for the full cold contract):
summarize "what we changed" into a reviewer prompt — that is the context
pollution the package exists to eliminate.
> **Maximum-independence path.** The strongest isolation is the operator
> running `/linkedin:headless-review --draft <frozen> --article NN` in a
> **fresh session** (the parent then has no drafting transcript at all) and
> pasting the consolidated report back. The inline fan-out here is the
> single-session path; both use the same agents.
> **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
@ -1573,6 +1616,13 @@ command runs — see `commands/headless-review.md` for the full cold contract):
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:
@ -2162,6 +2212,129 @@ Edition complete. Visible in /linkedin:calendar; mark live via /linkedin:calenda
---
## 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 810
@ -2249,6 +2422,8 @@ the honest decision surface; it sells nothing.
- `${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, 49 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