feat(linkedin): newsletter Step 7-10 lock, delivery, hook-gate, schedule (S10)

This commit is contained in:
Kjell Tore Guttormsen 2026-05-27 21:42:22 +02:00
commit f24c6e30f7

View file

@ -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) | | 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` | | 10 | **Scheduling** | register the edition in the plugin queue/state for native scheduling | `hooks/scripts/queue-manager.mjs` |
> **Build status:** Steps 06 are implemented below. Steps 710 are added in a > **Build status:** all 11 phases (Steps 010) are implemented below. This
> subsequent build session (plan step S10). Until then, this command takes an > command takes an edition end-to-end: load → calibration → verified research →
> edition from load → calibration → verified research → draft > draft → consistency/quality → fact-check sweep → pre-lock persona sweep
> consistency/quality → fact-check sweep → pre-lock persona sweep, persisting > optional annotation → LOCK/delivery → post-lock hook gate → scheduling,
> each phase to `edition-state.json` and the HANDOVER and stopping cleanly > persisting each phase to `edition-state.json` and the HANDOVER and stopping
> between sessions. > cleanly between sessions.
--- ---
@ -477,14 +477,205 @@ Next: Step 7 — Annotation (optional), then Step 8 — LOCK → delivery.
--- ---
## Steps 710 (added in a subsequent build session) ## Step 7: Annotation — optional annotatable review HTML
Optional annotation (render annotatable review HTML), lock/delivery (POST.html Before locking, you may render the draft as a self-contained, annotatable HTML
"all in one place"), the post-lock hook/conversion gate (`persona-reviewer` in page for one last manual read in the browser (the same pencil-toggle annotation
konverter-modus), and scheduling are implemented in plan step S10. Each will surface the render scripts ship). This step is **optional** — skip it if the
append its phase here, reading the phase contract from editor is satisfied with the in-session draft. It does not gate lock.
`config/edition-state.template.json` and (once extracted in S12) the long-form
quality rules from `references/longform-quality-rules.md`. **Procedure:**
1. **Confirm the draft path.** The consistency- and persona-passed draft from
Steps 46 is the `NN-utkast.md` (NN = zero-padded edition number) in the
series folder, with YAML front matter (`title`, etc.).
2. **Render the review HTML.** `render/build-html.mjs` writes to `./review/`
relative to the *current working directory*, so run it **with cwd = the
series folder** (not the plugin). Pass the draft file:
```bash
cd <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-html.mjs" NN-utkast.md
```
**Check the exit code (N3 — do not assume success).** 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 <path> (<KB>)`.
Surface `<serie-mappe>/review/NN-utkast.html` as a `file://` link for the
editor's manual annotation pass. Fold any resulting edits back **by
tightening** (the Step 4 rule still holds) — then, if the edit was
substantive, re-run the affected sweep (Step 5 or 6) before proceeding.
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: <serie-mappe>/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 <serie-mappe> && node "${CLAUDE_PLUGIN_ROOT}/render/build-linkedin.mjs" NN-utkast.md
```
**Check the exit code (N3 — do not assume success).** Confirm exit 0 AND
that `linkedin/NN/POST.html` now exists (the `✓ linkedin/NN/POST.html`
line). A skip-warning with exit 0 but no NN file means the prefix was wrong —
treat as failure, rename, re-run. Surface any throw (missing config /
delingstekst) with its stderr.
4. **Lock the article.** Set `locked: true`, `status: "locked"`, and
`currentPhase: "lock-delivery"` in `edition-state.json`. Surface
`<serie-mappe>/linkedin/NN/POST.html` as a `file://` link. From here the
body is frozen — Step 9 may only adjust the distribution hook, never the
locked edition text.
```
LOCK → delivery.
- Preconditions: factcheck 🔴 = none, persona resonans primær = JA (else: STOP)
- Delivered: <serie-mappe>/linkedin/NN/POST.html
- build-linkedin exit: 0 + POST.html present (else: non-zero / skip — NOT locked)
- Article status: locked
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: <name> 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. `<series-slug>-NN`)
- `draftPath` — the locked `linkedin/NN/POST.html`
- `format``newsletter` (so calendar/reporting can distinguish long-form)
- `hookPreview` — the conversion-passed distribution hook (first ~80 chars)
```bash
node -e 'import("'"${CLAUDE_PLUGIN_ROOT}"'/hooks/scripts/queue-manager.mjs").then(q => q.queueAdd("<series-slug>-NN","<serie-mappe>/linkedin/NN/POST.html","YYYY-MM-DD","HH:MM","<pillar>","newsletter","<hook ~80c>",<charCount>))'
```
The function appends to `assets/drafts/queue.json` with `status:
"scheduled"` and returns the new entry.
3. **Persist + close the edition.** Set the article's `status: "scheduled"`,
`scheduled: "<YYYY-MM-DD HH:MM>"`, and `currentPhase: "scheduling"` in
`edition-state.json`. Append a closing "edition scheduled → ready for
`/linkedin:publish` on <date>" pointer to the HANDOVER §6. The pipeline is
complete.
```
Scheduling.
- Queue entry: <series-slug>-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.
```
--- ---