Commit graph

212 commits

Author SHA1 Message Date
b185db9a12 feat(linkedin-studio): RE-R3b — trend lifecycle (re-score on re-capture · status · seen-log) [skip-docs]
The lifecycle layer over the trend store: what happens to a trend AFTER first capture.
- re-score on re-capture (last-wins; addTrend duplicate branch, score the one mutable
  field; provenance + lifecycle untouched; no false-merge via JSON compare). Reverses
  R3a's first-sight D3 — that R3a test reconciled to the new behaviour.
- status new/acted/skipped (effectiveStatus/setStatus + act/skip/reset CLI verbs);
  rankForBrief EXCLUDES handled trends (a work queue, not an archive).
- seen-log surfacedCount/lastSurfacedAt (markSurfaced, per-day idempotent); the brief
  CLI records surfacing on the store AFTER the pure render, unless --no-mark.
- render: entry id in backticks (copy-paste for act/skip) + · sett Nx prior-day hint.
- schema v3→v4 (additive lossless); the R3a migration block reconciled to the bump,
  the new R3b block committed against SCHEMA_VERSION (breaks the reconcile cycle).

score.ts + item.ts untouched (re-score reuses the R3a capture path). RED-first (two
phase: 16 logic-RED + 4 stub-RED). Gate: Section 16k (6 emitters), TRENDS_TESTS_FLOOR
146→171, ASSERT_BASELINE_FLOOR 99→105. trends 171/171, gate 120/0/0, hook suite 139/139.

Plan: docs/research-engine/{brief,plan}-re-r3b.md (light-Voyage hardened @ c40b937).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 01:08:43 +02:00
c40b937856 docs(linkedin-studio): RE-R3b brief + plan — trend lifecycle (re-score · status · seen-log), light-Voyage hardened
Slice (a) of the full-R3 build-out: the lifecycle layer over the trend store.
- re-score on re-capture (last-wins; R3a's explicit deferral)
- status new/acted/skipped (act/skip/reset CLI; brief excludes handled)
- seen-log surfacedCount/lastSurfacedAt (per-day idempotent, brief-recorded)

Architecture confirmed via AskUserQuestion: on-record seen-log + brief records
surfacing (rankForBrief stays pure, --no-mark dry-run) · last-score-wins ·
exclude acted/skipped. score.ts + item.ts untouched (re-score reuses the R3a
capture path); touched: types/store/brief/cli + schema v3->v4.

Light-Voyage hardened (3 Opus reviewers vs live code): scope-guardian ALIGNED;
brief-reviewer PROCEED_WITH_RISKS; plan-critic PROCEED_WITH_RISKS (78/B). All
folded — incl. the MAJOR (the brief CLI store binding hoist) + the v3->v4
migration-block reconcile (premise-verified before drafting).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 00:44:47 +02:00
e169c78710 feat(linkedin-studio): RE-R3a — persist relevance score on the store record + rank the morning brief on it [skip-docs]
R3 slice 1 (research-deepening). Stop discarding the relevance judgment the
trend-spotter already computes: persist a 4-field TrendScore {mode, dimensions,
composite, priority} on TrendRecord (schema v2->v3, additive lossless migrate),
computed by the existing score.ts composite()+band() (one owner, no new arithmetic),
threaded item->store; then rankForBrief sorts each bucket composite-first (sentinel
-1 for unscored) and renderBrief surfaces "· <priority> (<mode>)" per body entry
(briefSummary shows the band only). First-sight only; mode-blind ranking with the mode
shown so the operator can disambiguate instruments.

- score.ts: TrendScore + requiredDimensions(mode) (ordered) + scoreEnvelope (composes
  composite+band; throws on bad dim by contract)
- types.ts: SCHEMA_VERSION 2->3; TrendRecord.score?
- store.ts: TrendInput.score?; addTrend persists first-sight (duplicate keeps it);
  migrate comment v1->v2->v3 (logic unchanged, JSON.stringify preserves the field)
- item.ts: TrendItem.score?; normalizeItem validates (non-array score/dimensions + the
  mode's five dims in [1,10]) -> structured error never throw, carries validated dims;
  itemToInput -> scoreEnvelope (no throw on the capture path; direct call throws by contract)
- brief.ts: composite-primary comparator; band+mode render; exact ranking: descriptor
- cli.ts: capture persists score via itemToInput (doc-only); add/score paths unchanged
- agents/trend-spotter.md Step 4.5: capture batch carries the Step-2 dimensions
- gate: TRENDS_TESTS_FLOOR 104->146; new unconditional Section 16j; ASSERT floor 94->99

Tests: trends 146/146 (RED two-phase: logic-RED store/brief/cli; stub-first then
assertion-RED score/item). Gate green (Passed 114 / Failed 0; 113 checks >= 99).
Hook suite 139/139 untouched. Counts 27/19/29 unchanged. No new source file/agent/command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 14:05:27 +02:00
4d3b9f4711 docs(linkedin-studio): RE-R3a brief + plan — persist relevance score + rank morning brief on it (light-Voyage hardened)
R3 slice 1 (research-deepening): persist a 4-field TrendScore {mode, dimensions,
composite, priority} on the store record (schema v2->v3, additive lossless), computed
by the already-built score.ts (composite+band, one owner), threaded item->store, and
rank rankForBrief on composite first + surface band+mode in renderBrief.

Go-gate confirmed (operator "Go"): D1 4-field envelope · D2 composite primary within
bucket · D3 first-sight only · D4 one slice · D6 mode shown per body entry.

Light-Voyage: scope-guardian ALIGNED (0) / brief-reviewer PROCEED_WITH_RISKS (6 MINOR)
/ plan-critic REVISE (1 BLOCKER, 4 MAJOR, 4 MINOR) — all folded. Headline fold: the RED
proof is now explicitly two-phase (logic-RED for store/brief/cli; stub-first then
assertion-RED for score/item, since a missing named import throws at module-load under
Node16 ESM, not on assertion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 13:43:02 +02:00
9111f006cc refactor(linkedin-studio): move agent fasit fixtures + tests out of agents/ (v0.5.3) v0.5.3
The 6 long-form-review fasit fixtures (agents/fixtures/*-cases.md) were registered
by Claude Code as junk `fixtures:*` agents because agents/ is scanned recursively.
They are test fixtures, not capabilities. Moved them + their 6 lint tests to
tests/ + tests/fixtures/ and retargeted the 5 agent fasit-refs. Count-neutral
namespace hygiene (ls agents/*.md still 19).

- agents/fixtures/*-cases.md            -> tests/fixtures/        (6)
- agents/__tests__/*-fixture.test.mjs   -> tests/                 (6; ../fixtures -> ./fixtures)
- 5 agent refs ${CLAUDE_PLUGIN_ROOT}/agents/fixtures -> /tests/fixtures
- version-sync 0.5.2 -> 0.5.3 (plugin.json / README badge / CLAUDE.md / CHANGELOG)

Verify: fixture-lint 35/35 at new path; render 20/20, hooks 139/139; test-runner 109/0/0.
Reinstall/reload required for the 6 spurious agents to leave the registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrTb8ktf1XZWEVwgz5MTTo
2026-06-24 13:29:57 +02:00
fa7551070e feat(linkedin-studio): RE-R2b — dated morning-brief artifact + session-start surfacing [skip-docs]
The visible layer of R2. Pure brief.ts: rankForBrief (pillar-overlap -> recency over
the store; publishedAt ?? capturedAt freshness, 7d window; total-order sort), renderBrief
(dated Markdown + hook-surfaceable summary frontmatter), briefSummary (one summary source),
defaultBriefDir (derived from defaultStorePath). CLI `brief` writes
<data>/trends/morning-brief/YYYY-MM-DD.md; session-start surfaces the latest zero-tsx
(latestMorningBrief). Wired into trend-spotter Step 4.6 (scan->capture->brief->surfaced).
No store-schema/scoring change; no scheduler (R3).

25 new trends tests (21 brief.test + 4 cli brief, RED-first) + 3 hook tests (morning-brief
surfacing). trends 104/104 (floor 104), hook-suite 139/139, gate FAIL=0 (ASSERT floor 94,
Section 16i: cli brief-handler + trend-spotter brief-pointer + session-start surfacing
greps), tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 13:12:54 +02:00
88fbbadb1b docs(linkedin-studio): RE-R2b brief + plan — dated morning-brief artifact + session-start surfacing (light-Voyage hardened, go-gate: wire + English heading)
The visible layer of R2 (R2a landed the data layer @ 7a15803). A pure
rankForBrief (pillar-overlap -> recency over the store, publishedAt ?? capturedAt
freshness, 7d window) + renderBrief (dated Markdown with a hook-surfaceable
summary frontmatter) + briefSummary (one summary source) + defaultBriefDir
(derived from defaultStorePath). CLI `brief` writes the dated file; session-start
surfaces the latest, zero-tsx. Wired into trend-spotter (scan->capture->brief->
surfaced). No store-schema change, no scoring change, no scheduler (R3).

Light-Voyage hardened: scope-guardian ALIGNED (0); brief-reviewer
PROCEED_WITH_RISKS; plan-critic REVISE (2 blockers, 5 majors, 4 minors) — all
folded (briefSummary single source, wrong-value RED stubs, --out !== "true"
guard, defaultBriefDir derives from defaultStorePath, two-gate split since the
hook suite is not in test-runner.sh, url-asc total order). Go-gate: WIRE +
English "## Morning Brief" heading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 11:45:20 +02:00
7a158030b6 feat(linkedin-studio): RE-R2a — item→store capture bridge + publishedAt persistence (schema v1→v2, lossless migrate) [skip-docs]
Closes the research-engine capture loop RE-R1 deferred:
- itemToInput(item, capturedAt): pure envelope→TrendInput bridge in item.ts —
  injects capturedAt, carries publishedAt verbatim; no id, no re-validate
- publishedAt persisted: TrendRecord/TrendInput gain it; addTrend conditional-spread,
  first-sight kept on re-capture (no back-fill). SCHEMA_VERSION 1→2 with a lossless
  forward migrate-on-load: Math.max(onDisk, current) + numeric-typeof coercion
  (string/NaN/absent → current; non-array trends coercion preserved verbatim)
- `capture` CLI: stdin raw item|batch → normalize → bridge → addTrend → saveStore once;
  tally {added,duplicates,merged,errors} from AddResult; content-invalid → errors[],
  exit 2 only on bad stdin; --json summary
- wiring: trend-spotter.md Step 4.5 N×`add` → one normalizing `capture` batch; README
  add/capture framing corrected; test-runner Section 16h (capture wiring, unconditional)
  + floors bumped (trends 62→79, ASSERT 87→90)

TDD: 17 new tests (12 genuinely-RED logic-RED + 5 regression guards), tsc clean,
gate 105/0/0. No version bump (additive, v0.5.2 dev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 11:12:50 +02:00
b4e500fad4 docs(linkedin-studio): RE-R2a brief + plan — item→store capture bridge + publishedAt persistence (light-Voyage hardened, go-gate: wire)
R2 ("the visible topic-stream") split into two sequenced slices at the
2026-06-24 go-gate, foundation-first: R2a (this — the pure scripts/trends/
data layer) before R2b (dated morning-brief + session-start surfacing).

R2a builds the item→store bridge RE-R1 deferred: a pure itemToInput mapping
the validated TrendItem envelope to a store input (injecting capturedAt,
carrying publishedAt), a publishedAt schema bump (SCHEMA_VERSION 1→2) with a
lossless forward migrate-on-load, and a `capture` CLI closing the
poll→normalize→store loop. No scoring change, no hook touch.

Go-gate: WIRE (operator) — re-point trend-spotter Step 4.5 add→capture +
Section 16h grep/self-test. Q2 publishedAt-merge = first-sight, no back-fill.

Light-Voyage hardened: scope-guardian ALIGNED, brief-reviewer
PROCEED_WITH_RISKS, plan-critic REVISE — all folded (BLOCKER: v2-idempotence
is GREEN-only not RED; capture tally mapped exactly onto AddResult; "lossless"
scoped to well-formed stores; add --published-at deferred; README framing fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 10:43:31 +02:00
24775f4493 feat(linkedin-studio): RE-R1 — item-schema (B1) + triage-scorer (B2) as tested code behind CLI seam [skip-docs]
Lift the research engine's deterministic core out of agents/trend-spotter.md prose
into pure, tested TypeScript under scripts/trends/, behind a CLI seam the agent calls.

- B1 src/item.ts: TrendItem ingress envelope + normalizeItem/normalizeItems
  (required-field validation, topic normalize+dedupe via store's normalizeField,
  optional publishedAt ISO-validate). No id (store derives it); no store bridge
  (capturedAt injection is R2).
- B2 src/score.ts: per-mode weight consts mirroring the SSOT
  (references/trend-scoring-modes.md), composite (weighted sum, [1,10] guard),
  band (5-band map + exact SSOT action strings), triage (keep>=threshold, rank desc,
  annotate composite+band). Owns ONLY the arithmetic; the five judgment scores stay
  model-side.
- CLI normalize/score: JSON payload on STDIN, JSON to stdout (the existing --json
  output toggle is untouched); exit 2 on bad invocation, 0 otherwise.
- Wire trend-spotter.md to name 'src/cli.ts score' as the deterministic-step owner
  (prose pointer; the agent still supplies the five scores). Domain-general.
- Gate: TRENDS_TESTS_FLOOR 24->62; new unconditional Section 16g (score.ts both-mode
  weight-sets + trend-spotter scorer-pointer + non-vacuity self-test);
  ASSERT_BASELINE_FLOOR 84->87.

TDD: logic-RED proven (33/34 item+score fail on assertions, not module-not-found),
then GREEN (trends suite 62/62); CLI RED 2/4 -> GREEN 4/4. Full gate 102/0/0.
No store-schema change (SCHEMA_VERSION stays 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 10:09:45 +02:00
0e95ca8cce docs(linkedin-studio): RE-R1 brief + plan — lift research-engine item-schema (B1) + triage-scorer (B2) to tested code (light-Voyage hardened)
Research engine lifted to Tier-1 (operator 2026-06-24): the daily workflow
rests on a steady stream of topic suggestions, and research is the only
subsystem whose core logic (poll/score/digest) is ungated prose in
agents/trend-spotter.md rather than tested code.

RE-R1 (rung-2, slice 1): B1 canonical item envelope + normalizer
(scripts/trends/src/item.ts) and B2 deterministic triage-scorer
(score.ts — composite/band/triage, owns only the arithmetic; the five
1-10 dimension scores stay model judgment), plus a stdin/JSON CLI seam,
tests, a trend-spotter prose pointer, and gate-floor bumps. No
store-schema change (SCHEMA_VERSION stays 1). The visible morning-brief
stream (B3 + surfacing) is R2.

Light-Voyage hardened: scope-guardian ALIGNED; plan-critic blocker + 6
majors folded — incl. TrendItem does NOT map directly onto TrendInput
(capturedAt vs publishedAt → store bridge deferred to R2), CLI reads
stdin (no --json overload), gate Section 16g before the anti-erosion
Section 18, ASSERT floor recount-not-pinned, band thresholds + action
strings drift-pinned, wiring grep literal src/cli.ts score.

No code touched yet — awaiting operator go-gate on the plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-24 01:11:06 +02:00
68f6283d8a feat(linkedin-studio): SB-S3e — retire dead content-history + read-side brain reconcile [skip-docs]
The LAST second-brain slice; the S0–S3e arc is now complete.

(b) Retire the dead, zero-reader content-history.md across its 8 plumbing
surfaces: the flaky Stop-hook writer prose, the template (git-rm'd), the
migrate-data.mjs B1 MOVE entry + its test assertions, .gitignore, the gate
SC2_CLASSES guard, the data-path ref-doc, and a session-start comment. SC1
grep = 0; migrate suite 5/5 (R8 idempotency demonstrated).

(c) `brain reconcile` — read-side triple-post reconciliation joining silo 1
(## Recent Posts, auto-tracked creation) to the silo 2↔3 graph, surfacing the
coverage gap: posts created via the plugin but never `brain ingest`-ed. New
pure core scripts/brain/src/reconcile.ts (parseRecentPosts tracks the WRITER
format state-updater.mjs:116, NOT the date-only pruner; reconcileRecentPosts
matches hook→record.body→graph for the in-graph/in-brain-only/orphaned tiers;
loadRecentPosts reads STATE_FILE via the canonical getStateFile() chain — a new
cross-seam read, the state file lives outside the brain dataRoot). Wired as the
`brain reconcile` CLI subcommand (inline parsePublishedRecord loader, not the
body-less listPublished). Read-only: never writes the state silo.

Tests (verified live): gate 99/0/0 (ASSERT floor 82→84; new Section 16f
self-test + CLI grep) · brain 127/127 (floor 114→127, +13 reconcile) · hook
suite 136/136. SC4/SC6 end-to-end run is a real behavioural pass (STATE_FILE
seam read + fallback). Honest limit: read-side cannot reconstruct un-captured
specifics/trends — auto-capture is the flagged follow-up.

Brief/plan: docs/second-brain/{brief,plan}-sb-s3e.md (fbad29d). Go-before-code
gate cleared (operator: retire · read-side · build-now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 22:13:45 +02:00
fbad29d3d4 docs(linkedin-studio): SB-S3e brief + plan — hygiene + triple-post reconciliation (light-Voyage hardened)
The LAST S3 slice: (b) retire the dead, zero-reader content-history.md across
its 8 plumbing surfaces; (c) a read-side `brain reconcile` joining silo 1
(## Recent Posts, auto-tracked creation) to the silo 2↔3 graph — surfacing the
coverage gap (posts created but never `brain ingest`-ed), without writing the
state silo.

Light-Voyage hardened: scope-guardian ALIGNED (0/0/0); brief-reviewer REVISE
(4 FIX) + plan-critic REVISE (3 blockers + 6 major + 4 minor), all folded. The
three blockers were real factual errors against the code, each verified directly
before folding: listPublished() is body-less (use runAssemble's inline
parsePublishedRecord loader); the core signature must be {recentPosts, records,
graph} (PostGraphNode has no body); the parser tracks the WRITER format
(state-updater.mjs:116), not pruneContentHistory's date-only :145 regex. Plus the
getStateFile() HOME-chain seam, logic-RED, single-literal gate decoys, recorded
gate TOTAL 97.

Go-before-code gate pending (forks: retire vs back-fill; read-side vs write-side).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 21:51:14 +02:00
974e8d1b25 feat(linkedin-studio): SB-S3d — operations.md ops centre (strategy-advisor reader) [skip-docs]
Make brain/operations.md a READ tributary — the "operations centre" half of the
second brain. strategy-advisor now reads the dated "who I am now" anchor and
honours the frozen-past-self guard: when a profile.md fact predates or
contradicts the anchor, the anchor deprecates it (advisory/reader-side; NO
consolidate-engine change). Anti-sycophancy is INVERTED vs the profile —
profile facts stay evidence-to-TEST, the user-declared anchor is honoured.

- scaffold.ts operationsSeed(): dated-anchor convention (_As of YYYY-MM-DD:_) +
  Plans/Ideas guidance; existence-skip idempotency preserved (no-clobber green).
- strategy-advisor.md: operations.md in Step 0 context-load + a consumption
  contract (anchor authoritative, inversion, plans-vs-ideas, graceful absence).
- test-runner.sh Section 16e: 2 unconditional checks (non-vacuity self-test +
  wiring grep, sibling-file decoy); ASSERT_BASELINE_FLOOR 80->82; BRAIN floor 113->114.
- architecture.md:80: build-row reconciled (S3d done; S3e = dead content-history
  retirement + triple-post reconciliation = the new LAST S3 slice).

Gate 97/0/0; brain 114/114. TDD: RED proven (16e Check B + the scaffold
dated-anchor test both failed pre-fix) before GREEN. Splits the old S3d charter;
the hygiene half is deferred to SB-S3e. Light-Voyage hardened (scope-guardian
ALIGNED, brief-reviewer + plan-critic folds in docs 0061bf2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 21:28:26 +02:00
0061bf2bb5 docs(linkedin-studio): SB-S3d brief + plan — ops centre (operations.md reader, light-Voyage hardened)
Splits the old S3d charter: S3d = ops centre ONLY (operations.md becomes a
read tributary; strategy-advisor honours the dated frozen-past-self anchor
that deprecates older inferences — advisory/reader-side). The hygiene +
triple-post reconciliation half (dead content-history.md retirement +
post-tracking↔published↔analytics) moves to a new SB-S3e (the last S3 slice).

Light-Voyage hardening: scope-guardian ALIGNED (0 creep/gap); brief-reviewer
REVISE (5 FIX folded); plan-critic 3 major + 3 minor folded. Baselines
verified live: gate 95→97, brain 113→114, ASSERT floor 80→82. The RED-bearing
brain-test literal is pinned to the dated-anchor convention (_As of YYYY-MM-DD:_,
verified absent) — NOT 'deprecates older inferences', which already ships in
the seed comment and would make the RED gate vacuous.

Go-before-code gate pending (forks: Option B enrich seed · advisory deprecation
· one reader · doc home).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 21:13:50 +02:00
edd3e15ef7 feat(linkedin-studio): SB-S3c — cross-silo id-threading + post→analytics assembler [skip-docs]
Hub-side design: the published record now carries the specifics/trends ids
it was built from (additive, omit-empty → byte-backward-compatible), and a
new pure assembler (scripts/brain/src/assemble.ts + `brain assemble`) joins
post↔analytics by normalized title-prefix + date with honest confidence
tiers (high/low/none). Answers the arc's north-star query: which raw
material actually performs? (specific → post → measured analytics).

All four tributaries untouched (analytics READ-only via inlined raw-JSON,
no package import); profile.md grammar untouched (the fact→post link stays
OUT — C-1). The repeatable --specific/--trend ingest flags collect via a
new collectRepeated helper, leaving parseFlags untouched.

TDD: 19 new brain tests (ingest 4 + publish 3 + assemble 8 + cli 4), all
SC1–SC12. brain 113/113, gate 95/0/0, BRAIN_TESTS_FLOOR 94→113,
ASSERT_BASELINE_FLOOR unchanged at 80. Light-Voyage hardened
(brief-review 5 FIX · plan-critic 1 BLOCK+4 MAJOR+4 MINOR · scope-guardian ALIGNED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 20:47:34 +02:00
016d823f3b docs(linkedin-studio): SB-S3c brief + plan — cross-silo id-threading (light-Voyage hardened)
Hub-side design: thread specifics/trends ids onto the brain's published
record + a pure analytics resolver (title-prefix + date, confidence tiers).
Tributaries untouched; blast radius bounded to scripts/brain/. Answers the
north-star query "which raw material performs?" (specific -> post -> analytics).

Two hard constraints shaped the slice: profile.md's rigid 6-token grammar
keeps the fact->post link OUT (C-1, breaking SCHEMA_VERSION bump); analytics
has no body/URN so the analytics<->post join is a resolver, not a stored id (C-2).

Light-Voyage: brief-review APPROVE-WITH-FIXES (5 FIX), plan-critic
REVISE -> 1 BLOCK + 4 MAJOR + 4 MINOR folded, scope-guardian ALIGNED.
Awaiting operator go-before-code gate before any TDD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 20:34:38 +02:00
585f972a05 feat(linkedin-studio): SB-S3b — supersede arm in the consolidation engine [skip-docs]
The engine can now RETIRE a fact via an operator-gated, explicitly-signalled
temporal-update supersede — completing the consolidation motor's one S2 TODO
("no supersede in S2 — that's S3"). A `supersedes` signal on a candidate retires
the stale fact (re-minted to an archival id, status: superseded, REPLACED IN
PLACE, retained as audit) and installs the new winner under the canonical key-id,
so mintEntityId(key) always points at the live fact.

- consolidate.ts: Candidate.supersedes? + ProfileDiff.supersedes (SupersedeOp
  carries the full winner fact, so applyDiff stays a pure projector). proposeDiff
  value-guarded routing fork (routes only when the target holds a DIFFERENT value —
  else a re-sent signal would self-supersede every run) + intra-batch
  first-supersede-wins guard. applyDiff replace-in-place + value-matched state-check
  (oldId present, active, value===oldValue) → idempotent re-apply + stale-diff safe;
  superseded facts never bumped/promoted (supersede wins). Decay excludes superseded.
  archivalId seeded with the pre-archival id (collision-free).
- cli.ts: renderDiffMd `## Supersessions (old → new)` (rendered last, only when
  present → zero-supersession diffs stay byte-identical); validateCandidates optional
  single-line `supersedes`; `--gather` profileFacts filtered to active (superseded
  archival facts never re-presented as live context).
- Tests: +12 brain (consolidate 10 + consolidate-cli 2). TDD: RED (6 fail) → GREEN
  (94/94). BRAIN_TESTS_FLOOR 82->94; ASSERT_BASELINE_FLOOR unchanged at 80 (no new
  test-runner.sh section). Gate 95/0/0.
- Docs: consolidation-loop.md rule table + honest-limit reconciled (the operator
  gate is the only classification net); engine docstring updated.

All 13 success criteria deterministically tested (unlike S3a, no behavioural-only
SC). READ-only gate unchanged — brain consolidate --apply --confirm stays the sole
profile.md writer. Scope held: scripts/brain/ only; temporal-update only
(condition-dependent/distractor deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 19:58:09 +02:00
d2ec7482be docs(linkedin-studio): SB-S3b brief + plan — supersede arm (light-Voyage hardened)
Second sub-slice of SB-S3 (after S3a's reader landed). Adds an operator-gated,
explicitly-signalled supersede op to the consolidation engine: a temporal-update
candidate retires a stale fact (status: superseded, re-minted to an archival id,
replaced in place, retained as audit) and installs the new winner under the
canonical key-id. Bounded to scripts/brain/ — pure, deterministic, fully
unit-testable.

Operator-confirmed scope: temporal-update→supersede ONLY · winner→dynamic
(re-earn) · minimal `supersedes?: string` · retain superseded as audit.

Light-Voyage-hardened:
- brief-reviewer APPROVE-WITH-FIXES — 8 FIX folded (the load-bearing one: id
  lifecycle → winner-takes-key-id, retired fact re-minted, so mintEntityId(key)
  always points at the live fact).
- plan-critic REVISE → 2 BLOCK + 6 FIX folded: applyDiff must REPLACE-IN-PLACE
  (not copy → would dup the canonical key-id); proposeDiff must value-guard the
  supersede fork (a re-sent stale signal would self-supersede every run); plus
  --gather active-only filter + decay excludes superseded (else retired facts
  leak back as live context / spam stale flags).
- scope-guardian ALIGNED (0 creep / 0 gap).

Also reconciles brief-sb-s3a.md's stale DRAFT status (S3a landed 4fa411f).
No code yet — awaiting go-before-code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 18:51:25 +02:00
4fa411f13c feat(linkedin-studio): SB-S3a — wire strategy-advisor as first brain/profile.md reader [skip-docs]
The second brain now feeds generation: strategy-advisor reads brain/profile.md
as evidence-to-test (anti-sycophancy default, graceful absence on fresh installs).
First end-to-end proof of capture → consolidate → read-back-into-generation.

- agents/strategy-advisor.md: brain/profile.md added to Step 0 Load Context + a
  consumption subsection (Static/Dynamic layers, evidence_count/last_seen weighting,
  anti-sycophancy counter-pressure, silent degrade when the file is absent).
- scripts/test-runner.sh: new Section 16d (Brain Profile Reader) — 2 UNCONDITIONAL
  checks (non-vacuity self-test with legacy-path/lowercase decoy + exact-literal
  wiring grep on strategy-advisor.md), ASSERT_BASELINE_FLOOR 78→80, header
  enumeration extended. Gate 93→95/0/0; brain floor 82 untouched.
- docs/second-brain/consolidation-loop.md: reconciled the stale "no reader yet"
  honest-limit to the true partial state (one reader; broader consumption deferred).

TDD: RED (only the wiring grep failed, exit 1) → GREEN (95/0/0). READ-only — the
brain consolidate --apply --confirm gate stays the sole writer. SC4 (graceful
absence) verified by inspection; SC5 (read-back) deferred to a reloaded session
(honest — agent prompts load at session start). S3a scope held: one reader, no
parser, no id-threading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 18:23:04 +02:00
fbfbf71cdd docs(linkedin-studio): SB-S3a brief + plan — first profile.md reader (light-Voyage hardened)
SB-S3 decomposed into 4 sub-slices; operator picked S3a (profile.md reader)
first: wire strategy-advisor to consume brain/profile.md as evidence-to-test,
READ-only, guarded by a deterministic wiring lint (new test-runner.sh
Section 16d + ASSERT_BASELINE_FLOOR 78->80). Light-Voyage hardened:
brief-review APPROVE, scope-guardian ALIGNED, plan-critic REVISE (all 3 FIX
folded — incl. consolidation-loop.md:67 reconciliation, since no lint guards
docs/). No code yet — parked at the final go-before-code gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 18:09:54 +02:00
41fa9a5fa5 chore(linkedin-studio): release v0.5.2 — SB-S2 evolution loop v0.5.2
Bump version declarations 0.5.1 → 0.5.2 (plugin.json SSOT + README badge +
CLAUDE.md header + CHANGELOG [0.5.2]) for SB-S2. Flip the architecture SB-S2 row
to landed (+ top status: S0+S1+S2 landed, 82 tests). Additive minor: count-neutral
(no command/agent/ref/skill); voice-trainer unchanged. Gate 93/0/0, hook suite
136/0. Polyrepo release (tag v0.5.2 + catalog ref) follows (operator push-OK for today).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 17:11:38 +02:00
f91ffddc8c chore(linkedin-studio): SB-S2 gate brain floor 63→82 + consolidation-loop doc
Bump BRAIN_TESTS_FLOOR to 82 (SB-S2 adds consolidate(12)+consolidate-cli(7)).
No new test-runner section → ASSERT_BASELINE_FLOOR unchanged at 78 (the hook
SC6 test runs separately via `node --test hooks/scripts/__tests__/*.test.mjs`,
not the structure gate). Add docs/second-brain/consolidation-loop.md (CLI usage,
engine rules, the candidate-file session↔engine contract, operator gate, honest
limits incl. no-reader-until-S3). Gate 93/0/0; hook suite 136/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 17:09:48 +02:00
2d01dfca52 feat(linkedin-studio): SB-S2 session-start scaffold-ensure + consolidation nudge [skip-docs]
session-start.mjs (zero-dep, Edit): unconditional brain/+ingest/ scaffold-ensure
(mkdir, runs on the fresh-install path too); a consolidation-due nudge in the
reminders block (counts published records + reads consolidation-state.json last_run
via getDataRoot — same root the CLI --apply writes; no profile.md parse, \n idiom,
null-safe never-nags); a brain-init nudge appended AFTER the if/else so the
fresh-install branch's context reassignment can't clobber it. + readdirSync import.
SC6 hook test (5 cases) via heredoc. Hook suite 131→136/0; no recompile (body-only edit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 17:07:48 +02:00
88356b8a83 feat(linkedin-studio): SB-S2 brain consolidate CLI — gather/propose/apply [skip-docs]
Operator-invoked, operator-gated loop:
- --gather reads published bodies directly (parsePublishedRecord, filtered by
  published_date > last_run) + current profile, for the session to extract candidates.
- --propose validates candidates (shape + single-line key/value, else non-zero/no-write),
  proposeDiff, writes brain/pending-diff.{json,md}; never touches profile.md.
- --apply --diff <json> --confirm is the ONLY path that writes profile.md (refuses
  without --confirm), then records brain/consolidation-state.json last_run.
init/ingest/published preserved. 7 subprocess CLI tests (SC5). brain 75→82, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 17:03:34 +02:00
ff39d14206 feat(linkedin-studio): SB-S2 consolidation engine — proposeDiff/applyDiff [skip-docs]
Pure deterministic engine over the two-layer profile: add / reject-ai-draft /
evidence-bump / promote-at-N(3) / conflict-keep-both / decay-flag(90d). No-duplicate-id
guarantee: primary id = mintEntityId(observed,key), conflict-alt id =
mintContentId(observed-alt🔑:value::date) — byte-distinct. Folded profile-field
seeds immutable (different kind); static facts decay-exempt; no supersede (S3).
applyDiff produces the next ProfileDoc that round-trips through parse/serialize;
re-running is idempotent (bump, not duplicate). + consolidation-state.json sidecar IO.
12 engine tests (SC1a–g, SC2, SC3, SC4). brain 63→75, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 17:00:56 +02:00
204665e90b docs(linkedin-studio): second-brain SB-S2 brief + plan (review-hardened)
Slice SB-S2 (Evolution loop): operator-invoked consolidation engine → profile
diff (threshold-promotion N=3, conflict keep-both with distinct ids, decay-flag
90d, provenance-gated) + brain/consolidation-state.json sidecar + zero-dep
session-start consolidation-due nudge + scaffold-ensure. Light-Voyage-hardened:
brief-reviewer (REVISE→folded: state-file→sidecar fix), plan-critic (REPLAN
58→folded: distinct-id model, gather reads bodies, hook-tests-run-separately),
scope-guardian (ALIGNED). Operator scope: journal deferred · deterministic CLI
no-agent · motor-only no-reader. Defaults N=3/90d, version → 0.5.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 16:57:17 +02:00
be9c619d9d chore(linkedin-studio): release v0.5.1 — SB-S1 ingest + gold signal v0.5.1
Bump version declarations 0.5.0 → 0.5.1 (plugin.json SSOT + README badge +
CLAUDE.md header + CHANGELOG [0.5.1] entry) for SB-S1. Flip the architecture
SB-S1 row to landed (+ top status). Additive minor: no command/agent/ref/skill
count change; voice-trainer's learning behaviour changes (published-only).
Gate 93/0/0. Polyrepo release (tag v0.5.1 + catalog ref bump) pending push window
+ operator confirm (public open/ remote).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 15:06:16 +02:00
ac532f9218 feat(linkedin-studio): SB-S1 wire voice-trainer to published-only + gate-enforce it [skip-docs]
voice-trainer's Gather step now reads ingest/published/ (provenance=published) as
the primary gold source, keeps voice-samples as a human tributary (not reshaped),
forbids learning from provenance=ai-draft, and fences the auto-append trap. New gate
Section 16c (Brain Published-Only Invariant) enforces the wiring with exact-literal
greps (grep -F 'ingest/published' + 'provenance=ai-draft') + a non-vacuity self-test
(rejects an 'AI-generated'-only probe), against voice-trainer.md + the new contract
doc docs/second-brain/ingest-manual-import.md. BRAIN_TESTS_FLOOR 34→63; assertion
floor 75→78 (+3 unconditional checks; not pinned to deps-present TOTAL to preserve the
warn-skip margin). Gate 90→93/0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 15:02:32 +02:00
b65eb25328 feat(linkedin-studio): SB-S1 brain CLI — ingest + published list (init preserved) [skip-docs]
Add flag-routing infra (parseFlags) + two subcommands to the brain CLI, keeping
the existing `init` branch intact:
- `ingest --file <path> [--source] [--date]` / `ingest --scan-inbox` → capture
  published posts into ingest/published/ (Wrote / Duplicate / Collision report).
- `published list [--json]` → inspect the gold corpus (id · provenance · date · first line).
6 subprocess tests incl. an explicit `init` regression guard. brain 57→63 tests, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 14:56:45 +02:00
0d3e4911d7 feat(linkedin-studio): SB-S1 ingest IO — writePublished/ingestText/scanInbox/listPublished [skip-docs]
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
2026-06-23 14:54:31 +02:00
3e3990f36e feat(linkedin-studio): SB-S1 ingest data layer — record grammar + content-id [skip-docs]
PublishedRecord file-per-post grammar (fixed 5-line header + --- sentinel +
verbatim body, no YAML) with parse∘serialize identity, and mintContentId =
sha256(VERBATIM body)[:12] — byte-identity dedup so two structurally-different
posts never collide (avoids the normalizeContent silent-data-loss path).
parsePublishedRecord rejects a published/ record whose provenance != published
(corruption signal). 10 grammar tests incl. the B1 edge battery (empty body,
body starting with ---, mid-text \n---\n, header-shaped first line) + 4 id tests.
Pure layer only; IO + CLI + voice-trainer wiring follow. brain 34→48 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 14:51:40 +02:00
f549d9fdf3 docs(linkedin-studio): second-brain SB-S1 brief + plan (review-hardened)
Slice SB-S1 (Ingest + gold signal): manual import → ingest/published/ with
provenance=published; wire voice-trainer to learn from published-only (the
model-collapse guard). Brief + plan hardened through light-Voyage:
brief-reviewer (PROCEED_WITH_RISKS), plan-critic (REVISE 63→folded),
scope-guardian (GAP→folded). Key decisions: verbatim-body content hash (no
normalizeContent, avoids silent data loss), one pinned record separator with a
round-trip edge battery, gate-enforced published-only lint, S1/S2 boundary held
(no profile.md mutation in S1). Operator-approved scope; version bump to 0.5.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 14:51:29 +02:00
8c927198f5 feat(linkedin-studio): second-brain SB-S0 foundation — brain/ scaffold + profile fold + id/provenance spine [skip-docs]
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
2026-06-23 14:05:24 +02:00
d6acb5d36c docs(linkedin-studio): second-brain SB-S0 brief + plan (review-hardened)
Light-Voyage process artifacts for the first second-brain slice (SB-S0):

- brief.md — task brief, revised after adversarial brief-review (B1 fold-source
  premise, B2 pathguard claim [later verified: STATE was right, reviewer wrong],
  M1 floors-as->=, M2 scaffold trigger, M3 grammar contract, M4 runtime-only).
- plan-sb-s0.md — SB-S0 implementation plan, revised after adversarial
  plan-review (plan-critic + scope-guardian, both code-verified):
  B1 test convention -> real TS idiom (tests/*.test.ts via tsx, .js specifiers);
  M2 profile format -> defined line-grammar (repo has no YAML parser, no dep);
  M3 data-root -> inline per-package resolver (repo idiom, not a new seam);
  M4 fold extraction rule pinned against the actual user-profile template;
  + minors (gate-count rationale, npm-install-before-gate, provenance throws,
  stable-slug id, push-confirm).

scope-guardian: zero creep, zero MAJOR gap. plan-critic: B1+3 MAJOR, all folded.
Design phase, no code yet — awaiting operator go to build SB-S0 (TDD).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 13:24:18 +02:00
d3199eb997 docs(linkedin-studio): second-brain architecture (approved) + 3 research reports
Persist the operator-approved second-brain architecture design and the three
parallel research threads that fed it, so nothing is lost before build:

- architecture.md — approved design: thin Markdown `brain/` hub (two-layer
  profile.md + episodic journal/ + operations.md + index.md MOC) over the
  existing typed tributaries (voice/specifics/trends/analytics), a
  provenance-tagged ingest/ seam, a sleep-time consolidation loop with
  evidence-threshold promotion + temporal-validity + anti-sycophancy/
  anti-collapse invariants. Build sequence SB-S0..S4.
- research/connector-egress.md — LinkedIn data egress reality (EU/EEA DMA
  portability API = auto for content; analytics manual CSV; no scraping).
- research/secondbrain-sota.md — 2026 second-brain / AI-memory SOTA synthesis.
- research/silo-inventory.md — faithful inventory of the 12 existing per-user
  silos + the 5 hardest unification problems.

Boundary confirmed: engine -> plugin (domain-general), user data -> data dir,
cockpit -> Maskinrommet. Design phase, no code yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 12:55:17 +02:00
0907b2b92d feat(linkedin-studio): trend-store staleness nudge — SessionStart B-S3 [skip-docs]
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
2026-06-23 12:23:53 +02:00
9f9c4bbd86 feat(linkedin-studio): extend §17 de-niche guard to content-planner + content-framework (B-S2 follow-up) [skip-docs]
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
2026-06-23 12:02:01 +02:00
e75cd42bed feat(linkedin-studio): de-niche rest-sweep — vary KTG-beat examples across surfaces (B-S2b) [skip-docs]
The last de-niche slice: recast the 10 sites where the vendor/sector beat
(Microsoft|Azure|Copilot|public sector) sat as the PRIVILEGED/default example,
varying each to a concrete cross-domain example instead of sterilizing
(plugin-is-domain-general — domain comes from user config, never hardcoded).

Recast (10): url-processing-templates (news worked-example Copilot->Figma),
opportunity-generation (3 headline examples + About block -> varied/ops persona),
profile (3 "good example" headlines/impact -> healthcare/e-commerce/support),
first-comment-strategy (drop "Microsoft" from research-paper example),
poll-strategy-guide (Copilot option -> generic AI assistants),
engagement-frameworks (1 of 3 direct-address audiences -> RevOps/SaaS),
setup (audience e.g. -> two varied examples), post (invocation e.g. -> SaaS pricing),
network-builder (tagline example -> ops/manufacturing),
video-scripter (2 filename slugs -> neutral topics).

Kept as false positives (would sterilize): content-angles.md (Public Sector is
1 of 6 balanced industry tables + Industry-Agnostic section), outreach.md
(Microsoft Build/Ignite/Azure UG = 3 of ~20 varied real conferences),
linkedin-growth-playbook (biographical fact in a real case study), the
Gemini/Tavily/Perplexity MCP tool-name examples, and the algorithm-signals
"Gemini provenance" SSOT citation. AI-as-topic kept (not a niche token; the
de-AI/AI-slop mechanic is the plugin's legit subject).

Gate scripts/test-runner.sh 87/0/0 (no lint touches these files yet; §17-guard
extension to content-planner is the deferred next step). 10 files, 26/26.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY
2026-06-23 10:50:28 +02:00
94d4e707db feat(linkedin-studio): de-niche content framework + planner calendar — recast/rename to content-framework.md (B-S2a) [skip-docs]
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
2026-06-23 10:02:26 +02:00
05a22d2cc7 feat(linkedin-studio): de-niche trend-spotter — pillar-driven, no hardcoded beat (B-S1) [skip-docs]
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
2026-06-22 20:32:05 +02:00
be12fb8b63 feat(linkedin-studio): wire trend-spotter into the research engine — store + MCP routing + scoring SSOT (§5 slice 2b) [skip-docs]
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
2026-06-22 14:51:50 +02:00
b89868e3b1 feat(linkedin-studio): research-engine config layer — sources + scoring modes + MCP profile (§5 slice 2a) [skip-docs]
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
2026-06-22 13:36:28 +02:00
be21788321 feat(linkedin-studio): trends store — research-engine inventory (§5 slice 1)
[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>
2026-06-21 19:08:21 +02:00
527391ab47 feat(linkedin-studio): wire lived-specifics extraction into newsletter pipeline (fix #2 slice 3) v0.5.0
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>
2026-06-20 22:29:01 +02:00
33f18c35e6 feat(linkedin-studio): per-edition lived-specifics binding + kilder artifact (fix #2 slice 2)
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>
2026-06-20 21:33:44 +02:00
b390b87ad5 feat(linkedin-studio): specifics-bank store — lived-specifics inventory (fix #2 slice 1)
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>
2026-06-20 21:03:18 +02:00
e26d9fb659 feat(linkedin-studio): wire contract-gate + ratify into newsletter Step 4.5 (fix #1 slice 3)
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>
2026-06-20 20:34:49 +02:00
ff3467de79 feat(linkedin-studio): accumulation binding — ratify + §E foldIns (fix #1 slice 2)
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>
2026-06-20 17:48:20 +02:00
58aebf05a9 feat(linkedin-studio): deterministic §B/§C1 contract binding-gate (fix #1 slice 1)
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.
2026-06-20 17:17:51 +02:00