@therocketcode/gsd-core 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/lib/legacy-cleanup.cjs +44 -1
- package/hooks/dist/gsd-context-monitor.js +25 -190
- package/hooks/gsd-context-monitor.js +25 -190
- package/hooks/gsd-context-monitor.js.tmp.27634.318d8b7b56ab +39 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-core",
|
|
3
3
|
"displayName": "GSD Core",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.3",
|
|
5
5
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "TheRocketCodeMX",
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-core",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
|
@@ -56,6 +56,18 @@ const LEGACY_CACHE_FILENAMES = [
|
|
|
56
56
|
'gsd-update-check-opengsd-gsd-core.json',
|
|
57
57
|
];
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Legacy runtime directory names left inside a runtime config dir by a
|
|
61
|
+
* superseded install. The current fork installs its runtime under `gsd-core/`;
|
|
62
|
+
* the pre-rename upstream used `get-shit-done/`. When a user migrates, the
|
|
63
|
+
* file-level migration deletes the OLD dir's files but leaves the emptied
|
|
64
|
+
* directory tree behind — harmless, but untidy. We prune such a dir ONLY when
|
|
65
|
+
* it contains zero files (a strict emptiness guard, re-checked at apply time),
|
|
66
|
+
* so we can never delete a directory that still holds user-authored content.
|
|
67
|
+
* NEVER list 'gsd-core' here — that is the CURRENT runtime dir.
|
|
68
|
+
*/
|
|
69
|
+
const LEGACY_RUNTIME_DIR_NAMES = ['get-shit-done'];
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
72
|
* Subtrees within a configDir that GSD actively scans for old-package content.
|
|
61
73
|
* Deliberately excludes 'gsd-core' — the current package's own infra and
|
|
@@ -151,6 +163,8 @@ function fileContainsOldPackageSignal(absPath, fsMod) {
|
|
|
151
163
|
* - 'content-references-old-package': a code file whose content contains
|
|
152
164
|
* the old package name signal (hooks/ and commands/ subtrees only).
|
|
153
165
|
* - 'legacy-shared-cache': the old package's shared update-check cache file.
|
|
166
|
+
* - 'empty-legacy-runtime-dir': a superseded runtime dir (e.g. get-shit-done/)
|
|
167
|
+
* left empty after a migration deleted its files.
|
|
154
168
|
*
|
|
155
169
|
* @param {string[]} configDirs - absolute paths to runtime config dirs to scan
|
|
156
170
|
* @param {object} [opts]
|
|
@@ -207,6 +221,25 @@ function planLegacyCleanup(configDirs, opts = {}) {
|
|
|
207
221
|
}
|
|
208
222
|
}
|
|
209
223
|
|
|
224
|
+
// Emptied legacy runtime directories left behind after a migration deleted
|
|
225
|
+
// their files. Flag ONLY when the dir exists and contains zero files — a
|
|
226
|
+
// dir that still holds any file is left untouched (its code files are caught
|
|
227
|
+
// by the content scan above instead).
|
|
228
|
+
for (const configDir of configDirs) {
|
|
229
|
+
for (const legacyDirName of LEGACY_RUNTIME_DIR_NAMES) {
|
|
230
|
+
const legacyDir = path.join(configDir, legacyDirName);
|
|
231
|
+
let stat;
|
|
232
|
+
try {
|
|
233
|
+
stat = fsMod.statSync(legacyDir);
|
|
234
|
+
} catch {
|
|
235
|
+
continue; // absent — skip
|
|
236
|
+
}
|
|
237
|
+
if (stat.isDirectory() && collectFilesUnder(legacyDir, fsMod).length === 0) {
|
|
238
|
+
addCandidate(legacyDir, 'empty-legacy-runtime-dir');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
210
243
|
// Sort deterministically by path
|
|
211
244
|
const sorted = [...candidates.entries()]
|
|
212
245
|
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
@@ -246,6 +279,16 @@ function applyLegacyCleanup(plan, opts = {}) {
|
|
|
246
279
|
const errors = [];
|
|
247
280
|
|
|
248
281
|
for (const item of plan) {
|
|
282
|
+
// Legacy runtime dirs are removed recursively, but ONLY after re-verifying
|
|
283
|
+
// they are still empty at apply time — a defensive guard so a file that
|
|
284
|
+
// appeared between scan and apply is never destroyed.
|
|
285
|
+
const isLegacyDir = item.reason === 'empty-legacy-runtime-dir';
|
|
286
|
+
if (isLegacyDir && collectFilesUnder(item.path, fsMod).length > 0) {
|
|
287
|
+
errors.push({ path: item.path, error: 'legacy runtime dir not empty at apply time; skipped' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const rmOpts = isLegacyDir ? { recursive: true, force: true } : { force: true };
|
|
291
|
+
|
|
249
292
|
let lastErr;
|
|
250
293
|
const maxAttempts = process.platform === 'win32' ? 3 : 1;
|
|
251
294
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
@@ -254,7 +297,7 @@ function applyLegacyCleanup(plan, opts = {}) {
|
|
|
254
297
|
// Synchronous 100ms delay before retry (win32 EBUSY/EPERM from Defender)
|
|
255
298
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
256
299
|
}
|
|
257
|
-
fsMod.rmSync(item.path,
|
|
300
|
+
fsMod.rmSync(item.path, rmOpts);
|
|
258
301
|
lastErr = undefined;
|
|
259
302
|
break;
|
|
260
303
|
} catch (err) {
|
|
@@ -1,195 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
-
// Context Monitor
|
|
4
|
-
// Reads context metrics from the statusline bridge file and injects
|
|
5
|
-
// warnings when context usage is high. This makes the AGENT aware of
|
|
6
|
-
// context limits (the statusline only shows the user).
|
|
3
|
+
// Context Monitor — DISABLED in @therocketcode/gsd-core.
|
|
7
4
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
5
|
+
// Upstream GSD shipped a PostToolUse/Stop/SubagentStop/PreCompact hook here that
|
|
6
|
+
// injected "CONTEXT WARNING / CONTEXT CRITICAL" messages into the agent's own
|
|
7
|
+
// conversation as the context window filled. This fork deliberately removes that
|
|
8
|
+
// behavior: agent-facing context warnings stress the model, derail work, and
|
|
9
|
+
// override the user's judgment about when to wrap up. The user can still see
|
|
10
|
+
// context usage passively in the statusline — that is enough.
|
|
13
11
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const path = require('path');
|
|
24
|
-
const { spawn } = require('child_process');
|
|
25
|
-
|
|
26
|
-
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
27
|
-
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
28
|
-
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
29
|
-
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
30
|
-
|
|
31
|
-
let input = '';
|
|
32
|
-
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
|
|
33
|
-
// Windows/Git Bash, or slow Claude Code piping during large outputs),
|
|
34
|
-
// exit silently instead of hanging until Claude Code kills the process
|
|
35
|
-
// and reports "hook error". See #775, #1162.
|
|
12
|
+
// This file is intentionally kept as an inert no-op rather than deleted. It stays
|
|
13
|
+
// in the managed-hooks set so that an update OVERWRITES any previously-installed
|
|
14
|
+
// active version on EXISTING installs — guaranteeing the warning behavior is gone
|
|
15
|
+
// everywhere, not just on fresh installs. It reads and discards stdin, emits
|
|
16
|
+
// nothing, and exits 0 — it never injects context into the agent and never
|
|
17
|
+
// blocks tool execution.
|
|
18
|
+
|
|
19
|
+
// Drain and discard stdin so the caller's write side never sees EPIPE, then exit
|
|
20
|
+
// cleanly. Guard against stdin never closing (pipe issues on Windows/Git Bash).
|
|
36
21
|
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
|
|
37
|
-
process.
|
|
38
|
-
|
|
39
|
-
process.stdin.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Reject session IDs that contain path traversal sequences or path separators.
|
|
50
|
-
// session_id is used to construct file paths in /tmp — an unsanitized value
|
|
51
|
-
// could escape the temp directory and read or write arbitrary files.
|
|
52
|
-
if (/[/\\]|\.\./.test(sessionId)) {
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check if context warnings are disabled via config.
|
|
57
|
-
// Collapsed existsSync+readFileSync into a single read guarded by try/catch
|
|
58
|
-
// (ENOENT or parse error → use defaults, same as old "planningDir absent" branch).
|
|
59
|
-
const cwd = data.cwd || process.cwd();
|
|
60
|
-
try {
|
|
61
|
-
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
62
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
63
|
-
if (config.hooks?.context_warnings === false) {
|
|
64
|
-
process.exit(0);
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
// Missing or unparseable config → proceed with defaults (context warnings enabled)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const tmpDir = os.tmpdir();
|
|
71
|
-
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
72
|
-
|
|
73
|
-
// If no metrics file, this is a subagent or fresh session -- exit silently.
|
|
74
|
-
// Collapsed existsSync+readFileSync: ENOENT → exit 0 (identical to old !existsSync branch),
|
|
75
|
-
// other errors rethrow to the outer catch (swallowed → exit 0, as before).
|
|
76
|
-
let metricsRaw;
|
|
77
|
-
try {
|
|
78
|
-
metricsRaw = fs.readFileSync(metricsPath, 'utf8');
|
|
79
|
-
} catch (e) {
|
|
80
|
-
if (e && e.code === 'ENOENT') process.exit(0);
|
|
81
|
-
throw e;
|
|
82
|
-
}
|
|
83
|
-
const metrics = JSON.parse(metricsRaw);
|
|
84
|
-
const now = Math.floor(Date.now() / 1000);
|
|
85
|
-
|
|
86
|
-
// Ignore stale metrics
|
|
87
|
-
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const remaining = metrics.remaining_percentage;
|
|
92
|
-
const usedPct = metrics.used_pct;
|
|
93
|
-
|
|
94
|
-
// No warning needed
|
|
95
|
-
if (remaining > WARNING_THRESHOLD) {
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Debounce: check if we warned recently
|
|
100
|
-
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
|
101
|
-
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
102
|
-
let firstWarn = true;
|
|
103
|
-
|
|
104
|
-
// Collapsed existsSync+readFileSync: ENOENT or parse error → keep default warnData
|
|
105
|
-
// (same as old "file absent" branch). firstWarn tracks whether we read a valid sentinel.
|
|
106
|
-
try {
|
|
107
|
-
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
|
108
|
-
firstWarn = false;
|
|
109
|
-
} catch (e) {
|
|
110
|
-
// Missing or corrupted sentinel → firstWarn stays true, warnData stays at defaults
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
114
|
-
|
|
115
|
-
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
|
116
|
-
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
117
|
-
|
|
118
|
-
// Emit immediately on first warning, then debounce subsequent ones
|
|
119
|
-
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
|
120
|
-
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
121
|
-
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
122
|
-
// Update counter and exit without warning
|
|
123
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
124
|
-
process.exit(0);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Reset debounce counter
|
|
128
|
-
warnData.callsSinceWarn = 0;
|
|
129
|
-
warnData.lastLevel = currentLevel;
|
|
130
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
131
|
-
|
|
132
|
-
// Detect if GSD is active (has .planning/STATE.md in working directory)
|
|
133
|
-
const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
|
|
134
|
-
|
|
135
|
-
// On CRITICAL with active GSD project, auto-record session state as a
|
|
136
|
-
// breadcrumb for /gsd:resume-work (#1974). Fire-and-forget subprocess —
|
|
137
|
-
// doesn't block the hook or the agent. Fires ONCE per CRITICAL session,
|
|
138
|
-
// guarded by warnData.criticalRecorded to prevent repeated overwrites
|
|
139
|
-
// of the "crash moment" record on every debounce cycle.
|
|
140
|
-
if (isCritical && isGsdActive && !warnData.criticalRecorded) {
|
|
141
|
-
try {
|
|
142
|
-
// Runtime-agnostic path: this hook lives at <runtime-config>/hooks/
|
|
143
|
-
// and gsd-tools.cjs lives at <runtime-config>/gsd-core/bin/.
|
|
144
|
-
// Using __dirname makes this work on Claude Code, OpenCode, Gemini,
|
|
145
|
-
// Kilo, etc. without hardcoding ~/.claude/.
|
|
146
|
-
const gsdTools = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
|
147
|
-
// Coerce usedPct to a safe number in case bridge file is malformed
|
|
148
|
-
const safeUsedPct = Number(usedPct) || 0;
|
|
149
|
-
const stoppedAt = `context exhaustion at ${safeUsedPct}% (${new Date().toISOString().split('T')[0]})`;
|
|
150
|
-
spawn(
|
|
151
|
-
process.execPath,
|
|
152
|
-
[gsdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
|
|
153
|
-
{ cwd, detached: true, stdio: 'ignore', windowsHide: true }
|
|
154
|
-
).unref();
|
|
155
|
-
warnData.criticalRecorded = true;
|
|
156
|
-
// Persist the sentinel so subsequent debounce cycles don't re-fire
|
|
157
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
158
|
-
} catch { /* non-critical — don't let state recording break the hook */ }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Build advisory warning message (never use imperative commands that
|
|
162
|
-
// override user preferences — see #884)
|
|
163
|
-
let message;
|
|
164
|
-
if (isCritical) {
|
|
165
|
-
message = isGsdActive
|
|
166
|
-
? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
167
|
-
'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
|
|
168
|
-
'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
|
|
169
|
-
'/gsd:pause-work at the next natural stopping point.'
|
|
170
|
-
: `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
171
|
-
'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
|
|
172
|
-
'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
|
|
173
|
-
} else {
|
|
174
|
-
message = isGsdActive
|
|
175
|
-
? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
176
|
-
'Context is getting limited. Avoid starting new complex work. If not between ' +
|
|
177
|
-
'defined plan steps, inform the user so they can prepare to pause.'
|
|
178
|
-
: `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
179
|
-
'Be aware that context is getting limited. Avoid unnecessary exploration or ' +
|
|
180
|
-
'starting new complex work.';
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const output = {
|
|
184
|
-
hookSpecificOutput: {
|
|
185
|
-
hookEventName: process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse",
|
|
186
|
-
additionalContext: message
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
process.stdout.write(JSON.stringify(output));
|
|
191
|
-
} catch (e) {
|
|
192
|
-
// Silent fail -- never block tool execution
|
|
193
|
-
process.exit(0);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
22
|
+
const done = () => { clearTimeout(stdinTimeout); process.exit(0); };
|
|
23
|
+
try {
|
|
24
|
+
process.stdin.resume();
|
|
25
|
+
process.stdin.on('data', () => {});
|
|
26
|
+
process.stdin.on('end', done);
|
|
27
|
+
process.stdin.on('error', done);
|
|
28
|
+
} catch {
|
|
29
|
+
done();
|
|
30
|
+
}
|
|
@@ -1,195 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
-
// Context Monitor
|
|
4
|
-
// Reads context metrics from the statusline bridge file and injects
|
|
5
|
-
// warnings when context usage is high. This makes the AGENT aware of
|
|
6
|
-
// context limits (the statusline only shows the user).
|
|
3
|
+
// Context Monitor — DISABLED in @therocketcode/gsd-core.
|
|
7
4
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
5
|
+
// Upstream GSD shipped a PostToolUse/Stop/SubagentStop/PreCompact hook here that
|
|
6
|
+
// injected "CONTEXT WARNING / CONTEXT CRITICAL" messages into the agent's own
|
|
7
|
+
// conversation as the context window filled. This fork deliberately removes that
|
|
8
|
+
// behavior: agent-facing context warnings stress the model, derail work, and
|
|
9
|
+
// override the user's judgment about when to wrap up. The user can still see
|
|
10
|
+
// context usage passively in the statusline — that is enough.
|
|
13
11
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const path = require('path');
|
|
24
|
-
const { spawn } = require('child_process');
|
|
25
|
-
|
|
26
|
-
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
27
|
-
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
28
|
-
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
29
|
-
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
30
|
-
|
|
31
|
-
let input = '';
|
|
32
|
-
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
|
|
33
|
-
// Windows/Git Bash, or slow Claude Code piping during large outputs),
|
|
34
|
-
// exit silently instead of hanging until Claude Code kills the process
|
|
35
|
-
// and reports "hook error". See #775, #1162.
|
|
12
|
+
// This file is intentionally kept as an inert no-op rather than deleted. It stays
|
|
13
|
+
// in the managed-hooks set so that an update OVERWRITES any previously-installed
|
|
14
|
+
// active version on EXISTING installs — guaranteeing the warning behavior is gone
|
|
15
|
+
// everywhere, not just on fresh installs. It reads and discards stdin, emits
|
|
16
|
+
// nothing, and exits 0 — it never injects context into the agent and never
|
|
17
|
+
// blocks tool execution.
|
|
18
|
+
|
|
19
|
+
// Drain and discard stdin so the caller's write side never sees EPIPE, then exit
|
|
20
|
+
// cleanly. Guard against stdin never closing (pipe issues on Windows/Git Bash).
|
|
36
21
|
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
|
|
37
|
-
process.
|
|
38
|
-
|
|
39
|
-
process.stdin.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Reject session IDs that contain path traversal sequences or path separators.
|
|
50
|
-
// session_id is used to construct file paths in /tmp — an unsanitized value
|
|
51
|
-
// could escape the temp directory and read or write arbitrary files.
|
|
52
|
-
if (/[/\\]|\.\./.test(sessionId)) {
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check if context warnings are disabled via config.
|
|
57
|
-
// Collapsed existsSync+readFileSync into a single read guarded by try/catch
|
|
58
|
-
// (ENOENT or parse error → use defaults, same as old "planningDir absent" branch).
|
|
59
|
-
const cwd = data.cwd || process.cwd();
|
|
60
|
-
try {
|
|
61
|
-
const configPath = path.join(cwd, '.planning', 'config.json');
|
|
62
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
63
|
-
if (config.hooks?.context_warnings === false) {
|
|
64
|
-
process.exit(0);
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
// Missing or unparseable config → proceed with defaults (context warnings enabled)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const tmpDir = os.tmpdir();
|
|
71
|
-
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
72
|
-
|
|
73
|
-
// If no metrics file, this is a subagent or fresh session -- exit silently.
|
|
74
|
-
// Collapsed existsSync+readFileSync: ENOENT → exit 0 (identical to old !existsSync branch),
|
|
75
|
-
// other errors rethrow to the outer catch (swallowed → exit 0, as before).
|
|
76
|
-
let metricsRaw;
|
|
77
|
-
try {
|
|
78
|
-
metricsRaw = fs.readFileSync(metricsPath, 'utf8');
|
|
79
|
-
} catch (e) {
|
|
80
|
-
if (e && e.code === 'ENOENT') process.exit(0);
|
|
81
|
-
throw e;
|
|
82
|
-
}
|
|
83
|
-
const metrics = JSON.parse(metricsRaw);
|
|
84
|
-
const now = Math.floor(Date.now() / 1000);
|
|
85
|
-
|
|
86
|
-
// Ignore stale metrics
|
|
87
|
-
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const remaining = metrics.remaining_percentage;
|
|
92
|
-
const usedPct = metrics.used_pct;
|
|
93
|
-
|
|
94
|
-
// No warning needed
|
|
95
|
-
if (remaining > WARNING_THRESHOLD) {
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Debounce: check if we warned recently
|
|
100
|
-
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
|
101
|
-
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
102
|
-
let firstWarn = true;
|
|
103
|
-
|
|
104
|
-
// Collapsed existsSync+readFileSync: ENOENT or parse error → keep default warnData
|
|
105
|
-
// (same as old "file absent" branch). firstWarn tracks whether we read a valid sentinel.
|
|
106
|
-
try {
|
|
107
|
-
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
|
108
|
-
firstWarn = false;
|
|
109
|
-
} catch (e) {
|
|
110
|
-
// Missing or corrupted sentinel → firstWarn stays true, warnData stays at defaults
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
114
|
-
|
|
115
|
-
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
|
116
|
-
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
117
|
-
|
|
118
|
-
// Emit immediately on first warning, then debounce subsequent ones
|
|
119
|
-
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
|
120
|
-
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
121
|
-
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
122
|
-
// Update counter and exit without warning
|
|
123
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
124
|
-
process.exit(0);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Reset debounce counter
|
|
128
|
-
warnData.callsSinceWarn = 0;
|
|
129
|
-
warnData.lastLevel = currentLevel;
|
|
130
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
131
|
-
|
|
132
|
-
// Detect if GSD is active (has .planning/STATE.md in working directory)
|
|
133
|
-
const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
|
|
134
|
-
|
|
135
|
-
// On CRITICAL with active GSD project, auto-record session state as a
|
|
136
|
-
// breadcrumb for /gsd:resume-work (#1974). Fire-and-forget subprocess —
|
|
137
|
-
// doesn't block the hook or the agent. Fires ONCE per CRITICAL session,
|
|
138
|
-
// guarded by warnData.criticalRecorded to prevent repeated overwrites
|
|
139
|
-
// of the "crash moment" record on every debounce cycle.
|
|
140
|
-
if (isCritical && isGsdActive && !warnData.criticalRecorded) {
|
|
141
|
-
try {
|
|
142
|
-
// Runtime-agnostic path: this hook lives at <runtime-config>/hooks/
|
|
143
|
-
// and gsd-tools.cjs lives at <runtime-config>/gsd-core/bin/.
|
|
144
|
-
// Using __dirname makes this work on Claude Code, OpenCode, Gemini,
|
|
145
|
-
// Kilo, etc. without hardcoding ~/.claude/.
|
|
146
|
-
const gsdTools = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
|
|
147
|
-
// Coerce usedPct to a safe number in case bridge file is malformed
|
|
148
|
-
const safeUsedPct = Number(usedPct) || 0;
|
|
149
|
-
const stoppedAt = `context exhaustion at ${safeUsedPct}% (${new Date().toISOString().split('T')[0]})`;
|
|
150
|
-
spawn(
|
|
151
|
-
process.execPath,
|
|
152
|
-
[gsdTools, 'state', 'record-session', '--stopped-at', stoppedAt],
|
|
153
|
-
{ cwd, detached: true, stdio: 'ignore', windowsHide: true }
|
|
154
|
-
).unref();
|
|
155
|
-
warnData.criticalRecorded = true;
|
|
156
|
-
// Persist the sentinel so subsequent debounce cycles don't re-fire
|
|
157
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
158
|
-
} catch { /* non-critical — don't let state recording break the hook */ }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Build advisory warning message (never use imperative commands that
|
|
162
|
-
// override user preferences — see #884)
|
|
163
|
-
let message;
|
|
164
|
-
if (isCritical) {
|
|
165
|
-
message = isGsdActive
|
|
166
|
-
? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
167
|
-
'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
|
|
168
|
-
'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
|
|
169
|
-
'/gsd:pause-work at the next natural stopping point.'
|
|
170
|
-
: `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
171
|
-
'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
|
|
172
|
-
'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
|
|
173
|
-
} else {
|
|
174
|
-
message = isGsdActive
|
|
175
|
-
? `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
176
|
-
'Context is getting limited. Avoid starting new complex work. If not between ' +
|
|
177
|
-
'defined plan steps, inform the user so they can prepare to pause.'
|
|
178
|
-
: `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
179
|
-
'Be aware that context is getting limited. Avoid unnecessary exploration or ' +
|
|
180
|
-
'starting new complex work.';
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const output = {
|
|
184
|
-
hookSpecificOutput: {
|
|
185
|
-
hookEventName: process.env.GEMINI_API_KEY ? "AfterTool" : "PostToolUse",
|
|
186
|
-
additionalContext: message
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
process.stdout.write(JSON.stringify(output));
|
|
191
|
-
} catch (e) {
|
|
192
|
-
// Silent fail -- never block tool execution
|
|
193
|
-
process.exit(0);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
22
|
+
const done = () => { clearTimeout(stdinTimeout); process.exit(0); };
|
|
23
|
+
try {
|
|
24
|
+
process.stdin.resume();
|
|
25
|
+
process.stdin.on('data', () => {});
|
|
26
|
+
process.stdin.on('end', done);
|
|
27
|
+
process.stdin.on('error', done);
|
|
28
|
+
} catch {
|
|
29
|
+
done();
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
3
|
+
// Context Monitor — INTENTIONALLY DISABLED in @therocketcode/gsd-core.
|
|
4
|
+
//
|
|
5
|
+
// Upstream GSD shipped a PostToolUse / Stop / SubagentStop / PreCompact hook
|
|
6
|
+
// here that read context-usage metrics and injected "CONTEXT WARNING" /
|
|
7
|
+
// "CONTEXT CRITICAL" messages into the AGENT's conversation when remaining
|
|
8
|
+
// context dropped below 35% / 25%. This fork removes that behavior on purpose:
|
|
9
|
+
// agent-facing context warnings stress the model and pre-empt the user's own
|
|
10
|
+
// judgment about when to wrap up. Context usage is still visible to the USER in
|
|
11
|
+
// the statusline — that passive display is unaffected.
|
|
12
|
+
//
|
|
13
|
+
// Why a no-op stub instead of deleting the file: this hook stays in the managed
|
|
14
|
+
// hook set, so a GSD update OVERWRITES any previously-installed active version
|
|
15
|
+
// with this inert one. That guarantees the warning behavior is gone on EVERY
|
|
16
|
+
// install — existing and fresh — not just new ones. (Deleting the file would
|
|
17
|
+
// orphan the old active copy on existing installs, leaving it firing.)
|
|
18
|
+
//
|
|
19
|
+
// Contract: drain stdin (so the caller's write side never hits EPIPE), emit
|
|
20
|
+
// nothing, exit 0. Never blocks tool execution. No fs reads, no subprocess.
|
|
21
|
+
|
|
22
|
+
// Guard against stdin never closing (pipe quirks on Windows / Git Bash, slow
|
|
23
|
+
// piping during large tool outputs) — exit silently rather than hang. See
|
|
24
|
+
// #775, #1162 for the original rationale.
|
|
25
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
|
|
26
|
+
|
|
27
|
+
function done() {
|
|
28
|
+
clearTimeout(stdinTimeout);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
process.stdin.resume();
|
|
34
|
+
process.stdin.on('data', () => {}); // discard input
|
|
35
|
+
process.stdin.on('end', done);
|
|
36
|
+
process.stdin.on('error', done);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
done();
|
|
39
|
+
}
|
package/package.json
CHANGED