@tekyzinc/gsd-t 3.29.11 → 4.0.14

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 (114) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/bin/gsd-t-parallel.cjs +64 -13
  3. package/bin/gsd-t.js +48 -376
  4. package/bin/journey-coverage.cjs +6 -3
  5. package/bin/parallel-cli.cjs +13 -1
  6. package/commands/gsd-t-complete-milestone.md +6 -12
  7. package/commands/gsd-t-debug.md +47 -533
  8. package/commands/gsd-t-design-decompose.md +24 -497
  9. package/commands/gsd-t-doc-ripple.md +23 -139
  10. package/commands/gsd-t-execute.md +41 -958
  11. package/commands/gsd-t-feature.md +2 -6
  12. package/commands/gsd-t-gap-analysis.md +3 -6
  13. package/commands/gsd-t-help.md +2 -4
  14. package/commands/gsd-t-impact.md +26 -295
  15. package/commands/gsd-t-init-scan-setup.md +7 -7
  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-scan.md +45 -652
  24. package/commands/gsd-t-verify.md +52 -569
  25. package/commands/gsd-t-wave.md +41 -430
  26. package/package.json +1 -1
  27. package/scripts/gsd-t-calibration-hook.js +3 -1
  28. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  29. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  30. package/templates/CLAUDE-global.md +44 -173
  31. package/templates/CLAUDE-project.md +118 -104
  32. package/templates/prompts/design-verify-subagent.md +3 -0
  33. package/templates/prompts/qa-subagent.md +3 -0
  34. package/templates/prompts/red-team-subagent.md +5 -2
  35. package/templates/workflows/_lib.js +176 -0
  36. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  37. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  38. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  39. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  40. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  41. package/templates/workflows/gsd-t-scan.workflow.js +711 -0
  42. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  43. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  44. package/bin/check-headless-sessions.js +0 -140
  45. package/bin/context-budget-audit.cjs +0 -447
  46. package/bin/context-meter-config.cjs +0 -101
  47. package/bin/context-meter-config.test.cjs +0 -101
  48. package/bin/event-stream.cjs +0 -205
  49. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  50. package/bin/gsd-t-capture-lint.cjs +0 -440
  51. package/bin/gsd-t-economics.cjs +0 -315
  52. package/bin/gsd-t-in-session-usage.cjs +0 -213
  53. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  54. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  55. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  56. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  57. package/bin/gsd-t-orchestrator.js +0 -535
  58. package/bin/gsd-t-parallel-probe.cjs +0 -132
  59. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  60. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  61. package/bin/gsd-t-report-tokens.cjs +0 -549
  62. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  63. package/bin/gsd-t-token-backfill.cjs +0 -366
  64. package/bin/gsd-t-token-capture.cjs +0 -321
  65. package/bin/gsd-t-token-dashboard.cjs +0 -353
  66. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  67. package/bin/gsd-t-tool-attribution.cjs +0 -377
  68. package/bin/gsd-t-tool-cost.cjs +0 -195
  69. package/bin/gsd-t-transcript-tee.cjs +0 -246
  70. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  71. package/bin/gsd-t-unattended-platform.cjs +0 -551
  72. package/bin/gsd-t-unattended-platform.js +0 -381
  73. package/bin/gsd-t-unattended-safety.cjs +0 -773
  74. package/bin/gsd-t-unattended-safety.js +0 -788
  75. package/bin/gsd-t-unattended.cjs +0 -2109
  76. package/bin/gsd-t-unattended.js +0 -1367
  77. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  78. package/bin/handoff-lock.cjs +0 -249
  79. package/bin/handoff-lock.js +0 -249
  80. package/bin/headless-auto-spawn.cjs +0 -711
  81. package/bin/headless-auto-spawn.js +0 -373
  82. package/bin/headless-exit-codes.cjs +0 -67
  83. package/bin/live-activity-report.cjs +0 -615
  84. package/bin/log-tail.cjs +0 -81
  85. package/bin/m44-proof-measure.cjs +0 -285
  86. package/bin/m46-iter-proof.cjs +0 -149
  87. package/bin/m46-worker-proof.cjs +0 -201
  88. package/bin/m55-substrate-proof.cjs +0 -134
  89. package/bin/metrics-rollup.js +0 -200
  90. package/bin/model-windows.cjs +0 -99
  91. package/bin/model-windows.test.cjs +0 -75
  92. package/bin/parallelism-report.cjs +0 -535
  93. package/bin/runway-estimator.cjs +0 -242
  94. package/bin/spawn-plan-derive.cjs +0 -163
  95. package/bin/spawn-plan-status-updater.cjs +0 -292
  96. package/bin/spawn-plan-writer.cjs +0 -204
  97. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  98. package/bin/token-budget.cjs +0 -265
  99. package/bin/unattended-watch-format.cjs +0 -178
  100. package/bin/watch-progress.js +0 -155
  101. package/commands/gsd-t-unattended-stop.md +0 -85
  102. package/commands/gsd-t-unattended-watch.md +0 -465
  103. package/commands/gsd-t-unattended.md +0 -476
  104. package/commands/gsd-t-visualize.md +0 -116
  105. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  106. package/scripts/gsd-t-context-meter.js +0 -341
  107. package/scripts/gsd-t-context-meter.test.js +0 -471
  108. package/scripts/gsd-t-dashboard-server.js +0 -1203
  109. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  110. package/scripts/gsd-t-transcript.html +0 -1821
  111. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  112. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  113. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  114. 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 };