@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,224 @@
1
+ // scripts/lib/init-runner/scaffold.ts — filesystem helpers for the
2
+ // `gdd-sdk init` runner (Plan 21-08, SDK-20).
3
+ //
4
+ // This module is synchronous + side-effectful; every helper either
5
+ // mutates disk in a well-defined way or reports a boolean/null/string
6
+ // result. No session-runner / SDK dependencies — keep it cheap to test.
7
+ //
8
+ // Helpers exported:
9
+ //
10
+ // * writeStateFromTemplate — copy reference/STATE-TEMPLATE.md →
11
+ // .design/STATE.md with `{TODAY}` replaced.
12
+ // * backupExistingDesignDir — rename `.design/` to `.design.backup.<ISO>/`.
13
+ // * resolveStateTemplatePath — walk up from process.argv[1]/cwd to find
14
+ // the plugin package root, then join
15
+ // reference/STATE-TEMPLATE.md.
16
+ // * ensureDesignDirs — `mkdir -p` both `.design/` and
17
+ // `.design/research/`.
18
+ //
19
+ // All helpers are pure w.r.t. filesystem ordering — every caller can
20
+ // invoke them in any order without corrupting a concurrent init.
21
+
22
+ import {
23
+ copyFileSync,
24
+ cpSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ renameSync,
29
+ rmSync,
30
+ statSync,
31
+ writeFileSync,
32
+ } from 'node:fs';
33
+ import { dirname, join, resolve } from 'node:path';
34
+
35
+ /** The plugin's package.json `name` field used to anchor the walk-up in
36
+ * `resolveStateTemplatePath`. */
37
+ const PLUGIN_PACKAGE_NAME = '@hegemonart/get-design-done';
38
+
39
+ /** Maximum directories to climb looking for the plugin root. Eight
40
+ * matches session-runner's repo-root discovery depth — a forgiving
41
+ * upper bound without being pathological. */
42
+ const MAX_WALKUP_DEPTH = 8;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // writeStateFromTemplate
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Copy `templatePath` to `destPath`, replacing every `{TODAY}` token
50
+ * with today's ISO date (`YYYY-MM-DD`). Other placeholders (e.g.,
51
+ * `{PROJECT_NAME}`) are left verbatim for the user to fill in later.
52
+ *
53
+ * Returns `true` on success, `false` when the template is missing.
54
+ * Never throws for expected failure modes; unexpected errors (permission
55
+ * denied, out of disk) surface as thrown errors since the caller cannot
56
+ * meaningfully recover.
57
+ */
58
+ export function writeStateFromTemplate(args: {
59
+ readonly cwd: string;
60
+ readonly templatePath: string;
61
+ readonly destPath: string;
62
+ }): boolean {
63
+ const { templatePath, destPath } = args;
64
+
65
+ if (!existsSync(templatePath)) return false;
66
+
67
+ // Read template → substitute `{TODAY}` → write to dest. If the template
68
+ // has no `{TODAY}` token we write it verbatim (plan spec: "Template
69
+ // without placeholder → copied verbatim").
70
+ const raw = readFileSync(templatePath, 'utf8');
71
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
72
+ const body = raw.includes('{TODAY}')
73
+ ? raw.split('{TODAY}').join(today)
74
+ : raw;
75
+
76
+ // Ensure destination directory exists before writing.
77
+ mkdirSync(dirname(destPath), { recursive: true });
78
+ writeFileSync(destPath, body, 'utf8');
79
+ return true;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // backupExistingDesignDir
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * If `.design/` exists inside `cwd`, move it aside to
88
+ * `.design.backup.<ISO>/` (ISO safe-for-filename form) and return the
89
+ * backup directory path. Returns `null` when nothing exists to back up.
90
+ *
91
+ * Rename is the default (atomic on same filesystem); on EXDEV or any
92
+ * other rename error we fall back to recursive copy + rm.
93
+ */
94
+ export function backupExistingDesignDir(cwd: string): string | null {
95
+ const designDir = resolve(cwd, '.design');
96
+ if (!existsSync(designDir)) return null;
97
+
98
+ // ISO → filesystem-safe: replace ':' and '.' (e.g. 2026-04-24T10:15:30.123Z
99
+ // → 2026-04-24T10-15-30-123Z) so Windows accepts the directory name.
100
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
101
+ let backupDir = resolve(cwd, `.design.backup.${stamp}`);
102
+
103
+ // Collision guard — sub-millisecond double-invoke could land on the
104
+ // same ISO stamp. Append a numeric suffix until the path is free.
105
+ let suffix = 0;
106
+ while (existsSync(backupDir)) {
107
+ suffix += 1;
108
+ backupDir = resolve(cwd, `.design.backup.${stamp}-${suffix}`);
109
+ if (suffix > 1000) {
110
+ // Pathological — bail rather than loop forever.
111
+ throw new Error(`backupExistingDesignDir: could not find a free backup directory after ${suffix} attempts`);
112
+ }
113
+ }
114
+
115
+ try {
116
+ renameSync(designDir, backupDir);
117
+ } catch (err) {
118
+ // EXDEV (cross-volume), EPERM (Windows permissions), or anything
119
+ // else — try recursive copy + rm as a slower fallback.
120
+ const code = (err as NodeJS.ErrnoException | null)?.code;
121
+ if (code === 'EXDEV' || code === 'EPERM' || code === 'ENOTEMPTY') {
122
+ cpSync(designDir, backupDir, { recursive: true });
123
+ rmSync(designDir, { recursive: true, force: true });
124
+ } else {
125
+ throw err;
126
+ }
127
+ }
128
+
129
+ return backupDir;
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // ensureDesignDirs
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * Create `.design/` and `.design/research/` inside `cwd` (`mkdir -p`).
138
+ * Idempotent — safe to call on an already-initialized project. Returns
139
+ * the resolved absolute paths so callers can drop them directly into
140
+ * `writeStateFromTemplate({destPath: ...})`.
141
+ */
142
+ export function ensureDesignDirs(cwd: string): {
143
+ readonly design_dir: string;
144
+ readonly research_dir: string;
145
+ } {
146
+ const designDir = resolve(cwd, '.design');
147
+ const researchDir = join(designDir, 'research');
148
+ mkdirSync(designDir, { recursive: true });
149
+ mkdirSync(researchDir, { recursive: true });
150
+ return Object.freeze({ design_dir: designDir, research_dir: researchDir });
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // resolveStateTemplatePath
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * Walk up from `process.argv[1]`'s directory (falling back to cwd if
159
+ * argv[1] isn't a real path) looking for a `package.json` whose `name`
160
+ * field matches `@hegemonart/get-design-done`. When found, return
161
+ * `<pkg-root>/reference/STATE-TEMPLATE.md`. Return `null` if we run out
162
+ * of parent directories without finding the plugin root — e.g., when
163
+ * invoked from a fork that has renamed the package.
164
+ *
165
+ * The walk is bounded to `MAX_WALKUP_DEPTH` (8) to stop us from
166
+ * traversing the entire filesystem on pathological inputs.
167
+ */
168
+ export function resolveStateTemplatePath(): string | null {
169
+ const startCandidates: string[] = [];
170
+ // argv[1] is the executing script's path (e.g., bin wrapper).
171
+ const argv1 = process.argv[1];
172
+ if (argv1 !== undefined && argv1.length > 0 && existsSync(argv1)) {
173
+ startCandidates.push(dirname(resolve(argv1)));
174
+ }
175
+ // Fall back to cwd for cases where argv[1] isn't meaningful (tests,
176
+ // repl, etc.).
177
+ startCandidates.push(process.cwd());
178
+
179
+ for (const start of startCandidates) {
180
+ let dir = start;
181
+ for (let depth = 0; depth < MAX_WALKUP_DEPTH; depth += 1) {
182
+ const pkgPath = join(dir, 'package.json');
183
+ if (existsSync(pkgPath)) {
184
+ try {
185
+ const raw = readFileSync(pkgPath, 'utf8');
186
+ const parsed = JSON.parse(raw) as { name?: unknown };
187
+ if (parsed.name === PLUGIN_PACKAGE_NAME) {
188
+ const tpl = join(dir, 'reference', 'STATE-TEMPLATE.md');
189
+ if (existsSync(tpl)) return tpl;
190
+ }
191
+ } catch {
192
+ // Malformed package.json — keep walking; maybe a parent has it.
193
+ }
194
+ }
195
+ const parent = dirname(dir);
196
+ if (parent === dir) break;
197
+ dir = parent;
198
+ }
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Internal helpers (kept exported for tests that want to drill in)
206
+ // ---------------------------------------------------------------------------
207
+
208
+ /** Expose copy-then-write as a unit for tests that want to bypass the
209
+ * template-path existence check. Public but undocumented in the index. */
210
+ export function _copyTemplateVerbatim(src: string, dest: string): void {
211
+ mkdirSync(dirname(dest), { recursive: true });
212
+ copyFileSync(src, dest);
213
+ }
214
+
215
+ /** Size of a file on disk, or 0 if missing. Used by the researcher
216
+ * dispatcher to measure `output_bytes` without bubbling up EEXIST /
217
+ * ENOENT. */
218
+ export function fileSize(p: string): number {
219
+ try {
220
+ return statSync(p).size;
221
+ } catch {
222
+ return 0;
223
+ }
224
+ }
@@ -0,0 +1,224 @@
1
+ // scripts/lib/init-runner/synthesizer.ts — composes
2
+ // `.design/DESIGN-CONTEXT.md` from researcher outputs (Plan 21-08, SDK-20).
3
+ //
4
+ // The synthesizer is a single headless session spawned through
5
+ // `session-runner.run()` with a prompt that embeds every researcher's
6
+ // content. Its success contract is simple: did the agent write
7
+ // `.design/DESIGN-CONTEXT.md` to disk? We don't parse its text output —
8
+ // the file's presence is the sole pass/fail signal.
9
+ //
10
+ // Exports:
11
+ // * DEFAULT_SYNTHESIZER_PROMPT — the embedded prompt (frozen string).
12
+ // * buildSynthesizerPrompt — splice researcher bodies into the prompt.
13
+ // * spawnSynthesizer — session dispatch + file-presence check.
14
+
15
+ import { existsSync } from 'node:fs';
16
+ import { resolve } from 'node:path';
17
+
18
+ import { run as runSession } from '../session-runner/index.ts';
19
+ import type {
20
+ BudgetCap,
21
+ QueryOverride,
22
+ } from '../session-runner/types.ts';
23
+ import { enforceScope } from '../tool-scoping/index.ts';
24
+ import type { ResearcherName } from './types.ts';
25
+
26
+ /**
27
+ * Default synthesizer prompt with a `{{RESEARCH_BLOCKS}}` placeholder
28
+ * that `buildSynthesizerPrompt` substitutes with the researcher
29
+ * content. The template encodes the DESIGN-CONTEXT.md schema the
30
+ * downstream discuss stage expects (decisions, must-haves, connections,
31
+ * ARM, flow diagram, open questions).
32
+ *
33
+ * The prompt is explicit about "write file; emit no prose" because the
34
+ * synthesizer's success is measured by file-on-disk — we don't need the
35
+ * agent's text stream in the transcript.
36
+ */
37
+ export const DEFAULT_SYNTHESIZER_PROMPT: string = Object.freeze(
38
+ `You are the init-synthesizer. Four researchers have produced these outputs:
39
+
40
+ {{RESEARCH_BLOCKS}}
41
+
42
+ Compose .design/DESIGN-CONTEXT.md following this schema:
43
+
44
+ ---
45
+ cycle: init
46
+ generated_at: <ISO>
47
+ ---
48
+
49
+ # Design Context (Draft)
50
+
51
+ ## Decisions
52
+ (Draft as D-01, D-02, … from researcher outputs. Each decision gets id,
53
+ rationale, source researcher.)
54
+
55
+ ## Must-Haves
56
+ (Draft as M-01, M-02, … from researcher blockers. Each must-have gets id,
57
+ stakeholder, test.)
58
+
59
+ ## Connections
60
+ (Which AI-design-tool connections the project's tech stack recommends.)
61
+
62
+ ## Architectural Responsibility Map
63
+ (ARM tiers inferred from the codebase.)
64
+
65
+ ## Flow Diagram
66
+ \`\`\`mermaid
67
+ flowchart TD
68
+ ...
69
+ \`\`\`
70
+
71
+ ## Open Questions
72
+ (Anything that requires the user to answer before brief can start.)
73
+
74
+ Write the final content to .design/DESIGN-CONTEXT.md via Write tool.
75
+ Do NOT emit any prose response — only write the file.
76
+ `,
77
+ ) as string;
78
+
79
+ export interface SynthesizerInput {
80
+ readonly name: ResearcherName;
81
+ readonly path: string;
82
+ readonly content: string;
83
+ }
84
+
85
+ /**
86
+ * Build the synthesizer prompt by substituting `{{RESEARCH_BLOCKS}}`
87
+ * with concatenated research bodies. Each block is wrapped in an HTML
88
+ * comment header so the agent can parse the boundaries deterministically.
89
+ *
90
+ * Exported for tests + for callers that want to assert prompt content
91
+ * without running a session.
92
+ */
93
+ export function buildSynthesizerPrompt(
94
+ inputs: readonly SynthesizerInput[],
95
+ override?: string,
96
+ ): string {
97
+ const template = override ?? DEFAULT_SYNTHESIZER_PROMPT;
98
+ const blocks = inputs
99
+ .map((inp) => `<!-- ${inp.name} -->\n${inp.content}`)
100
+ .join('\n\n');
101
+ // If the caller's override doesn't include the placeholder, append
102
+ // the blocks. This keeps custom prompts functional without forcing
103
+ // them to adopt our template marker.
104
+ if (!template.includes('{{RESEARCH_BLOCKS}}')) {
105
+ return `${template}\n\n${blocks}`;
106
+ }
107
+ return template.split('{{RESEARCH_BLOCKS}}').join(blocks);
108
+ }
109
+
110
+ export interface SpawnSynthesizerArgs {
111
+ readonly researcherOutputs: readonly SynthesizerInput[];
112
+ readonly cwd: string;
113
+ readonly budget: BudgetCap;
114
+ readonly maxTurns: number;
115
+ readonly runOverride?: QueryOverride;
116
+ readonly promptOverride?: string;
117
+ }
118
+
119
+ export interface SpawnSynthesizerResult {
120
+ readonly status: 'completed' | 'error';
121
+ /** Absolute path to where .design/DESIGN-CONTEXT.md should live. */
122
+ readonly design_context_path: string;
123
+ readonly usage: {
124
+ readonly input_tokens: number;
125
+ readonly output_tokens: number;
126
+ readonly usd_cost: number;
127
+ };
128
+ readonly error?: string;
129
+ }
130
+
131
+ /**
132
+ * Spawn one synthesizer session. Success is determined by the presence
133
+ * of `.design/DESIGN-CONTEXT.md` inside `cwd` after the session ends —
134
+ * we do NOT inspect the session's text output.
135
+ *
136
+ * Never throws; session-level errors and missing-file outcomes both
137
+ * land as `status: 'error'` with `error` populated.
138
+ */
139
+ export async function spawnSynthesizer(
140
+ args: SpawnSynthesizerArgs,
141
+ ): Promise<SpawnSynthesizerResult> {
142
+ const designContextPath = resolve(
143
+ args.cwd,
144
+ '.design',
145
+ 'DESIGN-CONTEXT.md',
146
+ );
147
+
148
+ // Resolve allowed tools. The synthesizer only needs Read (to load
149
+ // research outputs if the prompt drops the inline bodies) and Write
150
+ // (to produce DESIGN-CONTEXT.md). The init stage scope covers both.
151
+ let allowedTools: readonly string[];
152
+ try {
153
+ allowedTools = enforceScope({ stage: 'init' });
154
+ } catch (err) {
155
+ const message = err instanceof Error ? err.message : String(err);
156
+ return Object.freeze({
157
+ status: 'error' as const,
158
+ design_context_path: designContextPath,
159
+ usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
160
+ error: `scope enforcement failed: ${message}`,
161
+ });
162
+ }
163
+
164
+ const prompt = buildSynthesizerPrompt(
165
+ args.researcherOutputs,
166
+ args.promptOverride,
167
+ );
168
+
169
+ let usage = { input_tokens: 0, output_tokens: 0, usd_cost: 0 };
170
+ try {
171
+ const session = await runSession({
172
+ prompt,
173
+ stage: 'init',
174
+ budget: args.budget,
175
+ turnCap: { maxTurns: args.maxTurns },
176
+ allowedTools: [...allowedTools],
177
+ ...(args.runOverride !== undefined
178
+ ? { queryOverride: args.runOverride }
179
+ : {}),
180
+ });
181
+ usage = {
182
+ input_tokens: session.usage.input_tokens,
183
+ output_tokens: session.usage.output_tokens,
184
+ usd_cost: session.usage.usd_cost,
185
+ };
186
+
187
+ if (session.status !== 'completed') {
188
+ const code = session.error?.code ?? session.status.toUpperCase();
189
+ const msg = session.error?.message ?? `session ended: ${session.status}`;
190
+ return Object.freeze({
191
+ status: 'error' as const,
192
+ design_context_path: designContextPath,
193
+ usage,
194
+ error: `${code}: ${msg}`,
195
+ });
196
+ }
197
+ } catch (err) {
198
+ // session-runner is documented never-throws, but a test-injected
199
+ // runOverride could throw during setup. Package gracefully.
200
+ const message = err instanceof Error ? err.message : String(err);
201
+ return Object.freeze({
202
+ status: 'error' as const,
203
+ design_context_path: designContextPath,
204
+ usage,
205
+ error: `session threw: ${message}`,
206
+ });
207
+ }
208
+
209
+ // Success is file-on-disk, NOT session.status.
210
+ if (!existsSync(designContextPath)) {
211
+ return Object.freeze({
212
+ status: 'error' as const,
213
+ design_context_path: designContextPath,
214
+ usage,
215
+ error: 'synthesizer did not produce .design/DESIGN-CONTEXT.md',
216
+ });
217
+ }
218
+
219
+ return Object.freeze({
220
+ status: 'completed' as const,
221
+ design_context_path: designContextPath,
222
+ usage,
223
+ });
224
+ }
@@ -0,0 +1,143 @@
1
+ // scripts/lib/init-runner/types.ts — public type surface for the
2
+ // `gdd-sdk init` runner (Plan 21-08, SDK-20).
3
+ //
4
+ // The init runner bootstraps a new project's `.design/` directory by
5
+ // spawning a fixed roster of 4 researchers in parallel through the
6
+ // session-runner (Plan 21-01) and then running a synthesizer pass that
7
+ // composes `.design/DESIGN-CONTEXT.md` from the researcher outputs.
8
+ //
9
+ // These types are consumed by:
10
+ // * `researchers.ts` — dispatch + outcome shape.
11
+ // * `synthesizer.ts` — synthesizer I/O contract.
12
+ // * `scaffold.ts` — STATE.md + backup helpers.
13
+ // * `index.ts` — top-level `run()` orchestrator.
14
+ // * CLI wiring (Plan 21-09) — consumes `InitRunnerResult`.
15
+ //
16
+ // No file outside `scripts/lib/init-runner/` should construct any of
17
+ // these shapes directly; `index.ts` re-exports each one.
18
+
19
+ import type { BudgetCap, QueryOverride } from '../session-runner/types.ts';
20
+
21
+ /**
22
+ * Locked roster of researcher names. The init cycle always spawns
23
+ * exactly these four; adding a fifth is a breaking change because it
24
+ * widens every `ResearcherOutcome[]` consumer's input.
25
+ */
26
+ export type ResearcherName =
27
+ | 'design-system-audit'
28
+ | 'brand-context'
29
+ | 'accessibility-baseline'
30
+ | 'competitive-references';
31
+
32
+ /**
33
+ * One researcher's spec. The `agentPath` is optional — if the file is
34
+ * absent on disk, the runner falls back to the `init` stage scope from
35
+ * `tool-scoping` (Plan 21-03), which grants the broad research-capable
36
+ * toolset (Read, Write, Grep, Glob, Bash, Task, WebSearch, WebFetch).
37
+ */
38
+ export interface ResearcherSpec {
39
+ readonly name: ResearcherName;
40
+ /** Optional agent frontmatter path; missing → init stage scope. */
41
+ readonly agentPath?: string;
42
+ readonly prompt: string;
43
+ /** Where the researcher's markdown output lands (relative to cwd). */
44
+ readonly outputPath: string;
45
+ }
46
+
47
+ /**
48
+ * Per-researcher outcome. Never-throws contract: if the session errored
49
+ * internally, `status` is `'error'` and the `error` field is populated —
50
+ * the runner never re-raises.
51
+ *
52
+ * `output_exists` + `output_bytes` are measured on disk AFTER the
53
+ * session returns; a successful session that failed to write its file
54
+ * lands with `status: 'completed'` and `output_exists: false` so the
55
+ * synthesizer can drop the slot cleanly.
56
+ */
57
+ export interface ResearcherOutcome {
58
+ readonly name: ResearcherName;
59
+ readonly status: 'completed' | 'error';
60
+ readonly output_exists: boolean;
61
+ readonly output_bytes: number;
62
+ readonly usage: {
63
+ readonly input_tokens: number;
64
+ readonly output_tokens: number;
65
+ readonly usd_cost: number;
66
+ };
67
+ readonly duration_ms: number;
68
+ readonly error?: { readonly code: string; readonly message: string };
69
+ }
70
+
71
+ /**
72
+ * Terminal status for a full `run()` invocation.
73
+ *
74
+ * * `completed` — every step ran; scaffold + synth produced.
75
+ * * `already-initialized` — `.design/STATE.md` exists and `force` is off.
76
+ * * `no-researchers-succeeded` — all 4 researchers errored; synth skipped.
77
+ * * `error` — precondition failure (e.g., STATE-TEMPLATE.md missing).
78
+ */
79
+ export type InitStatus =
80
+ | 'completed'
81
+ | 'already-initialized'
82
+ | 'no-researchers-succeeded'
83
+ | 'error';
84
+
85
+ /**
86
+ * Options for the top-level `run()`. Every researcher and the synthesizer
87
+ * get their own budget envelope so a runaway researcher cannot starve
88
+ * the synthesizer's token budget.
89
+ *
90
+ * `runOverride` + `synthesizerPromptOverride` are test injection points;
91
+ * production callers leave them unset.
92
+ */
93
+ export interface InitRunnerOptions {
94
+ /** Researchers to run. Default: `DEFAULT_RESEARCHERS` (the 4-locked roster). */
95
+ readonly researchers?: readonly ResearcherSpec[];
96
+ /** Per-researcher budget envelope. Each researcher gets a fresh copy. */
97
+ readonly budget: BudgetCap;
98
+ /** Max assistant turns per researcher session. */
99
+ readonly maxTurnsPerResearcher: number;
100
+ /** Synthesizer budget envelope. */
101
+ readonly synthesizerBudget: BudgetCap;
102
+ /** Max assistant turns for the synthesizer session. */
103
+ readonly synthesizerMaxTurns: number;
104
+ /** Parallelism cap for researcher dispatch. Default: 4. */
105
+ readonly concurrency?: number;
106
+ /** Working directory; `.design/` resolved relative to this. Default: `process.cwd()`. */
107
+ readonly cwd?: string;
108
+ /** Force re-init: backup existing `.design/` to `.design.backup.<ISO>/`. */
109
+ readonly force?: boolean;
110
+ /** Path to `reference/STATE-TEMPLATE.md`; defaults to plugin-package-local. */
111
+ readonly stateTemplatePath?: string;
112
+ /** Test-injectable session-runner `run()` replacement. */
113
+ readonly runOverride?: QueryOverride;
114
+ /** Override the synthesizer's embedded prompt. */
115
+ readonly synthesizerPromptOverride?: string;
116
+ }
117
+
118
+ /**
119
+ * Terminal result from `run()`. The union discriminant is `status`;
120
+ * `researchers` + `scaffold` + `total_usage` are populated on all
121
+ * branches (empty arrays / false flags / zeroed usage) so callers can
122
+ * present a uniform summary.
123
+ */
124
+ export interface InitRunnerResult {
125
+ readonly status: InitStatus;
126
+ /** Resolved working directory the run targeted. */
127
+ readonly cwd: string;
128
+ /** Resolved `.design/` directory path (absolute). */
129
+ readonly design_dir: string;
130
+ readonly researchers: readonly ResearcherOutcome[];
131
+ readonly scaffold: {
132
+ readonly state_md_written: boolean;
133
+ readonly design_context_md_written: boolean;
134
+ /** When `opts.force` triggered a backup, the backup dir path. */
135
+ readonly backup_dir?: string;
136
+ };
137
+ /** Aggregated usage across all researchers + the synthesizer. */
138
+ readonly total_usage: {
139
+ readonly input_tokens: number;
140
+ readonly output_tokens: number;
141
+ readonly usd_cost: number;
142
+ };
143
+ }