ktg-plugin-marketplace/plugins/linkedin-thought-leadership/docs/voyage-build/dogfood-S13-friction.md
Kjell Tore Guttormsen adfa2085fc test(linkedin): dogfood newsletter pipeline end-to-end (S13)
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>
2026-05-27 22:57:13 +02:00

195 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, 398399, 467469, 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, 364370) → 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 10451048 (`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 4559 (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.