Idempotent, collision-safe, create-on-demand IO under the brain dataRoot:
- writePublished: identical body → no-op; differing body at same id → disambiguated
<id>-N.md (never clobber, never silently drop a gold record, B2 defence-in-depth).
- ingestText: provenance always published; published_date defaults to captured_at.
- scanInbox: top-level *.md only (skips .DS_Store/dotfiles/non-md), non-destructive,
empty/absent inbox = clean no-op, re-scan re-skips via dedup.
- listPublished: parses each file in try/catch (malformed → skipped count, never
crashes), surfaces provenance so ai-draft leakage is eyeball-visible.
9 temp-dir IO tests. brain 48→57 tests, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
SB-S0 (Foundation) of the second-brain arc: a new TS package scripts/brain/
(structural copy of specifics-bank) establishing the spine later slices hang on.
TDD throughout (failing test first); fold scope = P1+P2 only per operator decision.
- types.ts: one provenance vocab (human|published|ai-draft) + the six-field
ProfileFact record + two-layer ProfileDoc.
- id.ts: slugify + mintEntityId (sha256[:12], kind-namespaced, slug-keyed so the
id is stable across value edits) + normalizeProvenance (throws on unknown). [SC4]
- profile.ts: no-YAML line-grammar parse/serialize (parse∘serialize = identity over
the whole doc; values may contain ]/|/quotes) [SC2] + foldUserProfile: lossless,
idempotent, source-absent-aware fold of config/user-profile.template.md. Pinned
extraction — P1 labeled scalars (group-headers skipped, [placeholder]→empty) + P2
expertise group; stops at "### Research Tooling" so deferred explainer prose can't
leak in as fields. Checkbox-prefs (Goals/Tone/MCPs/Assets) deferred (§8). [SC3]
- dataRoot.ts: inline per-package resolver (repo idiom; no new seam → SC6).
- scaffold.ts/cli.ts: idempotent `brain init` — brain/{index,profile,operations}.md
+ journal/ + ingest/{inbox,published} under the data-dir; compare-then-skip, never
clobbers a user edit. No session-start wiring (SB-S2 owns it). [SC1]
Gate: new BRAIN floor in test-runner.sh (34 tests; trends/specifics/contract floors
unchanged) + anti-erosion floor 74→75. Full gate 90/0/0, tsc --noEmit clean. [SC5/SC6]
No new command/agent/reference → no version bump, no structure-count change.
Refs docs/second-brain/{brief,plan-sb-s0,architecture}.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
SessionStart now warns (>=7d, warn-only) when the persistent trend store's
newest capture is stale, firing ONLY when the store already holds captures
(a never-scanned user is never nagged). Neutral wording — "scan for trends"
hits trend-spotter's own trigger; no hardcoded beat (de-niche invariant).
- store.ts: newestCaptureDate() — pure max-capturedAt staleness signal (SSOT)
- cli.ts: status [--json] subcommand (count + newest + daysStale)
- session-start.mjs: trendsNewestCapture() reads trends.json as raw JSON
(no tsx spawn at session start) + the reminder line, beside import-staleness
- tests: +3 store tests (newestCaptureDate) + hook subprocess test (3 cases:
>=7d fires, <7d silent, absent/empty silent + no crash)
- test-runner.sh: trends floor 21->24
Verified: trends 24/24 · all hook tests 131/131 · gate 89/0/0 · real render
confirms "Trend signals are N days old. Scan for trends…".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
The de-niche sweep (B-S1/B-S2) stripped the hardcoded KTG beat
(Microsoft|Azure|Copilot|public sector|offentlig sektor) from three surfaces,
but §17 only locked agents/trend-spotter.md against its return. Generalize the
guard to the full de-niched set so a beat token cannot creep back into the
other two:
- agents/trend-spotter.md (B-S1)
- agents/content-planner.md (B-S2a)
- references/content-framework.md (B-S2a)
Single shared NICHE_TOKENS criterion + non-vacuity/false-positive self-test
(unchanged shape), then a per-file grep loop over the explicit allowlist.
Scoped per file BY DESIGN, not tree-wide: references/content-angles.md keeps
"Public Sector" as 1 of 6 example industry tables (a KEEP surface — generic
illustration, not a beat) and is deliberately NOT guarded; "AI" is the plugin's
own subject, not a niche token. Added an "AI-driven content planning ..."
negative probe to lock that in (self-test now 5 caught / 6 ignored).
Non-vacuity proven by injection: a beat token appended to each new file makes
the guard fail naming the file+line; reverts clean. Gate 87→89/0/0 (+2 file
checks; assertion-count floor §18 still satisfied).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
B-S2a, the constraining-first slice of the de-niche sweep: kill the niche at
its source. B-S1 made trend-spotter pillar-driven, but the agent still READ
references/ai-content-framework.md (and so did differentiation-checker,
voice-trainer, and the content-creation skill) — an AI/Microsoft-specific file
whose very name baked in the niche. So the niche leaked back regardless of how
clean the agents were. This recasts that file domain-general and de-niches the
content-planner seasonal calendar (the other hardcoded beat: MS Build/Ignite as
THE anchors). The principle: vary concreteness, don't sterilize
(plugin-is-domain-general).
- Recast + rename references/ai-content-framework.md -> references/content-framework.md:
title "AI Content Framework" -> "Content Framework"; the 4 pillars kept as a
domain-general pattern (News/Implementation/Strategy/Tools) with examples now
spanning multiple fields instead of AI-only; AI-specific placeholders
([AI announcement], [AI system], GPT-X/Claude X) generalized to neutral
brackets; anti-patterns "AI will change everything" -> "[Field] will change
everything". The "News Monitoring / Sources by Priority" section (AI sources:
The Batch, ArXiv, r/MachineLearning, OpenAI/Anthropic blogs) — now duplicated
by the trend engine's config source-list — is thinned to point at
config/trends-sources.template.md + the data-dir override, keeping the
daily/weekly RHYTHM (general) and dropping the baked source list.
- Rename ripple, 6 referrers repointed: trend-spotter, differentiation-checker,
voice-trainer (reference lines, + dropped "AI" from descriptions), glossary
(Used-in + de-niched the "Example for AI content" pillar illustration),
linkedin-content-creation SKILL ("AI-specific angles" -> "Domain content
pillars + angles"), and test-runner §17 (NEGATIVE17 probe path + comment).
docs/hardening/log.md left intact — historical record, not a live pointer.
- content-planner.md seasonal calendar de-niched: header "Nordic/Tech Focus" ->
"rhythm, adapt to your field & region" + intro prompt; Microsoft Build,
Ignite (x2), Apple/Microsoft launches, NDC, EU AI Act, "Azure AI" example
pillar, "AI predictions", Nordic/17.mai locale anchors -> domain/region-
neutral prompts. Global anchors kept (New Year, IWD, Halloween, Black Friday,
year-end).
Deferred to after the full sweep (per STATE): extending the §17 de-niche guard
to content-planner (and content-framework) — the guard's token set + agent
scope is best designed once the sweep (B-S2b) reflects the final clean surface.
ref count unchanged (27; rename is 1->1). Gate 87/0/0 (§17 self-test green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY
The trend-spotter agent stated its own contract — "the niche lives in the
source list and the user's pillars, never in this agent" — yet contradicted it
by hardcoding the Microsoft/public-sector beat in four surfaces. Resolve the
contradiction so the file is genuinely domain-general (plugin-is-domain-general):
the domain comes from the user's profile/pillars at runtime, never baked in.
- Description: "trending topics in AI, Microsoft, and public sector" ->
"across the user's content pillars and domain"; trigger phrases "what's
happening in AI" -> "in my field"/"in my space".
- Mission: drop "intersection of AI, Microsoft technology, and public sector
digitalization" -> "within the creator's own domain, defined entirely by
their content pillars and expertise areas, never by a beat baked into this
agent".
- Content Trigger Classification: "Microsoft platform changes" -> "platform
changes in the user's stack"; "public sector milestones" -> "sector
milestones in the user's domain".
- 4-Question Relevance Filter: "Public sector leaders or enterprise AI
implementers" -> "the user's target audience (per their profile)".
- Anti-pattern example: "AI is changing everything" -> "[topic] is changing
everything".
- CI: new test-runner Section 17 (trend-spotter de-niche guard) forbids the
KTG-beat proper nouns (Microsoft|Azure|Copilot|public sector|offentlig
sektor, case-insensitive) from returning to agents/trend-spotter.md, with a
non-vacuity self-test mirroring Sections 8/13. Scoped to this one agent
(B-S1); the wider sweep (B-S2) owns the other surfaces + the
ai-content-framework.md reference filename. Assertion-count renumbered to
Section 18. Gate 85 -> 87/0/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY
Bind the layers slices 1 (store) and 2a (config) built: trend-spotter goes from
an amnesiac, niche-hardcoded scanner to a persistent, generic engine.
- Tools: drop the `tools:` allowlist (inherit all session tools incl. any
research MCP) + `disallowedTools: Write, Edit, NotebookEdit`. An explicit
allowlist would block every MCP unless its mcp__server__tool name were
hardcoded — which breaks "prefer whatever MCP the user connected, hardcode
nothing". WebSearch+WebFetch stay as the always-available floor; Bash runs
the deterministic store CLI. (CC mechanic verified vs code.claude.com/docs.)
- Store-wiring (de-amnesia): query prior history before polling; persist every
kept trend through scripts/trends `add` (dedup/union preserved — a raw Write
would bypass it). Mirrors how specifics-bank is wired from the command layer.
- MCP-first routing: read the profile's "### Research Tooling" declaration,
prefer a declared MCP, fall back to the floor, fail soft. No hardcoded names.
- Scoring -> SSOT: replace the inline matrix/composite/bands with a pointer to
references/trend-scoring-modes.md (kortform default, long-form on request).
- Sources from config: replace the hardcoded vendor/outlet list + query bank
with a read of trends/sources.md (user override) -> trends-sources.template.md
(shipped default).
- CI: new test-runner Section 16 (trends-store binding guard, floor 21,
KTG-only skip) mirroring the specifics-bank guard; assertion-count renumbered
to Section 17. Gate 84 -> 85/0/0. model-consistency green (model unchanged).
Reload required: the tools-grant change takes effect only after a session reload.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY
Declaration/config groundwork that slice 2b's trend-spotter upgrade reads.
Standalone (no agent wiring yet — that's 2b), mirroring slice 1's pattern;
[skip-docs] for the same reason slice 1 was — user-facing docs land when 2b
wires the engine live.
- references/trend-scoring-modes.md: methodology SSOT for two rubrics —
kortform (feed post, timing 20%) + long-form (chronicle, depth 25% / timing 10%,
per tema-research-motor-spec §4.2). Both sum to 100%. trend-spotter renders from
this in 2b instead of inlining a matrix (S12-consistent).
- config/trends-sources.template.md: shipped generic source-list defaults →
user override at ${LINKEDIN_STUDIO_DATA}/trends/sources.md (data-dir, survives
reinstall; same template->data-dir pattern as user-profile).
- user-profile.template.md: new "Research Tooling" section — declared research MCPs
(Tavily/Gemini/Perplexity/Other) + WebSearch/WebFetch floor. 2b routes MCP-first.
- setup.md Step 3f + onboarding.md Phase 2: ask "which research MCPs?" -> profile.
Store only what the user declares; no hard-coded MCP names.
- test-runner.sh: EXPECT_REFS 26->27; generalized the M0 +1 delta-guard into a
named-post-M0-additions guard (POSTM0_REFS) so a legit later ref doc passes while
the anti-masking intent holds. Gate green 84/0/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY
[skip-docs] internal plumbing — standalone store, no command/agent/pipeline
surface change until slice 2 wiring (mirrors specifics-bank slice 2). CLAUDE.md
"Telling"/counts untouched; lint stays 84/0/0.
Research-engine §5 (foundation layer) slice 1: the deterministic STORE half of
the persistent trend store — a topic-tagged, provenance-bearing inventory of
trend signals captured over time, so the research engine accumulates HISTORY
instead of starting amnesiac each session. Trend-side twin of the lived-specifics
bank (same store/dedup/query discipline; dedupe key is normalized title+URL, not
free-text content). Generic by architecture: nothing niche-specific lives here —
topics and source are free-form, decided upstream via config/profile.
scripts/trends/ (sibling to specifics-bank, same tsx convention):
- src/types.ts — TrendRecord/TrendStore schema (schemaVersion 1), minimal
generic core: title, url, source, capturedAt, topics[], optional summary
- src/store.ts — pure store: normalizeField, title+url-hash id (= dedupe key),
load/save, addTrend (dedupe + topic union on re-capture; first-sighting
source/capturedAt kept), queryByTopic (overlap-ranked then recency), history
(time-scoped, since/limit)
- src/cli.ts — add / query / list; default store under
${LINKEDIN_STUDIO_DATA:-~/.claude/linkedin-studio}/trends/ so trend history
survives plugin upgrades/reinstalls (M0 data-path seam)
- tests/store.test.ts — 21/21 green; tsc clean
- README + .gitignore for node_modules/build
Capture/scoring agent + MCP-first routing land in slice 2; the CI binding guard
is deferred to wiring, mirroring the specifics-bank timeline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the kilde-så-draft binding (slices 1–2) into the live /linkedin:newsletter
pipeline so a draft is grounded in the operator's real material BEFORE any prose.
- New Step 1.5 "Lived-specifics extraction" BETWEEN brief (1) and research (2):
query the bank → 3–5 sharp elicitation questions (vagueness refused, never
fabricated — invariant 3) → `add` to the specifics-bank → populate the
articles.NN.livedSpecifics slot-map → render the NN-kilder.md ledger. First
phase to write machine state (persists brief + personas + binding).
- Deterministic binding-gate (`validate-binding`, exit 1 = BLOCK) wired as a new
step 5 of the Step 2.5 skeleton-gate (renumbered 5→9): no prose is written on
an unresolved load-bearing claim; livedSpecifics → "bound" on pass.
- Step 2 re-scoped BY the binding: research fills the ekstern/unresolved slots +
verifies the operator's own numbers (regel 6/7) → flips bank unverified→verified
→ re-renders NN-kilder.md, instead of re-sourcing what is already grounded.
- Pipeline 17 → 18 phases: new `lived-specifics` phase id in edition-state
_doc.phases; resumption table + pipeline overview + CLAUDE.md + README updated.
- Lint: new Section 15 "Specifics-Bank Binding" wires the specifics-bank suite
into CI (suite green + case-count floor 28, KTG-only/adopter-skip); the
assertion-count anti-erosion floor moves to Section 16.
Verification: structure gate 84/0/0 (was 83), specifics-bank 28/28, contract-gate
ratify 1:1, edition-state template valid JSON (18 phases), gitleaks clean on all
5 files. Design record: docs/fix2/slice2-binding.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 2 of fix#2 (kilde-så-draft): the per-edition bridge between the global
specifics-bank (slice 1) and a draft. In scripts/specifics-bank:
- binding.ts — livedSpecifics slot-map model + validateBinding, the «vaghet
avvises» gate (G4). BLOCK on unresolved / dangling specific-id / unjustified
abstrakt; WARN on an unverified-number-backed slot (regel 6/7); PASS otherwise.
- kilder.ts — renderKilder, the read-only NN-kilder.md sources ledger, rendered
deterministically from the slot-map + bank (coverage via validateBinding, so
the artifact and the skeleton-gate cannot disagree).
- cli.ts — validate-binding (exit 1 = BLOCK) + render-kilder subcommands.
edition-state schema gains articles.NN.livedSpecifics ({ slots, status }) + _doc.
Design record + the Step 2 research re-scope spec: docs/fix2/slice2-binding.md.
Wiring into newsletter.md (Step 1.5 elicitation + Step 2.5 gate + pipeline 17->18)
is slice 3 — not in this commit.
Tests 28/28 (specifics-bank; +9 binding +4 kilder), tsc clean, plugin gate 83/0/0
unaffected, gitleaks clean on all 8 files.
[skip-docs] internal plumbing — no command/agent/pipeline surface change until
slice 3 wires it; documenting it in the user-facing root CLAUDE.md/README now would
overclaim unreachable functionality. Docs ARE updated where they belong: the design
record docs/fix2/slice2-binding.md, scripts/specifics-bank/README.md, and the
edition-state _doc. Root-doc update lands with the slice-3 wiring feat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix#2 (kilde-så-draft) slice 1: the deterministic STORE half of the
lived-specifics bank — the operator's real, un-generatable raw material
(measured number / named-but-real case / what broke / contrarian opinion /
mind-change), so drafts start grounded instead of hollow (retning §3;
dream-spec "Lived-Specifics Extraction", the single biggest missing upstream
piece for the authenticity thesis).
scripts/specifics-bank/ (sibling to contract-gate, same tsx convention):
- src/types.ts — Specific/Bank schema (schemaVersion 1), un-generatable taxonomy
- src/bank.ts — pure store: normalizeContent, content-hash id (= dedupe key),
load/save, addSpecific (dedupe + topicTag union on re-capture), queryByTopic
(active-only, ranked by tag overlap then recency)
- src/cli.ts — query / add / list; default bank under
${LINKEDIN_STUDIO_DATA:-~/.claude/linkedin-studio}/specifics-bank/ so it
survives plugin upgrades/reinstalls (M0 data-path seam)
- tests/bank.test.ts — 15/15 green; tsc clean
- README + .gitignore for node_modules/build
Content is stored verbatim; never AI-generated. The elicitation interview
(refuses vague answers; abstrakt/ekstern escape hatches) lands in slice 3
(/linkedin:newsletter Step 1.5). Forward-compatible with the profile-evolution
/ "second brain" architecture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 3 of fix#1 (§C2 bindende + akkumulerende): wire the deterministic
§B/§C1 contract-gate into the long-form pipeline as a pre-handoff step, so
mechanical violations die deterministically BEFORE the costly AI gates and
never reach the operator at a judgment gate ("rettelser fester seg").
Newsletter pipeline:
- New Step 4.5 (between Step 4 consistency/quality and Step 5 fact-check):
(1) ratify the gate (rules.ts <-> §E-manifest 1:1; DRIFT escalates to
operator, not a draft fix) and (2) gate the draft (exit 0 PASS / exit 1
BLOCK -> fix by tightening + re-run; WARN = direction, never blocks).
Adopter-safe: no-op when contract-gate deps / the maskinrommet contract
are absent.
- Pipeline phase count 16 -> 17 across every LIVE surface (overview table +
build-status + narrative + banner + schema pointer in newsletter.md;
command-table rows in CLAUDE.md and README). Historical version-narrative
entries left untouched.
- edition-state.template.json: new "contract-gate" phase string (Step 4.5),
between consistency-quality and factcheck-sweep. JSON valid; phases 16->17.
CI integration (test-runner.sh, Section 14 — new):
- 14a: contract-gate suite stays green (npm test exit 0) with a 33-test
case-count floor (anti-erosion).
- 14b: ratify invariant — rules.ts bound 1:1 to the §E-manifest.
- Both KTG-internal and adopter-safe (warn/skip, never fail, when deps or the
contract are absent). set +e inside the test command-substitution keeps a
red suite from aborting the runner; ratify exit 1 captured via if-subshell.
- SC6 anti-erosion section renumbered 14 -> 15; header comment updated.
Verification:
- Structure lint 81 -> 83/0/0 (contract-gate suite + ratify both green).
- Documented Step 4.5 command smoke-tested on a real draft
(serier/silvija-seres-motsvar/utkast/04-utkast.md): 0 BLOCK / 2 WARN ->
"Gate passert", exit 0.
- edition-state.template.json valid JSON; phases array = 17.
foldIns capture into the pipeline phases deferred to a later slice (scope A).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 2 av fix#1 («rettelser fester seg»): binder rules.ts til den menneskelige
sannhetskilden (skrivekontrakt §E), så en korreksjon gjort én gang håndheves for
alltid i stedet for å gjenoppdages hver utgave (diagnose-gap 2).
- ratify.ts: importerer RULES direkte (ingen skjør tekst-parsing), krever en
bijeksjon mot §E gate-bindings-manifestet (id ↔ ref ↔ severity) + dangling-
§-anker-sjekk. CLI: `--ratify [kontrakt]` / `npm run ratify`. Sti: arg > env
MASKINROMMET_CONTRACT > default (relativt til modulen).
- edition-state.template: foldIns[] fangst-kø (per-artikkel proveniens,
pending|promoted|rejected, mekanisk→rules.ts / dømmekraft→§C2).
- 12 nye tester (33 totalt), tsc rent. Live-kontrakt-integrasjonstest ratify-er
det ekte §E-manifestet rent (10 regler bundet 1:1).
Companion: skrivekontrakt §E (JA-promoter-prosedyre + manifest) i maskinrommet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds scripts/contract-gate/ — a deterministic, AI-free gate that enforces the
mechanical half of the Maskinrommet writing contract (§B red flags + §C1
regelsjekk) BEFORE operator handoff, so the same mechanical correction never
reaches KTG twice. First slice of fix#1 (binding + accumulating); target
re-scoped to close all 4 diagnosis gaps = sustainable newsletter workflow.
- Structured ruleset as DATA (src/rules.ts) so accumulation (slice 2) appends.
- 3 BLOCK (modell-strawman, series-thesis "er ikke modellen", _underscore_)
+ 7 WARN (judgment-needed). Block only on provable, ~zero-FP matches.
- Code/frontmatter masking; line-accurate reporting; CLI receipt + exit code.
- 21/21 tests (node:test), tsc clean.
- Validated on 6 real editions: 0 BLOCK on published Del 2-5; caught a real
Rule-10 violation in Del 6 (WIP) automatically. Versal-tic uses an
emphasis-word denylist after real-data FPs (NAV/DFØ/NPM).
Finding: the plugin already mirrors §A + §C2; §C1 was the unhomed gap this fills.
Plugin structure lint unchanged (81/0/0); no new agents/commands/refs/skills.
Closes the 2 grep/Read-verified findings from the S12 cold full-brief re-review
(docs/remediation/review.md, WARN 0/1/1/0, 0 dropped) and closes the $-injection
CLASS — not the line — across the whole state-updater.mjs mutation surface.
See docs/remediation/review.md (S13 ALLOW, 0/0/0/0) for the full closure record:
replaceField -> replacement function; the 3 additive-insert sites -> functions
(m === $1, behavior-preserving); a scalar assert.match pins last_post_topic; and a
behavioral, coverage-complete, self-testing Section 12 guard (check-replace-safety.mjs)
that is mutation-proven. Docs three-doc + residuals updated. test-runner.sh 71/0/0,
node --test 98/98.
Closes the 2 grep-verified findings from the S11 cold full-brief re-review
(docs/remediation/review.md, BLOCK 1/0/1/0, 0 dropped). Both were the NEXT
RING of the meta-class S10/S11 converged: a propagation miss — the fix had
landed where the SC named the file, not in the render-source it depends on.
BLOCKER (command->reference propagation): references/ab-testing-framework.md:166
still shipped the banned A/B "Significant? (>20%)" Yes/No verdict column while
commands/ab-test.md (which RENDERS from it, inlined at :30, presented at :69)
had been cleaned to the honest "Directional?" framing. Re-framed the reference
result template to match the command verbatim (header + the directional note)
and retuned :38 "20% significance threshold" -> "minimum-meaningful-difference
threshold". The whole render chain is now significance-verdict-free.
MINOR ($-replacement, class-closed not line-patched): the newest-first section
appends/rewrites in hooks/scripts/state-updater.mjs passed a replacement STRING
embedding untrusted user content to String.replace, so dollar-sequences
($1 / $& / dollar-backtick / dollar-apostrophe / $$) in a topic/hook/partner
(e.g. "$100 budget cut") re-injected the captured heading and dropped
characters, silently corrupting state. Converted all 5 content-bearing sites
(Recent Posts, prune, Milestone Log, First-Hour, Outreach) to replacement
FUNCTIONS; the 3 remaining $1 sites only interpolate date scalars. +4
$-bearing regression tests — incl. the prune fixture, which itself had to
switch to a function (the bug bit the fixture as it was being written).
META (generalize the lint to the propagation-miss class): new test-runner.sh
Section 11 — render-chain propagation guard. Forbids the significance-verdict
column (Significant? adjacent to "(" or a table pipe) across the WHOLE render
chain (commands + every inlined reference + adjacent surfaces), with a
permanent non-vacuity self-test (3 verdict forms caught, 6 legitimate
Significant/significance/Directional? forms ignored) and an e2e mutation-proof.
Generalizes S10/S11's "fix the class, not the line" to command->reference.
Pre-patch render-chain sweep confirmed ab-testing-framework.md was the SOLE
propagation survivor (so a 6th review finds no 3rd). test-runner.sh 70/0/0;
node --test 98/98. CLAUDE.md lint enumeration synced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cold full-brief re-review (S10) reached a class the S7->S9 algorithm-stat lens
never did:
BLOCKER — post-feedback-monitor published as Haiku in four surfaces
(README:259, skills/linkedin-studio:159 with wrong color Green too,
skills/linkedin-analytics:41, agents-capability-matrix:20) while
agents/post-feedback-monitor.md runs Opus. v4.0.0's Opus promotion never
reached the user-facing tables. Synced all to Opus/Lime. Refreshed
agents-capability-matrix.md (frozen at the v2.0 14-agent era): header 14->19,
+5 missing longform agents, tier counts Opus 2->8 / Haiku 1->0, longform-gate
diagram updated to the real 8-Opus-agent chain.
MAJOR — de-branded docs/plan-fullspektrum-innholdsmotor.md:70 (model brand +
jan-2026 asserted as fact -> no-name/no-month relevance-model phrasing). It was
the only tracked survivor; the rest live in gitignored ROADMAP.md /
.claude/research/ (not shipped, out of honesty scope).
META — added Section 10 model-consistency guard
(scripts/check-model-consistency.mjs): each agents/*.md model: must match every
surface declaration AND the canonical rosters must list all 19 agents.
Permanent non-vacuity self-test + e2e mutation-proven.
Pre-patch sweep confirmed post-feedback-monitor was the sole drifted agent
(89 model rows, 0 other mismatches). test-runner.sh 68/0/0, node --test 94/94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the S9 re-review (1 BLOCKER + 2 MAJOR, all grep-verified). The survivor
set converged 8 -> 6 -> 2; this closes the meta-problem behind the convergence,
not just the two lines.
BLOCKER — references/glossary.md:10: drop the fabricated "150-parameter
foundation model" (a garbled 150B that the S9 enumerative grep/lint, requiring a
"B"/"billion", could not match). Reframe to "a real input to LinkedIn's 2026
relevance-ranking model" with no parameter count, citing
algorithm-signals-reference.md inline — which makes the :12 "Used in" provenance
accurate (the reference does state the relevance-ranking framing; it never stated
"150-parameter").
MAJOR — CHANGELOG.md:308: de-brand "360Brew profile optimization (January 2026
algorithm update)" -> "Profile/topic-relevance optimization". Removes the
unpublishable brand + asserted Jan-2026 date, honouring v4.0.0's "removed
everywhere" claim. It was the only STALE_STATS hit in CHANGELOG.
MAJOR — scripts/test-runner.sh: the rebuilt lint was enumerative on surface form.
Generalize it to the PATTERN CLASS so the same grep that defines the SC fails on
any surface form, present or future:
- STALE_STATS model token: "150 ?B param|150 billion param"
-> "[0-9]+[ -]?(B|billion)?[ -]?param"
(covers 150-parameter / 150B param / 150 billion param). This robustifies the
review's literal suggestion "[0-9]+[ -]?(B|billion )?param", which missed the
space form "150B param"; the separator is moved out of the group.
- STAT_HITS grep scope += CHANGELOG.md (the 360Brew survivor lived outside it).
- Permanent non-vacuity SELF-TEST before the real scan: 13 forbidden probes must
match (incl. the exact "150-parameter" survivor), 8 legitimate probes must not
("Language parameter", "parameterized", "different parameters",
"175-milliarders parametermodell", 5x5x5, cadence, pixel dims, "10x your
reach"). S7->S9 each shipped a green lint because the proof was run by hand and
never committed; this makes narrowing STALE_STATS fail the suite.
Verification: test-runner.sh 67/0/0 exit 0 (was 66/0/0; +1 self-test);
node --test 94/94; broadened exhaustive grep across the tree -> zero survivors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the S8 re-review (BLOCK 3/4/1). The S8 fix patched only the 2 strings S7 named; the re-review found 6 more same-class survivors. Per the systemic read, this is a comprehensive sweep, not a per-line patch.
Reconciled every retired engagement-coefficient + model-fact survivor against the canonical references/algorithm-signals-reference.md (order, not coefficients; comment ≈ 2x a like; no model name/params):
- glossary.md: coefficient table + Save-Signal '10x weight' → canonical ordering (citation now true)
- engagement-frameworks.md, analytics-interpreter.md, content-optimizer.md, pipeline.md, engagement-coach.md: the 10x/8x/7-9x/2.5x/0.2x system (incl. 4 survivors the re-review did not cite) → ordering
- playbook: '15x more algorithmic boost' + video '5x more conversations' → directional, sourced
- profile.md + linkedin-voice/SKILL.md: '150B parameter foundation model' → '2026 relevance-ranking model'
- quality-scorecard.md: '360Brew Validation' → topic-relevance framing
- setup.md: 'thought leadership plugin' → 'LinkedIn Studio plugin'
Lint (MAJOR 4): rebuilt scripts/test-runner.sh STALE_STATS to forbid EVERY retired-class phrasing (not the 2 S7 strings) + widened scope to assets/checklists/. Targets retired phrasings (7-9x, (10x), '10x weight', '5x more conversations'), NOT bare 10x/15x/5x (legit 5x5x5 / cadence / pixel-dims / '10x your reach' hyperbole). Proven non-vacuous: catches all 10 retired strings, ignores all 10 legit uses.
Tests (MAJOR 7): added no-anchor fall-through tests for recordFirstHourPlan + recordOutreachContact (date scalar not written/reported, section still appended). MINOR 8: reflowed newsletter.md content-repurposer wiring onto one line.
test-runner.sh 66/0/0; node --test 94/94 (was 92, +2). NO push until /trekreview re-clears the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the 5 findings from the S7 /trekreview release gate (review.md, verdict BLOCK):
- BLOCKER: comment-multiplier "5x" reconciled to the canonical order-only framing
(no fixed multiplier) in agents/engagement-coach.md, linkedin-growth-playbook,
linkedin-formats.md — per algorithm-signals-reference.md ("do not quote a comment
multiplier").
- BLOCKER: carousel rate "6.60%/6.6% (highest)" reconciled to "~7% top organic
format" in linkedin-formats.md:42 (was self-contradicting :50) and
assets/templates/carousel-templates.md.
- Lint hardening: test-runner.sh STALE_STATS now matches 6.60% + the 5x comment
folklore and scans agents/ + assets/templates/ — the grep that defines the
Phase-0 criterion now catches both BLOCKERs.
- MAJOR: onboarding.md command count 26 -> 27.
- MAJOR: add section-append-branch (production-path) tests for recordFirstHourPlan
+ recordOutreachContact against a template-layout fixture.
- MINOR: move date-scalar changes.push inside the write branch in state-updater.mjs.
Verify: node --test hooks/scripts/__tests__/*.test.mjs -> 92/92; bash
scripts/test-runner.sh -> 66/0/0. NO push until /trekreview re-confirms ALLOW/WARN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 21 (remediation Wave 4 / S6, SOLO): finalize the audit-remediation as
v4.0.0. Version 3.1.0 -> 4.0.0 across all current-version declarations; counts
reconciled to the ls-derived source of truth (19 agents / 27 commands / 6 skills
/ 9 hooks / 25 reference docs / 16 newsletter phases — Step 20 confirmed NO
TRIM); three-doc sync (plugin README + plugin CLAUDE.md + root README) clears the
[skip-docs] debt accumulated across Wave 1-4; CHANGELOG v4.0.0 entry summarizing
Steps 1-20.
Scope additions beyond the plan's literal Files list, all version-sync or
[skip-docs]-debt in nature (flagged, not feature creep):
- ../../CLAUDE.md (root marketplace): linkedin-studio entry v3.1.0 -> v4.0.0
(the version-sync invariant mandates updating every version reference; leaving
it stale is a real inconsistency).
- scripts/test-runner.sh: added the version-consistency grep the file's own
Step-1 comment promised ("added in Step 21") — plugin.json version must match
the README badge, the CLAUDE.md header, and the CHANGELOG top entry — and
folded the Wave 2 lint gap (plugin.json now covered by the stat-consistency
scan). 66/0/0, exit 0.
- plugin README: added the missing /linkedin:firsthour command row (Step 16
[skip-docs] debt) and the 25th reference doc (longform-quality-rules.md) to
the knowledge-base table; fixed body counts ("All 26 commands" -> 27, "24
reference documents" -> 25); badges + intro 26 -> 27 commands, 24 -> 25 refs.
- root README + marketplace.json: dropped the unpublishable model brand/date
("360Brew" / "January 2026") the algorithm-signal reconciliation already
removed everywhere inside the plugin.
Surviving "3.1.0" strings are intentional history, not stale declarations: the
README version-history table row, the "vX added Y" attributions in
plugin.json/CLAUDE.md, and the headless-review reload caveat are all
changelog-genre. Every current-version declaration (plugin.json version, README
badge, CLAUDE.md header, root README marker, marketplace.json) reads 4.0.0.
The major bump reflects the remediation's scope plus the reinstall/reload the
newly-wired agents need to register, and consolidates — does not repeat — the
v3.0.0 identity break; it is not a fresh breaking API change (locked operator
decision).
Pre-existing and out of scope (flagged, untouched): a duplicate /linkedin:setup
row in the README command tables.
Verify: bash scripts/test-runner.sh exit 0 (66/0/0); plugin.json + marketplace.json
parse; counts consistent README == CLAUDE.md == root README; stale-count sweep
clean. NO push — /trekreview (S7) is the release gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire orphan agent #11 (engagement-coach) by giving it a command surface, and
add the tracked first-hour state the plan calls for (remediation Step 16).
- commands/firsthour.md (new, 27th command): post-publish first-hour /
reply-loop sprint. Delegates plan construction to engagement-coach via
Task (subagent_type: linkedin-studio:engagement-coach) — returns a grouped
target list (whales/inner-circle/ICPs/new connections), 2-3 seed
self-comments + 3-5 CEA replies in the user's voice, and a minute-by-minute
timeline anchored to publish time. Presents timeline/targets/drafts +
velocity checkpoints, auto-copies the drafts to clipboard, persists the
plan, then hands off to post-feedback-monitor for the 48h window.
- hooks/scripts/state-updater.mjs: new pure mutation recordFirstHourPlan()
mirroring updatePostTracking — additive by contract (inserts
last_firsthour_date after last_post_date when absent, creates the
## First-Hour Plans section when absent, never touches existing fields).
Section name is deliberately non-R-initial so it stays outside
pruneContentHistory's "## Recent Posts ... (?=\n## [^R])" capture window.
+ a --record-firsthour CLI branch for parity with the other mutations.
- config/state-file.template.md: additive scalars (last_firsthour_date,
firsthour_active) + the ## First-Hour Plans section.
- hooks/scripts/__tests__/state-updater.test.mjs: extend (existing file) with
7 recordFirstHourPlan tests — section creation, field insertion vs in-place
update (no duplication), round-trip non-interference, graceful empty
defaults, changes array.
- CLAUDE.md: register the command (## Commands 26 -> 27, table row).
- scripts/test-runner.sh: EXPECT_COMMANDS 26 -> 27 (registration guard).
Verify: grep 'subagent_type: linkedin-studio:engagement-coach' commands/ ->
firsthour.md; node --test state-updater -> 26/26; full hook suite -> 83/83;
bash scripts/test-runner.sh -> exit 0 (62 passed, commands 27/27).
Plan Step 16 (Wave 4 S3).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 2 / Step 7 of the remediation plan (research/02 D2/D4).
The tool parses the LinkedIn analytics CSV export, which has no saves/dwell — and
there is no self-serve API to pull them. Stop implying it tracks them:
- report.md: replace "Saves (10x weight) and expert comments (7-9x) are the
highest-impact signals" with honest wording — saves rank highest in the
engagement ORDER (cite references/algorithm-signals-reference.md, not a restated
coefficient), but are visible only in native LinkedIn post analytics (count-only,
~Sept 2025+) with no self-serve API, so this tool does not auto-track them; dwell
is internal to LinkedIn for organic posts.
- types.ts: document why PostMetrics intentionally omits saves/dwell (no ingest
source) so a future contributor does not "add the missing fields".
- strategy.md: reconcile two saves references (signature-content criterion +
authority scorecard) to say the count is read from native LinkedIn analytics, not
captured by this tool.
No new metric field, no manual-entry feature (operator Q3).
Verify: report.md has no "Saves (10x"/"highest-impact signals" and does say "no
self-serve API"; tsc --noEmit clean; analytics suite 106/106; structural lint
0 failed (report.md cites the reference, so stat-consistency stays green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 2 / Step 4 of the remediation plan (docs/remediation/plan.md).
PRIMARY (the real fresh-clone failure):
- scripts/analytics/node_modules is gitignored, so a fresh clone has neither
tsx nor csv-parse. Surface an idempotent `npm install --silent` prerequisite
at point-of-use in report.md (Step 1b) and import.md (Step 4).
DEVIATION FROM PLAN (correction-in-scope, to satisfy the plan's own Verify gate):
- The plan assumed prepending `npm install` was sufficient. Verified it is NOT:
the commands invoke the CLI with an absolute src/cli.ts path but from the
user's arbitrary CWD, and `node --import tsx` resolves the `tsx` specifier
relative to CWD, not the script. There is no global tsx, so the call still
fails with ERR_MODULE_NOT_FOUND from any CWD other than scripts/analytics.
- Complete fix: invoke the locally-installed tsx by its absolute
node_modules/.bin/tsx path in all CLI calls (report.md x10, import.md x3), so
they resolve from any working directory once the install above has run.
Verified: 0 ERR_MODULE_NOT_FOUND running `report` from /tmp.
SECONDARY (latent correctness / hardening):
- Add findPluginRoot(): walks up to the dir holding .claude-plugin/plugin.json
and anchors getAnalyticsRoot() on it, falling back to the legacy 4-up count.
MEASURED that ../../../../ already resolved to the plugin root from BOTH
src/utils and build/utils (both 4 levels deep), so the plan's "src-vs-build
depth miscalibration" premise was false — this is correct-by-construction
hardening (survives a future source move), not a live-bug fix.
- Reconcile cli.ts usage/help text: `node build/cli.js` -> `node --import tsx
src/cli.ts` (the real runtime).
- Fix report.md troubleshooting: "Verify tsx is available" -> the actual
install command on ERR_MODULE_NOT_FOUND.
Test-first: scripts/analytics/tests/storage-root.test.ts (red on missing
findPluginRoot export, green after). Full suite 106/106, tsc --noEmit clean,
structural lint 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Add /ultraresearch-local for structured research combining local codebase
analysis with external knowledge via parallel agent swarms. Produces research
briefs with triangulation, confidence ratings, and source quality assessment.
New command: /ultraresearch-local with modes --quick, --local, --external, --fg.
New agents: research-orchestrator (opus), docs-researcher, community-researcher,
security-researcher, contrarian-researcher, gemini-bridge (all sonnet).
New template: research-brief-template.md.
Integration: --research flag in /ultraplan-local accepts pre-built research
briefs (up to 3), enriches the interview and exploration phases. Planning
orchestrator cross-references brief findings during synthesis.
Design principle: Context Engineering — right information to right agent at
right time. Research briefs are structured artifacts in the pipeline:
ultraresearch → brief → ultraplan --research → plan → ultraexecute.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>