fix(hooks): address three blocking defects in context exhaustion record (#1974)
Review feedback from @trek-e — three blocking fixes:
1. **Sentinel prevents repeated firing**
Added warnData.criticalRecorded flag persisted to the warn state file.
Previously the subprocess fired on every DEBOUNCE_CALLS cycle (5 tool
uses) for the rest of the session, overwriting the "crash moment"
record with a new timestamp each time. Now fires exactly once per
CRITICAL session.
2. **Runtime-agnostic path via __dirname**
Replaced hardcoded `path.join(process.env.HOME, '.claude', ...)` with
`path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs')`.
The hook lives at <runtime-config>/hooks/ and gsd-tools.cjs at
<runtime-config>/get-shit-done/bin/ — __dirname resolves correctly on
all runtimes (Claude Code, OpenCode, Gemini, Kilo) without assuming
~/.claude/.
3. **Correct subcommand: state record-session**
Switched from `state update "Stopped At" ...` to
`state record-session --stopped-at ...`. The dedicated command
updates Last session, Last Date, Stopped At, and Resume File
atomically under the state lock.
Also:
- Hoisted `const { spawn } = require('child_process')` to top of file
to match existing require() style.
- Coerced usedPct to Number(usedPct) || 0 to sanitize the bridge file
in case it's malformed or adversarially crafted.
Tests (tests/bug-1974-context-exhaustion-record.test.cjs, 4 cases):
- Subprocess spawns and writes "context exhaustion" on CRITICAL
- Subprocess does NOT spawn when .planning/STATE.md is absent
- Sentinel guard prevents second fire within same session
- Hook source uses __dirname-based path (not hardcoded ~/.claude/)
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
||||
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
||||
@@ -130,16 +131,27 @@ process.stdin.on('end', () => {
|
||||
|
||||
// On CRITICAL with active GSD project, auto-record session state as a
|
||||
// breadcrumb for /gsd-resume-work (#1974). Fire-and-forget subprocess —
|
||||
// doesn't block the hook or the agent.
|
||||
if (isCritical && isGsdActive) {
|
||||
// doesn't block the hook or the agent. Fires ONCE per CRITICAL session,
|
||||
// guarded by warnData.criticalRecorded to prevent repeated overwrites
|
||||
// of the "crash moment" record on every debounce cycle.
|
||||
if (isCritical && isGsdActive && !warnData.criticalRecorded) {
|
||||
try {
|
||||
const gsdTools = path.join(process.env.HOME || '', '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
require('child_process').spawn(
|
||||
// Runtime-agnostic path: this hook lives at <runtime-config>/hooks/
|
||||
// and gsd-tools.cjs lives at <runtime-config>/get-shit-done/bin/.
|
||||
// Using __dirname makes this work on Claude Code, OpenCode, Gemini,
|
||||
// Kilo, etc. without hardcoding ~/.claude/.
|
||||
const gsdTools = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
// Coerce usedPct to a safe number in case bridge file is malformed
|
||||
const safeUsedPct = Number(usedPct) || 0;
|
||||
const stoppedAt = `context exhaustion at ${safeUsedPct}% (${new Date().toISOString().split('T')[0]})`;
|
||||
spawn(
|
||||
process.execPath,
|
||||
[gsdTools, 'state', 'update', 'Stopped At', `context exhaustion at ${usedPct}% (${timestamp})`],
|
||||
[gsdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
|
||||
{ cwd, detached: true, stdio: 'ignore' }
|
||||
).unref();
|
||||
warnData.criticalRecorded = true;
|
||||
// Persist the sentinel so subsequent debounce cycles don't re-fire
|
||||
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
||||
} catch { /* non-critical — don't let state recording break the hook */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user