@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,184 @@
|
|
|
1
|
+
// scripts/lib/discuss-parallel-runner/types.ts — Plan 21-07 (SDK-19).
|
|
2
|
+
//
|
|
3
|
+
// Public type surface for the parallel discussion runner. The runner
|
|
4
|
+
// spawns N design-discussant variants (each a different persona / angle
|
|
5
|
+
// — user-journey, technical-constraint, brand-fit, accessibility)
|
|
6
|
+
// concurrently via `session-runner`, collects each discussant's open
|
|
7
|
+
// questions and concerns as a structured `DiscussionContribution`, and
|
|
8
|
+
// runs an aggregator pass that deduplicates, clusters by theme, and
|
|
9
|
+
// surfaces a single ranked question list for the user.
|
|
10
|
+
//
|
|
11
|
+
// Consumers: the `discuss` skill (standalone leaf) and the `gdd-sdk
|
|
12
|
+
// discuss` CLI subcommand (Plan 21-09).
|
|
13
|
+
//
|
|
14
|
+
// Design invariants (see PLAN.md Context):
|
|
15
|
+
// * Discussant sessions are independent — none write to shared files;
|
|
16
|
+
// parallelism is always safe.
|
|
17
|
+
// * The aggregator runs AFTER all discussants complete.
|
|
18
|
+
// * Error isolation: one discussant's failure never cascades into
|
|
19
|
+
// other discussant sessions.
|
|
20
|
+
// * `AggregatedQuestion.key` is SHA-256-based (stable across runs)
|
|
21
|
+
// per the aggregator prompt contract.
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
BudgetCap,
|
|
25
|
+
SessionResult,
|
|
26
|
+
SessionRunnerOptions,
|
|
27
|
+
} from '../session-runner/types.ts';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Named discussant variants. The default roster is the four values in
|
|
31
|
+
* the union below, but callers can pass any string (arbitrary custom
|
|
32
|
+
* discussant).
|
|
33
|
+
*/
|
|
34
|
+
export type DiscussantName =
|
|
35
|
+
| 'user-journey'
|
|
36
|
+
| 'technical-constraint'
|
|
37
|
+
| 'brand-fit'
|
|
38
|
+
| 'accessibility'
|
|
39
|
+
| string;
|
|
40
|
+
|
|
41
|
+
/** Severity ordering is blocker > major > minor > nice-to-have. */
|
|
42
|
+
export type Severity = 'blocker' | 'major' | 'minor' | 'nice-to-have';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One discussant specification. The runner passes `prompt` verbatim
|
|
46
|
+
* through `session-runner.run()` as the prompt body.
|
|
47
|
+
*
|
|
48
|
+
* `agentPath` is optional; when absent, the runner defaults to the
|
|
49
|
+
* stage scope from tool-scoping (`discuss` maps to `custom` — see
|
|
50
|
+
* Plan 21-03). When present, the runner reads the agent markdown's
|
|
51
|
+
* `tools:` frontmatter via `parseAgentToolsByName` and passes the
|
|
52
|
+
* resolved list as `allowedTools`.
|
|
53
|
+
*/
|
|
54
|
+
export interface DiscussantSpec {
|
|
55
|
+
name: DiscussantName;
|
|
56
|
+
/** Optional agent frontmatter path; missing → stage scope from tool-scoping. */
|
|
57
|
+
agentPath?: string;
|
|
58
|
+
/** Per-discussant prompt body. */
|
|
59
|
+
prompt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One parsed item from a discussant's DISCUSSION COMPLETE block.
|
|
64
|
+
*
|
|
65
|
+
* `kind` discriminates questions (things the discussant wants
|
|
66
|
+
* answered) from concerns (things they want to flag).
|
|
67
|
+
*
|
|
68
|
+
* `tag` captures the per-item annotation from the discussant output
|
|
69
|
+
* (`Concern: <stakeholder>` for questions, `Area: <scope>` for
|
|
70
|
+
* concerns). Optional because lenient parse allows missing.
|
|
71
|
+
*/
|
|
72
|
+
export interface DiscussionItem {
|
|
73
|
+
kind: 'question' | 'concern';
|
|
74
|
+
text: string;
|
|
75
|
+
/** Area / angle / concern tag per discussant output. */
|
|
76
|
+
tag?: string;
|
|
77
|
+
severity: Severity;
|
|
78
|
+
rationale?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One discussant's complete contribution. `items` is empty when the
|
|
83
|
+
* session errored or the block was missing/malformed.
|
|
84
|
+
*
|
|
85
|
+
* `status`:
|
|
86
|
+
* * `completed` — session ended cleanly AND a DISCUSSION COMPLETE
|
|
87
|
+
* block was parsed successfully.
|
|
88
|
+
* * `parse-error` — session ended cleanly but the block was absent
|
|
89
|
+
* or malformed (items: []).
|
|
90
|
+
* * `error` — session failed (budget, turn cap, aborted, error);
|
|
91
|
+
* `error` populated; items: [].
|
|
92
|
+
*/
|
|
93
|
+
export interface DiscussionContribution {
|
|
94
|
+
discussant: DiscussantName;
|
|
95
|
+
items: readonly DiscussionItem[];
|
|
96
|
+
/** Raw final_text captured for audit / aggregator input. */
|
|
97
|
+
raw: string;
|
|
98
|
+
usage: { input_tokens: number; output_tokens: number; usd_cost: number };
|
|
99
|
+
status: 'completed' | 'error' | 'parse-error';
|
|
100
|
+
error?: { code: string; message: string };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One aggregated (post-dedup, post-cluster) question.
|
|
105
|
+
*
|
|
106
|
+
* * `key` — SHA-256 of the normalized question text (lowercase,
|
|
107
|
+
* whitespace-collapsed) truncated to 8 hex chars.
|
|
108
|
+
* Stable across runs.
|
|
109
|
+
* * `raised_by` — discussants that raised (a semantic variant of)
|
|
110
|
+
* this question.
|
|
111
|
+
* * `theme` — cluster/theme name from aggregator.
|
|
112
|
+
* * `rank` — 0-indexed priority (0 = highest). Ranking combines
|
|
113
|
+
* severity + frequency per the aggregator prompt.
|
|
114
|
+
*/
|
|
115
|
+
export interface AggregatedQuestion {
|
|
116
|
+
/** Stable key across runs (hash of normalized question text). */
|
|
117
|
+
key: string;
|
|
118
|
+
text: string;
|
|
119
|
+
severity: Severity;
|
|
120
|
+
/** Discussants that raised this question. */
|
|
121
|
+
raised_by: readonly DiscussantName[];
|
|
122
|
+
/** Cluster/theme assignment from aggregator. */
|
|
123
|
+
theme: string;
|
|
124
|
+
/** Aggregator-assigned rank (0 = highest priority). */
|
|
125
|
+
rank: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The aggregator's final output. `output_path` is the Markdown file
|
|
130
|
+
* written to disk (`.design/DISCUSSION.md` by default). `usage` is
|
|
131
|
+
* the aggregator session's token/cost spend — separate from the
|
|
132
|
+
* per-discussant usage aggregated in `DiscussRunnerResult.total_usage`.
|
|
133
|
+
*/
|
|
134
|
+
export interface AggregatedDiscussion {
|
|
135
|
+
themes: readonly { name: string; summary: string }[];
|
|
136
|
+
questions: readonly AggregatedQuestion[];
|
|
137
|
+
/** Output path. */
|
|
138
|
+
output_path: string;
|
|
139
|
+
/** Aggregator session usage. */
|
|
140
|
+
usage: { input_tokens: number; output_tokens: number; usd_cost: number };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Options for `run()` — the top-level orchestrator entry point.
|
|
145
|
+
*
|
|
146
|
+
* * `discussants` — omit to use `DEFAULT_DISCUSSANTS` (4 variants).
|
|
147
|
+
* * `budget` + `maxTurnsPerDiscussant` — applied per-discussant.
|
|
148
|
+
* * `aggregatorBudget` + `aggregatorMaxTurns` — applied to the
|
|
149
|
+
* aggregator session specifically.
|
|
150
|
+
* * `concurrency` — defaults to 4 (matches the default roster size).
|
|
151
|
+
* * `runOverride` — test injection for `session-runner.run()`. All
|
|
152
|
+
* discussants AND the aggregator receive the SAME override so one
|
|
153
|
+
* mock controls the entire run.
|
|
154
|
+
* * `aggregatorPrompt` — replace the default aggregator prompt
|
|
155
|
+
* (advanced / debug use).
|
|
156
|
+
*/
|
|
157
|
+
export interface DiscussRunnerOptions {
|
|
158
|
+
/** Discussants to run. Default: the 4-variant roster. */
|
|
159
|
+
discussants?: readonly DiscussantSpec[];
|
|
160
|
+
budget: BudgetCap;
|
|
161
|
+
maxTurnsPerDiscussant: number;
|
|
162
|
+
aggregatorBudget: BudgetCap;
|
|
163
|
+
aggregatorMaxTurns: number;
|
|
164
|
+
concurrency?: number;
|
|
165
|
+
runOverride?: (opts: SessionRunnerOptions) => Promise<SessionResult>;
|
|
166
|
+
cwd?: string;
|
|
167
|
+
/** Custom aggregator prompt override. */
|
|
168
|
+
aggregatorPrompt?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The final return value from `run()`.
|
|
173
|
+
*
|
|
174
|
+
* * `contributions` — one per discussant, in input spec order (NOT
|
|
175
|
+
* completion order). Failed discussants are included with
|
|
176
|
+
* `status !== 'completed'`.
|
|
177
|
+
* * `aggregated` — the aggregator's parsed output.
|
|
178
|
+
* * `total_usage` — sum of per-discussant + aggregator usage.
|
|
179
|
+
*/
|
|
180
|
+
export interface DiscussRunnerResult {
|
|
181
|
+
contributions: readonly DiscussionContribution[];
|
|
182
|
+
aggregated: AggregatedDiscussion;
|
|
183
|
+
total_usage: { input_tokens: number; output_tokens: number; usd_cost: number };
|
|
184
|
+
}
|
|
@@ -61,7 +61,17 @@ let cachedHost: string | null = null;
|
|
|
61
61
|
*/
|
|
62
62
|
export function getWriter(opts?: WriterOptions): EventWriter {
|
|
63
63
|
if (defaultWriter === null) {
|
|
64
|
-
|
|
64
|
+
// Honor GDD_EVENTS_PATH env var as the first-choice default path
|
|
65
|
+
// when the caller doesn't pass an explicit `opts.path`. Lets test
|
|
66
|
+
// harnesses and Plan 21-11's E2E subprocess steer the on-disk
|
|
67
|
+
// stream into a fixture-specific directory without chdir'ing the
|
|
68
|
+
// entire process. Explicit `opts.path` always wins.
|
|
69
|
+
const envPath: string | undefined = process.env['GDD_EVENTS_PATH'];
|
|
70
|
+
const finalOpts: WriterOptions =
|
|
71
|
+
opts?.path === undefined && envPath !== undefined && envPath.length > 0
|
|
72
|
+
? { ...(opts ?? {}), path: envPath }
|
|
73
|
+
: (opts ?? {});
|
|
74
|
+
defaultWriter = new EventWriter(finalOpts);
|
|
65
75
|
}
|
|
66
76
|
return defaultWriter;
|
|
67
77
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// scripts/lib/explore-parallel-runner/index.ts — Plan 21-06 (SDK-18).
|
|
2
|
+
//
|
|
3
|
+
// Public surface:
|
|
4
|
+
//
|
|
5
|
+
// run(opts: ExploreRunnerOptions): Promise<ExploreRunnerResult>
|
|
6
|
+
// DEFAULT_MAPPERS — the locked Phase-21 4-mapper roster (frozen).
|
|
7
|
+
// isParallelismSafe, spawnMapper, spawnMappersParallel (from mappers.ts)
|
|
8
|
+
// synthesizeStreaming (from synthesizer.ts)
|
|
9
|
+
// Types (from types.ts) — MapperName, MapperSpec, MapperOutcome,
|
|
10
|
+
// ExploreRunnerOptions, ExploreRunnerResult.
|
|
11
|
+
//
|
|
12
|
+
// Algorithm:
|
|
13
|
+
// 1. specs = opts.mappers ?? DEFAULT_MAPPERS.
|
|
14
|
+
// 2. Partition by isParallelismSafe(spec.agentPath).
|
|
15
|
+
// 3. Run safe mappers via spawnMappersParallel(concurrency).
|
|
16
|
+
// 4. Run unsafe mappers sequentially (tail phase).
|
|
17
|
+
// 5. Run synthesizer via synthesizeStreaming.
|
|
18
|
+
// 6. Aggregate total_usage = sum mapper + synthesizer.
|
|
19
|
+
// 7. Emit logger + explore.runner.* lifecycle events.
|
|
20
|
+
// 8. Return ExploreRunnerResult.
|
|
21
|
+
//
|
|
22
|
+
// Empty specs short-circuits: no mappers spawned, synthesizer skipped,
|
|
23
|
+
// returns an all-zero result.
|
|
24
|
+
|
|
25
|
+
import { resolve as resolvePath } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { getLogger } from '../logger/index.ts';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
isParallelismSafe,
|
|
31
|
+
spawnMapper,
|
|
32
|
+
spawnMappersParallel,
|
|
33
|
+
} from './mappers.ts';
|
|
34
|
+
import { synthesizeStreaming } from './synthesizer.ts';
|
|
35
|
+
import type {
|
|
36
|
+
ExploreRunnerOptions,
|
|
37
|
+
ExploreRunnerResult,
|
|
38
|
+
MapperOutcome,
|
|
39
|
+
MapperSpec,
|
|
40
|
+
} from './types.ts';
|
|
41
|
+
|
|
42
|
+
// Re-exports.
|
|
43
|
+
export type {
|
|
44
|
+
MapperName,
|
|
45
|
+
MapperSpec,
|
|
46
|
+
MapperOutcome,
|
|
47
|
+
ExploreRunnerOptions,
|
|
48
|
+
ExploreRunnerResult,
|
|
49
|
+
} from './types.ts';
|
|
50
|
+
export {
|
|
51
|
+
isParallelismSafe,
|
|
52
|
+
spawnMapper,
|
|
53
|
+
spawnMappersParallel,
|
|
54
|
+
} from './mappers.ts';
|
|
55
|
+
export { synthesizeStreaming } from './synthesizer.ts';
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// DEFAULT_MAPPERS — locked Phase-21 roster
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Locked 4-mapper roster for the explore stage. Frozen end-to-end so
|
|
63
|
+
* consumers can't mutate entries; override via ExploreRunnerOptions.mappers.
|
|
64
|
+
*
|
|
65
|
+
* Agent paths use the exact filenames from `agents/` (as of Phase 21):
|
|
66
|
+
* token-mapper.md, component-taxonomy-mapper.md, a11y-mapper.md,
|
|
67
|
+
* visual-hierarchy-mapper.md.
|
|
68
|
+
*
|
|
69
|
+
* When an agent file is missing, session-runner scope computation
|
|
70
|
+
* gracefully falls through to the stage default (see mappers.ts).
|
|
71
|
+
*/
|
|
72
|
+
export const DEFAULT_MAPPERS: readonly MapperSpec[] = Object.freeze([
|
|
73
|
+
Object.freeze({
|
|
74
|
+
name: 'token' as const,
|
|
75
|
+
agentPath: 'agents/token-mapper.md',
|
|
76
|
+
outputPath: '.design/map/token.md',
|
|
77
|
+
prompt:
|
|
78
|
+
'Enumerate every design token found in the UI source: colors, typography, spacing, radii, shadows, motion durations. Output to .design/map/token.md as a canonical token inventory.',
|
|
79
|
+
}),
|
|
80
|
+
Object.freeze({
|
|
81
|
+
name: 'component-taxonomy' as const,
|
|
82
|
+
agentPath: 'agents/component-taxonomy-mapper.md',
|
|
83
|
+
outputPath: '.design/map/component-taxonomy.md',
|
|
84
|
+
prompt:
|
|
85
|
+
'Enumerate component archetypes and their variants. Output to .design/map/component-taxonomy.md — one entry per archetype with variant list, slot inventory, and usage count.',
|
|
86
|
+
}),
|
|
87
|
+
Object.freeze({
|
|
88
|
+
name: 'a11y' as const,
|
|
89
|
+
agentPath: 'agents/a11y-mapper.md',
|
|
90
|
+
outputPath: '.design/map/a11y.md',
|
|
91
|
+
prompt:
|
|
92
|
+
'WCAG-axis scan: contrast ratios, keyboard navigation, ARIA semantics, focus management, reduced-motion respect. Output to .design/map/a11y.md — one section per axis with findings.',
|
|
93
|
+
}),
|
|
94
|
+
Object.freeze({
|
|
95
|
+
name: 'visual-hierarchy' as const,
|
|
96
|
+
agentPath: 'agents/visual-hierarchy-mapper.md',
|
|
97
|
+
outputPath: '.design/map/visual-hierarchy.md',
|
|
98
|
+
prompt:
|
|
99
|
+
'Describe z-order, focal points, and attention grammar. Output to .design/map/visual-hierarchy.md — one section per surface describing layering, emphasis, and scan path.',
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// run — main orchestrator
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Spawn the 4 mapper sessions (parallel) + the synthesizer (sequential
|
|
109
|
+
* after mappers become stable), aggregate usage, emit lifecycle events,
|
|
110
|
+
* return terminal ExploreRunnerResult.
|
|
111
|
+
*
|
|
112
|
+
* Contract:
|
|
113
|
+
* * Never throws. All failure modes land as outcomes / synth status.
|
|
114
|
+
* * Individual mapper errors do NOT abort other mappers.
|
|
115
|
+
* * parallelism_safe: false mappers run serially AFTER the safe batch.
|
|
116
|
+
* * total_usage aggregates mappers + synthesizer.
|
|
117
|
+
*/
|
|
118
|
+
export async function run(
|
|
119
|
+
opts: ExploreRunnerOptions,
|
|
120
|
+
): Promise<ExploreRunnerResult> {
|
|
121
|
+
const specs: readonly MapperSpec[] = opts.mappers ?? DEFAULT_MAPPERS;
|
|
122
|
+
const cwd: string = opts.cwd ?? process.cwd();
|
|
123
|
+
const concurrency: number = opts.concurrency ?? 4;
|
|
124
|
+
|
|
125
|
+
const logger = getLogger().child('explore.runner');
|
|
126
|
+
|
|
127
|
+
const outputPath: string = resolvePath(cwd, '.design/DESIGN-PATTERNS.md');
|
|
128
|
+
|
|
129
|
+
logger.info('explore.runner.started', {
|
|
130
|
+
mapper_count: specs.length,
|
|
131
|
+
concurrency,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Empty-spec short-circuit — no mappers, no synthesizer, zero usage.
|
|
135
|
+
if (specs.length === 0) {
|
|
136
|
+
logger.info('explore.runner.completed', {
|
|
137
|
+
parallel_count: 0,
|
|
138
|
+
serial_count: 0,
|
|
139
|
+
synthesizer_status: 'skipped',
|
|
140
|
+
total_usd_cost: 0,
|
|
141
|
+
});
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
mappers: Object.freeze([]),
|
|
144
|
+
synthesizer: Object.freeze({
|
|
145
|
+
status: 'skipped' as const,
|
|
146
|
+
output_path: outputPath,
|
|
147
|
+
usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
|
|
148
|
+
files_fed: Object.freeze([]),
|
|
149
|
+
}),
|
|
150
|
+
parallel_count: 0,
|
|
151
|
+
serial_count: 0,
|
|
152
|
+
total_usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Partition specs by parallelism_safe frontmatter ---------------------
|
|
157
|
+
const safeSpecs: MapperSpec[] = [];
|
|
158
|
+
const serialSpecs: MapperSpec[] = [];
|
|
159
|
+
for (const spec of specs) {
|
|
160
|
+
const resolvedAgentPath: string = resolvePath(cwd, spec.agentPath);
|
|
161
|
+
if (isParallelismSafe(resolvedAgentPath)) {
|
|
162
|
+
safeSpecs.push(spec);
|
|
163
|
+
} else {
|
|
164
|
+
serialSpecs.push(spec);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Parallel batch ------------------------------------------------------
|
|
169
|
+
const parallelOutcomes: readonly MapperOutcome[] =
|
|
170
|
+
safeSpecs.length > 0
|
|
171
|
+
? await spawnMappersParallel(safeSpecs, {
|
|
172
|
+
concurrency,
|
|
173
|
+
budget: opts.budget,
|
|
174
|
+
maxTurns: opts.maxTurnsPerMapper,
|
|
175
|
+
cwd,
|
|
176
|
+
...(opts.runOverride !== undefined ? { runOverride: opts.runOverride } : {}),
|
|
177
|
+
})
|
|
178
|
+
: Object.freeze([]);
|
|
179
|
+
|
|
180
|
+
for (const o of parallelOutcomes) {
|
|
181
|
+
logger.info('explore.runner.mapper_done', {
|
|
182
|
+
mapper: o.name,
|
|
183
|
+
status: o.status,
|
|
184
|
+
duration_ms: o.duration_ms,
|
|
185
|
+
output_exists: o.output_exists,
|
|
186
|
+
output_bytes: o.output_bytes,
|
|
187
|
+
mode: 'parallel',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Serial tail --------------------------------------------------------
|
|
192
|
+
const serialOutcomes: MapperOutcome[] = [];
|
|
193
|
+
for (const spec of serialSpecs) {
|
|
194
|
+
const spawnOpts: Parameters<typeof spawnMapper>[1] = {
|
|
195
|
+
budget: opts.budget,
|
|
196
|
+
maxTurns: opts.maxTurnsPerMapper,
|
|
197
|
+
cwd,
|
|
198
|
+
...(opts.runOverride !== undefined ? { runOverride: opts.runOverride } : {}),
|
|
199
|
+
};
|
|
200
|
+
const outcome = await spawnMapper(spec, spawnOpts);
|
|
201
|
+
serialOutcomes.push(outcome);
|
|
202
|
+
logger.info('explore.runner.mapper_done', {
|
|
203
|
+
mapper: outcome.name,
|
|
204
|
+
status: outcome.status,
|
|
205
|
+
duration_ms: outcome.duration_ms,
|
|
206
|
+
output_exists: outcome.output_exists,
|
|
207
|
+
output_bytes: outcome.output_bytes,
|
|
208
|
+
mode: 'serial',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Merge outcomes in ORIGINAL spec order -------------------------------
|
|
213
|
+
//
|
|
214
|
+
// Callers rely on `.mappers[i]` pairing with `opts.mappers[i]` (or
|
|
215
|
+
// DEFAULT_MAPPERS[i]). We rebuild by indexing the name→outcome map.
|
|
216
|
+
const byName: Map<string, MapperOutcome> = new Map();
|
|
217
|
+
for (const o of parallelOutcomes) byName.set(o.name, o);
|
|
218
|
+
for (const o of serialOutcomes) byName.set(o.name, o);
|
|
219
|
+
const mergedOutcomes: MapperOutcome[] = specs.map((s) => {
|
|
220
|
+
const o = byName.get(s.name);
|
|
221
|
+
if (o === undefined) {
|
|
222
|
+
// Shouldn't happen unless partitioning dropped a spec. Surface
|
|
223
|
+
// as a synthetic error outcome rather than throwing.
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
name: s.name,
|
|
226
|
+
status: 'error',
|
|
227
|
+
output_exists: false,
|
|
228
|
+
output_bytes: 0,
|
|
229
|
+
usage: { input_tokens: 0, output_tokens: 0, usd_cost: 0 },
|
|
230
|
+
duration_ms: 0,
|
|
231
|
+
error: Object.freeze({
|
|
232
|
+
code: 'PARTITION_LOST',
|
|
233
|
+
message: `mapper ${s.name} was not executed by either batch`,
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return o;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// --- Synthesizer --------------------------------------------------------
|
|
241
|
+
logger.info('explore.runner.synthesizer_started', {
|
|
242
|
+
mappers_ready: mergedOutcomes.filter((m) => m.output_exists).length,
|
|
243
|
+
mappers_total: mergedOutcomes.length,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const synthResult = await synthesizeStreaming({
|
|
247
|
+
mapperNames: specs.map((s) => s.name),
|
|
248
|
+
mapperOutputPaths: specs.map((s) => s.outputPath),
|
|
249
|
+
synthesizerPrompt: opts.synthesizerPrompt,
|
|
250
|
+
budget: opts.synthesizerBudget,
|
|
251
|
+
maxTurns: opts.synthesizerMaxTurns,
|
|
252
|
+
cwd,
|
|
253
|
+
...(opts.runOverride !== undefined ? { runOverride: opts.runOverride } : {}),
|
|
254
|
+
...(opts.pollIntervalMs !== undefined ? { pollIntervalMs: opts.pollIntervalMs } : {}),
|
|
255
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// --- Aggregate usage ----------------------------------------------------
|
|
259
|
+
let totalInput = synthResult.usage.input_tokens;
|
|
260
|
+
let totalOutput = synthResult.usage.output_tokens;
|
|
261
|
+
let totalCost = synthResult.usage.usd_cost;
|
|
262
|
+
for (const m of mergedOutcomes) {
|
|
263
|
+
totalInput += m.usage.input_tokens;
|
|
264
|
+
totalOutput += m.usage.output_tokens;
|
|
265
|
+
totalCost += m.usage.usd_cost;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
logger.info('explore.runner.completed', {
|
|
269
|
+
parallel_count: safeSpecs.length,
|
|
270
|
+
serial_count: serialSpecs.length,
|
|
271
|
+
synthesizer_status: synthResult.status,
|
|
272
|
+
total_usd_cost: totalCost,
|
|
273
|
+
total_input_tokens: totalInput,
|
|
274
|
+
total_output_tokens: totalOutput,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
mappers: Object.freeze(mergedOutcomes),
|
|
279
|
+
synthesizer: Object.freeze({
|
|
280
|
+
status: synthResult.status,
|
|
281
|
+
output_path: synthResult.output_path,
|
|
282
|
+
usage: synthResult.usage,
|
|
283
|
+
files_fed: synthResult.files_fed,
|
|
284
|
+
...(synthResult.error !== undefined ? { error: synthResult.error } : {}),
|
|
285
|
+
}),
|
|
286
|
+
parallel_count: safeSpecs.length,
|
|
287
|
+
serial_count: serialSpecs.length,
|
|
288
|
+
total_usage: {
|
|
289
|
+
input_tokens: totalInput,
|
|
290
|
+
output_tokens: totalOutput,
|
|
291
|
+
usd_cost: totalCost,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|