@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,612 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GSD-T verify-gate (M55 D5)
|
|
6
|
+
*
|
|
7
|
+
* Two-track gate:
|
|
8
|
+
* Track 1 — D1 preflight envelope (`bin/cli-preflight.cjs::runPreflight`).
|
|
9
|
+
* Hard-fail on any severity:"error" check.
|
|
10
|
+
* Track 2 — D2 parallel-CLI substrate (`bin/parallel-cli.cjs::runParallel`).
|
|
11
|
+
* Fans out tsc / lint / tests / dead-code / secrets / complexity.
|
|
12
|
+
*
|
|
13
|
+
* Returns a ≤500-token JSON summary the LLM judge consumes via
|
|
14
|
+
* `bin/gsd-t-verify-gate-judge.cjs`. Raw worker output stays on disk under
|
|
15
|
+
* `.gsd-t/verify-gate/{runId}/`.
|
|
16
|
+
*
|
|
17
|
+
* Contract: .gsd-t/contracts/verify-gate-contract.md v1.0.0 STABLE.
|
|
18
|
+
*
|
|
19
|
+
* Hard rules:
|
|
20
|
+
* 1. Zero external runtime deps. Only Node built-ins + sibling D1/D2 libraries.
|
|
21
|
+
* 2. NEVER call child_process.spawn directly. Track 2 fans out via D2.
|
|
22
|
+
* 3. ok = (skipTrack1 || track1.ok) && (skipTrack2 || track2.ok). Purely deterministic.
|
|
23
|
+
* 4. summary serialization ≤summaryTokenCap (default 500 tokens at 4 chars/token).
|
|
24
|
+
* 5. Defensive on missing .gsd-t/ratelimit-map.json — fall back to maxConcurrency=2.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const { runPreflight } = require('./cli-preflight.cjs');
|
|
31
|
+
const { runParallel } = require('./parallel-cli.cjs');
|
|
32
|
+
|
|
33
|
+
const SCHEMA_VERSION = '1.0.0';
|
|
34
|
+
const DEFAULT_SUMMARY_TOKEN_CAP = 500;
|
|
35
|
+
const TOKENS_PER_CHAR = 0.25; // 4 chars/token approximation
|
|
36
|
+
const DEFAULT_FALLBACK_MAX_CONCURRENCY = 2;
|
|
37
|
+
const SNIPPET_CHARS_PER_SIDE_DEFAULT = 200;
|
|
38
|
+
const SNIPPET_CHARS_PER_SIDE_FLOOR = 16; // 32 chars total per snippet
|
|
39
|
+
|
|
40
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute Track 1 + Track 2 and return the v1.0.0 envelope.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} [opts]
|
|
46
|
+
* @param {string} [opts.projectDir='.']
|
|
47
|
+
* @param {string[]} [opts.preflightChecks] restrict D1 to these checks
|
|
48
|
+
* @param {Array<object>} [opts.parallelTrack] override default Track 2 worker spec list
|
|
49
|
+
* @param {number} [opts.maxConcurrency] override D3-map default
|
|
50
|
+
* @param {boolean} [opts.failFast=false] passed to runParallel
|
|
51
|
+
* @param {number} [opts.summaryTokenCap=500] summary hard cap
|
|
52
|
+
* @param {boolean} [opts.skipTrack1=false] diagnostic only
|
|
53
|
+
* @param {boolean} [opts.skipTrack2=false] diagnostic only
|
|
54
|
+
* @param {Date} [opts.now] injected for tests
|
|
55
|
+
* @param {Function} [opts.runParallelImpl] DI for tests; default = real runParallel
|
|
56
|
+
* @returns {Promise<object>}
|
|
57
|
+
*/
|
|
58
|
+
async function runVerifyGate(opts) {
|
|
59
|
+
opts = opts || {};
|
|
60
|
+
const projectDir = opts.projectDir || '.';
|
|
61
|
+
const summaryTokenCap = Number.isFinite(opts.summaryTokenCap) && opts.summaryTokenCap > 0
|
|
62
|
+
? opts.summaryTokenCap
|
|
63
|
+
: DEFAULT_SUMMARY_TOKEN_CAP;
|
|
64
|
+
const skipTrack1 = !!opts.skipTrack1;
|
|
65
|
+
const skipTrack2 = !!opts.skipTrack2;
|
|
66
|
+
const failFast = !!opts.failFast;
|
|
67
|
+
const now = opts.now instanceof Date ? opts.now : new Date();
|
|
68
|
+
const runParallelImpl = typeof opts.runParallelImpl === 'function'
|
|
69
|
+
? opts.runParallelImpl
|
|
70
|
+
: runParallel;
|
|
71
|
+
|
|
72
|
+
const notes = [];
|
|
73
|
+
|
|
74
|
+
// ── Track 1 ───────────────────────────────────────────────────────────────
|
|
75
|
+
let track1;
|
|
76
|
+
if (skipTrack1) {
|
|
77
|
+
track1 = {
|
|
78
|
+
schemaVersion: '1.0.0',
|
|
79
|
+
ok: true,
|
|
80
|
+
skipped: true,
|
|
81
|
+
checks: [],
|
|
82
|
+
notes: ['skipped by flag'],
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
try {
|
|
86
|
+
track1 = runPreflight({ projectDir, checks: opts.preflightChecks });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
track1 = {
|
|
89
|
+
schemaVersion: '1.0.0',
|
|
90
|
+
ok: false,
|
|
91
|
+
checks: [],
|
|
92
|
+
notes: ['runPreflight threw: ' + (err && err.message || String(err))],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Resolve maxConcurrency from D3 map (defensive) ────────────────────────
|
|
98
|
+
const resolved = _resolveMaxConcurrency({
|
|
99
|
+
projectDir,
|
|
100
|
+
explicit: opts.maxConcurrency,
|
|
101
|
+
});
|
|
102
|
+
const maxConcurrency = resolved.value;
|
|
103
|
+
for (const n of resolved.notes) notes.push(n);
|
|
104
|
+
|
|
105
|
+
// ── runId + on-disk dir ───────────────────────────────────────────────────
|
|
106
|
+
const runId = _runIdFromDate(now);
|
|
107
|
+
const teeDir = path.join(projectDir, '.gsd-t', 'verify-gate', runId);
|
|
108
|
+
|
|
109
|
+
// ── Track 2 ───────────────────────────────────────────────────────────────
|
|
110
|
+
let track2;
|
|
111
|
+
if (skipTrack2) {
|
|
112
|
+
track2 = {
|
|
113
|
+
ok: true,
|
|
114
|
+
skipped: true,
|
|
115
|
+
wallClockMs: 0,
|
|
116
|
+
maxConcurrencyApplied: maxConcurrency,
|
|
117
|
+
workers: [],
|
|
118
|
+
notes: ['skipped by flag'],
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
const plan = Array.isArray(opts.parallelTrack)
|
|
122
|
+
? opts.parallelTrack
|
|
123
|
+
: _detectDefaultTrack2(projectDir, notes);
|
|
124
|
+
|
|
125
|
+
if (plan.length === 0) {
|
|
126
|
+
track2 = {
|
|
127
|
+
ok: true,
|
|
128
|
+
wallClockMs: 0,
|
|
129
|
+
maxConcurrencyApplied: maxConcurrency,
|
|
130
|
+
workers: [],
|
|
131
|
+
notes: ['track 2: no detected CLIs — Track 2 is a no-op'],
|
|
132
|
+
};
|
|
133
|
+
} else {
|
|
134
|
+
let envelope;
|
|
135
|
+
try {
|
|
136
|
+
envelope = await runParallelImpl({
|
|
137
|
+
workers: plan,
|
|
138
|
+
maxConcurrency,
|
|
139
|
+
failFast,
|
|
140
|
+
teeDir,
|
|
141
|
+
projectDir,
|
|
142
|
+
command: 'gsd-t-verify-gate',
|
|
143
|
+
step: 'Track 2',
|
|
144
|
+
domain: 'm55-d5',
|
|
145
|
+
task: '-',
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
envelope = {
|
|
149
|
+
ok: false,
|
|
150
|
+
wallClockMs: 0,
|
|
151
|
+
maxConcurrencyApplied: maxConcurrency,
|
|
152
|
+
results: [],
|
|
153
|
+
notes: ['runParallel threw: ' + (err && err.message || String(err))],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
track2 = _shapeTrack2(envelope, plan);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Summary (≤summaryTokenCap) ────────────────────────────────────────────
|
|
162
|
+
const summary = _buildSummary({ track1, track2, summaryTokenCap });
|
|
163
|
+
|
|
164
|
+
const ok = (skipTrack1 ? true : !!track1.ok) && (skipTrack2 ? true : !!track2.ok);
|
|
165
|
+
|
|
166
|
+
// Sort notes for determinism.
|
|
167
|
+
notes.sort();
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: SCHEMA_VERSION,
|
|
171
|
+
ok,
|
|
172
|
+
track1,
|
|
173
|
+
track2,
|
|
174
|
+
summary,
|
|
175
|
+
llmJudgePromptHint: 'Render PASS / FAIL verdict on the summary above. Be terse. The deterministic verdict is `summary.verdict`; you confirm or contradict.',
|
|
176
|
+
meta: {
|
|
177
|
+
runId,
|
|
178
|
+
generatedAt: now.toISOString(),
|
|
179
|
+
},
|
|
180
|
+
notes,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Internal: maxConcurrency resolution ─────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function _resolveMaxConcurrency({ projectDir, explicit }) {
|
|
187
|
+
const notes = [];
|
|
188
|
+
if (Number.isFinite(explicit) && explicit > 0) {
|
|
189
|
+
return { value: Math.floor(explicit), notes };
|
|
190
|
+
}
|
|
191
|
+
const mapPath = path.join(projectDir, '.gsd-t', 'ratelimit-map.json');
|
|
192
|
+
let mapData;
|
|
193
|
+
try {
|
|
194
|
+
mapData = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
|
195
|
+
} catch (_err) {
|
|
196
|
+
notes.push('ratelimit-map.json absent — using maxConcurrency=' + DEFAULT_FALLBACK_MAX_CONCURRENCY + ' conservative default');
|
|
197
|
+
return { value: DEFAULT_FALLBACK_MAX_CONCURRENCY, notes };
|
|
198
|
+
}
|
|
199
|
+
const peak = mapData
|
|
200
|
+
&& mapData.recommended
|
|
201
|
+
&& typeof mapData.recommended.peakConcurrency === 'number'
|
|
202
|
+
&& mapData.recommended.peakConcurrency >= 1
|
|
203
|
+
? Math.floor(mapData.recommended.peakConcurrency)
|
|
204
|
+
: null;
|
|
205
|
+
if (peak == null) {
|
|
206
|
+
notes.push('ratelimit-map.json missing recommended.peakConcurrency — using maxConcurrency=' + DEFAULT_FALLBACK_MAX_CONCURRENCY);
|
|
207
|
+
return { value: DEFAULT_FALLBACK_MAX_CONCURRENCY, notes };
|
|
208
|
+
}
|
|
209
|
+
return { value: peak, notes };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Internal: Track 2 default plan detection ───────────────────────────────
|
|
213
|
+
//
|
|
214
|
+
// Detection is read-only — D5 NEVER auto-installs.
|
|
215
|
+
// CLIs that aren't installed surface as workers with skipped:true downstream.
|
|
216
|
+
|
|
217
|
+
function _detectDefaultTrack2(projectDir, notes) {
|
|
218
|
+
const plan = [];
|
|
219
|
+
const has = (rel) => {
|
|
220
|
+
try { return fs.existsSync(path.join(projectDir, rel)); } catch (_) { return false; }
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// typecheck — tsc
|
|
224
|
+
if (has('node_modules/.bin/tsc') || has('tsconfig.json')) {
|
|
225
|
+
plan.push({
|
|
226
|
+
id: 'tsc',
|
|
227
|
+
cmd: 'npx',
|
|
228
|
+
args: ['--no-install', 'tsc', '--noEmit'],
|
|
229
|
+
timeoutMs: 120000,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// lint (JS) — biome
|
|
234
|
+
if (has('biome.json') || has('biome.jsonc')) {
|
|
235
|
+
plan.push({
|
|
236
|
+
id: 'lint-js',
|
|
237
|
+
cmd: 'npx',
|
|
238
|
+
args: ['--no-install', 'biome', 'check'],
|
|
239
|
+
timeoutMs: 60000,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// lint (Py) — ruff (only if pyproject.toml has [tool.ruff])
|
|
244
|
+
if (has('pyproject.toml')) {
|
|
245
|
+
let pyproject = '';
|
|
246
|
+
try { pyproject = fs.readFileSync(path.join(projectDir, 'pyproject.toml'), 'utf8'); } catch (_) {}
|
|
247
|
+
if (/\[tool\.ruff\]/.test(pyproject)) {
|
|
248
|
+
plan.push({
|
|
249
|
+
id: 'lint-py',
|
|
250
|
+
cmd: 'ruff',
|
|
251
|
+
args: ['check', '.'],
|
|
252
|
+
timeoutMs: 60000,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// tests — npm test
|
|
258
|
+
if (has('package.json')) {
|
|
259
|
+
let pkg = {};
|
|
260
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8')); } catch (_) {}
|
|
261
|
+
if (pkg && pkg.scripts && typeof pkg.scripts.test === 'string') {
|
|
262
|
+
plan.push({
|
|
263
|
+
id: 'tests',
|
|
264
|
+
cmd: 'npm',
|
|
265
|
+
args: ['test', '--silent'],
|
|
266
|
+
timeoutMs: 600000,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// dead-code — knip
|
|
272
|
+
if (has('node_modules/.bin/knip')) {
|
|
273
|
+
plan.push({
|
|
274
|
+
id: 'dead-code',
|
|
275
|
+
cmd: 'npx',
|
|
276
|
+
args: ['--no-install', 'knip'],
|
|
277
|
+
timeoutMs: 60000,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// secrets — gitleaks (PATH detection deferred to runtime)
|
|
282
|
+
if (_hasOnPath('gitleaks')) {
|
|
283
|
+
plan.push({
|
|
284
|
+
id: 'secrets',
|
|
285
|
+
cmd: 'gitleaks',
|
|
286
|
+
args: ['detect', '--no-git', '-v'],
|
|
287
|
+
timeoutMs: 60000,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// complexity — scc preferred, lizard fallback
|
|
292
|
+
if (_hasOnPath('scc')) {
|
|
293
|
+
plan.push({
|
|
294
|
+
id: 'complexity',
|
|
295
|
+
cmd: 'scc',
|
|
296
|
+
args: ['.'],
|
|
297
|
+
timeoutMs: 60000,
|
|
298
|
+
});
|
|
299
|
+
} else if (_hasOnPath('lizard')) {
|
|
300
|
+
plan.push({
|
|
301
|
+
id: 'complexity',
|
|
302
|
+
cmd: 'lizard',
|
|
303
|
+
args: ['.'],
|
|
304
|
+
timeoutMs: 120000,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (plan.length === 0) {
|
|
309
|
+
notes.push('track 2: no off-the-shelf CLIs detected — Track 2 plan is empty');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return plan;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function _hasOnPath(cmd) {
|
|
316
|
+
// Probe via PATH segments. Read-only, no spawn.
|
|
317
|
+
const PATH = process.env.PATH || '';
|
|
318
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
319
|
+
for (const dir of PATH.split(sep)) {
|
|
320
|
+
if (!dir) continue;
|
|
321
|
+
try {
|
|
322
|
+
if (fs.existsSync(path.join(dir, cmd))) return true;
|
|
323
|
+
} catch (_) {}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Internal: Shape D2 envelope into track2 ─────────────────────────────────
|
|
329
|
+
|
|
330
|
+
function _shapeTrack2(envelope, plan) {
|
|
331
|
+
const planById = new Map();
|
|
332
|
+
for (const w of plan) planById.set(w.id, w);
|
|
333
|
+
|
|
334
|
+
const workers = (envelope.results || []).map((r) => {
|
|
335
|
+
const planEntry = planById.get(r.id) || {};
|
|
336
|
+
const cap = Number.isFinite(planEntry.summarySnippetCharsPerSide)
|
|
337
|
+
&& planEntry.summarySnippetCharsPerSide > 0
|
|
338
|
+
? Math.floor(planEntry.summarySnippetCharsPerSide)
|
|
339
|
+
: SNIPPET_CHARS_PER_SIDE_DEFAULT;
|
|
340
|
+
return {
|
|
341
|
+
id: r.id,
|
|
342
|
+
ok: !!r.ok,
|
|
343
|
+
exitCode: typeof r.exitCode === 'number' ? r.exitCode : null,
|
|
344
|
+
durationMs: typeof r.durationMs === 'number' ? r.durationMs : 0,
|
|
345
|
+
skipped: false,
|
|
346
|
+
reason: null,
|
|
347
|
+
summarySnippet: _readSummarySnippet(r, cap),
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
workers.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
352
|
+
|
|
353
|
+
const track2Ok = workers.every((w) => w.ok || w.skipped);
|
|
354
|
+
const track2Notes = Array.isArray(envelope.notes) ? envelope.notes.slice() : [];
|
|
355
|
+
track2Notes.sort();
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
ok: track2Ok,
|
|
359
|
+
wallClockMs: typeof envelope.wallClockMs === 'number' ? envelope.wallClockMs : 0,
|
|
360
|
+
maxConcurrencyApplied: typeof envelope.maxConcurrencyApplied === 'number'
|
|
361
|
+
? envelope.maxConcurrencyApplied
|
|
362
|
+
: 0,
|
|
363
|
+
workers,
|
|
364
|
+
notes: track2Notes,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function _readSummarySnippet(workerResult, cap) {
|
|
369
|
+
// Prefer reading the tee NDJSON, but the file-system read is best-effort —
|
|
370
|
+
// a failure surfaces as an empty snippet, not a crash.
|
|
371
|
+
let stdoutText = '';
|
|
372
|
+
let stderrText = '';
|
|
373
|
+
if (workerResult.stdoutPath) {
|
|
374
|
+
stdoutText = _readNdjsonText(workerResult.stdoutPath);
|
|
375
|
+
}
|
|
376
|
+
if (workerResult.stderrPath) {
|
|
377
|
+
stderrText = _readNdjsonText(workerResult.stderrPath);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const stdoutSnip = _headTail(stdoutText, cap);
|
|
381
|
+
const stderrSnip = stderrText.length > 0 ? '\nSTDERR: ' + _headTail(stderrText, cap) : '';
|
|
382
|
+
const out = (stdoutSnip + stderrSnip).trim();
|
|
383
|
+
return _sanitizeForJson(out);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function _readNdjsonText(filePath) {
|
|
387
|
+
try {
|
|
388
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
389
|
+
const lines = raw.split(/\n/);
|
|
390
|
+
const data = [];
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
if (!line.trim()) continue;
|
|
393
|
+
try {
|
|
394
|
+
const obj = JSON.parse(line);
|
|
395
|
+
if (typeof obj.data === 'string') data.push(obj.data);
|
|
396
|
+
} catch (_) {
|
|
397
|
+
// Skip malformed line.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return data.join('\n');
|
|
401
|
+
} catch (_) {
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function _headTail(text, cap) {
|
|
407
|
+
if (!text) return '';
|
|
408
|
+
if (text.length <= cap * 2) return text;
|
|
409
|
+
return text.slice(0, cap) + '\n…\n' + text.slice(-cap);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function _sanitizeForJson(s) {
|
|
413
|
+
// Replace unprintable control chars (except \n, \t) with '?'.
|
|
414
|
+
let out = '';
|
|
415
|
+
for (let i = 0; i < s.length; i++) {
|
|
416
|
+
const c = s.charCodeAt(i);
|
|
417
|
+
if (c === 9 || c === 10) { out += s[i]; continue; }
|
|
418
|
+
if (c < 32 || c === 127) { out += '?'; continue; }
|
|
419
|
+
out += s[i];
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── Internal: build summary with hard cap ──────────────────────────────────
|
|
425
|
+
|
|
426
|
+
function _buildSummary({ track1, track2, summaryTokenCap }) {
|
|
427
|
+
const track1Failed = (track1.checks || [])
|
|
428
|
+
.filter((c) => c.ok === false)
|
|
429
|
+
.map((c) => ({ id: c.id, severity: c.severity, msg: String(c.msg || '') }));
|
|
430
|
+
|
|
431
|
+
const track2FailedFull = (track2.workers || [])
|
|
432
|
+
.filter((w) => !w.ok && !w.skipped)
|
|
433
|
+
.map((w) => ({
|
|
434
|
+
id: w.id,
|
|
435
|
+
exitCode: w.exitCode,
|
|
436
|
+
summarySnippet: String(w.summarySnippet || ''),
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
let snippetCap = SNIPPET_CHARS_PER_SIDE_DEFAULT;
|
|
440
|
+
let truncatedNote = null;
|
|
441
|
+
let working = track2FailedFull.map((w) => ({ ...w }));
|
|
442
|
+
|
|
443
|
+
// Iteratively shrink snippets until the serialized summary fits.
|
|
444
|
+
while (true) {
|
|
445
|
+
const summary = {
|
|
446
|
+
verdict: (track1.ok && track2.ok) ? 'PASS' : 'FAIL',
|
|
447
|
+
track1: {
|
|
448
|
+
ok: !!track1.ok,
|
|
449
|
+
failedChecks: track1Failed,
|
|
450
|
+
},
|
|
451
|
+
track2: {
|
|
452
|
+
ok: !!track2.ok,
|
|
453
|
+
failedWorkers: working,
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
const json = JSON.stringify(summary);
|
|
457
|
+
const tokenEstimate = Math.ceil(json.length * TOKENS_PER_CHAR);
|
|
458
|
+
if (tokenEstimate <= summaryTokenCap) {
|
|
459
|
+
if (truncatedNote && Array.isArray(track2.notes)) {
|
|
460
|
+
track2.notes.push(truncatedNote);
|
|
461
|
+
track2.notes.sort();
|
|
462
|
+
}
|
|
463
|
+
return summary;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Halve the per-side snippet cap.
|
|
467
|
+
snippetCap = Math.floor(snippetCap / 2);
|
|
468
|
+
if (snippetCap < SNIPPET_CHARS_PER_SIDE_FLOOR) {
|
|
469
|
+
// We're at the floor. Truncate the failedWorkers list.
|
|
470
|
+
if (working.length > 1) {
|
|
471
|
+
const removed = working.length - 1;
|
|
472
|
+
working = working.slice(0, 1);
|
|
473
|
+
truncatedNote = 'truncated: ' + removed + ' more failed workers';
|
|
474
|
+
// Re-loop with truncated list.
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
// Single worker, smallest snippet, still over cap — accept and emit.
|
|
478
|
+
const summaryFinal = {
|
|
479
|
+
verdict: (track1.ok && track2.ok) ? 'PASS' : 'FAIL',
|
|
480
|
+
track1: {
|
|
481
|
+
ok: !!track1.ok,
|
|
482
|
+
failedChecks: track1Failed,
|
|
483
|
+
},
|
|
484
|
+
track2: {
|
|
485
|
+
ok: !!track2.ok,
|
|
486
|
+
failedWorkers: working,
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
if (truncatedNote && Array.isArray(track2.notes)) {
|
|
490
|
+
track2.notes.push(truncatedNote);
|
|
491
|
+
track2.notes.sort();
|
|
492
|
+
}
|
|
493
|
+
return summaryFinal;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Re-shrink each working snippet head+tail to floor*2 chars.
|
|
497
|
+
working = working.map((w) => ({
|
|
498
|
+
...w,
|
|
499
|
+
summarySnippet: _shrinkSnippet(w.summarySnippet, snippetCap),
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function _shrinkSnippet(snip, cap) {
|
|
505
|
+
if (typeof snip !== 'string' || snip.length <= cap * 2) return snip || '';
|
|
506
|
+
return snip.slice(0, cap) + '\n…\n' + snip.slice(-cap);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ── Internal: runId ─────────────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
function _runIdFromDate(d) {
|
|
512
|
+
const iso = d.toISOString().replace(/[:.]/g, '-');
|
|
513
|
+
return 'verify-gate-' + iso.slice(0, 19) + 'Z';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
function _parseArgv(argv) {
|
|
519
|
+
const out = {
|
|
520
|
+
projectDir: '.',
|
|
521
|
+
mode: 'json',
|
|
522
|
+
skipTrack1: false,
|
|
523
|
+
skipTrack2: false,
|
|
524
|
+
};
|
|
525
|
+
for (let i = 0; i < argv.length; i++) {
|
|
526
|
+
const a = argv[i];
|
|
527
|
+
if (a === '--project') out.projectDir = argv[++i] || '.';
|
|
528
|
+
else if (a === '--json') out.mode = 'json';
|
|
529
|
+
else if (a === '--skip-track1') out.skipTrack1 = true;
|
|
530
|
+
else if (a === '--skip-track2') out.skipTrack2 = true;
|
|
531
|
+
else if (a === '--max-concurrency') {
|
|
532
|
+
const v = parseInt(argv[++i] || '', 10);
|
|
533
|
+
if (Number.isFinite(v) && v >= 1) out.maxConcurrency = v;
|
|
534
|
+
else out._badFlag = '--max-concurrency requires a positive integer';
|
|
535
|
+
} else if (a === '--fail-fast') out.failFast = true;
|
|
536
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
537
|
+
else { out._badFlag = 'unknown flag: ' + a; }
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function _printHelp() {
|
|
543
|
+
const lines = [
|
|
544
|
+
'Usage: gsd-t verify-gate [options]',
|
|
545
|
+
'',
|
|
546
|
+
'Options:',
|
|
547
|
+
' --project DIR Project root (default: .)',
|
|
548
|
+
' --json Print JSON envelope (default)',
|
|
549
|
+
' --skip-track1 Skip preflight (diagnostic only)',
|
|
550
|
+
' --skip-track2 Skip parallel CLIs (diagnostic only)',
|
|
551
|
+
' --max-concurrency N Override D3-map default (default: read .gsd-t/ratelimit-map.json)',
|
|
552
|
+
' --fail-fast Cancel siblings on first failure (passed to runParallel)',
|
|
553
|
+
' --help Show this help',
|
|
554
|
+
'',
|
|
555
|
+
'Exit codes:',
|
|
556
|
+
' 0 ok=true',
|
|
557
|
+
' 4 ok=false',
|
|
558
|
+
' 2 CLI usage error',
|
|
559
|
+
' 3 unhandled internal error',
|
|
560
|
+
];
|
|
561
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function _runCli(argv) {
|
|
565
|
+
const args = _parseArgv(argv);
|
|
566
|
+
if (args.help) {
|
|
567
|
+
_printHelp();
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
if (args._badFlag) {
|
|
571
|
+
process.stderr.write('verify-gate: ' + args._badFlag + '\n');
|
|
572
|
+
return 2;
|
|
573
|
+
}
|
|
574
|
+
let envelope;
|
|
575
|
+
try {
|
|
576
|
+
envelope = await runVerifyGate({
|
|
577
|
+
projectDir: args.projectDir,
|
|
578
|
+
maxConcurrency: args.maxConcurrency,
|
|
579
|
+
failFast: !!args.failFast,
|
|
580
|
+
skipTrack1: args.skipTrack1,
|
|
581
|
+
skipTrack2: args.skipTrack2,
|
|
582
|
+
});
|
|
583
|
+
} catch (err) {
|
|
584
|
+
process.stderr.write('verify-gate: ' + (err && err.message || String(err)) + '\n');
|
|
585
|
+
return 3;
|
|
586
|
+
}
|
|
587
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
588
|
+
return envelope.ok ? 0 : 4;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (require.main === module) {
|
|
592
|
+
_runCli(process.argv.slice(2)).then(
|
|
593
|
+
(code) => process.exit(code),
|
|
594
|
+
(err) => { process.stderr.write(String(err) + '\n'); process.exit(3); }
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
module.exports = {
|
|
599
|
+
runVerifyGate,
|
|
600
|
+
SCHEMA_VERSION,
|
|
601
|
+
// Test surface (not part of the public contract):
|
|
602
|
+
_resolveMaxConcurrency,
|
|
603
|
+
_detectDefaultTrack2,
|
|
604
|
+
_shapeTrack2,
|
|
605
|
+
_buildSummary,
|
|
606
|
+
_shrinkSnippet,
|
|
607
|
+
_runIdFromDate,
|
|
608
|
+
_parseArgv,
|
|
609
|
+
_readNdjsonText,
|
|
610
|
+
_headTail,
|
|
611
|
+
_sanitizeForJson,
|
|
612
|
+
};
|
package/bin/gsd-t.js
CHANGED
|
@@ -1175,7 +1175,15 @@ function installUtilityScripts() {
|
|
|
1175
1175
|
// `path.join(__dirname, "..", "bin", <tool>)` (e.g. gsd-t-dashboard-server.js
|
|
1176
1176
|
// → parallelism-report.cjs). Distinct from PROJECT_BIN_TOOLS, which copy into
|
|
1177
1177
|
// each registered project's bin/.
|
|
1178
|
-
const GLOBAL_BIN_TOOLS = [
|
|
1178
|
+
const GLOBAL_BIN_TOOLS = [
|
|
1179
|
+
"parallelism-report.cjs",
|
|
1180
|
+
"live-activity-report.cjs",
|
|
1181
|
+
// M55 D5 — preflight + brief + verify-gate dispatch targets propagated to ~/.claude/bin/.
|
|
1182
|
+
"cli-preflight.cjs",
|
|
1183
|
+
"gsd-t-context-brief.cjs",
|
|
1184
|
+
"gsd-t-verify-gate.cjs",
|
|
1185
|
+
"gsd-t-verify-gate-judge.cjs",
|
|
1186
|
+
];
|
|
1179
1187
|
|
|
1180
1188
|
function installGlobalBinTools() {
|
|
1181
1189
|
ensureDir(GLOBAL_BIN_DIR);
|
|
@@ -4500,6 +4508,42 @@ if (require.main === module) {
|
|
|
4500
4508
|
});
|
|
4501
4509
|
process.exit(res.status == null ? 1 : res.status);
|
|
4502
4510
|
}
|
|
4511
|
+
case "preflight": {
|
|
4512
|
+
// M55 D5 — `gsd-t preflight` thin dispatcher to bin/cli-preflight.cjs.
|
|
4513
|
+
const { spawnSync } = require("child_process");
|
|
4514
|
+
const js = path.join(__dirname, "cli-preflight.cjs");
|
|
4515
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
4516
|
+
stdio: "inherit",
|
|
4517
|
+
});
|
|
4518
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
4519
|
+
}
|
|
4520
|
+
case "brief": {
|
|
4521
|
+
// M55 D5 — `gsd-t brief` thin dispatcher to bin/gsd-t-context-brief.cjs.
|
|
4522
|
+
const { spawnSync } = require("child_process");
|
|
4523
|
+
const js = path.join(__dirname, "gsd-t-context-brief.cjs");
|
|
4524
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
4525
|
+
stdio: "inherit",
|
|
4526
|
+
});
|
|
4527
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
4528
|
+
}
|
|
4529
|
+
case "verify-gate": {
|
|
4530
|
+
// M55 D5 — `gsd-t verify-gate` thin dispatcher to bin/gsd-t-verify-gate.cjs.
|
|
4531
|
+
const { spawnSync } = require("child_process");
|
|
4532
|
+
const js = path.join(__dirname, "gsd-t-verify-gate.cjs");
|
|
4533
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
4534
|
+
stdio: "inherit",
|
|
4535
|
+
});
|
|
4536
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
4537
|
+
}
|
|
4538
|
+
case "verify-gate-judge": {
|
|
4539
|
+
// M55 D5 — `gsd-t verify-gate-judge` thin dispatcher to bin/gsd-t-verify-gate-judge.cjs.
|
|
4540
|
+
const { spawnSync } = require("child_process");
|
|
4541
|
+
const js = path.join(__dirname, "gsd-t-verify-gate-judge.cjs");
|
|
4542
|
+
const res = spawnSync(process.execPath, [js, ...args.slice(1)], {
|
|
4543
|
+
stdio: "inherit",
|
|
4544
|
+
});
|
|
4545
|
+
process.exit(res.status == null ? 1 : res.status);
|
|
4546
|
+
}
|
|
4503
4547
|
case "stream-feed": {
|
|
4504
4548
|
doStreamFeed(args.slice(1));
|
|
4505
4549
|
break;
|