@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.
- package/.claude-plugin/marketplace.json +9 -12
- package/.claude-plugin/plugin.json +8 -31
- package/CHANGELOG.md +200 -0
- package/README.md +48 -7
- package/bin/gdd-sdk +55 -0
- package/hooks/_hook-emit.js +81 -0
- package/hooks/gdd-bash-guard.js +8 -0
- package/hooks/gdd-decision-injector.js +2 -0
- package/hooks/gdd-protected-paths.js +8 -0
- package/hooks/gdd-trajectory-capture.js +64 -0
- package/hooks/hooks.json +9 -0
- package/package.json +19 -47
- package/reference/codex-tools.md +53 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +14 -0
- package/scripts/cli/gdd-events.mjs +283 -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/connection-probe/index.cjs +263 -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-chain.cjs +177 -0
- package/scripts/lib/event-stream/index.ts +31 -1
- package/scripts/lib/event-stream/reader.ts +139 -0
- package/scripts/lib/event-stream/types.ts +155 -1
- package/scripts/lib/event-stream/writer.ts +65 -8
- 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/redact.cjs +122 -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
- package/scripts/lib/trajectory/index.cjs +126 -0
- 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
|
+
}
|