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>
72 KiB
| task | slug | project_dir | created | plan_version | profile | phase_models | profile_source | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Lift linkedin-thought-leadership plugin v1.2.0 → v2.0.0 — full-spectrum content engine + newsletter, net-fewer commands/agents | ltl-v2-fullspektrum | docs/voyage-build | 2026-05-26 | 1.7 | premium |
|
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.7This is the Voyage-executable plan. The authoritative human spec is
../plan-fullspektrum-innholdsmotor.md(the "fasit"). This file translates that spec's 20 sessions (S1–S20) 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):
- Renovation — consolidate real redundancy (27→~23 commands, 16→~14 agents).
- Long-form capability — one new
/linkedin:newslettercommand with a phased, multi-session pipeline (research → draft → fact-check → persona-review BEFORE lock → delivery → hook-gate). - Render + annotation into the plugin — move 4 render scripts + fonts;
generalize
build-linkedin; generalizebuild-htmlinto an artifact annotation renderer.
Decisions A–H (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
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
.mdfiles (YAML frontmatter + Markdown system-prompt bodies). Hooks are Node.js.mjscompiled fromhooks/hooks.template.jsonviahooks/scripts/compile-hooks.py. Tests use the built-innode:test+node:assert/strict. Zero npm deps in hooks/scripts. Node v25.8.2 (sonode --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 isname: linkedin:<verb>,description: |block ending in aTriggers on:line,allowed-tools:YAML bullet list (only tools actually used). Body: H1 + "You are a…" persona line +## Step 0: Load Contextonward + closing## Reference Filesbullet list using${CLAUDE_PLUGIN_ROOT}/...paths. - Agent frontmatter (
agents/differentiation-checker.md,agents/content-repurposer.md):name:bare kebab-case,description: |block ending inTriggers on:,model:bare keyword,color:bare word,tools: ["Read", ...]JSON inline array. Existing agents usemodel: sonnet, but the fasit (§6.2/§6.3) + KTG global rule mandatemodel: opusfor the two new agents. - Reuse targets (verified):
hooks/scripts/state-updater.mjs(export functions + CLI guardif (import.meta.url === \file://${process.argv[1]}`)at line 227 — the canonical export/guard pattern),queue-manager.mjs(PLUGIN_ROOTenv +__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 34–50: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):
- Fonts: only
build-pdf.mjs+build-carousel.mjsconsume fonts (viafile://URLs from__dirname/fonts, not base64).build-html.mjs+build-linkedin.mjsuse system-font stacks and need no fonts. Fonts dir = 1.5 MB, 8 .ttf (Inter 400/600/700 + Newsreader 400/400i/600/600i/700). - No OFL license file exists anywhere in maskinrommet.
render/OFL.txtmust be authored/sourced (Inter + Newsreader are OFL-1.1), not copied. - weasyprint graceful degradation is NOT implemented —
build-pdf.mjs:339andbuild-carousel.mjs:262hard-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. - 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 addexport+ the CLI guard (copystate-updater.mjs:227). The failing import-test drives this naturally. - Agent "known-answer fixtures" cannot be
node:test(agents are .md prompts, no importable function, no deterministic LLM output). Split: (a) an automatednode:testlints 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." - Dead-link blast radius is larger than the fasit's "Lav" labels (N1, High):
publishalone 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 addagents/README.md+CLAUDE.md. Treat every consolidation merge as Medium. - Skill catalogs duplicated across 6 skill dirs (N2): the langform trigger
- catalog updates (Steps 11, 21) must sweep all 6
skills/*/SKILL.md, not justlinkedin-content-creation.
- catalog updates (Steps 11, 21) must sweep all 6
engagement-coach.md:24has 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.- 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
/trekcontinuesession is the correct granularity (not the usual 3–5 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). Authorrender/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 torender/build-pdf.mjs+render/build-carousel.mjs(correction #3): beforeexecFileSync("weasyprint", …), detect weasyprint on PATH; if absent, print a clear install instruction and skip the PDF step with a non-fatal warning instead ofprocess.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
weasyprintis 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)
- File:
- 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:
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
exporttomarkdownToHtml,inline, and the table/heading helpers, and wrap the CLI body in the guardif (import.meta.url === \file://${process.argv[1]}`)(copystate-updater.mjs:227). Then generalize the markdown→HTML engine (beslutning H): support tables (| a | b |→), all heading levels#–####(today only##/###), inline ``code`` →(todayinline()only doesbold/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:227CLI-guard pattern; the existing annotation engine inside the sourcebuild-html.mjs.- Test first:
- File:
render/__tests__/build-html.test.mjs(new) - Verifies:
markdownToHtmlconverts 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; thencd /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:
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 productionbuild-html.mjsare the real generalization predicate — a no-op that adds onlyexport+ the CLI guard but leaves the markdown engine untouched will NOT emit<table>/<h4/<code>and fails the Manifest. Thenode --testin 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 hardcodedCALENDAR/FRESHNESS/CAPTIONS/COVER_CREDITconstants (build-linkedin.mjs:34–50) with a read oflinkedin/edition-config.jsonfromprocess.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:227guard; 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)
- File:
- 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:
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:
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 fromagents/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.mdgate-prompt form;state-updater.test.mjstest 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
- File:
- Verify:
node --test 'agents/__tests__/*.test.mjs' && grep -E '^model: opus' agents/fact-checker.md→ expected: fixture-lint passes; frontmatter hasmodel: 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:
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.mdform;state-updater.test.mjstest 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
- File:
- 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:
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), removeagents/content-tracker.md, removeagents/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) andstate-updater.mjs/calendar(plan-vs-queue diff) — verify the script path covers it, then delete the agent files. Updateagents/README.md(flow diagrams + Haiku table reference them — N1) andCLAUDE.mdagent count. (codebase analysis) - Reuses:
hooks/scripts/personalization-score.mjscalculateScore();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 -eon 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:
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_containis 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 (16–21), 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 0–2 (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.mdstep structure +${CLAUDE_PLUGIN_ROOT}Bash form;commands/react.mdmulti-source synthesis discipline. - Test first: (command prose — archetype E) run Step 0–2 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 0–2 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:
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/parallelpatterns force all three phases + the fan-out wiring to be present (a single-string no-op fails). The runtime behavior — that foregroundTaskfan-out keeps the Task tool and returns non-degraded results (Assumption 1, the highest-uncertainty checkpoint) — is the[GATE]in Verify and theescalateOn-failure; it cannot be encoded statically.
Step 9: S8 — newsletter.md Step 3–4 (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-repurposerextended + 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.mdis not authored until Step 13. So Step 4 inlines the fasit §8 rules directly innewsletter.mdhere; Step 13 then EXTRACTS them toreferences/longform-quality-rules.mdand 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 3–4 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 3–4 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:
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 5–6 (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-checkercalls in foreground) and Step 6 (persona sweep BEFORE lock: reader-jury viapersona-reviewerresonance-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 5–6 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:
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_containproves 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 passmust_containbut fail the Verify order-assert.
Step 11: S10 — newsletter.md Step 7–10 (annotate, lock/delivery, hook-gate, schedule)
- Files:
commands/newsletter.md(edit) - Changes: Add Step 7 (optional annotation:
render/build-html.mjs→ review HTML indocs/review/), Step 8 (LOCK → delivery: POST.html viarender/build-linkedin.mjs, cwd = serie-mappe), Step 9 (hook/conversion gate:persona-reviewerconversion-mode on the distribution text, AFTER lock — order assertion), Step 10 (register edition in the queue viaqueue-manager.mjsfor 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 7–10 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:
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_containproves 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 5skills/*/SKILL.mdcatalogs (edit — correction #7) - Changes: Remove the newsletter/blog adaptation path from
multiplatform.mdso there is exactly ONE entry to long-form (fasit §4.1). Add the langform trigger toskills/linkedin-content-creation/SKILL.md(fasit §5.3) AND sweep the catalog tables in all 6skills/*/SKILL.md(N2). Add a router row fornewsletterincommands/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 originalgrep -Eci ... && grep -c ...was&&-broken —grep -creturns 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:
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:
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 headlessclaude -pcannot 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:newsletterend-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 insidedocs/review/scope. Open the review HTML in a browser and walk the core flows (dogfood-UI gate). Write a structured friction log todocs/voyage-build/dogfood-S13-friction.mdrecording: 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 8–13 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:
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 everygit 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:
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), removecommands/templates.md,commands/linkedin.md(router edit) - Changes: Enumerate every one of the 8 template types in
templates.mdand confirm each is covered by a mode inquick.md(capability checklist — archetype F). Removetemplates.md. Grep the expanded target set (commands/ agents/ skills/ hooks/ README.md CLAUDE.md agents/README.md) fortemplatesroute-refs and fix all (N1). (codebase analysis) - Reuses: existing
quick.md3-line formula + templates bank. - Test first: (archetype F) capability checklist: all 8 types in quick;
templates.mdgone;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:
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), removecommands/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 readqueue.json; calendar already routes to publish). Removepublish.md. Critical (N1):publishhas 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 anyhooks/source changed. (codebase analysis) - Reuses:
queue-manager.mjs; existing calendar→publish routing. - Test first: (archetype F) capability checklist: calendar covers publish;
publish.mdgone; 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 --checkclean 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:
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), removecommands/collab.md, removecommands/speaking.md,commands/linkedin.md(router edit) - Changes: Create
outreach.mdcovering 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:511ToS 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:
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), removecommands/authority.md,commands/audit.md(edit — point to profile/strategy),commands/analyze.md(edit — point to profile) - Changes: Absorb
authority.mdinto a section ofstrategy.md(authority has no unique core). De-duplicate trajectory logic to live only instrategy.md;audit.mdreferences it. Makeprofile.mdthe canonical source for profile-alignment;audit.md/analyze.mdpoint there. Removeauthority.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.mdgone; 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:
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), removeagents/performance-reporter.md,agents/engagement-coach.md(edit → engagement), removeagents/comment-strategist.md,agents/README.md(edit),CLAUDE.md(edit) - Changes: Merge
performance-reporterintoanalytics-interpreter(one analytics agent, interpret/report modes — identical data sources). Mergecomment-strategistintoengagement-coach(5x5x5 + first-hour + CEA). Rewriteengagement-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.mdflow 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:
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, marketplaceREADME.md+CLAUDE.md(version refs) - Changes: Delegate
import.mdStep 6 analysis toreport.md(two report generators run the sametrendsCLI). Add router gating inlinkedin.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-v120anchor →#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.mdtrends 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:
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.mdfor long-formReuse 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 (3–5 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 formnode --test 'render/__tests__/*.test.mjs'(Node 25 breaksnode --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 intests/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). Mirrorsstate-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.mjsmissing weasyprint → skip-signal, not throw state-updater.test.mjsUnit render/__tests__/build-html.test.mjstables, #–####, inline codestate-updater.test.mjsUnit render/__tests__/build-linkedin.test.mjsreads edition-config; config diff → output diff csv-parser.test.ts(file fixture)Lint agents/__tests__/fact-checker-fixture.test.mjsfixture has 3 cases, one each 🟢/🔴/🟡 state-updater.test.mjsLint agents/__tests__/persona-reviewer-fixture.test.mjspersona def + 6 axes + both modes state-updater.test.mjsRisks and Mitigations
Priority Risk Location Impact Mitigation High Dead-link blast radius (N1) — publishhas 21 refs incl. 9 in hook scripts emitting runtime guidancesession-start.mjs:253,258,333,336,383,posting-reminder.mjs:94,95,user-prompt-context.mjs:46Ships text pointing at a removed command Step 17 grep target set incl. agents/README.md+CLAUDE.md; recompile hooks; treat every merge as MediumHigh Parallel Task fan-out (fasit assumption 4) has NO existing precedent to copy commands/newsletter.mdStep 2If 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:262PDF 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.txtLicense-compliance gap redistributing OFL fonts Author OFL-1.1 text in Step 1 Medium Skill catalogs duplicated across 6 dirs (N2) skills/*/SKILL.mdStale 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 BashProven 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 passtest "$(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 movedagents/README.mdinto the per-agent files, soagents/README.mdno longer exists; thegrep -v READMEfilter 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 includedagents/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 inREADME.md:294that 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 parallelclaude -p, API billing)./trekcontinueadvances 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;/clearbetween sessions. The 21 sessions below map 1:1 to the 21 steps. Waves are dependency groupings, not parallelism licenses. Continuity handoff is viaSTATE.md—NEXT-SESSION-PROMPT.local.mdis 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 0–2 (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 3–4 (draft + consistency/quality)
- Step: 9 · Wave: 2 · Depends on: Session 8 (newsletter.md, strict order)
Session 10: S9 — newsletter.md Step 5–6 (fact-check sweep + persona sweep BEFORE lock)
- Step: 10 · Wave: 2 · Depends on: Session 9
Session 11: S10 — newsletter.md Step 7–10 (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 2–3)
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 1–7): 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 touchcommands/newsletter.md(Wave 2), any consolidation target (Wave 4). - Wave 2 (Sessions 8–13): 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 14–15): 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 16–21): 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 touchcommands/newsletter.mdinternals (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 16–21) is independent of Waves 2–3 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 8–11 all touch
newsletter.md). - Render (Sessions 1–3) 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 S1–S20 (+S1a) mapped; all decisions A–H realized Specification quality 0.15 90 concrete paths + reuse refs; a few [OPERATØR]/[GATE]steps are intentionally non-mechanicalRisk & pre-mortem 0.15 92 R1–R5 + N1–N3 + 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 S1–S20 (+S1a) mapped 1:1; decisions A–H 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.md2.0.0checks2 Step 2 Manifest (string export/import.meta.url) doesn't prove the table/heading/inline-code generalization (a no-op passes)blocker Manifest must_containnow greps the production renderer for<table>,<h4,<code>— output markers a no-op cannot emit3 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 addsStep 3/Step 4/AI-slop4 Newsletter Steps 8–11 hide 2–4 phases behind single-string Manifests; order assertions only in grep major Each Manifest now requires all phase Step Nheadings present; order (sweep-before-lock, hook-after-lock) bound to Verify grep +[GATE]with explicit notes5 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 added6 Step 9 forward-references longform-quality-rules.mdcreated in Step 13major 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 14–15 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:newslettermajor Verify rewritten to assert no multi-step newsletter section survives ( Step.*newslettercount = 0); a one-line pointer of any wording is allowed10 build-linkedin constants cited "32–50"; actual 34–50 minor Citations corrected to 34–50 with per-constant line refs (CALENDAR:34, FRESHNESS:44, COVER_CREDIT:49, CAPTIONS:50) 11 Step 1 min_file_count: 6undercounts; fonts absent from Manifestminor Added render/fonts/Inter-400.ttf+render/fonts/Newsreader-400.ttfto expected_paths;min_file_count: 8; Verify asserts 8 .ttf; build-carousel weasyprint added to must_contain12 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 assertedminor Added to end-to-end Verification as an [OPERATØR]PDF-metadata check - Reuses: