* feat(#770): register Claude Code lifecycle hooks (SubagentStop/Stop/PreCompact/FileChanged) Wire three new context-tracking events (SubagentStop, Stop, PreCompact) to gsd-context-monitor so context-headroom warnings surface at model-stop and subagent-finalisation moments — not just on PostToolUse. Add a new FileChanged hook (gsd-config-reload.js) that hot-reloads .planning/config.json context mid-session when the user edits it, injecting a config summary as hookSpecificOutput.additionalContext. Updates plugin manifest hooks.json, managed-hooks-registry, installer-migration-report allowlist, and shell-command-projection cleanup tables. Tests: 21 new assertions in enh-770-claude-hook-events.test.cjs; enh-788 and issue-766 test suites updated. Closes #770 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#770): document newly-registered Claude Code lifecycle hooks Add a Hook coverage table to the Claude Code npm installer section of docs/how-to/install-on-your-runtime.md describing SubagentStop, Stop, PreCompact, and the new FileChanged (gsd-config-reload.js) hook that hot-reloads .planning/config.json mid-session. Also fixes the changeset frontmatter (adds type: Added + pr: 821) so docs-lint can consume the fragment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#770): add gsd-config-reload.js to INVENTORY.md and regenerate manifest The feat commit added hooks/gsd-config-reload.js but did not bump the Hooks count in docs/INVENTORY.md (14→15) or add the new row, and did not regenerate docs/INVENTORY-MANIFEST.json. Both inventory-counts and inventory-manifest-sync tests failed across the full CI matrix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#770): make lifecycle-hook tests deterministic on scoped runner Replace the shared hooks/dist/ ensemble setup (ensureHooksDist / teardownHooksDist) in the Claude hook tests with per-test isolation: pre-populate each test's own tmpDir/.claude/hooks/ with stub files and pass installerMigrations:[] to install() so the first-time-baseline migration does not remove the stubs before the copy step can run. Root cause: hooks/dist/ is gitignored and absent on a fresh npm ci. ensureHooksDist() created it and teardownHooksDist() deleted it, but with --test-concurrency=4 both test files ran concurrently as separate Node.js worker processes sharing the same filesystem. One file's afterEach teardown deleted hooks/dist/ while the other file's install() was copying from it, producing an ENOENT (reproduced 2/10 runs locally). The additional issue: even with pre-placed stubs surviving the copy race, the 000-first-time-baseline migration classified hooks/gsd-*.js as bundled-gsd-hook artifacts, auto-removed them, and the copy step never re-ran (hooks/dist/ absent) — leaving contextMonitorFile missing and all hook registrations silently skipped (the 'got: []' symptom). Fix: pre-populate targetDir/hooks/ per-test (isolated temp dir) AND pass installerMigrations:[] so the baseline scan is skipped. The Qwen suites already used this pattern correctly; the Claude suites are aligned to it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#770): ship gsd-config-reload.js by adding it to build-hooks HOOKS_TO_COPY The #770 feature added hooks/gsd-config-reload.js and registered it in MANAGED_HOOKS, the installer, INVENTORY, and the test EXPECTED_ALL_HOOKS list — but never added it to scripts/build-hooks.js HOOKS_TO_COPY. As a result the hook was never copied into hooks/dist/ during the build, so: - the hook would never ship to users (real production bug — the FileChanged config-reload feature was dead-on-arrival), and - install-minimal-hooks.test.cjs #1755 ("all expected hooks are copied from hooks/dist/ to target", ".js hooks are executable after copy", "manifest contains .js hook entries") failed on any environment with a clean checkout (no pre-existing hooks/dist/): coverage, full test macos-22/macos-24, test ubuntu-24. The failures were masked locally only by a stale hooks/dist/ left from a prior build (build-hooks copies into dist without clearing it). On CI's fresh `npm ci` there is no dist, so the omission surfaced. Fix: add 'gsd-config-reload.js' to HOOKS_TO_COPY so build-hooks stages it into hooks/dist/ alongside the other JS hooks. Verified by removing hooks/dist/ and rerunning the full suite green (0 fail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#770): make config prototype-pollution beforeEach deterministic on scoped runner Root cause: the #663 and alert-#26 prototype-pollution describe blocks seeded .planning/config.json in beforeEach via a bare runGsdTools('config-ensure-section') whose result was discarded. That command runs in a spawned gsd-tools child; on the scoped CI lane (--test-concurrency=4, config.test.cjs scheduled alongside the heavy install/tarball suites that #770 pulled into the targeted set) the child can be transiently killed under resource pressure (non-zero exit, empty stderr — an OS-level kill, not an app error). The swallowed failure left config.json absent, so the first subtest's readConfig() threw ENOENT opening <tmp>/.planning/config.json. Only 1 of 4 subtests failed, confirming a per-invocation transient, not a deterministic miss; the full suite schedules files differently so config.test.cjs did not collide with those heavy neighbors → passed there. Fix: add ensureConfigReady(tmpDir) which retries config-ensure-section on ANY failure or missing file and throws a clear diagnostic if it still cannot create config.json, then use it in both prototype-pollution beforeEach blocks. Setup is now deterministic under load; the #663/alert-#26 security assertions are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,37 @@ async function runConfigEnsureSectionWithRetry(tmpDir, attempts = 4) {
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `.planning/config.json` for a test and guarantee it lands on disk
|
||||
* before the test body runs.
|
||||
*
|
||||
* `config-ensure-section` is invoked through a spawned `gsd-tools.cjs` child.
|
||||
* On the scoped CI lane (`--test-concurrency=4`, config.test.cjs scheduled
|
||||
* alongside the heavy install/tarball suites) that child can be transiently
|
||||
* killed under resource pressure — surfacing as a non-zero exit with empty
|
||||
* stderr (an OS-level kill, not a gsd-tools application error; see the
|
||||
* `runGsdTools` catch). A bare `runGsdTools('config-ensure-section')` in
|
||||
* `beforeEach` swallows that failure, leaving config.json absent so the first
|
||||
* subtest's `readConfig()` throws a confusing ENOENT (#770 scoped-lane flake).
|
||||
*
|
||||
* This retries on ANY failure or missing file (not just the EPERM/EBUSY class
|
||||
* `runConfigEnsureSectionWithRetry` covers) and throws a clear diagnostic if it
|
||||
* still cannot create the file, so setup is deterministic under load.
|
||||
*/
|
||||
async function ensureConfigReady(tmpDir, attempts = 5) {
|
||||
const configPath = path.join(tmpDir, '.planning', 'config.json');
|
||||
let last;
|
||||
for (let i = 0; i < attempts; i += 1) {
|
||||
last = runGsdTools('config-ensure-section', tmpDir);
|
||||
if (last.success && fs.existsSync(configPath)) return last;
|
||||
if (i < attempts - 1) await delay(150 * (i + 1));
|
||||
}
|
||||
throw new Error(
|
||||
`config-ensure-section failed to create ${configPath} after ${attempts} attempts: ` +
|
||||
`${(last && last.error) || 'unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── config-ensure-section ───────────────────────────────────────────────────
|
||||
|
||||
describe('config-ensure-section command', () => {
|
||||
@@ -1106,10 +1137,12 @@ describe('config-path command (#2282)', () => {
|
||||
describe('config-set prototype-pollution guard (#663)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = createTempProject();
|
||||
// Initialise config so there is a config.json to write to.
|
||||
runGsdTools('config-ensure-section', tmpDir);
|
||||
// Initialise config so there is a config.json to write to. Retry + assert
|
||||
// so a transient config-ensure-section child failure under scoped-lane load
|
||||
// cannot leave config.json absent (#770).
|
||||
await ensureConfigReady(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1163,10 +1196,12 @@ describe('config-set prototype-pollution guard (#663)', () => {
|
||||
describe('config-set prototype-pollution guard via dynamic-key prefixes (alert #26)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = createTempProject();
|
||||
// Initialise config so there is a config.json to write to.
|
||||
runGsdTools('config-ensure-section', tmpDir);
|
||||
// Initialise config so there is a config.json to write to. Retry + assert
|
||||
// so a transient config-ensure-section child failure under scoped-lane load
|
||||
// cannot leave config.json absent (#770).
|
||||
await ensureConfigReady(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user