Dogfood mot throwaway-fixture (docs/review/dogfood-serie/, gitignored), syntetisk tema. Deterministisk ryggrad (build-html/build-linkedin) kjørt ekte og passerte; gate-laget (fact-check/persona) BLOKKERT — escalert, ikke tvunget grønt. 9 friksjonspunkter (2 BLOCKER, 3 MAJOR, 4 MINOR) med order-proof (persona-sweep wiret FØR lås) og per-punkt implicated file for S14 revert-mål. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
12 KiB
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):
config/edition-state.template.json→_doc.phasesorders the phasesconsistency-quality → factcheck-sweep → persona-sweep-prelock → annotation → lock-delivery.persona-sweep-prelock(Step 6) precedeslock-delivery(Step 8) in the canonical phase list.commands/newsletter.mdStep 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.- The fixture
linkedin/edition-HANDOVER.mdphysically records §5 persona-sweep (BEFORE lock) above §lås, andedition-state.jsoncurrentPhasenever advances tolock-deliverybecause 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 continues;
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.mdStep 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) → wrotereview/01-utkast.html(30.4 KB), exit 0. ✅ - Step 8 render —
build-linkedin.mjs 01-utkast.md→linkedin/01/POST.htmllinkedin/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/andreview/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.