Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
140 lines
5.2 KiB
JavaScript
140 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// msd-hook-version: {{MSD_VERSION}}
|
|
// msd-config-reload.js — FileChanged hook: hot-reload MSD config context
|
|
// Fires when .planning/config.json is modified, created, or deleted.
|
|
//
|
|
// When the user edits .planning/config.json mid-session, this hook reads the
|
|
// updated config and injects a summary as additionalContext so the agent knows
|
|
// the new configuration without requiring a session restart.
|
|
//
|
|
// Input (from Claude Code):
|
|
// { session_id, cwd, hook_event_name: "FileChanged",
|
|
// file_path: "/abs/path/.planning/config.json", event: "change"|"add"|"unlink" }
|
|
//
|
|
// Output:
|
|
// { hookSpecificOutput: { hookEventName: "FileChanged", additionalContext: "..." } }
|
|
// or exits 0 silently (if config absent, unreadable, or event is "unlink").
|
|
//
|
|
// Enabled for all Claude Code installs. This hook is always-on — it is a
|
|
// no-op when .planning/config.json is absent (ENOENT → exit 0).
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { HOOK_ON_CRASH, allow, crash } = require('./lib/hook-exit.js');
|
|
|
|
// This hook only injects an advisory config-reload summary; a crash mid-parse
|
|
// must not block the session or the FileChanged event that triggered it — the
|
|
// agent simply keeps using the config context it already had (#3911).
|
|
const ON_CRASH = HOOK_ON_CRASH.ALLOW;
|
|
|
|
let input = '';
|
|
// Timeout guard: if stdin does not close within 8s exit silently rather than
|
|
// hanging until Claude Code kills the process and reports "hook error".
|
|
const stdinTimeout = setTimeout(() => allow(undefined), 8000);
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => (input += chunk));
|
|
process.stdin.on('end', () => {
|
|
clearTimeout(stdinTimeout);
|
|
try {
|
|
const data = JSON.parse(input);
|
|
const event = data.event; // "change" | "add" | "unlink"
|
|
const filePath = data.file_path || '';
|
|
const cwd = data.cwd || process.cwd();
|
|
|
|
// Only handle the MSD planning config — verify both basename and that the
|
|
// resolved path is .planning/config.json relative to cwd. The hook
|
|
// matcher ('config.json') fires on any watched config.json; this guard
|
|
// ensures an unrelated config.json in node_modules/ or elsewhere does not
|
|
// inject spurious additionalContext.
|
|
const basename = path.basename(filePath);
|
|
if (basename !== 'config.json') {
|
|
allow(undefined);
|
|
}
|
|
const expectedPath = path.resolve(cwd, '.planning', 'config.json');
|
|
if (path.resolve(filePath) !== expectedPath) {
|
|
allow(undefined);
|
|
}
|
|
|
|
// On unlink (deletion) emit a brief notice and exit
|
|
if (event === 'unlink') {
|
|
allow({
|
|
hookSpecificOutput: {
|
|
hookEventName: 'FileChanged',
|
|
additionalContext:
|
|
'MSD config (.planning/config.json) was deleted. ' +
|
|
'Falling back to built-in defaults for this session.',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Read the updated config file
|
|
let config;
|
|
try {
|
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
config = JSON.parse(raw);
|
|
} catch (e) {
|
|
if (e && e.code === 'ENOENT') allow(undefined);
|
|
// Malformed JSON — inform the agent without crashing
|
|
allow({
|
|
hookSpecificOutput: {
|
|
hookEventName: 'FileChanged',
|
|
additionalContext:
|
|
'MSD config (.planning/config.json) was modified but could not be parsed. ' +
|
|
'Check the file for JSON syntax errors.',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Build a concise summary of key config fields the agent cares about
|
|
const lines = ['MSD config reloaded (.planning/config.json updated):'];
|
|
|
|
if (config.runtime) lines.push(` runtime: ${config.runtime}`);
|
|
if (config.mode) lines.push(` mode: ${config.mode}`);
|
|
|
|
// hooks section (opt-in toggles agents act on)
|
|
if (config.hooks && typeof config.hooks === 'object') {
|
|
const hookKeys = Object.entries(config.hooks)
|
|
.filter(([, v]) => v !== undefined)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(', ');
|
|
if (hookKeys) lines.push(` hooks: { ${hookKeys} }`);
|
|
}
|
|
|
|
// workflow section (key toggles)
|
|
if (config.workflow && typeof config.workflow === 'object') {
|
|
const wfKeys = Object.entries(config.workflow)
|
|
.filter(([, v]) => v !== undefined)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(', ');
|
|
if (wfKeys) lines.push(` workflow: { ${wfKeys} }`);
|
|
}
|
|
|
|
// model overrides (agents use these)
|
|
if (config.models && typeof config.models === 'object') {
|
|
const modelKeys = Object.entries(config.models)
|
|
.filter(([, v]) => v !== undefined)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(', ');
|
|
if (modelKeys) lines.push(` models: { ${modelKeys} }`);
|
|
}
|
|
|
|
if (lines.length === 1) {
|
|
// No notable fields — still confirm the reload happened
|
|
lines.push(' (no notable keys changed)');
|
|
}
|
|
|
|
const additionalContext = lines.join('\n');
|
|
process.stdout.write(JSON.stringify({
|
|
hookSpecificOutput: {
|
|
hookEventName: 'FileChanged',
|
|
additionalContext,
|
|
},
|
|
}));
|
|
} catch (e) {
|
|
// Silent fail — never block the session on a config reload error.
|
|
// ON_CRASH is declared ALLOW at module top: this preserves today's
|
|
// exit(0) fail-open behavior exactly (#3911).
|
|
crash(ON_CRASH, undefined);
|
|
}
|
|
});
|