# Dogfood S13 — friction log (`/linkedin:newsletter` end-to-end) **Step 14 (fasit S13) deliverable.** A real end-to-end dogfood of the long-form pipeline against a **throwaway fixture** (operator decision 2026-05-27: throwaway fixture in `docs/review/` scope, synthetic topic, no cross-repo write to maskinrommet). The fixture series lives at `docs/review/dogfood-serie/` (gitignored — throwaway, not committed). This log is the only committed artifact. This is a **design-friction** log, not a content-quality review. Per plan Step 14 "On failure: escalate — capture friction in the log, do not force a green check." The pipeline's deterministic backbone (render scripts, edition-state, queue) was **executed for real**; the gate layer (fact-check, persona sweep) is **BLOCKED** (see F1/F2) and its verdicts were **not fabricated**. --- ## Order-proof (the headline assertion this step must record) The persona sweep is wired to run **FØR lås / before lock**, exactly as fasit §0.4 mandates. Verified structurally (gate execution itself is blocked by F1/F2, so the proof is at the wiring level, not a live JA): 1. `config/edition-state.template.json` → `_doc.phases` orders the phases `consistency-quality → factcheck-sweep → persona-sweep-prelock → annotation → lock-delivery`. `persona-sweep-prelock` (Step 6) precedes `lock-delivery` (Step 8) in the canonical phase list. 2. `commands/newsletter.md` Step 6 and Step 8 each carry an explicit **"Order assertion (enforced)"** block stating the pipeline may not reach lock until the primær persona returns a clean JA from the pre-lock sweep. 3. The fixture `linkedin/edition-HANDOVER.md` physically records §5 persona-sweep (BEFORE lock) **above** §lås, and `edition-state.json` `currentPhase` never advances to `lock-delivery` because the persona JA precondition is unmet. So the before-lock ordering holds in the wiring. What the dogfood could **not** do is execute the gate — because of F1/F2 below. --- ## Friction points (numbered; ordered by severity) ### F1 — [BLOCKER] Agents invoked by bare name; harness requires namespace **What:** `commands/newsletter.md` issues every `Task` fan-out by **bare agent name** — `subagent_type: fact-checker` (Step 5), `persona-reviewer` (Steps 6, 9), and `content-repurposer` (Step 3). The Claude Code harness registers plugin agents **namespaced** as `linkedin-thought-leadership:`. A bare name does not resolve. **Evidence:** Live test this session — `Task(subagent_type: "content-repurposer")` returned `Agent type 'content-repurposer' not found`, with the available list showing only `linkedin-thought-leadership:content-repurposer`. Same failure for `fact-checker` and `persona-reviewer`. **Impact:** Steps 3, 5, 6, 9 — the entire gate/draft fan-out machinery — fail as written. This is the core of the long-form pipeline. **Implicates:** `commands/newsletter.md` (lines ~299, 398–399, 467–469, 638) → **Step 15 fix:** namespace all `subagent_type` references to `linkedin-thought-leadership:` (or confirm the intended invocation form and align the command to it). ### F2 — [BLOCKER / ENV] `fact-checker` + `persona-reviewer` not registered this session **What:** Even namespaced, neither `fact-checker` nor `persona-reviewer` appears in the harness's available-agents list, while every other LTL agent (incl. `content-repurposer`) does. The two agents were added in S4/S5 (commits `be03d44`, `1faffac`) after the running session's plugin agent registry was built. **Evidence:** Available list this session contains `linkedin-thought-leadership:content-repurposer` etc. but **not** `…:fact-checker` or `…:persona-reviewer`. Their frontmatter is well-formed and structurally identical to `content-repurposer` (verified `name`/`description`/`model`/`color`/ `tools`) — so this is **not** a frontmatter defect; it is a registry/reload gap. **Impact:** Compounds F1 — even after namespacing, Steps 5/6/9 cannot run until the session reloads the plugin agent set. **Implicates:** environment (Claude Code reload to register new agents) + **doc fix:** note in `CLAUDE.md`/`README.md` that adding a plugin agent requires a session reload before it is invokable. Not a code defect in the agent files. ### F3 — [MAJOR] No template for 3 of the 4 series-folder input artifacts **What:** The pipeline depends on four artifacts in the series folder: `edition-state.json`, `edition-config.json`, `edition-delingstekst.md`, and the `edition-HANDOVER.md`. Only `edition-state.json` ships a template (`config/edition-state.template.json`). The formats of `edition-config.json` and `edition-delingstekst.md` are discoverable **only** by reading the render scripts. **Evidence:** To run the dogfood I had to reverse-engineer both formats from `render/build-linkedin.mjs` (`loadEditionConfig` shape; `parseDelingstekst` section grammar `## Del N —` / `## Samle`, `**Første kommentar:**`, `#hashtag` line). A new operator has no shipped reference. **Impact:** Step 8 says "confirm the delivery inputs exist" with **no generation path** — a fresh edition cannot produce these from anything the plugin ships. **Implicates:** `config/` (add `edition-config.template.json` + `edition-delingstekst.template.md` + an `edition-HANDOVER.template.md`) and `commands/newsletter.md` Step 8 (point at the templates) → **Step 15 fix.** ### F4 — [MAJOR] Draft filename/location is inconsistent across steps **What:** Step 3 says write the draft to `/linkedin/
.draft.md`. Steps 7 and 8 expect `NN-utkast.md` (two-digit NN prefix) in the **series root**. `build-linkedin.mjs` **silently skips** any file without an `NN` prefix (`↷ hopper over … (ikke NN-prefiks)`). **Evidence:** `render/build-linkedin.mjs` line 357 regex `^(\d{2})`; the command text uses two different paths/names for the same draft. **Impact:** Following Step 3 literally produces a file (`*.draft.md`, inside `linkedin/`) that Step 8 then silently skips — a green exit with no POST.html. **Implicates:** `commands/newsletter.md` (reconcile Step 3 vs Steps 7/8 on draft filename + location) → **Step 15 fix.** ### F5 — [MAJOR] Series root hardcoded to maskinrommet **What:** Step 0 resolves the series folder under `/Users/ktg/repos/maskinrommet/serier//` with no parameter to point elsewhere. Dogfooding (or any other repo / a throwaway fixture) requires a manual mental override of the documented path. **Evidence:** Step 0.1 and the architecture note both hardcode the maskinrommet path; the dogfood had to invent `docs/review/dogfood-serie/` off-spec. **Impact:** The command is coupled to one specific external repo. Operator must deviate from the written procedure for any other location. **Implicates:** `commands/newsletter.md` Step 0 (accept/derive a series-root arg; keep maskinrommet as default, not the only path) → **Step 15 fix (or deferred with operator note if maskinrommet-only is intentional).** ### F6 — [MINOR] `build-linkedin.mjs` carries Seres-specific hardcoding **What:** `CAROUSEL = new Set(["03","06"])` and the **unconditional** samle-post build are Seres-series assumptions baked into a script that S2 "generalized." **Evidence:** Dogfood produced `linkedin/samle/POST.html` for a single-edition fixture that has no series to summarize; the carousel set is dead for any series whose carousel editions aren't 03/06. **Impact:** Low for correctness (samle is harmless extra output), but it is generalization debt — the script still assumes the Seres shape. **Implicates:** `render/build-linkedin.mjs` (lines 63, 364–370) → likely **defer** to a later generalization pass; note for operator. ### F7 — [MINOR] `build-html.mjs` returns exit 0 on a missing input file **What:** A missing input arg prints `Fant ikke: ` to stderr and `continue`s; the loop completes with **exit 0** and no HTML produced. **Evidence:** `render/build-html.mjs` lines 1045–1048 (`continue`, no `process.exit`, no error accumulation). **Impact:** Step 7 already flags this (N3), but the script's silent exit-0 is a footgun: a typo'd filename looks like success. The command must check that the expected output file exists, not just the exit code. **Implicates:** `render/build-html.mjs` (exit non-zero if zero files written) +/or `commands/newsletter.md` Step 7 (verify output file, not just exit) → **Step 15 fix candidate.** ### F8 — [MINOR] Fatal-vs-graceful asymmetry on missing delivery inputs (Step 8) **What:** Step 8 says "if either file is absent the script throws on read." In fact `edition-config.json` is **graceful** (`loadEditionConfig` falls back to empty defaults), while `edition-delingstekst.md` is **fatal** — `parseDelingstekst()` runs unconditionally at the top of `main()` and ENOENT-throws **before any POST.html is written**, including the article POST that does not depend on it. **Evidence:** `render/build-linkedin.mjs` lines 45–59 (graceful config) vs 180 (`fs.readFileSync(DELINGSTEKST_FILE)` with no try) called at line 349. **Impact:** The command's description of the failure mode is inaccurate, and a missing delingstekst kills the whole build rather than degrading. **Implicates:** `render/build-linkedin.mjs` (guard `parseDelingstekst` like config) + `commands/newsletter.md` Step 8 (correct the "either file throws" wording) → **Step 15 fix candidate.** ### F9 — [MINOR] `agents/README.md` registered as an agent **What:** The harness available list includes `linkedin-thought-leadership:README` — a non-agent README in `agents/` is being picked up as an invokable agent. **Evidence:** Available-agents list this session contains `…:README`. **Impact:** Cosmetic/registry noise; not harmful but pollutes the agent namespace. **Implicates:** `agents/README.md` (relocate, or ensure it lacks agent frontmatter) → likely **defer** / doc-pass. --- ## What ran clean (no friction) - **Step 7 annotation** — `build-html.mjs 01-utkast.md` (cwd = serie-mappe) → wrote `review/01-utkast.html` (30.4 KB), exit 0. ✅ - **Step 8 render** — `build-linkedin.mjs 01-utkast.md` → `linkedin/01/POST.html` + `linkedin/samle/POST.html`, exit 0; body survived (headings/lists/strong present in POST.html). ✅ (Render works; the *lock gate* is blocked, not the renderer.) - **cwd contract** — running both scripts from the series folder resolved `linkedin/` and `review/` correctly. The command's "cd to the series folder" instruction is right; the footgun is only if you forget (then output lands under the plugin). - **edition-state schema** — the template's phase list and article-status values were sufficient to represent the walk; resumption table in Step 0 is coherent. ## Friction summary for Step 15 (revert/fix targets) | # | Severity | Implicated file | Step 15 disposition | |---|----------|-----------------|---------------------| | F1 | BLOCKER | `commands/newsletter.md` | namespace agent calls | | F2 | BLOCKER/env | env + `CLAUDE.md`/`README.md` | reload + document | | F3 | MAJOR | `config/` + `commands/newsletter.md` | add 3 templates | | F4 | MAJOR | `commands/newsletter.md` | reconcile draft path/name | | F5 | MAJOR | `commands/newsletter.md` | de-hardcode series root | | F6 | MINOR | `render/build-linkedin.mjs` | defer (generalization debt) | | F7 | MINOR | `render/build-html.mjs` (+ Step 7) | exit non-zero on no output | | F8 | MINOR | `render/build-linkedin.mjs` (+ Step 8) | guard delingstekst + fix wording | | F9 | MINOR | `agents/README.md` | relocate / defer | **Headline:** the long-form pipeline's deterministic backbone is sound, but its **gate layer is currently un-runnable** (F1 + F2). Step 15 must close F1 (a concrete one-line-per-call edit) and F2 (reload + a doc note) before the pre-lock persona sweep can actually execute — the order is correctly wired, it just cannot fire yet.