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

@ -59,6 +59,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **D-5: the shipped schedule is marked for what it is**`references/scheduling-strategy.md` now carries the same epistemic standard as the algorithm reference (**confidence: low / directional**, practitioner heuristic, no primary source) and defers to the operator's own grid, tested against their own analytics. The tables are a starting point, not a finding about anyone's audience.
- Suites: hooks 140 → 174 (25 slot-core + 9 session-start tests); test-runner 184 → 197 (Section 16t: 13 unconditional greps incl. a de-niche check that no publishing time is hardcoded in the slot path and no operator grid is committed, + non-vacuity self-test; anti-erosion floor 166 → 179).
### Added — the fold-in loop: a correction made once is enforced from then on (N14 / A2-F6, A2-F7, A2-F8, A2-F9, C-10)
- **Capture is finally wired (A2-F7)** — the `articles.NN.foldIns[]` queue and the promote→ratify machinery have shipped since fix #1, but **nothing ever wrote to the queue and no step ever emptied it**. All four steps where an operator's judgment enters the text — skeleton annotations (2.5), spine annotations (3a), editorial flags (5.5), cold-review flags (6.5) — now append the correction near-verbatim with its trigger, `decision: "pending"`. Capture is unconditional and deliberately dumb: deciding at capture time whether a correction "deserves" a rule is exactly how the queue stays empty and the loop dies. Edition-specific fixes (a wrong number) are explicitly *not* rules.
- **New Step 11 — retro (A2-F8), ≤5 min, AFTER scheduling** — three fixed jobs, one operator pass. (1) **Promote**: every pending fold-in gets an explicit JA/NEI; mechanical → an atomic `scripts/contract-gate` promotion (rule + contract row + manifest row) that only counts when `--ratify` is green, judgment → the operator's language-rules file; NEI marks the row `rejected` and **keeps** it. (2) **Effort summary**: read straight from the measured `articles.NN.phaseLog` (N12) — transitions, *recurred* phases (where the rounds actually went), lead time — never re-estimated, and "not measured" when the log is absent. (3) **One** process-friction question, answered back to the operator: the plugin never writes into the operator's own notes or register. It runs after delivery 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. Phases 18 → 19; the resumption table routes `scheduling` → Step 11 and `retro` → complete, so an aborted session cannot drop the queue.
- **`articles.NN.retro` in edition-state** — additive-optional (default `null`), so schemaVersion stays **1** and pre-N14 editions load unchanged.
- **Background headless review (A2-F6)**`/linkedin:headless-review --background` runs the cold package in a background agent with its own context window that writes the consolidated report to disk; the drafting session then *reads the file*. Maximum independence used to cost a manual session switch plus pasting the report back — the seam is gone, and the isolation is identical (the background agent gets only the cold-contract inputs). The inline fan-out remains the explicit fallback; the isolation is never silently downgraded.
- **Language rules accumulate in a user file (C-10)**`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules/<lang>.md` (opt-in, template `config/language-rules.template.md`) holds the judgment-class corrections Step 11 promoted. It is read **twice**: by `language-reviewer` at Step 6.5 (catch) and by Step 4 while the draft is still being cleaned (**prevent**) — a rule that only ever catches is worth half a rule. The shipped ban-list (`longform-quality-rules.md` rule 3) stays the baseline and is never edited by the loop; the user file extends it, and its *Deliberate exceptions* section is honoured. Absent file ⇒ baseline alone, silently.
- **`references/fold-in-loop.md` (A2-F9)** — the capture → classify → promote → enforce loop documented **domain-generally and in-tree**, so an adopter with no external writing contract has the whole loop (that absence "is not a gap" — the same mirror rule `editorial-reviewer` uses). Names the other four accumulation silos (specifics-bank, brain, voice drift log, A/B learnings) that Step 11 surfaces together, because a retro that empties one queue teaches the operator the others are empty too. references 28 → 29.
- Suites: test-runner 197 → 217 (Section 16u: 18 unconditional greps + a non-vacuity self-test on the capture predicate; the phase-coverage sweep now spans **17** phases; anti-erosion floor 179 → 198). All nine suites green.
### Built feedback (awaiting consumer-side proof)
- **MR-F9** (demand-sweep «innenfra og ut») — built; the (a)/(b)/(c) evidence gate is a runtime demonstration, proven consumer-side (plugin agents don't resolve in the dev repo).

View file

@ -13,9 +13,10 @@ Full-spectrum LinkedIn content engine — short-form feed posts, carousels, vide
- **Analytics:** CLI `scripts/analytics/` (TypeScript, needs `tsx` + `npm install`); data `assets/analytics/` (gitignored)
- **Specifics-bank:** `scripts/specifics-bank/` (TypeScript, needs `tsx` + `npm install`) — deterministic, topic-tagged store of the operator's lived specifics (real numbers, named cases, held opinions) in the per-user data dir; elicited/bound by `/linkedin:newsletter` Step 1.5 so drafts draw from real inventory, never invented filler. Consumption is logged back at Step 8 lock (`record-usage``usedIn`), so cross-edition re-use of the same material is visible rather than silently encouraged by the dedupe
- **Series distillate:** `scripts/editions/` (TypeScript, needs `tsx` + `npm install`) — series-level memory: each locked edition's spent narrative units (anecdotes/arguments/hooks) in `<serie>/linkedin/series-distillate.json`; written at `/linkedin:newsletter` Step 8 (`distil-append`), checked against the next skeleton at Step 2.5 (`distil-check`, advisory). Deterministic character-trigram similarity — AI extracts, code compares
- **Editions register:** `scripts/editions/` (same package) — one row per edition in production in the per-user data dir (`editions/register.json`): series, edition, path, current phase, next action, slot, `startedAt`/`completedAt`. Each of the 16 canonical `/linkedin:newsletter` phase transitions runs `register-upsert --edition-state <path>`, which appends `articles.NN.phaseLog` (lead-time telemetry, additive — schemaVersion stays 1) **and** mirrors the row in one call; Step 10 runs `register-complete`. `register-list` is the work-in-progress view. The register is a **mirror** — deterministic resumption reads `edition-state.json` only, and a lost register is rebuilt by the next transition
- **Editions register:** `scripts/editions/` (same package) — one row per edition in production in the per-user data dir (`editions/register.json`): series, edition, path, current phase, next action, slot, `startedAt`/`completedAt`. Each of the 17 canonical `/linkedin:newsletter` phase transitions runs `register-upsert --edition-state <path>`, which appends `articles.NN.phaseLog` (lead-time telemetry, additive — schemaVersion stays 1) **and** mirrors the row in one call; Step 10 runs `register-complete`. `register-list` is the work-in-progress view. The register is a **mirror** — deterministic resumption reads `edition-state.json` only, and a lost register is rebuilt by the next transition
- **Publishing slots:** `hooks/scripts/slots.mjs` — the weekly production grid. Slots are **operator config** in the per-user data dir (`profile/publishing-slots.json`, schema + opt-in template in `config/publishing-slots.template.json`); absent config ⇒ every slot surface stays silent, and the plugin hardcodes no publishing times. Coverage of a slot counts the short-form queue (`scheduled`/`published`) **and** editions-register rows claiming that date, so long-form and short-form cannot be double-booked. Zero-dep (SessionStart may not spawn tsx) and imported identically by the hook and by the commands (`queue-manager.mjs` pattern), so session-start's vacancy warning and `/linkedin:newsletter` Step 10's slot default are the same computation
- **Contract-gate:** `scripts/contract-gate/` (TypeScript, needs `tsx` + `npm install`) — deterministic §B/§C1 rule-gate on the full draft (`/linkedin:newsletter` Step 4.5, before the AI sweeps); ratifies `rules.ts` against the edition's §E-manifest, then gates with BLOCK/WARN
- **Fold-in loop:** capture → classify → promote → enforce, documented domain-generally in `references/fold-in-loop.md`. The four `/linkedin:newsletter` steps where operator judgment enters the text (2.5, 3a, 5.5, 6.5) append corrections to `articles.NN.foldIns[]` (`decision: "pending"`); **Step 11 (retro)** empties the queue with an explicit operator JA/NEI — mechanical → an atomic contract-gate promotion (only counts when `--ratify` is green), judgment → the operator's opt-in language-rules file (`${DATA}/language-rules/<lang>.md`, template `config/language-rules.template.md`), which Step 4 reads while drafting (prevent) and `language-reviewer` reads at Step 6.5 (catch). Rejected rows are kept, never deleted. Step 11 also reports effort from the **measured** `phaseLog` and asks one friction question whose answer goes back to the operator — the plugin never writes their register
- **Analytics metrics (S16):** parsed CSV columns + an optional, manually-entered `saves` count (count-only in native LinkedIn analytics since ~Sept 2025, no CSV export; the Marketing API exposes `POST_SAVE` on `/memberCreatorPostAnalytics` from v202604, but access is partner-gated — so manual entry remains the right UX). `parseOptionalCount()`: blank / non-numeric / negative → `undefined` (`unknown`, never 0), a genuine `0` is kept; saves surfaced per-post + as `totalSaves`, but **not** folded into `engagementRate`. `dwell` stays **explicitly unmeasurable** (internal to LinkedIn, no export/API). All analytics I/O routes through the `getAnalyticsRoot()` seam (M0 per-user data-dir).
## Hooks
@ -55,8 +56,8 @@ All content commands auto-copy output to clipboard via `clipboard-helper.mjs`; i
| `/linkedin:quick` | 5-minute quick post (3-line formula) + 8 post-type templates |
| `/linkedin:pipeline` | Full end-to-end content pipeline |
| `/linkedin:trends` | Trend discovery pass — delegates to `trend-spotter` (default long-form mode), persists to trend store + dated morning brief, triage per id (act/skip) |
| `/linkedin:newsletter` | Long-form orchestrator (newsletter / essay / series article) — multi-session 18-phase pipeline with gates before prose (skeleton + spine), fact-check, editorial-review (Step 5.5), persona sweep, headless adversarial review (Step 6.5), and visual-assets — all before lock |
| `/linkedin:headless-review` | Cold adversarial review package — 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` on a FROZEN draft, no drafting-session context; operator-gated report (Step 6.5 standalone surface) |
| `/linkedin:newsletter` | Long-form orchestrator (newsletter / essay / series article) — multi-session 19-phase pipeline with gates before prose (skeleton + spine), fact-check, editorial-review (Step 5.5), persona sweep, headless adversarial review (Step 6.5), and visual-assets — all before lock; closes with the Step 11 retro that promotes this edition's corrections so the next one starts with them enforced |
| `/linkedin:headless-review` | Cold adversarial review package — 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` on a FROZEN draft, no drafting-session context; operator-gated report (Step 6.5 standalone surface). `--background` runs the package in a background agent that writes the report to disk — fresh-session isolation without the paste-back seam |
| `/linkedin:pivot` | Re-open a long-form edition after a late substantive change so cleared gates re-run before lock (pivot heuristic: >20% word-count or >2 new sections) |
| `/linkedin:batch` | Create a full week of content |
| `/linkedin:calendar` | View/manage post scheduling queue + publish action |

View file

@ -140,6 +140,26 @@ chronicle**, not a saksframlegg — judge against that register.
L1, L2, L5 are partly countable — report the count where you have one. L3, L4
need a read but are still crisp yes/no findings.
### L6 — the operator's accumulated rules (read this file first)
Before judging, read
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules/<lang>.md`
(`<lang>` = the `language` input; e.g. `no.md`, `en.md`). It holds the language
corrections **this operator already made and confirmed** in earlier editions,
promoted there by `/linkedin:newsletter` Step 11 (the fold-in loop —
`references/fold-in-loop.md`). A defect this file names is one the operator has
already paid to fix once: **flag it at REWORK or higher**, and cite the rule.
- The file **extends** the five checks above; it never replaces them. Where a
rule there and a check here disagree, the stricter one wins.
- It also has a *Deliberate exceptions* section — a baseline rule the operator
consciously overturned. **Do not flag against an exception listed there.**
- **Absent file (the default — it is opt-in): judge on L1L5 alone and say
nothing about it.** Its absence is not a gap and never a flag.
This is what stops the review being Groundhog Day: without it, every edition
re-earns the same correction from scratch.
## Severity scale — BLOCK / REWORK / NICE
Every flag carries exactly one severity (mirrors `editorial-reviewer`, adapted to

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

View file

@ -22,7 +22,8 @@
"visual-assets — cover (+ optional inline figures) or carousel deck: brief → generate → operator-gate → approve, BEFORE lock so build-linkedin.mjs picks them up (Step 7.5)",
"lock-delivery — LOCK → POST.html all-in-one-place deliverable (Step 8)",
"hook-conversion-gate — persona gate on distribution text post-lock: would YOU click? (Step 9)",
"scheduling — register edition in plugin queue/state for native LinkedIn scheduling (Step 10)"
"scheduling — register edition in plugin queue/state for native LinkedIn scheduling (Step 10)",
"retro — close the loop (≤5 min, AFTER scheduling): promote the pending articles.NN.foldIns with an explicit operator JA/NEI (mechanical → a contract-gate rule; judgment → the language-rules file / craft checklist), summarize the MEASURED effort from articles.NN.phaseLog, and ask ONE process-friction question. The corrections made in this edition become enforced before the next one starts (Step 11)"
],
"articleStatusValues": ["pending", "in-progress", "locked", "scheduled"],
"phaseLog": "Per-article phase-transition log (N12 / A1-12) — the raw material for measuring lead time. Additive-optional (default [], NO schemaVersion bump — same pattern as sourceTrendId/targetLevel/language; an edition produced before N12 simply has none and the register treats its absence as \"not measured\"). Shape: [ { phase, completedAt } ] in transition order, where phase is one of the canonical _doc.phases identifiers and completedAt is an ISO-8601 timestamp. Written DETERMINISTICALLY, never by hand: every /linkedin:newsletter phase transition runs `scripts/editions` (`node --import tsx src/cli.ts register-upsert --edition-state <path>`), which appends the entry for the just-written currentPhase AND mirrors the edition into the editions register (${LINKEDIN_STUDIO_DATA}/editions/register.json) in one call — the log and the register cannot drift apart because nothing writes one without the other. Idempotent against an immediately repeated transition (a re-run of the same step logs once), but a phase that recurs AFTER another one is logged again: /linkedin:pivot legitimately sends an edition back through cleared gates, and that second pass is real production time. Per-article rather than top-level because lead time is a property of an edition, mirroring articles.NN.phase. The register is a MIRROR of this file, never a source of truth — losing it costs telemetry, not an edition.",
@ -33,6 +34,7 @@
"headlessReview": "Per-article headless-review record written by Step 6.5 (headless-review phase). Runs AFTER the in-session persona sweep (Step 6) and BEFORE lock (Step 8), on a FROZEN snapshot of the publish-ready (or pivoted) draft, fanned out from the command layer (foreground) or invoked standalone via /linkedin:headless-review in a fresh/cold session. Five archetypes judge independently with NO drafting-session context: content-reviewer (argument integrity), language-reviewer (Norwegian language), fact-reviewer (cold re-verification incl. claims a late pivot bolted on), persona-reviewer mode=resonans (per active persona), persona-reviewer mode=konverter (primær, hook only). The consolidated report is surfaced to the operator via SendUserFile; the operator decides which flags fold in. Shape: { frozenDraft, reviewers: { content, language, fact, personaResonance, personaConversion } (each { reportPath, summary, status }), consolidatedReport, foldedIn, waived, status }. status ladder: pending → run → folded. null until Step 6.5 runs. This is the adversarial-independence companion to the in-session gates (editorialReview, personaSweep, factcheckLog) — deliberately redundant: a cold reader catches what the framing-biased in-session pass missed.",
"pivots": "Per-article pivot log (Endring 9c). A pivot is a substantive change to a draft AFTER a gate had already cleared — e.g. a new argument anchor / section added late (the Del 4 Security Champions case: +~530 words, 2 new sections, +42 %). Each /linkedin:pivot invocation appends one entry and moves currentPhase back so the cleared gates (Steps 56.5) re-run on the pivoted version before lock. Heuristic (documented, checked at the Step 8 lock precondition): if the current draft's word count differs > 20 % from the version that last cleared Step 6, OR it has > 2 new sections, a pivot-reopen is suggested/required. Each entry: { timestamp, reason, fromPhase, toPhase, wordCountBefore, wordCountAfter, deltaPct, newSections, gatesToRerun: [phase…] }. Default [].",
"foldIns": "Per-article accumulation queue (slice 2 of fix #1 — «rettelser fester seg»). Each correction KTG makes during an edition that is NOT yet a contract rule is captured here, then routed by the JA-promoter (maskinrommet/docs/skrivekontrakt.md §E) to a permanent home so it never has to be re-discovered: a MECHANICAL correction → a rules.ts gate rule (BLOCK/WARN) + a §B-row/§C1/§C2-box + a §E-manifest row (the contract-gate `ratify` check then asserts rules.ts ↔ §E-manifest stay in bijection); a JUDGMENT correction → a §C2-box only (stays with editorial-reviewer, no gate rule). Capture is per-article (provenance = which article surfaced it); promotion is series/contract-wide. Each entry: { id, date (ISO-8601), correction (what KTG corrected, near-verbatim), trigger (where/why it surfaced), classification: \"mechanical-block\" | \"mechanical-warn\" | \"judgment\" | null (set at the classify step), decision: \"pending\" | \"promoted\" | \"rejected\" (the JA-promoter outcome), ruleId: <rules.ts id> | null (set on promote for mechanical), note?: where a judgment/rejected fold-in landed }. Default []. Rejected fold-ins are kept for traceability, never deleted.",
"retro": "Per-article retro record written by Step 11 (retro phase), the step that makes a correction STICK. Additive-optional (default null, NO schemaVersion bump — same pattern as phaseLog/sourceTrendId/targetLevel; an edition produced before N14 simply has none). Runs AFTER scheduling (Step 10) because the retro is about the NEXT edition, not this one's delivery. Three jobs, all recorded here: (1) fold-in promotion — every articles.NN.foldIns row with decision \"pending\" gets an explicit operator JA/NEI; JA on a mechanical correction promotes it atomically to a scripts/contract-gate rule (rules.ts + contract row + manifest row, `--ratify` green or the promotion does not count), JA on a judgment correction appends it to the user language-rules file (${LINKEDIN_STUDIO_DATA}/language-rules/<lang>.md) or the craft checklist; NEI marks the row \"rejected\" and KEEPS it. (2) effort summary — read from the MEASURED articles.NN.phaseLog (N12), never re-estimated: number of transitions, phases that recurred (a re-run gate), and the elapsed lead time. (3) ONE process-friction question, whose answer is handed BACK to the operator — the plugin never writes the operator's own notes/register. Shape: { promoted, rejected, ruleIds: [<rules.ts id>…], languageRulesAppended, effort: { transitions, recurredPhases: [phase…], leadTimeDays }, frictionNoted: boolean, completedAt }. The full loop (capture → classify → promote → enforce) is documented domain-generally in references/fold-in-loop.md.",
"language": "Review language for this series/edition (additive, default \"en\"). Threads into the long-form review agents so they grade against THIS language's rules: language-reviewer applies Norwegian-specific checks (anglicism→Norwegian idiom, «kanselli-stil») only when language == \"no\"; voice-scrubber's gold standard is the approved editions IN this language; any other value → the agents apply that language's equivalents and never grade prose against Norwegian idiom. \"no\" = Norwegian (the author's case). Resolved at Step 1 / load-context and passed to the language-dependent agents.",
"sourceTrendId": "Per-article provenance link to the trend this edition was started from (N7 trend→newsletter bridge, MR-F3). Additive-optional (default null, NO schemaVersion bump — an edition started manually never sets it, and Step 10 reads its absence as \"no source trend\"). Set at the Step 1.5 checkpoint when Step 1's trend-intake read a candidate from the trends store (scripts/trends) to prefill the brief (angle / targetLevel / key-points / source-URLs). Its ONE runtime consumer is Step 10 (scheduling): when the article reaches scheduling AND sourceTrendId is set, the command flips that trend's store status to \"acted\" (trends CLI `act --id <sourceTrendId>`), closing the discovery→production loop deterministically instead of by hand. Value = the trend's store id (normalized title+url, as shown in the /linkedin:trends brief and `CLI list --json`).",
"targetLevel": "Per-article target-level for the edition (N10 / C-8). Additive-optional (default null, NO schemaVersion bump — same pattern as sourceTrendId/language; an edition that never resolves one leaves it null and the reader agents fall back to each persona's own `ekspertise` field). Fixes WHERE this edition sits on the configured target-level span (references/longform-quality-rules.md + config/personas.template.md): it tells persona-reviewer which end is the reader-facing (primær) end and which is the mandatory-secondary end, so «practically usable at all levels» is judged from both ends rather than one baked-in reader role. Set at Step 1 of /linkedin:newsletter — the N7 trend bridge / N6 proposal layer pre-fills it from the trend record's `targetLevel` (scripts/trends), the operator confirms or edits it during calibration, and it is persisted here at the Step 1.5 checkpoint (alongside sourceTrendId). Free-text level descriptor (e.g. \"line leader → solution engineer\", \"beginner → practitioner\"), NOT an enum — the span is domain-general and defined by the operator's persona set, never hardcoded."
@ -93,6 +95,7 @@
},
"pivots": [],
"foldIns": [],
"retro": null,
"locked": false,
"scheduled": null,
"sourceTrendId": null,

View file

@ -0,0 +1,70 @@
---
language: en
schemaVersion: 1
---
# Accumulated language rules — `<language>`
> **OPT-IN — the plugin never writes this file for you.** Copy it into your data
> dir, one file per review language, and let it grow:
>
> ```bash
> mkdir -p "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules"
> cp "${CLAUDE_PLUGIN_ROOT}/config/language-rules.template.md" \
> "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules/en.md"
> ```
>
> Name the file after the edition's `language` value (`en.md`, `no.md`, …).
> While it is absent, the shipped baseline applies alone and nothing warns.
This file is **yours**. It holds the language corrections you have made that you
do not want to make again — the judgment half of the fold-in loop
(`references/fold-in-loop.md`). Two things read it:
- **`agents/language-reviewer.md`** (Step 6.5, cold review) — grades against
these rules in addition to its five standard checks, so a correction you
confirmed once is caught every edition after.
- **`/linkedin:newsletter` Step 4** (consistency + quality) — reads it while the
draft is still being cleaned, so the defect is *prevented*, not only caught.
**`/linkedin:newsletter` Step 11** (retro) appends to it: when you promote a
judgment-class fold-in, the rule lands here with the edition that surfaced it.
Mechanical corrections do **not** come here — those become deterministic
`scripts/contract-gate` rules instead.
## Relationship to the shipped baseline
`references/longform-quality-rules.md` rule 3 (the AI-slop ban-list) is the
**baseline** and is never edited by the loop. This file **extends** it. Where the
two disagree, the stricter one wins; if you actually want to overturn a baseline
rule, say so explicitly under *Deliberate exceptions* below — an unexplained
contradiction reads as drift to the next reviewer.
## Format
One rule per bullet. Keep each rule **checkable** — a reviewer must be able to
tell from the text alone whether it was broken. Add the date and the edition that
surfaced it, so a rule that turns out to be wrong can be traced back and dropped.
```markdown
- **<what to avoid>** → <what to do instead>. _(<YYYY-MM-DD>, edition <NN>)_
```
Good: *«"leverage" as a verb → use the plain verb ("use", "build on").»*
Bad: *«write better sentences»* — nothing can check that.
---
## Rules
<!-- Step 11 appends promoted judgment-class fold-ins below this line. -->
_(empty — no rules accumulated yet)_
## Deliberate exceptions
Baseline rules you have consciously decided not to follow, **with the reason**.
Keep this short; a long list here means the baseline is wrong for your practice
and should be discussed, not quietly bypassed.
_(none)_

166
references/fold-in-loop.md Normal file
View file

@ -0,0 +1,166 @@
# The fold-in loop — how a correction made once gets enforced forever
**Capture → Classify → Promote → Enforce.** This is the plugin's answer to the
single most expensive failure mode in a long-running writing practice: you
correct the same thing in edition after edition, because the correction lives in
a chat transcript that is gone by the next session.
The loop makes a correction *stick*. You fix something in edition N; the machine
enforces it in edition N+1 without you remembering it existed.
> **Domain-general by construction.** Nothing here is specific to a topic, a
> language, or an author. The stages and the artifacts they write are the same
> for any adopter. Where this document says "the contract", read "whatever
> document holds your writing rules" — the plugin ships one
> (`references/longform-quality-rules.md`) and it is enough.
---
## The four stages
| Stage | Where it runs | What it writes | Who decides |
|-------|---------------|----------------|-------------|
| **Capture** | `/linkedin:newsletter` Steps 2.5, 3a, 5.5, 6.5 — every step where the operator corrects prose | one row in `articles.NN.foldIns[]`, `decision: "pending"` | nobody — capture is unconditional |
| **Classify** | `/linkedin:newsletter` Step 11 (retro) | `classification` on the row: `mechanical-block` / `mechanical-warn` / `judgment` | the command layer proposes, the operator confirms |
| **Promote** | `/linkedin:newsletter` Step 11 (retro) | mechanical → a `scripts/contract-gate` rule; judgment → a line in the language-rules file / the craft checklist | **the operator** (JA/NEI per row) |
| **Enforce** | Step 4.5 (contract-gate), Step 4 + `language-reviewer` (language rules) | nothing — it reads | deterministic; no model in the loop for the mechanical half |
### 1. Capture — never lose a correction
The four fold-in steps are the four places where an operator's judgment enters
the text: skeleton annotations (2.5), spine annotations (3a), editorial flags
(5.5), cold-review flags (6.5). Each appends the correction **near-verbatim**
with the trigger that surfaced it, and leaves it `pending`.
Capture is deliberately dumb and unconditional. Deciding *at capture time*
whether a correction is "worth" a rule is how the queue stays empty and the loop
dies — a five-second append is cheaper than re-discovering the rule in three
editions' time. Capture only what the operator actually corrected; do not invent
rules the operator did not ask for.
Not every correction becomes a rule. A fix that is true only of *this* edition's
facts ("that number is 4.2, not 4.4") is edition-specific and does not belong in
the queue; a fix that would be true of the *next* edition too ("never open with a
meta-sentence about what the text will do") does.
### 2. Classify — mechanical or judgment
Two kinds of correction, two different permanent homes:
- **Mechanical** — provable by string, regex, or count with ~zero false
positives ("this exact phrase is banned", "no more than one em-dash per 50
words"). It becomes a **gate rule** and is enforced before a draft ever
reaches a human.
- **Judgment** — real editorial discretion ("the conclusion overloads; it needs
one grip, not four"). No gate can prove it. It becomes a **checklist line**
read by the craft reviewer and by the drafting step.
Misclassifying a judgment call as mechanical is the expensive error: a gate rule
with false positives trains the operator to ignore the gate, and a gate nobody
trusts enforces nothing. **When unsure, classify as judgment.**
### 3. Promote — the operator gates, atomically
Each pending row gets an explicit **JA / NEI** from the operator. NEI marks the
row `rejected` — kept for traceability, never deleted, because "we considered
this and said no" is itself worth not re-discovering.
JA promotes the row **atomically**: the rule entry, the contract row that
documents it, and the manifest row that binds them are written together, and the
fold-in row is marked `promoted` with the resulting rule id. A rule that exists
in code but not in the contract is a gate enforcing something undocumented; a
contract row with no rule is a rule nobody enforces. Both are drift, and both are
what the ratify check exists to refuse:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate" && node --import tsx src/cli.ts --ratify
```
**Green ratify is what makes the promotion count.** If it is red, the promotion
is not finished.
### 4. Enforce — the loop's whole point
- Mechanical rules run at **Step 4.5** (contract-gate) on the full draft, before
the costly AI gates. A violation dies there.
- Judgment rules are read at **Step 4** (drafting/consistency) and by
**`language-reviewer`** at Step 6.5 — so they prevent the defect as well as
catch it. A rule that only ever catches is worth half a rule.
Language rules accumulate in a **user-owned, per-language file** under the
per-user data dir (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/language-rules/<lang>.md`,
template: `config/language-rules.template.md`). The shipped ban-list in
`references/longform-quality-rules.md` rule 3 stays the **baseline** and is never
edited by the loop; the user file **extends** it. Absent that file, the baseline
applies alone and nothing warns.
---
## Adopting the loop without a private writing contract
The author's own setup keeps an upstream writing contract with a rule manifest
outside this plugin. **You do not need one.** The plugin ships everything the
loop requires:
- rules as data — `scripts/contract-gate/src/rules.ts`
- the baseline craft rules — `references/longform-quality-rules.md`
- the accumulating language file — `config/language-rules.template.md`
- the in-tree craft checklists — `agents/editorial-reviewer.md`,
`agents/language-reviewer.md`
If you run the plugin without an external contract, `--ratify` has nothing to
bind against and says so plainly; promote to `rules.ts` + the language-rules file
and treat those as your contract. **That absence is not a gap** — it is the
default configuration. (Same mirror rule as `agents/editorial-reviewer.md`: an
upstream contract, when it exists, is the source of truth and the in-tree copy
must not drift from it; when it does not exist, the in-tree copy *is* the
contract.)
---
## The other accumulation silos
The fold-in loop is one of five places the plugin accumulates. Step 11 is where
they are all in view at once, because a retro that only empties one queue leaves
the operator believing the others are empty too:
| Silo | What accumulates | Written by |
|------|------------------|------------|
| **fold-in loop** (this doc) | corrections → enforced rules | Steps 2.5/3a/5.5/6.5 → Step 11 |
| **specifics-bank** (`scripts/specifics-bank`) | the operator's lived specifics + where each was used | Step 1.5 / Step 8 `record-usage` |
| **brain** (`scripts/brain`, `consolidate`) | published-only knowledge from locked editions | Step 8 |
| **voice** (`voice-trainer`, drift log) | the chronicle voice + its recurring drifts | Step 8 auto-gold / `voice-scrubber` |
| **A/B learnings** (`/linkedin:ab-test`) | which variant actually won | `/linkedin:ab-test` adopt verdicts |
Each is deterministic, each is read by an upstream phase, and each is
independently useless if never read. **A silo nothing reads is a diary, not a
loop.**
---
## Anti-patterns
- **Capturing nothing because the queue "should stay clean".** An empty queue
after a full edition means capture is broken, not that the edition was perfect.
- **Promoting everything.** A rule set that grows every edition without ever
rejecting becomes noise, and the gate loses the operator's trust.
- **Promoting without ratify.** A promotion that leaves rules and contract out of
sync has not happened, however good it felt.
- **Emptying the queue silently.** The retro promotes with an explicit verdict
per row; a queue drained by the model's own judgment is the operator's
authority quietly taken away.
- **Writing into the operator's own notes.** The retro asks one friction
question and hands the answer back. Their register is theirs.
---
## Related
- `${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate/README.md` — the deterministic
mechanical gate + the ratify binding
- `${CLAUDE_PLUGIN_ROOT}/references/longform-quality-rules.md` — the shipped
baseline rules (rule 3 = the ban-list the language file extends)
- `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json`
`articles.NN.foldIns` + `articles.NN.retro` schema
- `${CLAUDE_PLUGIN_ROOT}/commands/newsletter.md` — Steps 2.5/3a/5.5/6.5
(capture) and Step 11 (classify → promote)

View file

@ -100,7 +100,7 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); }
# with the files when adding/removing an agent, command, reference, or skill.
EXPECT_AGENTS=20
EXPECT_COMMANDS=30
EXPECT_REFS=28
EXPECT_REFS=29
EXPECT_SKILLS=6
# Pre-M0 references/ baseline was 25. Every ref doc added since is NAMED below, so the
# count bump always maps to an intended, named addition — never an incidental doc masked
@ -112,6 +112,7 @@ M0_REF="references/data-path-convention.md"
POSTM0_REFS=(
"references/trend-scoring-modes.md" # research-engine slice 2a (scoring SSOT)
"references/figure-design-guidelines.md" # MR-F4 N3 (coded-figure design rules + token convention)
"references/fold-in-loop.md" # N14 A2-F9 (capture->classify->promote->enforce, domain-general)
)
echo "================================================"
@ -1996,13 +1997,14 @@ NL_FLAT=$(tr '\n' ' ' < "$NL_N12" | tr -s ' ')
MISSING_PHASES=""
for phase in lived-specifics research skeleton-pitch spine-prose draft consistency-quality \
contract-gate factcheck-sweep editorial-review persona-sweep-prelock \
headless-review annotation visual-assets lock-delivery hook-conversion-gate scheduling; do
headless-review annotation visual-assets lock-delivery hook-conversion-gate scheduling \
retro; do
if ! printf '%s' "$NL_FLAT" | grep -qF "run the protocol (Step 0) for ${BT}${phase}${BT}"; then
MISSING_PHASES="$MISSING_PHASES $phase"
fi
done
if [ -z "$MISSING_PHASES" ]; then
pass "all 16 canonical phases carry a transition call - the lead-time log has no holes (A1-12)"
pass "all 17 canonical phases carry a transition call - the lead-time log has no holes (A1-12)"
else
fail "newsletter phases with no transition call:$MISSING_PHASES - lead time silently undercounts them (A1-12)"
fi
@ -2194,6 +2196,202 @@ fi
echo ""
# --- Section 16u: Fold-In Loop + Retro Step (N14 / A2-F6,F7,F8,F9, C-10) ---
echo "--- Fold-In Loop + Retro (N14) ---"
# N14 closes the loop the whole pipeline was missing: a correction the operator makes in
# edition N is ENFORCED in edition N+1 instead of being re-discovered. The machinery already
# existed (foldIns schema + promote->ratify); what was absent was the wiring - nothing wrote
# to the queue and no step ever emptied it. So the bindings worth linting are:
# (A2-F7 capture) every step where the operator corrects prose appends to the queue - a
# compound predicate (queue named + left open + routed), so a non-vacuity
# self-test guards it (mirrors Sections 16p/16q/16r/16s/16t).
# (A2-F8 retro) Step 11 exists and does all three jobs: promote the queue, summarize the
# measured effort (phaseLog, N12), ask ONE friction question.
# (A2-F6 headless) the cold package runs as a background agent writing to a file - the
# maximum-independence path with no manual copy-paste seam.
# (C-10 language) confirmed language corrections accumulate in a USER file the reviewer
# and the drafting step both read - the shipped ban-list stays the baseline.
# (A2-F9 adopter) the fang->klassifiser->promoter->handhev loop is documented in-tree, so
# an adopter without the author's contract still has the whole loop.
NL_N14="commands/newsletter.md"
HR_N14="commands/headless-review.md"
LR_N14="agents/language-reviewer.md"
EST_N14="config/edition-state.template.json"
LANG_TPL="config/language-rules.template.md"
FOLDIN_REF="references/fold-in-loop.md"
foldin_capture_wired() { # $1 = text; wired iff it NAMES the queue, leaves it OPEN, and ROUTES it
echo "$1" | grep -qF "foldIns" \
&& echo "$1" | grep -qF "pending" \
&& echo "$1" | grep -qF "Step 11"
}
FI_SELFTEST_OK=1
if ! foldin_capture_wired "append the correction to articles.NN.foldIns with decision pending; Step 11 routes it"; then
FI_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired fold-in-capture probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if foldin_capture_wired "$probe"; then
FI_SELFTEST_OK=0; echo " false-positive FAIL: under-wired fold-in-capture probe accepted -> $probe"
fi
done <<'NEGATIVE16U'
append the correction to articles.NN.foldIns with decision pending, routed by nobody
capture the correction as pending and let Step 11 route it, queue left unnamed
append it to articles.NN.foldIns and Step 11 promotes it, never marked open
NEGATIVE16U
if [ "$FI_SELFTEST_OK" -eq 1 ]; then
pass "fold-in-capture self-test: predicate needs foldIns + pending + Step 11 (1 accepted, 3 under-wired rejected)"
else
fail "fold-in-capture self-test failed - the N14 wiring lint is vacuous or over-eager"
fi
# (A2-F7 capture) each of the four fold-in steps feeds the queue - a step that folds a
# correction in without capturing it is exactly how a rule gets re-discovered next edition
STEP25_N14=$(awk '/^## Step 2.5:/{f=1} /^## Step 3a:/{f=0} f' "$NL_N14")
STEP3A_N14=$(awk '/^## Step 3a:/{f=1} /^## Step 3b:/{f=0} f' "$NL_N14")
STEP55_N14=$(awk '/^## Step 5.5:/{f=1} /^## Step 6:/{f=0} f' "$NL_N14")
STEP65_N14=$(awk '/^## Step 6.5:/{f=1} /^## Step 7:/{f=0} f' "$NL_N14")
STEP11_N14=$(awk '/^## Step 11:/{f=1} /^## Distribution channel/{f=0} f' "$NL_N14")
if foldin_capture_wired "$STEP25_N14"; then
pass "Step 2.5 captures operator corrections into the fold-in queue (A2-F7)"
else
fail "newsletter Step 2.5 folds annotations in without capturing them - the rule is lost (A2-F7)"
fi
if foldin_capture_wired "$STEP3A_N14"; then
pass "Step 3a captures operator corrections into the fold-in queue (A2-F7)"
else
fail "newsletter Step 3a folds annotations in without capturing them - the rule is lost (A2-F7)"
fi
if foldin_capture_wired "$STEP55_N14"; then
pass "Step 5.5 captures approved editorial flags into the fold-in queue (A2-F7)"
else
fail "newsletter Step 5.5 folds flags in without capturing them - the rule is lost (A2-F7)"
fi
if foldin_capture_wired "$STEP65_N14"; then
pass "Step 6.5 captures approved cold-review flags into the fold-in queue (A2-F7)"
else
fail "newsletter Step 6.5 folds flags in without capturing them - the rule is lost (A2-F7)"
fi
# (A2-F8 retro) the step exists and empties the queue with an explicit operator verdict
if echo "$STEP11_N14" | grep -qF "foldIns" && echo "$STEP11_N14" | grep -qF "promoted"; then
pass "Step 11 promotes the fold-in queue with an operator verdict (A2-F8)"
else
fail "newsletter has no Step 11 fold-in promotion - the queue fills up and is never emptied (A2-F8)"
fi
# (A2-F8 effort) the effort summary reads the MEASURED log (N12), not the model's memory
if echo "$STEP11_N14" | grep -qF "phaseLog"; then
pass "Step 11 summarizes effort from the measured phaseLog - no new measuring apparatus (A2-F8/A1-12)"
else
fail "newsletter Step 11 does not read phaseLog - effort is re-estimated from memory (A2-F8)"
fi
# (A2-F8 friction) exactly one friction question, and the operator's own register stays theirs
if echo "$STEP11_N14" | grep -qF "process friction" && echo "$STEP11_N14" | grep -qF "never write"; then
pass "Step 11 asks one friction question and never writes the operator's register (A2-F8)"
else
fail "newsletter Step 11 lacks the single friction question or claims write access to the operator's register (A2-F8)"
fi
# (A2-F8 transition) retro is a real phase - logged like every other, and it re-closes the row
if echo "$STEP11_N14" | grep -qF "register-complete" && echo "$STEP11_N14" | grep -qF "${BT}retro${BT}"; then
pass "Step 11 runs the retro transition and re-closes the register row (A1-11/A2-F8)"
else
fail "newsletter Step 11 leaves the edition re-opened in the register or logs no retro phase (A1-11/A2-F8)"
fi
# (A2-F8 schema) edition-state documents AND carries the retro slot
if grep -qF '"retro": "Per-article retro record' "$EST_N14" && grep -qF '"retro": null' "$EST_N14"; then
pass "edition-state documents and carries the per-article retro slot (A2-F8 schema)"
else
fail "$EST_N14 has no documented retro slot - the retro leaves no durable record (A2-F8)"
fi
# (A2-F8 resumption) an edition aborted after scheduling resumes INTO the retro, not past it
RESUME_N14=$(awk '/^### Deterministic resumption/{f=1} /^### Phase-transition protocol/{f=0} f' "$NL_N14")
if echo "$RESUME_N14" | grep -qF "Step 11" && echo "$RESUME_N14" | grep -qF "${BT}retro${BT}"; then
pass "resumption table routes scheduling -> Step 11 and retro -> complete (A2-F8)"
else
fail "newsletter resumption table skips the retro - an aborted session drops the fold-in queue (A2-F8)"
fi
# (A2-F6 headless) the cold package can run as a background agent writing its report to a file
if grep -qF "run_in_background" "$HR_N14" && grep -qF -- "--background" "$HR_N14"; then
pass "headless-review offers a background run - cold isolation without a fresh session (A2-F6)"
else
fail "$HR_N14 has no background mode - maximum independence still costs a manual session switch (A2-F6)"
fi
if grep -qF "no copy-paste" "$HR_N14"; then
pass "headless-review states the seam it removes - the report is read from disk, not pasted (A2-F6)"
else
fail "$HR_N14 still relies on pasting the report back into the drafting session (A2-F6)"
fi
# (A2-F6 wiring) Step 6.5 actually offers that path instead of the paste-back one
if echo "$STEP65_N14" | grep -qF -- "--background"; then
pass "newsletter Step 6.5 routes maximum-independence through the background run (A2-F6)"
else
fail "newsletter Step 6.5 still sends the operator to a fresh session with a paste-back (A2-F6)"
fi
# (C-10 language) the accumulating user file is READ by the reviewer and by the drafting step
if grep -qF "language-rules/" "$LR_N14"; then
pass "language-reviewer reads the accumulated user language rules (C-10)"
else
fail "$LR_N14 grades only against the shipped list - confirmed corrections never bind (C-10)"
fi
STEP4_N14=$(awk '/^## Step 4: /{f=1} /^## Step 4.5:/{f=0} f' "$NL_N14")
if echo "$STEP4_N14" | grep -qF "language-rules/"; then
pass "newsletter Step 4 reads the accumulated user language rules while drafting (C-10)"
else
fail "newsletter Step 4 ignores the accumulated language rules - they only catch, never prevent (C-10)"
fi
if echo "$STEP11_N14" | grep -qF "language-rules/"; then
pass "Step 11 appends confirmed language corrections to the user rules file (C-10)"
else
fail "newsletter Step 11 never writes the language-rules file - judgment corrections evaporate (C-10)"
fi
# (C-10 opt-in) the file is offered as a template, never written for the user (slots pattern)
if [ -f "$LANG_TPL" ] && grep -qF "OPT-IN" "$LANG_TPL"; then
pass "language-rules template ships with an explicit opt-in - nothing is imposed (C-10/de-niche)"
else
fail "$LANG_TPL missing or does not state the opt-in (C-10/de-niche)"
fi
# (A2-F9 adopter) the whole loop is documented in-tree, all four stages named
FOLDIN_LOOP_OK=1
for stage in "Capture" "Classify" "Promote" "Enforce"; do
grep -qF "$stage" "$FOLDIN_REF" 2>/dev/null || FOLDIN_LOOP_OK=0
done
if [ -f "$FOLDIN_REF" ] && [ "$FOLDIN_LOOP_OK" -eq 1 ]; then
pass "fold-in loop documented in-tree with all four stages (A2-F9)"
else
fail "$FOLDIN_REF missing or does not name capture/classify/promote/enforce (A2-F9)"
fi
# (A2-F9 self-contained) an adopter without the author's private contract still has the loop,
# and the four accumulation silos are named as one collection point
if grep -qF "not a gap" "$FOLDIN_REF" 2>/dev/null \
&& grep -qF "specifics-bank" "$FOLDIN_REF" 2>/dev/null \
&& grep -qF "ab-test" "$FOLDIN_REF" 2>/dev/null; then
pass "fold-in loop is adopter-self-contained and names the accumulation silos (A2-F9)"
else
fail "$FOLDIN_REF reads as requiring the author's private contract, or omits the silos (A2-F9)"
fi
echo ""
# --- Section 18: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count
@ -2245,12 +2443,19 @@ echo ""
# wiring grep + calendar opt-in grep + session-start two-block compound grep + coverage
# queue+register grep + data-dir placement grep + Step-10 nextFreeSlot grep + Step-10 --slot
# grep + scheduling-ref directional grep + scheduling-ref operator-grid grep + slot-template
# schema/opt-in grep + de-niche no-hardcoded-times/no-committed-grid check) = 179.
# schema/opt-in grep + de-niche no-hardcoded-times/no-committed-grid check) = 179; +19 for
# N14's nineteen UNCONDITIONAL Section-16u checks (fold-in-capture self-test + four capture
# greps for Steps 2.5/3a/5.5/6.5 + Step-11 promotion grep + Step-11 phaseLog grep + Step-11
# friction/never-write grep + Step-11 retro-transition/re-close grep + edition-state retro
# doc+slot grep + resumption retro-routing grep + headless background-mode grep + headless
# no-copy-paste grep + Step-6.5 background-routing grep + language-reviewer rules-file grep +
# Step-4 rules-file grep + Step-11 rules-file append grep + language-rules template opt-in
# check + fold-in-loop four-stage check + fold-in-loop adopter/silos grep) = 198.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop
# the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the
# deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh
# clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=179
ASSERT_BASELINE_FLOOR=198
TOTAL_CHECKS=$((PASS + FAIL))
if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then
pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"