ktg-plugin-marketplace/plugins/graceful-handoff/hooks/scripts/session-start-load-handoff.mjs
Kjell Tore Guttormsen 4076bf904a feat(graceful-handoff): 2.0 — SessionStart auto-load handoff on resume/compact [skip-docs]
Step 6 of v2.0 plan. SessionStart hook fires on source: resume or
source: compact, walks up to 3 levels searching for
NEXT-SESSION-*.local.md, injects content via additionalContext, and
archives the file (rename to *.archived.local.md) to prevent stale-load
in later sessions. 9 tests cover sources, multi-level search,
topic-slug variants, archive filtering, malformed payload.
2026-05-01 06:06:25 +02:00

93 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
// session-start-load-handoff.mjs — graceful-handoff v2.0
// SessionStart hook: on `source: resume` or `source: compact`, find
// NEXT-SESSION-PROMPT.local.md (or NEXT-SESSION-*.local.md) in cwd and up
// to 3 levels above, inject contents into the new session's context, then
// archive the file to prevent stale-load in subsequent sessions.
import { readFileSync, existsSync, readdirSync, renameSync } from 'node:fs';
import { dirname, join } from 'node:path';
function readStdin() {
try {
return readFileSync(0, 'utf-8');
} catch {
return '';
}
}
// Find NEXT-SESSION-*.local.md in dir; returns path or null
function findHandoffIn(dir) {
try {
const entries = readdirSync(dir);
// Prefer NEXT-SESSION-PROMPT.local.md, then any NEXT-SESSION-*.local.md
const exact = entries.find(e => e === 'NEXT-SESSION-PROMPT.local.md');
if (exact) return join(dir, exact);
const match = entries.find(e => /^NEXT-SESSION-.*\.local\.md$/.test(e) && !/\.archived\./.test(e));
if (match) return join(dir, match);
} catch { /* ignore */ }
return null;
}
// Walk up from start, max 3 levels, looking for handoff
function findHandoffUpwards(start) {
let cur = start;
for (let i = 0; i < 4; i++) { // 0,1,2,3 — start + 3 ancestors
const found = findHandoffIn(cur);
if (found) return found;
const parent = dirname(cur);
if (parent === cur) break;
cur = parent;
}
return null;
}
function main() {
const raw = readStdin();
if (!raw.trim()) process.exit(0);
let payload;
try {
payload = JSON.parse(raw);
} catch {
process.exit(0);
}
const source = payload?.source;
if (source !== 'resume' && source !== 'compact') {
process.exit(0);
}
const cwd = payload?.cwd || process.cwd();
const handoffPath = findHandoffUpwards(cwd);
if (!handoffPath) {
process.exit(0);
}
let content;
try {
content = readFileSync(handoffPath, 'utf-8');
} catch {
process.exit(0);
}
// Inject via additionalContext for clean structured output
const output = {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: `<session-handoff source="${handoffPath}" loaded-by="graceful-handoff">\n${content}\n</session-handoff>`,
},
};
process.stdout.write(JSON.stringify(output));
// Archive to prevent stale-load in subsequent sessions
try {
const archived = handoffPath.replace(/\.local\.md$/, '.archived.local.md');
if (!existsSync(archived)) {
renameSync(handoffPath, archived);
}
} catch { /* archival is best-effort, never block injection */ }
process.exit(0);
}
main();