@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,527 @@
|
|
|
1
|
+
// scripts/lib/pipeline-runner/index.ts — Plan 21-05 Task 5 (SDK-17).
|
|
2
|
+
//
|
|
3
|
+
// The public surface of the pipeline runner. Re-exports every type
|
|
4
|
+
// and helper callers need, plus the `run()` driver.
|
|
5
|
+
//
|
|
6
|
+
// `run(config)` orchestrates the 5-stage design pipeline end to end:
|
|
7
|
+
//
|
|
8
|
+
// 1. Resolve the stage order from config (stages / resumeFrom /
|
|
9
|
+
// stopAfter / skipStages).
|
|
10
|
+
// 2. For each stage in order:
|
|
11
|
+
// a. Ask the gdd-state transition gate whether we may advance.
|
|
12
|
+
// On veto, halt with `halted-gate-veto` and surface blockers.
|
|
13
|
+
// b. Emit `stage.entered`.
|
|
14
|
+
// c. Invoke the stage via `invokeStage` (retry-once inside).
|
|
15
|
+
// d. Accumulate usage; push outcome.
|
|
16
|
+
// e. On `halted-human-gate` → dispatch the callback. `resume`
|
|
17
|
+
// re-invokes the stage with an optional payload suffix;
|
|
18
|
+
// `stop` halts pipeline with `awaiting-gate`.
|
|
19
|
+
// f. On any other `halted-*` → halt pipeline with `halted`.
|
|
20
|
+
// g. On `stage === config.stopAfter` → halt with `stopped-after`.
|
|
21
|
+
// h. Emit `stage.exited` (outcome mirrors stage status).
|
|
22
|
+
// 3. Emit `pipeline.started` at entry + `pipeline.completed` at exit.
|
|
23
|
+
//
|
|
24
|
+
// NEVER throws — every failure becomes a `PipelineResult`.
|
|
25
|
+
|
|
26
|
+
import { appendEvent } from '../event-stream/index.ts';
|
|
27
|
+
import type { BaseEvent } from '../event-stream/index.ts';
|
|
28
|
+
import { getLogger } from '../logger/index.ts';
|
|
29
|
+
import { transition as defaultTransition, TransitionGateFailed } from '../gdd-state/index.ts';
|
|
30
|
+
import { ValidationError } from '../gdd-errors/index.ts';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
STAGE_ORDER,
|
|
34
|
+
nextStage,
|
|
35
|
+
resolveStageOrder,
|
|
36
|
+
stageIndex,
|
|
37
|
+
} from './state-machine.ts';
|
|
38
|
+
import { invokeStage, type InvokeStageOverrides } from './stage-handlers.ts';
|
|
39
|
+
import { dispatchHumanGate, extractGateMarker } from './human-gate.ts';
|
|
40
|
+
import type {
|
|
41
|
+
HumanGateInfo,
|
|
42
|
+
PipelineConfig,
|
|
43
|
+
PipelineResult,
|
|
44
|
+
PipelineStatus,
|
|
45
|
+
Stage,
|
|
46
|
+
StageOutcome,
|
|
47
|
+
StageStatus,
|
|
48
|
+
} from './types.ts';
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Re-exports.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export type {
|
|
55
|
+
AgentsByStage,
|
|
56
|
+
BudgetCap,
|
|
57
|
+
HumanGateDecision,
|
|
58
|
+
HumanGateInfo,
|
|
59
|
+
PipelineConfig,
|
|
60
|
+
PipelineResult,
|
|
61
|
+
PipelineStatus,
|
|
62
|
+
Stage,
|
|
63
|
+
StageOutcome,
|
|
64
|
+
StageStatus,
|
|
65
|
+
} from './types.ts';
|
|
66
|
+
export {
|
|
67
|
+
STAGE_ORDER,
|
|
68
|
+
nextStage,
|
|
69
|
+
stageIndex,
|
|
70
|
+
resolveStageOrder,
|
|
71
|
+
} from './state-machine.ts';
|
|
72
|
+
export type { InvokeStageArgs, InvokeStageOverrides } from './stage-handlers.ts';
|
|
73
|
+
export { invokeStage } from './stage-handlers.ts';
|
|
74
|
+
export { dispatchHumanGate, extractGateMarker } from './human-gate.ts';
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Driver — `run()`.
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Result of the transition gate check for a single stage. `ok: false`
|
|
82
|
+
* surfaces blockers that the `halted-gate-veto` outcome carries.
|
|
83
|
+
*/
|
|
84
|
+
export interface TransitionResult {
|
|
85
|
+
readonly ok: boolean;
|
|
86
|
+
readonly blockers?: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Test + integration overrides for `run()`. Omitted fields fall back
|
|
91
|
+
* to the real implementations (session-runner, context-engine,
|
|
92
|
+
* tool-scoping, gdd-state transition).
|
|
93
|
+
*/
|
|
94
|
+
export interface RunOverrides extends InvokeStageOverrides {
|
|
95
|
+
/**
|
|
96
|
+
* Override the gdd-state transition gate. Defaults to a shim that
|
|
97
|
+
* invokes `gdd-state.transition(path, to)`. In test mode, returns
|
|
98
|
+
* `{ ok: true }` or `{ ok: false, blockers }`.
|
|
99
|
+
*/
|
|
100
|
+
readonly transitionStageOverride?: (to: Stage) => Promise<TransitionResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Override the state file path used by the default transition shim.
|
|
103
|
+
* Defaults to `.design/STATE.md` resolved against `config.cwd`.
|
|
104
|
+
*/
|
|
105
|
+
readonly statePathOverride?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Default transition-stage shim. Calls `gdd-state.transition(path, to)`
|
|
110
|
+
* against the working directory's `.design/STATE.md`. Maps
|
|
111
|
+
* `TransitionGateFailed` to `{ok: false, blockers}`; propagates other
|
|
112
|
+
* errors as `{ok: false, blockers: [message]}` so the pipeline never
|
|
113
|
+
* crashes on state-file hiccups.
|
|
114
|
+
*
|
|
115
|
+
* Wave C (Plan 21-09) may replace this with a direct MCP tool handler
|
|
116
|
+
* import. Until then, the shim calls the module directly.
|
|
117
|
+
*/
|
|
118
|
+
async function defaultTransitionShim(
|
|
119
|
+
to: Stage,
|
|
120
|
+
statePath: string,
|
|
121
|
+
): Promise<TransitionResult> {
|
|
122
|
+
try {
|
|
123
|
+
await defaultTransition(statePath, to);
|
|
124
|
+
return { ok: true };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof TransitionGateFailed) {
|
|
127
|
+
return { ok: false, blockers: err.blockers };
|
|
128
|
+
}
|
|
129
|
+
// Any other error (lock contention, parse error, etc.) — surface
|
|
130
|
+
// its message as a single blocker so the pipeline can halt
|
|
131
|
+
// gracefully with `halted-gate-veto`.
|
|
132
|
+
const msg: string =
|
|
133
|
+
err instanceof Error ? err.message : String(err);
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
blockers: Object.freeze([`transition failed: ${msg}`]),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the state-path used by the default transition shim.
|
|
143
|
+
*/
|
|
144
|
+
function defaultStatePath(cwd: string | undefined, override: string | undefined): string {
|
|
145
|
+
if (override !== undefined) return override;
|
|
146
|
+
const root = cwd ?? process.cwd();
|
|
147
|
+
// .design/STATE.md is the canonical Phase-20 location.
|
|
148
|
+
const sep = root.endsWith('/') || root.endsWith('\\') ? '' : '/';
|
|
149
|
+
return `${root}${sep}.design/STATE.md`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate `config` shape before entering the driver loop. Catches
|
|
154
|
+
* missing required fields early so the first stage's run doesn't
|
|
155
|
+
* proceed on a malformed config.
|
|
156
|
+
*/
|
|
157
|
+
function validateConfig(config: PipelineConfig, order: readonly Stage[]): void {
|
|
158
|
+
if (
|
|
159
|
+
config.prompts === null ||
|
|
160
|
+
config.prompts === undefined ||
|
|
161
|
+
typeof config.prompts !== 'object'
|
|
162
|
+
) {
|
|
163
|
+
throw new ValidationError(
|
|
164
|
+
'PipelineConfig.prompts is required',
|
|
165
|
+
'MISSING_PROMPTS',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
for (const s of order) {
|
|
169
|
+
const p: string | undefined = config.prompts[s];
|
|
170
|
+
if (p === undefined || p === null) {
|
|
171
|
+
throw new ValidationError(
|
|
172
|
+
`PipelineConfig.prompts["${s}"] is required (stage is in run order)`,
|
|
173
|
+
'MISSING_STAGE_PROMPT',
|
|
174
|
+
{ stage: s, order: [...order] },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!Number.isFinite(config.maxTurnsPerStage) || config.maxTurnsPerStage < 0) {
|
|
179
|
+
throw new ValidationError(
|
|
180
|
+
'PipelineConfig.maxTurnsPerStage must be a non-negative finite number',
|
|
181
|
+
'INVALID_MAX_TURNS',
|
|
182
|
+
{ value: config.maxTurnsPerStage },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const retries = config.stageRetries ?? 1;
|
|
186
|
+
if (retries !== 0 && retries !== 1) {
|
|
187
|
+
throw new ValidationError(
|
|
188
|
+
'PipelineConfig.stageRetries must be 0 or 1',
|
|
189
|
+
'INVALID_STAGE_RETRIES',
|
|
190
|
+
{ value: retries },
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Emit a pipeline-level event. Mirrors the session-runner's emit shim —
|
|
197
|
+
* persist-first, broadcast-second, silent on subscriber errors.
|
|
198
|
+
*/
|
|
199
|
+
function emitPipelineEvent(
|
|
200
|
+
type: string,
|
|
201
|
+
payload: Record<string, unknown>,
|
|
202
|
+
stage?: Stage,
|
|
203
|
+
): void {
|
|
204
|
+
const ev: BaseEvent = {
|
|
205
|
+
type,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
sessionId: `gdd-pipeline-${process.pid}`,
|
|
208
|
+
payload,
|
|
209
|
+
};
|
|
210
|
+
if (stage !== undefined) ev.stage = stage;
|
|
211
|
+
try {
|
|
212
|
+
appendEvent(ev);
|
|
213
|
+
} catch {
|
|
214
|
+
// Subscriber errors are swallowed per event-stream contract.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Accumulate `session.usage` onto the running total. Missing sessions
|
|
220
|
+
* (skipped stages) contribute zero.
|
|
221
|
+
*/
|
|
222
|
+
function foldUsage(
|
|
223
|
+
total: { input_tokens: number; output_tokens: number; usd_cost: number },
|
|
224
|
+
outcome: StageOutcome,
|
|
225
|
+
): void {
|
|
226
|
+
const u = outcome.session?.usage;
|
|
227
|
+
if (u === undefined) return;
|
|
228
|
+
total.input_tokens += u.input_tokens;
|
|
229
|
+
total.output_tokens += u.output_tokens;
|
|
230
|
+
total.usd_cost += u.usd_cost;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Drive the full or partial pipeline. Returns once a terminal state is
|
|
235
|
+
* reached: `completed`, `halted`, `stopped-after`, or `awaiting-gate`.
|
|
236
|
+
*
|
|
237
|
+
* Never throws — all failures land on `PipelineResult`.
|
|
238
|
+
*/
|
|
239
|
+
export async function run(
|
|
240
|
+
config: PipelineConfig,
|
|
241
|
+
overrides: RunOverrides = {},
|
|
242
|
+
): Promise<PipelineResult> {
|
|
243
|
+
const cycle_start: string = new Date().toISOString();
|
|
244
|
+
const total: { input_tokens: number; output_tokens: number; usd_cost: number } = {
|
|
245
|
+
input_tokens: 0,
|
|
246
|
+
output_tokens: 0,
|
|
247
|
+
usd_cost: 0,
|
|
248
|
+
};
|
|
249
|
+
const outcomes: StageOutcome[] = [];
|
|
250
|
+
let pipelineStatus: PipelineStatus = 'completed';
|
|
251
|
+
let halted_at: Stage | undefined;
|
|
252
|
+
let finalGate: HumanGateInfo | undefined;
|
|
253
|
+
|
|
254
|
+
// 1. Resolve the stage order (may throw on invalid config).
|
|
255
|
+
let order: readonly Stage[];
|
|
256
|
+
try {
|
|
257
|
+
order = resolveStageOrder({
|
|
258
|
+
...(config.stages !== undefined ? { stages: config.stages } : {}),
|
|
259
|
+
...(config.skipStages !== undefined ? { skipStages: config.skipStages } : {}),
|
|
260
|
+
...(config.resumeFrom !== undefined ? { resumeFrom: config.resumeFrom } : {}),
|
|
261
|
+
...(config.stopAfter !== undefined ? { stopAfter: config.stopAfter } : {}),
|
|
262
|
+
});
|
|
263
|
+
validateConfig(config, order);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Convert to a halted pipeline with no outcomes — driver's
|
|
266
|
+
// no-throw contract.
|
|
267
|
+
try {
|
|
268
|
+
getLogger().error('pipeline.invalid_config', {
|
|
269
|
+
error: err instanceof Error ? err.message : String(err),
|
|
270
|
+
});
|
|
271
|
+
} catch {
|
|
272
|
+
// Ignore logger failures.
|
|
273
|
+
}
|
|
274
|
+
const cycle_end = new Date().toISOString();
|
|
275
|
+
return {
|
|
276
|
+
status: 'halted',
|
|
277
|
+
cycle_start,
|
|
278
|
+
cycle_end,
|
|
279
|
+
outcomes: [],
|
|
280
|
+
total_usage: { ...total },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2. Emit pipeline.started.
|
|
285
|
+
emitPipelineEvent('pipeline.started', {
|
|
286
|
+
stages: [...order],
|
|
287
|
+
budget: { ...config.budget },
|
|
288
|
+
maxTurnsPerStage: config.maxTurnsPerStage,
|
|
289
|
+
});
|
|
290
|
+
try {
|
|
291
|
+
getLogger().info('pipeline.started', {
|
|
292
|
+
stages: [...order],
|
|
293
|
+
stageRetries: config.stageRetries ?? 1,
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
// Ignore logger failures.
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const transitionImpl =
|
|
300
|
+
overrides.transitionStageOverride ??
|
|
301
|
+
((to: Stage) =>
|
|
302
|
+
defaultTransitionShim(to, defaultStatePath(config.cwd, overrides.statePathOverride)));
|
|
303
|
+
|
|
304
|
+
// 3. Drive the stage loop.
|
|
305
|
+
stageLoop: for (const stage of order) {
|
|
306
|
+
// 3a. Transition gate.
|
|
307
|
+
let gate: TransitionResult;
|
|
308
|
+
try {
|
|
309
|
+
gate = await transitionImpl(stage);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
// The override contract says "return, never throw"; if it does
|
|
312
|
+
// throw, treat it as an implicit veto with the message as the
|
|
313
|
+
// blocker.
|
|
314
|
+
const msg: string = err instanceof Error ? err.message : String(err);
|
|
315
|
+
gate = { ok: false, blockers: Object.freeze([msg]) };
|
|
316
|
+
}
|
|
317
|
+
if (!gate.ok) {
|
|
318
|
+
const blockers: readonly string[] = gate.blockers ?? [];
|
|
319
|
+
const outcome: StageOutcome = {
|
|
320
|
+
stage,
|
|
321
|
+
status: 'halted-gate-veto',
|
|
322
|
+
blockers,
|
|
323
|
+
started_at: new Date().toISOString(),
|
|
324
|
+
ended_at: new Date().toISOString(),
|
|
325
|
+
retries: 0,
|
|
326
|
+
};
|
|
327
|
+
outcomes.push(outcome);
|
|
328
|
+
pipelineStatus = 'halted';
|
|
329
|
+
halted_at = stage;
|
|
330
|
+
emitPipelineEvent('stage.entered', { stage }, stage);
|
|
331
|
+
emitPipelineEvent(
|
|
332
|
+
'stage.exited',
|
|
333
|
+
{
|
|
334
|
+
stage,
|
|
335
|
+
duration_ms: 0,
|
|
336
|
+
outcome: 'halted',
|
|
337
|
+
},
|
|
338
|
+
stage,
|
|
339
|
+
);
|
|
340
|
+
try {
|
|
341
|
+
getLogger().warn('pipeline.halted', {
|
|
342
|
+
stage,
|
|
343
|
+
reason: 'gate-veto',
|
|
344
|
+
blockers: [...blockers],
|
|
345
|
+
});
|
|
346
|
+
} catch {
|
|
347
|
+
// Ignore logger failures.
|
|
348
|
+
}
|
|
349
|
+
break stageLoop;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 3b. Emit stage.entered.
|
|
353
|
+
emitPipelineEvent('stage.entered', { stage }, stage);
|
|
354
|
+
|
|
355
|
+
// 3c. Run the stage.
|
|
356
|
+
let outcome: StageOutcome = await invokeStage({
|
|
357
|
+
stage,
|
|
358
|
+
config,
|
|
359
|
+
retries: config.stageRetries ?? 1,
|
|
360
|
+
...(overrides.runOverride !== undefined ? { runOverride: overrides.runOverride } : {}),
|
|
361
|
+
...(overrides.bundleOverride !== undefined
|
|
362
|
+
? { bundleOverride: overrides.bundleOverride }
|
|
363
|
+
: {}),
|
|
364
|
+
...(overrides.scopeOverride !== undefined
|
|
365
|
+
? { scopeOverride: overrides.scopeOverride }
|
|
366
|
+
: {}),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// 3d. Human-gate resolution.
|
|
370
|
+
if (outcome.status === 'halted-human-gate') {
|
|
371
|
+
const gateInfo: HumanGateInfo = buildGateInfo(stage, outcome);
|
|
372
|
+
const decision = await dispatchHumanGate(gateInfo, config);
|
|
373
|
+
if (decision.decision === 'resume') {
|
|
374
|
+
// Re-invoke with the payload suffix — replaces the first outcome.
|
|
375
|
+
const resumed: StageOutcome = await invokeStage({
|
|
376
|
+
stage,
|
|
377
|
+
config,
|
|
378
|
+
retries: config.stageRetries ?? 1,
|
|
379
|
+
...(decision.payload !== undefined ? { _promptSuffix: decision.payload } : {}),
|
|
380
|
+
...(overrides.runOverride !== undefined ? { runOverride: overrides.runOverride } : {}),
|
|
381
|
+
...(overrides.bundleOverride !== undefined
|
|
382
|
+
? { bundleOverride: overrides.bundleOverride }
|
|
383
|
+
: {}),
|
|
384
|
+
...(overrides.scopeOverride !== undefined
|
|
385
|
+
? { scopeOverride: overrides.scopeOverride }
|
|
386
|
+
: {}),
|
|
387
|
+
});
|
|
388
|
+
outcome = resumed;
|
|
389
|
+
} else {
|
|
390
|
+
// Stop — record outcome, halt pipeline with awaiting-gate.
|
|
391
|
+
outcomes.push(outcome);
|
|
392
|
+
foldUsage(total, outcome);
|
|
393
|
+
pipelineStatus = 'awaiting-gate';
|
|
394
|
+
finalGate = gateInfo;
|
|
395
|
+
emitPipelineEvent(
|
|
396
|
+
'stage.exited',
|
|
397
|
+
{
|
|
398
|
+
stage,
|
|
399
|
+
duration_ms: durationMs(outcome),
|
|
400
|
+
outcome: 'halted',
|
|
401
|
+
},
|
|
402
|
+
stage,
|
|
403
|
+
);
|
|
404
|
+
try {
|
|
405
|
+
getLogger().info('pipeline.awaiting_gate', {
|
|
406
|
+
stage,
|
|
407
|
+
gateName: gateInfo.gateName,
|
|
408
|
+
});
|
|
409
|
+
} catch {
|
|
410
|
+
// Ignore logger failures.
|
|
411
|
+
}
|
|
412
|
+
break stageLoop;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 3e. Accumulate + record.
|
|
417
|
+
outcomes.push(outcome);
|
|
418
|
+
foldUsage(total, outcome);
|
|
419
|
+
|
|
420
|
+
// 3f. Emit stage.exited.
|
|
421
|
+
emitPipelineEvent(
|
|
422
|
+
'stage.exited',
|
|
423
|
+
{
|
|
424
|
+
stage,
|
|
425
|
+
duration_ms: durationMs(outcome),
|
|
426
|
+
outcome: mapOutcomeLabel(outcome.status),
|
|
427
|
+
},
|
|
428
|
+
stage,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// 3g. Non-gate halt?
|
|
432
|
+
if (isHaltingStatus(outcome.status)) {
|
|
433
|
+
pipelineStatus = 'halted';
|
|
434
|
+
halted_at = stage;
|
|
435
|
+
try {
|
|
436
|
+
getLogger().warn('pipeline.halted', {
|
|
437
|
+
stage,
|
|
438
|
+
status: outcome.status,
|
|
439
|
+
});
|
|
440
|
+
} catch {
|
|
441
|
+
// Ignore logger failures.
|
|
442
|
+
}
|
|
443
|
+
break stageLoop;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 3h. stopAfter boundary?
|
|
447
|
+
if (config.stopAfter !== undefined && stage === config.stopAfter) {
|
|
448
|
+
pipelineStatus = 'stopped-after';
|
|
449
|
+
break stageLoop;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const cycle_end: string = new Date().toISOString();
|
|
454
|
+
|
|
455
|
+
const result: PipelineResult = {
|
|
456
|
+
status: pipelineStatus,
|
|
457
|
+
cycle_start,
|
|
458
|
+
cycle_end,
|
|
459
|
+
outcomes: Object.freeze([...outcomes]),
|
|
460
|
+
total_usage: { ...total },
|
|
461
|
+
...(halted_at !== undefined ? { halted_at } : {}),
|
|
462
|
+
...(finalGate !== undefined ? { gate: finalGate } : {}),
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
emitPipelineEvent('pipeline.completed', {
|
|
466
|
+
status: result.status,
|
|
467
|
+
outcomes_count: outcomes.length,
|
|
468
|
+
total_usage: { ...total },
|
|
469
|
+
...(halted_at !== undefined ? { halted_at } : {}),
|
|
470
|
+
});
|
|
471
|
+
try {
|
|
472
|
+
getLogger().info('pipeline.completed', {
|
|
473
|
+
status: result.status,
|
|
474
|
+
outcomes_count: outcomes.length,
|
|
475
|
+
total_cost_usd: total.usd_cost,
|
|
476
|
+
...(halted_at !== undefined ? { halted_at } : {}),
|
|
477
|
+
});
|
|
478
|
+
} catch {
|
|
479
|
+
// Ignore logger failures.
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Build a HumanGateInfo from a stage outcome whose status is
|
|
487
|
+
* `halted-human-gate`. Falls back to re-extracting the gate marker
|
|
488
|
+
* from `session.final_text` if `outcome.gate` is missing (defensive).
|
|
489
|
+
*/
|
|
490
|
+
function buildGateInfo(stage: Stage, outcome: StageOutcome): HumanGateInfo {
|
|
491
|
+
if (outcome.gate !== undefined) return outcome.gate;
|
|
492
|
+
const finalText: string = outcome.session?.final_text ?? '';
|
|
493
|
+
const marker = extractGateMarker(finalText);
|
|
494
|
+
return {
|
|
495
|
+
stage,
|
|
496
|
+
gateName: marker?.name ?? 'unnamed',
|
|
497
|
+
stdoutTail: finalText,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Elapsed wall-clock ms between `started_at` and `ended_at`. */
|
|
502
|
+
function durationMs(outcome: StageOutcome): number {
|
|
503
|
+
if (outcome.started_at === undefined || outcome.ended_at === undefined) {
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
const s = Date.parse(outcome.started_at);
|
|
507
|
+
const e = Date.parse(outcome.ended_at);
|
|
508
|
+
if (!Number.isFinite(s) || !Number.isFinite(e)) return 0;
|
|
509
|
+
return Math.max(0, e - s);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Map a stage status into the event-stream `StageExitedEvent` outcome label. */
|
|
513
|
+
function mapOutcomeLabel(status: StageStatus): 'pass' | 'fail' | 'halted' {
|
|
514
|
+
if (status === 'completed') return 'pass';
|
|
515
|
+
if (status === 'skipped') return 'pass';
|
|
516
|
+
return 'halted';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** True when `status` is a terminal halt (non-human-gate, non-completed). */
|
|
520
|
+
function isHaltingStatus(status: StageStatus): boolean {
|
|
521
|
+
return (
|
|
522
|
+
status === 'halted-gate-veto' ||
|
|
523
|
+
status === 'halted-budget' ||
|
|
524
|
+
status === 'halted-turn-cap' ||
|
|
525
|
+
status === 'halted-error'
|
|
526
|
+
);
|
|
527
|
+
}
|