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>
16 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 | 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
Taskcall sites incommands/newsletter.md(content-repurposer Step 3, fact-checker Step 5, persona-reviewer Steps 6 + 9) now use the namespacedsubagent_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) andREADME.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-reviewerwere 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 intonewsletter.mdStep 0 (HANDOVER), Step 8 (config + delingstekst), and the Reference Files footer. Check: all three exist underconfig/;edition-config.template.jsonparses as valid JSON. - F4 — ✅ closed. Step 3 now writes the canonical
<serie>/NN-utkast.mdin the series root (the exact file Steps 7/8 render), with an explicit "do NOT writelinkedin/<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) vialoadEditionConfigEMPTY_CONFIG+ the new config template. Misleading "samle bygges alltid" comment corrected (build has always been gated onshareMap.samle). Check:grep -n CAROUSEL render/build-linkedin.mjs→ none; 20/20 render tests pass (one assertion updated to include thecarousel: []default).
- F7 — ✅ closed.
build-html.mjs main()now counts files written, printsIngen 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.htmlwritten. - F8 — ✅ closed.
parseDelingstekst()wrapped in try/catch returning{}on ENOENT, matchingloadEditionConfig's fail-soft contract; Step 8 wording corrected ("both inputs optional and graceful," not "either file throws"). Re-tested live: no delingstekst →build-linkedin.mjsexit 0 +linkedin/01/POST.htmlstill 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 aslinkedin-thought-leadership:README). The stale registration clears on the next Claude Code reload (env, same as F2). Check: no README inagents/;docs/agents-capability-matrix.mdpresent.
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.