@hegemonart/get-design-done 1.20.0 → 1.21.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.
- package/.claude-plugin/marketplace.json +9 -12
- package/.claude-plugin/plugin.json +8 -31
- package/CHANGELOG.md +78 -0
- package/README.md +48 -7
- package/bin/gdd-sdk +55 -0
- package/package.json +15 -47
- package/reference/codex-tools.md +53 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +14 -0
- package/scripts/e2e/run-headless.ts +514 -0
- package/scripts/lib/cli/commands/audit.ts +382 -0
- package/scripts/lib/cli/commands/init.ts +217 -0
- package/scripts/lib/cli/commands/query.ts +329 -0
- package/scripts/lib/cli/commands/run.ts +656 -0
- package/scripts/lib/cli/commands/stage.ts +468 -0
- package/scripts/lib/cli/index.ts +167 -0
- package/scripts/lib/cli/parse-args.ts +336 -0
- package/scripts/lib/context-engine/index.ts +116 -0
- package/scripts/lib/context-engine/manifest.ts +69 -0
- package/scripts/lib/context-engine/truncate.ts +282 -0
- package/scripts/lib/context-engine/types.ts +59 -0
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
- package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
- package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
- package/scripts/lib/discuss-parallel-runner/types.ts +184 -0
- package/scripts/lib/event-stream/index.ts +11 -1
- package/scripts/lib/explore-parallel-runner/index.ts +294 -0
- package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
- package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
- package/scripts/lib/explore-parallel-runner/types.ts +139 -0
- package/scripts/lib/harness/detect.ts +90 -0
- package/scripts/lib/harness/index.ts +64 -0
- package/scripts/lib/harness/tool-map.ts +142 -0
- package/scripts/lib/init-runner/index.ts +396 -0
- package/scripts/lib/init-runner/researchers.ts +245 -0
- package/scripts/lib/init-runner/scaffold.ts +224 -0
- package/scripts/lib/init-runner/synthesizer.ts +224 -0
- package/scripts/lib/init-runner/types.ts +143 -0
- package/scripts/lib/logger/index.ts +251 -0
- package/scripts/lib/logger/sinks.ts +269 -0
- package/scripts/lib/logger/types.ts +110 -0
- package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
- package/scripts/lib/pipeline-runner/index.ts +527 -0
- package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
- package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
- package/scripts/lib/pipeline-runner/types.ts +183 -0
- package/scripts/lib/session-runner/errors.ts +406 -0
- package/scripts/lib/session-runner/index.ts +715 -0
- package/scripts/lib/session-runner/transcript.ts +189 -0
- package/scripts/lib/session-runner/types.ts +144 -0
- package/scripts/lib/tool-scoping/index.ts +219 -0
- package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
- package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
- package/scripts/lib/tool-scoping/types.ts +77 -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
|
+
}
|