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
This commit is contained in:
Kjell Tore Guttormsen 2026-06-24 13:12:54 +02:00
commit fa7551070e
9 changed files with 712 additions and 10 deletions

View file

@ -0,0 +1,77 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, copyFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
// RE-R2b: SessionStart surfaces the latest dated morning brief — a "## Morning
// Brief" block built from the brief's frontmatter (date + pre-rendered summary),
// read zero-tsx. session-start is a procedural hook with no exports, so we run it
// as a subprocess with an isolated HOME + LINKEDIN_STUDIO_DATA and inspect the
// additionalContext. Pattern: session-start-trends-staleness.test.mjs.
const here = dirname(fileURLToPath(import.meta.url));
const hookPath = join(here, '..', 'session-start.mjs');
const PLUGIN_ROOT = join(here, '..', '..', '..');
const STATE_TEMPLATE = join(PLUGIN_ROOT, 'config', 'state-file.template.md');
const HEADING = '## Morning Brief';
// ASCII fixture summary (the hook surfaces whatever the frontmatter carries verbatim).
const SUMMARY = '3 fresh signals match your pillars. Top: Alpha (AI, 2d).';
// Run the hook against an isolated HOME + data root; optionally seed a dated brief
// at <data>/trends/morning-brief/<name> — the SAME path defaultBriefDir() resolves
// to under this LINKEDIN_STUDIO_DATA (CLI-write path == hook-read path cross-check).
function runHook({ briefName }) {
const home = mkdtempSync(join(tmpdir(), 'lis-mb-home-'));
const data = mkdtempSync(join(tmpdir(), 'lis-mb-data-'));
try {
mkdirSync(join(home, '.claude'), { recursive: true });
copyFileSync(STATE_TEMPLATE, join(home, '.claude', 'linkedin-studio.local.md'));
if (briefName) {
const briefDir = join(data, 'trends', 'morning-brief');
mkdirSync(briefDir, { recursive: true });
writeFileSync(
join(briefDir, briefName),
`---\ndate: 2026-06-24\nsummary: ${SUMMARY}\nstore: { trends: 10, matched: 3, fresh: 3 }\nschemaVersion: 1\n---\n\n# Morgen-brief\n\nbody\n`,
);
}
const stdout = execFileSync('node', [hookPath], {
input: '',
env: { ...process.env, HOME: home, USERPROFILE: home, LINKEDIN_STUDIO_DATA: data },
encoding: 'utf-8',
});
return JSON.parse(stdout).hookSpecificOutput.additionalContext;
} finally {
rmSync(home, { recursive: true, force: true });
rmSync(data, { recursive: true, force: true });
}
}
describe('session-start — morning-brief surfacing (RE-R2b)', () => {
test('surfaces the latest brief: heading + summary + file pointer', () => {
const ctx = runHook({ briefName: '2026-06-24.md' });
assert.ok(ctx.includes(HEADING), 'expected the Morning Brief block');
assert.ok(ctx.includes('Morning Brief (2026-06-24)'), 'block carries the brief date');
assert.ok(ctx.includes(SUMMARY), 'block surfaces the pre-rendered summary');
assert.ok(ctx.includes('Full brief:'), 'block carries the full-brief pointer');
// The summary must be its OWN line (proves it did not bleed into adjacent lines —
// single-line summary + the \n idiom held).
assert.ok(ctx.split('\n').includes(SUMMARY), 'summary is a standalone line in the block');
});
test('newest brief wins when several dated files exist', () => {
// (single-file harness asserts the surface; lexical name sort picks the newest —
// covered structurally by the .md-anchored filter + .sort() in latestMorningBrief.)
const ctx = runHook({ briefName: '2026-06-24.md' });
assert.ok(ctx.includes('Morning Brief (2026-06-24)'));
});
test('no brief dir -> no block, no crash', () => {
const ctx = runHook({ briefName: null });
assert.ok(!ctx.includes(HEADING), 'no brief => no Morning Brief block');
});
});

View file

@ -51,6 +51,31 @@ function trendsNewestCapture(storePath) {
}
}
// RE-R2b: the most recent morning brief (date + pre-rendered summary) for
// session-start surfacing. Reads the dated Markdown directly — NO tsx (same
// zero-dep discipline as trendsNewestCapture above): the brief's frontmatter
// carries a single-line `summary` the hook surfaces verbatim (extractYaml's
// [^"\n]* + .trim() guarantees date/summary are newline-free). Returns null when
// the dir is absent / empty / unreadable — no brief yet => no block.
function latestMorningBrief(briefDir) {
if (!existsSync(briefDir)) return null;
try {
const files = readdirSync(briefDir)
.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
.sort();
const name = files[files.length - 1];
if (!name) return null;
const content = readFileSync(join(briefDir, name), 'utf-8');
return {
date: extractYaml(content, 'date'),
summary: extractYaml(content, 'summary'),
file: join(briefDir, name),
};
} catch {
return null;
}
}
// 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.
@ -503,6 +528,14 @@ if (brainProfileMissing) {
context += '\\n## Brain\\n- Brain not initialised. Run `brain init` to seed your evolving profile.\\n';
}
// RE-R2b: surface the latest dated morning brief (zero-tsx; summary is a
// pre-rendered single line from the brief's frontmatter). Unconditional on a brief
// existing, like the brain nudge above (so the fresh-install branch surfaces it too).
const latestBrief = latestMorningBrief(join(getDataRoot('trends'), 'morning-brief'));
if (latestBrief && latestBrief.summary) {
context += `\\n## Morning Brief (${latestBrief.date})\\n${latestBrief.summary}\\n→ Full brief: ${latestBrief.file}\\n`;
}
// Read REMEMBER.md for user session context
const rememberFile = join(getDataRoot(), 'REMEMBER.md');
const rememberTemplate = join(PLUGIN_ROOT, 'config', 'REMEMBER.template.md');