@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.
- package/CHANGELOG.md +171 -0
- package/bin/gsd-t-parallel.cjs +64 -13
- package/bin/gsd-t.js +48 -376
- package/bin/journey-coverage.cjs +6 -3
- package/bin/parallel-cli.cjs +13 -1
- package/commands/gsd-t-complete-milestone.md +6 -12
- package/commands/gsd-t-debug.md +47 -533
- package/commands/gsd-t-design-decompose.md +24 -497
- package/commands/gsd-t-doc-ripple.md +23 -139
- package/commands/gsd-t-execute.md +41 -958
- package/commands/gsd-t-feature.md +2 -6
- package/commands/gsd-t-gap-analysis.md +3 -6
- package/commands/gsd-t-help.md +2 -4
- package/commands/gsd-t-impact.md +26 -295
- package/commands/gsd-t-init-scan-setup.md +7 -7
- package/commands/gsd-t-integrate.md +44 -369
- package/commands/gsd-t-milestone.md +25 -124
- package/commands/gsd-t-partition.md +28 -539
- package/commands/gsd-t-plan.md +26 -472
- package/commands/gsd-t-prd.md +25 -311
- package/commands/gsd-t-quick.md +1 -1
- package/commands/gsd-t-resume.md +0 -33
- package/commands/gsd-t-scan.md +45 -652
- package/commands/gsd-t-verify.md +52 -569
- package/commands/gsd-t-wave.md +41 -430
- package/package.json +1 -1
- package/scripts/gsd-t-calibration-hook.js +3 -1
- package/scripts/hooks/pre-commit-journey-coverage +0 -2
- package/scripts/hooks/pre-commit-playwright-gate +0 -2
- package/templates/CLAUDE-global.md +44 -173
- package/templates/CLAUDE-project.md +118 -104
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +5 -2
- package/templates/workflows/_lib.js +176 -0
- package/templates/workflows/gsd-t-debug.workflow.js +86 -0
- package/templates/workflows/gsd-t-execute.workflow.js +206 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
- package/templates/workflows/gsd-t-phase.workflow.js +94 -0
- package/templates/workflows/gsd-t-quick.workflow.js +72 -0
- package/templates/workflows/gsd-t-scan.workflow.js +711 -0
- package/templates/workflows/gsd-t-verify.workflow.js +348 -0
- package/templates/workflows/gsd-t-wave.workflow.js +43 -0
- package/bin/check-headless-sessions.js +0 -140
- package/bin/context-budget-audit.cjs +0 -447
- package/bin/context-meter-config.cjs +0 -101
- package/bin/context-meter-config.test.cjs +0 -101
- package/bin/event-stream.cjs +0 -205
- package/bin/gsd-t-benchmark-orchestrator.js +0 -437
- package/bin/gsd-t-capture-lint.cjs +0 -440
- package/bin/gsd-t-economics.cjs +0 -315
- package/bin/gsd-t-in-session-usage.cjs +0 -213
- package/bin/gsd-t-orchestrator-config.cjs +0 -161
- package/bin/gsd-t-orchestrator-queue.cjs +0 -180
- package/bin/gsd-t-orchestrator-recover.cjs +0 -231
- package/bin/gsd-t-orchestrator-worker.cjs +0 -219
- package/bin/gsd-t-orchestrator.js +0 -535
- package/bin/gsd-t-parallel-probe.cjs +0 -132
- package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
- package/bin/gsd-t-ratelimit-probe.cjs +0 -648
- package/bin/gsd-t-report-tokens.cjs +0 -549
- package/bin/gsd-t-stream-feed-client.cjs +0 -151
- package/bin/gsd-t-token-backfill.cjs +0 -366
- package/bin/gsd-t-token-capture.cjs +0 -321
- package/bin/gsd-t-token-dashboard.cjs +0 -353
- package/bin/gsd-t-token-regenerate-log.cjs +0 -129
- package/bin/gsd-t-tool-attribution.cjs +0 -377
- package/bin/gsd-t-tool-cost.cjs +0 -195
- package/bin/gsd-t-transcript-tee.cjs +0 -246
- package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
- package/bin/gsd-t-unattended-platform.cjs +0 -551
- package/bin/gsd-t-unattended-platform.js +0 -381
- package/bin/gsd-t-unattended-safety.cjs +0 -773
- package/bin/gsd-t-unattended-safety.js +0 -788
- package/bin/gsd-t-unattended.cjs +0 -2109
- package/bin/gsd-t-unattended.js +0 -1367
- package/bin/gsd-t-worker-dispatch.cjs +0 -211
- package/bin/handoff-lock.cjs +0 -249
- package/bin/handoff-lock.js +0 -249
- package/bin/headless-auto-spawn.cjs +0 -711
- package/bin/headless-auto-spawn.js +0 -373
- package/bin/headless-exit-codes.cjs +0 -67
- package/bin/live-activity-report.cjs +0 -615
- package/bin/log-tail.cjs +0 -81
- package/bin/m44-proof-measure.cjs +0 -285
- package/bin/m46-iter-proof.cjs +0 -149
- package/bin/m46-worker-proof.cjs +0 -201
- package/bin/m55-substrate-proof.cjs +0 -134
- package/bin/metrics-rollup.js +0 -200
- package/bin/model-windows.cjs +0 -99
- package/bin/model-windows.test.cjs +0 -75
- package/bin/parallelism-report.cjs +0 -535
- package/bin/runway-estimator.cjs +0 -242
- package/bin/spawn-plan-derive.cjs +0 -163
- package/bin/spawn-plan-status-updater.cjs +0 -292
- package/bin/spawn-plan-writer.cjs +0 -204
- package/bin/supervisor-pid-fingerprint.cjs +0 -126
- package/bin/token-budget.cjs +0 -265
- package/bin/unattended-watch-format.cjs +0 -178
- package/bin/watch-progress.js +0 -155
- package/commands/gsd-t-unattended-stop.md +0 -85
- package/commands/gsd-t-unattended-watch.md +0 -465
- package/commands/gsd-t-unattended.md +0 -476
- package/commands/gsd-t-visualize.md +0 -116
- package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
- package/scripts/gsd-t-context-meter.js +0 -341
- package/scripts/gsd-t-context-meter.test.js +0 -471
- package/scripts/gsd-t-dashboard-server.js +0 -1203
- package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
- package/scripts/gsd-t-transcript.html +0 -1821
- package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
- package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
- package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
- 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 };
|