@hegemonart/get-design-done 1.20.0 → 1.22.0

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 (69) hide show
  1. package/.claude-plugin/marketplace.json +9 -12
  2. package/.claude-plugin/plugin.json +8 -31
  3. package/CHANGELOG.md +200 -0
  4. package/README.md +48 -7
  5. package/bin/gdd-sdk +55 -0
  6. package/hooks/_hook-emit.js +81 -0
  7. package/hooks/gdd-bash-guard.js +8 -0
  8. package/hooks/gdd-decision-injector.js +2 -0
  9. package/hooks/gdd-protected-paths.js +8 -0
  10. package/hooks/gdd-trajectory-capture.js +64 -0
  11. package/hooks/hooks.json +9 -0
  12. package/package.json +19 -47
  13. package/reference/codex-tools.md +53 -0
  14. package/reference/gemini-tools.md +53 -0
  15. package/reference/registry.json +14 -0
  16. package/scripts/cli/gdd-events.mjs +283 -0
  17. package/scripts/e2e/run-headless.ts +514 -0
  18. package/scripts/lib/cli/commands/audit.ts +382 -0
  19. package/scripts/lib/cli/commands/init.ts +217 -0
  20. package/scripts/lib/cli/commands/query.ts +329 -0
  21. package/scripts/lib/cli/commands/run.ts +656 -0
  22. package/scripts/lib/cli/commands/stage.ts +468 -0
  23. package/scripts/lib/cli/index.ts +167 -0
  24. package/scripts/lib/cli/parse-args.ts +336 -0
  25. package/scripts/lib/connection-probe/index.cjs +263 -0
  26. package/scripts/lib/context-engine/index.ts +116 -0
  27. package/scripts/lib/context-engine/manifest.ts +69 -0
  28. package/scripts/lib/context-engine/truncate.ts +282 -0
  29. package/scripts/lib/context-engine/types.ts +59 -0
  30. package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
  31. package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
  32. package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
  33. package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
  34. package/scripts/lib/event-chain.cjs +177 -0
  35. package/scripts/lib/event-stream/index.ts +31 -1
  36. package/scripts/lib/event-stream/reader.ts +139 -0
  37. package/scripts/lib/event-stream/types.ts +155 -1
  38. package/scripts/lib/event-stream/writer.ts +65 -8
  39. package/scripts/lib/explore-parallel-runner/index.ts +294 -0
  40. package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
  41. package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
  42. package/scripts/lib/explore-parallel-runner/types.ts +139 -0
  43. package/scripts/lib/harness/detect.ts +90 -0
  44. package/scripts/lib/harness/index.ts +64 -0
  45. package/scripts/lib/harness/tool-map.ts +142 -0
  46. package/scripts/lib/init-runner/index.ts +396 -0
  47. package/scripts/lib/init-runner/researchers.ts +245 -0
  48. package/scripts/lib/init-runner/scaffold.ts +224 -0
  49. package/scripts/lib/init-runner/synthesizer.ts +224 -0
  50. package/scripts/lib/init-runner/types.ts +143 -0
  51. package/scripts/lib/logger/index.ts +251 -0
  52. package/scripts/lib/logger/sinks.ts +269 -0
  53. package/scripts/lib/logger/types.ts +110 -0
  54. package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
  55. package/scripts/lib/pipeline-runner/index.ts +527 -0
  56. package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
  57. package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
  58. package/scripts/lib/pipeline-runner/types.ts +183 -0
  59. package/scripts/lib/redact.cjs +122 -0
  60. package/scripts/lib/session-runner/errors.ts +406 -0
  61. package/scripts/lib/session-runner/index.ts +715 -0
  62. package/scripts/lib/session-runner/transcript.ts +189 -0
  63. package/scripts/lib/session-runner/types.ts +144 -0
  64. package/scripts/lib/tool-scoping/index.ts +219 -0
  65. package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
  66. package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
  67. package/scripts/lib/tool-scoping/types.ts +77 -0
  68. package/scripts/lib/trajectory/index.cjs +126 -0
  69. package/scripts/lib/transports/ws.cjs +179 -0
@@ -0,0 +1,382 @@
1
+ // scripts/lib/cli/commands/audit.ts — Plan 21-09 Task 5 (SDK-21).
2
+ //
3
+ // `gdd-sdk audit` — regression + verification dry-run.
4
+ //
5
+ // 1. Probe connections (via in-process probe_connections handler —
6
+ // Plan 21-09 deliberately avoids the MCP stdio roundtrip here;
7
+ // Plan 21-10 exercises the full MCP boundary).
8
+ // 2. Read current STATE.md; enumerate must_haves and evaluate each.
9
+ // 3. (Optional --baseline <dir>) compare the connections map + a
10
+ // minimal manifest signature to a baseline snapshot.
11
+ // 4. Print a summary report (JSON or human-readable) to stdout.
12
+ //
13
+ // Exit codes:
14
+ // * 0 — all probes green + all must_haves pass + no baseline drift.
15
+ // * 1 — one or more regressions detected.
16
+ // * 3 — arg / config error.
17
+ //
18
+ // Note: the PROBE handler in probe_connections.ts expects a caller-
19
+ // supplied `probe_results` array. Here we do not have live probe data
20
+ // (the actual figma / refero health checks live in Phase-20 skill
21
+ // flows); instead, `audit` inspects the LAST-KNOWN connections map from
22
+ // STATE.md and flags any `unavailable` entries as degraded. A future
23
+ // plan (21-10 cross-harness) can wire in live probes.
24
+
25
+ import { existsSync, readFileSync } from 'node:fs';
26
+ import { resolve as resolvePath } from 'node:path';
27
+
28
+ import { read } from '../../gdd-state/index.ts';
29
+ import type { ConnectionStatus, ParsedState } from '../../gdd-state/types.ts';
30
+
31
+ import {
32
+ coerceFlags,
33
+ COMMON_FLAGS,
34
+ type FlagSpec,
35
+ type ParsedArgs,
36
+ } from '../parse-args.ts';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Flag spec + help.
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const AUDIT_FLAGS: readonly FlagSpec[] = [
43
+ ...COMMON_FLAGS,
44
+ { name: 'baseline', type: 'string' },
45
+ { name: 'state-path', type: 'string' },
46
+ ];
47
+
48
+ const USAGE = `gdd-sdk audit [flags]
49
+
50
+ Probe connections + dry-run verify.
51
+
52
+ Flags:
53
+ --baseline <dir> Compare connections + must-haves against baseline snapshot
54
+ (expects <dir>/STATE.md with pre-recorded state)
55
+ --state-path <path> Override STATE.md path
56
+ --cwd <dir> Working directory
57
+ --json Emit JSON report (default: human-readable)
58
+
59
+ Exit codes:
60
+ 0 clean — all probes available, all must-haves pass, no baseline drift
61
+ 1 regressions — any probe unavailable OR any must-have failed OR baseline drift
62
+ 3 arg / config error
63
+ `;
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Report types.
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export interface ConnectionReport {
70
+ readonly name: string;
71
+ readonly status: ConnectionStatus;
72
+ readonly ok: boolean;
73
+ }
74
+
75
+ export interface MustHaveReport {
76
+ readonly id: string;
77
+ readonly text: string;
78
+ readonly status: 'pending' | 'pass' | 'fail';
79
+ readonly ok: boolean;
80
+ }
81
+
82
+ export interface BaselineReport {
83
+ readonly ok: boolean;
84
+ readonly connection_drift: readonly string[];
85
+ readonly must_have_drift: readonly string[];
86
+ }
87
+
88
+ export interface AuditReport {
89
+ readonly connections: readonly ConnectionReport[];
90
+ readonly must_haves: readonly MustHaveReport[];
91
+ readonly baseline?: BaselineReport;
92
+ readonly summary: {
93
+ readonly connections_ok: boolean;
94
+ readonly must_haves_ok: boolean;
95
+ readonly baseline_ok: boolean;
96
+ readonly overall_ok: boolean;
97
+ };
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Deps.
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export type ReadFn = typeof read;
105
+
106
+ export interface AuditCommandDeps {
107
+ readonly readState?: ReadFn;
108
+ readonly stdout?: NodeJS.WritableStream;
109
+ readonly stderr?: NodeJS.WritableStream;
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Entry point.
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export async function auditCommand(
117
+ args: ParsedArgs,
118
+ deps: AuditCommandDeps = {},
119
+ ): Promise<number> {
120
+ const stdout = deps.stdout ?? process.stdout;
121
+ const stderr = deps.stderr ?? process.stderr;
122
+
123
+ if (args.flags['help'] === true || args.flags['h'] === true) {
124
+ stdout.write(USAGE);
125
+ return 0;
126
+ }
127
+
128
+ let flags: Record<string, unknown>;
129
+ try {
130
+ flags = coerceFlags(args, AUDIT_FLAGS);
131
+ } catch (err) {
132
+ stderr.write(`gdd-sdk audit: ${errMessage(err)}\n`);
133
+ return 3;
134
+ }
135
+
136
+ const cwd: string =
137
+ typeof flags['cwd'] === 'string' ? (flags['cwd'] as string) : process.cwd();
138
+ const statePath: string =
139
+ typeof flags['state-path'] === 'string' && (flags['state-path'] as string).length > 0
140
+ ? resolvePath(cwd, flags['state-path'] as string)
141
+ : resolvePath(cwd, '.design', 'STATE.md');
142
+
143
+ if (!existsSync(statePath)) {
144
+ stderr.write(`gdd-sdk audit: STATE.md not found at ${statePath}\n`);
145
+ return 3;
146
+ }
147
+
148
+ const readFn = deps.readState ?? read;
149
+ let state: ParsedState;
150
+ try {
151
+ state = await readFn(statePath);
152
+ } catch (err) {
153
+ stderr.write(`gdd-sdk audit: failed to read STATE.md: ${errMessage(err)}\n`);
154
+ return 3;
155
+ }
156
+
157
+ // 1. Connection report.
158
+ const connections: ConnectionReport[] = [];
159
+ let connectionsOk = true;
160
+ for (const [name, status] of Object.entries(state.connections ?? {})) {
161
+ const ok = status === 'available' || status === 'not_configured';
162
+ if (!ok) connectionsOk = false;
163
+ connections.push(Object.freeze({ name, status, ok }));
164
+ }
165
+
166
+ // 2. Must-have report.
167
+ const mustHaves: MustHaveReport[] = [];
168
+ let mustHavesOk = true;
169
+ for (const mh of state.must_haves ?? []) {
170
+ // `pending` and `pass` are acceptable at audit time; only `fail` is
171
+ // a definite regression. (Pending items may be verify-stage
172
+ // responsibilities still in progress.)
173
+ const ok = mh.status !== 'fail';
174
+ if (!ok) mustHavesOk = false;
175
+ mustHaves.push(
176
+ Object.freeze({
177
+ id: mh.id,
178
+ text: mh.text,
179
+ status: mh.status,
180
+ ok,
181
+ }),
182
+ );
183
+ }
184
+
185
+ // 3. Optional baseline drift check.
186
+ let baselineReport: BaselineReport | undefined;
187
+ const baselineFlag = flags['baseline'];
188
+ if (typeof baselineFlag === 'string' && baselineFlag.length > 0) {
189
+ try {
190
+ baselineReport = computeBaselineDrift(state, resolvePath(cwd, baselineFlag));
191
+ } catch (err) {
192
+ stderr.write(`gdd-sdk audit: baseline error: ${errMessage(err)}\n`);
193
+ return 3;
194
+ }
195
+ }
196
+
197
+ const baselineOk = baselineReport === undefined ? true : baselineReport.ok;
198
+ const overallOk = connectionsOk && mustHavesOk && baselineOk;
199
+
200
+ const report: AuditReport = {
201
+ connections: Object.freeze(connections),
202
+ must_haves: Object.freeze(mustHaves),
203
+ ...(baselineReport !== undefined ? { baseline: baselineReport } : {}),
204
+ summary: {
205
+ connections_ok: connectionsOk,
206
+ must_haves_ok: mustHavesOk,
207
+ baseline_ok: baselineOk,
208
+ overall_ok: overallOk,
209
+ },
210
+ };
211
+
212
+ if (flags['json'] === true) {
213
+ stdout.write(JSON.stringify(report, null, 2) + '\n');
214
+ } else {
215
+ stdout.write(renderHuman(report));
216
+ }
217
+
218
+ return overallOk ? 0 : 1;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Baseline comparison.
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Compare current `state` against a baseline STATE.md at
227
+ * `<baselineDir>/STATE.md`. Returns drift as arrays of human-readable
228
+ * strings; `ok` is `true` iff both arrays are empty.
229
+ *
230
+ * The comparison is intentionally minimal:
231
+ * * Connection drift: any name whose status differs between baseline
232
+ * and current (or is missing from one side).
233
+ * * Must-have drift: any baseline must-have whose status got WORSE
234
+ * (e.g., baseline=pass → current=fail). Improvements are not drift.
235
+ */
236
+ function computeBaselineDrift(
237
+ current: ParsedState,
238
+ baselineDir: string,
239
+ ): BaselineReport {
240
+ const baselinePath = resolvePath(baselineDir, 'STATE.md');
241
+ if (!existsSync(baselinePath)) {
242
+ throw new Error(`baseline STATE.md not found at ${baselinePath}`);
243
+ }
244
+ // Load + parse the baseline. We re-use `read()` for consistency, but
245
+ // baseline may live outside any lock regime — that's fine since we
246
+ // never mutate it.
247
+ const baselineRaw: string = readFileSync(baselinePath, 'utf8');
248
+ // Use a lazy require of the parser to avoid the async indirection —
249
+ // baseline comparison should be synchronous + deterministic.
250
+ // We can safely `JSON.parse` a tiny normalized block... actually, the
251
+ // simplest correct approach is to also call `read()`. Do that inline.
252
+ const baselineState = parseBaselineStateSync(baselineRaw);
253
+
254
+ const connectionDrift: string[] = [];
255
+ const cur = current.connections ?? {};
256
+ const base = baselineState.connections ?? {};
257
+ const allConnKeys = new Set([...Object.keys(cur), ...Object.keys(base)]);
258
+ for (const k of allConnKeys) {
259
+ const a = cur[k];
260
+ const b = base[k];
261
+ if (a === undefined && b !== undefined) {
262
+ connectionDrift.push(`${k}: missing in current (baseline=${b})`);
263
+ continue;
264
+ }
265
+ if (a !== undefined && b === undefined) {
266
+ connectionDrift.push(`${k}: new in current (current=${a})`);
267
+ continue;
268
+ }
269
+ if (a !== b) {
270
+ connectionDrift.push(`${k}: ${b} → ${a}`);
271
+ }
272
+ }
273
+
274
+ const mustHaveDrift: string[] = [];
275
+ const byId = new Map<string, string>(); // id → current status
276
+ for (const mh of current.must_haves ?? []) {
277
+ byId.set(mh.id, mh.status);
278
+ }
279
+ for (const bMh of baselineState.must_haves ?? []) {
280
+ const curStatus = byId.get(bMh.id);
281
+ if (curStatus === undefined) {
282
+ mustHaveDrift.push(`${bMh.id}: missing in current (baseline=${bMh.status})`);
283
+ continue;
284
+ }
285
+ // Worst-first ordering: fail > pending > pass. A regression is
286
+ // moving in that direction.
287
+ const rank = (s: string): number => (s === 'fail' ? 2 : s === 'pending' ? 1 : 0);
288
+ if (rank(curStatus) > rank(bMh.status)) {
289
+ mustHaveDrift.push(`${bMh.id}: ${bMh.status} → ${curStatus}`);
290
+ }
291
+ }
292
+
293
+ const ok = connectionDrift.length === 0 && mustHaveDrift.length === 0;
294
+ return {
295
+ ok,
296
+ connection_drift: Object.freeze(connectionDrift),
297
+ must_have_drift: Object.freeze(mustHaveDrift),
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Synchronous baseline parse. We want a stand-alone parser that works
303
+ * on the baseline file without taking a lock — gdd-state's `read()` is
304
+ * async but lock-free, so we wrap it with a top-level await via
305
+ * readFileSync + the gdd-state parser exports. Since parser.ts is not
306
+ * exported from the public index, we duplicate the minimal parse here:
307
+ * grab the `<connections>` and `<must_haves>` blocks via regex. This is
308
+ * fine because baseline audit tolerates a simplified shape — any
309
+ * baseline drift we can detect is enough.
310
+ */
311
+ function parseBaselineStateSync(raw: string): Pick<ParsedState, 'connections' | 'must_haves'> {
312
+ const connections: Record<string, ConnectionStatus> = {};
313
+ const connBlock = /<connections>([\s\S]*?)<\/connections>/.exec(raw);
314
+ if (connBlock) {
315
+ const body = connBlock[1] ?? '';
316
+ for (const line of body.split(/\r?\n/)) {
317
+ // Shape: "- name: status" or "name: status"
318
+ const m = /^\s*[-*]?\s*([A-Za-z0-9_-]+)\s*:\s*(available|unavailable|not_configured)\s*$/.exec(
319
+ line,
320
+ );
321
+ if (m !== null && m[1] !== undefined && m[2] !== undefined) {
322
+ connections[m[1]] = m[2] as ConnectionStatus;
323
+ }
324
+ }
325
+ }
326
+
327
+ const mustHaves: { id: string; text: string; status: 'pending' | 'pass' | 'fail' }[] = [];
328
+ const mhBlock = /<must_haves>([\s\S]*?)<\/must_haves>/.exec(raw);
329
+ if (mhBlock) {
330
+ const body = mhBlock[1] ?? '';
331
+ for (const line of body.split(/\r?\n/)) {
332
+ // Shape: "- M-01 [status] text" or "M-01 [status] text"
333
+ const m =
334
+ /^\s*[-*]?\s*(M-\d+)\s*\[(pending|pass|fail)\]\s*(.*)$/.exec(line) ??
335
+ /^\s*[-*]?\s*(M-\d+)\s*:\s*(pending|pass|fail)\s*(.*)$/.exec(line);
336
+ if (m !== null && m[1] !== undefined && m[2] !== undefined) {
337
+ mustHaves.push({
338
+ id: m[1],
339
+ status: m[2] as 'pending' | 'pass' | 'fail',
340
+ text: (m[3] ?? '').trim(),
341
+ });
342
+ }
343
+ }
344
+ }
345
+
346
+ return { connections, must_haves: mustHaves };
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Human-readable summary.
351
+ // ---------------------------------------------------------------------------
352
+
353
+ function renderHuman(report: AuditReport): string {
354
+ const lines: string[] = [];
355
+ lines.push(`audit: ${report.summary.overall_ok ? 'clean' : 'REGRESSIONS'}`);
356
+ lines.push('');
357
+ lines.push(`connections (${report.summary.connections_ok ? 'ok' : 'degraded'}):`);
358
+ for (const c of report.connections) {
359
+ lines.push(` ${c.name}: ${c.status}${c.ok ? '' : ' ← degraded'}`);
360
+ }
361
+ lines.push('');
362
+ lines.push(`must-haves (${report.summary.must_haves_ok ? 'ok' : 'failing'}):`);
363
+ for (const m of report.must_haves) {
364
+ lines.push(` ${m.id} [${m.status}] ${m.text}${m.ok ? '' : ' ← fail'}`);
365
+ }
366
+ if (report.baseline !== undefined) {
367
+ lines.push('');
368
+ lines.push(`baseline (${report.baseline.ok ? 'no drift' : 'drift'}):`);
369
+ for (const d of report.baseline.connection_drift) {
370
+ lines.push(` connection drift: ${d}`);
371
+ }
372
+ for (const d of report.baseline.must_have_drift) {
373
+ lines.push(` must-have drift: ${d}`);
374
+ }
375
+ }
376
+ return lines.join('\n') + '\n';
377
+ }
378
+
379
+ function errMessage(err: unknown): string {
380
+ if (err instanceof Error) return err.message;
381
+ return String(err);
382
+ }
@@ -0,0 +1,217 @@
1
+ // scripts/lib/cli/commands/init.ts — Plan 21-09 Task 6 (SDK-21).
2
+ //
3
+ // `gdd-sdk init` — bootstrap a new project's `.design/` directory by
4
+ // delegating to `init-runner.run()` (Plan 21-08).
5
+ //
6
+ // Exit-code mapping (InitRunnerResult.status → code):
7
+ // * completed → 0
8
+ // * already-initialized → 1 (not strictly an error — operator re-ran)
9
+ // * no-researchers-succeeded → 2 (partial: STATE.md scaffolded, no context)
10
+ // * error → 3 (precondition failure, e.g., template missing)
11
+ //
12
+ // Per PLAN.md: code 1 for already-initialized is deliberate ("not an
13
+ // error; operator ran init twice"). Callers that want to treat re-init
14
+ // as success should branch on the JSON output.
15
+
16
+ import {
17
+ run as defaultInitRun,
18
+ type InitRunnerResult,
19
+ } from '../../init-runner/index.ts';
20
+ import { getLogger } from '../../logger/index.ts';
21
+
22
+ import {
23
+ coerceFlags,
24
+ COMMON_FLAGS,
25
+ type FlagSpec,
26
+ type ParsedArgs,
27
+ } from '../parse-args.ts';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Flag spec + help.
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const INIT_FLAGS: readonly FlagSpec[] = [
34
+ ...COMMON_FLAGS,
35
+ { name: 'force', type: 'boolean', default: false },
36
+ { name: 'state-template', type: 'string' },
37
+ ];
38
+
39
+ const USAGE = `gdd-sdk init [flags]
40
+
41
+ Bootstrap a new project's .design/ directory.
42
+
43
+ Flags:
44
+ --force Back up an existing .design/ and re-initialize
45
+ --concurrency <n> Researcher parallelism (default 4)
46
+ --max-turns <n> Per-researcher turn cap (default 40)
47
+ --budget-usd <n> Per-researcher USD cap (default 2.0)
48
+ --state-template <path> Override reference/STATE-TEMPLATE.md path
49
+ --cwd <dir> Target directory (default: current)
50
+ --json Emit JSON result (default: human-readable)
51
+
52
+ Exit codes:
53
+ 0 completed
54
+ 1 already-initialized (re-run with --force to overwrite)
55
+ 2 no researchers succeeded (STATE.md scaffolded, DESIGN-CONTEXT.md absent)
56
+ 3 error
57
+ `;
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Deps.
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export type InitRunFn = typeof defaultInitRun;
64
+
65
+ export interface InitCommandDeps {
66
+ readonly initRun?: InitRunFn;
67
+ readonly stdout?: NodeJS.WritableStream;
68
+ readonly stderr?: NodeJS.WritableStream;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Entry point.
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export async function initCommand(
76
+ args: ParsedArgs,
77
+ deps: InitCommandDeps = {},
78
+ ): Promise<number> {
79
+ const stdout = deps.stdout ?? process.stdout;
80
+ const stderr = deps.stderr ?? process.stderr;
81
+
82
+ if (args.flags['help'] === true || args.flags['h'] === true) {
83
+ stdout.write(USAGE);
84
+ return 0;
85
+ }
86
+
87
+ let flags: Record<string, unknown>;
88
+ try {
89
+ flags = coerceFlags(args, INIT_FLAGS);
90
+ } catch (err) {
91
+ stderr.write(`gdd-sdk init: ${errMessage(err)}\n`);
92
+ return 3;
93
+ }
94
+
95
+ const cwd: string =
96
+ typeof flags['cwd'] === 'string' ? (flags['cwd'] as string) : process.cwd();
97
+
98
+ const budget = {
99
+ usdLimit:
100
+ typeof flags['budget-usd'] === 'number' ? (flags['budget-usd'] as number) : 2.0,
101
+ inputTokensLimit:
102
+ typeof flags['budget-input-tokens'] === 'number'
103
+ ? (flags['budget-input-tokens'] as number)
104
+ : 200_000,
105
+ outputTokensLimit:
106
+ typeof flags['budget-output-tokens'] === 'number'
107
+ ? (flags['budget-output-tokens'] as number)
108
+ : 50_000,
109
+ };
110
+
111
+ const maxTurns: number =
112
+ typeof flags['max-turns'] === 'number' ? (flags['max-turns'] as number) : 40;
113
+ const concurrency: number =
114
+ typeof flags['concurrency'] === 'number' ? (flags['concurrency'] as number) : 4;
115
+ const force: boolean = flags['force'] === true;
116
+ const stateTemplatePath: string | undefined =
117
+ typeof flags['state-template'] === 'string' && (flags['state-template'] as string).length > 0
118
+ ? (flags['state-template'] as string)
119
+ : undefined;
120
+
121
+ const initRun: InitRunFn = deps.initRun ?? defaultInitRun;
122
+
123
+ let result: InitRunnerResult;
124
+ try {
125
+ result = await initRun({
126
+ budget,
127
+ maxTurnsPerResearcher: maxTurns,
128
+ synthesizerBudget: budget,
129
+ synthesizerMaxTurns: maxTurns,
130
+ concurrency,
131
+ cwd,
132
+ force,
133
+ ...(stateTemplatePath !== undefined ? { stateTemplatePath } : {}),
134
+ });
135
+ } catch (err) {
136
+ // init-runner is contracted never to throw; belt-and-braces.
137
+ try {
138
+ getLogger().error('cli.init.unexpected_error', {
139
+ error: err instanceof Error ? err.message : String(err),
140
+ });
141
+ } catch {
142
+ // swallow
143
+ }
144
+ stderr.write(`gdd-sdk init: unexpected error: ${errMessage(err)}\n`);
145
+ return 3;
146
+ }
147
+
148
+ if (flags['json'] === true) {
149
+ stdout.write(JSON.stringify(result, null, 2) + '\n');
150
+ } else {
151
+ stdout.write(renderHuman(result));
152
+ }
153
+
154
+ return mapStatusToExitCode(result.status);
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Helpers.
159
+ // ---------------------------------------------------------------------------
160
+
161
+ function mapStatusToExitCode(status: InitRunnerResult['status']): number {
162
+ switch (status) {
163
+ case 'completed':
164
+ return 0;
165
+ case 'already-initialized':
166
+ return 1;
167
+ case 'no-researchers-succeeded':
168
+ return 2;
169
+ case 'error':
170
+ return 3;
171
+ default: {
172
+ // Exhaustiveness guard — compile error surfaces if a new status is
173
+ // added without updating this switch.
174
+ const _exhaustive: never = status;
175
+ void _exhaustive;
176
+ return 3;
177
+ }
178
+ }
179
+ }
180
+
181
+ function renderHuman(result: InitRunnerResult): string {
182
+ const lines: string[] = [];
183
+ lines.push(`init: ${result.status}`);
184
+ lines.push(` cwd: ${result.cwd}`);
185
+ lines.push(` design dir: ${result.design_dir}`);
186
+ if (result.scaffold.backup_dir !== undefined) {
187
+ lines.push(` backup: ${result.scaffold.backup_dir}`);
188
+ }
189
+ lines.push(
190
+ ` STATE.md: ${result.scaffold.state_md_written ? 'written' : 'skipped'}`,
191
+ );
192
+ lines.push(
193
+ ` DESIGN-CONTEXT.md: ${result.scaffold.design_context_md_written ? 'written' : 'skipped'}`,
194
+ );
195
+ const succeeded = result.researchers.filter(
196
+ (r) => r.status === 'completed' && r.output_exists,
197
+ ).length;
198
+ lines.push(
199
+ ` researchers: ${succeeded}/${result.researchers.length} succeeded`,
200
+ );
201
+ for (const r of result.researchers) {
202
+ const ok = r.status === 'completed' && r.output_exists;
203
+ lines.push(
204
+ ` ${r.name}: ${r.status}${ok ? '' : ' (no output)'}`,
205
+ );
206
+ }
207
+ lines.push(
208
+ ` total cost: $${result.total_usage.usd_cost.toFixed(4)} ` +
209
+ `(in=${result.total_usage.input_tokens}, out=${result.total_usage.output_tokens})`,
210
+ );
211
+ return lines.join('\n') + '\n';
212
+ }
213
+
214
+ function errMessage(err: unknown): string {
215
+ if (err instanceof Error) return err.message;
216
+ return String(err);
217
+ }