@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +7 -0
  3. package/bin/cli-preflight-checks/branch-guard.cjs +110 -0
  4. package/bin/cli-preflight-checks/contracts-stable.cjs +128 -0
  5. package/bin/cli-preflight-checks/deps-installed.cjs +89 -0
  6. package/bin/cli-preflight-checks/manifest-fresh.cjs +98 -0
  7. package/bin/cli-preflight-checks/ports-free.cjs +110 -0
  8. package/bin/cli-preflight-checks/working-tree-state.cjs +149 -0
  9. package/bin/cli-preflight.cjs +265 -0
  10. package/bin/gsd-t-context-brief-kinds/design-verify.cjs +139 -0
  11. package/bin/gsd-t-context-brief-kinds/execute.cjs +205 -0
  12. package/bin/gsd-t-context-brief-kinds/qa.cjs +130 -0
  13. package/bin/gsd-t-context-brief-kinds/red-team.cjs +131 -0
  14. package/bin/gsd-t-context-brief-kinds/scan.cjs +118 -0
  15. package/bin/gsd-t-context-brief-kinds/verify.cjs +157 -0
  16. package/bin/gsd-t-context-brief.cjs +395 -0
  17. package/bin/gsd-t-ratelimit-probe-worker.cjs +236 -0
  18. package/bin/gsd-t-ratelimit-probe.cjs +648 -0
  19. package/bin/gsd-t-verify-gate-judge.cjs +224 -0
  20. package/bin/gsd-t-verify-gate.cjs +612 -0
  21. package/bin/gsd-t.js +45 -1
  22. package/bin/live-activity-report.cjs +615 -0
  23. package/bin/m55-substrate-proof.cjs +134 -0
  24. package/bin/parallel-cli-tee.cjs +206 -0
  25. package/bin/parallel-cli.cjs +478 -0
  26. package/commands/gsd-t-execute.md +31 -0
  27. package/commands/gsd-t-help.md +21 -0
  28. package/commands/gsd-t-verify.md +38 -0
  29. package/docs/architecture.md +194 -0
  30. package/docs/diagrams/.gsd-t/.context-meter-state.json +10 -0
  31. package/docs/diagrams/.gsd-t/context-meter.log +9 -0
  32. package/docs/diagrams/.gsd-t/events/2026-05-08.jsonl +45 -0
  33. package/docs/diagrams/.gsd-t/events/2026-05-09.jsonl +1 -0
  34. package/docs/diagrams/.gsd-t/heartbeat-cd9e7f59-ba5b-406a-9ed6-16762f039e81.jsonl +48 -0
  35. package/docs/diagrams/01-top-level-map-d2.png +0 -0
  36. package/docs/diagrams/01-top-level-map.d2 +77 -0
  37. package/docs/diagrams/01-top-level-map.mmd +48 -0
  38. package/docs/diagrams/01-top-level-map.png +0 -0
  39. package/docs/diagrams/01-top-level-map.svg +126 -0
  40. package/docs/diagrams/02-milestone-lifecycle-d2.png +0 -0
  41. package/docs/diagrams/02-milestone-lifecycle.d2 +38 -0
  42. package/docs/diagrams/02-milestone-lifecycle.mmd +36 -0
  43. package/docs/diagrams/02-milestone-lifecycle.png +0 -0
  44. package/docs/diagrams/02-milestone-lifecycle.svg +114 -0
  45. package/docs/diagrams/03-wave-mode-d2.png +0 -0
  46. package/docs/diagrams/03-wave-mode.d2 +33 -0
  47. package/docs/diagrams/03-wave-mode.mmd +21 -0
  48. package/docs/diagrams/03-wave-mode.png +0 -0
  49. package/docs/diagrams/03-wave-mode.svg +113 -0
  50. package/docs/diagrams/04-design-to-code-d2.png +0 -0
  51. package/docs/diagrams/04-design-to-code.d2 +35 -0
  52. package/docs/diagrams/04-design-to-code.mmd +29 -0
  53. package/docs/diagrams/04-design-to-code.png +0 -0
  54. package/docs/diagrams/04-design-to-code.svg +115 -0
  55. package/docs/diagrams/05-backlog-d2.png +0 -0
  56. package/docs/diagrams/05-backlog.d2 +40 -0
  57. package/docs/diagrams/05-backlog.mmd +20 -0
  58. package/docs/diagrams/05-backlog.png +0 -0
  59. package/docs/diagrams/05-backlog.svg +113 -0
  60. package/docs/diagrams/06-automation-utilities-d2.png +0 -0
  61. package/docs/diagrams/06-automation-utilities.d2 +48 -0
  62. package/docs/diagrams/06-automation-utilities.mmd +47 -0
  63. package/docs/diagrams/06-automation-utilities.png +0 -0
  64. package/docs/diagrams/06-automation-utilities.svg +110 -0
  65. package/docs/diagrams/_theme.d2 +86 -0
  66. package/docs/requirements.md +48 -0
  67. package/docs/workflow-diagram.md +338 -0
  68. package/package.json +1 -1
  69. package/scripts/gsd-t-dashboard-server.js +190 -0
  70. package/scripts/gsd-t-transcript.html +200 -0
  71. package/templates/CLAUDE-global.md +46 -0
  72. package/templates/prompts/design-verify-subagent.md +3 -0
  73. package/templates/prompts/qa-subagent.md +3 -0
  74. 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 = ["parallelism-report.cjs"];
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;