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
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 17:07:48 +02:00
commit 2d01dfca52
2 changed files with 136 additions and 1 deletions

View file

@ -2,7 +2,7 @@
// SessionStart hook for linkedin-studio plugin
// Reads persistent state and session context, outputs JSON with additionalContext
import { readFileSync, existsSync, copyFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { readFileSync, existsSync, copyFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { calculateScore } from './personalization-score.mjs';
@ -51,6 +51,28 @@ function trendsNewestCapture(storePath) {
}
}
// SB-S2: brain consolidation freshness. Both read zero-dep through getDataRoot —
// the SAME data root the brain CLI's --apply writes to (sidecar reachable by both).
// No tsx, no profile.md parse: a readdir count + a tiny JSON read, cost-bounded.
function brainLastRun(statePath) {
if (!existsSync(statePath)) return null;
try {
const j = JSON.parse(readFileSync(statePath, 'utf-8'));
return typeof j?.last_run === 'string' ? j.last_run : null;
} catch {
return null;
}
}
function countPublished(publishedDir) {
if (!existsSync(publishedDir)) return 0;
try {
return readdirSync(publishedDir).filter((f) => f.endsWith('.md') && !f.startsWith('.')).length;
} catch {
return 0;
}
}
function isoWeek() {
const d = new Date();
const dayNum = d.getUTCDay() || 7;
@ -77,6 +99,22 @@ try {
// Non-critical: never block session start on migration failure.
}
// SB-S2: ensure the brain/ + ingest/ scaffold exists (zero-dep, idempotent).
// UNCONDITIONAL — must run on the fresh-install path too (the reminders block
// below only runs when the state file exists). Profile.md SEEDING needs the tsx
// fold, so it stays the manual `brain init`; here we only mkdir, and remember
// whether to nudge (the nudge is appended AFTER the if/else, since the else
// branch reassigns `context` and would clobber an early append).
let brainProfileMissing = false;
try {
for (const d of ['brain/journal', 'ingest/inbox', 'ingest/published']) {
mkdirSync(join(getDataRoot(''), d), { recursive: true });
}
brainProfileMissing = !existsSync(join(getDataRoot('brain'), 'profile.md'));
} catch {
// Non-critical: never block session start on scaffold-ensure failure.
}
if (existsSync(STATE_FILE)) {
const stateContent = readFileSync(STATE_FILE, 'utf-8');
@ -341,6 +379,17 @@ if (existsSync(STATE_FILE)) {
reminders += `- Trend signals are ${daysSinceTrend} days old. Scan for trends to refresh the digest before planning content.\\n`;
}
// Brain consolidation-due (SB-S2): fires only when published gold records exist
// and the loop is stale/never-run (a never-used brain never nags). Counts files
// + reads the sidecar last_run — no profile.md parse. Zero-dep.
const publishedCount = countPublished(join(getDataRoot('ingest'), 'published'));
const brainLastRunDate = brainLastRun(join(getDataRoot('brain'), 'consolidation-state.json'));
const daysSinceConsolidation = daysSince(brainLastRunDate);
if (publishedCount > 0 && (brainLastRunDate === null || (daysSinceConsolidation !== null && daysSinceConsolidation >= 7))) {
const since = brainLastRunDate === null ? 'never run' : `${daysSinceConsolidation}d ago`;
reminders += `- ${publishedCount} published post(s) captured, last brain consolidation ${since}. Run \`brain consolidate\` to evolve your profile.\\n`;
}
// Milestone reminders
if (milestonePhase && followerCount > 0) {
if (milestoneStatus === 'SIGNIFICANTLY BEHIND') {
@ -448,6 +497,12 @@ if (existsSync(STATE_FILE)) {
}
}
// SB-S2: nudge to seed the brain if it isn't initialised (after the if/else, so
// the fresh-install branch's `context = …` reassignment can't clobber it).
if (brainProfileMissing) {
context += '\\n## Brain\\n- Brain not initialised. Run `brain init` to seed your evolving profile.\\n';
}
// Read REMEMBER.md for user session context
const rememberFile = join(getDataRoot(), 'REMEMBER.md');
const rememberTemplate = join(PLUGIN_ROOT, 'config', 'REMEMBER.template.md');