diff --git a/plugins/linkedin-thought-leadership/docs/voyage-build/dogfood-S13-friction.md b/plugins/linkedin-thought-leadership/docs/voyage-build/dogfood-S13-friction.md new file mode 100644 index 0000000..e4ddaa5 --- /dev/null +++ b/plugins/linkedin-thought-leadership/docs/voyage-build/dogfood-S13-friction.md @@ -0,0 +1,195 @@ +# 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.