@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,535 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
-
7
- const { loadConfig } = require('./gsd-t-orchestrator-config.cjs');
8
- const { readAllTasks, groupByWave, validateNoForwardDeps } = require('./gsd-t-orchestrator-queue.cjs');
9
- const { runWorker } = require('./gsd-t-orchestrator-worker.cjs');
10
- const { buildTaskBrief } = require('./gsd-t-task-brief.js');
11
- const { createStreamFeedClient } = require('./gsd-t-stream-feed-client.cjs');
12
- const { recoverRunState, writeRecoveredState, archiveState } = require('./gsd-t-orchestrator-recover.cjs');
13
-
14
- const STATE_DIR = '.gsd-t/orchestrator';
15
- const STATE_FILE = 'state.json';
16
-
17
- function nowIso() { return new Date().toISOString(); }
18
-
19
- function parseCliArgs(argv) {
20
- const args = {
21
- milestone: null,
22
- maxParallel: null,
23
- workerTimeoutMs: null,
24
- projectDir: process.cwd(),
25
- resume: false,
26
- noArchive: false,
27
- help: false,
28
- streamFeed: true,
29
- streamFeedPort: null,
30
- streamFeedHost: null
31
- };
32
- for (let i = 0; i < argv.length; i++) {
33
- const a = argv[i];
34
- if (a === '--help' || a === '-h') { args.help = true; }
35
- else if (a === '--milestone') { args.milestone = argv[++i]; }
36
- else if (a === '--max-parallel') { args.maxParallel = argv[++i]; }
37
- else if (a === '--worker-timeout') { args.workerTimeoutMs = argv[++i]; }
38
- else if (a === '--project-dir') { args.projectDir = path.resolve(argv[++i]); }
39
- else if (a === '--resume') { args.resume = true; }
40
- else if (a === '--no-archive') { args.noArchive = true; }
41
- else if (a === '--no-stream-feed') { args.streamFeed = false; }
42
- else if (a === '--stream-feed-port') { args.streamFeedPort = Number(argv[++i]); }
43
- else if (a === '--stream-feed-host') { args.streamFeedHost = argv[++i]; }
44
- }
45
- return args;
46
- }
47
-
48
- function printHelp() {
49
- process.stdout.write([
50
- 'Usage: gsd-t orchestrate --milestone <id> [options]',
51
- '',
52
- 'Options:',
53
- ' --milestone <id> Milestone id (e.g. M40). Required.',
54
- ' --max-parallel <n> Max concurrent workers (default: adaptive by free RAM,',
55
- ' 1 worker per 2GB, floor 3, ceiling 15).',
56
- ' --worker-timeout <ms> Per-worker timeout in ms (default 270000).',
57
- ' --project-dir <path> Project directory (default cwd).',
58
- ' --resume Resume from .gsd-t/orchestrator/state.json.',
59
- ' --no-archive When --resume + state is terminal, do NOT archive — fail instead.',
60
- ' --no-stream-feed Disable pushing frames to local stream-feed server.',
61
- ' --stream-feed-port <N> Override stream-feed port (default 7842 / env GSD_T_STREAM_FEED_PORT).',
62
- ' --stream-feed-host <H> Override stream-feed host (default 127.0.0.1).',
63
- ' -h, --help Show this help.',
64
- ''
65
- ].join('\n'));
66
- }
67
-
68
- function atomicWriteJson(fp, obj) {
69
- const dir = path.dirname(fp);
70
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
71
- const tmp = fp + '.tmp.' + process.pid;
72
- fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
73
- fs.renameSync(tmp, fp);
74
- }
75
-
76
- function appendEventLine(projectDir, event) {
77
- const dayStr = nowIso().slice(0, 10);
78
- const eventsDir = path.join(projectDir, '.gsd-t', 'events');
79
- if (!fs.existsSync(eventsDir)) fs.mkdirSync(eventsDir, { recursive: true });
80
- const fp = path.join(eventsDir, dayStr + '.jsonl');
81
- fs.appendFileSync(fp, JSON.stringify(event) + '\n');
82
- }
83
-
84
- function writeEvent(projectDir, eventType, extra) {
85
- const event = {
86
- ts: nowIso(),
87
- command: 'orchestrate',
88
- phase: null,
89
- trace_id: null,
90
- event_type: eventType,
91
- agent_id: process.env.GSD_T_AGENT_ID || null,
92
- parent_agent_id: process.env.GSD_T_PARENT_AGENT_ID || null,
93
- reasoning: null,
94
- outcome: null,
95
- ...(extra || {})
96
- };
97
- try { appendEventLine(projectDir, event); } catch (_) { /* best effort */ }
98
- }
99
-
100
- function makeState(projectDir) {
101
- const fp = path.join(projectDir, STATE_DIR, STATE_FILE);
102
- const defaults = {
103
- startedAt: nowIso(),
104
- status: 'running',
105
- currentWave: null,
106
- tasks: {}
107
- };
108
- return {
109
- fp,
110
- data: defaults,
111
- save(patch) {
112
- if (patch) Object.assign(this.data, patch);
113
- atomicWriteJson(this.fp, this.data);
114
- },
115
- patchTask(id, fields) {
116
- this.data.tasks[id] = { ...(this.data.tasks[id] || {}), ...fields };
117
- atomicWriteJson(this.fp, this.data);
118
- }
119
- };
120
- }
121
-
122
- function filterTasksByMilestone(tasks, milestone) {
123
- // M40 tasks don't yet carry a milestone field in tasks.md — for now,
124
- // the milestone context is global. This function is a placeholder for
125
- // when tasks.md entries include an explicit milestone marker.
126
- return tasks;
127
- }
128
-
129
- async function runWaveTasks({ tasks, config, state, logger, spawnImpl, runWorkerImpl = runWorker, onFrame, interrupt, liveChildrenRef, streamFeedFactory }) {
130
- const queue = [...tasks];
131
- const running = new Set();
132
- const results = [];
133
- const liveChildren = liveChildrenRef || new Map();
134
- let haltRequested = false;
135
- if (!interrupt) interrupt = { interrupted: false };
136
-
137
- const emit = (frame) => {
138
- if (typeof onFrame === 'function') { try { onFrame(frame); } catch (_) {} }
139
- };
140
-
141
- const launch = async (task) => {
142
- const started = nowIso();
143
- state.patchTask(task.id, { status: 'running', startedAt: started, retryCount: 0 });
144
- writeEvent(config.projectDir, 'task_start', { task_id: task.id, wave: task.wave, domain: task.domain });
145
-
146
- const canonicalTaskId = task.id.includes(':T')
147
- ? `${task.domain}-t${task.id.split(':T')[1]}`
148
- : task.id;
149
- const taskForWorker = { ...task, canonicalId: canonicalTaskId };
150
-
151
- const attempt = async (retryCount) => {
152
- let brief;
153
- try {
154
- brief = buildTaskBrief({
155
- milestone: config.milestone,
156
- domain: task.domain,
157
- taskId: canonicalTaskId,
158
- projectDir: config.projectDir,
159
- expectedBranch: config.expectedBranch || 'main'
160
- });
161
- } catch (err) {
162
- logger.log(`[orchestrator] brief build failed for ${task.id}: ${err.message}`);
163
- return {
164
- result: { ok: false, missing: ['brief_build_error'], details: { error: String(err) } },
165
- exitCode: -1,
166
- durationMs: 0,
167
- timedOut: false
168
- };
169
- }
170
- if (retryCount > 0) {
171
- brief += `\n\n## Retry Note\nPrevious attempt failed. Try again, paying attention to the Done Signal checklist.\n`;
172
- }
173
-
174
- // D1-T7: per-worker stream-feed client. Opens when we get a pid; every
175
- // onFrame call tees to both the user callback and the feed client so the
176
- // D4 server + UI see every frame.
177
- let feedClient = null;
178
-
179
- const teeFrame = (frame) => {
180
- emit(frame);
181
- if (feedClient) {
182
- try { feedClient.pushFrame(frame); } catch (_) { /* best-effort */ }
183
- }
184
- };
185
-
186
- const outcome = await runWorkerImpl({
187
- task: taskForWorker,
188
- brief,
189
- config,
190
- onFrame: teeFrame,
191
- onSpawn: ({ child, pid }) => {
192
- if (pid != null) {
193
- state.patchTask(task.id, { workerPid: pid });
194
- liveChildren.set(task.id, child);
195
- if (streamFeedFactory) {
196
- try {
197
- feedClient = streamFeedFactory({
198
- workerPid: pid,
199
- taskId: canonicalTaskId,
200
- projectDir: config.projectDir
201
- });
202
- } catch (err) {
203
- logger.log(`[orchestrator] stream-feed client open failed for ${task.id}: ${err.message}`);
204
- }
205
- }
206
- }
207
- },
208
- env: process.env,
209
- spawnImpl
210
- });
211
-
212
- if (feedClient) {
213
- try { await feedClient.close(); } catch (_) { /* best-effort */ }
214
- }
215
- return outcome;
216
- };
217
-
218
- let outcome = await attempt(0);
219
- liveChildren.delete(task.id);
220
- if (!outcome.result.ok && !interrupt.interrupted && config.retryOnFail) {
221
- state.patchTask(task.id, { retryCount: 1 });
222
- outcome = await attempt(1);
223
- liveChildren.delete(task.id);
224
- }
225
-
226
- const endedAt = nowIso();
227
- state.patchTask(task.id, {
228
- status: outcome.result.ok ? 'done' : 'failed',
229
- endedAt,
230
- exitCode: outcome.exitCode,
231
- durationMs: outcome.durationMs,
232
- missing: outcome.result.missing || []
233
- });
234
- writeEvent(config.projectDir, outcome.result.ok ? 'task_done' : 'task_failed', {
235
- task_id: task.id,
236
- wave: task.wave,
237
- domain: task.domain,
238
- exit_code: outcome.exitCode,
239
- duration_ms: outcome.durationMs
240
- });
241
-
242
- results.push({ task, outcome });
243
- if (!outcome.result.ok && config.haltOnSecondFail) {
244
- haltRequested = true;
245
- }
246
- return outcome;
247
- };
248
-
249
- const pump = async () => {
250
- while (queue.length && running.size < config.maxParallel && !haltRequested && !interrupt.interrupted) {
251
- const task = queue.shift();
252
- const p = launch(task).finally(() => running.delete(p));
253
- running.add(p);
254
- }
255
- };
256
-
257
- await pump();
258
- while (running.size) {
259
- await Promise.race(running);
260
- if (!haltRequested && !interrupt.interrupted) await pump();
261
- }
262
-
263
- return { results, halted: haltRequested, liveChildren };
264
- }
265
-
266
- function emitWaveBoundary(onFrame, state, wave, taskCount, extra) {
267
- if (typeof onFrame !== 'function') return;
268
- const frame = { type: 'wave-boundary', wave, state, taskCount, ts: nowIso() };
269
- if (extra) Object.assign(frame, extra);
270
- try { onFrame(frame); } catch (_) {}
271
- }
272
-
273
- async function runOrchestrator(opts) {
274
- const {
275
- projectDir,
276
- milestone,
277
- maxParallel,
278
- workerTimeoutMs,
279
- logger = console,
280
- spawnImpl,
281
- runWorkerImpl,
282
- onFrame,
283
- installSignalHandlers = true,
284
- streamFeed = null, // D1-T7: null|false = off, true = default (env-aware), object = client opts
285
- streamFeedFactory: streamFeedFactoryOverride = null, // test hook
286
- resume = false,
287
- noArchive = false
288
- } = opts;
289
-
290
- const config = loadConfig({
291
- projectDir,
292
- cliFlags: {
293
- ...(maxParallel != null ? { maxParallel } : {}),
294
- ...(workerTimeoutMs != null ? { workerTimeoutMs } : {})
295
- },
296
- env: process.env
297
- });
298
- config.milestone = milestone || 'unknown';
299
-
300
- const allTasks = readAllTasks(projectDir);
301
- const scopedTasks = filterTasksByMilestone(allTasks, milestone);
302
- if (!scopedTasks.length) {
303
- logger.log(`[orchestrator] no tasks found under ${projectDir}/.gsd-t/domains/*/tasks.md`);
304
- return { status: 'empty', waves: [] };
305
- }
306
-
307
- validateNoForwardDeps(scopedTasks);
308
- const waves = groupByWave(scopedTasks);
309
-
310
- const state = makeState(projectDir);
311
-
312
- // D6-T2: --resume handling. Ran before we touch state.save().
313
- // fresh → error (no run to resume)
314
- // terminal → archive (unless --no-archive) and start fresh
315
- // resume → seed state.tasks from reconciled recovery output, skip done/ambiguous
316
- let resumedSkipIds = new Set(); // task ids to not re-launch (done + ambiguous)
317
- let resumeStartWave = null;
318
- if (resume) {
319
- const recovery = recoverRunState({ projectDir });
320
- for (const note of (recovery.notes || [])) logger.log(`[resume] ${note}`);
321
- if (recovery.mode === 'fresh') {
322
- const err = new Error('no run to resume — .gsd-t/orchestrator/state.json is missing');
323
- err.code = 'NO_RESUME_STATE';
324
- throw err;
325
- }
326
- if (recovery.mode === 'terminal') {
327
- if (noArchive) {
328
- const err = new Error('--resume requested but state is terminal and --no-archive was set');
329
- err.code = 'TERMINAL_NO_ARCHIVE';
330
- throw err;
331
- }
332
- const { archived, archivePath } = archiveState(projectDir);
333
- if (archived) logger.log(`[resume] archived terminal state → ${archivePath}; starting fresh`);
334
- } else if (recovery.mode === 'resume') {
335
- // Persist reconciled task statuses and load them into the in-memory state.
336
- writeRecoveredState(projectDir, { ...recovery.state, tasks: recovery.tasks });
337
- state.data = { ...recovery.state, tasks: recovery.tasks };
338
- for (const [tid, t] of Object.entries(recovery.tasks)) {
339
- if (t.status === 'done' || t.status === 'ambiguous') resumedSkipIds.add(tid);
340
- if (t.status === 'ambiguous') logger.log(`[resume] task ${tid} is AMBIGUOUS — commit without progress entry; skipped, needs operator triage`);
341
- }
342
- resumeStartWave = recovery.currentWave;
343
- if (resumeStartWave != null) {
344
- logger.log(`[resume] continuing from wave ${resumeStartWave} (${resumedSkipIds.size}/${Object.keys(recovery.tasks).length} tasks already done or ambiguous)`);
345
- } else {
346
- logger.log('[resume] all tasks reconciled as done/ambiguous — nothing left to run');
347
- }
348
- }
349
- }
350
-
351
- state.save({ milestone: config.milestone, totalTasks: scopedTasks.length, waves: [...waves.keys()] });
352
- writeEvent(projectDir, resume ? 'orchestrator_resume' : 'orchestrator_start', { milestone: config.milestone, total_tasks: scopedTasks.length });
353
-
354
- // D1-T7: stream-feed wiring. `streamFeed: true` means "open clients pointed at
355
- // the default local server". `streamFeed: { port, host }` overrides. `false`/null
356
- // disables. If a user-supplied onFrame is the consumer, they can still opt-in.
357
- let streamFeedFactory = streamFeedFactoryOverride;
358
- let orchestratorFeedClient = null;
359
- const feedOpts = (streamFeed && typeof streamFeed === 'object') ? streamFeed : {};
360
- const feedEnabled = streamFeed === true || (streamFeed && typeof streamFeed === 'object');
361
- if (feedEnabled && !streamFeedFactory) {
362
- streamFeedFactory = ({ workerPid, taskId, projectDir }) => createStreamFeedClient({
363
- ...feedOpts,
364
- projectDir,
365
- workerPid,
366
- taskId
367
- });
368
- }
369
- if (streamFeedFactory) {
370
- try {
371
- orchestratorFeedClient = streamFeedFactory({
372
- workerPid: process.pid,
373
- taskId: 'orchestrator',
374
- projectDir
375
- });
376
- } catch (err) {
377
- logger.log(`[orchestrator] stream-feed orchestrator client open failed: ${err.message}`);
378
- }
379
- }
380
-
381
- const teeWaveFrame = (frame) => {
382
- if (typeof onFrame === 'function') { try { onFrame(frame); } catch (_) {} }
383
- if (orchestratorFeedClient) {
384
- try { orchestratorFeedClient.pushFrame(frame); } catch (_) {}
385
- }
386
- };
387
-
388
- const interrupt = { interrupted: false, currentWaveChildren: null };
389
-
390
- let sigintHandler = null;
391
- if (installSignalHandlers && typeof process.on === 'function') {
392
- sigintHandler = () => {
393
- if (interrupt.interrupted) return;
394
- interrupt.interrupted = true;
395
- logger.log('[orchestrator] SIGINT received — terminating workers');
396
- writeEvent(projectDir, 'orchestrator_interrupt', {});
397
- if (interrupt.currentWaveChildren) {
398
- for (const child of interrupt.currentWaveChildren.values()) {
399
- try { child.kill('SIGTERM'); } catch (_) {}
400
- }
401
- }
402
- };
403
- process.on('SIGINT', sigintHandler);
404
- }
405
-
406
- const cleanup = () => {
407
- if (sigintHandler && typeof process.removeListener === 'function') {
408
- try { process.removeListener('SIGINT', sigintHandler); } catch (_) {}
409
- }
410
- if (orchestratorFeedClient) {
411
- try { orchestratorFeedClient.close(); } catch (_) {}
412
- orchestratorFeedClient = null;
413
- }
414
- };
415
-
416
- try {
417
- const waveResults = [];
418
- for (const [waveNum, waveTasks] of waves) {
419
- if (interrupt.interrupted) break;
420
- // D6-T2: on resume, skip waves entirely behind the resume point.
421
- if (resumeStartWave != null && waveNum < resumeStartWave) {
422
- writeEvent(projectDir, 'wave_skipped', { wave: waveNum, reason: 'resume_before' });
423
- continue;
424
- }
425
- // Filter out tasks that recovery marked done/ambiguous.
426
- const effectiveTasks = resumedSkipIds.size
427
- ? waveTasks.filter((t) => !resumedSkipIds.has(t.id))
428
- : waveTasks;
429
- if (effectiveTasks.length === 0) {
430
- writeEvent(projectDir, 'wave_skipped', { wave: waveNum, reason: 'all_tasks_reconciled' });
431
- continue;
432
- }
433
-
434
- state.save({ currentWave: waveNum, status: 'running' });
435
- writeEvent(projectDir, 'wave_start', { wave: waveNum, task_count: effectiveTasks.length });
436
- emitWaveBoundary(teeWaveFrame, 'start', waveNum, effectiveTasks.length);
437
-
438
- const waveStartedMs = Date.now();
439
- const waveLiveChildren = new Map();
440
- interrupt.currentWaveChildren = waveLiveChildren;
441
- const { results, halted } = await runWaveTasks({
442
- tasks: effectiveTasks,
443
- config,
444
- state,
445
- logger,
446
- spawnImpl,
447
- runWorkerImpl,
448
- onFrame,
449
- interrupt,
450
- liveChildrenRef: waveLiveChildren,
451
- streamFeedFactory
452
- });
453
-
454
- const failed = results.filter((r) => !r.outcome.result.ok);
455
- const waveDurationMs = Date.now() - waveStartedMs;
456
- waveResults.push({ wave: waveNum, total: waveTasks.length, done: results.length - failed.length, failed: failed.length });
457
- writeEvent(projectDir, failed.length ? 'wave_failed' : 'wave_done', {
458
- wave: waveNum,
459
- failed_count: failed.length
460
- });
461
- emitWaveBoundary(teeWaveFrame, failed.length ? 'failed' : 'done', waveNum, waveTasks.length, { durationMs: waveDurationMs, failed: failed.length });
462
-
463
- if (interrupt.interrupted) break;
464
-
465
- if (halted || failed.length) {
466
- state.save({ status: 'failed' });
467
- logger.log(`[wave_halt] wave=${waveNum} failed_tasks=${failed.length}`);
468
- writeEvent(projectDir, 'orchestrator_halt', { wave: waveNum, failed_count: failed.length });
469
- return { status: 'failed', waves: waveResults, failedWave: waveNum };
470
- }
471
- }
472
-
473
- if (interrupt.interrupted) {
474
- state.save({ status: 'interrupted', endedAt: nowIso() });
475
- writeEvent(projectDir, 'orchestrator_done_interrupted', {});
476
- return { status: 'interrupted', waves: waveResults };
477
- }
478
-
479
- state.save({ status: 'done', endedAt: nowIso() });
480
- writeEvent(projectDir, 'orchestrator_done', { waves: waveResults.length });
481
- return { status: 'done', waves: waveResults };
482
- } finally {
483
- cleanup();
484
- }
485
- }
486
-
487
- async function main() {
488
- const args = parseCliArgs(process.argv.slice(2));
489
- if (args.help) { printHelp(); process.exit(0); }
490
- if (!args.milestone) {
491
- process.stderr.write('Error: --milestone is required\n\n');
492
- printHelp();
493
- process.exit(2);
494
- }
495
- let streamFeedOpt = args.streamFeed;
496
- if (streamFeedOpt) {
497
- if (args.streamFeedPort || args.streamFeedHost) {
498
- streamFeedOpt = {};
499
- if (args.streamFeedPort) streamFeedOpt.port = args.streamFeedPort;
500
- if (args.streamFeedHost) streamFeedOpt.host = args.streamFeedHost;
501
- }
502
- }
503
- try {
504
- const res = await runOrchestrator({
505
- projectDir: args.projectDir,
506
- milestone: args.milestone,
507
- maxParallel: args.maxParallel,
508
- workerTimeoutMs: args.workerTimeoutMs,
509
- streamFeed: streamFeedOpt,
510
- resume: args.resume,
511
- noArchive: args.noArchive
512
- });
513
- if (res.status === 'interrupted') {
514
- process.exit(130);
515
- }
516
- if (res.status === 'failed') {
517
- process.exit(1);
518
- }
519
- process.exit(0);
520
- } catch (err) {
521
- process.stderr.write(`[orchestrator] ${err.message}\n`);
522
- process.exit(1);
523
- }
524
- }
525
-
526
- if (require.main === module) {
527
- main();
528
- }
529
-
530
- module.exports = {
531
- runOrchestrator,
532
- runWaveTasks,
533
- parseCliArgs,
534
- atomicWriteJson
535
- };
@@ -1,132 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * gsd-t-parallel-probe — M44 D9 Step 3
5
- *
6
- * Deterministic in-session probe that runs the parallel planner in dry-run mode
7
- * and emits a single JSON line to stdout. Command files (execute / quick /
8
- * debug / integrate) invoke this as a bash shim to get a mechanical answer —
9
- * replacing LLM prose judgment with a JSON shape that branching logic can read.
10
- *
11
- * Per user memory `feedback_deterministic_orchestration.md`:
12
- * "prompt-based blocking doesn't work; use JS orchestrators for gates/waits"
13
- *
14
- * Contract: wave-join-contract.md v1.1.0; unattended-supervisor-contract.md v1.5.0
15
- *
16
- * Output shape (stdout, one line):
17
- * {"workerCount":N,"parallelTasks":["M44-D9-T1",...],"mode":"in-session",
18
- * "reducedCount":null|N,"warnings":[...],"ok":true}
19
- *
20
- * On unexpected error (planner throw, missing repo state, etc.), emits a safe
21
- * fallback shape so shell callers never have to parse stderr:
22
- * {"workerCount":1,"parallelTasks":[],"mode":"in-session","ok":false,"error":"..."}
23
- *
24
- * Exit code 0 on both success and safe-fallback. This is intentional: the shim
25
- * is a decision probe, not a command; non-zero would force command files to
26
- * defensively wrap it and add LLM noise.
27
- *
28
- * Hard rules:
29
- * - Zero external runtime deps
30
- * - Never writes to stderr by default (shell shim relies on quiet)
31
- * - Never writes to `.gsd-t/events/*` — that's runParallel's job
32
- */
33
-
34
- const path = require("node:path");
35
-
36
- function parseArgv(argv) {
37
- const out = { milestone: null, domain: null, mode: null };
38
- for (let i = 0; i < argv.length; i++) {
39
- const a = argv[i];
40
- if (a === "--milestone" && argv[i + 1]) {
41
- out.milestone = argv[++i];
42
- } else if (a === "--domain" && argv[i + 1]) {
43
- out.domain = argv[++i];
44
- } else if (a === "--mode" && argv[i + 1]) {
45
- out.mode = argv[++i];
46
- } else if (a === "--help" || a === "-h") {
47
- out.help = true;
48
- }
49
- }
50
- return out;
51
- }
52
-
53
- const HELP = `gsd-t-parallel-probe — deterministic planner probe (JSON out)
54
-
55
- Usage:
56
- node bin/gsd-t-parallel-probe.cjs [--milestone Mxx] [--domain name] [--mode in-session|unattended]
57
-
58
- Output: one JSON line to stdout with keys workerCount, parallelTasks, mode,
59
- reducedCount, warnings, ok. Exit 0 always. Use with in-session command files
60
- (execute, quick, debug, integrate) to replace prose-based parallel dispatch
61
- decisions with a mechanical branch on workerCount.
62
- `;
63
-
64
- function probe(argv, env) {
65
- const args = parseArgv(argv || []);
66
- if (args.help) {
67
- process.stdout.write(HELP);
68
- return 0;
69
- }
70
-
71
- let runParallel;
72
- try {
73
- ({ runParallel } = require(path.join(__dirname, "gsd-t-parallel.cjs")));
74
- } catch (e) {
75
- process.stdout.write(
76
- JSON.stringify({
77
- workerCount: 1,
78
- parallelTasks: [],
79
- mode: args.mode || "in-session",
80
- reducedCount: null,
81
- warnings: [],
82
- ok: false,
83
- error: `planner_load:${(e && e.message) || "unknown"}`,
84
- }) + "\n",
85
- );
86
- return 0;
87
- }
88
-
89
- let result;
90
- try {
91
- result = runParallel({
92
- projectDir: process.cwd(),
93
- mode: args.mode || undefined,
94
- milestone: args.milestone || undefined,
95
- domain: args.domain || undefined,
96
- dryRun: true,
97
- env: env || process.env,
98
- });
99
- } catch (e) {
100
- process.stdout.write(
101
- JSON.stringify({
102
- workerCount: 1,
103
- parallelTasks: [],
104
- mode: args.mode || "in-session",
105
- reducedCount: null,
106
- warnings: [],
107
- ok: false,
108
- error: `planner_error:${(e && e.message) || "unknown"}`,
109
- }) + "\n",
110
- );
111
- return 0;
112
- }
113
-
114
- process.stdout.write(
115
- JSON.stringify({
116
- workerCount: Number(result.workerCount) || 0,
117
- parallelTasks: Array.isArray(result.parallelTasks) ? result.parallelTasks : [],
118
- mode: result.mode || args.mode || "in-session",
119
- reducedCount: typeof result.reducedCount === "number" ? result.reducedCount : null,
120
- warnings: Array.isArray(result.warnings) ? result.warnings : [],
121
- ok: true,
122
- }) + "\n",
123
- );
124
- return 0;
125
- }
126
-
127
- module.exports = { probe, _parseArgv: parseArgv, _HELP: HELP };
128
-
129
- if (require.main === module) {
130
- const code = probe(process.argv.slice(2), process.env);
131
- process.exit(code);
132
- }