fix(linkedin): close dogfood friction (S14)

Close all 9 friction points from the S13 newsletter dogfood (operator
elected to fix F6-F9 rather than defer):

- F1: namespace all subagent_type calls in newsletter.md to
  linkedin-thought-leadership:<name> (4 sites + canonical note)
- F2: document agent invocation form + reload requirement in CLAUDE.md
  + README.md (reload itself is an operator action)
- F3: add edition-config / edition-delingstekst / edition-HANDOVER
  templates under config/ + wire into Steps 0 and 8 + footer
- F4: reconcile draft path to <serie>/NN-utkast.md (series root)
- F5: de-hardcode series root (explicit arg / LTL_SERIES_ROOT / default)
- F6: config-derive carousel editions (remove Seres CAROUSEL set);
  correct samle comment
- F7: build-html.mjs exits non-zero when zero HTML produced
- F8: guard parseDelingstekst (graceful ENOENT) + correct Step 8 wording
- F9: relocate agents/README.md -> docs/agents-capability-matrix.md

Re-tested: 87/87 plugin tests pass; build-html/build-linkedin behavior
re-verified live. Per-item outcomes logged in dogfood-S13-friction.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-27 23:37:39 +02:00
commit 92e0a0b4f5
11 changed files with 339 additions and 54 deletions

View file

@ -0,0 +1,56 @@
<!--
TEMPLATE — edition-HANDOVER.md (narrative production state for one edition)
Purpose : the human-readable narrative companion to edition-state.json. Where
edition-state.json is the machine-readable resumption state (currentPhase,
per-article status), this HANDOVER carries the *narrative* state a human
(or the next session) reads to understand where the edition is, what is
immutable, and what happens next. /linkedin:newsletter Step 0 reads it;
every phase appends to §6.
Decision: G — production lives in the series root, NOT the plugin. Copy this to
<serie>/linkedin/edition-HANDOVER.md (or <serie>/HANDOVER.md) and fill in.
DISTINCT from the plugin's own docs/BUILD-HANDOVER.local.md (which governs building
the plugin). Never merge the two.
Section numbering is referenced by name in commands/newsletter.md:
§1 where we are · §3§5 brief/rules/calibration · §4 immutable rules + fact-check
log · §5 persona calibration + conversion verdict · §6 next-session pointer.
-->
# Edition HANDOVER — <series title>, article <NN> "<edition title>"
## §1 — Where we are
- Current phase: <currentPhase, mirrors edition-state.json>
- Article status: <pending | in-progress | locked | scheduled>
- One-line state: <e.g. "draft complete, consistency pass next" or "locked, awaiting hook gate">
## §2 — Premise & angle
- Premise (one clear claim): <…>
- Angle / dramaturgical spine: <…>
- Leader-takeaway (the one thing a busy reader keeps): <…>
## §3 — Brief & scope
- Audience personas (active set; mark primær): <persona A (PRIMÆR), persona B, …>
- Key points (24): <…>
- Tone / voice anchor: <reference to assets/voice-samples + any edition-specific note>
- Out of scope for this edition: <…>
## §4 — Immutable rules + fact-check log
> Rules locked for this edition (do not relitigate mid-pipeline):
- <immutable rule 1 — e.g. "no vendor names in the hook">
- <immutable rule 2>
> Fact-check log (Step 5 — guilty-until-disproven; 🔴 must be empty before lock):
| Claim | Risk | Source / verification | Status |
|-------|------|-----------------------|--------|
| <claim> | 🔴/🟡/🟢 | <primary source> | open / resolved |
## §5 — Persona calibration + verdicts
- Pre-lock resonance sweep (Step 6): primær <name> → <JA / NEI + one-line reason>
- Method/persona calibration notes: <any axis tuning, secondary-NO signals>
- Post-lock conversion sweep (Step 9): primær <name> mode konverter → <JA / NEI>
## §6 — Next session
- Next step: Step <N> — <name> (per the Step 0 resumption table)
- Precise pointer: <e.g. "draft resumes at section 3" or "render POST.html, then hook gate">
- Open questions for the operator: <…, or "none">

View file

@ -0,0 +1,28 @@
{
"_doc": {
"purpose": "Schema + starter for edition-config.json — the STATIC per-edition delivery metadata that render/build-linkedin.mjs reads (calendar, freshness, cover credit, captions). Complements edition-state.json (machine resumption state) and edition-delingstekst.md (distribution copy).",
"decision": "G — production lives in the series root, NOT in the plugin. Copy this template to <serie>/linkedin/edition-config.json and fill it in. This file is the schema-defining TEMPLATE only.",
"location": "<serie>/linkedin/edition-config.json (read relative to cwd = series root; OUT_ROOT = <cwd>/linkedin)",
"graceful": "render/build-linkedin.mjs loadEditionConfig() falls back to empty defaults if this file is missing or malformed — every field below is optional. Provide it for a complete delivery page (calendar slot, freshness banner, cover credit, alt-text caption).",
"keys": "Article keys are zero-padded strings mirroring edition-state.json + the NN-prefix of each NN-utkast.md draft: \"01\", \"02\", ..., plus \"samle\" for the collected post.",
"fields": {
"calendar[NN]": "{ dag: human date label e.g. \"Mandag 02.06\", klokke: \"HH:MM\" } — the scheduled slot shown on POST.html. Default if absent: { dag: \"—\", klokke: \"08:00\" }.",
"freshness[NN]": "string — a freshness/recency note rendered in the amber banner (e.g. \"Tall fra Q1 2026; sjekk før publisering etter 01.07\"). Omit for no banner.",
"coverCredit": "string — global cover-image credit line (\"Add credit and caption\" field). One value for the whole edition.",
"captions[NN]": "string — per-article cover-image caption / alt text. Default if absent: \"—\".",
"carousel": "list of zero-padded NN strings (e.g. [\"03\",\"06\"]) — the editions that ship an optional carousel/document post. POST.html shows a carousel block only for these NN. Empty/absent → no carousel block. (S14/F6: replaces the old hardcoded Seres set.)"
}
},
"calendar": {
"01": { "dag": "<Ukedag DD.MM>", "klokke": "08:00" },
"samle": { "dag": "<Ukedag DD.MM>", "klokke": "08:00" }
},
"freshness": {
"01": "<optional freshness note shown in the banner — omit the key for no banner>"
},
"coverCredit": "<cover-image credit line, or empty string>",
"captions": {
"01": "<cover-image caption / alt text for article 01>"
},
"carousel": []
}

View file

@ -0,0 +1,55 @@
<!--
TEMPLATE — edition-delingstekst.md (distribution copy for a newsletter edition)
Purpose : the per-edition LinkedIn distribution text that render/build-linkedin.mjs
folds into each POST.html "all-in-one-place" deliverable. This is the
feed copy the reader sees BEFORE "…see more" — the hook that earns the
click (gated in /linkedin:newsletter Step 9).
Decision: G — production lives in the series root, NOT the plugin. Copy this to
<serie>/linkedin/edition-delingstekst.md and fill it in.
Location: <serie>/linkedin/edition-delingstekst.md (cwd = series root).
Graceful: render/build-linkedin.mjs degrades if this file is missing (no
distribution copy is folded in; the article POST.html still builds).
Provide it for a complete delivery.
GRAMMAR (exactly what parseDelingstekst() recognizes — do not improvise):
- A section starts with a heading: "## Del N — <title>" (N = article number,
mapped to zero-padded key "0N") OR "## Samle <…>" (the collected post,
key "samle").
- "## SYSTEM …" headings are ignored.
- Inside a section, until the next "## " heading or a "---" line:
* "**Første kommentar:** <text>" → first-comment text (one line).
* a line beginning with "#" + non-space (e.g. "#KI #offentligsektor")
→ the hashtag line.
* a "> …" blockquote line → ignored (use it for NB/notes to yourself).
* every other line → part of the share text (the hook + body shown in feed).
Keys MUST match the NN-prefix of the draft (NN-utkast.md) and edition-config.json.
-->
## Del 1 — <edition title>
<First line = the krok/hook: the single line that must stop the scroll. Keep the
strongest claim or tension here; this is what shows before "…see more".>
<Then 24 short lines that pay off the hook and point at the article. Tighten,
never pad — this is feed copy, not the article.>
**Første kommentar:** <the first-comment text — e.g. a link, a question to seed
discussion, or the "full edition here" pointer. LinkedIn suppresses links in the
body, so the link belongs here.>
#hashtag1 #hashtag2 #hashtag3
> NB to self (ignored by the renderer): note any freshness caveat or A/B variant
> you want to remember for this edition.
---
## Samle <collected-post title, if shipping a roundup of the series>
<Hook for the collected/summary post. Same grammar. Omit this whole section if the
edition has no samle post.>
**Første kommentar:** <first comment for the samle post>
#hashtag1 #hashtag2