feat(linkedin-studio): parameterize series path + de-brand render output [skip-docs]

Wave 3 / Step 8 of the remediation plan (Phase 1 — usable by a non-author).

The flagship long-form engine shipped bespoke-as-general: a maintainer-private
absolute series path and a hardcoded 'Maskinrommet' brand in the renderers. Both
are now generalized so a non-author can run the pipeline without inheriting the
author's filesystem or publication identity.

- commands/newsletter.md + config/edition-state.template.json: series-root default
  /Users/ktg/repos/maskinrommet/serier -> $HOME/linkedin-series; reconciled the
  prose that called the maskinrommet folder 'the default' so it no longer
  contradicts the new neutral default. LTL_SERIES_ROOT override contract preserved.
- render/build-linkedin.mjs + render/build-carousel.mjs: brand is now an LTL_BRAND
  env-var (empty default -> generic). Samle-post title, carousel footer brand-span,
  and cover-eyebrow fallback are de-branded; empty brand renders clean chrome. The
  operator sets LTL_BRAND=Maskinrommet in their own env to re-brand (same pattern
  as LTL_SERIES_ROOT).
- config/image-credit-caption.template.md: 'Maskinrommet-/serie-badge' -> generic
  'serie-badge'.

Out of scope (Step 9): the residual 'Maskinrommet skrivekontrakt §C2/§A'
references in newsletter.md are the writing-contract generalization, handled next.

[skip-docs]: three-doc + version reconciliation is Step 21 (pre-review-gate, per
plan: 'LAST so it captures everything'). These intermediate Wave commits are NOT
pushed before the /trekreview gate, so the three-doc obligation (which governs
pushed changes) is satisfied at Step 21, not per local checkpoint commit.

Verify: grep -rIn '/Users/ktg' config/ commands/ render/ (excl .local) -> no
matches; grep -rn 'Maskinrommet' render/ -> no matches (de-branded); node --check
on both render scripts -> OK; LTL_SERIES_ROOT still present in newsletter.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-30 00:46:05 +02:00
commit 305b99c0e4
5 changed files with 24 additions and 13 deletions

View file

@ -31,9 +31,9 @@ This command is **fundamentally different** from the short-form commands:
phases* (fact-check sweep + persona sweep + hook-gate), NOT by the short-form
`PreToolUse` content-gatekeeper/voice-guardian hooks (those stay short-form-only).
- **State lives in the series folder, not the plugin.** Production state for an
edition lives in the resolved **series root** (Step 0) — by default the
maskinrommet series folder
(`${LTL_SERIES_ROOT:-/Users/ktg/repos/maskinrommet/serier}/<slug>/`), per
edition lives in the resolved **series root** (Step 0) — by default a per-slug
folder under your series base
(`${LTL_SERIES_ROOT:-$HOME/linkedin-series}/<slug>/`), per
decision G, but any explicit path works (Step 0 resolution order). The plugin
ships the *schema* (`config/edition-state.template.json`) and this command;
the edition's actual state + drafts live with the series.
@ -135,13 +135,14 @@ the edition left off before doing anything.
<path-to-serie>`), use it verbatim. This is how the edition is produced for
any repo, a throwaway fixture, or a non-default location.
- Otherwise derive it from the series slug under the **default series base**,
`${LTL_SERIES_ROOT:-/Users/ktg/repos/maskinrommet/serier}/<slug>/`. The
`LTL_SERIES_ROOT` env-var overrides the base without editing this command;
the maskinrommet path is the default, not the only path.
`${LTL_SERIES_ROOT:-$HOME/linkedin-series}/<slug>/`. The
`LTL_SERIES_ROOT` env-var overrides the base without editing this command
(and `LTL_BRAND` re-brands the rendered output — empty by default); the
default base is a default, not the only path.
- If neither a path nor a resolvable slug is available, ask once which series
(or series-root path) this edition belongs to.
All later steps treat `<serie>` as this resolved series root; nothing below
re-hardcodes the maskinrommet path.
re-hardcodes a specific series path.
2. **Read edition-state** (`<serie>/linkedin/edition-state.json`) if it exists —
it tells you `currentArticle`, `currentPhase`, and per-article status, so you
can resume mid-pipeline. The schema is documented in