feat(linkedin-studio): wire lived-specifics extraction into newsletter pipeline (fix #2 slice 3)

Wire the kilde-så-draft binding (slices 1–2) into the live /linkedin:newsletter
pipeline so a draft is grounded in the operator's real material BEFORE any prose.

- New Step 1.5 "Lived-specifics extraction" BETWEEN brief (1) and research (2):
  query the bank → 3–5 sharp elicitation questions (vagueness refused, never
  fabricated — invariant 3) → `add` to the specifics-bank → populate the
  articles.NN.livedSpecifics slot-map → render the NN-kilder.md ledger. First
  phase to write machine state (persists brief + personas + binding).
- Deterministic binding-gate (`validate-binding`, exit 1 = BLOCK) wired as a new
  step 5 of the Step 2.5 skeleton-gate (renumbered 5→9): no prose is written on
  an unresolved load-bearing claim; livedSpecifics → "bound" on pass.
- Step 2 re-scoped BY the binding: research fills the ekstern/unresolved slots +
  verifies the operator's own numbers (regel 6/7) → flips bank unverified→verified
  → re-renders NN-kilder.md, instead of re-sourcing what is already grounded.
- Pipeline 17 → 18 phases: new `lived-specifics` phase id in edition-state
  _doc.phases; resumption table + pipeline overview + CLAUDE.md + README updated.
- Lint: new Section 15 "Specifics-Bank Binding" wires the specifics-bank suite
  into CI (suite green + case-count floor 28, KTG-only/adopter-skip); the
  assertion-count anti-erosion floor moves to Section 16.

Verification: structure gate 84/0/0 (was 83), specifics-bank 28/28, contract-gate
ratify 1:1, edition-state template valid JSON (18 phases), gitleaks clean on all
5 files. Design record: docs/fix2/slice2-binding.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-20 22:29:01 +02:00
commit 527391ab47
5 changed files with 255 additions and 44 deletions

View file

@ -61,7 +61,7 @@ All content commands (post, quick, react, pipeline, first-post, video, multiplat
| `/linkedin:post` | Full post creation (10-15 min) |
| `/linkedin:quick` | 5-minute quick post (3-line formula) + 8 post-type templates |
| `/linkedin:pipeline` | Full end-to-end content pipeline |
| `/linkedin:newsletter` | Long-form orchestrator: newsletter edition / essay / series article — multi-session 17-phase pipeline with **skeleton + spine-prose gates BEFORE prose (v2.1)**, a **deterministic §B/§C1 contract-gate BEFORE the AI sweeps (Step 4.5)**, **editorial-review craft gate BEFORE the persona sweep (Step 5.5, v2.4)**, fact-check + persona-sweep BEFORE lock, **headless adversarial review BEFORE lock (Step 6.5, v3.1)**, and **visual-assets gate BEFORE lock (Step 7.5, v2.3)** |
| `/linkedin:newsletter` | Long-form orchestrator: newsletter edition / essay / series article — multi-session 18-phase pipeline with **lived-specifics extraction BEFORE research (Step 1.5, Fix #2 — kilde-så-draft; binding-gate enforced at Step 2.5)**, **skeleton + spine-prose gates BEFORE prose (v2.1)**, a **deterministic §B/§C1 contract-gate BEFORE the AI sweeps (Step 4.5)**, **editorial-review craft gate BEFORE the persona sweep (Step 5.5, v2.4)**, fact-check + persona-sweep BEFORE lock, **headless adversarial review BEFORE lock (Step 6.5, v3.1)**, and **visual-assets gate BEFORE lock (Step 7.5, v2.3)** |
| `/linkedin:headless-review` | **(v3.1)** Cold adversarial review package — run the 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` (resonance/conversion) on a FROZEN draft with no drafting-session context; consolidated, operator-gated report. Step 6.5's standalone surface (run in a fresh session for maximum isolation) |
| `/linkedin:pivot` | **(v3.1)** Re-open a long-form edition after a late substantive change so cleared gates (fact-check → editorial → persona → headless) re-run before lock; logs `pivots[]`, resets `currentPhase`, un-locks if needed (pivot heuristic: >20 % word-count change or >2 new sections) |
| `/linkedin:batch` | Create a full week of content |

View file

@ -45,7 +45,7 @@ For everyday presence. The point is *velocity without losing quality*: every con
### 📖 The Long-Form Engine — `/linkedin:newsletter`
For the pieces that build authority — newsletter editions, essays, series articles. This is what sets LinkedIn Studio apart from "AI writes your post" tools: a **17-phase pipeline where the draft has to survive a gauntlet of quality gates *before it locks*.**
For the pieces that build authority — newsletter editions, essays, series articles. This is what sets LinkedIn Studio apart from "AI writes your post" tools: an **18-phase pipeline where the draft is grounded in your real material *before research* (lived-specifics extraction) and then has to survive a gauntlet of quality gates *before it locks*.**
```
skeleton gate ──▶ voice scrub ──▶ fact-check ──▶ editorial craft gate ──▶
@ -133,7 +133,7 @@ All 29 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. Th
| `/linkedin:multiplatform` | Adapt LinkedIn content for X threads, newsletter sections, blog posts, slides, YouTube scripts. |
| `/linkedin:batch` | A full week of content in one session — one theme in, 35 posts out, written to the queue. |
| `/linkedin:pipeline` | Full end-to-end pipeline from idea to published post (ideation → publish → post-analysis). |
| `/linkedin:newsletter` | Long-form orchestrator — newsletter/essay/series article at series quality. Multi-session 17-phase pipeline; all gates BEFORE lock. |
| `/linkedin:newsletter` | Long-form orchestrator — newsletter/essay/series article at series quality. Multi-session 18-phase pipeline; lived-specifics grounding BEFORE research, all gates BEFORE lock. |
| `/linkedin:headless-review` | Cold adversarial review package on a FROZEN draft (`content-reviewer` + `language-reviewer` + `fact-reviewer` + `persona-reviewer`) — run in a fresh session for maximum isolation. |
| `/linkedin:pivot` | Re-open a long-form edition after a substantive late change so cleared gates re-run before lock (heuristic: >20 % word-count or >2 new sections). |

View file

@ -22,7 +22,8 @@ allowed-tools:
# LinkedIn Newsletter — Long-Form Content Engine
> **⏱ Before you start — this is a campaign, not a post.** Producing one edition is
> a **multi-session, multi-gate process (17 phases)**: research → skeleton gate →
> a **multi-session, multi-gate process (18 phases)**: lived-specifics extraction →
> research → skeleton gate →
> spine gate → full draft → contract-gate → fact-check → editorial craft gate →
> persona sweep → cold headless review → visual assets → lock → hook-conversion
> gate → schedule.
@ -82,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 (17 phases)
## Pipeline overview (18 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
@ -96,7 +97,8 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
|------|-------|------|-------|
| 0 | **Load context** | edition-state + `<serie>/STATE.md`, voice profile, persona library, series brief | `Read` |
| 1 | **Brief + calibration** | angle, voice, audience personas (mark primær), key points, tone, leader-takeaway. ≤3 questions | `AskUserQuestion` |
| 2 | **Research** | parallel scoped mandates → verified notes; triangulation | **`Task` fan-out (foreground)** |
| 1.5 | **Lived-specifics extraction — BEFORE research** | elicitation interview (35 sharp questions, **vagueness refused**) → `add` lived material to the specifics-bank + populate `articles.NN.livedSpecifics` slot-map (each load-bearing key-point bound specific/abstrakt/ekstern/unresolved) → render `NN-kilder.md`. The grounded-sourcing layer (Fix #2, kilde-så-draft; never fabricated — invariant 3). | `AskUserQuestion` + `scripts/specifics-bank` (`node --import tsx src/cli.ts`) |
| 2 | **Research — scoped by the binding** | parallel scoped mandates → verified notes; triangulation. Fills the `ekstern`/`unresolved` slots + verifies your own numbers (regel 6/7) | **`Task` fan-out (foreground)** |
| 2.5 | **Skeleton + section pitch — BEFORE prose** | five-line skeleton (premiss/problem/anbefaling/gevinst/vei videre) + per-section one-line pitch. Operator-gate JA/NEI/REVIDER. Persona-skjelett-sweep before any prose is written. | `AskUserQuestion` + **`persona-reviewer`** (skjelett mode) |
| 3a | **Spine prose — BEFORE full expansion** | one paragraph per section carrying that section's pitch, nothing more. ~2030 % of final length. Operator-gate on whether the axis is right now that there is prose on it. | inline drafting + `content-repurposer` |
| 3b | **Full prose expansion** | expand each section with argument, examples, anchors from research; may span sessions | `content-repurposer` + `Task` |
@ -112,9 +114,11 @@ split; v3.1 / Endring 9 on adversarial independence + framing-bias).
| 9 | **Hook / conversion gate** | persona gate on the distribution text post-lock: "would YOU click?" | **`persona-reviewer`** (conversion mode) |
| 10 | **Scheduling** | register the edition in the plugin queue/state for native scheduling | `hooks/scripts/queue-manager.mjs` |
> **Build status:** all 17 phases (Steps 02.5, 3a, 3b, 4, 4.5, 5, 5.5, 6, 6.5,
> 7, 7.5, 810) are implemented below. This command takes an edition end-to-end:
> load → calibration → verified research → **skeleton + section pitch (operator +
> **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
> end-to-end:
> load → calibration → **lived-specifics extraction (elicitation + bank binding,
> BEFORE research)** → verified research (scoped by the binding) → **skeleton + section pitch (operator +
> persona gate BEFORE prose)** → **spine prose (operator gate BEFORE full
> expansion)** → full prose draft → consistency/quality → **contract-gate (deterministic §B/§C1)** → fact-check sweep →
> **editorial review (craft gate, operator-gated BEFORE the persona sweep)**
@ -206,7 +210,8 @@ Look up `edition-state.json` → `articles.<currentArticle>` (and the top-level
|---------------------------------|-----------|
| *(no state file)* | **NEW edition** → Step 1 (init state at end of Step 2) |
| `load-context` | Step 1 — Brief + calibration |
| `brief-calibration` | Step 2 — Research |
| `brief-calibration` | Step 1.5 — Lived-specifics extraction *(Fix #2 — elicitation + bank binding BEFORE research)* |
| `lived-specifics` | Step 2 — Research *(scoped by the binding)* |
| `research` | Step 2.5 — Skeleton + section pitch *(v2.1 — skeleton gate BEFORE prose)* |
| `skeleton-pitch` | Step 3a — Spine prose *(v2.1 — one paragraph per section, BEFORE full expansion)* |
| `spine-prose` | Step 3b — Full prose expansion |
@ -281,7 +286,8 @@ Settle these dimensions (most should come from context, not questions):
(accept it), but a *primær* NO is never accepted (revise until a clean YES).
**Persist** the resolved set to `edition-state.json`
`articles.NN.personas` (each entry: name, tier, the five fields, source) at the
Step 2 checkpoint — it is then stable across sessions and is the single source
**Step 1.5 checkpoint** (the first machine-state write; Step 2 then adds the
research notes) — it is then stable across sessions and is the single source
every later sweep reads. See `config/personas.template.md`
"Per-artifact personas".
- **Key points** — the 24 load-bearing claims the edition must make.
@ -305,6 +311,128 @@ Edition brief
- Leader-takeaway: <one takeaway + one action>
```
## Step 1.5: Lived-specifics extraction — BEFORE research
> **The grounded-sourcing layer (Fix #2, kilde-så-draft).** Sourcing, not styling,
> is where authenticity is won. A draft that starts from your real number / named
> case / what-actually-broke is forwardable; a draft that invents plausible filler
> is hollow, and every downstream voice gate then only polishes hollow content. So
> BEFORE research and BEFORE prose, each load-bearing claim is bound to real
> material — or you *consciously* decide it does not need a concrete instance. This
> step is that binding; **vagueness is refused** (drømme-spec G4). The plugin never
> invents lived experience (invariant 3).
> **Order assertion (enforced).** Step 1.5 runs AFTER the brief (Step 1) and BEFORE
> research (Step 2). It populates the slot-map and renders the `NN-kilder.md`
> ledger; the BLOCKing «vaghet avvises» gate (`validate-binding`) is enforced at
> the **Step 2.5 skeleton-gate**, once the spine is visible. Research (Step 2) is
> then scoped BY this binding — it fills the `ekstern`/`unresolved` gaps and
> verifies your own numbers, instead of re-sourcing what is already grounded. `[GATE]`
> **Adopter note.** This step uses the KTG-internal specifics-bank
> (`${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank` — run `npm install` there once).
> If its deps are absent (a plain adopter ships none), this Step is a **no-op**:
> skip to Step 2 and supply specifics inline during drafting. It never blocks an
> adopter.
**Procedure:**
1. **Surface inventory you already have.** Derive the edition's topic tags from the
brief (Step 1 angle + key points), and query the bank for lived material that
already matches — so the interview asks only for what is genuinely missing. The
CLI runs from its own dir (where `tsx` lives):
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
node --import tsx src/cli.ts query --tags "<tag1,tag2,…>"
```
Show the hits to the operator; each is a candidate binding for a load-bearing
key-point.
2. **Elicitation interview — 35 sharp questions, vagueness refused.** For each
load-bearing key-point (Step 1) NOT already covered by inventory, ask one tight
question that demands a *concrete instance*, not a generality. Pick the shape the
claim needs (one per question):
- a **number** you measured ("how many / how much / how long — your figure"),
- a **named case** ("which project / tool / team — name it"),
- a **what-broke** ("what actually failed, concretely"),
- a **contrarian** point ("where do you disagree with the consensus, and why"),
- a **mind-change** ("what did you used to believe and stop — what flipped it").
**Refuse vagueness.** If the answer is a generality ("it's generally faster",
"people struggle with this"), push back once for the concrete instance. If the
operator genuinely has none, that key-point becomes an `abstrakt` slot (with a
stated rationale) or an `ekstern` slot (research will source it) — **never a
fabricated specific** (invariant 3). The plugin elicits your raw material; it
does not invent it.
3. **Fold elicited material into the bank.** For each concrete answer, `add` it so
it is reusable across editions (content-hash dedupe + tag-union are automatic):
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
node --import tsx src/cli.ts add \
--type <number|named-case|what-broke|contrarian|mind-change|other> \
--content "<verbatim — the operator's own words>" --tags "<tags>" \
[--source "<where it came from>"] [--verification <verified|unverified|n/a>]
```
A figure the operator states from memory is `--verification unverified` — it
stays guilty-until-checked, and Step 2 verifies it (regel 6/7).
4. **Populate the slot-map.** Write `articles.NN.livedSpecifics` in
`edition-state.json` — one `kind: "key-point"` slot per load-bearing claim, each
bound to exactly one of:
- `{ type: "specific", specificId: "<bank id>" }` — grounded in lived material;
- `{ type: "abstrakt", rationale: "<why no concrete instance is needed here>" }`;
- `{ type: "ekstern", source?: "<research ref — Step 2 fills it>" }` — to be
sourced, not lived;
- `{ type: "unresolved" }` — a temporary placeholder only; this is the vagueness
the Step 2.5 gate BLOCKs, so resolve it before then.
Set `status: "pending"` (Step 2.5 flips it to `"bound"` when the gate passes).
The shape is documented in `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json`
(`_doc.livedSpecifics`).
5. **Render the `NN-kilder.md` ledger.** Deterministically render the read-only
sources artifact from the slot-map + bank (NN = the same zero-padded edition
number as `NN-utkast.md`). Pass absolute paths:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
node --import tsx src/cli.ts render-kilder \
--edition "<abs>/linkedin/edition-state.json" \
--out "<abs>/linkedin/NN-kilder.md"
```
It is regenerated like POST.html, never hand-edited. Surface it to the operator
so the binding is visible (coverage by type + the numbers-to-verify line).
6. **Persist + checkpoint state.** Step 1.5 is the **first phase that writes machine
state**, so:
- If `edition-state.json` does not exist yet (fresh edition), initialize it from
the template schema (`${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json`).
- Persist the resolved **brief** (Step 1) and **personas** (the set + primær)
into `articles.NN` — this is the first durable write of both (Step 2 then adds
the research notes).
- Write `articles.NN.livedSpecifics` (the slot-map above, `status: "pending"`).
- Set `currentPhase: "lived-specifics"` in `edition-state.json`.
- Write a "lived-specifics bound → next: research (scoped by the binding)" line
to `<serie>/STATE.md` (overwrite).
```
Lived-specifics extraction (BEFORE research) — complete.
- Inventory hits reused: <N> New material elicited + added to bank: <N>
- Slot-map: <total> slots — levd <N> · abstrakt <N> · ekstern <N> · uavklart <N>
- Numbers to verify (regel 6/7): <N> (carried to Step 2)
- Ledger rendered: <serie>/linkedin/NN-kilder.md
- Vagueness refused: <N or none> (never fabricated — invariant 3)
Next: Step 2 — Research (scoped by the binding: fill ekstern/unresolved, verify numbers).
```
---
## Step 2: Research — parallel `Task` fan-out (foreground)
> **This is the load-bearing phase.** Quality long-form needs verified, triangulated
@ -313,12 +441,22 @@ Edition brief
**Procedure:**
1. **Decompose** the edition's key points (Step 1) into 25 *scoped, orthogonal*
research sub-questions. Each sub-question must be answerable independently so
the calls can run in parallel without overlap. Reuse the multi-source synthesis
discipline from `commands/react.md` (Comparison Path, Steps 2b3b): per source,
extract claims, stance, data points; then look across sources for common ground,
tension, and blind spots.
0. **Scope by the binding, not the raw key points (Fix #2 re-scope).** Read
`articles.NN.livedSpecifics` (Step 1.5). Research targets the **gaps the binding
names** — slots bound `ekstern` (and any still `unresolved` at entry): the claims
that explicitly need an external source. Slots bound to a `specific` are already
grounded in lived material; do **not** re-source their substance (you may
corroborate, but it is not the gap). Every **`unverified-number`** warning from
the Step 1.5 binding is also a research target: confirm the operator's own figure
against a primary source (regel 6/7). This is what makes research *fill* the
draft instead of re-deriving what the operator already knows.
1. **Decompose** the binding's `ekstern`/`unresolved` slots + unverified numbers
(step 0) into 25 *scoped, orthogonal* research sub-questions. Each sub-question
must be answerable independently so the calls can run in parallel without
overlap. Reuse the multi-source synthesis discipline from `commands/react.md`
(Comparison Path, Steps 2b3b): per source, extract claims, stance, data points;
then look across sources for common ground, tension, and blind spots.
2. **Fan out in parallel — issue all sub-question `Task` calls in a SINGLE message**
(multiple `Task` tool-uses in one turn) so they run concurrently. Each call gets
@ -353,21 +491,27 @@ Edition brief
source(s) and a confidence marker. Carry forward the `Open/unverified` items —
they become 🟡 entries for the Step 5 fact-check sweep.
5. **Persist + checkpoint state.** Write the resolved brief (Step 1), the
resolved **per-article personas** (`articles.NN.personas` — the set + primær
confirmed/defined in Step 1), and the verified research notes into the
edition's `edition-state.json` (`currentPhase: "research"`, article status
`in-progress`) and append a
5. **Persist + checkpoint state.** Write the verified research notes into
`edition-state.json` (the brief + personas were first persisted at the Step 1.5
checkpoint — re-affirm them here if calibration changed). **Update the binding
from what research resolved:** when a sub-question sources an `ekstern` slot, set
its `source`; when it confirms an operator figure, flip the bank specific
`unverified → verified` (the bank's `add`/verify path), then **re-render
`NN-kilder.md`** so the ledger stays current (a number that could not be verified
stays `unverified` and is carried to the Step 5 fact-check sweep as 🟡 — never
silently asserted). Set `currentPhase: "research"`, article status
`in-progress`, and append a
"research complete → next: skeleton + section pitch (BEFORE prose)" next-step
line to `<serie>/STATE.md` (overwrite). If this is a fresh edition, initialize
`edition-state.json` from the template schema first. Stop cleanly here if
context budget is tight — Step 2.5 begins in the next session; otherwise
Step 2.5 may run inline (it is short and operator-interactive).
line to `<serie>/STATE.md` (overwrite). If somehow no `edition-state.json` exists
yet (Step 1.5 was a no-op for an adopter), initialize it from the template schema
first. Stop cleanly here if context budget is tight — Step 2.5 begins in the next
session; otherwise Step 2.5 may run inline (it is short and operator-interactive).
```
Research phase complete.
- Sub-questions: <N> (ran in parallel)
- Sub-questions: <N> (ran in parallel; scoped to ekstern/unresolved slots + numbers)
- Verified notes: <N> by key point
- Binding updated: ekstern slots sourced <N> · numbers verified <N> → NN-kilder.md re-rendered
- Carried to fact-check (🟡 unverified): <N>
State written: <serie>/linkedin/edition-state.json (phase: research)
Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE prose).
@ -386,10 +530,11 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
> **Order assertion (enforced).** Step 2.5 runs AFTER research (Step 2) and
> BEFORE any prose (Step 3a). No section of the draft is written — not even
> spine prose — until the operator says JA on the skeleton and the
> spine prose — until the operator says JA on the skeleton, the **deterministic
> binding-gate** passes (every load-bearing claim grounded — Fix #2), and the
> persona-skjelett-sweep returns a clean primær JA. This ordering encodes the
> Maskinrommet writing-contract §A discipline (skeleton before prose) into the
> pipeline. `[GATE]`
> Maskinrommet writing-contract §A discipline (skeleton + sources before prose)
> into the pipeline. `[GATE]`
**Procedure:**
@ -454,17 +599,47 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
JA proceed, or another round?». `AskUserQuestion` (JA / REVIDER / NEI) is
also the **fallback** gate when rendering is unavailable; it is not the
primary flow:
- **JA** — proceed to the persona-skjelett-sweep (step 5).
- **JA** — proceed to the binding-gate (step 5), then the
persona-skjelett-sweep (step 6).
- **REVIDER** — another annotation round; revise and re-render.
- **NEI** — the skeleton is wrong at a load-bearing level (premise unsound,
argument-line incoherent). Return to brief calibration (Step 1) or
research (Step 2) to surface the missing piece.
Do not proceed past this gate without an explicit JA. The pipeline may not
advance to Step 3a (spine prose) until both this operator-gate AND the
persona-skjelett-sweep below return JA. `[OPERATØR]`
advance to Step 3a (spine prose) until this operator-gate, the deterministic
binding-gate, AND the persona-skjelett-sweep below all pass. `[OPERATØR]`
5. **Persona-skjelett-sweep — fan out `persona-reviewer` in skjelett-mode.**
5. **Binding-gate — deterministic «vaghet avvises» (Fix #2, BEFORE the persona
sweep).** Now that the spine is visible, enforce the Step 1.5 binding: every
load-bearing slot must resolve to a real specific, a justified `abstrakt`, or an
`ekstern` source — no `unresolved` slot, no `specific` pointing at a missing bank
id. It is pure/deterministic (no model in the loop), so the same vagueness is
caught the same way every time. Run it from the package dir; pass the edition
state's **absolute** path:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank" && \
node --import tsx src/cli.ts validate-binding \
--edition "<abs>/linkedin/edition-state.json"
```
- **exit 0** → PASS. Surface any **WARN** (`unverified-number`, regel 6/7) as
direction — a number still unverified after Step 2 research is carried to the
Step 5 fact-check sweep as 🟡, never silently asserted.
- **exit 1** → BLOCKED. The output names each blocking slot (`unresolved` /
`dangling-specific` / `unjustified-abstrakt`). Resolve it — elicit the missing
specific (loop back to Step 1.5 step 2), fix the dangling id, or give the
`abstrakt` escape a real rationale — then **re-render `NN-kilder.md`** and
re-run until exit 0. No prose is written on an unresolved load-bearing
claim. `[GATE]`
- deps/contract absent (adopter — Step 1.5 was a no-op) → skip with a one-line
note; the persona-skjelett-sweep below still runs.
When the gate passes, set `articles.NN.livedSpecifics.status: "bound"` in
`edition-state.json`.
6. **Persona-skjelett-sweep — fan out `persona-reviewer` in skjelett-mode.**
Issue one `persona-reviewer` call per active persona in parallel — a SINGLE
message with multiple `Task` tool-uses, `subagent_type:
linkedin-studio:persona-reviewer`, from THIS command layer in
@ -474,7 +649,7 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
This is NOT resonans mode (Step 6 — that runs on full prose) and NOT
konverter mode (Step 9 — that judges the hook only).
6. **Collect skjelett verdicts and gate.** Each call returns per-axis flags
7. **Collect skjelett verdicts and gate.** Each call returns per-axis flags
(HOLDER/TVILER/MANGLER), ≤3 direction-only flags, a section-pitch check
(any pitch that does not pay in), a per-persona verdict (JA/NEI), and a
gate decision. Aggregate per the agent's rule:
@ -493,17 +668,19 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
the editor (this session) holds the pen; never paste a persona's rewritten
skeleton. `[GATE]`
7. **Convergence loop.** If gate is REWORK/BLOCK, fold flags into the
8. **Convergence loop.** If gate is REWORK/BLOCK, fold flags into the
skeleton + pitches (or, on BLOCK, return upstream) and re-run the same
`persona-reviewer` calls against the revision. Loop until the primær
returns a clean JA. This loop is **cheap and frequent at this stage**
every round saved here is hours saved at the prose stage.
8. **Persist + checkpoint state.** Once the skeleton is JA from both operator
AND persona-skjelett-sweep, record:
9. **Persist + checkpoint state.** Once the skeleton is JA from the operator, the
binding-gate passed, AND the persona-skjelett-sweep returns a clean primær JA,
record:
- The final skeleton + pitches in `<serie>/NN-skjelett.md` (already written
in step 3, with any in-loop revisions applied).
- `articles.NN.livedSpecifics.status: "bound"` (set by the binding-gate, step 5).
- Per-persona skjelett verdicts in
`edition-state.json``articles.NN.personaSweep.skeleton` (or alongside
resonance/conversion under the same `personaSweep` object).
@ -517,10 +694,11 @@ Skeleton + section pitch (BEFORE prose) — complete.
- Skeleton: 5 lines (premiss / problem / anbefaling / gevinst / vei videre)
- Section pitches: <N> sections, all paying into the spine (else: pitches reworked, see flags)
- Operator gate: JA (after <N> revision rounds)
- Binding-gate (vaghet avvises): PASS — all load-bearing slots bound; livedSpecifics → "bound" (else BLOCK: <N> unresolved/dangling)
- Persona-skjelett-sweep: primær JA (else: still NEI — loop open, NOT ready for prose)
- Convergence rounds: <N>
- Accepted sekundær ceiling-NOs (signal, not failure): <N or none>
Gate: [PASS — primær JA, ready for spine prose] (else REWORK/BLOCK)
Gate: [PASS — primær JA + binding bound, ready for spine prose] (else REWORK/BLOCK)
Next: Step 3a — Spine prose (one paragraph per section, BEFORE full expansion).
```
@ -1642,7 +1820,8 @@ the honest decision surface; it sells nothing.
## Reference Files
- `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json` — edition-state schema (17 phases including the deterministic §B/§C1 contract-gate (Step 4.5), v2.1 skeleton + spine-prose gates, v2.3 visual-assets, v2.4 editorial-review, and v3.1 headless-review + per-article `personas` + `pivots`)
- `${CLAUDE_PLUGIN_ROOT}/config/edition-state.template.json` — edition-state schema (18 phases including Fix #2 lived-specifics extraction (Step 1.5) + `articles.NN.livedSpecifics`, the deterministic §B/§C1 contract-gate (Step 4.5), v2.1 skeleton + spine-prose gates, v2.3 visual-assets, v2.4 editorial-review, and v3.1 headless-review + per-article `personas` + `pivots`)
- `${CLAUDE_PLUGIN_ROOT}/scripts/specifics-bank/` — lived-specifics store + per-edition binding (Fix #2, kilde-så-draft): `query`/`add` (Step 1.5), `validate-binding` (Step 2.5 gate), `render-kilder` (`NN-kilder.md` ledger). Design record: `docs/fix2/slice2-binding.md`
- `${CLAUDE_PLUGIN_ROOT}/config/edition-config.template.json` — static delivery metadata schema (calendar, freshness, credit, captions) — Step 8
- `${CLAUDE_PLUGIN_ROOT}/config/image-credit-caption.template.md` — cover motif + credit + caption table (honest-about-AI credit) — Step 7.5
- `${CLAUDE_PLUGIN_ROOT}/config/edition-delingstekst.template.md` — distribution-copy grammar (`## Del N —` / `## Samle`) — Steps 8/9

View file

@ -7,7 +7,8 @@
"phases": [
"load-context — read <serie>/STATE.md, voice profile, persona library, series brief (Step 0)",
"brief-calibration — angle, voice, audience personas, key points, leader-takeaway (Step 1)",
"research — parallel scoped mandates → verified notes, triangulation (Step 2)",
"lived-specifics — elicitation interview (35 sharp questions, vagueness refused) → add lived material to the specifics-bank + populate articles.NN.livedSpecifics slot-map (each load-bearing key-point bound specific/abstrakt/ekstern/unresolved) → render NN-kilder.md; BETWEEN brief (Step 1) and research (Step 2). The grounded-sourcing layer (Fix #2, kilde-så-draft; never fabricated — invariant 3). The BLOCKing «vaghet avvises» gate (validateBinding) is enforced at the Step 2.5 skeleton-gate (Step 1.5)",
"research — parallel scoped mandates → verified notes, triangulation; scoped BY the binding (fills ekstern/unresolved slots + verifies the operator's own numbers, regel 6/7) (Step 2)",
"skeleton-pitch — five-line skeleton (premise/problem/recommendation/payoff/forward) + section pitches, operator gate + persona-skjelett-sweep BEFORE prose (Step 2.5)",
"spine-prose — one paragraph per section against the gated skeleton, operator gate BEFORE full expansion (Step 3a)",
"draft — full prose expansion against the gated spine; may span sessions (Step 3b)",

View file

@ -22,8 +22,11 @@
# user data) in Section 13, each with a non-vacuity self-test; the contract-gate
# binding guard (Slice 3: the §B/§C1 gate's own suite stays green and rules.ts
# ratifies 1:1 against the maskinrommet §E-manifest — KTG-only, skipped for an adopter
# shipping no deps/contract) in Section 14; the assertion-count anti-erosion floor
# (SC6) in Section 15. All are live below (Sections 815).
# shipping no deps/contract) in Section 14; the specifics-bank binding guard (Fix #2
# slice 3: the lived-specifics store/binding suite stays green and its case count
# never erodes — KTG-only, skipped for an adopter shipping no deps) in Section 15;
# the assertion-count anti-erosion floor (SC6) in Section 16. All are live below
# (Sections 816).
#
# Usage: bash scripts/test-runner.sh
# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`.
@ -618,7 +621,35 @@ fi
echo ""
# --- Section 15: Assertion-Count Anti-Erosion (SC6) ---
# --- Section 15: Specifics-Bank Binding (Fix #2 slice 3) ---
echo "--- Specifics-Bank Binding ---"
# The lived-specifics store + per-edition binding (scripts/specifics-bank) is wired
# into /linkedin:newsletter as Step 1.5 (elicitation + slot-map) and its
# validate-binding gate into the Step 2.5 skeleton-gate. The seam is now load-bearing,
# so its suite belongs in CI: the bank/binding/kilder tests stay green and the case
# count never erodes. KTG-internal: skipped (warn, never fail) for an adopter that
# ships no specifics-bank deps — so the lint stays green everywhere. Same set +e /
# subshell discipline as Section 14 (bash 3.2-safe; keeps a red npm test from
# aborting the runner under set -e).
SB_DIR="scripts/specifics-bank"
if [ -x "$SB_DIR/node_modules/.bin/tsx" ]; then
SB_OUT=$( set +e; (cd "$SB_DIR" && npm test) 2>&1; echo "SB_EXIT:$?" )
SB_EXIT=$(echo "$SB_OUT" | grep -oE 'SB_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
SB_TESTS=$(echo "$SB_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
SPECIFICS_BANK_TESTS_FLOOR=28
if [ "$SB_EXIT" = "0" ] && [ -n "$SB_TESTS" ] && [ "$SB_TESTS" -ge "$SPECIFICS_BANK_TESTS_FLOOR" ]; then
pass "specifics-bank suite green: $SB_TESTS tests pass (floor $SPECIFICS_BANK_TESTS_FLOOR)"
else
fail "specifics-bank suite NOT green (exit=${SB_EXIT:-?}, tests=${SB_TESTS:-?}, floor $SPECIFICS_BANK_TESTS_FLOOR) — run: (cd $SB_DIR && npm test)"
fi
else
warn "specifics-bank skipped — deps absent ($SB_DIR/node_modules); run: (cd $SB_DIR && npm install)"
fi
echo ""
# --- Section 16: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the pre-M0 total (74 pass()+fail() invocations) as a floor; the count
# may only grow (brief-reviewer assumption 3). Runs last so TOTAL_CHECKS sees every prior check.