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

261 lines
16 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 | 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.