@tekyzinc/gsd-t 3.23.11 → 3.25.10
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 +48 -0
- package/README.md +7 -0
- package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
- package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
- package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
- package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
- package/bin/cli-preflight-checks/ports-free.cjs +110 -0
- package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
- package/bin/cli-preflight.cjs +265 -0
- package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
- package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
- package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
- package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
- package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
- package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
- package/bin/gsd-t-context-brief.cjs +395 -0
- package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
- package/bin/gsd-t-ratelimit-probe.cjs +648 -0
- package/bin/gsd-t-verify-gate-judge.cjs +224 -0
- package/bin/gsd-t-verify-gate.cjs +612 -0
- package/bin/gsd-t.js +45 -1
- package/bin/live-activity-report.cjs +615 -0
- package/bin/m55-substrate-proof.cjs +134 -0
- package/bin/parallel-cli-tee.cjs +206 -0
- package/bin/parallel-cli.cjs +478 -0
- package/commands/gsd-t-execute.md +31 -0
- package/commands/gsd-t-help.md +21 -0
- package/commands/gsd-t-verify.md +38 -0
- package/docs/architecture.md +194 -0
- package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
- package/docs/diagrams/.gsd-t/context-meter.log +9 -0
- package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
- package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
- package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
- package/docs/diagrams/01-top-level-map-d2.png +0 -0
- package/docs/diagrams/01-top-level-map.d2 +77 -0
- package/docs/diagrams/01-top-level-map.mmd +48 -0
- package/docs/diagrams/01-top-level-map.png +0 -0
- package/docs/diagrams/01-top-level-map.svg +126 -0
- package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
- package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
- package/docs/diagrams/02-milestone-lifecycle.png +0 -0
- package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
- package/docs/diagrams/03-wave-mode-d2.png +0 -0
- package/docs/diagrams/03-wave-mode.d2 +33 -0
- package/docs/diagrams/03-wave-mode.mmd +21 -0
- package/docs/diagrams/03-wave-mode.png +0 -0
- package/docs/diagrams/03-wave-mode.svg +113 -0
- package/docs/diagrams/04-design-to-code-d2.png +0 -0
- package/docs/diagrams/04-design-to-code.d2 +35 -0
- package/docs/diagrams/04-design-to-code.mmd +29 -0
- package/docs/diagrams/04-design-to-code.png +0 -0
- package/docs/diagrams/04-design-to-code.svg +115 -0
- package/docs/diagrams/05-backlog-d2.png +0 -0
- package/docs/diagrams/05-backlog.d2 +40 -0
- package/docs/diagrams/05-backlog.mmd +20 -0
- package/docs/diagrams/05-backlog.png +0 -0
- package/docs/diagrams/05-backlog.svg +113 -0
- package/docs/diagrams/06-automation-utilities-d2.png +0 -0
- package/docs/diagrams/06-automation-utilities.d2 +48 -0
- package/docs/diagrams/06-automation-utilities.mmd +47 -0
- package/docs/diagrams/06-automation-utilities.png +0 -0
- package/docs/diagrams/06-automation-utilities.svg +110 -0
- package/docs/diagrams/_theme.d2 +86 -0
- package/docs/requirements.md +48 -0
- package/docs/workflow-diagram.md +338 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +190 -0
- package/scripts/gsd-t-transcript.html +200 -0
- package/templates/CLAUDE-global.md +46 -0
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +3 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GSD-T parallel-cli substrate (M55 D2)
|
|
6
|
+
*
|
|
7
|
+
* N-worker pool runner. Every worker spawn flows through
|
|
8
|
+
* `bin/gsd-t-token-capture.cjs::captureSpawn`. Per-worker tee log streams
|
|
9
|
+
* via `bin/parallel-cli-tee.cjs`. Lifecycle / per-worker timeout / fail-fast
|
|
10
|
+
* policy.
|
|
11
|
+
*
|
|
12
|
+
* Engine-only — does NOT touch any command file in M55.
|
|
13
|
+
*
|
|
14
|
+
* Contract: .gsd-t/contracts/parallel-cli-contract.md v1.0.0 STABLE.
|
|
15
|
+
*
|
|
16
|
+
* Hard rules:
|
|
17
|
+
* 1. Zero external runtime deps. Only Node built-ins + sibling D2 helper.
|
|
18
|
+
* 2. NEVER call child_process.spawn directly outside `_makeSpawnFn`.
|
|
19
|
+
* 3. Every spawn flows through captureSpawn (the wrapper logs the row).
|
|
20
|
+
* 4. results[] sorted by id ASC. Sort runs AFTER all workers complete.
|
|
21
|
+
* 5. wallClockMs = orchestrator real time, NOT cumulative worker time.
|
|
22
|
+
* 6. Fail-fast: SIGTERM in-flight, escalate SIGKILL after 5s grace.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { spawn } = require('child_process');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
const { captureSpawn } = require('./gsd-t-token-capture.cjs');
|
|
30
|
+
const { attachTee, VALID_ID_RE } = require('./parallel-cli-tee.cjs');
|
|
31
|
+
|
|
32
|
+
const SCHEMA_VERSION = '1.0.0';
|
|
33
|
+
const SIGKILL_GRACE_MS = 5000;
|
|
34
|
+
|
|
35
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {Array<object>} opts.workers
|
|
40
|
+
* @param {number} opts.maxConcurrency
|
|
41
|
+
* @param {boolean} [opts.failFast=false]
|
|
42
|
+
* @param {string} [opts.teeDir]
|
|
43
|
+
* @param {string} [opts.projectDir]
|
|
44
|
+
* @param {string} [opts.command='parallel-cli']
|
|
45
|
+
* @param {string} [opts.step='parallel']
|
|
46
|
+
* @param {string} [opts.domain='-']
|
|
47
|
+
* @param {string} [opts.task='-']
|
|
48
|
+
* @returns {Promise<object>} envelope per contract
|
|
49
|
+
*/
|
|
50
|
+
async function runParallel(opts) {
|
|
51
|
+
_validateOpts(opts);
|
|
52
|
+
|
|
53
|
+
const workers = opts.workers;
|
|
54
|
+
const maxConcurrency = opts.maxConcurrency;
|
|
55
|
+
const failFast = !!opts.failFast;
|
|
56
|
+
const teeDir = opts.teeDir || null;
|
|
57
|
+
const projectDir = opts.projectDir || process.cwd();
|
|
58
|
+
const command = opts.command || 'parallel-cli';
|
|
59
|
+
const step = opts.step || 'parallel';
|
|
60
|
+
const domain = opts.domain || '-';
|
|
61
|
+
const task = opts.task || '-';
|
|
62
|
+
|
|
63
|
+
const notes = [];
|
|
64
|
+
const orchestratorStart = process.hrtime.bigint();
|
|
65
|
+
|
|
66
|
+
// Shared cancellation flag (fail-fast). Children registered for cancel.
|
|
67
|
+
const inFlight = new Map(); // workerId → child handle
|
|
68
|
+
const failFastTriggered = { value: false };
|
|
69
|
+
|
|
70
|
+
// Rolling pool: walk workers in declaration order, but throttle to maxConcurrency.
|
|
71
|
+
const pending = workers.slice();
|
|
72
|
+
const running = [];
|
|
73
|
+
const results = [];
|
|
74
|
+
|
|
75
|
+
while (pending.length > 0 || running.length > 0) {
|
|
76
|
+
// Once failFast trips, do not start any new workers — drain only.
|
|
77
|
+
if (failFast && failFastTriggered.value) {
|
|
78
|
+
pending.length = 0;
|
|
79
|
+
}
|
|
80
|
+
while (running.length < maxConcurrency && pending.length > 0) {
|
|
81
|
+
const w = pending.shift();
|
|
82
|
+
const p = _runOneWorker({
|
|
83
|
+
worker: w,
|
|
84
|
+
teeDir,
|
|
85
|
+
projectDir,
|
|
86
|
+
command,
|
|
87
|
+
step,
|
|
88
|
+
domain,
|
|
89
|
+
task,
|
|
90
|
+
inFlight,
|
|
91
|
+
failFastTriggered,
|
|
92
|
+
failFast,
|
|
93
|
+
})
|
|
94
|
+
.then((r) => ({ workerId: w.id, result: r }))
|
|
95
|
+
.catch((err) => ({
|
|
96
|
+
workerId: w.id,
|
|
97
|
+
result: _errorResult(w.id, err),
|
|
98
|
+
}));
|
|
99
|
+
running.push(p);
|
|
100
|
+
|
|
101
|
+
// If failFast, attach a callback to trigger cancel-siblings on first failure.
|
|
102
|
+
p.then(({ result }) => {
|
|
103
|
+
if (failFast && !failFastTriggered.value && result && result.ok === false && !result.cancelled) {
|
|
104
|
+
failFastTriggered.value = true;
|
|
105
|
+
for (const [otherId, child] of inFlight.entries()) {
|
|
106
|
+
if (otherId === result.id) continue;
|
|
107
|
+
_terminateChild(child);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}).catch(() => { /* swallowed; result already shaped */ });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wait for the next worker to finish — Promise.race over current `running`.
|
|
114
|
+
const settled = await Promise.race(running.map((p, idx) => p.then((v) => ({ idx, v }))));
|
|
115
|
+
running.splice(settled.idx, 1);
|
|
116
|
+
results.push(settled.v.result);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Deterministic sort by id ASC, AFTER all workers complete.
|
|
120
|
+
results.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
121
|
+
notes.sort();
|
|
122
|
+
|
|
123
|
+
const orchestratorEnd = process.hrtime.bigint();
|
|
124
|
+
const wallClockMs = Math.round(Number(orchestratorEnd - orchestratorStart) / 1e6);
|
|
125
|
+
|
|
126
|
+
const ok = !results.some((r) => r.ok === false);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion: SCHEMA_VERSION,
|
|
130
|
+
ok,
|
|
131
|
+
wallClockMs,
|
|
132
|
+
maxConcurrencyApplied: maxConcurrency,
|
|
133
|
+
failFast,
|
|
134
|
+
results,
|
|
135
|
+
notes,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function _validateOpts(opts) {
|
|
142
|
+
if (!opts || typeof opts !== 'object') {
|
|
143
|
+
throw new Error('runParallel: opts is required');
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(opts.workers) || opts.workers.length === 0) {
|
|
146
|
+
throw new Error('runParallel: opts.workers must be a non-empty array');
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isInteger(opts.maxConcurrency) || opts.maxConcurrency < 1) {
|
|
149
|
+
throw new Error('runParallel: opts.maxConcurrency must be a positive integer');
|
|
150
|
+
}
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
for (const w of opts.workers) {
|
|
153
|
+
if (!w || typeof w !== 'object') {
|
|
154
|
+
throw new Error('runParallel: every worker must be an object');
|
|
155
|
+
}
|
|
156
|
+
if (typeof w.id !== 'string' || w.id === '') {
|
|
157
|
+
throw new Error('runParallel: worker.id is required and must be a non-empty string');
|
|
158
|
+
}
|
|
159
|
+
if (!VALID_ID_RE.test(w.id)) {
|
|
160
|
+
throw new Error('runParallel: worker.id contains illegal characters: ' + w.id);
|
|
161
|
+
}
|
|
162
|
+
if (seen.has(w.id)) {
|
|
163
|
+
throw new Error('runParallel: duplicate worker.id: ' + w.id);
|
|
164
|
+
}
|
|
165
|
+
seen.add(w.id);
|
|
166
|
+
if (typeof w.cmd !== 'string' || w.cmd === '') {
|
|
167
|
+
throw new Error('runParallel: worker.cmd is required (worker ' + w.id + ')');
|
|
168
|
+
}
|
|
169
|
+
if (!Array.isArray(w.args)) {
|
|
170
|
+
throw new Error('runParallel: worker.args must be an array (worker ' + w.id + ')');
|
|
171
|
+
}
|
|
172
|
+
if (w.timeoutMs != null && (!Number.isFinite(w.timeoutMs) || w.timeoutMs <= 0)) {
|
|
173
|
+
throw new Error('runParallel: worker.timeoutMs must be a positive finite number or null (worker ' + w.id + ')');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Per-worker run ──────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
async function _runOneWorker(ctx) {
|
|
181
|
+
const w = ctx.worker;
|
|
182
|
+
const startNs = process.hrtime.bigint();
|
|
183
|
+
|
|
184
|
+
// captureSpawn-wrapped. spawnFn wires the actual child_process.spawn.
|
|
185
|
+
const wrapped = await captureSpawn({
|
|
186
|
+
command: ctx.command,
|
|
187
|
+
step: ctx.step,
|
|
188
|
+
model: w.model || 'cli',
|
|
189
|
+
description: w.description || w.id,
|
|
190
|
+
projectDir: ctx.projectDir,
|
|
191
|
+
domain: ctx.domain,
|
|
192
|
+
task: ctx.task,
|
|
193
|
+
spawnFn: () => _makeSpawnFn({
|
|
194
|
+
worker: w,
|
|
195
|
+
teeDir: ctx.teeDir,
|
|
196
|
+
projectDir: ctx.projectDir,
|
|
197
|
+
inFlight: ctx.inFlight,
|
|
198
|
+
failFastTriggered: ctx.failFastTriggered,
|
|
199
|
+
})(),
|
|
200
|
+
notes: 'parallel-cli worker',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const endNs = process.hrtime.bigint();
|
|
204
|
+
const durationMs = Math.round(Number(endNs - startNs) / 1e6);
|
|
205
|
+
|
|
206
|
+
// captureSpawn returns { result, usage, rowWritten }; we tunneled our
|
|
207
|
+
// structured result through `result`.
|
|
208
|
+
const r = wrapped.result || {};
|
|
209
|
+
return {
|
|
210
|
+
id: w.id,
|
|
211
|
+
ok: r.exitCode === 0 && !r.cancelled && !r.timedOut && r.signal == null,
|
|
212
|
+
exitCode: typeof r.exitCode === 'number' ? r.exitCode : null,
|
|
213
|
+
signal: r.signal || null,
|
|
214
|
+
durationMs,
|
|
215
|
+
stdoutPath: r.stdoutPath || null,
|
|
216
|
+
stderrPath: r.stderrPath || null,
|
|
217
|
+
stdoutBytes: typeof r.stdoutBytes === 'number' ? r.stdoutBytes : 0,
|
|
218
|
+
stderrBytes: typeof r.stderrBytes === 'number' ? r.stderrBytes : 0,
|
|
219
|
+
stdoutTruncatedToTemp: !!r.stdoutTruncatedToTemp,
|
|
220
|
+
stderrTruncatedToTemp: !!r.stderrTruncatedToTemp,
|
|
221
|
+
timedOut: !!r.timedOut,
|
|
222
|
+
cancelled: !!r.cancelled,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _errorResult(id, err) {
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
ok: false,
|
|
230
|
+
exitCode: null,
|
|
231
|
+
signal: null,
|
|
232
|
+
durationMs: 0,
|
|
233
|
+
stdoutPath: null,
|
|
234
|
+
stderrPath: null,
|
|
235
|
+
stdoutBytes: 0,
|
|
236
|
+
stderrBytes: 0,
|
|
237
|
+
stdoutTruncatedToTemp: false,
|
|
238
|
+
stderrTruncatedToTemp: false,
|
|
239
|
+
timedOut: false,
|
|
240
|
+
cancelled: false,
|
|
241
|
+
error: err && err.message ? err.message : String(err),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Spawn factory ───────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function _makeSpawnFn(ctx) {
|
|
248
|
+
const w = ctx.worker;
|
|
249
|
+
return function spawnAndWait() {
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
const child = spawn(w.cmd, w.args || [], {
|
|
252
|
+
cwd: w.cwd || ctx.projectDir,
|
|
253
|
+
env: { ...process.env, ...(w.env || {}) },
|
|
254
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
255
|
+
shell: false,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const tee = attachTee(child, { workerId: w.id, teeDir: ctx.teeDir });
|
|
259
|
+
|
|
260
|
+
let timedOut = false;
|
|
261
|
+
let cancelled = false;
|
|
262
|
+
let timeoutTimer = null;
|
|
263
|
+
let killTimer = null;
|
|
264
|
+
|
|
265
|
+
// Attach cancel hook BEFORE inFlight registration so the orchestrator
|
|
266
|
+
// never observes a child without its hook.
|
|
267
|
+
child.__pcli_cancel = () => {
|
|
268
|
+
if (cancelled) return;
|
|
269
|
+
cancelled = true;
|
|
270
|
+
_killChild(child, killTimer);
|
|
271
|
+
};
|
|
272
|
+
ctx.inFlight.set(w.id, child);
|
|
273
|
+
|
|
274
|
+
if (Number.isFinite(w.timeoutMs) && w.timeoutMs > 0) {
|
|
275
|
+
timeoutTimer = setTimeout(() => {
|
|
276
|
+
timedOut = true;
|
|
277
|
+
cancelled = true;
|
|
278
|
+
_killChild(child, killTimer);
|
|
279
|
+
}, w.timeoutMs);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
child.on('close', async (code, signal) => {
|
|
283
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
284
|
+
if (killTimer) clearTimeout(killTimer);
|
|
285
|
+
ctx.inFlight.delete(w.id);
|
|
286
|
+
await tee.close();
|
|
287
|
+
// captureSpawn does not consume `usage` from this object (no `usage`
|
|
288
|
+
// field present), so the token-log row will render `—`.
|
|
289
|
+
resolve({
|
|
290
|
+
exitCode: typeof code === 'number' ? code : null,
|
|
291
|
+
signal: signal || null,
|
|
292
|
+
stdoutPath: tee.stdoutPath,
|
|
293
|
+
stderrPath: tee.stderrPath,
|
|
294
|
+
stdoutBytes: tee.stdoutBytes(),
|
|
295
|
+
stderrBytes: tee.stderrBytes(),
|
|
296
|
+
stdoutTruncatedToTemp: tee.stdoutTruncatedToTemp(),
|
|
297
|
+
stderrTruncatedToTemp: tee.stderrTruncatedToTemp(),
|
|
298
|
+
timedOut,
|
|
299
|
+
cancelled,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
child.on('error', async (err) => {
|
|
304
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
305
|
+
if (killTimer) clearTimeout(killTimer);
|
|
306
|
+
ctx.inFlight.delete(w.id);
|
|
307
|
+
await tee.close();
|
|
308
|
+
resolve({
|
|
309
|
+
exitCode: null,
|
|
310
|
+
signal: null,
|
|
311
|
+
stdoutPath: tee.stdoutPath,
|
|
312
|
+
stderrPath: tee.stderrPath,
|
|
313
|
+
stdoutBytes: tee.stdoutBytes(),
|
|
314
|
+
stderrBytes: tee.stderrBytes(),
|
|
315
|
+
stdoutTruncatedToTemp: tee.stdoutTruncatedToTemp(),
|
|
316
|
+
stderrTruncatedToTemp: tee.stderrTruncatedToTemp(),
|
|
317
|
+
timedOut,
|
|
318
|
+
cancelled,
|
|
319
|
+
error: err && err.message ? err.message : String(err),
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function _killChild(child, killTimer) {
|
|
327
|
+
if (!child) return;
|
|
328
|
+
try { child.kill('SIGTERM'); } catch (_) { /* may be dead already */ }
|
|
329
|
+
if (!killTimer) {
|
|
330
|
+
setTimeout(() => {
|
|
331
|
+
try { child.kill('SIGKILL'); } catch (_) { /* may be dead already */ }
|
|
332
|
+
}, SIGKILL_GRACE_MS).unref?.();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _terminateChild(child) {
|
|
337
|
+
if (!child || typeof child.__pcli_cancel !== 'function') return;
|
|
338
|
+
child.__pcli_cancel();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
function _parseCliArgs(argv) {
|
|
344
|
+
const out = {
|
|
345
|
+
plan: null,
|
|
346
|
+
maxConcurrency: null,
|
|
347
|
+
failFast: false,
|
|
348
|
+
teeDir: null,
|
|
349
|
+
json: false,
|
|
350
|
+
};
|
|
351
|
+
for (let i = 0; i < argv.length; i++) {
|
|
352
|
+
const a = argv[i];
|
|
353
|
+
if (a === '--plan') out.plan = argv[++i];
|
|
354
|
+
else if (a === '--max-concurrency') out.maxConcurrency = parseInt(argv[++i], 10);
|
|
355
|
+
else if (a === '--fail-fast') out.failFast = true;
|
|
356
|
+
else if (a === '--tee-dir') out.teeDir = argv[++i];
|
|
357
|
+
else if (a === '--json') out.json = true;
|
|
358
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
359
|
+
else throw new Error('unknown flag: ' + a);
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function _readStdin() {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
let data = '';
|
|
367
|
+
process.stdin.setEncoding('utf8');
|
|
368
|
+
process.stdin.on('data', (c) => { data += c; });
|
|
369
|
+
process.stdin.on('end', () => resolve(data));
|
|
370
|
+
process.stdin.on('error', reject);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function _cliMain(argv) {
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = _parseCliArgs(argv);
|
|
378
|
+
} catch (e) {
|
|
379
|
+
process.stderr.write('parallel-cli: ' + e.message + '\n');
|
|
380
|
+
process.exit(2);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (parsed.help) {
|
|
384
|
+
process.stdout.write([
|
|
385
|
+
'Usage: node bin/parallel-cli.cjs --max-concurrency N [--plan FILE] [--fail-fast] [--tee-dir DIR] [--json]',
|
|
386
|
+
'',
|
|
387
|
+
'Reads worker plan from --plan FILE or stdin. Plan shape:',
|
|
388
|
+
' { "workers": [ { "id": "lint", "cmd": "npx", "args": ["biome","check"] }, ... ] }',
|
|
389
|
+
'',
|
|
390
|
+
].join('\n'));
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!Number.isInteger(parsed.maxConcurrency) || parsed.maxConcurrency < 1) {
|
|
395
|
+
process.stderr.write('parallel-cli: --max-concurrency is required (positive integer)\n');
|
|
396
|
+
process.exit(2);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let planText;
|
|
400
|
+
try {
|
|
401
|
+
if (parsed.plan) {
|
|
402
|
+
planText = fs.readFileSync(parsed.plan, 'utf8');
|
|
403
|
+
} else {
|
|
404
|
+
planText = await _readStdin();
|
|
405
|
+
}
|
|
406
|
+
} catch (e) {
|
|
407
|
+
process.stderr.write('parallel-cli: cannot read plan: ' + e.message + '\n');
|
|
408
|
+
process.exit(2);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let plan;
|
|
412
|
+
try {
|
|
413
|
+
plan = JSON.parse(planText);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
process.stderr.write('parallel-cli: plan JSON parse error: ' + e.message + '\n');
|
|
416
|
+
process.exit(2);
|
|
417
|
+
}
|
|
418
|
+
if (!plan || !Array.isArray(plan.workers)) {
|
|
419
|
+
process.stderr.write('parallel-cli: plan.workers must be an array\n');
|
|
420
|
+
process.exit(2);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let envelope;
|
|
424
|
+
try {
|
|
425
|
+
envelope = await runParallel({
|
|
426
|
+
workers: plan.workers,
|
|
427
|
+
maxConcurrency: parsed.maxConcurrency,
|
|
428
|
+
failFast: parsed.failFast,
|
|
429
|
+
teeDir: parsed.teeDir,
|
|
430
|
+
command: 'parallel-cli',
|
|
431
|
+
step: 'cli',
|
|
432
|
+
});
|
|
433
|
+
} catch (e) {
|
|
434
|
+
process.stderr.write('parallel-cli: runtime error: ' + (e && e.stack || e) + '\n');
|
|
435
|
+
process.exit(3);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (parsed.json) {
|
|
439
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
440
|
+
} else {
|
|
441
|
+
process.stdout.write(_renderText(envelope));
|
|
442
|
+
}
|
|
443
|
+
process.exit(envelope.ok ? 0 : 1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function _renderText(envelope) {
|
|
447
|
+
const lines = [];
|
|
448
|
+
lines.push('parallel-cli: ' + (envelope.ok ? 'OK' : 'FAIL') +
|
|
449
|
+
' (workers=' + envelope.results.length +
|
|
450
|
+
', maxConcurrency=' + envelope.maxConcurrencyApplied +
|
|
451
|
+
', wallClockMs=' + envelope.wallClockMs + ')');
|
|
452
|
+
for (const r of envelope.results) {
|
|
453
|
+
const tag = r.ok ? '✓' : '✗';
|
|
454
|
+
lines.push(' ' + tag + ' ' + r.id +
|
|
455
|
+
' exit=' + r.exitCode +
|
|
456
|
+
' sig=' + r.signal +
|
|
457
|
+
' ' + r.durationMs + 'ms' +
|
|
458
|
+
(r.timedOut ? ' [TIMEOUT]' : '') +
|
|
459
|
+
(r.cancelled ? ' [CANCELLED]' : ''));
|
|
460
|
+
}
|
|
461
|
+
return lines.join('\n') + '\n';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (require.main === module) {
|
|
465
|
+
_cliMain(process.argv.slice(2)).catch((e) => {
|
|
466
|
+
process.stderr.write('parallel-cli: unhandled: ' + (e && e.stack || e) + '\n');
|
|
467
|
+
process.exit(3);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
module.exports = {
|
|
472
|
+
runParallel,
|
|
473
|
+
SCHEMA_VERSION,
|
|
474
|
+
SIGKILL_GRACE_MS,
|
|
475
|
+
// exposed for tests
|
|
476
|
+
_validateOpts,
|
|
477
|
+
_renderText,
|
|
478
|
+
};
|
|
@@ -84,6 +84,37 @@ Identify:
|
|
|
84
84
|
- Which tasks are unblocked (no pending dependencies)
|
|
85
85
|
- Which tasks are blocked (waiting on checkpoints)
|
|
86
86
|
|
|
87
|
+
<!-- M55-D5: preflight + brief wire-in -->
|
|
88
|
+
**M55 Preflight + Context-Brief (mandatory before fan-out):**
|
|
89
|
+
|
|
90
|
+
Once per execute invocation, before spawning ANY worker subagent:
|
|
91
|
+
|
|
92
|
+
1. Run state preflight:
|
|
93
|
+
```bash
|
|
94
|
+
gsd-t preflight --json > /tmp/gsd-t-preflight.json || true
|
|
95
|
+
```
|
|
96
|
+
If exit code is `4`, STOP. Read `/tmp/gsd-t-preflight.json`, surface the failed
|
|
97
|
+
`severity:"error"` checks (wrong branch, occupied port, etc.) to the user, and
|
|
98
|
+
do NOT spawn any workers. The verify-gate (Track 1) will hard-fail the same
|
|
99
|
+
way at verify time — fail-early here saves the round-trip.
|
|
100
|
+
|
|
101
|
+
2. For each domain `D` about to receive a worker spawn, generate a context brief
|
|
102
|
+
ONCE per domain per spawn (replaces 30–60k context re-read with ≤2,500-token
|
|
103
|
+
JSON snapshot — M55 charter Pattern B):
|
|
104
|
+
```bash
|
|
105
|
+
SPAWN_ID="execute-${D}-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
106
|
+
gsd-t brief --kind execute --domain "${D}" --spawn-id "${SPAWN_ID}" --out ".gsd-t/briefs/${SPAWN_ID}.json"
|
|
107
|
+
export BRIEF_PATH=".gsd-t/briefs/${SPAWN_ID}.json"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
3. Pass `$BRIEF_PATH` into each worker's prompt scaffold so the worker greps the
|
|
111
|
+
brief instead of re-walking the repo. Workers receive the canonical
|
|
112
|
+
instruction "check the brief first" via the subagent protocols
|
|
113
|
+
(`templates/prompts/{qa,red-team,design-verify}-subagent.md`).
|
|
114
|
+
|
|
115
|
+
The `.gsd-t/briefs/` directory is gitignored — briefs are per-spawn ephemera,
|
|
116
|
+
not committed artifacts.
|
|
117
|
+
|
|
87
118
|
## Step 1.5: Graph-Enhanced Domain Isolation Check (if available)
|
|
88
119
|
|
|
89
120
|
```bash
|
package/commands/gsd-t-help.md
CHANGED
|
@@ -434,6 +434,27 @@ Use these when user asks for help on a specific command:
|
|
|
434
434
|
- **Files**: `.gsd-t/backlog-settings.md`
|
|
435
435
|
- **Use when**: Customizing the classification dimensions for your project
|
|
436
436
|
|
|
437
|
+
### preflight (M55)
|
|
438
|
+
- **Summary**: Deterministic state-precondition check (`bin/cli-preflight.cjs`); 6 built-in checks (branch-guard, contracts-stable, deps-installed, manifest-fresh, ports-free, working-tree-state). Exit 0/4. Pluggable, zero-dep, captureSpawn-exempt.
|
|
439
|
+
- **Auto-invoked**: Yes — by `gsd-t-execute` Step 1 and `gsd-t verify-gate` Track 1
|
|
440
|
+
- **Files**: `bin/cli-preflight.cjs`, `bin/cli-preflight-checks/`
|
|
441
|
+
- **Use when**: Before any worker spawn or verify run — catches drift (wrong branch, port collision, DRAFT contracts) early.
|
|
442
|
+
- **CLI**: `gsd-t preflight [--project DIR] [--json | --text] [--skip id1,id2]`
|
|
443
|
+
|
|
444
|
+
### brief (M55)
|
|
445
|
+
- **Summary**: Generate a ≤10 KB / ≤2,500-token context brief for a worker spawn (`bin/gsd-t-context-brief.cjs`); 6 kinds (`execute`, `verify`, `qa`, `red-team`, `design-verify`, `scan`). Replaces 30–60k context re-read with structured JSON snapshot.
|
|
446
|
+
- **Auto-invoked**: Yes — by `gsd-t-execute` Step 1 (one brief per domain spawn)
|
|
447
|
+
- **Files**: `bin/gsd-t-context-brief.cjs`, `bin/gsd-t-context-brief-kinds/`, `.gsd-t/briefs/` (gitignored)
|
|
448
|
+
- **Use when**: Replacing repo re-reads with a structured snapshot for parallel workers.
|
|
449
|
+
- **CLI**: `gsd-t brief --kind X --domain Y --spawn-id Z [--out PATH] [--strict]`
|
|
450
|
+
|
|
451
|
+
### verify-gate (M55)
|
|
452
|
+
- **Summary**: Two-track verify-gate (`bin/gsd-t-verify-gate.cjs`); Track 1 runs preflight (hard-fail on severity:error), Track 2 fans out tsc/lint/tests/dead-code/secrets/complexity via `bin/parallel-cli.cjs::runParallel`. Returns ≤500-token JSON summary an LLM judges. Defensive on missing `.gsd-t/ratelimit-map.json`.
|
|
453
|
+
- **Auto-invoked**: Yes — by `gsd-t-verify` Step 2
|
|
454
|
+
- **Files**: `bin/gsd-t-verify-gate.cjs`, `bin/gsd-t-verify-gate-judge.cjs`
|
|
455
|
+
- **Use when**: Final pre-merge gate. Both tracks always run; both report. `ok` is purely deterministic — LLM verdict is advisory.
|
|
456
|
+
- **CLI**: `gsd-t verify-gate [--skip-track1] [--skip-track2] [--max-concurrency N] [--fail-fast] [--json]`. Exit 0/4/2/3.
|
|
457
|
+
|
|
437
458
|
## Unknown Command
|
|
438
459
|
|
|
439
460
|
If user asks for help on unrecognized command:
|
package/commands/gsd-t-verify.md
CHANGED
|
@@ -64,6 +64,44 @@ Run the full test audit directly:
|
|
|
64
64
|
|
|
65
65
|
Verification cannot complete if any test fails or critical contract gaps remain.
|
|
66
66
|
|
|
67
|
+
<!-- M55-D5: verify-gate wire-in -->
|
|
68
|
+
**M55 Verify-Gate (mandatory two-track gate):**
|
|
69
|
+
|
|
70
|
+
Before finalizing the verify report, invoke the M55 verify-gate. It runs Track 1
|
|
71
|
+
(`bin/cli-preflight.cjs::runPreflight` — preflight envelope; hard-fails on any
|
|
72
|
+
`severity:"error"` check) AND Track 2 (`bin/parallel-cli.cjs::runParallel` fans
|
|
73
|
+
out off-the-shelf CLIs: tsc, biome/ruff, npm test, knip, gitleaks, scc/lizard).
|
|
74
|
+
Both tracks always run; both report. The gate's `ok` flag is purely
|
|
75
|
+
deterministic (`track1.ok && track2.ok`) — the LLM judge's verdict is advisory.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gsd-t verify-gate --json > /tmp/gsd-t-verify-gate.json || true
|
|
79
|
+
GATE_OK=$(node -e "const e=require('/tmp/gsd-t-verify-gate.json');console.log(e.ok?'true':'false')")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If `GATE_OK=false`, surface the failed track to the user and do NOT mark verify
|
|
83
|
+
complete. The summary in the envelope is ≤500 tokens — pipe it to the LLM judge
|
|
84
|
+
for a confirming verdict:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cat /tmp/gsd-t-verify-gate.json | gsd-t verify-gate-judge > /tmp/gsd-t-verify-gate-prompt.txt
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The judge prompt (`bin/gsd-t-verify-gate-judge.cjs`) is ≤500 tokens regardless
|
|
91
|
+
of envelope size — feed it to the LLM judge to render a `PASS` / `FAIL` verdict
|
|
92
|
+
on the deterministic summary. The LLM verdict NEVER overrides `ok` — it
|
|
93
|
+
confirms or contradicts. A contradiction is a Red Team finding, not a gate
|
|
94
|
+
override.
|
|
95
|
+
|
|
96
|
+
Raw worker output stays at `.gsd-t/verify-gate/{runId}/{workerId}.{stdout,stderr}.ndjson`
|
|
97
|
+
for human-only inspection. The directory is gitignored.
|
|
98
|
+
|
|
99
|
+
Defensive on missing `.gsd-t/ratelimit-map.json` — verify-gate falls back to
|
|
100
|
+
`maxConcurrency=2` and logs a structured note. Override with
|
|
101
|
+
`gsd-t verify-gate --max-concurrency N --json` if needed.
|
|
102
|
+
|
|
103
|
+
Contract: `.gsd-t/contracts/verify-gate-contract.md` v1.0.0 STABLE.
|
|
104
|
+
|
|
67
105
|
## Step 2.5: High-Risk Domain Gate (MANDATORY — Categories 2 and 7)
|
|
68
106
|
|
|
69
107
|
```bash
|