10 KiB
M0 — Implementation Log
Running record of decisions, deviations, and out-of-scope follow-ups discovered
during M0 execution. Plan: docs/m0/plan.md (18 steps). History → git; this file
captures only what the commit messages cannot.
Session 4 — Steps 14–18 (2026-06-18)
Step 14 GATE outcome — the D3 convention works; edit count is ~1:1, not reduced
Prototyped references/data-path-convention.md on the voice-readers family:
38 refs across 19 files repointed — exactly the plan's prediction. The measured
answer to brief D3's open question (can a convention reduce edits, or do commands
need literal paths for Claude to act on?): command/agent prose that tells Claude to
read a file needs a resolvable path on the line. A "the data dir's
voice-samples/ (see convention doc)" reference adds a lookup hop and is not directly
actionable. The inline ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/…
token is both self-resolving and points at the doc. So the convention does not
cut edit count below ~1 per ref — it makes every edit a uniform mechanical token
swap (vs bespoke per-line decisions), with the doc as single source of truth.
GATE = proceed (convention confirmed working); Step 15 applies the same uniform
token to the remaining families. This is D3 realized as one token — NOT a re-decision
to literal-edit (alt. a). Applied via an ordered swap (prefixed ${CLAUDE_PLUGIN_ROOT}/ assets/voice-samples/ form before bare assets/voice-samples/, so the bare pass can't
corrupt the prefixed one).
Designed inter-step red lint (Step 14 → Step 16)
After Step 14 the structure lint is Failed: 1 — references/*.md: 26 (expected 25),
the new convention doc as the 26th ref file. EXPECT_REFS bumps to 26 in Step 16
(plan Session-4 scope forbids touching tests). This session lands 14→18 in one go, so
the lint is restored to green at Step 16 — no red is left at session end. The only
surviving bare assets/voice-samples/ is inside the convention doc itself (it documents
the in-plugin placeholder-scaffold location for the fallback rule) — Step 16's
no-bare-path assertion must exempt references/data-path-convention.md.
Step 15 — 130 refs / 34 files repointed; shipped read-only preserved
Rule-based repoint (negative-lookahead per subdir): analytics 50, drafts 24,
audience-insights 14, profile/D1 12, examples 11, plans 6, my-post-templates 4,
frameworks 3, case-studies 3, network 2, repurposing-tracker 1. Counts match the
plan's family estimates once the shipped exclusions are accounted for
(frameworks 6→3 drops framework-template.md; case-studies 5→3 drops
case-study-template.md; analytics drops README.md; the Step-11 ANALYTICS_ROOT
pins were already gone). Style-A ${CLAUDE_PLUGIN_ROOT} preserved for shipped
read-only: analytics/README.md, all *-template.md seeds, assets/checklists/,
assets/quick-post-resources.md, the shipped assets/templates/* (only the
my-post-templates.md instance repointed), config/*.template.*, and every
scripts/analytics / hooks/scripts CODE path. profile/D1: config/user-profile.local.md
→ ${…}/profile/user-profile.md (path + filename change, drops .local, per
MOVE_FILES). ab-tests/ routes under ${…}/analytics/ab-tests/ (brief §7.1);
plans/ at ${…}/plans/ (top-level). Verify: the only in-plugin data-dir paths
left are the three shipped exclusions; full lint Failed:1 = EXPECT_REFS only.
Step 15 scope notes — two additions, one deferral
- network/ + repurposing-tracker.md repointed (additions beyond the plan's named
families). Both are code-invisible data classes the agents write
(
network-builder.md,content-repurposer.mdsave trackers into the plugin tree). They fit Step 15's stated goal ("route the code-invisible data classes so they don't silently orphan when the default flips"); the plan named ab-tests/plans as examples, not an exhaustive list. As write-targets they are self-sufficient — the agent creates the file external on first write, no migration entry needed. config/personas.local.mddeferred — OUT of M0 scope. Unlike the write-targets, this is a read fallback in a resolution chain (edition-state → series file → pluginpersonas.local.md→ template). Repointing the read external without a migration dest would break the read, and adding it toMOVE_FILESis a.mjschange Step 15's scope fence forbids. Personas are newsletter/series production data (already external via$LTL_SERIES_ROOT); the plugin-levelpersonas.local.mdis a deliberate in-plugin fallback library. Left as-is; Step 16's no-bare-path lint targets the migrated data classes only, so it does not flag personas. Track as a follow-up if a full personas externalization is wanted later.
Step 16 — lint extended 74→81 checks; the no-pin guard caught 5 real survivors
Added to test-runner.sh: EXPECT_REFS 25→26 + "delta = exactly +1 (data-path-convention.md)"
assert (m3/m11); Section 13 (no command/agent/skill/hook prose references a migrated
user-data path in-plugin — bare or ${CLAUDE_PLUGIN_ROOT}-pinned — R1; + the no-ANALYTICS_ROOT-
pin sibling M3; + the SC2 git status --porcelain --ignored dry-run), each with a
non-vacuity self-test mirroring Section 8; Section 14 assertion-count anti-erosion
floor (pre-M0 baseline 74, count may only grow — SC6 / assumption 3).
The no-pin guard earned its keep on first run: it flagged 5 surviving in-plugin
ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics" pins that Step 11 missed —
agents/analytics-interpreter.md (×2) + skills/linkedin-analytics/SKILL.md (×3). Step 11
scoped the pin-drop to the commands (import.md/report.md); the agent + skill invoke the
CLI directly and kept the pin, which would force the analytics CLI to read in-plugin even
after the default flipped external (the exact M3 failure). Dropped all 5 (surgical: removed
only the ANALYTICS_ROOT=… prefix, kept the node --import tsx form — no CLI-style change),
matching how Step 11 left the commands. Lint now 81/0.
Step 18 — release 0.4.0 → 0.5.0 (minor, non-breaking)
Bumped .claude-plugin/plugin.json + the three lint-grepped literals (README
version-0.5.0-blue badge, CLAUDE.md (v0.5.0) header, CHANGELOG ## [0.5.0]).
Also synced the README reference_docs-25→26 badge (not lint-checked, but stale after
the convention doc) and refreshed the now-false "user data still lives inside the plugin"
prose in README + CLAUDE.md to "M0 done — data external." Version-consistency lint green;
the only remaining 0.4.0 refs are CHANGELOG history + the docs/m0/ planning docs that
describe the 0.4.0 → 0.5.0 transition itself (correct, not stale declarations).
Plan deviation — root marketplace docs NOT touched (scope). Plan Step 18 listed
/Users/ktg/.claude/plugins/marketplaces/ktg-plugin-marketplace/{README,CLAUDE}.md as
"root docs." Skipped: that path is the installed marketplace artifact, outside this
polyrepo, and STATE.md scope is "KUN denne pluginnen" (+ global "don't write in other
repos"). The binding gate (the lint) checks only this plugin's four declarations, all
green. Follow-up (separate catalog repo task): reflect linkedin-studio v0.5.0 in the
marketplace catalog README if/when desired — operator's call, not this plugin's M0.
Session 3 — Steps 11–13 (2026-06-18)
Environment reality vs. plan assumptions
The plan was authored assuming the operator's real .local.md runtime data
sat in the plugin tree (227-line voice profile, analytics exports, draft queue).
On this machine that data is absent — it is a clean clone:
assets/voice-samples/: only the PII-free placeholderauthentic-voice-samples.md(+.template.md). No.local.mdsource.assets/drafts/: only.gitkeep.assets/analytics/: onlyREADME.md+ emptyab-tests/.- The 4 tracked D2 scaffold instances DO exist (
high-engagement-posts.md,demographics.md,engagement-patterns.md,my-post-templates.md).
Consequence for Step 12 (live migration): every MOVE_FILES / MOVE_DIRS
entry resolved to an absent source → clean no-op. Only the 4 COPY_FILES
scaffolds were relocated. Result: migrated — moved 0, copied 4, skipped 0;
.migrated marker written; idempotent re-run confirmed already-migrated.
Step 12 verify adapted: the plan's literal check
(test -f .../voice-samples/authentic-voice-samples.md) cannot pass without a
.local.md source to move, so it was replaced with the achievable post-condition:
.migrated marker present + 4 scaffolds external + migrateData wired into
session-start.mjs + idempotency. The voice MOVE correctly no-op'd. (Operator
pre-approved this adaptation before the run.)
OUT OF M0 SCOPE — git-history scrub of the leaked post (FOLLOW-UP)
assets/examples/high-engagement-posts.md held the operator's real LinkedIn
post at HEAD (the "Ralph Wiggum / vibe-coding" post — real names, real engagement
metrics, real personal narrative). Step 13 scrubbed the working-tree content
to a generic placeholder (0 ## Post N sections → personalization score 0, no
PII). This does not remove the post from git history. A history rewrite
(git filter-repo / BFG) on assets/examples/high-engagement-posts.md is a
separate, explicit operation — deferred, not done here (brief §13: out of M0
scope). Track until the repo is published.
Note — external instance retains pre-scrub content (by design)
The Step-12 migration copied high-engagement-posts.md to
~/.claude/linkedin-studio/examples/high-engagement-posts.md before Step 13
scrubbed the in-plugin file (B3 ordering). The external copy therefore still holds
the migrated content. That is the operator's private data dir (outside any repo) —
the operator may curate it; not an M0 concern.
D2 scaffolds completed
All 6 scaffolds now have a read-only *-template.* seed: case-study-template.md
and framework-template.md already shipped; Step 13 added the 4 missing ones
(high-engagement-posts-template.md, demographics-template.md,
engagement-patterns-template.md, my-post-templates-template.md). The 3
already-generic instances seeded their templates verbatim; the high-engagement
template is a freshly-authored generic seed (the old instance was the leak).