ktg-plugin-marketplace/plugins/linkedin-studio/docs/voyage-build/dogfood-S13-friction.md
Kjell Tore Guttormsen b6bb61246b refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)
BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.

- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
  config-audit v5.0.0 snapshots) intentionally retain the old slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:32:02 +02:00

16 KiB
Raw Permalink Blame History

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 namesubagent_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 continues; 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 fatalparseDelingstekst() 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 annotationbuild-html.mjs 01-utkast.md (cwd = serie-mappe) → wrote review/01-utkast.html (30.4 KB), exit 0.
  • Step 8 renderbuild-linkedin.mjs 01-utkast.mdlinkedin/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 F6F9 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 1618: drop the dangling agents/README.md arg from their Verify greps, or repoint to the new path.