ktg-plugin-marketplace/plugins/linkedin-studio/docs/voyage-build/plan.md
Kjell Tore Guttormsen b6bb61246b refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)
BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.

- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
  config-audit v5.0.0 snapshots) intentionally retain the old slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:32:02 +02:00

995 lines
72 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
task: "Lift linkedin-thought-leadership plugin v1.2.0 → v2.0.0 — full-spectrum content engine + newsletter, net-fewer commands/agents"
slug: ltl-v2-fullspektrum
project_dir: docs/voyage-build
created: 2026-05-26
plan_version: 1.7
profile: premium
phase_models:
plan: opus
execute: opus
profile_source: state-mandate
---
# LTL v2.0.0 — Full-Spectrum LinkedIn Content Engine
> **Plan quality: A** (90/100) — APPROVE_WITH_NOTES (revised after adversarial review: 3 blockers + 5 major + 4 minor resolved; see Revisions)
>
> Generated by trekplan v5.1.1 on 2026-05-26 — `plan_version: 1.7`
>
> **This is the Voyage-executable plan.** The authoritative human spec is
> [`../plan-fullspektrum-innholdsmotor.md`](../plan-fullspektrum-innholdsmotor.md)
> (the "fasit"). This file translates that spec's 20 sessions (S1S20) into
> Voyage `### Step N:` steps with per-step Manifests. Where the two differ,
> the corrections in this plan (verified against actual files by the
> exploration swarm) win — they are noted inline.
## Context
LTL must own the entire chain for ALL LinkedIn content — from short-form post
to newsletter edition — at the quality the "Seres series" production proved
possible, **while reducing** the total command/agent surface through
consolidation. Three work-bodies (brief §1):
1. **Renovation** — consolidate real redundancy (27→~23 commands, 16→~14 agents).
2. **Long-form capability** — one new `/linkedin:newsletter` command with a
phased, multi-session pipeline (research → draft → fact-check → persona-review
BEFORE lock → delivery → hook-gate).
3. **Render + annotation into the plugin** — move 4 render scripts + fonts;
generalize `build-linkedin`; generalize `build-html` into an artifact
annotation renderer.
Decisions AH (fasit §2) are LOCKED and not re-litigated here. The model is
Opus 4.7 on everything; execution is `/trekexecute --fg` (subscription),
`/trekcontinue` per fresh session.
## Architecture Diagram
```mermaid
graph TD
subgraph "Plugin = engine"
CMD["/linkedin:newsletter (new)"]
FC["agents/fact-checker (new)"]
PR["agents/persona-reviewer (new)"]
PERS["config/personas.template.md (new)"]
RENDER["render/ — build-html, build-linkedin, build-pdf, build-carousel + fonts"]
QRULES["references/longform-quality-rules.md (new)"]
CMD --> FC
CMD --> PR
CMD --> PERS
CMD --> RENDER
CMD --> QRULES
end
subgraph "Consolidation (net fewer)"
Q["templates→quick"]
C["publish→calendar"]
O["collab+speaking→outreach"]
A["authority→strategy"]
AN["analytics merge / engagement merge"]
RETIRE["retire content-tracker + personalization-scorer → scripts"]
end
subgraph "maskinrommet = workbench (other repo, read-only here)"
SERIE["serier/<slug>/ — content + edition-state + edition-config.json"]
end
RENDER -. "cwd = serie-mappe" .-> SERIE
```
## Codebase Analysis
- **Tech stack:** Claude Code plugin. Commands + agents are `.md` files
(YAML frontmatter + Markdown system-prompt bodies). Hooks are Node.js `.mjs`
compiled from `hooks/hooks.template.json` via `hooks/scripts/compile-hooks.py`.
Tests use the built-in `node:test` + `node:assert/strict`. Zero npm deps in
hooks/scripts. Node v25.8.2 (so `node --test <dir>` is broken — glob form only).
- **Size:** 166 source files (medium). 27 commands, 16 agents (+ `agents/README.md`),
9 hooks, 6 skills.
- **Command template** (`commands/pipeline.md`, `commands/react.md`): frontmatter
is `name: linkedin:<verb>`, `description: |` block ending in a `Triggers on:`
line, `allowed-tools:` YAML bullet list (only tools actually used). Body: H1 +
"You are a…" persona line + `## Step 0: Load Context` onward + closing
`## Reference Files` bullet list using `${CLAUDE_PLUGIN_ROOT}/...` paths.
- **Agent frontmatter** (`agents/differentiation-checker.md`,
`agents/content-repurposer.md`): `name:` bare kebab-case, `description: |`
block ending in `Triggers on:`, `model:` bare keyword, `color:` bare word,
`tools: ["Read", ...]` JSON inline array. **Existing agents use `model: sonnet`,
but the fasit (§6.2/§6.3) + KTG global rule mandate `model: opus` for the two
new agents.**
- **Reuse targets (verified):** `hooks/scripts/state-updater.mjs` (export
functions + CLI guard `if (import.meta.url === \`file://${process.argv[1]}\`)`
at line 227 — the canonical export/guard pattern), `queue-manager.mjs`
(`PLUGIN_ROOT` env + `__dirname`), `personalization-score.mjs`
(`calculateScore(pluginRoot)`), `clipboard-helper.mjs`, `compile-hooks.py`,
`skills/linkedin-content-creation/SKILL.md`, `references/newsletter-strategy-guide.md`.
- **${CLAUDE_PLUGIN_ROOT}** is injected by Claude Code into command-prompt Bash
(proven at runtime: `pipeline.md:108,114,137,188`, `calendar.md:25`). Scripts
do NOT read it internally — they self-resolve via `__dirname`/env. (Correction
to fasit wording.)
- **Render scripts** (`/Users/ktg/repos/maskinrommet/tools/`, read-only here):
all 4 are zero-npm-dep (Node builtins only). build-html.mjs (963 lines),
build-linkedin.mjs (364 lines, hardcoded Seres calendar/captions/freshness at
lines 3450: `CALENDAR`:34, `FRESHNESS`:44, `COVER_CREDIT`:49, `CAPTIONS`:50),
build-pdf.mjs (345), build-carousel.mjs (268).
## Research Sources
No external web research was needed — the brief declares research complete
(brief §4) and the four exploration agents (task-finder, convention-scanner,
risk-assessor, test-strategist) confirmed the spec against actual files. Their
material corrections are folded into the steps below and the Assumptions table.
## Corrections folded in from confirmatory exploration
These are verified deviations from the fasit. Each is a correction-in-scope
(implements what the fasit already mandates, against the real files):
1. **Fonts:** only `build-pdf.mjs` + `build-carousel.mjs` consume fonts (via
`file://` URLs from `__dirname/fonts`, not base64). `build-html.mjs` +
`build-linkedin.mjs` use system-font stacks and need no fonts. Fonts dir =
1.5 MB, 8 .ttf (Inter 400/600/700 + Newsreader 400/400i/600/600i/700).
2. **No OFL license file exists** anywhere in maskinrommet. `render/OFL.txt`
must be **authored/sourced** (Inter + Newsreader are OFL-1.1), not copied.
3. **weasyprint graceful degradation is NOT implemented** — `build-pdf.mjs:339`
and `build-carousel.mjs:262` hard-fail (`execFileSync` + `process.exit(1)`)
on missing weasyprint. §7.4 degradation must be **written** during migration
(Step 1), not assumed present. weasyprint IS installed on this machine (67.0)
so the degradation path needs a forced-PATH-miss test.
4. **Render scripts are not importable** — no `export`, no CLI guard, `main()`
called unconditionally. The first production change for the generalized
scripts (Steps 2, 3) is to add `export` + the CLI guard (copy
`state-updater.mjs:227`). The failing import-test drives this naturally.
5. **Agent "known-answer fixtures" cannot be `node:test`** (agents are .md
prompts, no importable function, no deterministic LLM output). Split: (a) an
automated `node:test` lints the fixture file's structure; (b) the actual
accuracy comparison is an `[OPERATØR]`/`[GATE]` manual check — consistent
with fasit §10.0 "subjective quality is never self-certified."
6. **Dead-link blast radius is larger than the fasit's "Lav" labels** (N1, High):
`publish` alone has 21 route-refs incl. 9 inside hook scripts
(`session-start.mjs`, `posting-reminder.mjs`, `user-prompt-context.mjs`) that
emit runtime guidance and break silently. The §10.2-F grep target set must add
`agents/README.md` + `CLAUDE.md`. Treat every consolidation merge as Medium.
7. **Skill catalogs duplicated across 6 skill dirs** (N2): the langform trigger
+ catalog updates (Steps 11, 21) must sweep all 6 `skills/*/SKILL.md`, not
just `linkedin-content-creation`.
8. **`engagement-coach.md:24`** has a live "defer to the comment-strategist
agent" cross-ref. Since comment-strategist merges INTO engagement-coach
(Step 20), that line must be rewritten, not just deleted.
9. **No existing parallel Task fan-out** to copy — the Step 8 fan-out
(fasit assumption 4) is the single highest-uncertainty checkpoint; it is a
pure runtime assumption with no static precedent.
## Implementation Plan
> **Mapping:** one Voyage step = one fasit session. Each fasit session is
> already sized to ≤35 % context as a single testable deliverable, so 1 step =
> 1 `/trekcontinue` session is the correct granularity (not the usual 35
> finer steps). The fasit session id (S1, S1a, …) is in each step title.
> Internal TDD sub-steps live in **Changes** / **Test first**; the Manifest
> encodes the session's binary Definition-of-Done (fasit §10.3).
### Step 1: S1 — Migrate render scripts + fonts into the plugin
- **Files:** `render/build-html.mjs`, `render/build-linkedin.mjs`, `render/build-pdf.mjs`, `render/build-carousel.mjs`, `render/fonts/` (8 .ttf), `render/OFL.txt` (all new)
- **Changes:** Create `render/`. Copy the 4 scripts + `fonts/` (8 .ttf, 1.5 MB) FROM `/Users/ktg/repos/maskinrommet/tools/` (read-only source; do NOT modify maskinrommet). **Author `render/OFL.txt`** — the SIL Open Font License 1.1 text covering Inter + Newsreader (no license file exists at source; correction #2). **Add weasyprint graceful degradation** to `render/build-pdf.mjs` + `render/build-carousel.mjs` (correction #3): before `execFileSync("weasyprint", …)`, detect weasyprint on PATH; if absent, print a clear install instruction and skip the PDF step with a non-fatal warning instead of `process.exit(1)`. (codebase analysis)
- **Reuses:** font-resolution pattern already in the scripts (`const FONT_DIR = path.join(__dirname, "fonts")`, build-pdf.mjs:154).
- **Test first:**
- File: `render/__tests__/weasyprint-degradation.test.mjs` (new)
- Verifies: when `weasyprint` is not resolvable, the degradation helper returns a skip-signal (not a throw) and emits an install hint
- Pattern: `hooks/scripts/__tests__/state-updater.test.mjs` (node:test + assert/strict, pure-function call)
- **Verify:** `ls render/ && ls render/fonts/*.ttf | wc -l && node --test 'render/__tests__/*.test.mjs'` → expected: 4 `.mjs` + `fonts/` + `OFL.txt`; **8** .ttf (Inter-400/600/700, Newsreader-400/400i/600/600i/700); tests pass
- **On failure:** revert — `git checkout -- render/ && rm -rf render/`
- **Checkpoint:** `git commit -m "feat(linkedin): migrate render scripts + fonts into plugin (S1)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- render/build-html.mjs
- render/build-linkedin.mjs
- render/build-pdf.mjs
- render/build-carousel.mjs
- render/OFL.txt
- render/__tests__/weasyprint-degradation.test.mjs
- render/fonts/Inter-400.ttf
- render/fonts/Newsreader-400.ttf
min_file_count: 8
commit_message_pattern: "^feat\\(linkedin\\): migrate render scripts"
bash_syntax_check: []
forbidden_paths:
- /Users/ktg/repos/maskinrommet/tools/build-html.mjs
- /Users/ktg/repos/maskinrommet/tools/build-linkedin.mjs
must_contain:
- path: render/build-pdf.mjs
pattern: "weasyprint"
- path: render/build-carousel.mjs
pattern: "weasyprint"
- path: render/OFL.txt
pattern: "SIL Open Font License"
```
### Step 2: S1a — Generalize the annotation renderer (build-html.mjs)
- **Files:** `render/build-html.mjs`, `render/__tests__/build-html.test.mjs` (new)
- **Changes:** First make the script importable (correction #4): add `export` to `markdownToHtml`, `inline`, and the table/heading helpers, and wrap the CLI body in the guard `if (import.meta.url === \`file://${process.argv[1]}\`)` (copy `state-updater.mjs:227`). Then generalize the markdown→HTML engine (beslutning H): support tables (`| a | b |` → `<table>`), all heading levels `#``####` (today only `##`/`###`), inline `` `code` `` → `<code>` (today `inline()` only does `**bold**`/`*italic*`), and generic frontmatter/title. The annotation engine (CSS + client-JS: mark → Endre/Legg til/Fjern/Avklar/Risiko → sidebar → localStorage → export) is artifact-agnostic and embedded verbatim — no runtime dependency on maskinrommet or a temp file. (codebase analysis)
- **Reuses:** `state-updater.mjs:227` CLI-guard pattern; the existing annotation engine inside the source `build-html.mjs`.
- **Test first:**
- File: `render/__tests__/build-html.test.mjs` (new)
- Verifies: `markdownToHtml` converts a `| a | b |` block to `<table>`/`<tr>`/`<td>`; `#`→`<h1>` … `####`→`<h4>`; backtick span → `<code>`; empty input → no table; malformed row tolerated
- Pattern: `hooks/scripts/__tests__/state-updater.test.mjs`
- **Verify:** `node --test 'render/__tests__/*.test.mjs'` → expected: pass; then `cd /tmp && node <plugin>/render/build-html.mjs <a-table-heavy-md>` renders all tables (manual visual = `[OPERATØR]`)
- **On failure:** revert — `git checkout -- render/build-html.mjs render/__tests__/build-html.test.mjs`
- **Checkpoint:** `git commit -m "feat(linkedin): generalize build-html annotation renderer — tables, headings, inline code (S1a)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- render/build-html.mjs
- render/__tests__/build-html.test.mjs
min_file_count: 2
commit_message_pattern: "^feat\\(linkedin\\): generalize build-html"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: render/build-html.mjs
pattern: "export"
- path: render/build-html.mjs
pattern: "import.meta.url"
- path: render/build-html.mjs
pattern: "<table>"
- path: render/build-html.mjs
pattern: "<h4"
- path: render/build-html.mjs
pattern: "<code>"
- path: render/__tests__/build-html.test.mjs
pattern: "<table>"
```
> **Manifest note (blocker fix):** the three `<table>`/`<h4`/`<code>`
> patterns on the production `build-html.mjs` are the real generalization
> predicate — a no-op that adds only `export` + the CLI guard but leaves the
> markdown engine untouched will NOT emit `<table>`/`<h4`/`<code>` and fails
> the Manifest. The `node --test` in Verify is the second gate.
### Step 3: S2 — Generalize build-linkedin.mjs to read edition-config.json
- **Files:** `render/build-linkedin.mjs`, `render/__tests__/build-linkedin.test.mjs` (new), `render/__tests__/fixtures/edition-config.json` (new)
- **Changes:** First add `export` + CLI guard (correction #4). Replace the hardcoded `CALENDAR`/`FRESHNESS`/`CAPTIONS`/`COVER_CREDIT` constants (build-linkedin.mjs:3450) with a read of `linkedin/edition-config.json` from `process.cwd()` (the serie-mappe). Resolve Q1 in favour of JSON (deterministic parsing). Provide a sensible default/empty-config path so a missing config degrades gracefully. (codebase analysis)
- **Reuses:** `state-updater.mjs:227` guard; analytics fixture-dir convention (`scripts/analytics/tests/fixtures/`).
- **Test first:**
- File: `render/__tests__/build-linkedin.test.mjs` (new) + `fixtures/edition-config.json`
- Verifies: changing values in the fixture config changes POST.html output (no code change) — fasit assumption 5; regression: a config matching the old hardcoded Seres values yields the previous output
- Pattern: `scripts/analytics/tests/csv-parser.test.ts` (file-fixture loading)
- **Verify:** `node --test 'render/__tests__/*.test.mjs'` → expected: pass (diff of two configs differs; regression config matches baseline)
- **On failure:** revert — `git checkout -- render/build-linkedin.mjs render/__tests__/build-linkedin.test.mjs render/__tests__/fixtures/`
- **Checkpoint:** `git commit -m "feat(linkedin): generalize build-linkedin to read edition-config.json (S2)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- render/build-linkedin.mjs
- render/__tests__/build-linkedin.test.mjs
- render/__tests__/fixtures/edition-config.json
min_file_count: 3
commit_message_pattern: "^feat\\(linkedin\\): generalize build-linkedin"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: render/build-linkedin.mjs
pattern: "edition-config"
- path: render/build-linkedin.mjs
pattern: "import.meta.url"
```
### Step 4: S3 — Persona library (config/personas.template.md)
- **Files:** `config/personas.template.md` (new)
- **Changes:** Create the reusable reader-persona library (beslutning D, fasit §6.1) with the 3 Seres seed personas: IT division director, AI-section lead, and line manager (primary). Per persona document: role, what disconnects them, what convinces them, expertise level, jargon tolerance. Mark the primary explicitly. Active overrides live in a gitignored `config/personas.local.md` (per `*.local.md`). (codebase analysis)
- **Reuses:** template style of `config/state-file.template.md`, `config/user-profile.template.md`.
- **Test first:** *(config file — structural check, not node:test)* DoD archetype C: file exists, required fields present (grep), parses.
- **Verify:** `test -f config/personas.template.md && grep -c -E 'rolle|avkobler|overbeviser|ekspertise|sjargong' config/personas.template.md && grep -ci 'primær' config/personas.template.md` → expected: file present; field markers present for 3 personas; primary marked
- **On failure:** revert — `git checkout -- config/personas.template.md`
- **Checkpoint:** `git commit -m "feat(linkedin): add reusable persona library template (S3)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- config/personas.template.md
min_file_count: 1
commit_message_pattern: "^feat\\(linkedin\\): add reusable persona library"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: config/personas.template.md
pattern: "[Pp]rimær"
```
### Step 5: S4 — fact-checker agent (agents/fact-checker.md)
- **Files:** `agents/fact-checker.md` (new), `agents/fixtures/fact-checker-cases.md` (new), `agents/__tests__/fact-checker-fixture.test.mjs` (new)
- **Changes:** New Opus agent, `tools: ["Read", "WebSearch"]` (fasit §6.2). Mandate: given a block of factual claims, verify each against a primary/credible source under "guilty until proven" (aldri fyll hull med gjetninger; flag unverifiable explicitly); return a verification log + risk-sort 🔴/🟡/🟢. Copy the gate structure from `agents/differentiation-checker.md` (Mission → numbered search process with literal example queries → scored dimensions + verdict-threshold table → PASS/REWORK/BLOCK gate → fenced Output Format → Key Principles + Anti-Patterns) but change the mandate from originality to factual correctness. **Do not extend differentiation-checker** — orthogonal questions. Per correction #5: write a fasit fixture (3 claims: one true→🟢, one false→🔴, one unverifiable→🟡) + a node:test that lints the fixture's structure; the accuracy comparison is `[OPERATØR]`/`[GATE]`. (codebase analysis)
- **Reuses:** `agents/differentiation-checker.md` gate-prompt form; `state-updater.test.mjs` test shape.
- **Test first:**
- File: `agents/__tests__/fact-checker-fixture.test.mjs` (new)
- Verifies: the fixture file has exactly 3 cases, each with exactly one of 🟢/🔴/🟡 and a non-empty fasit field
- Pattern: `hooks/scripts/__tests__/state-updater.test.mjs`
- **Verify:** `node --test 'agents/__tests__/*.test.mjs' && grep -E '^model: opus' agents/fact-checker.md` → expected: fixture-lint passes; frontmatter has `model: opus`. Accuracy run against fixture = `[GATE: fact-checker output matches fasit form + verdicts]`
- **On failure:** revert — `git checkout -- agents/fact-checker.md agents/fixtures/ agents/__tests__/fact-checker-fixture.test.mjs`
- **Checkpoint:** `git commit -m "feat(linkedin): add fact-checker agent + fixture (S4)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- agents/fact-checker.md
- agents/fixtures/fact-checker-cases.md
- agents/__tests__/fact-checker-fixture.test.mjs
min_file_count: 3
commit_message_pattern: "^feat\\(linkedin\\): add fact-checker agent"
bash_syntax_check: []
forbidden_paths:
- agents/differentiation-checker.md
must_contain:
- path: agents/fact-checker.md
pattern: "model: opus"
- path: agents/fact-checker.md
pattern: "WebSearch"
```
### Step 6: S5 — persona-reviewer agent (agents/persona-reviewer.md, 2 modes)
- **Files:** `agents/persona-reviewer.md` (new), `agents/fixtures/persona-reviewer-cases.md` (new), `agents/__tests__/persona-reviewer-fixture.test.mjs` (new)
- **Changes:** New Opus agent, `tools: ["Read"]` (fasit §6.3). Mandate: read one persona definition (from Step 4's library) + the text → judge on 6 axes (hook holds? resonance? tone? credibility? leader-takeaway + concrete action? length/drive?). Return top-5 flags as **direction, not rewritten copy** (the jury NEVER writes text). Two modes in the same file (parameter in the call): resonance-mode (Step 8 of newsletter, BEFORE lock) and conversion-mode (Step 11, after lock — binary YES/NO "would YOU click?" on the hook only). Convergence-loop: re-run per persona judging LØST/DELVIS/IKKE until clean YES from primary. Per correction #5: fixture + structural lint test; accuracy = manual gate. (codebase analysis)
- **Reuses:** `agents/differentiation-checker.md` form; `state-updater.test.mjs` test shape.
- **Test first:**
- File: `agents/__tests__/persona-reviewer-fixture.test.mjs` (new)
- Verifies: fixture has a persona def + sample text + the 6 axis labels; both modes documented
- Pattern: `hooks/scripts/__tests__/state-updater.test.mjs`
- **Verify:** `node --test 'agents/__tests__/*.test.mjs' && grep -E '^model: opus' agents/persona-reviewer.md && grep -Eci 'resonans|konverter' agents/persona-reviewer.md` → expected: lint passes; opus; both modes present (BSD-grep-safe `-E`). Output-shape (≤5 flags, 6 axes, NO rewritten copy) = `[GATE]`
- **On failure:** revert — `git checkout -- agents/persona-reviewer.md agents/fixtures/persona-reviewer-cases.md agents/__tests__/persona-reviewer-fixture.test.mjs`
- **Checkpoint:** `git commit -m "feat(linkedin): add persona-reviewer agent (2 modes) + fixture (S5)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- agents/persona-reviewer.md
- agents/fixtures/persona-reviewer-cases.md
- agents/__tests__/persona-reviewer-fixture.test.mjs
min_file_count: 3
commit_message_pattern: "^feat\\(linkedin\\): add persona-reviewer agent"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: agents/persona-reviewer.md
pattern: "model: opus"
```
### Step 7: S6 — Edition-state schema + retire content-tracker & personalization-scorer
- **Files:** `config/edition-state.template.json` (new), remove `agents/content-tracker.md`, remove `agents/personalization-scorer.md`, `agents/README.md` (update), `CLAUDE.md` (update agent table)
- **Changes:** Define the edition-state schema (fasit §5.2) — current phase + per-article status — as a documented JSON template (beslutment G: production state lives in the serie-mappe, this is the schema the plugin defines). Retire the 2 deterministic agents (fasit §4.2): their function is already covered by `hooks/scripts/personalization-score.mjs` (placeholder detection) and `state-updater.mjs`/`calendar` (plan-vs-queue diff) — verify the script path covers it, then delete the agent files. Update `agents/README.md` (flow diagrams + Haiku table reference them — N1) and `CLAUDE.md` agent count. (codebase analysis)
- **Reuses:** `hooks/scripts/personalization-score.mjs` `calculateScore()`; `state-updater.mjs`.
- **Test first:** *(schema file — archetype C+F)* parse the JSON template; enumerate the retired agents' capabilities and confirm each is covered by an existing script (run `node -e` on personalization-score.mjs).
- **Verify:** `node -e "JSON.parse(require('fs').readFileSync('config/edition-state.template.json','utf8'))" && ! test -f agents/content-tracker.md && ! test -f agents/personalization-scorer.md && ! grep -rn 'content-tracker\|personalization-scorer' agents/ README.md CLAUDE.md skills/ && ls agents/*.md | grep -v README | wc -l` → expected: JSON parses; both agents gone; **zero stray refs**; agent count = 16 (14 + 2 new fact-checker/persona-reviewer)
- **On failure:** revert — `git checkout -- agents/ config/edition-state.template.json CLAUDE.md`
- **Checkpoint:** `git commit -m "refactor(linkedin): edition-state schema + retire 2 deterministic agents to scripts (S6)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- config/edition-state.template.json
min_file_count: 1
commit_message_pattern: "^refactor\\(linkedin\\): edition-state schema"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: config/edition-state.template.json
pattern: "phase"
```
> **Manifest-schema limit (acknowledged):** `must_contain` is positive-match
> only, so the two agent *deletions* + capability-parity claim (Assumption 3)
> cannot be encoded as a Manifest predicate. For this and every consolidation
> step (1621), the **`! test -f` + dead-link grep in Verify is the binding
> completion predicate** — trekexecute must treat Verify as a gate here, not
> just the Manifest. Archetype F (fasit §10.2) governs.
### Step 8: S7 — newsletter.md skeleton, Step 02 (load, calibrate, research fan-out)
- **Files:** `commands/newsletter.md` (new)
- **Changes:** Create the orchestrator command following the LTL command template (`name: linkedin:newsletter`, `description: |` + Triggers, `allowed-tools:` incl. `Task`, `AskUserQuestion`, `Read`, `Bash`). Implement Step 0 (load edition-state/HANDOVER, voice-profile, persona library, serie-brief), Step 1 (brief + calibration, ≤3 questions, mark primary persona), Step 2 (parallel research **Task fan-out in foreground** — correction #9: highest-uncertainty checkpoint; no existing pattern to copy). Principle 4: fan-out from the command layer, never from a nested background agent. (codebase analysis)
- **Reuses:** `commands/pipeline.md` step structure + `${CLAUDE_PLUGIN_ROOT}` Bash form; `commands/react.md` multi-source synthesis discipline.
- **Test first:** *(command prose — archetype E)* run Step 02 against a dummy serie fixture; fasit assumption 4: confirm 2+ parallel research Task calls return structured (not degraded) results.
- **Verify:** `grep -E '^name: linkedin:newsletter' commands/newsletter.md && grep -c 'Step [012]' commands/newsletter.md` → expected: frontmatter correct; Steps 02 present. Parallel-fan-out behavior = `[GATE: assumption-4 runtime test — count parallel calls + structured replies]`
- **On failure:** escalate — if parallel Task fan-out degrades, stop and report (this is the load-bearing assumption); do not paper over with sequential calls without operator sign-off
- **Checkpoint:** `git commit -m "feat(linkedin): newsletter command skeleton Step 0-2 (S7)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/newsletter.md
min_file_count: 1
commit_message_pattern: "^feat\\(linkedin\\): newsletter command skeleton"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "name: linkedin:newsletter"
- path: commands/newsletter.md
pattern: "Step 0"
- path: commands/newsletter.md
pattern: "Step 2"
- path: commands/newsletter.md
pattern: "[Pp]arallel"
```
> **Manifest note:** the `Step 0`/`Step 2`/`parallel` patterns force all three
> phases + the fan-out wiring to be present (a single-string no-op fails). The
> *runtime* behavior — that foreground `Task` fan-out keeps the Task tool and
> returns non-degraded results (Assumption 1, the highest-uncertainty
> checkpoint) — is the `[GATE]` in Verify and the `escalate` On-failure; it
> cannot be encoded statically.
### Step 9: S8 — newsletter.md Step 34 (draft + consistency/quality)
- **Files:** `commands/newsletter.md` (edit)
- **Changes:** Add Step 3 (draft in dramaturgical order, voice-matched, may span sessions with maintained HANDOVER — use `content-repurposer` extended + Task) and Step 4 (consistency + quality: threads, premise→conclusion arc, leader-takeaway, AI-slop removal, minimal formatting-dose). **Forward-reference fix (major):** `references/longform-quality-rules.md` is not authored until Step 13. So Step 4 **inlines the fasit §8 rules directly in `newsletter.md` here**; Step 13 then EXTRACTS them to `references/longform-quality-rules.md` and replaces the inline block with a pointer. No dangling reference at any point. (codebase analysis)
- **Reuses:** `agents/content-repurposer.md`; voice-samples (always read before content — existing LTL rule); fasit §8 rule text (inlined now, extracted in Step 13).
- **Test first:** *(archetype E)* Step 34 produce a draft file on the dummy serie; voice-match is `[OPERATØR]`/`[GATE: voice-trainer]` — NOT self-certified (fasit §10.0).
- **Verify:** `grep -c 'Step 3' commands/newsletter.md && grep -c 'Step 4' commands/newsletter.md && grep -ci 'AI-slop\|premiss' commands/newsletter.md` → expected: Steps 34 present; §8 rules inlined. Draft quality = `[OPERATØR]`/`[GATE]`
- **On failure:** revert — `git checkout -- commands/newsletter.md`
- **Checkpoint:** `git commit -m "feat(linkedin): newsletter Step 3-4 draft + consistency (S8)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/newsletter.md
min_file_count: 1
commit_message_pattern: "^feat\\(linkedin\\): newsletter Step 3-4"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "content-repurposer"
- path: commands/newsletter.md
pattern: "Step 3"
- path: commands/newsletter.md
pattern: "Step 4"
- path: commands/newsletter.md
pattern: "AI-slop"
```
### Step 10: S9 — newsletter.md Step 56 (fact-check sweep + persona sweep BEFORE lock)
- **Files:** `commands/newsletter.md` (edit)
- **Changes:** Add Step 5 (fact-check sweep: risk-sorted 🔴/🟡/🟢, "guilty until proven", verification log — fan-out N parallel `fact-checker` calls in foreground) and Step 6 (persona sweep BEFORE lock: reader-jury via `persona-reviewer` resonance-mode, primary trumps, convergence-loop to clean YES). This is the fix for the single biggest Seres process error (fasit §0.4 / principle 5). Order assertion: sweep precedes lock. (codebase analysis)
- **Reuses:** `agents/fact-checker.md` (Step 5), `agents/persona-reviewer.md` (Step 6).
- **Test first:** *(archetype E)* both agents invoked in parallel; order-assert: persona-sweep step appears before the lock step; `[GATE]` clean-YES-from-primary required to proceed.
- **Verify:** `grep -n 'Step 5\|Step 6\|fact-checker\|persona-reviewer\|FØR lås\|before lock' commands/newsletter.md` → expected: Steps 56 present, both agents referenced, sweep ordered before lock. Verdicts = `[GATE]`
- **On failure:** revert — `git checkout -- commands/newsletter.md`
- **Checkpoint:** `git commit -m "feat(linkedin): newsletter Step 5-6 fact-check + persona sweep before lock (S9)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/newsletter.md
min_file_count: 1
commit_message_pattern: "^feat\\(linkedin\\): newsletter Step 5-6"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "fact-checker"
- path: commands/newsletter.md
pattern: "persona-reviewer"
- path: commands/newsletter.md
pattern: "Step 5"
- path: commands/newsletter.md
pattern: "Step 6"
```
> **Order assertion** (persona-sweep Step 6 BEFORE lock — the single biggest
> Seres process error, fasit §0.4): `must_contain` proves both phases exist;
> the *ordering* (Step 6 precedes the Step 8 lock) is asserted by the grep in
> Verify + is a `[GATE]`. A wiring that placed the sweep after lock would pass
> `must_contain` but fail the Verify order-assert.
### Step 11: S10 — newsletter.md Step 710 (annotate, lock/delivery, hook-gate, schedule)
- **Files:** `commands/newsletter.md` (edit)
- **Changes:** Add Step 7 (optional annotation: `render/build-html.mjs` → review HTML in `docs/review/`), Step 8 (LOCK → delivery: POST.html via `render/build-linkedin.mjs`, cwd = serie-mappe), Step 9 (hook/conversion gate: `persona-reviewer` conversion-mode on the distribution text, AFTER lock — order assertion), Step 10 (register edition in the queue via `queue-manager.mjs` for native scheduling). Correction (N3): when shelling out to render scripts, check exit codes — don't assume success. (codebase analysis)
- **Reuses:** `render/build-html.mjs`, `render/build-linkedin.mjs`, `hooks/scripts/queue-manager.mjs`.
- **Test first:** *(archetype E)* Step 8 produces POST.html on the dummy serie; order-assert: hook-gate (Step 9) runs AFTER lock (Step 8); edition registered in queue.
- **Verify:** `grep -n 'Step 7\|Step 8\|Step 9\|Step 10\|POST.html\|build-linkedin\|queue-manager' commands/newsletter.md` → expected: Steps 710 present; render + queue wired; hook-gate after lock
- **On failure:** revert — `git checkout -- commands/newsletter.md`
- **Checkpoint:** `git commit -m "feat(linkedin): newsletter Step 7-10 lock, delivery, hook-gate, schedule (S10)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/newsletter.md
min_file_count: 1
commit_message_pattern: "^feat\\(linkedin\\): newsletter Step 7-10"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "build-linkedin"
- path: commands/newsletter.md
pattern: "queue-manager"
- path: commands/newsletter.md
pattern: "Step 7"
- path: commands/newsletter.md
pattern: "Step 9"
- path: commands/newsletter.md
pattern: "Step 10"
```
> **Order assertion** (hook-gate Step 9 AFTER lock Step 8): `must_contain`
> proves all four phases exist; ordering is the Verify grep + `[GATE]`.
### Step 12: S11 — Reconcile newsletter path out of multiplatform + skill trigger + router row
- **Files:** `commands/multiplatform.md` (edit), `commands/linkedin.md` (edit), `skills/linkedin-content-creation/SKILL.md` (edit) + the other 5 `skills/*/SKILL.md` catalogs (edit — correction #7)
- **Changes:** Remove the newsletter/blog adaptation path from `multiplatform.md` so there is exactly ONE entry to long-form (fasit §4.1). Add the langform trigger to `skills/linkedin-content-creation/SKILL.md` (fasit §5.3) AND sweep the catalog tables in all 6 `skills/*/SKILL.md` (N2). Add a router row for `newsletter` in `commands/linkedin.md`. (codebase analysis)
- **Reuses:** existing router-row format in `linkedin.md`; skill trigger format.
- **Test first:** *(archetype F)* grep proves only one newsletter entry; router row present; no dead newsletter path in multiplatform.
- **Verify:** `[ "$(grep -Eci 'Step.*newsletter|newsletter (pipeline|workflow|edition)' commands/multiplatform.md)" = "0" ] && grep -q 'newsletter' commands/linkedin.md` → expected: **0** multi-step newsletter section in multiplatform (a one-line pointer is allowed, any wording); router row present in linkedin.md. (Robust: does not depend on exact pointer phrasing — only that no *pipeline/section* survives. **Fix v2.0 doc-pass:** the original `grep -Eci ... && grep -c ...` was `&&`-broken — `grep -c` returns the count as stdout but exits non-zero when count=0, so the chain short-circuited even on the desired "0 matches" outcome. Reworked to an explicit string-equality test.)
- **On failure:** revert — `git checkout -- commands/multiplatform.md commands/linkedin.md skills/`
- **Checkpoint:** `git commit -m "refactor(linkedin): single newsletter entry + skill trigger + router row (S11)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/linkedin.md
- commands/multiplatform.md
min_file_count: 2
commit_message_pattern: "^refactor\\(linkedin\\): single newsletter entry"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/linkedin.md
pattern: "newsletter"
```
### Step 13: S12 — longform-quality-rules.md + resumption wiring
- **Files:** `references/longform-quality-rules.md` (new), `commands/newsletter.md` (edit — Step 0 reads edition-state for resumption)
- **Changes:** Codify the fasit §8 quality rules (leader-takeaway, premise→conclusion arc, forbidden AI-slop phrases, generic-not-agency-specific, minimal formatting-dose, gap-closing by tightening not expansion, per-sweep calibration). Wire resumption: Step 0 reads edition-state and continues from the right step (abort → re-run → resumes). (codebase analysis)
- **Reuses:** edition-state schema (Step 7); fasit §8 content.
- **Test first:** *(archetype C+E)* file exists with all §8 rules (grep); resumption: abort after Step 6 → re-run → resumes from Step 7 (deterministic via edition-state).
- **Verify:** `test -f references/longform-quality-rules.md && grep -ci 'leder-takeaway\|premiss\|AI-slop\|formaterings-dose' references/longform-quality-rules.md` → expected: file present; rules present. Resumption = deterministic test
- **On failure:** revert — `git checkout -- references/longform-quality-rules.md commands/newsletter.md`
- **Checkpoint:** `git commit -m "feat(linkedin): longform quality rules + edition resumption wiring (S12)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- references/longform-quality-rules.md
- commands/newsletter.md
min_file_count: 2
commit_message_pattern: "^feat\\(linkedin\\): longform quality rules"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: references/longform-quality-rules.md
pattern: "AI-slop"
```
### Step 14: S13 — Dogfood: produce a real edition end-to-end
> **`[OPERATØR]`-gated session — not pure headless.** This step runs the live
> pipeline with a human in the loop (browser walkthrough, real edition).
> A headless `claude -p` cannot self-certify a real edition's quality.
- **Files:** `docs/voyage-build/dogfood-S13-friction.md` (new — the in-plugin deliverable). Edition content is produced in a maskinrommet serie-mappe (operator-gated, stays in that repo).
- **Changes:** Run `/linkedin:newsletter` end-to-end to produce one real edition (files in the serie-mappe). **Cross-repo write to maskinrommet requires explicit operator instruction** (R1) — confirm before writing there; otherwise dogfood against a throwaway serie fixture inside `docs/review/` scope. Open the review HTML in a browser and walk the core flows (dogfood-UI gate). **Write a structured friction log to `docs/voyage-build/dogfood-S13-friction.md`** recording: each friction point (numbered), an order-proof note (edition-HANDOVER shows persona-sweep BEFORE lock), and which pipeline file each friction implicates (drives Step 15's revert targets). (codebase analysis)
- **Reuses:** the full Step 813 pipeline.
- **Test first:** *(archetype G — operator/manual)* an edition produced end-to-end; order-proof: edition-HANDOVER shows persona-sweep BEFORE lock; review HTML opened.
- **Verify:** `test -f docs/voyage-build/dogfood-S13-friction.md && grep -ci 'sweep.*lås\|before lock\|FØR lås' docs/voyage-build/dogfood-S13-friction.md` → expected: friction log exists; order-proof recorded. Edition quality + UI = `[OPERATØR]`
- **On failure:** escalate — dogfood reveals design friction; capture it in the log, do not force a green check
- **Checkpoint:** `git commit -m "test(linkedin): dogfood newsletter pipeline end-to-end (S13)"` *(edition content stays in maskinrommet; only the friction log is committed plugin-side)*
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/voyage-build/dogfood-S13-friction.md
min_file_count: 1
commit_message_pattern: "^test\\(linkedin\\): dogfood newsletter"
bash_syntax_check: []
forbidden_paths:
- /Users/ktg/repos/maskinrommet/tools/build-html.mjs
must_contain:
- path: docs/voyage-build/dogfood-S13-friction.md
pattern: "[Ff]riction|[Ff]riksjon"
```
> **Blocker fix:** the friction-log file is now a real, checkable deliverable
> (file must exist + record the order-proof) — the step can no longer pass by
> producing nothing. The edition's subjective quality stays `[OPERATØR]` per
> fasit §10.0.
### Step 15: S14 — Fix dogfood friction
> **`[OPERATØR]`-gated session.** Revert targets come from the Step 14 friction
> log's "implicates file X" notes — that log is the referent for every fix and
> every `git checkout`.
- **Files:** the pipeline files named in `docs/voyage-build/dogfood-S13-friction.md`; `docs/voyage-build/dogfood-S13-friction.md` (update with re-test outcomes)
- **Changes:** Close each friction point from Step 14 with a concrete fix; re-test each with a concrete check (not "fixed"). Update the friction log with per-item status (✅ re-tested / 🔶 deferred). Remaining items either closed or explicitly deferred with operator's knowledge. (codebase analysis)
- **Reuses:** the S13 friction log (names the files to touch + revert).
- **Test first:** *(archetype G)* each closed friction point re-tested with a concrete check; restliste empty or explicitly deferred.
- **Verify:** `grep -c '✅\|🔶' docs/voyage-build/dogfood-S13-friction.md` → expected: every friction item has a ✅ (re-tested) or 🔶 (deferred with operator note) — no silent closures
- **On failure:** revert the specific fix using the file path recorded against that friction item in the log (`git checkout -- <that file>`); if the log does not name the file, escalate
- **Checkpoint:** `git commit -m "fix(linkedin): close dogfood friction (S14)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/voyage-build/dogfood-S13-friction.md
min_file_count: 1
commit_message_pattern: "^fix\\(linkedin\\): close dogfood friction"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: docs/voyage-build/dogfood-S13-friction.md
pattern: "✅|🔶"
```
### Step 16: S15 — templates.md → mode in quick.md
- **Files:** `commands/quick.md` (edit), remove `commands/templates.md`, `commands/linkedin.md` (router edit)
- **Changes:** Enumerate every one of the 8 template types in `templates.md` and confirm each is covered by a mode in `quick.md` (capability checklist — archetype F). Remove `templates.md`. Grep the expanded target set (`commands/ agents/ skills/ hooks/ README.md CLAUDE.md agents/README.md`) for `templates` route-refs and fix all (N1). (codebase analysis)
- **Reuses:** existing `quick.md` 3-line formula + templates bank.
- **Test first:** *(archetype F)* capability checklist: all 8 types in quick; `templates.md` gone; `ls commands/` down 1; no dead links.
- **Verify:** `! test -f commands/templates.md && grep -rn '/linkedin:templates\|commands/templates' commands/ agents/ skills/ hooks/ README.md CLAUDE.md agents/README.md` → expected: file gone; zero stray route-refs (only intentional)
- **On failure:** revert — `git checkout -- commands/quick.md commands/templates.md commands/linkedin.md`
- **Checkpoint:** `git commit -m "refactor(linkedin): merge templates into quick (S15)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/quick.md
min_file_count: 1
commit_message_pattern: "^refactor\\(linkedin\\): merge templates into quick"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/quick.md
pattern: "template"
```
### Step 17: S16 — publish.md → action in calendar.md
- **Files:** `commands/calendar.md` (edit), remove `commands/publish.md`, plus the 9 hook-script refs (N1: `session-start.mjs`, `posting-reminder.mjs`, `user-prompt-context.mjs`, `hooks/prompts/state-update-reminder.md`)
- **Changes:** Move the publish action into `calendar.md` (both read `queue.json`; calendar already routes to publish). Remove `publish.md`. **Critical (N1):** `publish` has 21 route-refs, 9 inside hook scripts that emit runtime guidance — update every one or the plugin ships text pointing at a dead command. Re-compile hooks if any `hooks/` source changed. (codebase analysis)
- **Reuses:** `queue-manager.mjs`; existing calendar→publish routing.
- **Test first:** *(archetype F)* capability checklist: calendar covers publish; `publish.md` gone; all 21 refs (incl. 9 hook refs) reconciled.
- **Verify:** `! test -f commands/publish.md && grep -rn '/linkedin:publish\|commands/publish' commands/ agents/ skills/ hooks/ README.md CLAUDE.md` → expected: file gone; zero stray refs; `python3 hooks/scripts/compile-hooks.py --check` clean if hooks touched
- **On failure:** revert — `git checkout -- commands/calendar.md commands/publish.md hooks/`
- **Checkpoint:** `git commit -m "refactor(linkedin): merge publish into calendar — reconcile hook refs (S16)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/calendar.md
min_file_count: 1
commit_message_pattern: "^refactor\\(linkedin\\): merge publish into calendar"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/calendar.md
pattern: "[Pp]ublish"
```
### Step 18: S17 — collab.md + speaking.md → new outreach.md
- **Files:** `commands/outreach.md` (new), remove `commands/collab.md`, remove `commands/speaking.md`, `commands/linkedin.md` (router edit)
- **Changes:** Create `outreach.md` covering both collab and speaking (structural twins: same outreach/pitch paradigm + pipeline table). Capability checklist: every function of both predecessors present in outreach. Remove both. Reconcile all route-refs (collab 8, speaking 8, incl. `README.md:511` ToS table). (codebase analysis)
- **Reuses:** the shared outreach/pitch structure from both files.
- **Test first:** *(archetype F)* checklist covers collab + speaking; both predecessors gone; net down 1; no dead links.
- **Verify:** `test -f commands/outreach.md && ! test -f commands/collab.md && ! test -f commands/speaking.md && grep -rn '/linkedin:collab\|/linkedin:speaking' commands/ agents/ skills/ hooks/ README.md CLAUDE.md` → expected: outreach present; both gone; zero stray refs
- **On failure:** revert — `git checkout -- commands/`
- **Checkpoint:** `git commit -m "refactor(linkedin): merge collab + speaking into outreach (S17)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/outreach.md
min_file_count: 1
commit_message_pattern: "^refactor\\(linkedin\\): merge collab \\+ speaking into outreach"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/outreach.md
pattern: "[Ss]peaking"
```
### Step 19: S18 — authority.md → strategy.md + trajectory dedup + profile canon
- **Files:** `commands/strategy.md` (edit), remove `commands/authority.md`, `commands/audit.md` (edit — point to profile/strategy), `commands/analyze.md` (edit — point to profile)
- **Changes:** Absorb `authority.md` into a section of `strategy.md` (authority has no unique core). De-duplicate trajectory logic to live only in `strategy.md`; `audit.md` references it. Make `profile.md` the canonical source for profile-alignment; `audit.md`/`analyze.md` point there. Remove `authority.md`. (codebase analysis)
- **Reuses:** existing strategy phase content; profile-alignment check in `profile.md`.
- **Test first:** *(archetype F)* strategy covers authority + trajectory; audit/analyze point to profile canon; `authority.md` gone; no dead links.
- **Verify:** `! test -f commands/authority.md && grep -rn '/linkedin:authority\|commands/authority' commands/ agents/ skills/ hooks/ README.md CLAUDE.md` → expected: gone; zero stray refs
- **On failure:** revert — `git checkout -- commands/`
- **Checkpoint:** `git commit -m "refactor(linkedin): absorb authority into strategy + profile canon (S18)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/strategy.md
min_file_count: 1
commit_message_pattern: "^refactor\\(linkedin\\): absorb authority into strategy"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/strategy.md
pattern: "[Aa]uthority"
```
### Step 20: S19 — Agent merges: analytics (2→1) + engagement (2→1)
- **Files:** `agents/analytics-interpreter.md` (edit → analytics, 2 modes), remove `agents/performance-reporter.md`, `agents/engagement-coach.md` (edit → engagement), remove `agents/comment-strategist.md`, `agents/README.md` (edit), `CLAUDE.md` (edit)
- **Changes:** Merge `performance-reporter` into `analytics-interpreter` (one analytics agent, interpret/report modes — identical data sources). Merge `comment-strategist` into `engagement-coach` (5x5x5 + first-hour + CEA). **Rewrite `engagement-coach.md:24`** ("defer to the comment-strategist agent" — correction #8) since the target now lives in-file. Decide Q2 (video-scripter → content-repurposer) here. Reconcile all refs incl. `agents/README.md` flow diagrams + `skills/linkedin-analytics/SKILL.md:40`. (codebase analysis)
- **Reuses:** existing agent bodies (merge, don't rewrite from scratch).
- **Test first:** *(archetype F)* each mode in the merged agent covers predecessors' functions (checklist); `ls agents/` down 2; no dead links; the self-ref at line 24 rewritten.
- **Verify:** `! test -f agents/performance-reporter.md && ! test -f agents/comment-strategist.md && ! grep -n 'comment-strategist agent' agents/engagement-coach.md && grep -rn 'performance-reporter\|comment-strategist' agents/ skills/ README.md CLAUDE.md agents/README.md` → expected: both gone; self-ref rewritten; zero stray refs
- **On failure:** revert — `git checkout -- agents/ CLAUDE.md`
- **Checkpoint:** `git commit -m "refactor(linkedin): merge analytics + engagement agents 2→1 each (S19)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- agents/analytics-interpreter.md
- agents/engagement-coach.md
min_file_count: 2
commit_message_pattern: "^refactor\\(linkedin\\): merge analytics \\+ engagement"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: agents/engagement-coach.md
pattern: "CEA"
```
### Step 21: S20 — import.md trim + router gating + final doc pass → v2.0.0
- **Files:** `commands/import.md` (edit), `commands/linkedin.md` (router gating edit), `README.md`, `CLAUDE.md`, `.claude-plugin/plugin.json`, `CHANGELOG.md`, marketplace `README.md` + `CLAUDE.md` (version refs)
- **Changes:** Delegate `import.md` Step 6 analysis to `report.md` (two report generators run the same `trends` CLI). Add router gating in `linkedin.md` (show monetize/outreach/collab as "unlocks at ~1K followers"). **Version bump to v2.0.0** — update all 7 in-tree refs (`plugin.json:3`, `CLAUDE.md:1`, `README.md:9/23/42/522`, `CHANGELOG.md:8`) + 2 marketplace refs (`README.md:209`, `CLAUDE.md:12`), and fix the `#whats-new-v120` anchor → `#whats-new-v200`. Final doc pass: all 3 doc levels (plugin README + plugin CLAUDE + root README) reflect v2.0.0 scope (fasit §10.5). (codebase analysis)
- **Reuses:** `report.md` trends CLI; existing version-sync discipline.
- **Test first:** *(archetype F)* command count verified down; all version refs = 2.0.0 (grep); all 3 doc levels updated; router gating present.
- **Changes (count, corrected):** net command count = **24** = 27 today 5 removed (`templates`, `publish`, `authority`, `collab`, `speaking`) + 2 added (`newsletter`, `outreach`). (The fasit's "~23" was approximate; the exact arithmetic is 24. This is the binding number.)
- **Verify:** `test "$(ls commands/*.md | wc -l | tr -d ' ')" = "24" && grep -q '"version": "2\\.0\\.0"' .claude-plugin/plugin.json && grep -q '^# LinkedIn Thought Leadership Plugin (v2\\.0\\.0)' CLAUDE.md && grep -q 'version-2\\.0\\.0-blue' README.md && grep -q '^## \\[2\\.0\\.0\\]' CHANGELOG.md` → expected: command count is **exactly 24**; every target file carries an **active** v2.0.0 marker (forward-positive assertion). **Fix v2.0 doc-pass:** original `! grep '1\\.2\\.0'` was overly strict — it would have blocked any release that correctly preserves historical changelog entries. Forward-positive form asserts the actual release intent (active version is 2.0.0) without requiring deletion of changelog history.
- **On failure:** revert — `git checkout -- commands/ README.md CLAUDE.md .claude-plugin/ CHANGELOG.md`
- **Checkpoint:** `git commit -m "chore(linkedin): v2.0.0 — import trim, router gating, full doc pass (S20)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- .claude-plugin/plugin.json
- CHANGELOG.md
- README.md
- CLAUDE.md
min_file_count: 4
commit_message_pattern: "^chore\\(linkedin\\): v2\\.0\\.0"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: .claude-plugin/plugin.json
pattern: "2.0.0"
- path: CHANGELOG.md
pattern: "2.0.0"
- path: CLAUDE.md
pattern: "2.0.0"
- path: README.md
pattern: "2.0.0"
```
> **Blocker fix:** the count predicate is now a single exact value (24), tested
> with a string-equality assertion in Verify. The earlier self-contradictory
> "22 / 23 / verify net" is removed. Note: agent net count stays 16 (4 removed,
> 2 added — verified in the end-to-end Verification block).
## Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|----------|------|------|--------------|
| Newsletter as a suite of 5 phase-commands | Each phase independently invocable | Surface explosion; against beslutning A; fragments resumption | Locked decision A: one orchestrator command |
| Extend `commands/pipeline.md` for long-form | Reuse existing pipeline | pipeline is locked to feed-post artifact contract (wrong output shape) | Locked decision A |
| Keep render scripts in maskinrommet, call cross-repo | No migration work | Forgejo downloaders get no render pipeline; cross-repo coupling | Locked decision C: ship render in plugin |
| Finer Voyage steps (35 per session) | Matches Voyage default granularity | Explodes to 60+ steps; fasit already validated session sizing at ≤35 % context | 1 step = 1 session is the right grain here |
| Integrate fact-check into research/review | Fewer agents | "Altinn-feilen" proved research notes miss facts; needs a dedicated sweep | Locked decision E: fact-check is its own step |
## Test Strategy
- **Framework:** `node:test` + `node:assert/strict`, zero deps. Run with the
**glob form** `node --test 'render/__tests__/*.test.mjs'` (Node 25 breaks
`node --test <dir>`).
- **Existing patterns:** `hooks/scripts/__tests__/*.test.mjs` (inline
template-literal fixtures, pure-function calls, `assert.match`/`assert.equal`);
`scripts/analytics/tests/*.test.ts` (file fixtures in `tests/fixtures/`).
- **Clean test-first (render scripts):** the first production change is to add
`export` + the CLI guard, so the failing import-test drives the refactor
(Steps 2, 3). Mirrors `state-updater.mjs:227`.
- **Awkward test-first (agents):** agents are .md prompts — not unit-testable.
Automate a **fixture-schema lint** as `node:test`; route the actual accuracy
comparison to an `[OPERATØR]`/`[GATE]` manual check (corrections #5, fasit §10.0).
### Tests to write
| Type | File | Verifies | Model test |
|------|------|----------|------------|
| Unit | `render/__tests__/weasyprint-degradation.test.mjs` | missing weasyprint → skip-signal, not throw | `state-updater.test.mjs` |
| Unit | `render/__tests__/build-html.test.mjs` | tables, `#``####`, inline code | `state-updater.test.mjs` |
| Unit | `render/__tests__/build-linkedin.test.mjs` | reads edition-config; config diff → output diff | `csv-parser.test.ts` (file fixture) |
| Lint | `agents/__tests__/fact-checker-fixture.test.mjs` | fixture has 3 cases, one each 🟢/🔴/🟡 | `state-updater.test.mjs` |
| Lint | `agents/__tests__/persona-reviewer-fixture.test.mjs` | persona def + 6 axes + both modes | `state-updater.test.mjs` |
## Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|----------|------|----------|--------|------------|
| High | Dead-link blast radius (N1) — `publish` has 21 refs incl. 9 in hook scripts emitting runtime guidance | `session-start.mjs:253,258,333,336,383`, `posting-reminder.mjs:94,95`, `user-prompt-context.mjs:46` | Ships text pointing at a removed command | Step 17 grep target set incl. `agents/README.md` + `CLAUDE.md`; recompile hooks; treat every merge as Medium |
| High | Parallel Task fan-out (fasit assumption 4) has NO existing precedent to copy | `commands/newsletter.md` Step 2 | If it degrades, the whole research phase falls back to guessing | Step 8 = highest-uncertainty checkpoint; escalate on degrade, don't paper over |
| Medium | weasyprint hard-fails; degradation not implemented (N/3) | `build-pdf.mjs:339`, `build-carousel.mjs:262` | PDF steps abort on machines without weasyprint | Write degradation in Step 1; force-PATH-miss test |
| Medium | No OFL license file at source (correction #2) | `render/OFL.txt` | License-compliance gap redistributing OFL fonts | Author OFL-1.1 text in Step 1 |
| Medium | Skill catalogs duplicated across 6 dirs (N2) | `skills/*/SKILL.md` | Stale catalogs ship | Steps 12, 21 sweep all 6 |
| Low | Font weight 1.5 MB (R3) | `render/fonts/` | Plugin size | Acceptable; OFL attached |
| Low | Cross-repo render migration (R1) | maskinrommet | Out of plugin scope | Explicit operator instruction before any maskinrommet write |
| Low | Opus cost on many parallel agent calls (R5) | Steps 10, 14 | Cost | Expected/accepted; escalate if volume spikes |
## Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|-----------|-----------------|-----------------|
| 1 | Foreground Task fan-out from a command keeps the Task tool (vs background agents losing it) | Pure runtime behavior; no static precedent in repo | Step 8/10 research + sweeps degrade to sequential/guessing |
| 2 | `${CLAUDE_PLUGIN_ROOT}` resolves in command Bash | Proven at runtime in existing commands, but env-injected | Render/script invocations fail to resolve |
| 3 | Retired agents' function is fully covered by existing scripts | personalization-score.mjs + state-updater exist, but coverage parity is judgment | A capability silently lost in Step 7 |
| 4 | maskinrommet write for dogfood (Step 14) gets explicit operator OK | Cross-repo; operator-gated | Dogfood blocked or done against a fixture only |
## Verification
End-to-end / cross-step checks (per-step Manifests run automatically during execution):
- [ ] `ls render/ && ls render/fonts/*.ttf | wc -l && node --test 'render/__tests__/*.test.mjs'` → 4 scripts + 8 fonts + OFL.txt; all render tests pass
- [ ] **Antakelse 2 (fonts resolve via `__dirname`, not fallback):** `cd /tmp && node <plugin>/render/build-pdf.mjs <sample.md>` produces a PDF embedding Newsreader/Inter (inspect PDF metadata / visual) — `[OPERATØR]` visual confirm
- [ ] `node --test 'agents/__tests__/*.test.mjs'` → fixture-lint tests pass
- [ ] `test "$(ls commands/*.md | wc -l | tr -d ' ')" = "24"` → command count is exactly 24 (27 5 removed + 2 added)
- [ ] `ls agents/*.md | grep -v README | wc -l` → **14** (content-tracker, personalization-scorer, performance-reporter, comment-strategist removed = 4; fact-checker, persona-reviewer added = +2; net 16 4 + 2 = 14). **Fix v2.0 doc-pass:** the original "16" was the pre-S20 figure carried over by mistake. **Correction:** S14 moved `agents/README.md` into the per-agent files, so `agents/README.md` no longer exists; the `grep -v README` filter is a no-op but harmless.
- [ ] `grep -rn '1\.2\.0' .claude-plugin/plugin.json CLAUDE.md README.md CHANGELOG.md` → zero matches (all bumped to 2.0.0)
- [ ] `grep -rEn '/linkedin:(templates|publish|authority|collab|speaking)\b|commands/(templates|publish|authority|collab|speaking)\.md|`:(templates|publish|authority|collab|speaking)`' commands/ agents/ skills/ hooks/ README.md CLAUDE.md` → zero stray route-refs to removed commands. **Fix v2.0 doc-pass:** original target set included `agents/README.md` (file removed in S14); extended to also match the shorthand backtick form (`` `:templates` ``, etc.) used in the pillar/skill tables — Step 21 doc-pass uncovered three live shorthand refs in `README.md:294` that the original grep would have missed.
- [ ] `python3 hooks/scripts/compile-hooks.py --check` → clean (no drift after hook-ref edits)
- [ ] `[OPERATØR]` one real edition produced end-to-end with persona-sweep before lock, reviewed in browser
- [ ] All 3 doc levels (plugin README + plugin CLAUDE + root README) reflect v2.0.0
## Estimated Scope
- **Files to create:** ~13 (newsletter.md, outreach.md, fact-checker.md, persona-reviewer.md, personas.template.md, edition-state.template.json, longform-quality-rules.md, OFL.txt, 4 render scripts under render/ + fonts/, plus test/fixture files)
- **Files to modify:** ~15 (multiplatform, linkedin router, quick, calendar, strategy, audit, analyze, import, profile, analytics-interpreter, engagement-coach, 6 skills, agents/README, CLAUDE.md, README.md, plugin.json, CHANGELOG, hook scripts, marketplace docs)
- **Files to remove:** **9** = 5 commands (`templates`, `publish`, `authority`, `collab`, `speaking`) + 4 agents (`content-tracker`, `personalization-scorer`, `performance-reporter`, `comment-strategist`). **Fix v2.0 doc-pass:** original "7" was a stale count from an earlier draft that listed only one of the two outreach precursors. The 5 + 4 arithmetic is binding.
- **Complexity:** high (21 sessions, multi-session resumption, cross-repo touchpoint, runtime-assumption checkpoint)
## Execution Strategy
> **Execution is strictly ONE step per session, run sequentially via
> `/trekexecute --step N --project docs/voyage-build`** (subscription; never
> `--fg`, which runs all 21 steps in a single session; never parallel
> `claude -p`, API billing). `/trekcontinue` advances exactly one session
> (= one step) at a time. Each session is a self-contained ≤35 %-context
> deliverable that MUST complete within its own context window; `/clear`
> between sessions. The 21 sessions below map **1:1** to the 21 steps. Waves
> are dependency groupings, **not** parallelism licenses. **Continuity handoff
> is via `STATE.md`** — `NEXT-SESSION-PROMPT.local.md` is deprecated per the
> global continuity system (STATE.md + MEMORY.md + CLAUDE.md). trekexecute may
> still auto-write that file; treat it as ignorable noise, never as the handoff.
### Session 1: S1 — Migrate render scripts + fonts into the plugin
- **Step:** 1 · **Wave:** 1 · **Depends on:** none
### Session 2: S1a — Generalize the annotation renderer (build-html.mjs)
- **Step:** 2 · **Wave:** 1 · **Depends on:** Session 1 (render present)
### Session 3: S2 — Generalize build-linkedin.mjs to read edition-config.json
- **Step:** 3 · **Wave:** 1 · **Depends on:** Session 1 (render present)
### Session 4: S3 — Persona library (config/personas.template.md)
- **Step:** 4 · **Wave:** 1 · **Depends on:** none (internal)
### Session 5: S4 — fact-checker agent (agents/fact-checker.md)
- **Step:** 5 · **Wave:** 1 · **Depends on:** none (internal)
### Session 6: S5 — persona-reviewer agent (agents/persona-reviewer.md, 2 modes)
- **Step:** 6 · **Wave:** 1 · **Depends on:** Session 4 (personas)
### Session 7: S6 — Edition-state schema + retire content-tracker & personalization-scorer
- **Step:** 7 · **Wave:** 1 · **Depends on:** none (internal)
### Session 8: S7 — newsletter.md skeleton, Step 02 (load, calibrate, research fan-out)
- **Step:** 8 · **Wave:** 2 · **Depends on:** Wave 1 complete (agents, personas, render, edition-state)
### Session 9: S8 — newsletter.md Step 34 (draft + consistency/quality)
- **Step:** 9 · **Wave:** 2 · **Depends on:** Session 8 (newsletter.md, strict order)
### Session 10: S9 — newsletter.md Step 56 (fact-check sweep + persona sweep BEFORE lock)
- **Step:** 10 · **Wave:** 2 · **Depends on:** Session 9
### Session 11: S10 — newsletter.md Step 710 (annotate, lock/delivery, hook-gate, schedule)
- **Step:** 11 · **Wave:** 2 · **Depends on:** Session 10
### Session 12: S11 — Reconcile newsletter path out of multiplatform + skill trigger + router row
- **Step:** 12 · **Wave:** 2 · **Depends on:** Session 11
### Session 13: S12 — longform-quality-rules.md + resumption wiring
- **Step:** 13 · **Wave:** 2 · **Depends on:** Session 11 (rules inlined in newsletter.md at Session 9; extracted here)
### Session 14: S13 — Dogfood: produce a real edition end-to-end `[OPERATØR]`
- **Step:** 14 · **Wave:** 3 · **Depends on:** Wave 2 complete (full pipeline) + Wave 1 render
### Session 15: S14 — Fix dogfood friction `[OPERATØR]`
- **Step:** 15 · **Wave:** 3 · **Depends on:** Session 14 (friction log)
### Session 16: S15 — templates.md → mode in quick.md
- **Step:** 16 · **Wave:** 4 · **Depends on:** Wave 1 (independent of Wave 23)
### Session 17: S16 — publish.md → action in calendar.md
- **Step:** 17 · **Wave:** 4 · **Depends on:** Wave 1
### Session 18: S17 — collab.md + speaking.md → new outreach.md
- **Step:** 18 · **Wave:** 4 · **Depends on:** Wave 1
### Session 19: S18 — authority.md → strategy.md + trajectory dedup + profile canon
- **Step:** 19 · **Wave:** 4 · **Depends on:** Wave 1
### Session 20: S19 — Agent merges: analytics (2→1) + engagement (2→1)
- **Step:** 20 · **Wave:** 4 · **Depends on:** Wave 1
### Session 21: S20 — import.md trim + router gating + final doc pass → v2.0.0
- **Step:** 21 · **Wave:** 4 · **Depends on:** ALL prior sessions (closes v2.0.0 — always last overall)
### Wave scope fences (reference)
Scope fences are defined per wave; each session inherits its wave's fence.
- **Wave 1 (Sessions 17):** Touch `render/`, `config/personas.template.md`, `config/edition-state.template.json`, `agents/fact-checker.md`, `agents/persona-reviewer.md`, `agents/fixtures/`, `agents/__tests__/`, remove content-tracker + personalization-scorer, `agents/README.md`, `CLAUDE.md` (agent table). Never touch `commands/newsletter.md` (Wave 2), any consolidation target (Wave 4).
- **Wave 2 (Sessions 813):** Touch `commands/newsletter.md`, `commands/multiplatform.md`, `commands/linkedin.md`, `skills/*/SKILL.md`, `references/longform-quality-rules.md`. Never touch render scripts (frozen after Wave 1), consolidation targets (Wave 4).
- **Wave 3 (Sessions 1415):** Touch a serie-mappe (maskinrommet — operator-gated) or a `docs/review/` fixture; friction log; whichever pipeline files S14 fixes name. Never touch maskinrommet without explicit operator instruction (R1).
- **Wave 4 (Sessions 1621):** Touch consolidation targets (quick, calendar, outreach, strategy, audit, analyze, import, profile, linkedin router), `agents/analytics-interpreter.md`, `agents/engagement-coach.md`, removed files, all doc levels, version refs, hook scripts (publish refs). Never touch `commands/newsletter.md` internals (frozen after Wave 2).
### Execution Order
Run sessions **1 → 21 in numeric order**, one per `/trekcontinue` (or
`/trekexecute --step N`). Wave boundaries are dependency gates: do not begin a
Wave-2 session before Wave 1 is complete; Session 21 is always last (closes
v2.0.0). Wave 4 (Sessions 1621) is independent of Waves 23 and may run any
time after Wave 1, but the canonical order is sequential 1→21.
### Grouping rules applied
- One step per session — each is a full ≤35 %-context deliverable that completes within its own context window.
- Steps sharing files are adjacent and strictly ordered (newsletter Sessions 811 all touch `newsletter.md`).
- Render (Sessions 13) frozen before the newsletter command consumes it.
- Consolidation (Wave 4) isolated from langform files to avoid cross-contamination.
## Plan Quality Score
| Dimension | Weight | Score | Notes |
|-----------|--------|-------|-------|
| Structural integrity | 0.15 | 95 | 21 steps, dependency-ordered, waves match fasit phases |
| Step quality | 0.20 | 92 | each step has Files/Changes/Reuses/Test-first/Verify/On-failure/Checkpoint/Manifest; some Verify cmds approximate (agent gates) |
| Coverage completeness | 0.20 | 95 | every fasit session S1S20 (+S1a) mapped; all decisions AH realized |
| Specification quality | 0.15 | 90 | concrete paths + reuse refs; a few `[OPERATØR]`/`[GATE]` steps are intentionally non-mechanical |
| Risk & pre-mortem | 0.15 | 92 | R1R5 + N1N3 + 4 assumptions; highest-uncertainty checkpoint flagged |
| Headless readiness | 0.10 | 90 | On-failure + Checkpoint per step; multi-session resumption via project dir |
| Manifest quality | 0.05 | 85 | every step has a real predicate after revision; consolidation deletions bind to Verify (schema is positive-match only — acknowledged) |
| **Weighted total** | **1.00** | **90** | **Grade: A** |
**Adversarial review:**
- **Plan critic:** REPLAN → revised. 3 blockers + 6 major + 5 minor found; all blockers + 5/6 major + 4/5 minor addressed (see Revisions). The one major not "fixed" (M2: Manifest can't encode deletions) is an acknowledged schema limitation — bound to the Verify gate instead.
- **Scope guardian:** ALIGNED. 0 scope-creep; all S1S20 (+S1a) mapped 1:1; decisions AH realized, none re-litigated; maskinrommet read-only/operator-gated; decision B (no short-form extension) honored. 2 gaps + 1 dependency issue — all addressed in Revisions.
## Revisions
| # | Finding | Severity | Resolution |
|---|---------|----------|------------|
| 1 | Step 21 command-count predicate self-contradictory (22/23/"verify net"); correct net is 24 | blocker | Verify rewritten to exact string-equality `= "24"`; Changes states the binding number; "~23" noted as approximate. Manifest adds CLAUDE.md + README.md `2.0.0` checks |
| 2 | Step 2 Manifest (string `export`/`import.meta.url`) doesn't prove the table/heading/inline-code generalization (a no-op passes) | blocker | Manifest `must_contain` now greps the production renderer for `<table>`, `<h4`, `<code>` — output markers a no-op cannot emit |
| 3 | Steps 14, 15 empty Manifests + Step 9 single-string Manifest = rubber stamps | blocker | Step 14 now requires `docs/voyage-build/dogfood-S13-friction.md` (with order-proof); Step 15 requires the log updated with ✅/🔶 per item; Step 9 Manifest adds `Step 3`/`Step 4`/`AI-slop` |
| 4 | Newsletter Steps 811 hide 24 phases behind single-string Manifests; order assertions only in grep | major | Each Manifest now requires all phase `Step N` headings present; order (sweep-before-lock, hook-after-lock) bound to Verify grep + `[GATE]` with explicit notes |
| 5 | Step 7 deletions + capability-parity not verified by Manifest | major | Acknowledged schema limit (positive-match only); Verify `! test -f` + dead-link grep made the binding predicate; archetype-F note added |
| 6 | Step 9 forward-references `longform-quality-rules.md` created in Step 13 | major | Step 9 now inlines the §8 rules in newsletter.md; Step 13 extracts them to the reference file + leaves a pointer. No dangling reference at any point |
| 7 | Step 15 On-failure ("revert the specific fix") had no referent | major | On-failure now reverts using the file path recorded against each friction item in the S13 log; escalate if unnamed. Steps 1415 marked `[OPERATØR]`-gated (not pure headless) |
| 8 | Step 8 fan-out Manifest only checked the command name | major | Manifest adds `Step 0`/`Step 2`/`parallel`; runtime fan-out behavior remains the `[GATE]` + escalate On-failure (cannot be static) |
| 9 | Step 12 grep depended on exact pointer wording `see /linkedin:newsletter` | major | Verify rewritten to assert no multi-step newsletter *section* survives (`Step.*newsletter` count = 0); a one-line pointer of any wording is allowed |
| 10 | build-linkedin constants cited "3250"; actual 3450 | minor | Citations corrected to 3450 with per-constant line refs (CALENDAR:34, FRESHNESS:44, COVER_CREDIT:49, CAPTIONS:50) |
| 11 | Step 1 `min_file_count: 6` undercounts; fonts absent from Manifest | minor | Added `render/fonts/Inter-400.ttf` + `render/fonts/Newsreader-400.ttf` to expected_paths; `min_file_count: 8`; Verify asserts 8 .ttf; build-carousel weasyprint added to must_contain |
| 12 | Step 6 Verify `grep -ci 'modus\|mode'` — `\|` not portable on BSD grep (darwin) | minor | Rewritten to `grep -Eci 'resonans\|konverter'` (BSD-safe `-E`) |
| 13 | Scope gap: antakelse 2 (PDF fonts resolve via `__dirname`, not fallback) not asserted | minor | Added to end-to-end Verification as an `[OPERATØR]` PDF-metadata check |