---
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 (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):

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 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

```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 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):

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 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). **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:34–50) 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 (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.md` step structure + `${CLAUDE_PLUGIN_ROOT}` Bash form; `commands/react.md` multi-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:**
  ```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 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-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 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:**
  ```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 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-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 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:**
  ```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 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 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 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:**
  ```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 && grep -c '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.)
- **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 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:**
  ```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 -rIn '1\\.2\\.0' .claude-plugin/plugin.json CLAUDE.md README.md CHANGELOG.md` → expected: command count is **exactly 24**; zero `1.2.0` refs remain (all bumped to 2.0.0)
- **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 (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 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` → 16 (content-tracker, personalization-scorer, performance-reporter, comment-strategist removed; fact-checker, persona-reviewer added)
- [ ] `grep -rn '1\.2\.0' .claude-plugin/plugin.json CLAUDE.md README.md CHANGELOG.md` → zero matches (all bumped to 2.0.0)
- [ ] `grep -rn '/linkedin:templates\|/linkedin:publish\|/linkedin:authority\|/linkedin:collab\|/linkedin:speaking' commands/ agents/ skills/ hooks/ README.md CLAUDE.md agents/README.md` → zero stray route-refs to removed commands
- [ ] `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:** 7 (templates, publish, authority, collab, speaking commands; content-tracker, personalization-scorer, performance-reporter, comment-strategist agents)
- **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 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 touch `commands/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 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 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-mechanical |
| Risk & 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.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 8–11 hide 2–4 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 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: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 "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: 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 |
