@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,157 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * verify kind collector — scans every contract under `.gsd-t/contracts/`
5
+ * for status, gathers the success-criteria list from the active charter,
6
+ * and surfaces the verify-gate plan reference (consumed by D5).
7
+ *
8
+ * Fail-open: missing optional source → null/empty field, brief still written.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const NAME = 'verify';
15
+
16
+ function _readMaybe(file) {
17
+ try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
18
+ }
19
+
20
+ function _scanContracts(projectDir) {
21
+ // Pure scan — does NOT record sources (caller decides which subset to record).
22
+ const dir = path.join(projectDir, '.gsd-t', 'contracts');
23
+ let entries;
24
+ try { entries = fs.readdirSync(dir); } catch (_) { return []; }
25
+ const out = [];
26
+ for (const f of entries) {
27
+ if (!f.endsWith('.md')) continue;
28
+ const rel = '.gsd-t/contracts/' + f;
29
+ const text = _readMaybe(path.join(projectDir, rel));
30
+ if (!text) continue;
31
+ let status = 'UNKNOWN';
32
+ const m = text.match(/Status:\s*\**\s*(STABLE|DRAFT|PROPOSED)/i);
33
+ if (m) status = m[1].toUpperCase();
34
+ out.push({ path: rel, status });
35
+ }
36
+ out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
37
+ return out;
38
+ }
39
+
40
+ /**
41
+ * Extract numbered success-criterion entries (`1. ...`) from any charter
42
+ * file under `.gsd-t/charters/`. Picks the most recent by mtime.
43
+ */
44
+ function _successCriteriaFromCharter(projectDir, recordSource) {
45
+ const dir = path.join(projectDir, '.gsd-t', 'charters');
46
+ let entries;
47
+ try { entries = fs.readdirSync(dir); } catch (_) { return { source: null, items: [] }; }
48
+ const candidates = entries
49
+ .filter((f) => f.endsWith('-charter.md') || f.endsWith('.md'))
50
+ .map((f) => {
51
+ const full = path.join(dir, f);
52
+ let mt = 0;
53
+ try { mt = fs.statSync(full).mtimeMs; } catch (_) { /* ignore */ }
54
+ return { f, mt };
55
+ })
56
+ .sort((a, b) => b.mt - a.mt);
57
+ if (!candidates.length) return { source: null, items: [] };
58
+
59
+ const top = candidates[0];
60
+ const rel = '.gsd-t/charters/' + top.f;
61
+ const text = _readMaybe(path.join(projectDir, rel));
62
+ if (!text) return { source: null, items: [] };
63
+ recordSource(rel);
64
+
65
+ // Find the success-criteria section: `## Falsifiable Success Criteria` (or similar).
66
+ const re = /^##[^\n]*(?:Success Criteria|Falsifiable[^\n]*)\s*$([\s\S]*?)(?=^##\s+|\Z)/mi;
67
+ const m = text.match(re);
68
+ if (!m) return { source: rel, items: [] };
69
+
70
+ const items = [];
71
+ // Match `1. ...` or `1) ...` (number, dot or paren, space).
72
+ const numberedRe = /^\s*\d+[.)]\s+(.+)$/gm;
73
+ let nm;
74
+ while ((nm = numberedRe.exec(m[1])) != null) {
75
+ let item = nm[1].replace(/`/g, '').replace(/\*\*/g, '').trim();
76
+ if (item.length > 200) item = item.slice(0, 197) + '...';
77
+ items.push(item);
78
+ }
79
+ return { source: rel, items };
80
+ }
81
+
82
+ function _verifyGateContractRef(projectDir, recordSource) {
83
+ const rel = '.gsd-t/contracts/verify-gate-contract.md';
84
+ const full = path.join(projectDir, rel);
85
+ if (!fs.existsSync(full)) return null;
86
+ recordSource(rel);
87
+ const text = _readMaybe(full);
88
+ if (!text) return rel;
89
+ return rel;
90
+ }
91
+
92
+ function _priorVerifyResults(projectDir, recordSource) {
93
+ // Heuristic: most recent file under .gsd-t/verify/ if it exists.
94
+ const dir = path.join(projectDir, '.gsd-t', 'verify');
95
+ let entries;
96
+ try { entries = fs.readdirSync(dir); } catch (_) { return null; }
97
+ if (!entries.length) return null;
98
+ const sorted = entries
99
+ .map((f) => {
100
+ const full = path.join(dir, f);
101
+ let mt = 0;
102
+ try { mt = fs.statSync(full).mtimeMs; } catch (_) {}
103
+ return { f, mt };
104
+ })
105
+ .sort((a, b) => b.mt - a.mt);
106
+ if (!sorted.length) return null;
107
+ const rel = '.gsd-t/verify/' + sorted[0].f;
108
+ recordSource(rel);
109
+ return rel;
110
+ }
111
+
112
+ function collect(ctx) {
113
+ const { projectDir, recordSource } = ctx;
114
+ const contracts = _scanContracts(projectDir);
115
+ // Record mtimes only for the enumerated subset (DRAFT/PROPOSED).
116
+ for (const c of contracts) {
117
+ if (c.status === 'DRAFT' || c.status === 'PROPOSED') recordSource(c.path);
118
+ }
119
+ const sc = _successCriteriaFromCharter(projectDir, recordSource);
120
+ const verifyGatePlan = _verifyGateContractRef(projectDir, recordSource);
121
+ const priorVerifyResults = _priorVerifyResults(projectDir, recordSource);
122
+
123
+ // Summarize contract list rather than inlining all 65+ paths twice.
124
+ // The full status map lives in `contracts` already.
125
+ const statusCounts = { STABLE: 0, DRAFT: 0, PROPOSED: 0, UNKNOWN: 0 };
126
+ for (const c of contracts) {
127
+ statusCounts[c.status] = (statusCounts[c.status] || 0) + 1;
128
+ }
129
+ // Only DRAFT / PROPOSED contracts (the ones a verifier cares about) are
130
+ // surfaced individually to the brief; STABLE / UNKNOWN are counted only.
131
+ const enumerated = contracts.filter((c) => c.status === 'DRAFT' || c.status === 'PROPOSED');
132
+
133
+ const ancillary = {
134
+ charterSource: sc.source,
135
+ contractCount: contracts.length,
136
+ contractStatusCounts: statusCounts,
137
+ enumeratedContracts: enumerated.map((c) => c.path).sort(),
138
+ priorVerifyResults,
139
+ successCriteria: sc.items,
140
+ verifyGatePlan,
141
+ };
142
+
143
+ return {
144
+ scope: { owned: [], notOwned: [], deliverables: [] },
145
+ constraints: [],
146
+ contracts: enumerated,
147
+ ancillary,
148
+ };
149
+ }
150
+
151
+ module.exports = {
152
+ name: NAME,
153
+ requiresSources: [],
154
+ collect,
155
+ _scanContracts,
156
+ _successCriteriaFromCharter,
157
+ };
@@ -0,0 +1,395 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GSD-T Context Brief Generator (M55 D4)
5
+ *
6
+ * Pluggable, deterministic, zero-dep context-brief library + thin CLI.
7
+ *
8
+ * Pure inspector — no LLM spawn, no token spend, no side effects beyond reading
9
+ * the filesystem, running read-only `git` commands inside per-kind collectors,
10
+ * and (optionally) writing the resulting brief JSON to `--out`.
11
+ *
12
+ * Contract: .gsd-t/contracts/context-brief-contract.md v1.0.0 STABLE.
13
+ *
14
+ * Hard rules (mirroring bin/cli-preflight.cjs / bin/parallelism-report.cjs):
15
+ * 1. Zero external runtime deps. Only Node built-ins.
16
+ * 2. Synchronous public API.
17
+ * 3. Per-kind throws are caught and surface as a structured error.
18
+ * 4. Deterministic output — sorted arrays, alphabetical JSON keys.
19
+ * 5. captureSpawn-exempt — see contract § captureSpawn Exemption.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ const SCHEMA_VERSION = '1.0.0';
26
+ const MAX_BRIEF_BYTES = 10240;
27
+ const KINDS_DIR_NAME = 'gsd-t-context-brief-kinds';
28
+ const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
29
+
30
+ const FAIL_CLOSED_KINDS = new Set(['qa', 'red-team', 'design-verify']);
31
+
32
+ // ── Public API ──────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Synchronously assemble a context brief.
36
+ *
37
+ * @param {object} opts
38
+ * @param {string} opts.projectDir
39
+ * @param {string} opts.kind one of KINDS
40
+ * @param {string|null} [opts.domain]
41
+ * @param {string} opts.spawnId
42
+ * @param {boolean} [opts.strict=false] upgrade fail-open kinds to fail-closed
43
+ * @param {Date} [opts.now] injected for tests / determinism
44
+ * @returns {object} brief envelope
45
+ */
46
+ function generateBrief(opts) {
47
+ if (!opts || typeof opts !== 'object') {
48
+ throw new Error('generateBrief: opts required');
49
+ }
50
+ const projectDir = typeof opts.projectDir === 'string' && opts.projectDir.length
51
+ ? opts.projectDir
52
+ : '.';
53
+ const kind = opts.kind;
54
+ const domain = (typeof opts.domain === 'string' && opts.domain.length) ? opts.domain : null;
55
+ const spawnId = opts.spawnId;
56
+ const strict = !!opts.strict;
57
+ const now = opts.now instanceof Date ? opts.now : new Date();
58
+
59
+ if (typeof kind !== 'string' || !kind.length) {
60
+ throw new Error('generateBrief: kind required');
61
+ }
62
+ if (typeof spawnId !== 'string' || !spawnId.length) {
63
+ throw new Error('generateBrief: spawnId required');
64
+ }
65
+ if (!SAFE_NAME_RE.test(spawnId)) {
66
+ throw new Error('generateBrief: spawnId contains unsafe characters (allowed: [a-zA-Z0-9_-])');
67
+ }
68
+ if (domain != null && !SAFE_NAME_RE.test(domain)) {
69
+ throw new Error('generateBrief: domain contains unsafe characters (allowed: [a-zA-Z0-9_-])');
70
+ }
71
+
72
+ const registry = loadKindRegistry();
73
+ const kindMod = registry.find((k) => k.name === kind);
74
+ if (!kindMod) {
75
+ throw new Error('generateBrief: unknown kind "' + kind + '" (known: ' +
76
+ registry.map((k) => k.name).sort().join(',') + ')');
77
+ }
78
+
79
+ // sourceMtimes is gathered via the recorder helper passed into the collector.
80
+ const sourceMtimes = {};
81
+ function recordSource(relPath) {
82
+ if (typeof relPath !== 'string' || !relPath.length) return null;
83
+ const full = path.join(projectDir, relPath);
84
+ let stat;
85
+ try { stat = fs.statSync(full); } catch (_) { return null; }
86
+ sourceMtimes[relPath] = new Date(stat.mtimeMs).toISOString();
87
+ return stat;
88
+ }
89
+
90
+ // Required-source check (fail-closed kinds)
91
+ const missingRequired = [];
92
+ for (const req of (kindMod.requiresSources || [])) {
93
+ const full = path.join(projectDir, req);
94
+ if (!fs.existsSync(full)) missingRequired.push(req);
95
+ }
96
+ const isFailClosedKind = FAIL_CLOSED_KINDS.has(kind);
97
+ if (missingRequired.length && (isFailClosedKind || strict)) {
98
+ const err = new Error('generateBrief: required source(s) missing for kind=' + kind +
99
+ ': ' + missingRequired.sort().join(', '));
100
+ err.code = 'EREQUIRED_MISSING';
101
+ err.missing = missingRequired.slice().sort();
102
+ throw err;
103
+ }
104
+
105
+ let collected;
106
+ try {
107
+ collected = kindMod.collect({
108
+ projectDir,
109
+ kind,
110
+ domain,
111
+ spawnId,
112
+ strict,
113
+ recordSource,
114
+ });
115
+ } catch (err) {
116
+ // Preserve structured failures (EREQUIRED_MISSING from OR-required kinds
117
+ // such as design-verify) so the CLI can map them to exit 4.
118
+ if (err && err.code === 'EREQUIRED_MISSING') throw err;
119
+ const wrapped = new Error('generateBrief: kind "' + kind + '" collector threw: ' +
120
+ (err && err.message || String(err)));
121
+ wrapped.cause = err;
122
+ throw wrapped;
123
+ }
124
+
125
+ if (!collected || typeof collected !== 'object') {
126
+ throw new Error('generateBrief: kind "' + kind + '" collector returned non-object');
127
+ }
128
+
129
+ // Branch detection (read-only). Captured at the library level, not per-kind,
130
+ // so all briefs share a uniform branch field.
131
+ const branch = _gitCurrentBranch(projectDir);
132
+
133
+ const brief = {
134
+ ancillary: _normalizeAncillary(collected.ancillary),
135
+ branch: branch || '',
136
+ constraints: _normalizeConstraints(collected.constraints),
137
+ contracts: _normalizeContracts(collected.contracts),
138
+ domain,
139
+ generatedAt: now.toISOString(),
140
+ kind,
141
+ schemaVersion: SCHEMA_VERSION,
142
+ scope: _normalizeScope(collected.scope),
143
+ sourceMtimes: _sortObjectKeys(sourceMtimes),
144
+ spawnId,
145
+ };
146
+
147
+ // Hard cap enforcement at write-time.
148
+ const serialized = stableStringify(brief);
149
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_BRIEF_BYTES) {
150
+ const err = new Error('brief exceeds MAX_BRIEF_BYTES (' + Buffer.byteLength(serialized, 'utf8') +
151
+ ' > ' + MAX_BRIEF_BYTES + ')');
152
+ err.code = 'EBRIEF_TOO_LARGE';
153
+ throw err;
154
+ }
155
+
156
+ return brief;
157
+ }
158
+
159
+ /**
160
+ * Discover and load all kind collector modules from
161
+ * `bin/gsd-t-context-brief-kinds/*.cjs`.
162
+ * @returns {Array<{name:string, requiresSources:string[], collect:Function}>}
163
+ */
164
+ function loadKindRegistry() {
165
+ const dir = path.join(__dirname, KINDS_DIR_NAME);
166
+ let entries;
167
+ try {
168
+ entries = fs.readdirSync(dir);
169
+ } catch (_) {
170
+ return [];
171
+ }
172
+ const out = [];
173
+ for (const filename of entries) {
174
+ if (!filename.endsWith('.cjs')) continue;
175
+ const full = path.join(dir, filename);
176
+ let mod;
177
+ try { mod = require(full); } catch (_) { continue; }
178
+ if (!_isValidKindModule(mod, filename)) continue;
179
+ out.push(mod);
180
+ }
181
+ out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
182
+ return out;
183
+ }
184
+
185
+ const KINDS = ['design-verify', 'execute', 'qa', 'red-team', 'scan', 'verify'];
186
+
187
+ /**
188
+ * Deterministic JSON stringifier — alphabetical keys at every nesting level,
189
+ * arrays preserved in caller-supplied order. Used by `generateBrief` for
190
+ * cap-check and by the test suite for byte-identical assertions.
191
+ * @param {*} value
192
+ * @returns {string}
193
+ */
194
+ function stableStringify(value) {
195
+ return JSON.stringify(_canonicalize(value), null, 2);
196
+ }
197
+
198
+ // ── Internal helpers ────────────────────────────────────────────────────────
199
+
200
+ function _isValidKindModule(mod, filename) {
201
+ if (!mod || typeof mod !== 'object') return false;
202
+ if (typeof mod.name !== 'string' || !mod.name.length) return false;
203
+ if (typeof mod.collect !== 'function') return false;
204
+ if (mod.requiresSources != null && !Array.isArray(mod.requiresSources)) return false;
205
+ // filename stem must match name
206
+ const stem = filename.replace(/\.cjs$/, '');
207
+ if (stem !== mod.name) return false;
208
+ return true;
209
+ }
210
+
211
+ function _gitCurrentBranch(projectDir) {
212
+ try {
213
+ const { execSync } = require('child_process');
214
+ const stdout = execSync('git branch --show-current', {
215
+ cwd: projectDir,
216
+ encoding: 'utf8',
217
+ stdio: ['ignore', 'pipe', 'ignore'],
218
+ });
219
+ return String(stdout || '').trim();
220
+ } catch (_) {
221
+ return '';
222
+ }
223
+ }
224
+
225
+ function _normalizeScope(scope) {
226
+ const s = scope && typeof scope === 'object' ? scope : {};
227
+ return {
228
+ deliverables: _sortStringArray(s.deliverables),
229
+ notOwned: _sortStringArray(s.notOwned),
230
+ owned: _sortStringArray(s.owned),
231
+ };
232
+ }
233
+
234
+ function _normalizeConstraints(c) {
235
+ return _sortStringArray(c);
236
+ }
237
+
238
+ function _normalizeContracts(arr) {
239
+ if (!Array.isArray(arr)) return [];
240
+ const out = [];
241
+ for (const item of arr) {
242
+ if (!item || typeof item !== 'object') continue;
243
+ out.push({
244
+ path: typeof item.path === 'string' ? item.path : '',
245
+ status: typeof item.status === 'string' ? item.status : 'UNKNOWN',
246
+ });
247
+ }
248
+ out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
249
+ return out;
250
+ }
251
+
252
+ function _normalizeAncillary(a) {
253
+ if (!a || typeof a !== 'object') return {};
254
+ // Recursively canonicalize ancillary objects so JSON is deterministic.
255
+ return _canonicalize(a);
256
+ }
257
+
258
+ function _sortStringArray(arr) {
259
+ if (!Array.isArray(arr)) return [];
260
+ const xs = arr.filter((v) => typeof v === 'string');
261
+ xs.sort();
262
+ return xs;
263
+ }
264
+
265
+ function _sortObjectKeys(obj) {
266
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
267
+ const out = {};
268
+ for (const k of Object.keys(obj).sort()) out[k] = obj[k];
269
+ return out;
270
+ }
271
+
272
+ function _canonicalize(v) {
273
+ if (v == null) return v;
274
+ if (Array.isArray(v)) return v.map(_canonicalize);
275
+ if (typeof v === 'object') {
276
+ const out = {};
277
+ for (const k of Object.keys(v).sort()) out[k] = _canonicalize(v[k]);
278
+ return out;
279
+ }
280
+ return v;
281
+ }
282
+
283
+ // ── CLI ─────────────────────────────────────────────────────────────────────
284
+
285
+ function _parseArgv(argv) {
286
+ const out = { projectDir: '.', mode: 'json', strict: false };
287
+ for (let i = 0; i < argv.length; i++) {
288
+ const a = argv[i];
289
+ if (a === '--kind') out.kind = argv[++i];
290
+ else if (a === '--domain') out.domain = argv[++i];
291
+ else if (a === '--spawn-id') out.spawnId = argv[++i];
292
+ else if (a === '--out') out.out = argv[++i];
293
+ else if (a === '--project') out.projectDir = argv[++i] || '.';
294
+ else if (a === '--json') out.mode = 'json';
295
+ else if (a === '--strict') out.strict = true;
296
+ else if (a === '--help' || a === '-h') out.help = true;
297
+ }
298
+ return out;
299
+ }
300
+
301
+ function _printHelp(stream) {
302
+ const lines = [
303
+ 'Usage: node bin/gsd-t-context-brief.cjs --kind X --spawn-id Y [options]',
304
+ '',
305
+ 'Options:',
306
+ ' --kind X one of: execute|verify|qa|red-team|design-verify|scan',
307
+ ' --domain Y domain id, [a-zA-Z0-9_-]+ (required for execute/verify/qa/red-team)',
308
+ ' --spawn-id Z spawn id, [a-zA-Z0-9_-]+ (required)',
309
+ ' --out PATH write brief to file (default: stdout)',
310
+ ' --json JSON output to stdout (default)',
311
+ ' --strict fail-closed on any missing source',
312
+ ' --project DIR project root (default: .)',
313
+ ' --help, -h show this help',
314
+ '',
315
+ 'Exit codes:',
316
+ ' 0 brief generated',
317
+ ' 2 CLI usage error / path-safety reject',
318
+ ' 4 required source missing OR --strict missing source OR over-cap',
319
+ ];
320
+ (stream || process.stdout).write(lines.join('\n') + '\n');
321
+ }
322
+
323
+ function _runCli(argv) {
324
+ const args = _parseArgv(argv);
325
+ if (args.help) { _printHelp(); return 0; }
326
+
327
+ if (typeof args.kind !== 'string' || !args.kind.length) {
328
+ process.stderr.write('error: --kind is required\n');
329
+ return 2;
330
+ }
331
+ if (typeof args.spawnId !== 'string' || !args.spawnId.length) {
332
+ process.stderr.write('error: --spawn-id is required\n');
333
+ return 2;
334
+ }
335
+ if (!SAFE_NAME_RE.test(args.spawnId)) {
336
+ process.stderr.write('error: --spawn-id must match [a-zA-Z0-9_-]+ (got: ' + JSON.stringify(args.spawnId) + ')\n');
337
+ return 2;
338
+ }
339
+ if (args.domain != null && !SAFE_NAME_RE.test(args.domain)) {
340
+ process.stderr.write('error: --domain must match [a-zA-Z0-9_-]+ (got: ' + JSON.stringify(args.domain) + ')\n');
341
+ return 2;
342
+ }
343
+
344
+ let brief;
345
+ try {
346
+ brief = generateBrief({
347
+ projectDir: args.projectDir,
348
+ kind: args.kind,
349
+ domain: args.domain || null,
350
+ spawnId: args.spawnId,
351
+ strict: args.strict,
352
+ });
353
+ } catch (err) {
354
+ process.stderr.write('error: ' + (err && err.message || String(err)) + '\n');
355
+ if (err && (err.code === 'EREQUIRED_MISSING' || err.code === 'EBRIEF_TOO_LARGE')) return 4;
356
+ // Unknown-kind / unsafe-input / other usage errors → exit 2.
357
+ if (err && /unsafe|required|unknown|spawnId|domain/i.test(err.message)) return 2;
358
+ return 4;
359
+ }
360
+
361
+ const json = stableStringify(brief);
362
+ if (args.out) {
363
+ try {
364
+ fs.mkdirSync(path.dirname(args.out), { recursive: true });
365
+ fs.writeFileSync(args.out, json + '\n');
366
+ } catch (err) {
367
+ process.stderr.write('error: failed to write --out: ' + (err && err.message || err) + '\n');
368
+ return 4;
369
+ }
370
+ } else {
371
+ process.stdout.write(json + '\n');
372
+ }
373
+ return 0;
374
+ }
375
+
376
+ if (require.main === module) {
377
+ const code = _runCli(process.argv.slice(2));
378
+ process.exit(code);
379
+ }
380
+
381
+ module.exports = {
382
+ generateBrief,
383
+ loadKindRegistry,
384
+ stableStringify,
385
+ SCHEMA_VERSION,
386
+ MAX_BRIEF_BYTES,
387
+ KINDS,
388
+ // Test-only exports
389
+ _parseArgv,
390
+ _isValidKindModule,
391
+ _canonicalize,
392
+ _gitCurrentBranch,
393
+ SAFE_NAME_RE,
394
+ FAIL_CLOSED_KINDS,
395
+ };