@therocketcode/gsd-core 1.7.0 → 1.7.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.7.0",
4
+ "version": "1.7.2",
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/README.md CHANGED
@@ -74,6 +74,20 @@ Once installed, start your first project:
74
74
 
75
75
  New here? Follow [Your first project](docs/tutorials/your-first-project.md) for a guided walkthrough from install to first shipped phase.
76
76
 
77
+ ### Installing & updating
78
+
79
+ How you get the latest version depends on what you already have installed:
80
+
81
+ - **Nothing installed yet** — run the Quickstart command once:
82
+ ```bash
83
+ npx -y @therocketcode/gsd-core@latest --claude --global # or --local for a single project
84
+ ```
85
+ After that you're on the self-update path below.
86
+
87
+ - **Already have this package (`@therocketcode/gsd-core`)** — just run `/gsd-update` in your session. The package coordinate is baked into the install, so a SessionStart check surfaces a banner ("GSD update available: X → Y. Run /gsd:update.") and `/gsd-update` pulls the new version. No special command, no reinstall.
88
+
89
+ - **Coming from the original upstream GSD (`@opengsd/gsd-core` / `get-shit-done`)** — its `/gsd-update` points at the upstream package and will never find this fork (different npm coordinate). Switch with a one-time install using the Quickstart command above. The installer overwrites the same-named `/gsd-*` files, re-points the baked identity at this fork (so future `/gsd-update` works), and the bundled legacy-cleanup removes superseded upstream hooks and stale update-check caches. After that, the self-update path applies.
90
+
77
91
  ---
78
92
 
79
93
  ## Documentation
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
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
  }
@@ -21,12 +21,40 @@ const fs = require('fs');
21
21
  // ─── Constants ───────────────────────────────────────────────────────────────
22
22
 
23
23
  /**
24
- * Substring that identifies a file as belonging to the old package.
25
- * Assembled from parts so this source file itself never contains the literal
24
+ * Substrings that identify a code file as belonging to a superseded GSD
25
+ * package either the very-old pre-rename original or the rug-pulled upstream
26
+ * this project forked away from. A scanned code file (.js/.cjs/.mjs/.sh under
27
+ * hooks/ or commands/) containing ANY of these is a genuine leftover from a
28
+ * package this install replaces, and is safe to flag.
29
+ *
30
+ * Each is assembled from parts so THIS source file never contains the literal
26
31
  * as a plain substring (avoids self-flagging if the content scan were ever
27
- * widened back to include this subtree).
32
+ * widened to include the gsd-core/ subtree). Our own shipped code contains
33
+ * none of these — the package-identity drift lint guards that — so flagging
34
+ * them can never delete a freshly-installed @therocketcode file.
35
+ *
36
+ * - 'gsd-core-cc' — the pre-rename original package.
37
+ * - '@opengsd/gsd-core' — the upstream npm coordinate (rug-pulled fork source).
38
+ * - 'open-gsd/gsd-core' — the upstream GitHub repo slug.
39
+ */
40
+ const OLD_PACKAGE_SIGNALS = [
41
+ 'gsd-core' + '-cc',
42
+ '@opengsd' + '/gsd-core',
43
+ 'open-gsd' + '/gsd-core',
44
+ ];
45
+
46
+ /**
47
+ * Update-check cache files written by superseded packages, removed so a stale
48
+ * cache can't suppress or misreport update availability after switching.
49
+ * NEVER include the current package's own cache
50
+ * (`gsd-update-check-therocketcode-gsd-core.json`).
51
+ * - 'gsd-update-check.json' — the old shared (pre-per-package) cache.
52
+ * - 'gsd-update-check-opengsd-gsd-core.json' — the upstream @opengsd per-package cache.
28
53
  */
29
- const OLD_PACKAGE_SIGNAL = 'gsd-core' + '-cc';
54
+ const LEGACY_CACHE_FILENAMES = [
55
+ 'gsd-update-check.json',
56
+ 'gsd-update-check-opengsd-gsd-core.json',
57
+ ];
30
58
 
31
59
  /**
32
60
  * Subtrees within a configDir that GSD actively scans for old-package content.
@@ -97,7 +125,7 @@ function collectFilesUnder(dir, fsMod) {
97
125
  }
98
126
 
99
127
  /**
100
- * Return true if the file at `absPath` contains the old-package substring.
128
+ * Return true if the file at `absPath` contains ANY superseded-package signal.
101
129
  * Skips unreadable files (returns false on any error).
102
130
  *
103
131
  * @param {string} absPath
@@ -107,7 +135,7 @@ function collectFilesUnder(dir, fsMod) {
107
135
  function fileContainsOldPackageSignal(absPath, fsMod) {
108
136
  try {
109
137
  const content = fsMod.readFileSync(absPath, 'utf8');
110
- return content.includes(OLD_PACKAGE_SIGNAL);
138
+ return OLD_PACKAGE_SIGNALS.some((signal) => content.includes(signal));
111
139
  } catch {
112
140
  return false;
113
141
  }
@@ -166,15 +194,17 @@ function planLegacyCleanup(configDirs, opts = {}) {
166
194
  }
167
195
  }
168
196
 
169
- // Legacy shared cache (fixed name from the old package)
170
- const legacyCachePath = path.join(homeDir, '.cache', 'gsd', 'gsd-update-check.json');
171
- try {
172
- const stat = fsMod.statSync(legacyCachePath);
173
- if (stat.isFile()) {
174
- addCandidate(legacyCachePath, 'legacy-shared-cache');
197
+ // Update-check caches written by superseded packages (never the current one)
198
+ for (const cacheName of LEGACY_CACHE_FILENAMES) {
199
+ const legacyCachePath = path.join(homeDir, '.cache', 'gsd', cacheName);
200
+ try {
201
+ const stat = fsMod.statSync(legacyCachePath);
202
+ if (stat.isFile()) {
203
+ addCandidate(legacyCachePath, 'legacy-shared-cache');
204
+ }
205
+ } catch {
206
+ // absent — skip
175
207
  }
176
- } catch {
177
- // absent — skip
178
208
  }
179
209
 
180
210
  // Sort deterministically by path
@@ -1,195 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  // gsd-hook-version: {{GSD_VERSION}}
3
- // Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
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
- // How it works:
9
- // 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
10
- // 2. This hook reads those metrics after each tool use
11
- // 3. When remaining context drops below thresholds, it injects a warning
12
- // as additionalContext, which the agent sees in its conversation
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
- // Thresholds:
15
- // WARNING (remaining <= 35%): Agent should wrap up current task
16
- // CRITICAL (remaining <= 25%): Agent should stop immediately and save state
17
- //
18
- // Debounce: 5 tool uses between warnings to avoid spam
19
- // Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
20
-
21
- const fs = require('fs');
22
- const os = require('os');
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.stdin.setEncoding('utf8');
38
- process.stdin.on('data', chunk => input += chunk);
39
- process.stdin.on('end', () => {
40
- clearTimeout(stdinTimeout);
41
- try {
42
- const data = JSON.parse(input);
43
- const sessionId = data.session_id;
44
-
45
- if (!sessionId) {
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 - PostToolUse/AfterTool hook (Gemini uses AfterTool)
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
- // How it works:
9
- // 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
10
- // 2. This hook reads those metrics after each tool use
11
- // 3. When remaining context drops below thresholds, it injects a warning
12
- // as additionalContext, which the agent sees in its conversation
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
- // Thresholds:
15
- // WARNING (remaining <= 35%): Agent should wrap up current task
16
- // CRITICAL (remaining <= 25%): Agent should stop immediately and save state
17
- //
18
- // Debounce: 5 tool uses between warnings to avoid spam
19
- // Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
20
-
21
- const fs = require('fs');
22
- const os = require('os');
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.stdin.setEncoding('utf8');
38
- process.stdin.on('data', chunk => input += chunk);
39
- process.stdin.on('end', () => {
40
- clearTimeout(stdinTimeout);
41
- try {
42
- const data = JSON.parse(input);
43
- const sessionId = data.session_id;
44
-
45
- if (!sessionId) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@therocketcode/gsd-core",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "bin": {
6
6
  "gsd-core": "bin/install.js",