@tekyzinc/gsd-t 3.29.10 → 4.0.12

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.
Files changed (112) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/bin/gsd-t-parallel.cjs +64 -13
  3. package/bin/gsd-t-test-data-adapters/file-json-array.cjs +27 -3
  4. package/bin/gsd-t-test-data-adapters/localstorage-key-prefix.cjs +6 -1
  5. package/bin/gsd-t-test-data-adapters/sqlite-table-where.cjs +16 -1
  6. package/bin/gsd-t-test-data-ledger.cjs +1 -0
  7. package/bin/gsd-t.js +48 -376
  8. package/bin/journey-coverage.cjs +6 -3
  9. package/bin/parallel-cli.cjs +13 -1
  10. package/commands/gsd-t-debug.md +47 -533
  11. package/commands/gsd-t-design-decompose.md +24 -497
  12. package/commands/gsd-t-doc-ripple.md +23 -139
  13. package/commands/gsd-t-execute.md +41 -958
  14. package/commands/gsd-t-help.md +0 -2
  15. package/commands/gsd-t-impact.md +26 -295
  16. package/commands/gsd-t-integrate.md +44 -369
  17. package/commands/gsd-t-milestone.md +25 -124
  18. package/commands/gsd-t-partition.md +28 -539
  19. package/commands/gsd-t-plan.md +26 -472
  20. package/commands/gsd-t-prd.md +25 -311
  21. package/commands/gsd-t-quick.md +1 -1
  22. package/commands/gsd-t-resume.md +0 -33
  23. package/commands/gsd-t-verify.md +52 -569
  24. package/commands/gsd-t-wave.md +41 -430
  25. package/package.json +1 -1
  26. package/scripts/gsd-t-calibration-hook.js +3 -1
  27. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  28. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  29. package/templates/CLAUDE-global.md +43 -173
  30. package/templates/CLAUDE-project.md +118 -104
  31. package/templates/prompts/design-verify-subagent.md +3 -0
  32. package/templates/prompts/qa-subagent.md +3 -0
  33. package/templates/prompts/red-team-subagent.md +5 -2
  34. package/templates/workflows/_lib.js +176 -0
  35. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  36. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  37. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  38. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  39. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  40. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  41. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  42. package/bin/check-headless-sessions.js +0 -140
  43. package/bin/context-budget-audit.cjs +0 -447
  44. package/bin/context-meter-config.cjs +0 -101
  45. package/bin/context-meter-config.test.cjs +0 -101
  46. package/bin/event-stream.cjs +0 -205
  47. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  48. package/bin/gsd-t-capture-lint.cjs +0 -440
  49. package/bin/gsd-t-economics.cjs +0 -315
  50. package/bin/gsd-t-in-session-usage.cjs +0 -213
  51. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  52. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  53. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  54. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  55. package/bin/gsd-t-orchestrator.js +0 -535
  56. package/bin/gsd-t-parallel-probe.cjs +0 -132
  57. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  58. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  59. package/bin/gsd-t-report-tokens.cjs +0 -549
  60. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  61. package/bin/gsd-t-token-backfill.cjs +0 -366
  62. package/bin/gsd-t-token-capture.cjs +0 -321
  63. package/bin/gsd-t-token-dashboard.cjs +0 -353
  64. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  65. package/bin/gsd-t-tool-attribution.cjs +0 -377
  66. package/bin/gsd-t-tool-cost.cjs +0 -195
  67. package/bin/gsd-t-transcript-tee.cjs +0 -246
  68. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  69. package/bin/gsd-t-unattended-platform.cjs +0 -551
  70. package/bin/gsd-t-unattended-platform.js +0 -381
  71. package/bin/gsd-t-unattended-safety.cjs +0 -773
  72. package/bin/gsd-t-unattended-safety.js +0 -788
  73. package/bin/gsd-t-unattended.cjs +0 -2109
  74. package/bin/gsd-t-unattended.js +0 -1367
  75. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  76. package/bin/handoff-lock.cjs +0 -249
  77. package/bin/handoff-lock.js +0 -249
  78. package/bin/headless-auto-spawn.cjs +0 -711
  79. package/bin/headless-auto-spawn.js +0 -373
  80. package/bin/headless-exit-codes.cjs +0 -67
  81. package/bin/live-activity-report.cjs +0 -615
  82. package/bin/log-tail.cjs +0 -81
  83. package/bin/m44-proof-measure.cjs +0 -285
  84. package/bin/m46-iter-proof.cjs +0 -149
  85. package/bin/m46-worker-proof.cjs +0 -201
  86. package/bin/m55-substrate-proof.cjs +0 -134
  87. package/bin/metrics-rollup.js +0 -200
  88. package/bin/model-windows.cjs +0 -99
  89. package/bin/model-windows.test.cjs +0 -75
  90. package/bin/parallelism-report.cjs +0 -535
  91. package/bin/runway-estimator.cjs +0 -242
  92. package/bin/spawn-plan-derive.cjs +0 -163
  93. package/bin/spawn-plan-status-updater.cjs +0 -292
  94. package/bin/spawn-plan-writer.cjs +0 -204
  95. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  96. package/bin/token-budget.cjs +0 -265
  97. package/bin/unattended-watch-format.cjs +0 -178
  98. package/bin/watch-progress.js +0 -155
  99. package/commands/gsd-t-unattended-stop.md +0 -85
  100. package/commands/gsd-t-unattended-watch.md +0 -465
  101. package/commands/gsd-t-unattended.md +0 -476
  102. package/commands/gsd-t-visualize.md +0 -116
  103. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  104. package/scripts/gsd-t-context-meter.js +0 -341
  105. package/scripts/gsd-t-context-meter.test.js +0 -471
  106. package/scripts/gsd-t-dashboard-server.js +0 -1203
  107. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  108. package/scripts/gsd-t-transcript.html +0 -1821
  109. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  110. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  111. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  112. package/templates/hooks/post-commit-spawn-plan.sh +0 -85
@@ -1,373 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * GSD-T Headless Auto-Spawn — Detached headless continuation
5
- *
6
- * When the runway estimator refuses a run (projected context ≥ stop band),
7
- * the caller invokes `autoSpawnHeadless()` to hand off to a detached child
8
- * process running `gsd-t headless {command} --log`. The interactive session
9
- * never blocks on the child (`child.unref()`), so the user retains their
10
- * terminal and can work on unrelated tasks. On child completion, a macOS
11
- * notification fires (T2). The interactive session surfaces the result via
12
- * a read-back banner on the next `gsd-t-resume` or `gsd-t-status` call (T4).
13
- *
14
- * Zero external dependencies (Node.js built-ins only).
15
- *
16
- * Contract: .gsd-t/contracts/headless-auto-spawn-contract.md v1.0.0
17
- * Consumers: commands/gsd-t-execute|wave|integrate|quick|debug.md (via runway
18
- * estimator handoff), bin/runway-estimator.js (conceptual target).
19
- */
20
-
21
- const fs = require("fs");
22
- const path = require("path");
23
- const { spawn } = require("child_process");
24
- const {
25
- acquireHandoffLock,
26
- releaseHandoffLock,
27
- // waitForLockRelease — NOT consumed here. The child-side wait is
28
- // performed by `commands/gsd-t-resume.md` Step 0 (wired in m36
29
- // watch-loop Task 4); it calls waitForLockRelease(projectDir,
30
- // sessionId) before reading the continue-here file.
31
- } = require("./handoff-lock.js");
32
-
33
- // ── Constants ────────────────────────────────────────────────────────────────
34
-
35
- const SESSIONS_DIR_REL = path.join(".gsd-t", "headless-sessions");
36
- const LOG_DIR_REL = ".gsd-t"; // headless-{id}.log lives directly in .gsd-t
37
-
38
- // ── Exports ──────────────────────────────────────────────────────────────────
39
-
40
- module.exports = {
41
- autoSpawnHeadless,
42
- makeSessionId,
43
- writeSessionFile,
44
- writeContinueHereFile,
45
- markSessionCompleted,
46
- // M43 D4 — channel-separation invariant; see .cjs twin.
47
- shouldSpawnHeadless: () => true,
48
- };
49
-
50
- // M43 D4 — one-shot deprecation banner (legacy `.js` copy mirrors `.cjs`).
51
- let _deprecatedWatchWarned = false;
52
-
53
- // ── autoSpawnHeadless ────────────────────────────────────────────────────────
54
-
55
- /**
56
- * @param {{
57
- * command: string,
58
- * args?: string[],
59
- * continue_from?: string,
60
- * projectDir?: string,
61
- * context?: object,
62
- * sessionContext?: object,
63
- * sessionId?: string,
64
- * watch?: boolean,
65
- * spawnType?: 'primary' | 'validation'
66
- * }} opts
67
- * @returns {{ id: string | null, pid: number | null, logPath: string | null, timestamp: string, mode: 'headless' | 'in-context' }}
68
- */
69
- function autoSpawnHeadless(opts) {
70
- const command = opts.command;
71
- const args = opts.args || [];
72
- const continue_from = opts.continue_from || ".";
73
- const projectDir = opts.projectDir || process.cwd();
74
- const context = opts.context || opts.sessionContext || null;
75
- // M43 D4 — `watch`/`inSession` ignored under v2.0.0 channel-separation.
76
- const legacyWatch = opts.watch === true;
77
- const legacyInSession = opts.inSession === true;
78
- if ((legacyWatch || legacyInSession) && !_deprecatedWatchWarned) {
79
- _deprecatedWatchWarned = true;
80
- try {
81
- process.stderr.write(
82
- "[headless-default] `watch`/`inSession` flag is deprecated under headless-default-contract v2.0.0 — every spawn is headless; caller hint ignored.\n",
83
- );
84
- } catch (_) {
85
- /* best-effort */
86
- }
87
- }
88
- const spawnType = opts.spawnType || "primary";
89
-
90
- if (!command || typeof command !== "string") {
91
- throw new Error("autoSpawnHeadless: `command` is required");
92
- }
93
- if (spawnType !== "primary" && spawnType !== "validation") {
94
- throw new Error(
95
- `autoSpawnHeadless: \`spawnType\` must be 'primary' or 'validation' (got ${JSON.stringify(spawnType)})`,
96
- );
97
- }
98
-
99
- const timestamp = new Date().toISOString();
100
- const id = makeSessionId(command, new Date());
101
- const logPath = path.join(projectDir, LOG_DIR_REL, `headless-${id}.log`);
102
-
103
- ensureDir(path.join(projectDir, LOG_DIR_REL));
104
- ensureDir(path.join(projectDir, SESSIONS_DIR_REL));
105
-
106
- // Handoff-lock gate (m36 gap-fix T2). Only engaged when the caller
107
- // supplies a `sessionId` — existing callers that do not pass one keep
108
- // the pre-m36 behavior unchanged. When engaged, the lock is held
109
- // across writeContinueHereFile + spawn so a child that is already
110
- // waiting via `waitForLockRelease()` (see commands/gsd-t-resume.md
111
- // Step 0, wired by m36 watch-loop T4) cannot read a half-written
112
- // continue-here file. See .gsd-t/contracts/headless-auto-spawn-contract.md
113
- // v1.0.0 (implementation-detail primitive; no contract bump).
114
- const lockSessionId = typeof opts.sessionId === "string" && opts.sessionId
115
- ? opts.sessionId
116
- : null;
117
- let lockHandle = null;
118
- if (lockSessionId) {
119
- lockHandle = acquireHandoffLock(projectDir, lockSessionId);
120
- }
121
-
122
- let pid = 0;
123
- try {
124
- // Open log file descriptor before spawning — child writes directly.
125
- const logFd = fs.openSync(logPath, "a");
126
-
127
- // Headless invocation: `node bin/gsd-t.js headless <command> [args] --log`
128
- // The `gsd-t` CLI entry point is bin/gsd-t.js relative to projectDir.
129
- const gsdtCli = path.join(projectDir, "bin", "gsd-t.js");
130
- const childArgs = [gsdtCli, "headless", stripGsdtPrefix(command), ...args, "--log"];
131
-
132
- // v3.12.14 — env-var propagation for telemetry tagging (mirror of the
133
- // authoritative .cjs variant). PRODUCTION consumers import the .cjs; this
134
- // .js copy is retained only to keep the legacy test green.
135
- const workerEnv = Object.assign({}, process.env, {
136
- GSD_T_COMMAND: command,
137
- GSD_T_PHASE: process.env.GSD_T_PHASE || "execute",
138
- GSD_T_PROJECT_DIR: process.env.GSD_T_PROJECT_DIR || projectDir,
139
- });
140
- if (process.env.GSD_T_TRACE_ID) workerEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
141
- if (process.env.GSD_T_MODEL) workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
142
-
143
- const child = spawn("node", childArgs, {
144
- cwd: projectDir,
145
- detached: true,
146
- stdio: ["ignore", logFd, logFd],
147
- env: workerEnv,
148
- });
149
-
150
- child.unref();
151
- fs.closeSync(logFd);
152
-
153
- pid = child.pid || 0;
154
-
155
- writeSessionFile(projectDir, {
156
- id,
157
- pid,
158
- logPath: path.relative(projectDir, logPath),
159
- startTimestamp: timestamp,
160
- command,
161
- args,
162
- status: "running",
163
- continueFromPath: continue_from,
164
- surfaced: false,
165
- });
166
-
167
- writeContinueHereFile(projectDir, id, context);
168
- } finally {
169
- // Release the lock AFTER the child is confirmed started and
170
- // handoff artifacts are on disk. The finally ensures a spawn
171
- // failure still frees the slot for a retry.
172
- if (lockHandle) {
173
- try {
174
- releaseHandoffLock(lockHandle);
175
- } catch (_) {
176
- /* idempotent best-effort */
177
- }
178
- }
179
- }
180
-
181
- // T2 — install completion watcher. Non-blocking (setImmediate) so the
182
- // caller's return is not delayed. The watcher uses `child.on('exit')` on
183
- // a separately-spawned bridge process; here we defer to fs.watchFile for
184
- // a detached approach that survives even after the parent's `unref()`.
185
- installCompletionWatcher({ projectDir, id, logPath, pid, startTimestamp: timestamp });
186
-
187
- return {
188
- id,
189
- pid,
190
- logPath: path.relative(projectDir, logPath),
191
- timestamp,
192
- mode: "headless",
193
- };
194
- }
195
-
196
- // ── makeSessionId ────────────────────────────────────────────────────────────
197
-
198
- /**
199
- * @param {string} command
200
- * @param {Date} [now]
201
- * @returns {string} e.g., "gsd-t-execute-2026-04-15-01-23-45"
202
- */
203
- function makeSessionId(command, now) {
204
- const d = now || new Date();
205
- const pad = (n) => String(n).padStart(2, "0");
206
- const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
207
- const time = `${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
208
- const base = stripGsdtPrefix(command) || command;
209
- return `gsd-t-${base}-${date}-${time}`;
210
- }
211
-
212
- // ── writeSessionFile ─────────────────────────────────────────────────────────
213
-
214
- function writeSessionFile(projectDir, session) {
215
- const fp = path.join(projectDir, SESSIONS_DIR_REL, `${session.id}.json`);
216
- ensureDir(path.dirname(fp));
217
- fs.writeFileSync(fp, JSON.stringify(session, null, 2) + "\n");
218
- return fp;
219
- }
220
-
221
- // ── writeContinueHereFile ────────────────────────────────────────────────────
222
-
223
- function writeContinueHereFile(projectDir, id, context) {
224
- const fp = path.join(projectDir, SESSIONS_DIR_REL, `${id}-context.json`);
225
- const payload = context || buildContextSnapshot(projectDir);
226
- fs.writeFileSync(fp, JSON.stringify(payload, null, 2) + "\n");
227
- return fp;
228
- }
229
-
230
- function buildContextSnapshot(projectDir) {
231
- // Best-effort snapshot of current GSD-T state at handoff time.
232
- const snap = {
233
- capturedAt: new Date().toISOString(),
234
- progress: null,
235
- currentDomain: null,
236
- pendingTasks: [],
237
- lastDecisionLogEntry: null,
238
- currentWave: null,
239
- };
240
- try {
241
- const progressFp = path.join(projectDir, ".gsd-t", "progress.md");
242
- if (fs.existsSync(progressFp)) {
243
- const raw = fs.readFileSync(progressFp, "utf8");
244
- snap.progress = firstNLines(raw, 20);
245
- // Pull the last Decision Log entry (last non-empty bullet line).
246
- const lines = raw.split("\n");
247
- for (let i = lines.length - 1; i >= 0; i--) {
248
- const ln = lines[i].trim();
249
- if (ln.startsWith("- ")) {
250
- snap.lastDecisionLogEntry = ln;
251
- break;
252
- }
253
- }
254
- }
255
- } catch (_) {
256
- /* best-effort */
257
- }
258
- return snap;
259
- }
260
-
261
- function firstNLines(s, n) {
262
- return s.split("\n").slice(0, n).join("\n");
263
- }
264
-
265
- // ── markSessionCompleted ─────────────────────────────────────────────────────
266
-
267
- /**
268
- * T2 completion hook — updates the session file in place.
269
- * @param {string} projectDir
270
- * @param {string} id
271
- * @param {{ exitCode: number, endTimestamp?: string }} result
272
- */
273
- function markSessionCompleted(projectDir, id, result) {
274
- const fp = path.join(projectDir, SESSIONS_DIR_REL, `${id}.json`);
275
- if (!fs.existsSync(fp)) return;
276
- try {
277
- const s = JSON.parse(fs.readFileSync(fp, "utf8"));
278
- s.status = "completed";
279
- s.exitCode = result.exitCode;
280
- s.endTimestamp = result.endTimestamp || new Date().toISOString();
281
- fs.writeFileSync(fp, JSON.stringify(s, null, 2) + "\n");
282
- } catch (_) {
283
- /* ignore */
284
- }
285
- }
286
-
287
- // ── Completion watcher (T2) — macOS notification on exit ─────────────────────
288
-
289
- function installCompletionWatcher(opts) {
290
- const { projectDir, id, pid, startTimestamp } = opts;
291
- if (!pid || pid <= 0) return;
292
-
293
- // Poll-based watcher. We can't hold a reference to the child (it's unref'd
294
- // and detached), so we poll `process.kill(pid, 0)` which throws if the
295
- // process is gone. This is cheap and survives across detachment.
296
- const POLL_MS = 2000;
297
- const MAX_WAIT_MS = 60 * 60 * 1000; // 1 hour safety cap
298
- const startMs = Date.now();
299
-
300
- const timer = setInterval(() => {
301
- let alive = false;
302
- try {
303
- process.kill(pid, 0);
304
- alive = true;
305
- } catch (_) {
306
- alive = false;
307
- }
308
- if (!alive) {
309
- clearInterval(timer);
310
- // Exit code is unknown from a signal-based probe. Best-effort: read
311
- // the log's last lines to guess, otherwise default to 0.
312
- const exitCode = guessExitCodeFromLog(projectDir, id);
313
- markSessionCompleted(projectDir, id, {
314
- exitCode,
315
- endTimestamp: new Date().toISOString(),
316
- });
317
- fireMacNotification({ id, command: extractCommand(id), startTimestamp });
318
- } else if (Date.now() - startMs > MAX_WAIT_MS) {
319
- clearInterval(timer);
320
- }
321
- }, POLL_MS);
322
-
323
- // Let the timer not block the parent's exit.
324
- if (typeof timer.unref === "function") timer.unref();
325
- }
326
-
327
- function guessExitCodeFromLog(projectDir, id) {
328
- try {
329
- const fp = path.join(projectDir, LOG_DIR_REL, `headless-${id}.log`);
330
- if (!fs.existsSync(fp)) return 0;
331
- const raw = fs.readFileSync(fp, "utf8");
332
- if (/exit code[: ]+(\d+)/i.test(raw)) {
333
- const m = raw.match(/exit code[: ]+(\d+)/i);
334
- return parseInt(m[1], 10);
335
- }
336
- return 0;
337
- } catch (_) {
338
- return 0;
339
- }
340
- }
341
-
342
- function extractCommand(id) {
343
- // id format: gsd-t-{command}-{date}-{time}
344
- const m = id.match(/^gsd-t-(.+?)-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}$/);
345
- return m ? m[1] : id;
346
- }
347
-
348
- function fireMacNotification({ id, command }) {
349
- if (process.platform !== "darwin") return;
350
- try {
351
- const { spawn } = require("child_process");
352
- const msg = `GSD-T headless run complete: ${id}`;
353
- const script = `display notification "${msg}" with title "GSD-T" subtitle "${command}"`;
354
- const child = spawn("osascript", ["-e", script], {
355
- detached: true,
356
- stdio: "ignore",
357
- });
358
- child.unref();
359
- } catch (_) {
360
- /* graceful degradation */
361
- }
362
- }
363
-
364
- // ── Helpers ──────────────────────────────────────────────────────────────────
365
-
366
- function ensureDir(d) {
367
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
368
- }
369
-
370
- function stripGsdtPrefix(command) {
371
- if (typeof command !== "string") return "";
372
- return command.replace(/^gsd-t-/, "");
373
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Shared helper: map a `claude -p` process exit code + output to the
3
- * GSD-T headless exit-code contract.
4
- *
5
- * Lives in its own file so non-entry modules (e.g. gsd-t-unattended.cjs)
6
- * can require it without pulling in the full CLI at bin/gsd-t.js. That
7
- * decoupling lets PROJECT_BIN_TOOLS ship the supervisor without also
8
- * vendoring the CLI itself — projects resolve `gsd-t` from the global
9
- * install, not from a project-local copy.
10
- *
11
- * Exit codes (contract):
12
- * 0 — success
13
- * 1 — verification/test failure
14
- * 2 — context budget exceeded
15
- * 3 — non-zero process exit (other)
16
- * 4 — blocked / needs human
17
- * 5 — unknown slash command (claude -p exited 0 but rejected the prompt)
18
- */
19
-
20
- "use strict";
21
-
22
- // Match terminal markers, not narration. A bare "tests failed" substring will
23
- // appear in healthy output ("0 tests failed", "no tests failed", quoted as an
24
- // example in prose). Require either a non-zero count prefix or a structured
25
- // terminal marker (start of line, uppercase-FAIL prefix, Jest-style summary).
26
- // Bug history: M45 worker output contained "tests failed" 6× in narration,
27
- // causing the supervisor to map exit 0 → exit 1 and halt a successful run.
28
-
29
- const NONZERO_FAILURE_COUNT_RE =
30
- /(?:^|\b)([1-9]\d*)\s+(?:tests?|specs?|assertions?|examples?|suites?)\s+failed\b/i;
31
- const STRUCTURED_FAIL_RE = /^FAIL[:\s]/m;
32
- const JEST_SUMMARY_FAIL_RE = /^Tests:\s+\d+\s+failed/im;
33
-
34
- // Verification-phrase matchers: require the phrase at a line boundary or
35
- // preceded by a sentence-start punctuation — not mid-prose. Each phrase is
36
- // distinctive enough that start-of-line / post-punctuation is a reliable
37
- // terminal-marker signal.
38
- const VERIFICATION_FAILED_RE =
39
- /(?:^|[.!?]\s+)(?:verification|verify|quality gate)\s+failed\b/im;
40
-
41
- // Context-budget phrases — same polarity discipline. Tolerant of surrounding
42
- // punctuation (— / :) but requires the phrase at a line boundary.
43
- const CONTEXT_BUDGET_RE =
44
- /(?:^|[.!?]\s+)(?:context budget exceeded|context window exceeded|budget exceeded|token limit)\b/im;
45
-
46
- // Blocker compound: "blocked" within 80 chars of a human-gate phrase, both
47
- // anchored to recognizable boundaries. The 80-char proximity keeps unrelated
48
- // mentions from compounding.
49
- const BLOCKED_HUMAN_RE =
50
- /\bblocked\b[\s\S]{0,80}?\b(?:needs? human|human input|human approval)\b/i;
51
-
52
- function mapHeadlessExitCode(processExitCode, output) {
53
- if (processExitCode !== 0 && processExitCode !== null) return 3;
54
- const raw = output || "";
55
- if (/^unknown command:/im.test(raw)) return 5;
56
- if (CONTEXT_BUDGET_RE.test(raw)) return 2;
57
- if (BLOCKED_HUMAN_RE.test(raw)) return 4;
58
- if (
59
- VERIFICATION_FAILED_RE.test(raw) ||
60
- NONZERO_FAILURE_COUNT_RE.test(raw) ||
61
- STRUCTURED_FAIL_RE.test(raw) ||
62
- JEST_SUMMARY_FAIL_RE.test(raw)
63
- ) return 1;
64
- return 0;
65
- }
66
-
67
- module.exports = { mapHeadlessExitCode };