# 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 | Status | |---|----------|-----------------|---------------------|--------| | F1 | BLOCKER | `commands/newsletter.md` | namespace agent calls | ✅ | | F2 | BLOCKER/env | env + `CLAUDE.md`/`README.md` | reload + document | ✅ (doc) / 🔶 (reload = operator) | | 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` | config-derive carousel; fix samle comment | ✅ | | 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 out of `agents/` | ✅ (relocated) / 🔶 (de-register on reload) | **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. --- ## Step 15 (S14) — re-test outcomes All nine friction points were closed (operator elected to fix F6–F9 rather than defer). Each was re-tested with a concrete check, not a self-asserted "fixed." - **F1 — ✅ closed.** All four `Task` call sites in `commands/newsletter.md` (content-repurposer Step 3, fact-checker Step 5, persona-reviewer Steps 6 + 9) now use the namespaced `subagent_type: linkedin-thought-leadership:`, plus a canonical "Agent invocation form (required)" note near the foreground principle. **Check:** `grep -nE 'subagent_type: (fact-checker|persona-reviewer|content-repurposer)' commands/newsletter.md` → zero bare names; `grep -nE 'linkedin-thought-leadership:(fact-checker|persona-reviewer|content-repurposer)'` → all 4 sites namespaced. - **F2 — ✅ doc / 🔶 reload.** Documented in `CLAUDE.md` (Agents section: invocation form + reload requirement) and `README.md` (Agent Architecture note). The environmental half — registering a newly-added agent — inherently requires a Claude Code **session reload**; that is an operator action, not a code change. Confirmed F2 persists across `/clear`: `fact-checker`/`persona-reviewer` were still absent from this fresh session's agent registry (only the 15 older agents + README appeared), proving it is a reload gap, not a per-session fluke. - **F3 — ✅ closed.** Added `config/edition-config.template.json`, `config/edition-delingstekst.template.md`, `config/edition-HANDOVER.template.md` (formats reverse-engineered from the render scripts, now shipped as reference). Wired into `newsletter.md` Step 0 (HANDOVER), Step 8 (config + delingstekst), and the Reference Files footer. **Check:** all three exist under `config/`; `edition-config.template.json` parses as valid JSON. - **F4 — ✅ closed.** Step 3 now writes the canonical `/NN-utkast.md` in the series root (the exact file Steps 7/8 render), with an explicit "do NOT write `linkedin/
.draft.md`" warning. **Check:** no `.draft.md`/`
` path refs remain except the intentional anti-pattern warning. - **F5 — ✅ closed.** Step 0 resolves a **series root** via an order (explicit path arg → `${LTL_SERIES_ROOT:-…/maskinrommet/serier}//` → ask once); maskinrommet is the default, not the only path. The architecture preamble was aligned to match. - **F6 — ✅ closed.** `CAROUSEL = new Set(["03","06"])` removed; carousel editions are now config-derived (`config.carousel`, a list of NN strings) via `loadEditionConfig` + `EMPTY_CONFIG` + the new config template. Misleading "samle bygges alltid" comment corrected (build has always been gated on `shareMap.samle`). **Check:** `grep -n CAROUSEL render/build-linkedin.mjs` → none; 20/20 render tests pass (one assertion updated to include the `carousel: []` default). - **F7 — ✅ closed.** `build-html.mjs main()` now counts files written, prints `Ingen HTML produsert …` and the CLI guard exits non-zero when zero files are produced (no more silent exit-0 on a typo'd filename). Step 7 wording updated to rely on the exit code AND verify the output file. **Re-tested live:** missing input → exit 1; valid input → exit 0 + `review/NN-utkast.html` written. - **F8 — ✅ closed.** `parseDelingstekst()` wrapped in try/catch returning `{}` on ENOENT, matching `loadEditionConfig`'s fail-soft contract; Step 8 wording corrected ("both inputs optional and graceful," not "either file throws"). **Re-tested live:** no delingstekst → `build-linkedin.mjs` exit 0 + `linkedin/01/POST.html` still built. - **F9 — ✅ relocated / 🔶 de-register on reload.** `git mv agents/README.md docs/agents-capability-matrix.md` — the only reliable fix, since the file registers by filename (it had no frontmatter yet still appeared as `linkedin-thought-leadership:README`). The stale registration clears on the next Claude Code reload (env, same as F2). **Check:** no README in `agents/`; `docs/agents-capability-matrix.md` present. ### Finding (out of Step 15 scope — recorded, not actioned) `plan.md` Steps 16, 17, 18 hard-code `agents/README.md` as an explicit `grep` search path (plan.md:635, 727, 849). After F9's relocation that path no longer exists. The explicit arg is **redundant** with the recursive `agents/` search those greps already include, so dropping it (or repointing to `docs/agents-capability-matrix.md`) is safe — but `plan.md` is outside Step 15's Files list (Hard Rule 2), so this is logged for the operator/those steps rather than edited here. **Action for Steps 16–18:** drop the dangling `agents/README.md` arg from their Verify greps, or repoint to the new path.