Commit graph

54 commits

Author SHA1 Message Date
9e95222d12 feat(linkedin-studio): brain emits OKF-compatible form — Stage 1 (bundle=brain/, ingest/ excluded) [skip-docs]
Cross-plugin OKF convergence, Stage 1 (docs/okf-convergence-brief.md): the
second-brain hub now conforms to OKF-compatible form so a shared retrieval
skill (and a sibling agent) can traverse it.

- serializeProfile leads with a constant `type: Profile` frontmatter block
  (round-trip-safe: parseProfile skips it, parse-serialize identity holds).
- operations.md seed -> `type: Operations`; index.md seed -> `okf_version: 0.1`
  marker (markdown text; index files carry no frontmatter per OKF); new
  brain/journal/index.md (per-level index).

Premise correction: the brain is deliberately YAML-free and ingest/published
has a byte-exact round-trip invariant (SC2) a frontmatter block would break,
so the concept-bundle is scoped to brain/ ONLY; the ingest/ tributary is
excluded and pointed to from the hub. We emit frontmatter, add no parser.

Verified: new tests/okf-conform.test.ts (5/5); full brain suite 132/132 (0
regressions); cross-tool — okr/scripts/okf-check.mjs validates brain/ (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 20:52:41 +02:00
5b51b4baeb feat(linkedin-studio): RE-R3e — brief history + day-over-day diff (surfaced: frontmatter + Nytt siden sist) [skip-docs]
Closes hull #7 ("ingen brief-historikk"). Each morning brief now records the
trend ids it showed into its own YAML frontmatter (surfaced: <id-csv> =
surfacedIds(ranking)) and renders a day-over-day diff against the most recent
prior brief — a "## Nytt siden sist (<prior-date>)" section that leads the
ranked list, plus a " N nye siden sist." marker on the one-line summary the
SessionStart hook surfaces (no hook change).

- brief.ts: BRIEF_SCHEMA_VERSION 1->2 (artifact frontmatter gained surfaced:;
  the store's SCHEMA_VERSION stays 4 — no store field). Three PURE helpers
  (diffSurfaced / parseSurfacedFrontmatter / selectPriorBriefFile) + the
  surfaced: emit + the section + the summary marker. No fs/clock in brief.ts.
- cli.ts: the brief handler discovers the prior dated file (existsSync-guarded
  readdirSync -> selectPriorBriefFile, strict < today so a same-day re-run is
  byte-identical), parses its surfaced: line, computes the diff, threads it into
  renderBrief AND the shared briefSummary(ranking, diff) (one-source: file
  frontmatter == --json summary, cli.test one-source invariant). --json gains a
  diff:{priorDate,added,carried,dropped} counts object; the console line appends
  the delta. Any fs error degrades to the empty-prior (first-brief) path.

TDD two-phase: stubs -> 17 value-RED (no module-not-found) -> GREEN. Trends suite
216 -> 245 (brief +27, cli +2), 0 fail. New unconditional gate Section 16n (6
checks); ASSERT_BASELINE_FLOOR 117 -> 123; TRENDS_TESTS_FLOOR -> 245. Full gate
FAIL=0; hook suite 139/139 + R3c schedule/run-daily green untouched. Behavioural:
real two-day rename-real-write diff + same-day byte-identity confirmed. Counts
29/19/27 unchanged; 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_011vmzxpsFpc8q19LaogAWLD
2026-06-26 14:40:09 +02:00
2a8459c674 feat(linkedin-studio): RE-R3d — temporal overlay (first-mover + saturation) [skip-docs]
R3 slice (b): the rest of hull #3. The morning brief now reads the temporal axis
the R3b seen-log records but the ranking ignored. Two DERIVED signals, computed at
brief time from already-persisted fields (publishedAt/capturedAt -> ageDays,
surfacedCount), never stored:

- first-mover: recent (ageDays <= --first-mover-days, default 2) AND never surfaced
  on a prior day -> ranked up, badge "first ute". Future-dated (ageDays<0) excluded.
- saturation: surfaced on >= --saturation-at (default 3) prior days -> ranked down,
  badge "mettet (Nx)". Self-surfacing (our seen-log), not market coverage.
- warming (1..at-1) keeps the R3b "sett Nx" badge but only at >=2 (contract intact);
  neutral carries no badge.

SB1 derived (no schema bump: SCHEMA_VERSION 4 / BRIEF_SCHEMA_VERSION 1 untouched).
SB2 the R3a relevance composite stays the PRIMARY sort key; the temporal rank is a
new cmp key after pillar-overlap, before effectiveDate -> re-orders only WITHIN a
(composite, overlap) tier. temporalSignal is pure (saturationAt clamped >=1).

Prior-day surfacings exclude today (via lastSurfacedAt), so a same-day re-render is
byte-identical (caught by the R3c run-daily SC7 regression; fixes a latent R3b
prior-day imprecision too). brief CLI gains --first-mover-days / --saturation-at;
schedule untouched (nightly uses defaults).

Wiring: trend-spotter.md (prose), trend-scoring-modes.md (one-line consumer note),
README (## Temporal overlay), gate Section 16m (+6 unconditional -> ASSERT floor
111->117), TRENDS_TESTS_FLOOR 192->216. Counts 29/19/27 unchanged. Zero new files.

Gate: Passed 132 / Failed 0; trends 216/216; hook suite 139/139 untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 12:10:42 +02:00
3276e44dbf feat(linkedin-studio): RE-R3c — autonomous trigger (scheduler + headless entry) [skip-docs]
Closes research-engine hulls (1) no autonomous trigger + (6) no headless entry.
Makes the daily research loop closed + headless: deterministic-brief-only (C1),
print-first (C2 — the tool never runs launchctl or the cron table; --install writes
only the inert launchd plist file).

- NEW scripts/trends/src/schedule.ts — pure string emitters (launchd plist + cron-line +
  install/uninstall instructions + defaultLabel). No clock/fs/env/AI; byte-deterministic.
- NEW scripts/trends/run-daily.sh — bash-3.2 headless wrapper: resolves node, cd's into the
  package so tsx resolves, logs via the data-path twin seam; runs the deterministic brief and
  appends one compact cron.log line per fire. The (e) AI-capture seam is documented, not built.
- EDIT cli.ts — schedule --pillars <a,b> [--at HH:MM] [--fresh-days N]
  [--platform auto|launchd|cron] [--install|--uninstall] [--store <p>]; print-first, no new
  exit code; logPath anchored to dirname(defaultStorePath()) (not the --store override).
- WIRE trend-spotter.md (one prose line) + README (scheduler + wrapper + the C1 boundary).
- Gate: TRENDS_TESTS_FLOOR 171->192, ASSERT_BASELINE_FLOOR 105->111, new UNCONDITIONAL
  Section 16l (6 deps-absent greps + non-vacuity self-test), header-enum + floor-history append.

TDD two-phase RED -> GREEN. trends 192/192, gate 126/0, hook-suite 139/0 (untouched), plutil
-lint OK. No schema change (SCHEMA_VERSION 4 / BRIEF_SCHEMA_VERSION 1). Counts 29/19/27 unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD
2026-06-26 11:00:59 +02:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
8275159aad test(linkedin-studio): M0-16 — lint: EXPECT_REFS=26 + no-bare-path + no-pin + SC2 dry-run (R1) + drop 5 ANALYTICS_ROOT pins it caught 2026-06-18 13:11:50 +02:00
760d4c9101 refactor(linkedin-studio): M0-2 — storage.ts getDataRoot + external default (ANALYTICS_ROOT alias kept) 2026-06-18 11:12:11 +02:00
69fec21f43 feat(linkedin-studio): S16 — optional manual saves in analytics + close deferred onboarding Write MAJOR
Lifts the original v4.0.0 Non-Goal: an optional, manually-entered `saves`
metric through the analytics layer, built location-agnostic (option c) so
UI-brief §9b/M0 relocates the data dir in one place later.

- types: PostMetrics.saves? + Weekly/Monthly summary.totalSaves? (optional);
  new RankableMetric type for the always-numeric index-access whitelist
- parser: dedicated parseOptionalCount() — blank/non-numeric/negative -> undefined
  ("unknown != 0"), genuine 0 kept; saves NOT folded into engagementRate
- reports: totalSaves set only when >=1 post carries saves (backward-compat)
- cli: saves surfaced in import summary + weekly/monthly totals + per-post
- S16-pre: onboarding.md allowed-tools gains Write (closes S15-deferred MAJOR)
- docs (three-doc rule): plugin README boundary + analytics README + root README
  + plugin CLAUDE.md + CHANGELOG; dwell stays explicitly unmeasurable

Independent /trekreview: brief-conformance 0 findings; code-correctness 2 MAJOR
(own lockstep misses) FIXED in-session (parseOptionalCount + edge tests). Gate:
tsc clean, analytics 116/116, lint 74/0/0, hooks 98/98. Within-v4.1.0 refinement
(no surface/count/version change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 22:23:12 +02:00
52df229dfc feat(linkedin-studio): S14 — journey layer (create/measure front-doors + 5-journey router), v4.1.0
14a's cold command-rationalization found ZERO redundancy across the 27 commands
(no defensible merge/cut), so the operator reframed S14 from "merge/cut" to
"add a journey layer over the kept atomics".

- Add /linkedin:create + /linkedin:measure — delegate-only guided front-doors
  (Read/Glob/AskUserQuestion only; route to the command that owns the work)
- Re-tier commands/linkedin.md into 5 journeys (Start/Create/Engage/Measure/Grow);
  onboarding/strategy elevated as Start/Grow front-doors; Engage = calendar+firsthour tier
- 14a honesty nits: router now lists firsthour; calendar cross-links to firsthour;
  competitive confirmed UNGATED (the claimed 1K-gating inconsistency was unfounded)
- Lockstep: EXPECT_COMMANDS 27->29, v4.0.0->4.1.0 across plugin.json / README badges /
  plugin+root CLAUDE.md / README / CHANGELOG; new README commands-badge lint guard
- 14a deliverable corrected: multiplatform entry added (26/27 -> 27/27), header counts,
  competitive nit withdrawn
- Independent /trekreview (2 Opus reviewers) raised 3 MAJORs (stale commands badge,
  router competitive self-contradiction, STATE.md count) — ALL remediated in-session;
  verdict ALLOW

Gate: test-runner.sh 74/0/0; node --test 98/98; commands=29; v4.1.0 consistent.
Additive/minor — no command removed/renamed/behavior-changed; reload registers create+measure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:27:06 +02:00
3383abd3b6 fix(linkedin-studio): S13 — close S12 WARN ($-scalar + false-green test) + $-safety lint guard
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.
2026-05-30 19:12:45 +02:00
90cf98d287 fix(linkedin-studio): S12 — close S11 re-review + render-chain-propagation lint guard
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>
2026-05-30 15:42:25 +02:00
be8c9b01fb fix(linkedin-studio): S11 — model-name declaration drift + model-consistency lint guard
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>
2026-05-30 15:05:37 +02:00
bfdae744f9 fix(linkedin-studio): S10 — generalize stale-stat lint to the pattern class + permanent non-vacuity self-test
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>
2026-05-30 13:48:37 +02:00
0e4466b893 fix(linkedin-studio): S9 — full algorithm-magnitude sweep + lint rebuilt to the criterion
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>
2026-05-30 09:56:49 +02:00
b48e1305e6 fix(linkedin-studio): close v4.0.0 audit review findings (S8)
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>
2026-05-30 09:27:15 +02:00
cc4a81f583 chore(linkedin-studio): release v4.0.0 — counts, three-doc, CHANGELOG (clears [skip-docs] debt)
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>
2026-05-30 07:49:48 +02:00
b0e8c7b19e feat(linkedin-studio): first-hour/reply-loop command with tracked state
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>
2026-05-30 05:35:44 +02:00
d2e1f038c6 fix(linkedin-studio): honest saves/dwell wording, no false tracking claim
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>
2026-05-30 00:28:23 +02:00
26cd1c1d6d fix(linkedin-studio): anchor analytics root on plugin marker + surface npm install
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>
2026-05-29 21:18:33 +02:00
3dd8f40c31 fix(linkedin-studio): propagate reconciled algorithm numbers, cite-not-restate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:32:17 +02:00