diff --git a/plugins/linkedin-thought-leadership/commands/newsletter.md b/plugins/linkedin-thought-leadership/commands/newsletter.md index 8685594..26184f9 100644 --- a/plugins/linkedin-thought-leadership/commands/newsletter.md +++ b/plugins/linkedin-thought-leadership/commands/newsletter.md @@ -70,12 +70,12 @@ single most important correction from the Seres process (plan §0.4, principle 5 | 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:** Steps 0–6 are implemented below. Steps 7–10 are added in a -> subsequent build session (plan step S10). Until then, this command takes an -> edition from load → calibration → verified research → draft → -> consistency/quality → fact-check sweep → pre-lock persona sweep, persisting -> each phase to `edition-state.json` and the HANDOVER and stopping cleanly -> between sessions. +> **Build status:** all 11 phases (Steps 0–10) are implemented below. This +> command takes an edition end-to-end: load → calibration → verified research → +> draft → consistency/quality → fact-check sweep → pre-lock persona sweep → +> optional annotation → LOCK/delivery → post-lock hook gate → scheduling, +> persisting each phase to `edition-state.json` and the HANDOVER and stopping +> cleanly between sessions. --- @@ -477,14 +477,205 @@ Next: Step 7 — Annotation (optional), then Step 8 — LOCK → delivery. --- -## Steps 7–10 (added in a subsequent build session) +## Step 7: Annotation — optional annotatable review HTML -Optional annotation (render annotatable review HTML), lock/delivery (POST.html -"all in one place"), the post-lock hook/conversion gate (`persona-reviewer` in -konverter-modus), and scheduling are implemented in plan step S10. Each will -append its phase here, reading the phase contract from -`config/edition-state.template.json` and (once extracted in S12) the long-form -quality rules from `references/longform-quality-rules.md`. +Before locking, you may render the draft as a self-contained, annotatable HTML +page for one last manual read in the browser (the same pencil-toggle annotation +surface the render scripts ship). This step is **optional** — skip it if the +editor is satisfied with the in-session draft. It does not gate lock. + +**Procedure:** + +1. **Confirm the draft path.** The consistency- and persona-passed draft from + Steps 4–6 is the `NN-utkast.md` (NN = zero-padded edition number) in the + series folder, with YAML front matter (`title`, etc.). + +2. **Render the review HTML.** `render/build-html.mjs` writes to `./review/` + relative to the *current working directory*, so run it **with cwd = the + series folder** (not the plugin). Pass the draft file: + + ```bash + cd && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-utkast.md + ``` + + **Check the exit code (N3 — do not assume success).** A non-zero exit (e.g. + missing file → the script prints `Fant ikke:` and continues, or no-args → + exit 1) means no review HTML was produced. Report the failure and the + `build-html.mjs` stderr; do NOT advance the phase on a silent failure. + +3. **Hand off the link.** On success the script prints `Skrev ()`. + Surface `/review/NN-utkast.html` as a `file://` link for the + editor's manual annotation pass. Fold any resulting edits back **by + tightening** (the Step 4 rule still holds) — then, if the edit was + substantive, re-run the affected sweep (Step 5 or 6) before proceeding. + +4. **Persist.** Set `currentPhase: "annotation"` in `edition-state.json` and + append an "annotation rendered (optional) → next: lock/delivery" pointer to + the HANDOVER §6. If skipped, note "annotation skipped" and move on. + +``` +Annotation (optional). +- Rendered: /review/NN-utkast.html (or: skipped) +- build-html exit: 0 (else: non-zero — review HTML NOT produced, see stderr) +Next: Step 8 — LOCK → delivery. +``` + +--- + +## Step 8: LOCK → delivery — POST.html "all in one place" + +This is the **lock**. Only enter it once the Step 6 persona sweep returned a +clean primær JA and the Step 5 fact-check has no unresolved 🔴. Locking +produces the editor's single delivery artifact — `POST.html`, the +"all-in-one-place" page that carries the edition text plus its distribution +(delingstekst) copy, ready to paste into LinkedIn. + +> **Order assertion (enforced).** Lock (Step 8) runs AFTER the pre-lock persona +> sweep (Step 6) and BEFORE the hook/conversion gate (Step 9). Reversing lock +> and the pre-lock sweep reproduces the exact Seres failure (reopening locked +> texts) — see Step 6. The post-lock hook-gate (Step 9) judges only the +> distribution hook and never reopens the locked body. `[GATE]` + +**Procedure:** + +1. **Confirm lock preconditions.** In `edition-state.json`: the article's + `factcheckLog` has no open 🔴 and `personaSweep.resonance` recorded a primær + JA. If either is missing, STOP — return to Step 5/6. Do not lock past an + open gate. + +2. **Confirm the delivery inputs exist in the series folder.** + `render/build-linkedin.mjs` reads, relative to cwd: + - `linkedin/edition-config.json` — calendar, freshness, cover credit/caption + - `linkedin/edition-delingstekst.md` — the per-edition distribution text + (and the `samle` post, built unconditionally) + + If either file is absent the script throws on read — verify both are present + before invoking. + +3. **Render POST.html.** Run with **cwd = the series folder** (the script + resolves `linkedin/` from cwd and writes `linkedin/NN/POST.html`). The draft + filename MUST keep its two-digit `NN` prefix or the script skips it + (`↷ hopper over … (ikke NN-prefiks)`): + + ```bash + cd && node "${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs" NN-utkast.md + ``` + + **Check the exit code (N3 — do not assume success).** Confirm exit 0 AND + that `linkedin/NN/POST.html` now exists (the `✓ linkedin/NN/POST.html` + line). A skip-warning with exit 0 but no NN file means the prefix was wrong — + treat as failure, rename, re-run. Surface any throw (missing config / + delingstekst) with its stderr. + +4. **Lock the article.** Set `locked: true`, `status: "locked"`, and + `currentPhase: "lock-delivery"` in `edition-state.json`. Surface + `/linkedin/NN/POST.html` as a `file://` link. From here the + body is frozen — Step 9 may only adjust the distribution hook, never the + locked edition text. + +``` +LOCK → delivery. +- Preconditions: factcheck 🔴 = none, persona resonans primær = JA (else: STOP) +- Delivered: /linkedin/NN/POST.html +- build-linkedin exit: 0 + POST.html present (else: non-zero / skip — NOT locked) +- Article status: locked +Next: Step 9 — Hook / conversion gate (post-lock). +``` + +--- + +## Step 9: Hook / conversion gate — "would YOU click?" (AFTER lock) + +The locked edition still has to earn the click. The **distribution text** — the +delingstekst hook that fronts the post in the feed — now faces a final binary +gate: would the primær persona, scrolling, actually stop and open this? This is +the post-lock conversion sweep, distinct from the pre-lock resonance sweep +(Step 6): it judges the **hook only**, binary, never the body. + +> **Order assertion (enforced).** This hook-gate (Step 9) runs AFTER lock +> (Step 8) — it operates on the distribution text of an already-locked edition +> and must never reopen the locked body. If the hook fails, you revise the +> *delingstekst* (and re-render via Step 8's `build-linkedin.mjs`), not the +> edition text. This ordering is the inverse of the resonance sweep, which runs +> BEFORE lock — keeping the two apart is what fixed the Seres process. `[GATE]` + +**Procedure:** + +1. **Take the distribution hook** — the first two lines of the edition's entry + in `linkedin/edition-delingstekst.md` (and the `samle` hook, if shipping the + collected post). This is what the reader sees before "…see more". + +2. **Run `persona-reviewer` in conversion mode** for the **primær** persona + only, from THIS command layer in the foreground. Pass + **`mode: konverter`** (the after-lock, hook-only mode — NOT resonans). The + agent returns a single binary verdict, **JA / NEI**, on «would YOU click?» — + no axis scoring, no flags, no rewritten copy (principle: the jury judges, + the editor writes). + +3. **Gate on the binary.** + - **JA** → the hook converts. Proceed to Step 10. + - **NEI** → the hook does not earn the click. Revise the **delingstekst hook + only** (sharpen the krok by tightening — close the gap, hold the body + frozen), **re-render POST.html** via Step 8's `build-linkedin.mjs` (the + body stays locked; only the distribution copy changed), and re-run the + conversion check. Loop until JA. `[GATE]` + +4. **Persist.** Record the conversion verdict in + `edition-state.json` → `articles.NN.personaSweep.conversion` (and HANDOVER §5), + and set `currentPhase: "hook-conversion-gate"`. + +``` +Hook / conversion gate (post-lock). +- Primær persona: mode: konverter (hook only) +- Verdict: JA — hook converts (else: NEI — revise delingstekst hook, re-render, re-check) +- Body: untouched (locked in Step 8) +Gate: [PASS — JA] (else loop on the distribution hook) +Next: Step 10 — Scheduling. +``` + +--- + +## Step 10: Scheduling — register the edition in the plugin queue + +The locked, conversion-passed edition is ready to ship. Register it in the +plugin's native scheduling queue so it shows up in `/linkedin:calendar`, +`/linkedin:publish`, and the posting-time reminders — the same queue the +short-form pipeline uses. The edition is now a first-class scheduled post. + +**Procedure:** + +1. **Pick the slot.** Confirm the target `scheduled_date` (YYYY-MM-DD) and + `scheduled_time` from the edition-config calendar / the series brief. If the + slot is unset, ask once (Step 1's calibration may already have settled it). + +2. **Register via `queue-manager.mjs`.** Add one queue entry for the edition, + reusing the short-form queue contract — `queueAdd(id, draftPath, schedDate, + schedTime, pillar, format, hookPreview, charCount)`: + - `id` — stable edition id (e.g. `-NN`) + - `draftPath` — the locked `linkedin/NN/POST.html` + - `format` — `newsletter` (so calendar/reporting can distinguish long-form) + - `hookPreview` — the conversion-passed distribution hook (first ~80 chars) + + ```bash + node -e 'import("'"${CLAUDE_PLUGIN_ROOT}"'/hooks/scripts/queue-manager.mjs").then(q => q.queueAdd("-NN","/linkedin/NN/POST.html","YYYY-MM-DD","HH:MM","","newsletter","",))' + ``` + + The function appends to `assets/drafts/queue.json` with `status: + "scheduled"` and returns the new entry. + +3. **Persist + close the edition.** Set the article's `status: "scheduled"`, + `scheduled: ""`, and `currentPhase: "scheduling"` in + `edition-state.json`. Append a closing "edition scheduled → ready for + `/linkedin:publish` on " pointer to the HANDOVER §6. The pipeline is + complete. + +``` +Scheduling. +- Queue entry: -NN → assets/drafts/queue.json (status: scheduled) +- Slot: YYYY-MM-DD HH:MM format: newsletter +- Article status: scheduled +Edition complete. Visible in /linkedin:calendar; mark live with /linkedin:publish. +``` ---