# 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:<name>`. 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:<name>` (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 `<serie>/linkedin/<article>.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/<slug>/` 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: <path>` 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:<name>`, 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 `<serie>/NN-utkast.md` in the
  series root (the exact file Steps 7/8 render), with an explicit "do NOT write
  `linkedin/<article>.draft.md`" warning. **Check:** no `.draft.md`/`<article>` 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}/<slug>/` → 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.
