@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,180 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const DEFAULT_WAVE = 0;
7
-
8
- function listDomainTaskFiles(projectDir) {
9
- const domainsDir = path.join(projectDir, '.gsd-t', 'domains');
10
- if (!fs.existsSync(domainsDir)) return [];
11
- const out = [];
12
- for (const entry of fs.readdirSync(domainsDir, { withFileTypes: true })) {
13
- if (!entry.isDirectory()) continue;
14
- const p = path.join(domainsDir, entry.name, 'tasks.md');
15
- if (fs.existsSync(p)) out.push({ domain: entry.name, file: p });
16
- }
17
- return out.sort((a, b) => a.domain.localeCompare(b.domain));
18
- }
19
-
20
- function parseFilesField(text) {
21
- const patterns = [];
22
- const seen = new Set();
23
- const re = /`([^`]+)`/g;
24
- let m;
25
- while ((m = re.exec(text)) !== null) {
26
- const raw = m[1].trim();
27
- if (!raw) continue;
28
- const pattern = raw.replace(/\/$/, '/**');
29
- if (!seen.has(pattern)) { seen.add(pattern); patterns.push(pattern); }
30
- }
31
- if (patterns.length === 0) {
32
- for (const part of text.split(/[,;]/)) {
33
- const raw = part.replace(/\([^)]*\)/g, '').trim();
34
- if (raw && !/^(none|n\/a)$/i.test(raw) && !seen.has(raw)) {
35
- seen.add(raw); patterns.push(raw);
36
- }
37
- }
38
- }
39
- return patterns;
40
- }
41
-
42
- function parseTasksFile(markdown, domain) {
43
- const tasks = [];
44
- const lines = markdown.split(/\r?\n/);
45
-
46
- let current = null;
47
-
48
- const commitCurrent = () => {
49
- if (current) {
50
- if (current.wave == null) current.wave = DEFAULT_WAVE;
51
- if (!current.dependencies) current.dependencies = [];
52
- if (!current.ownedPatterns) current.ownedPatterns = [];
53
- tasks.push(current);
54
- }
55
- current = null;
56
- };
57
-
58
- for (let i = 0; i < lines.length; i++) {
59
- const line = lines[i];
60
- const taskHeader = line.match(/^###\s+Task\s+(\d+)\s*:\s*(.+?)\s*$/i);
61
- if (taskHeader) {
62
- commitCurrent();
63
- current = {
64
- domain,
65
- id: `${domain}:T${taskHeader[1]}`,
66
- taskNum: parseInt(taskHeader[1], 10),
67
- title: taskHeader[2].trim(),
68
- wave: null,
69
- dependencies: [],
70
- ownedPatterns: []
71
- };
72
- continue;
73
- }
74
- if (!current) continue;
75
-
76
- const waveMatch = line.match(/^\s*-\s*\*\*Wave\*\*\s*:\s*(\d+)\s*$/i);
77
- if (waveMatch) {
78
- current.wave = parseInt(waveMatch[1], 10);
79
- continue;
80
- }
81
-
82
- const depMatch = line.match(/^\s*-\s*\*\*Dependencies\*\*\s*:\s*(.+?)\s*$/i);
83
- if (depMatch) {
84
- current.dependencies = parseDependencies(depMatch[1], domain);
85
- continue;
86
- }
87
-
88
- const filesMatch = line.match(/^\s*-\s*\*\*Files\*\*\s*:\s*(.+?)\s*$/i);
89
- if (filesMatch) {
90
- current.ownedPatterns = parseFilesField(filesMatch[1]);
91
- continue;
92
- }
93
- }
94
- commitCurrent();
95
- return tasks;
96
- }
97
-
98
- function parseDependencies(text, domain) {
99
- const trimmed = text.trim();
100
- if (!trimmed || /^none$/i.test(trimmed) || /^n\/a$/i.test(trimmed)) return [];
101
-
102
- const deps = [];
103
- const seen = new Set();
104
-
105
- const push = (id) => {
106
- if (!seen.has(id)) { seen.add(id); deps.push(id); }
107
- };
108
-
109
- const sameDomainTaskRe = /\bTask\s+(\d+)\b/gi;
110
- const crossDomainRe = /\b([a-z][a-z0-9-]+)\s+Task\s+(\d+)\b/gi;
111
-
112
- let m;
113
- while ((m = crossDomainRe.exec(trimmed)) !== null) {
114
- const prefix = m[1].toLowerCase();
115
- if (prefix === 'requires' || prefix === 'blocked') continue;
116
- push(`${prefix}:T${m[2]}`);
117
- }
118
-
119
- const crossIds = new Set(deps);
120
- sameDomainTaskRe.lastIndex = 0;
121
- while ((m = sameDomainTaskRe.exec(trimmed)) !== null) {
122
- const id = `${domain}:T${m[1]}`;
123
- const crossMatch = [...crossIds].some((d) => d.endsWith(`:T${m[1]}`) && d !== id);
124
- if (!crossMatch) push(id);
125
- }
126
-
127
- return deps;
128
- }
129
-
130
- function readAllTasks(projectDir) {
131
- const all = [];
132
- for (const { domain, file } of listDomainTaskFiles(projectDir)) {
133
- const md = fs.readFileSync(file, 'utf8');
134
- all.push(...parseTasksFile(md, domain));
135
- }
136
- return all;
137
- }
138
-
139
- function groupByWave(tasks) {
140
- const groups = new Map();
141
- for (const t of tasks) {
142
- const w = t.wave == null ? DEFAULT_WAVE : t.wave;
143
- if (!groups.has(w)) groups.set(w, []);
144
- groups.get(w).push(t);
145
- }
146
- return new Map([...groups.entries()].sort((a, b) => a[0] - b[0]));
147
- }
148
-
149
- function validateNoForwardDeps(tasks) {
150
- const byId = new Map(tasks.map((t) => [t.id, t]));
151
- const errors = [];
152
- for (const t of tasks) {
153
- for (const depId of t.dependencies) {
154
- const dep = byId.get(depId);
155
- if (!dep) continue;
156
- if (dep.wave > t.wave) {
157
- errors.push(
158
- `Task ${t.id} (wave ${t.wave}) depends on ${depId} (wave ${dep.wave}) — forward cross-wave dependency not allowed`
159
- );
160
- }
161
- }
162
- }
163
- if (errors.length) {
164
- const err = new Error(`Wave dependency validation failed:\n ${errors.join('\n ')}`);
165
- err.errors = errors;
166
- throw err;
167
- }
168
- return true;
169
- }
170
-
171
- module.exports = {
172
- DEFAULT_WAVE,
173
- listDomainTaskFiles,
174
- parseTasksFile,
175
- parseDependencies,
176
- parseFilesField,
177
- readAllTasks,
178
- groupByWave,
179
- validateNoForwardDeps
180
- };
@@ -1,231 +0,0 @@
1
- 'use strict';
2
- /**
3
- * M40 D6-T1 — Orchestrator Recovery
4
- *
5
- * Reconstructs run state from an existing `.gsd-t/orchestrator/state.json`
6
- * so the orchestrator can be resumed after a crash/SIGINT/kill.
7
- *
8
- * Contract: stream-json-sink v1.1.0 (for workerPid/taskId attribution),
9
- * completion-signal v1.x (assertCompletion reconciliation),
10
- * wave-join v1.x (wave ordering + second-fail-halt policy).
11
- *
12
- * Rules:
13
- * - state.json absent → { mode: 'fresh' }
14
- * - status ∈ {done,failed,stopped,interrupted,completed} → { mode: 'terminal' }
15
- * - status === 'running' → { mode: 'resume', ... }
16
- * - Every task with status === 'running' is reconciled via assertCompletion:
17
- * ok → DONE (status rewritten, retry count preserved)
18
- * missing only progress → flagged for operator triage (status: 'ambiguous')
19
- * missing commit+test+etc → FAILED (respects existing retryCount — if already
20
- * retried once, second-fail-halt is triggered by caller)
21
- * - A live `workerPid` on a task gets a best-effort `kill -0` liveness check; stale
22
- * pid is logged but treated as crashed (not silently reclaimed).
23
- * - currentWave = first wave with any task not in {done, ambiguous}; null if all done.
24
- *
25
- * The caller (the orchestrator `--resume` CLI flag, D6-T2) decides what to do with
26
- * ambiguous tasks — this module never silently claims completion.
27
- */
28
-
29
- const fs = require('fs');
30
- const path = require('path');
31
-
32
- const STATE_DIR = path.join('.gsd-t', 'orchestrator');
33
- const STATE_FILE = 'state.json';
34
-
35
- const TERMINAL_STATUSES = new Set(['done', 'failed', 'stopped', 'interrupted', 'completed']);
36
-
37
- // ── Public API ───────────────────────────────────────────────────────────────
38
-
39
- /**
40
- * @param {object} opts
41
- * @param {string} opts.projectDir
42
- * @param {Function} [opts.assertCompletionImpl] - for testing (default: real impl)
43
- * @param {Function} [opts.pidLivenessCheck] - for testing (default: process.kill(pid, 0))
44
- * @param {Function} [opts.now] - iso string; default new Date().toISOString()
45
- * @returns {object} { mode, currentWave, tasks, state, ambiguous, notes }
46
- */
47
- function recoverRunState(opts) {
48
- const projectDir = (opts && opts.projectDir) || process.cwd();
49
- const statePath = path.join(projectDir, STATE_DIR, STATE_FILE);
50
- const notes = [];
51
-
52
- if (!fs.existsSync(statePath)) {
53
- return { mode: 'fresh', currentWave: null, tasks: {}, state: null, ambiguous: [], notes };
54
- }
55
-
56
- let state;
57
- try {
58
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
59
- } catch (err) {
60
- notes.push(`state.json parse failed: ${err.message} — treating as terminal/corrupt`);
61
- return { mode: 'terminal', currentWave: null, tasks: {}, state: null, ambiguous: [], notes };
62
- }
63
- if (!state || typeof state !== 'object') {
64
- notes.push('state.json is not an object — treating as terminal/corrupt');
65
- return { mode: 'terminal', currentWave: null, tasks: {}, state: null, ambiguous: [], notes };
66
- }
67
-
68
- if (TERMINAL_STATUSES.has(state.status)) {
69
- return { mode: 'terminal', currentWave: null, tasks: state.tasks || {}, state, ambiguous: [], notes };
70
- }
71
-
72
- // Non-terminal: reconcile every in-flight task.
73
- const assertCompletionImpl = (opts && opts.assertCompletionImpl) || defaultAssertCompletion;
74
- const pidCheck = (opts && opts.pidLivenessCheck) || defaultPidLiveness;
75
- const expectedBranch = (opts && opts.expectedBranch) || state.expectedBranch || 'main';
76
-
77
- const tasks = { ...(state.tasks || {}) };
78
- const ambiguous = [];
79
-
80
- for (const [taskId, t] of Object.entries(tasks)) {
81
- if (t.status !== 'running') continue;
82
-
83
- // PID liveness — informational only
84
- if (t.workerPid) {
85
- const alive = pidCheck(t.workerPid);
86
- if (!alive) notes.push(`task ${taskId}: workerPid ${t.workerPid} no longer alive (crashed)`);
87
- }
88
-
89
- // Reconcile via assertCompletion
90
- let result;
91
- try {
92
- result = assertCompletionImpl({
93
- taskId: t.canonicalId || taskId,
94
- projectDir,
95
- expectedBranch,
96
- taskStart: t.startedAt,
97
- ownedPatterns: t.ownedPatterns || [],
98
- skipTest: true, // recovery shouldn't rerun the test suite per task
99
- });
100
- } catch (err) {
101
- notes.push(`task ${taskId}: assertCompletion threw — ${err.message}; treating as failed`);
102
- tasks[taskId] = {
103
- ...t,
104
- status: 'failed',
105
- endedAt: (opts && opts.now ? opts.now() : new Date().toISOString()),
106
- missing: ['recovery_assert_threw'],
107
- recoverySource: 'assert_error',
108
- };
109
- continue;
110
- }
111
-
112
- const missing = Array.isArray(result && result.missing) ? result.missing : [];
113
-
114
- if (result && result.ok) {
115
- tasks[taskId] = {
116
- ...t,
117
- status: 'done',
118
- endedAt: t.endedAt || (opts && opts.now ? opts.now() : new Date().toISOString()),
119
- missing: [],
120
- recoverySource: 'recovered_ok',
121
- };
122
- continue;
123
- }
124
-
125
- // Ambiguous case: commit present BUT progress entry missing (and no other issues).
126
- // Don't silently claim done; caller must triage.
127
- const onlyProgressMissing = missing.length === 1 && missing[0] === 'no_progress_entry';
128
- if (onlyProgressMissing) {
129
- tasks[taskId] = {
130
- ...t,
131
- status: 'ambiguous',
132
- missing,
133
- recoverySource: 'commit_without_progress',
134
- };
135
- ambiguous.push(taskId);
136
- notes.push(`task ${taskId}: commit found but no progress.md entry — flagged for triage`);
137
- continue;
138
- }
139
-
140
- // Otherwise: treat as failed. Preserve retryCount.
141
- tasks[taskId] = {
142
- ...t,
143
- status: 'failed',
144
- endedAt: (opts && opts.now ? opts.now() : new Date().toISOString()),
145
- missing,
146
- recoverySource: 'reconcile_failed',
147
- };
148
- }
149
-
150
- const currentWave = firstIncompleteWave(tasks);
151
-
152
- return {
153
- mode: 'resume',
154
- currentWave,
155
- tasks,
156
- state,
157
- ambiguous,
158
- notes,
159
- };
160
- }
161
-
162
- /**
163
- * Write back a reconciled state.json (same path) with recovered task statuses.
164
- * The orchestrator `--resume` flow uses this to persist the recovery results
165
- * before it begins retrying / continuing.
166
- */
167
- function writeRecoveredState(projectDir, reconciled) {
168
- const statePath = path.join(projectDir, STATE_DIR, STATE_FILE);
169
- try { fs.mkdirSync(path.dirname(statePath), { recursive: true }); } catch { /* exists */ }
170
- const tmp = statePath + '.tmp';
171
- fs.writeFileSync(tmp, JSON.stringify(reconciled, null, 2) + '\n');
172
- fs.renameSync(tmp, statePath);
173
- }
174
-
175
- /**
176
- * Archive the existing state.json to .gsd-t/orchestrator/archive/{ts}/state.json
177
- * and remove the original. Used when mode === 'terminal' and a fresh run is desired.
178
- * Returns { archived: true, archivePath } or { archived: false } if nothing to archive.
179
- */
180
- function archiveState(projectDir, opts) {
181
- const statePath = path.join(projectDir, STATE_DIR, STATE_FILE);
182
- if (!fs.existsSync(statePath)) return { archived: false };
183
- const ts = (opts && opts.timestamp) || new Date().toISOString().replace(/[:.]/g, '-');
184
- const archiveDir = path.join(projectDir, STATE_DIR, 'archive', ts);
185
- fs.mkdirSync(archiveDir, { recursive: true });
186
- const dest = path.join(archiveDir, STATE_FILE);
187
- fs.renameSync(statePath, dest);
188
- return { archived: true, archivePath: dest };
189
- }
190
-
191
- // ── Helpers ──────────────────────────────────────────────────────────────────
192
-
193
- function firstIncompleteWave(tasks) {
194
- let min = Infinity;
195
- for (const t of Object.values(tasks)) {
196
- if (!t || typeof t !== 'object') continue;
197
- if (t.status === 'done' || t.status === 'ambiguous') continue;
198
- if (t.wave != null && Number.isFinite(Number(t.wave))) {
199
- const w = Number(t.wave);
200
- if (w < min) min = w;
201
- }
202
- }
203
- return min === Infinity ? null : min;
204
- }
205
-
206
- function defaultAssertCompletion(opts) {
207
- const { assertCompletion } = require('./gsd-t-completion-check.cjs');
208
- return assertCompletion(opts);
209
- }
210
-
211
- function defaultPidLiveness(pid) {
212
- if (!pid || !Number.isFinite(Number(pid))) return false;
213
- try {
214
- // signal 0 doesn't deliver a signal — only checks existence/permission.
215
- process.kill(Number(pid), 0);
216
- return true;
217
- } catch (err) {
218
- // ESRCH = no such process; EPERM = exists but not ours (treat as alive)
219
- if (err && err.code === 'EPERM') return true;
220
- return false;
221
- }
222
- }
223
-
224
- module.exports = {
225
- recoverRunState,
226
- writeRecoveredState,
227
- archiveState,
228
- // exported for tests
229
- _firstIncompleteWave: firstIncompleteWave,
230
- _TERMINAL_STATUSES: TERMINAL_STATUSES,
231
- };
@@ -1,219 +0,0 @@
1
- 'use strict';
2
-
3
- const { spawn } = require('child_process');
4
- const path = require('path');
5
- const { assertCompletion } = require('./gsd-t-completion-check.cjs');
6
- const transcriptTee = require('./gsd-t-transcript-tee.cjs');
7
-
8
- const DEFAULT_CLAUDE_BIN = 'claude';
9
-
10
- function nowIso() {
11
- return new Date().toISOString();
12
- }
13
-
14
- function pickClaudeBin(env) {
15
- return env.GSD_T_CLAUDE_BIN || DEFAULT_CLAUDE_BIN;
16
- }
17
-
18
- function buildArgs(task) {
19
- const model = (task && task.model) || 'sonnet';
20
- return [
21
- '-p',
22
- '--dangerously-skip-permissions',
23
- '--output-format', 'stream-json',
24
- '--verbose',
25
- '--model', model
26
- ];
27
- }
28
-
29
- function emitBoundary(onFrame, task, state, extra) {
30
- if (typeof onFrame !== 'function') return;
31
- const frame = {
32
- type: 'task-boundary',
33
- taskId: task.id,
34
- domain: task.domain || null,
35
- wave: task.wave == null ? null : task.wave,
36
- state,
37
- ts: nowIso()
38
- };
39
- if (extra) Object.assign(frame, extra);
40
- try { onFrame(frame); } catch (_) { /* onFrame must not kill worker */ }
41
- }
42
-
43
- function parseLines(buffer, onLine) {
44
- let start = 0;
45
- for (let i = 0; i < buffer.length; i++) {
46
- if (buffer[i] === '\n') {
47
- const line = buffer.slice(start, i);
48
- if (line) onLine(line);
49
- start = i + 1;
50
- }
51
- }
52
- return buffer.slice(start);
53
- }
54
-
55
- function runWorker(opts) {
56
- const {
57
- task,
58
- brief,
59
- config,
60
- onFrame,
61
- env = process.env,
62
- spawnImpl = spawn
63
- } = opts || {};
64
-
65
- if (!task || !task.id) throw new Error('runWorker requires task with id');
66
- if (typeof brief !== 'string' || !brief.length) throw new Error('runWorker requires non-empty brief');
67
- if (!config || !config.projectDir) throw new Error('runWorker requires config.projectDir');
68
- if (!config.workerTimeoutMs) throw new Error('runWorker requires config.workerTimeoutMs');
69
-
70
- const startMs = Date.now();
71
- const taskStart = nowIso();
72
- const bin = pickClaudeBin(env);
73
- const args = buildArgs(task);
74
-
75
- // M42 D1 — allocate a spawn-id and open the transcript registry entry
76
- const parentSpawnId = (opts && opts.parentSpawnId) || env.GSD_T_SPAWN_ID || null;
77
- const spawnId = (opts && opts.spawnId) || transcriptTee.allocateSpawnId({ parentId: parentSpawnId });
78
- let transcriptOpened = false;
79
- try {
80
- transcriptTee.openTranscript({
81
- spawnId,
82
- projectDir: config.projectDir,
83
- meta: {
84
- parentId: parentSpawnId,
85
- command: 'orchestrator-worker',
86
- description: `task=${task.id} domain=${task.domain || '-'} wave=${task.wave == null ? '-' : task.wave}`,
87
- model: (task && task.model) || 'sonnet',
88
- },
89
- });
90
- transcriptOpened = true;
91
- } catch (_) { /* tee is best-effort */ }
92
-
93
- emitBoundary(onFrame, task, 'start');
94
-
95
- return new Promise((resolve) => {
96
- const child = spawnImpl(bin, args, {
97
- cwd: config.projectDir,
98
- env: { ...env, GSD_T_PROJECT_DIR: config.projectDir, GSD_T_SPAWN_ID: spawnId },
99
- stdio: ['pipe', 'pipe', 'pipe']
100
- });
101
-
102
- const workerPid = child && typeof child.pid === 'number' ? child.pid : null;
103
- emitBoundary(onFrame, task, 'pid', { workerPid });
104
- if (transcriptOpened && workerPid != null) {
105
- try {
106
- const idx = transcriptTee._readIndex(config.projectDir);
107
- const i = idx.spawns.findIndex((s) => s.spawnId === spawnId);
108
- if (i >= 0) { idx.spawns[i].workerPid = workerPid; transcriptTee._writeIndex(config.projectDir, idx); }
109
- } catch (_) {}
110
- }
111
- if (typeof opts.onSpawn === 'function') {
112
- try { opts.onSpawn({ child, pid: workerPid, spawnId }); } catch (_) {}
113
- }
114
-
115
- let stdoutBuf = '';
116
- let stderrBuf = '';
117
- let timedOut = false;
118
- let killTimer = null;
119
-
120
- const handleLine = (line) => {
121
- if (transcriptOpened) {
122
- try { transcriptTee.appendFrame({ spawnId, projectDir: config.projectDir, frame: line }); } catch (_) {}
123
- }
124
- try {
125
- const frame = JSON.parse(line);
126
- if (typeof onFrame === 'function') onFrame(frame);
127
- } catch (_) {
128
- if (typeof onFrame === 'function') {
129
- onFrame({ type: 'raw', line });
130
- }
131
- }
132
- };
133
-
134
- child.stdout.on('data', (chunk) => {
135
- stdoutBuf += chunk.toString('utf8');
136
- stdoutBuf = parseLines(stdoutBuf, handleLine);
137
- });
138
- child.stderr.on('data', (chunk) => {
139
- stderrBuf += chunk.toString('utf8');
140
- });
141
-
142
- const timeout = setTimeout(() => {
143
- timedOut = true;
144
- const elapsed = Date.now() - startMs;
145
- if (config.logger && typeof config.logger.log === 'function') {
146
- config.logger.log(`[worker_timeout] task=${task.id} budget=${config.workerTimeoutMs}ms elapsed=${elapsed}ms`);
147
- }
148
- try { child.kill('SIGTERM'); } catch (_) {}
149
- killTimer = setTimeout(() => { try { child.kill('SIGKILL'); } catch (_) {} }, 5000);
150
- }, config.workerTimeoutMs);
151
-
152
- child.on('error', (err) => {
153
- clearTimeout(timeout);
154
- if (killTimer) clearTimeout(killTimer);
155
- const durationMs = Date.now() - startMs;
156
- emitBoundary(onFrame, task, 'failed', { reason: 'spawn_error', error: String(err), workerPid });
157
- resolve({
158
- result: { ok: false, missing: ['spawn_error'], details: { error: String(err) } },
159
- exitCode: -1,
160
- durationMs,
161
- timedOut: false,
162
- stderr: stderrBuf,
163
- workerPid
164
- });
165
- });
166
-
167
- child.on('exit', async (code, signal) => {
168
- clearTimeout(timeout);
169
- if (killTimer) clearTimeout(killTimer);
170
- if (stdoutBuf.length) {
171
- stdoutBuf = parseLines(stdoutBuf + '\n', handleLine);
172
- }
173
- const durationMs = Date.now() - startMs;
174
- const exitCode = code == null ? (signal ? -1 : 0) : code;
175
-
176
- let result;
177
- try {
178
- result = assertCompletion({
179
- taskId: task.canonicalId || task.id,
180
- projectDir: config.projectDir,
181
- expectedBranch: task.expectedBranch || config.expectedBranch || 'main',
182
- taskStart,
183
- skipTest: !!task.skipTest,
184
- ownedPatterns: task.ownedPatterns || []
185
- });
186
- } catch (err) {
187
- result = { ok: false, missing: ['completion_check_error'], details: { error: String(err) } };
188
- }
189
-
190
- if (timedOut) {
191
- result = {
192
- ok: false,
193
- missing: ['worker_exited_via_timeout', ...(result.missing || [])],
194
- details: { ...(result.details || {}), timedOut: true, budget: config.workerTimeoutMs }
195
- };
196
- } else if (exitCode !== 0) {
197
- const missing = result.missing ? [...result.missing] : [];
198
- if (!missing.includes('worker_exit_nonzero')) missing.unshift('worker_exit_nonzero');
199
- result = { ok: false, missing, details: { ...(result.details || {}), exitCode, signal: signal || null, stderr: stderrBuf.slice(-2000) } };
200
- }
201
-
202
- emitBoundary(onFrame, task, result.ok ? 'done' : 'failed', { exitCode, durationMs, workerPid });
203
- if (transcriptOpened) {
204
- try { transcriptTee.closeTranscript({ spawnId, projectDir: config.projectDir, status: result.ok ? 'done' : 'failed' }); } catch (_) {}
205
- }
206
- resolve({ result, exitCode, durationMs, timedOut, stderr: stderrBuf, workerPid, spawnId });
207
- });
208
-
209
- child.stdin.on('error', () => { /* ignore — covered by child exit */ });
210
- child.stdin.write(brief);
211
- child.stdin.end();
212
- });
213
- }
214
-
215
- module.exports = {
216
- runWorker,
217
- buildArgs,
218
- DEFAULT_CLAUDE_BIN
219
- };