@hegemonart/get-design-done 1.19.6 → 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 +11 -14
- package/.claude-plugin/plugin.json +9 -32
- package/CHANGELOG.md +138 -0
- package/README.md +54 -1
- package/agents/design-reflector.md +13 -0
- package/bin/gdd-sdk +55 -0
- package/connections/connections.md +3 -0
- package/connections/figma.md +2 -0
- package/connections/gdd-state.md +186 -0
- package/hooks/budget-enforcer.ts +716 -0
- package/hooks/context-exhaustion.ts +251 -0
- package/hooks/gdd-read-injection-scanner.ts +172 -0
- package/hooks/hooks.json +3 -3
- package/package.json +32 -51
- package/reference/codex-tools.md +53 -0
- package/reference/config-schema.md +2 -2
- package/reference/error-recovery.md +58 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +21 -0
- package/reference/schemas/budget.schema.json +42 -0
- package/reference/schemas/events.schema.json +55 -0
- package/reference/schemas/generated.d.ts +419 -0
- package/reference/schemas/iteration-budget.schema.json +36 -0
- package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
- package/reference/schemas/rate-limits.schema.json +31 -0
- package/scripts/aggregate-agent-metrics.ts +282 -0
- package/scripts/codegen-schema-types.ts +149 -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/error-classifier.cjs +232 -0
- package/scripts/lib/error-classifier.d.cts +44 -0
- package/scripts/lib/event-stream/emitter.ts +88 -0
- package/scripts/lib/event-stream/index.ts +164 -0
- package/scripts/lib/event-stream/types.ts +127 -0
- package/scripts/lib/event-stream/writer.ts +154 -0
- 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/gdd-errors/classification.ts +124 -0
- package/scripts/lib/gdd-errors/index.ts +218 -0
- package/scripts/lib/gdd-state/gates.ts +216 -0
- package/scripts/lib/gdd-state/index.ts +167 -0
- package/scripts/lib/gdd-state/lockfile.ts +232 -0
- package/scripts/lib/gdd-state/mutator.ts +574 -0
- package/scripts/lib/gdd-state/parser.ts +523 -0
- package/scripts/lib/gdd-state/types.ts +179 -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/iteration-budget.cjs +205 -0
- package/scripts/lib/iteration-budget.d.cts +32 -0
- package/scripts/lib/jittered-backoff.cjs +112 -0
- package/scripts/lib/jittered-backoff.d.cts +38 -0
- package/scripts/lib/lockfile.cjs +177 -0
- package/scripts/lib/lockfile.d.cts +21 -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/prompt-sanitizer/index.ts +435 -0
- package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
- package/scripts/lib/rate-guard.cjs +365 -0
- package/scripts/lib/rate-guard.d.cts +38 -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/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
- package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
- package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
- package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
- package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
- package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
- package/scripts/mcp-servers/gdd-state/server.ts +288 -0
- package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
- package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
- package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
- package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
- package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
- package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
- package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
- package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
- package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
- package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
- package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
- package/scripts/validate-frontmatter.ts +114 -0
- package/scripts/validate-schemas.ts +401 -0
- package/skills/brief/SKILL.md +15 -6
- package/skills/design/SKILL.md +31 -13
- package/skills/explore/SKILL.md +41 -17
- package/skills/health/SKILL.md +15 -4
- package/skills/optimize/SKILL.md +3 -3
- package/skills/pause/SKILL.md +16 -10
- package/skills/plan/SKILL.md +33 -17
- package/skills/progress/SKILL.md +15 -11
- package/skills/resume/SKILL.md +19 -10
- package/skills/settings/SKILL.md +11 -3
- package/skills/todo/SKILL.md +12 -3
- package/skills/verify/SKILL.md +65 -29
- package/hooks/budget-enforcer.js +0 -329
- package/hooks/context-exhaustion.js +0 -127
- package/hooks/gdd-read-injection-scanner.js +0 -39
- package/scripts/aggregate-agent-metrics.js +0 -173
- package/scripts/validate-frontmatter.cjs +0 -68
- package/scripts/validate-schemas.cjs +0 -242
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// scripts/lib/explore-parallel-runner/synthesizer.ts — Plan 21-06 (SDK-18).
|
|
2
|
+
//
|
|
3
|
+
// Incremental synthesizer driver. Watches `.design/map/<name>.md` for
|
|
4
|
+
// each mapper output, detects stable-size writes, and spawns a single
|
|
5
|
+
// synthesizer session with a concatenated prompt when all files are
|
|
6
|
+
// stable (or timeoutMs has elapsed).
|
|
7
|
+
//
|
|
8
|
+
// Design contract (as documented in the plan — "streaming-session
|
|
9
|
+
// injection contract"):
|
|
10
|
+
//
|
|
11
|
+
// 1. Wait for all mapper files to stabilize OR timeoutMs.
|
|
12
|
+
// 2. Read each stable file's content, concatenate under the synth
|
|
13
|
+
// prompt, mark missing/unstable files explicitly.
|
|
14
|
+
// 3. Spawn session-runner with the composite prompt.
|
|
15
|
+
//
|
|
16
|
+
// A fuller mid-session injection protocol would require Agent SDK
|
|
17
|
+
// multi-turn user-message injection; that's deferred to Phase 22.
|
|
18
|
+
// This implementation still satisfies the "streaming" acceptance: we
|
|
19
|
+
// DO NOT block the caller's mappers while waiting, and the
|
|
20
|
+
// synthesizer's prompt is composed dynamically from ready files.
|
|
21
|
+
//
|
|
22
|
+
// Stable-size detection: compare `statSync(path).size` across two
|
|
23
|
+
// consecutive polls. Unchanged size + still-present file → stable.
|
|
24
|
+
|
|
25
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
26
|
+
import { resolve as resolvePath } from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { run as defaultSessionRun } from '../session-runner/index.ts';
|
|
29
|
+
import type {
|
|
30
|
+
BudgetCap,
|
|
31
|
+
SessionResult,
|
|
32
|
+
SessionRunnerOptions,
|
|
33
|
+
} from '../session-runner/types.ts';
|
|
34
|
+
|
|
35
|
+
export interface SynthesizeStreamingArgs {
|
|
36
|
+
readonly mapperNames: readonly string[];
|
|
37
|
+
readonly mapperOutputPaths: readonly string[];
|
|
38
|
+
readonly synthesizerPrompt: string;
|
|
39
|
+
readonly budget: BudgetCap;
|
|
40
|
+
readonly maxTurns: number;
|
|
41
|
+
readonly runOverride?: (
|
|
42
|
+
opts: SessionRunnerOptions,
|
|
43
|
+
) => Promise<SessionResult>;
|
|
44
|
+
readonly cwd: string;
|
|
45
|
+
/** Polling interval for stable-size detection (ms). Default 200. */
|
|
46
|
+
readonly pollIntervalMs?: number;
|
|
47
|
+
/** Total watch timeout (ms). Default 600_000 (10 min). */
|
|
48
|
+
readonly timeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SynthesizeStreamingResult {
|
|
52
|
+
readonly status: 'completed' | 'error' | 'timeout' | 'skipped';
|
|
53
|
+
readonly output_path: string;
|
|
54
|
+
readonly usage: {
|
|
55
|
+
readonly input_tokens: number;
|
|
56
|
+
readonly output_tokens: number;
|
|
57
|
+
readonly usd_cost: number;
|
|
58
|
+
};
|
|
59
|
+
readonly files_fed: readonly string[];
|
|
60
|
+
readonly error?: { readonly code: string; readonly message: string };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_POLL_MS = 200;
|
|
64
|
+
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Probe a single mapper output path. Returns the current byte size when
|
|
68
|
+
* the path is a readable file, `null` when absent or unreadable.
|
|
69
|
+
*/
|
|
70
|
+
function probeSize(path: string): number | null {
|
|
71
|
+
try {
|
|
72
|
+
const st = statSync(path);
|
|
73
|
+
if (!st.isFile()) return null;
|
|
74
|
+
return st.size;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wait until every `mapperOutputPaths[i]` is present AND has stable size
|
|
82
|
+
* across two consecutive polls — OR `timeoutMs` elapses. Returns the
|
|
83
|
+
* list of paths that stabilized in time.
|
|
84
|
+
*
|
|
85
|
+
* We allow `pollIntervalMs` to be explicitly `0` in tests (synchronous
|
|
86
|
+
* ready state). The minimum practical interval is still sub-millisecond
|
|
87
|
+
* — we never block the event loop longer than Node's setTimeout jitter.
|
|
88
|
+
*/
|
|
89
|
+
async function waitForStableFiles(
|
|
90
|
+
paths: readonly string[],
|
|
91
|
+
pollIntervalMs: number,
|
|
92
|
+
timeoutMs: number,
|
|
93
|
+
): Promise<readonly string[]> {
|
|
94
|
+
if (paths.length === 0) return Object.freeze([]);
|
|
95
|
+
|
|
96
|
+
const deadline: number = Date.now() + timeoutMs;
|
|
97
|
+
const lastSize: Map<string, number | null> = new Map();
|
|
98
|
+
const stable: Set<string> = new Set();
|
|
99
|
+
|
|
100
|
+
// Prime: record current sizes.
|
|
101
|
+
for (const p of paths) {
|
|
102
|
+
lastSize.set(p, probeSize(p));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
while (stable.size < paths.length) {
|
|
106
|
+
if (Date.now() >= deadline) break;
|
|
107
|
+
await sleep(pollIntervalMs);
|
|
108
|
+
|
|
109
|
+
for (const p of paths) {
|
|
110
|
+
if (stable.has(p)) continue;
|
|
111
|
+
const cur: number | null = probeSize(p);
|
|
112
|
+
const prev: number | null | undefined = lastSize.get(p);
|
|
113
|
+
if (cur !== null && prev !== null && prev !== undefined && cur === prev && cur >= 0) {
|
|
114
|
+
// Also require file exists (already enforced by non-null cur).
|
|
115
|
+
// And prevent 0-byte false positives by requiring at least one
|
|
116
|
+
// prior observation with a non-null size that matches.
|
|
117
|
+
stable.add(p);
|
|
118
|
+
}
|
|
119
|
+
lastSize.set(p, cur);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return Object.freeze(
|
|
124
|
+
paths.filter((p) => stable.has(p)),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Promise-returning sleep. 0 ms resolves on next microtask. */
|
|
129
|
+
function sleep(ms: number): Promise<void> {
|
|
130
|
+
if (ms <= 0) return Promise.resolve();
|
|
131
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compose the synthesizer prompt from the base instructions + each
|
|
136
|
+
* stable mapper's file content, plus a note listing mappers that
|
|
137
|
+
* weren't ready.
|
|
138
|
+
*/
|
|
139
|
+
function composePrompt(args: {
|
|
140
|
+
basePrompt: string;
|
|
141
|
+
mapperNames: readonly string[];
|
|
142
|
+
mapperOutputPaths: readonly string[];
|
|
143
|
+
readyPaths: readonly string[];
|
|
144
|
+
}): { composed: string; filesFed: readonly string[] } {
|
|
145
|
+
const readySet: Set<string> = new Set(args.readyPaths);
|
|
146
|
+
const readBlocks: string[] = [];
|
|
147
|
+
const filesFed: string[] = [];
|
|
148
|
+
const missing: string[] = [];
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < args.mapperNames.length; i += 1) {
|
|
151
|
+
const name: string = args.mapperNames[i] ?? `mapper-${i}`;
|
|
152
|
+
const path: string = args.mapperOutputPaths[i] ?? '';
|
|
153
|
+
if (readySet.has(path)) {
|
|
154
|
+
let content = '';
|
|
155
|
+
try {
|
|
156
|
+
content = readFileSync(path, 'utf8');
|
|
157
|
+
} catch {
|
|
158
|
+
// File disappeared between stability check and read — treat
|
|
159
|
+
// as missing rather than erroring.
|
|
160
|
+
missing.push(name);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
readBlocks.push(`## Mapper: ${name}\n\n<path>${path}</path>\n\n${content}`);
|
|
164
|
+
filesFed.push(path);
|
|
165
|
+
} else {
|
|
166
|
+
missing.push(name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const missingNote: string =
|
|
171
|
+
missing.length > 0
|
|
172
|
+
? `\n\n<missing_mappers>\n${missing.join('\n')}\n</missing_mappers>\n`
|
|
173
|
+
: '';
|
|
174
|
+
|
|
175
|
+
const mapperSection: string =
|
|
176
|
+
readBlocks.length > 0
|
|
177
|
+
? `\n\n<mapper_outputs>\n\n${readBlocks.join('\n\n---\n\n')}\n\n</mapper_outputs>`
|
|
178
|
+
: '\n\n<mapper_outputs>(none ready)</mapper_outputs>';
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
composed: `${args.basePrompt}${missingNote}${mapperSection}`,
|
|
182
|
+
filesFed: Object.freeze(filesFed),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Public driver. Wait for mapper outputs to stabilize (or timeout),
|
|
188
|
+
* spawn the synthesizer session with the composite prompt, return
|
|
189
|
+
* terminal status + usage + fed-file list.
|
|
190
|
+
*
|
|
191
|
+
* Never throws. On session error, returns status 'error' with populated
|
|
192
|
+
* `.error`. On stability-wait timeout with NO files ready, returns
|
|
193
|
+
* status 'timeout' + empty `files_fed` (session NOT spawned).
|
|
194
|
+
*/
|
|
195
|
+
export async function synthesizeStreaming(
|
|
196
|
+
args: SynthesizeStreamingArgs,
|
|
197
|
+
): Promise<SynthesizeStreamingResult> {
|
|
198
|
+
const outputPath: string = resolvePath(args.cwd, '.design/DESIGN-PATTERNS.md');
|
|
199
|
+
const pollIntervalMs: number = args.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
200
|
+
const timeoutMs: number = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
201
|
+
|
|
202
|
+
// Resolve each mapper output path against cwd so callers can pass
|
|
203
|
+
// relative paths (e.g. '.design/map/token.md').
|
|
204
|
+
const absPaths: readonly string[] = args.mapperOutputPaths.map((p) =>
|
|
205
|
+
resolvePath(args.cwd, p),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const readyPaths: readonly string[] = await waitForStableFiles(
|
|
209
|
+
absPaths,
|
|
210
|
+
pollIntervalMs,
|
|
211
|
+
timeoutMs,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// If nothing stabilized within the timeout, short-circuit — don't
|
|
215
|
+
// burn a session with zero input. Callers log a warning.
|
|
216
|
+
if (readyPaths.length === 0 && absPaths.length > 0) {
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
status: 'timeout',
|
|
219
|
+
output_path: outputPath,
|
|
220
|
+
usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
|
|
221
|
+
files_fed: Object.freeze([]),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { composed, filesFed } = composePrompt({
|
|
226
|
+
basePrompt: args.synthesizerPrompt,
|
|
227
|
+
mapperNames: args.mapperNames,
|
|
228
|
+
mapperOutputPaths: absPaths,
|
|
229
|
+
readyPaths,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const runFn: (o: SessionRunnerOptions) => Promise<SessionResult> =
|
|
233
|
+
args.runOverride ?? defaultSessionRun;
|
|
234
|
+
|
|
235
|
+
const runnerOpts: SessionRunnerOptions = {
|
|
236
|
+
prompt: composed,
|
|
237
|
+
stage: 'explore',
|
|
238
|
+
budget: args.budget,
|
|
239
|
+
turnCap: { maxTurns: args.maxTurns },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
let sessionResult: SessionResult;
|
|
243
|
+
try {
|
|
244
|
+
sessionResult = await runFn(runnerOpts);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const message: string = err instanceof Error ? err.message : String(err);
|
|
247
|
+
return Object.freeze({
|
|
248
|
+
status: 'error',
|
|
249
|
+
output_path: outputPath,
|
|
250
|
+
usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
|
|
251
|
+
files_fed: filesFed,
|
|
252
|
+
error: Object.freeze({ code: 'RUN_THREW', message }),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Translate SessionResult → SynthesizeStreamingResult.
|
|
257
|
+
const usage = {
|
|
258
|
+
input_tokens: sessionResult.usage.input_tokens,
|
|
259
|
+
output_tokens: sessionResult.usage.output_tokens,
|
|
260
|
+
usd_cost: sessionResult.usage.usd_cost,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Determine terminal status. The plan allows the timeout outcome to
|
|
264
|
+
// bubble through even when a session DID spawn with partial inputs;
|
|
265
|
+
// we collapse to 'completed' when the session finished cleanly, even
|
|
266
|
+
// if not all files were ready. Callers inspect `files_fed.length <
|
|
267
|
+
// mapperNames.length` to detect partial coverage.
|
|
268
|
+
if (sessionResult.status !== 'completed') {
|
|
269
|
+
const err = sessionResult.error ?? {
|
|
270
|
+
code: sessionResult.status.toUpperCase(),
|
|
271
|
+
message: `synth session ended with status ${sessionResult.status}`,
|
|
272
|
+
};
|
|
273
|
+
return Object.freeze({
|
|
274
|
+
status: 'error',
|
|
275
|
+
output_path: outputPath,
|
|
276
|
+
usage,
|
|
277
|
+
files_fed: filesFed,
|
|
278
|
+
error: Object.freeze({ code: err.code, message: err.message }),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Happy path.
|
|
283
|
+
//
|
|
284
|
+
// If the caller provided 0 mapper paths, we didn't wait and didn't
|
|
285
|
+
// feed anything — that's the "synthesizer invoked with pre-rendered
|
|
286
|
+
// prompt" shape (used by the run() orchestrator's empty-mapper
|
|
287
|
+
// short-circuit elsewhere, though `run()` skips synth entirely in
|
|
288
|
+
// that case).
|
|
289
|
+
return Object.freeze({
|
|
290
|
+
status: 'completed',
|
|
291
|
+
output_path: outputPath,
|
|
292
|
+
usage,
|
|
293
|
+
files_fed: filesFed,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// scripts/lib/explore-parallel-runner/types.ts — Plan 21-06 (SDK-18).
|
|
2
|
+
//
|
|
3
|
+
// Public type surface for the 4-mapper parallel explore runner. Consumers
|
|
4
|
+
// import from ./index.ts (the barrel); this file carries the type graph
|
|
5
|
+
// so mappers.ts / synthesizer.ts / index.ts can share a single source of
|
|
6
|
+
// truth without cyclic imports.
|
|
7
|
+
//
|
|
8
|
+
// Design notes:
|
|
9
|
+
// * `MapperName` is a closed union. Extending it requires touching the
|
|
10
|
+
// DEFAULT_MAPPERS table in index.ts, so a type-level addition forces
|
|
11
|
+
// a compile error on any registry-consuming site.
|
|
12
|
+
// * `ExploreRunnerOptions.runOverride` mirrors `SessionRunnerOptions.queryOverride`
|
|
13
|
+
// at a higher altitude — it replaces the whole `session-runner.run()`
|
|
14
|
+
// call per-mapper/per-synthesizer. Tests build a deterministic
|
|
15
|
+
// `runOverride` that returns canned `SessionResult`s without touching
|
|
16
|
+
// the Agent SDK.
|
|
17
|
+
// * All numeric fields in usage/outcome default to 0 on unpopulated
|
|
18
|
+
// sessions; we never surface `undefined` in `total_usage` math.
|
|
19
|
+
// * `parallel_count` + `serial_count` together === `specs.length`
|
|
20
|
+
// (after parallelism_safe partitioning). Synthesizer is counted
|
|
21
|
+
// separately under `.synthesizer`.
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
BudgetCap,
|
|
25
|
+
SessionResult,
|
|
26
|
+
SessionRunnerOptions,
|
|
27
|
+
} from '../session-runner/types.ts';
|
|
28
|
+
|
|
29
|
+
/** Closed union of mapper roster. Changing this forces a compile-time
|
|
30
|
+
* touch of `DEFAULT_MAPPERS` in index.ts — a deliberate choke point
|
|
31
|
+
* so the roster stays the Phase-21 Locked 4. */
|
|
32
|
+
export type MapperName =
|
|
33
|
+
| 'token'
|
|
34
|
+
| 'component-taxonomy'
|
|
35
|
+
| 'a11y'
|
|
36
|
+
| 'visual-hierarchy';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A single mapper invocation specification. The runner does NOT own
|
|
40
|
+
* prompt construction — callers assemble the prompt body from their
|
|
41
|
+
* `DESIGN-CONTEXT.md` + roster scaffolding and pass it through here.
|
|
42
|
+
*/
|
|
43
|
+
export interface MapperSpec {
|
|
44
|
+
/** Mapper identifier — also used in event payloads + log scopes. */
|
|
45
|
+
readonly name: MapperName;
|
|
46
|
+
/** Path to `agents/<name>.md`. Missing file is tolerated (stage default). */
|
|
47
|
+
readonly agentPath: string;
|
|
48
|
+
/** Expected mapper output file, e.g. `.design/map/token.md`. */
|
|
49
|
+
readonly outputPath: string;
|
|
50
|
+
/** Per-mapper prompt body passed to session-runner. */
|
|
51
|
+
readonly prompt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Terminal record for a single mapper invocation. `output_exists` /
|
|
56
|
+
* `output_bytes` are captured AFTER the session terminates — if the
|
|
57
|
+
* mapper wrote its file but the session errored in cleanup, we still
|
|
58
|
+
* surface the file's presence.
|
|
59
|
+
*
|
|
60
|
+
* `error` is populated iff `status === 'error'`.
|
|
61
|
+
*/
|
|
62
|
+
export interface MapperOutcome {
|
|
63
|
+
readonly name: MapperName;
|
|
64
|
+
readonly status: 'completed' | 'error' | 'skipped';
|
|
65
|
+
readonly output_exists: boolean;
|
|
66
|
+
readonly output_bytes: number;
|
|
67
|
+
readonly usage: {
|
|
68
|
+
readonly input_tokens: number;
|
|
69
|
+
readonly output_tokens: number;
|
|
70
|
+
readonly usd_cost: number;
|
|
71
|
+
};
|
|
72
|
+
readonly duration_ms: number;
|
|
73
|
+
readonly error?: { readonly code: string; readonly message: string };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Caller-facing run options for the explore runner. Most fields mirror
|
|
78
|
+
* session-runner's BudgetCap / turn caps but applied per-mapper.
|
|
79
|
+
*
|
|
80
|
+
* `runOverride`: when supplied, the runner invokes this instead of the
|
|
81
|
+
* real `session-runner.run()`. Tests build overrides that return canned
|
|
82
|
+
* `SessionResult`s without touching the Agent SDK.
|
|
83
|
+
*/
|
|
84
|
+
export interface ExploreRunnerOptions {
|
|
85
|
+
/** Override the mapper roster. Defaults to DEFAULT_MAPPERS (the locked 4). */
|
|
86
|
+
readonly mappers?: readonly MapperSpec[];
|
|
87
|
+
/** Per-mapper hard budget cap. Shared envelope — does NOT pool across mappers. */
|
|
88
|
+
readonly budget: BudgetCap;
|
|
89
|
+
/** Per-mapper turn cap. */
|
|
90
|
+
readonly maxTurnsPerMapper: number;
|
|
91
|
+
/** Max concurrent mappers. Defaults to 4 (the full locked roster). */
|
|
92
|
+
readonly concurrency?: number;
|
|
93
|
+
/** Prompt body for the synthesizer session. */
|
|
94
|
+
readonly synthesizerPrompt: string;
|
|
95
|
+
/** Budget cap for the synthesizer session. */
|
|
96
|
+
readonly synthesizerBudget: BudgetCap;
|
|
97
|
+
/** Turn cap for the synthesizer session. */
|
|
98
|
+
readonly synthesizerMaxTurns: number;
|
|
99
|
+
/**
|
|
100
|
+
* Replace the session-runner.run() call entirely (test injection). Each
|
|
101
|
+
* mapper + the synthesizer each consume one invocation of this override.
|
|
102
|
+
*/
|
|
103
|
+
readonly runOverride?: (
|
|
104
|
+
opts: SessionRunnerOptions,
|
|
105
|
+
) => Promise<SessionResult>;
|
|
106
|
+
/** Current working directory used for path resolution (fixtures / agent files). */
|
|
107
|
+
readonly cwd?: string;
|
|
108
|
+
/** Override the file-stability polling interval (ms). Default 200. */
|
|
109
|
+
readonly pollIntervalMs?: number;
|
|
110
|
+
/** Override the file-watch timeout (ms). Default 600_000 (10 min). */
|
|
111
|
+
readonly timeoutMs?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Terminal record for the whole runner invocation. `parallel_count` +
|
|
116
|
+
* `serial_count` === mappers.length (after parallelism_safe partitioning).
|
|
117
|
+
* `total_usage` aggregates mappers + synthesizer.
|
|
118
|
+
*/
|
|
119
|
+
export interface ExploreRunnerResult {
|
|
120
|
+
readonly mappers: readonly MapperOutcome[];
|
|
121
|
+
readonly synthesizer: {
|
|
122
|
+
readonly status: 'completed' | 'error' | 'skipped' | 'timeout';
|
|
123
|
+
readonly output_path: string;
|
|
124
|
+
readonly usage: {
|
|
125
|
+
readonly input_tokens: number;
|
|
126
|
+
readonly output_tokens: number;
|
|
127
|
+
readonly usd_cost: number;
|
|
128
|
+
};
|
|
129
|
+
readonly files_fed: readonly string[];
|
|
130
|
+
readonly error?: { readonly code: string; readonly message: string };
|
|
131
|
+
};
|
|
132
|
+
readonly parallel_count: number;
|
|
133
|
+
readonly serial_count: number;
|
|
134
|
+
readonly total_usage: {
|
|
135
|
+
readonly input_tokens: number;
|
|
136
|
+
readonly output_tokens: number;
|
|
137
|
+
readonly usd_cost: number;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// scripts/lib/gdd-errors/classification.ts — error classification + MCP helpers.
|
|
2
|
+
//
|
|
3
|
+
// Two exports:
|
|
4
|
+
// * classify(err) — normalize any thrown value into an
|
|
5
|
+
// ErrorClassification descriptor
|
|
6
|
+
// * toToolError(err) — wrap into the shape MCP tool handlers return
|
|
7
|
+
// inside data.error
|
|
8
|
+
//
|
|
9
|
+
// These helpers are the bridge between the taxonomy (index.ts) and the
|
|
10
|
+
// MCP tool layer (plan 20-05 wires them into all 11 tool handlers).
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
GDDError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
StateConflictError,
|
|
16
|
+
OperationFailedError,
|
|
17
|
+
} from './index.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The four-valued classification of any error value. `kind === 'unknown'`
|
|
21
|
+
* means the thrown value was not a GDDError — either a plain Error or a
|
|
22
|
+
* non-Error value (string, number, etc.).
|
|
23
|
+
*
|
|
24
|
+
* Flag semantics:
|
|
25
|
+
* shouldThrow=true — caller should propagate (or rethrow) this error;
|
|
26
|
+
* it represents an invariant violation or bad input
|
|
27
|
+
* shouldThrow=false — caller should embed this in data.error and
|
|
28
|
+
* return normally (expected branching failure)
|
|
29
|
+
* retryable=true — upstream may retry after a backoff (only
|
|
30
|
+
* StateConflictError; e.g. lockfile contention)
|
|
31
|
+
*/
|
|
32
|
+
export interface ErrorClassification {
|
|
33
|
+
kind: 'validation' | 'state_conflict' | 'operation_failed' | 'unknown';
|
|
34
|
+
shouldThrow: boolean;
|
|
35
|
+
retryable: boolean;
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Classify any value thrown at us. Robust to non-Error throws (strings,
|
|
42
|
+
* numbers, plain objects) — returns `kind: 'unknown'` with a sensible
|
|
43
|
+
* message rather than blowing up.
|
|
44
|
+
*/
|
|
45
|
+
export function classify(err: unknown): ErrorClassification {
|
|
46
|
+
if (err instanceof ValidationError) {
|
|
47
|
+
return {
|
|
48
|
+
kind: 'validation',
|
|
49
|
+
shouldThrow: true,
|
|
50
|
+
retryable: false,
|
|
51
|
+
code: err.code,
|
|
52
|
+
message: err.message,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (err instanceof StateConflictError) {
|
|
56
|
+
return {
|
|
57
|
+
kind: 'state_conflict',
|
|
58
|
+
shouldThrow: true,
|
|
59
|
+
retryable: true,
|
|
60
|
+
code: err.code,
|
|
61
|
+
message: err.message,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (err instanceof OperationFailedError) {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'operation_failed',
|
|
67
|
+
shouldThrow: false,
|
|
68
|
+
retryable: false,
|
|
69
|
+
code: err.code,
|
|
70
|
+
message: err.message,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (err instanceof Error) {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'unknown',
|
|
76
|
+
shouldThrow: true,
|
|
77
|
+
retryable: false,
|
|
78
|
+
code: 'UNKNOWN',
|
|
79
|
+
message: err.message,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: 'unknown',
|
|
84
|
+
shouldThrow: true,
|
|
85
|
+
retryable: false,
|
|
86
|
+
code: 'UNKNOWN',
|
|
87
|
+
message: String(err),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Shape MCP tool handlers return inside `data.error`. The `context`
|
|
93
|
+
* key is only present when the error is a GDDError instance — plain
|
|
94
|
+
* errors don't carry structured context, and we intentionally don't
|
|
95
|
+
* fabricate one (the `code` + `message` pair is sufficient).
|
|
96
|
+
*/
|
|
97
|
+
export interface ToolErrorPayload {
|
|
98
|
+
error: {
|
|
99
|
+
code: string;
|
|
100
|
+
message: string;
|
|
101
|
+
kind: ErrorClassification['kind'];
|
|
102
|
+
context?: Readonly<Record<string, unknown>>;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert any error into the shape MCP tool handlers return in
|
|
108
|
+
* data.error. Plan 20-05 wires this into the 11 tool handlers; Plan
|
|
109
|
+
* 20-06 emits it as an `error` event on the telemetry stream.
|
|
110
|
+
*/
|
|
111
|
+
export function toToolError(err: unknown): ToolErrorPayload {
|
|
112
|
+
const c = classify(err);
|
|
113
|
+
const out: ToolErrorPayload = {
|
|
114
|
+
error: {
|
|
115
|
+
code: c.code,
|
|
116
|
+
message: c.message,
|
|
117
|
+
kind: c.kind,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
if (err instanceof GDDError) {
|
|
121
|
+
out.error.context = err.context;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|