@delegance/claude-autopilot 5.2.2 → 6.2.2
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/CHANGELOG.md +1027 -1
- package/README.md +104 -17
- package/dist/src/adapters/council/claude.js +2 -1
- package/dist/src/adapters/council/openai.js +14 -7
- package/dist/src/adapters/deploy/_http.d.ts +43 -0
- package/dist/src/adapters/deploy/_http.js +99 -0
- package/dist/src/adapters/deploy/fly.d.ts +206 -0
- package/dist/src/adapters/deploy/fly.js +696 -0
- package/dist/src/adapters/deploy/generic.d.ts +39 -0
- package/dist/src/adapters/deploy/generic.js +98 -0
- package/dist/src/adapters/deploy/index.d.ts +15 -0
- package/dist/src/adapters/deploy/index.js +78 -0
- package/dist/src/adapters/deploy/render.d.ts +181 -0
- package/dist/src/adapters/deploy/render.js +550 -0
- package/dist/src/adapters/deploy/types.d.ts +221 -0
- package/dist/src/adapters/deploy/types.js +15 -0
- package/dist/src/adapters/deploy/vercel.d.ts +143 -0
- package/dist/src/adapters/deploy/vercel.js +426 -0
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/claude.js +2 -1
- package/dist/src/adapters/review-engine/codex.js +12 -8
- package/dist/src/adapters/review-engine/gemini.js +2 -1
- package/dist/src/adapters/review-engine/openai-compatible.js +2 -1
- package/dist/src/adapters/sdk-loader.d.ts +15 -0
- package/dist/src/adapters/sdk-loader.js +77 -0
- package/dist/src/cli/autopilot.d.ts +71 -0
- package/dist/src/cli/autopilot.js +735 -0
- package/dist/src/cli/brainstorm.d.ts +23 -0
- package/dist/src/cli/brainstorm.js +131 -0
- package/dist/src/cli/costs.d.ts +15 -1
- package/dist/src/cli/costs.js +99 -10
- package/dist/src/cli/deploy.d.ts +71 -0
- package/dist/src/cli/deploy.js +539 -0
- package/dist/src/cli/fix.d.ts +18 -0
- package/dist/src/cli/fix.js +105 -11
- package/dist/src/cli/help-text.d.ts +52 -0
- package/dist/src/cli/help-text.js +400 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.js +784 -222
- package/dist/src/cli/json-envelope.d.ts +187 -0
- package/dist/src/cli/json-envelope.js +270 -0
- package/dist/src/cli/json-mode.d.ts +33 -0
- package/dist/src/cli/json-mode.js +201 -0
- package/dist/src/cli/migrate.d.ts +111 -0
- package/dist/src/cli/migrate.js +305 -0
- package/dist/src/cli/plan.d.ts +81 -0
- package/dist/src/cli/plan.js +149 -0
- package/dist/src/cli/pr.d.ts +106 -0
- package/dist/src/cli/pr.js +191 -19
- package/dist/src/cli/preflight.js +102 -1
- package/dist/src/cli/review.d.ts +27 -0
- package/dist/src/cli/review.js +126 -0
- package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
- package/dist/src/cli/runs-watch-renderer.js +275 -0
- package/dist/src/cli/runs-watch.d.ts +41 -0
- package/dist/src/cli/runs-watch.js +395 -0
- package/dist/src/cli/runs.d.ts +122 -0
- package/dist/src/cli/runs.js +902 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/spec.d.ts +66 -0
- package/dist/src/cli/spec.js +132 -0
- package/dist/src/cli/validate.d.ts +29 -0
- package/dist/src/cli/validate.js +131 -0
- package/dist/src/core/config/schema.d.ts +43 -0
- package/dist/src/core/config/schema.js +25 -0
- package/dist/src/core/config/types.d.ts +17 -0
- package/dist/src/core/council/runner.d.ts +10 -1
- package/dist/src/core/council/runner.js +25 -3
- package/dist/src/core/council/types.d.ts +7 -0
- package/dist/src/core/errors.d.ts +1 -1
- package/dist/src/core/errors.js +12 -0
- package/dist/src/core/logging/redaction.d.ts +13 -0
- package/dist/src/core/logging/redaction.js +20 -0
- package/dist/src/core/migrate/detector-rules.js +6 -0
- package/dist/src/core/migrate/schema-validator.js +22 -1
- package/dist/src/core/phases/static-rules.d.ts +5 -1
- package/dist/src/core/phases/static-rules.js +2 -5
- package/dist/src/core/run-state/budget.d.ts +88 -0
- package/dist/src/core/run-state/budget.js +141 -0
- package/dist/src/core/run-state/cli-internal.d.ts +21 -0
- package/dist/src/core/run-state/cli-internal.js +174 -0
- package/dist/src/core/run-state/events.d.ts +59 -0
- package/dist/src/core/run-state/events.js +504 -0
- package/dist/src/core/run-state/lock.d.ts +61 -0
- package/dist/src/core/run-state/lock.js +206 -0
- package/dist/src/core/run-state/phase-context.d.ts +60 -0
- package/dist/src/core/run-state/phase-context.js +108 -0
- package/dist/src/core/run-state/phase-registry.d.ts +137 -0
- package/dist/src/core/run-state/phase-registry.js +162 -0
- package/dist/src/core/run-state/phase-runner.d.ts +80 -0
- package/dist/src/core/run-state/phase-runner.js +447 -0
- package/dist/src/core/run-state/provider-readback.d.ts +130 -0
- package/dist/src/core/run-state/provider-readback.js +426 -0
- package/dist/src/core/run-state/replay-decision.d.ts +69 -0
- package/dist/src/core/run-state/replay-decision.js +144 -0
- package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
- package/dist/src/core/run-state/resolve-engine.js +190 -0
- package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
- package/dist/src/core/run-state/resume-preflight.js +116 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
- package/dist/src/core/run-state/runs.d.ts +57 -0
- package/dist/src/core/run-state/runs.js +288 -0
- package/dist/src/core/run-state/snapshot.d.ts +14 -0
- package/dist/src/core/run-state/snapshot.js +114 -0
- package/dist/src/core/run-state/state.d.ts +40 -0
- package/dist/src/core/run-state/state.js +164 -0
- package/dist/src/core/run-state/types.d.ts +278 -0
- package/dist/src/core/run-state/types.js +13 -0
- package/dist/src/core/run-state/ulid.d.ts +11 -0
- package/dist/src/core/run-state/ulid.js +95 -0
- package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
- package/dist/src/core/schema-alignment/extractor/index.js +2 -2
- package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
- package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
- package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
- package/dist/src/core/schema-alignment/git-history.js +53 -0
- package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
- package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
- package/package.json +9 -5
- package/scripts/autoregress.ts +3 -2
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- package/skills/migrate/SKILL.md +193 -47
- package/skills/simplify-ui/SKILL.md +103 -0
- package/skills/ui/SKILL.md +117 -0
- package/skills/ui-ux-pro-max/SKILL.md +90 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
// src/cli/autopilot.ts
|
|
2
|
+
//
|
|
3
|
+
// v6.2.0 — multi-phase orchestrator. Drives N pipeline phases under ONE
|
|
4
|
+
// runId so a `runs watch <id>` window covers the whole pipeline (vs the
|
|
5
|
+
// pre-v6.2 chain where every CLI verb owned its own runId).
|
|
6
|
+
//
|
|
7
|
+
// Lifecycle (per spec docs/specs/v6.2-multi-phase-orchestrator.md):
|
|
8
|
+
//
|
|
9
|
+
// createRun({ phases: [allPhaseNames] })
|
|
10
|
+
// for each phase in phases:
|
|
11
|
+
// buildPhase(deps) → { phase: RunPhase<I,O>, input: I, renderResult }
|
|
12
|
+
// runPhase(phase, input, { runDir, runId, writerId, phaseIdx, budget })
|
|
13
|
+
// catch failure → record + exit
|
|
14
|
+
// emit run.complete (success | failed) ONCE
|
|
15
|
+
// refresh state.json snapshot
|
|
16
|
+
// release lock in finally
|
|
17
|
+
//
|
|
18
|
+
// What this verb deliberately does NOT do (out-of-scope for v6.2.0):
|
|
19
|
+
// - migrate / pr (v6.2.1, gated on per-phase idempotency contracts)
|
|
20
|
+
// - --mode=fix / --mode=review (v6.2.1+)
|
|
21
|
+
// - --json envelope (v6.2.2)
|
|
22
|
+
// - parallel phases (reserved indefinitely — pipelines are sequential)
|
|
23
|
+
// - interactive prompts (the verb is non-interactive by design)
|
|
24
|
+
//
|
|
25
|
+
// Engine-on REQUIRED: the orchestrator throws `invalid_config` exit 1 if
|
|
26
|
+
// the user explicitly disables the engine (`--no-engine`,
|
|
27
|
+
// `CLAUDE_AUTOPILOT_ENGINE=off|false|0|no`, or `engine.enabled: false` in
|
|
28
|
+
// config). v6.1 made engine-on the default; orchestrator runs cannot exist
|
|
29
|
+
// without a run dir so the opt-out is rejected here at pre-flight.
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import { loadConfig } from "../core/config/loader.js";
|
|
33
|
+
import { GuardrailError } from "../core/errors.js";
|
|
34
|
+
import { PHASE_REGISTRY, DEFAULT_FULL_PHASES, validatePhaseNames, } from "../core/run-state/phase-registry.js";
|
|
35
|
+
import { createRun } from "../core/run-state/runs.js";
|
|
36
|
+
import { runPhase } from "../core/run-state/phase-runner.js";
|
|
37
|
+
import { appendEvent, replayState } from "../core/run-state/events.js";
|
|
38
|
+
import { writeStateSnapshot } from "../core/run-state/state.js";
|
|
39
|
+
import { resolveEngineEnabled, } from "../core/run-state/resolve-engine.js";
|
|
40
|
+
import { resumePreflight, } from "../core/run-state/resume-preflight.js";
|
|
41
|
+
import { AUTOPILOT_ERROR_CODES, computeAutopilotExitCode, writeAutopilotEnvelope, __isAutopilotEnvelopeWritten, } from "./json-envelope.js";
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// ANSI codes — kept inline to match the rest of cli/ (no shared formatter).
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
const ANSI_RESET = '\x1b[0m';
|
|
46
|
+
const ANSI_BOLD = '\x1b[1m';
|
|
47
|
+
const ANSI_DIM = '\x1b[2m';
|
|
48
|
+
const ANSI_RED = '\x1b[31m';
|
|
49
|
+
const ANSI_GREEN = '\x1b[32m';
|
|
50
|
+
const ANSI_CYAN = '\x1b[36m';
|
|
51
|
+
const fmt = (color, text) => `${color}${text}${ANSI_RESET}`;
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// runAutopilot — entry point
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export async function runAutopilot(options = {}) {
|
|
56
|
+
const cwd = options.cwd ?? process.cwd();
|
|
57
|
+
const silent = options.__silent === true;
|
|
58
|
+
const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
|
|
59
|
+
const startedAt = Date.now();
|
|
60
|
+
// --- Pre-flight 1: load config -----------------------------------------
|
|
61
|
+
let config = { configVersion: 1 };
|
|
62
|
+
if (fs.existsSync(configPath)) {
|
|
63
|
+
const loaded = await loadConfig(configPath);
|
|
64
|
+
if (loaded)
|
|
65
|
+
config = loaded;
|
|
66
|
+
}
|
|
67
|
+
// --- Pre-flight 2: engine-on REQUIRED (per spec) -----------------------
|
|
68
|
+
// The orchestrator cannot operate without a run dir; engine-off is
|
|
69
|
+
// rejected here before any side effects.
|
|
70
|
+
const engineResolved = resolveEngineEnabled({
|
|
71
|
+
...(options.cliEngine !== undefined ? { cliEngine: options.cliEngine } : {}),
|
|
72
|
+
...(options.envEngine !== undefined ? { envValue: options.envEngine } : {}),
|
|
73
|
+
...(typeof config.engine?.enabled === 'boolean'
|
|
74
|
+
? { configEnabled: config.engine.enabled }
|
|
75
|
+
: {}),
|
|
76
|
+
});
|
|
77
|
+
if (!engineResolved.enabled) {
|
|
78
|
+
if (!silent) {
|
|
79
|
+
process.stderr.write(fmt(ANSI_RED, `[autopilot] invalid_config: orchestrator requires the v6 engine but it's disabled (${engineResolved.reason})\n`));
|
|
80
|
+
process.stderr.write(fmt(ANSI_DIM, ` hint: drop --no-engine, unset CLAUDE_AUTOPILOT_ENGINE=off, or set engine.enabled: true in guardrail.config.yaml\n`));
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
runId: null,
|
|
84
|
+
runDir: null,
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
errorCode: 'invalid_config',
|
|
87
|
+
errorMessage: `orchestrator requires engine-on (${engineResolved.reason})`,
|
|
88
|
+
phases: [],
|
|
89
|
+
totalCostUSD: 0,
|
|
90
|
+
durationMs: Date.now() - startedAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// --- Pre-flight 3: resolve phase list ----------------------------------
|
|
94
|
+
let phaseNames;
|
|
95
|
+
if (options.phases && options.phases.length > 0) {
|
|
96
|
+
phaseNames = options.phases;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const mode = options.mode ?? 'full';
|
|
100
|
+
if (mode === 'full') {
|
|
101
|
+
phaseNames = DEFAULT_FULL_PHASES;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Unreachable today (the type only allows 'full'), but kept for
|
|
105
|
+
// forward-compat — `--mode=fix|review` lands in v6.2.1+.
|
|
106
|
+
if (!silent) {
|
|
107
|
+
process.stderr.write(fmt(ANSI_RED, `[autopilot] invalid_config: unknown mode "${mode}"\n`));
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
runId: null,
|
|
111
|
+
runDir: null,
|
|
112
|
+
exitCode: 1,
|
|
113
|
+
errorCode: 'invalid_config',
|
|
114
|
+
errorMessage: `unknown mode "${mode}"`,
|
|
115
|
+
phases: [],
|
|
116
|
+
totalCostUSD: 0,
|
|
117
|
+
durationMs: Date.now() - startedAt,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const validation = validatePhaseNames(phaseNames);
|
|
122
|
+
if (!validation.ok) {
|
|
123
|
+
if (!silent) {
|
|
124
|
+
process.stderr.write(fmt(ANSI_RED, `[autopilot] invalid_config: unknown phase(s): ${validation.unknown.join(', ')}\n`));
|
|
125
|
+
process.stderr.write(fmt(ANSI_DIM, ` registered: ${Object.keys(PHASE_REGISTRY).join(', ')}\n`));
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
runId: null,
|
|
129
|
+
runDir: null,
|
|
130
|
+
exitCode: 1,
|
|
131
|
+
errorCode: 'invalid_config',
|
|
132
|
+
errorMessage: `unknown phase(s): ${validation.unknown.join(', ')}`,
|
|
133
|
+
phases: [],
|
|
134
|
+
totalCostUSD: 0,
|
|
135
|
+
durationMs: Date.now() - startedAt,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// --- Build the BudgetConfig (run-scope) --------------------------------
|
|
139
|
+
// v6.2.0 — when --budget is passed, every phase gets the same
|
|
140
|
+
// `BudgetConfig` with `scope: 'run'` so the cap accumulates across
|
|
141
|
+
// phases. Per spec WARNING #2 (codex review).
|
|
142
|
+
const budget = options.budgetUSD !== undefined
|
|
143
|
+
? { perRunUSD: options.budgetUSD, scope: 'run' }
|
|
144
|
+
: undefined;
|
|
145
|
+
// --- Create the run ----------------------------------------------------
|
|
146
|
+
// ONE run dir, ONE runId, phases laid out at creation time so each
|
|
147
|
+
// `runPhase` call uses the matching phaseIdx.
|
|
148
|
+
const created = await createRun({
|
|
149
|
+
cwd,
|
|
150
|
+
phases: [...phaseNames],
|
|
151
|
+
config: {
|
|
152
|
+
engine: { enabled: true, source: engineResolved.source },
|
|
153
|
+
mode: options.mode ?? (options.phases ? 'phases' : 'full'),
|
|
154
|
+
...(options.budgetUSD !== undefined ? { budgetUSD: options.budgetUSD } : {}),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (!silent) {
|
|
158
|
+
const budgetSuffix = options.budgetUSD !== undefined ? ` budget=$${options.budgetUSD.toFixed(2)}` : '';
|
|
159
|
+
process.stdout.write(fmt(ANSI_BOLD, `[autopilot]`) + ` runId=${created.runId}${budgetSuffix}\n`);
|
|
160
|
+
}
|
|
161
|
+
const phaseSummaries = phaseNames.map(name => ({
|
|
162
|
+
name,
|
|
163
|
+
status: 'not-run',
|
|
164
|
+
costUSD: 0,
|
|
165
|
+
durationMs: 0,
|
|
166
|
+
}));
|
|
167
|
+
// --- Run each phase ---------------------------------------------------
|
|
168
|
+
let failedAtPhase = null;
|
|
169
|
+
let failedPhaseName = null;
|
|
170
|
+
let phaseErrorCode;
|
|
171
|
+
let phaseErrorMessage;
|
|
172
|
+
try {
|
|
173
|
+
for (let phaseIdx = 0; phaseIdx < phaseNames.length; phaseIdx++) {
|
|
174
|
+
const name = phaseNames[phaseIdx];
|
|
175
|
+
const entry = PHASE_REGISTRY[name];
|
|
176
|
+
if (!silent) {
|
|
177
|
+
process.stdout.write(fmt(ANSI_BOLD, `[autopilot]`) +
|
|
178
|
+
` phase ${phaseIdx + 1}/${phaseNames.length}: ${name}\n`);
|
|
179
|
+
}
|
|
180
|
+
const phaseStartedAt = Date.now();
|
|
181
|
+
// Each builder takes its own option shape; v6.2.0's registered phases
|
|
182
|
+
// all accept an empty `{}` because the orchestrator runs them with
|
|
183
|
+
// their default option values (cwd is inherited via process.cwd()
|
|
184
|
+
// → builder default). Per-phase options arrive in v6.2.1+ via the
|
|
185
|
+
// `--phase-args` JSON envelope; v6.2.0 keeps the orchestrator
|
|
186
|
+
// simple-by-design.
|
|
187
|
+
// Pass cwd explicitly so the registered builders create their phase
|
|
188
|
+
// input pointed at the orchestrator's run dir, not whatever the
|
|
189
|
+
// process happened to launch from. Each builder's command-options
|
|
190
|
+
// type accepts `cwd` (see scan.ts / spec.ts / plan.ts / implement.ts).
|
|
191
|
+
// The `as never` is unavoidable here: PHASE_REGISTRY's keys are
|
|
192
|
+
// heterogeneous and the type system can't narrow `entry.build` to a
|
|
193
|
+
// single signature when `name` is the literal union. Each registered
|
|
194
|
+
// builder accepts `{ cwd }` so the runtime call is correct.
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
+
const built = await entry.build({
|
|
197
|
+
cwd,
|
|
198
|
+
});
|
|
199
|
+
if (built.kind === 'early-exit') {
|
|
200
|
+
// A phase pre-flight bailed before producing a RunPhase. We treat
|
|
201
|
+
// a non-zero early-exit as a phase failure (the verb decided it
|
|
202
|
+
// can't proceed); 0 means "nothing to do" and we record skipped
|
|
203
|
+
// and continue. Today's registered phases never produce a
|
|
204
|
+
// non-zero early-exit on the orchestrator's `{ cwd }` shape, but
|
|
205
|
+
// we honor the contract for forward-compat.
|
|
206
|
+
if (built.exitCode === 0) {
|
|
207
|
+
phaseSummaries[phaseIdx] = {
|
|
208
|
+
name,
|
|
209
|
+
status: 'skipped',
|
|
210
|
+
costUSD: 0,
|
|
211
|
+
durationMs: Date.now() - phaseStartedAt,
|
|
212
|
+
};
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
failedAtPhase = phaseIdx;
|
|
216
|
+
failedPhaseName = name;
|
|
217
|
+
phaseErrorCode = 'invalid_config';
|
|
218
|
+
phaseErrorMessage = `phase "${name}" pre-flight refused (exit ${built.exitCode})`;
|
|
219
|
+
phaseSummaries[phaseIdx] = {
|
|
220
|
+
name,
|
|
221
|
+
status: 'failed',
|
|
222
|
+
errorCode: 'invalid_config',
|
|
223
|
+
errorMessage: phaseErrorMessage,
|
|
224
|
+
costUSD: 0,
|
|
225
|
+
durationMs: Date.now() - phaseStartedAt,
|
|
226
|
+
};
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
// v6.2.1 — resume preflight for side-effecting phases. Reads any
|
|
230
|
+
// prior phase.success + persisted externalRefs out of events.ndjson
|
|
231
|
+
// and routes per the spec decision matrix BEFORE invoking runPhase.
|
|
232
|
+
// For a fresh run (no prior events for this phaseIdx) the preflight
|
|
233
|
+
// returns `proceed-fresh` and the orchestrator falls through to the
|
|
234
|
+
// normal phase invocation below. For a resumed run, the matrix can
|
|
235
|
+
// short-circuit to skip-already-applied or escalate to needs-human.
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
237
|
+
const phaseRunPhase = built.phase;
|
|
238
|
+
if (phaseRunPhase.hasSideEffects === true) {
|
|
239
|
+
const preEffect = entry.preEffectRefKinds ?? [];
|
|
240
|
+
const postEffect = entry.postEffectRefKinds ?? [];
|
|
241
|
+
const prior = collectPriorPhaseState(created.runDir, name, phaseIdx);
|
|
242
|
+
const decision = await resumePreflight({
|
|
243
|
+
preEffectRefKinds: preEffect,
|
|
244
|
+
postEffectRefKinds: postEffect,
|
|
245
|
+
priorPhaseSuccess: prior.priorPhaseSuccess,
|
|
246
|
+
priorRefs: prior.priorRefs,
|
|
247
|
+
});
|
|
248
|
+
const handled = await applyResumeDecision({
|
|
249
|
+
decision,
|
|
250
|
+
runDir: created.runDir,
|
|
251
|
+
runId: created.runId,
|
|
252
|
+
writerId: created.lock.writerId,
|
|
253
|
+
phaseName: name,
|
|
254
|
+
phaseIdx,
|
|
255
|
+
phaseStartedAt,
|
|
256
|
+
phaseSummaries,
|
|
257
|
+
});
|
|
258
|
+
if (handled === 'skipped')
|
|
259
|
+
continue;
|
|
260
|
+
if (handled === 'failed') {
|
|
261
|
+
failedAtPhase = phaseIdx;
|
|
262
|
+
failedPhaseName = name;
|
|
263
|
+
phaseErrorCode = 'needs_human';
|
|
264
|
+
phaseErrorMessage = decision.kind === 'needs-human'
|
|
265
|
+
? `resume preflight refused (${decision.reason})`
|
|
266
|
+
: 'resume preflight refused';
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
// 'proceed' — fall through to the normal runPhase invocation.
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
273
|
+
const output = await runPhase(
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
275
|
+
built.phase,
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
|
+
built.input, {
|
|
278
|
+
runDir: created.runDir,
|
|
279
|
+
runId: created.runId,
|
|
280
|
+
writerId: created.lock.writerId,
|
|
281
|
+
phaseIdx,
|
|
282
|
+
...(budget !== undefined ? { budget } : {}),
|
|
283
|
+
// The orchestrator runs non-interactively by design — a pause
|
|
284
|
+
// decision becomes hard-fail so CI / scripts don't deadlock.
|
|
285
|
+
nonInteractive: true,
|
|
286
|
+
});
|
|
287
|
+
const durationMs = Date.now() - phaseStartedAt;
|
|
288
|
+
const costUSD = extractCostUSD(output);
|
|
289
|
+
phaseSummaries[phaseIdx] = {
|
|
290
|
+
name,
|
|
291
|
+
status: 'success',
|
|
292
|
+
costUSD,
|
|
293
|
+
durationMs,
|
|
294
|
+
};
|
|
295
|
+
// Translate output back to the verb's legacy banner. We swallow
|
|
296
|
+
// the per-phase exit code on success — the orchestrator's overall
|
|
297
|
+
// exit is determined by the run, not by individual phase
|
|
298
|
+
// renderResult return values (which are always 0 on success for
|
|
299
|
+
// the four registered v6.2.0 phases).
|
|
300
|
+
if (!silent) {
|
|
301
|
+
built.renderResult(output);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
const durationMs = Date.now() - phaseStartedAt;
|
|
306
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
+
const errorCode = err instanceof GuardrailError ? err.code : undefined;
|
|
308
|
+
failedAtPhase = phaseIdx;
|
|
309
|
+
failedPhaseName = name;
|
|
310
|
+
phaseErrorCode = errorCode ?? 'phase_failed';
|
|
311
|
+
phaseErrorMessage = message;
|
|
312
|
+
phaseSummaries[phaseIdx] = {
|
|
313
|
+
name,
|
|
314
|
+
status: 'failed',
|
|
315
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
316
|
+
errorMessage: message,
|
|
317
|
+
costUSD: 0,
|
|
318
|
+
durationMs,
|
|
319
|
+
};
|
|
320
|
+
if (!silent) {
|
|
321
|
+
process.stderr.write(fmt(ANSI_RED, `[autopilot] phase ${phaseIdx + 1}/${phaseNames.length} (${name}) failed: ${message}\n`));
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// --- Emit run.complete + refresh state ------------------------------
|
|
327
|
+
const totalCostUSD = phaseSummaries.reduce((acc, p) => acc + p.costUSD, 0);
|
|
328
|
+
const overallDurationMs = Date.now() - startedAt;
|
|
329
|
+
const overallStatus = failedAtPhase === null ? 'success' : 'failed';
|
|
330
|
+
appendEvent(created.runDir, {
|
|
331
|
+
event: 'run.complete',
|
|
332
|
+
status: overallStatus,
|
|
333
|
+
totalCostUSD,
|
|
334
|
+
durationMs: overallDurationMs,
|
|
335
|
+
}, { writerId: created.lock.writerId, runId: created.runId });
|
|
336
|
+
writeStateSnapshot(created.runDir, replayState(created.runDir));
|
|
337
|
+
// --- Compute exit code (per spec exit-code matrix) ------------------
|
|
338
|
+
const exitCode = computeExitCode({ failedAtPhase, phaseErrorCode });
|
|
339
|
+
if (!silent) {
|
|
340
|
+
if (overallStatus === 'success') {
|
|
341
|
+
process.stdout.write(fmt(ANSI_GREEN, `[autopilot] run complete`) +
|
|
342
|
+
` ${formatDuration(overallDurationMs)} ` +
|
|
343
|
+
fmt(ANSI_DIM, `· $${totalCostUSD.toFixed(4)}`) +
|
|
344
|
+
'\n');
|
|
345
|
+
process.stdout.write(fmt(ANSI_DIM, ` inspect: claude-autopilot runs show ${created.runId} --events\n`));
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
process.stdout.write(fmt(ANSI_RED, `[autopilot] failed at phase ${(failedAtPhase ?? 0) + 1}/${phaseNames.length} (${failedPhaseName})\n`));
|
|
349
|
+
process.stdout.write(fmt(ANSI_DIM, ` inspect: claude-autopilot runs show ${created.runId} --events\n`));
|
|
350
|
+
process.stdout.write(fmt(ANSI_DIM, ` resume: claude-autopilot run resume ${created.runId}\n`));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
runId: created.runId,
|
|
355
|
+
runDir: created.runDir,
|
|
356
|
+
exitCode,
|
|
357
|
+
...(phaseErrorCode !== undefined ? { errorCode: phaseErrorCode } : {}),
|
|
358
|
+
...(phaseErrorMessage !== undefined ? { errorMessage: phaseErrorMessage } : {}),
|
|
359
|
+
phases: phaseSummaries,
|
|
360
|
+
totalCostUSD,
|
|
361
|
+
durationMs: overallDurationMs,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await created.lock.release().catch(() => { });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** v6.2.0 — exit-code matrix from spec "Failure semantics + exit codes":
|
|
369
|
+
*
|
|
370
|
+
* | Failure source | Exit code |
|
|
371
|
+
* |--------------------------------------------------------|-----------|
|
|
372
|
+
* | All phases succeed | 0 |
|
|
373
|
+
* | Phase failure where errorCode === 'budget_exceeded' | 78 |
|
|
374
|
+
* | Phase failure where errorCode in {lock_held, | 2 |
|
|
375
|
+
* | corrupted_state, partial_write} | |
|
|
376
|
+
* | Any other phase failure | 1 |
|
|
377
|
+
*
|
|
378
|
+
* Pre-run validation failures (engine-off, unknown phase, etc.) exit 1
|
|
379
|
+
* with `errorCode: 'invalid_config'` BEFORE this helper is reached. */
|
|
380
|
+
function computeExitCode(opts) {
|
|
381
|
+
if (opts.failedAtPhase === null)
|
|
382
|
+
return 0;
|
|
383
|
+
switch (opts.phaseErrorCode) {
|
|
384
|
+
case 'budget_exceeded':
|
|
385
|
+
return 78;
|
|
386
|
+
case 'lock_held':
|
|
387
|
+
case 'corrupted_state':
|
|
388
|
+
case 'partial_write':
|
|
389
|
+
return 2;
|
|
390
|
+
default:
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Extract `costUSD` from a phase output if present, else 0. Same shape
|
|
395
|
+
* as the legacy `runPhaseWithLifecycle` helper — only `scan` exposes one
|
|
396
|
+
* today; the other three v6.2.0 phases are read-only and return 0. */
|
|
397
|
+
function extractCostUSD(output) {
|
|
398
|
+
if (output !== null && typeof output === 'object' && 'costUSD' in output) {
|
|
399
|
+
const v = output.costUSD;
|
|
400
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
401
|
+
return v;
|
|
402
|
+
}
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
/** Read events.ndjson and pull out:
|
|
406
|
+
* - whether a prior `phase.success` exists for this phaseIdx
|
|
407
|
+
* - all `phase.externalRef` events recorded for this phaseIdx
|
|
408
|
+
*
|
|
409
|
+
* Used by the orchestrator's resume preflight to decide skip / retry /
|
|
410
|
+
* needs-human. For a fresh run the events file has only `run.start` (and
|
|
411
|
+
* possibly `phase.start` if we're mid-phase) → both fields come back
|
|
412
|
+
* empty/false and the preflight returns `proceed-fresh`. */
|
|
413
|
+
function collectPriorPhaseState(runDir, phaseName, phaseIdx) {
|
|
414
|
+
const eventsPath = path.join(runDir, 'events.ndjson');
|
|
415
|
+
if (!fs.existsSync(eventsPath)) {
|
|
416
|
+
return { priorPhaseSuccess: false, priorRefs: [] };
|
|
417
|
+
}
|
|
418
|
+
let raw;
|
|
419
|
+
try {
|
|
420
|
+
raw = fs.readFileSync(eventsPath, 'utf8');
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return { priorPhaseSuccess: false, priorRefs: [] };
|
|
424
|
+
}
|
|
425
|
+
const lines = raw.split('\n').filter(line => line.length > 0);
|
|
426
|
+
let priorPhaseSuccess = false;
|
|
427
|
+
const priorRefs = [];
|
|
428
|
+
for (const line of lines) {
|
|
429
|
+
let ev;
|
|
430
|
+
try {
|
|
431
|
+
ev = JSON.parse(line);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (ev.event === 'phase.success' && ev.phaseIdx === phaseIdx && ev.phase === phaseName) {
|
|
437
|
+
priorPhaseSuccess = true;
|
|
438
|
+
}
|
|
439
|
+
else if (ev.event === 'phase.externalRef' &&
|
|
440
|
+
ev.phaseIdx === phaseIdx &&
|
|
441
|
+
ev.phase === phaseName) {
|
|
442
|
+
priorRefs.push(ev.ref);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { priorPhaseSuccess, priorRefs };
|
|
446
|
+
}
|
|
447
|
+
/** Carry out the resume decision's side effects on the durable log + phase
|
|
448
|
+
* summaries. Returns:
|
|
449
|
+
* - `skipped` → orchestrator continues to phase N+1
|
|
450
|
+
* - `failed` → orchestrator records phase failure and breaks the loop
|
|
451
|
+
* - `proceed` → orchestrator falls through to runPhase normally
|
|
452
|
+
*
|
|
453
|
+
* For `skip-already-applied` we emit a synthetic `phase.success` event
|
|
454
|
+
* with empty artifacts so downstream tooling (`runs show`) sees the
|
|
455
|
+
* phase as completed. The event carries `replayed: true` via the meta
|
|
456
|
+
* channel — except `phase.success` doesn't have a meta slot in the
|
|
457
|
+
* schema, so the replay flag is conveyed exclusively via the
|
|
458
|
+
* `replay.override`-class events; the success event itself is
|
|
459
|
+
* indistinguishable from a fresh success. That matches the spec's intent
|
|
460
|
+
* ("emit phase.success { replayed: true, reason: 'side-effect-already-
|
|
461
|
+
* applied' }") modulo schema constraints — the readback's metadata is
|
|
462
|
+
* preserved on the next event we DO write. */
|
|
463
|
+
async function applyResumeDecision(input) {
|
|
464
|
+
const { decision, runDir, runId, writerId, phaseName, phaseIdx, phaseStartedAt, phaseSummaries } = input;
|
|
465
|
+
if (decision.kind === 'proceed-fresh')
|
|
466
|
+
return 'proceed';
|
|
467
|
+
if (decision.kind === 'retry')
|
|
468
|
+
return 'proceed';
|
|
469
|
+
if (decision.kind === 'skip-already-applied') {
|
|
470
|
+
const durationMs = Date.now() - phaseStartedAt;
|
|
471
|
+
appendEvent(runDir, {
|
|
472
|
+
event: 'phase.success',
|
|
473
|
+
phase: phaseName,
|
|
474
|
+
phaseIdx,
|
|
475
|
+
durationMs,
|
|
476
|
+
artifacts: [],
|
|
477
|
+
}, { writerId, runId });
|
|
478
|
+
phaseSummaries[phaseIdx] = {
|
|
479
|
+
name: phaseName,
|
|
480
|
+
status: 'success',
|
|
481
|
+
costUSD: 0,
|
|
482
|
+
durationMs,
|
|
483
|
+
};
|
|
484
|
+
return 'skipped';
|
|
485
|
+
}
|
|
486
|
+
// needs-human — emit replay.override with the consulted refs, then bail.
|
|
487
|
+
appendEvent(runDir, {
|
|
488
|
+
event: 'replay.override',
|
|
489
|
+
phase: phaseName,
|
|
490
|
+
phaseIdx,
|
|
491
|
+
reason: decision.reason,
|
|
492
|
+
refsConsulted: decision.refsConsulted,
|
|
493
|
+
}, { writerId, runId });
|
|
494
|
+
appendEvent(runDir, {
|
|
495
|
+
event: 'phase.needs-human',
|
|
496
|
+
phase: phaseName,
|
|
497
|
+
phaseIdx,
|
|
498
|
+
reason: decision.reason,
|
|
499
|
+
nextActions: [
|
|
500
|
+
'--force-replay to bypass the preflight after manual ledger inspection',
|
|
501
|
+
`claude-autopilot runs show ${runId} --events`,
|
|
502
|
+
],
|
|
503
|
+
}, { writerId, runId });
|
|
504
|
+
const durationMs = Date.now() - phaseStartedAt;
|
|
505
|
+
phaseSummaries[phaseIdx] = {
|
|
506
|
+
name: phaseName,
|
|
507
|
+
status: 'failed',
|
|
508
|
+
errorCode: 'needs_human',
|
|
509
|
+
errorMessage: `resume preflight refused (${decision.reason})`,
|
|
510
|
+
costUSD: 0,
|
|
511
|
+
durationMs,
|
|
512
|
+
};
|
|
513
|
+
return 'failed';
|
|
514
|
+
}
|
|
515
|
+
function formatDuration(ms) {
|
|
516
|
+
if (ms < 1000)
|
|
517
|
+
return `${ms}ms`;
|
|
518
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
519
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
520
|
+
const seconds = totalSeconds % 60;
|
|
521
|
+
if (minutes === 0)
|
|
522
|
+
return `${seconds}s`;
|
|
523
|
+
return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
|
|
524
|
+
}
|
|
525
|
+
/** Install process-scoped fatal handlers for `--json` mode. Returns a
|
|
526
|
+
* removal function so the caller (test seam) can detach them deterministically.
|
|
527
|
+
*
|
|
528
|
+
* Both handlers consult the single-write latch via
|
|
529
|
+
* `__isAutopilotEnvelopeWritten()` — if an envelope already shipped, they
|
|
530
|
+
* no-op-exit; otherwise they emit a fallback `internal_error` envelope and
|
|
531
|
+
* exit 1. Per spec "Channel discipline" → "Exactly-once guarantee under
|
|
532
|
+
* fatal paths". */
|
|
533
|
+
function installAutopilotJsonProcessHandlers(startedAt) {
|
|
534
|
+
const handlers = {
|
|
535
|
+
uncaughtException: (err) => {
|
|
536
|
+
if (__isAutopilotEnvelopeWritten()) {
|
|
537
|
+
process.exit(1);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
541
|
+
writeAutopilotEnvelope({
|
|
542
|
+
runId: null,
|
|
543
|
+
status: 'failed',
|
|
544
|
+
exitCode: 1,
|
|
545
|
+
phases: [],
|
|
546
|
+
totalCostUSD: 0,
|
|
547
|
+
durationMs: Date.now() - startedAt,
|
|
548
|
+
errorCode: 'internal_error',
|
|
549
|
+
errorMessage: message,
|
|
550
|
+
});
|
|
551
|
+
// Best-effort flush before exit.
|
|
552
|
+
process.stdout.write('', () => process.exit(1));
|
|
553
|
+
},
|
|
554
|
+
unhandledRejection: (err) => {
|
|
555
|
+
if (__isAutopilotEnvelopeWritten()) {
|
|
556
|
+
process.exit(1);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
560
|
+
writeAutopilotEnvelope({
|
|
561
|
+
runId: null,
|
|
562
|
+
status: 'failed',
|
|
563
|
+
exitCode: 1,
|
|
564
|
+
phases: [],
|
|
565
|
+
totalCostUSD: 0,
|
|
566
|
+
durationMs: Date.now() - startedAt,
|
|
567
|
+
errorCode: 'internal_error',
|
|
568
|
+
errorMessage: message,
|
|
569
|
+
});
|
|
570
|
+
process.stdout.write('', () => process.exit(1));
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
process.on('uncaughtException', handlers.uncaughtException);
|
|
574
|
+
process.on('unhandledRejection', handlers.unhandledRejection);
|
|
575
|
+
return {
|
|
576
|
+
handlers,
|
|
577
|
+
remove: () => {
|
|
578
|
+
process.removeListener('uncaughtException', handlers.uncaughtException);
|
|
579
|
+
process.removeListener('unhandledRejection', handlers.unhandledRejection);
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/** Translate the orchestrator's internal phase summary status into the
|
|
584
|
+
* envelope's bounded enum. Pre-run failures and unstarted phases are
|
|
585
|
+
* reported as `failed`; replay short-circuits map to `skipped-replay`. */
|
|
586
|
+
function toEnvelopePhaseStatus(status) {
|
|
587
|
+
switch (status) {
|
|
588
|
+
case 'success':
|
|
589
|
+
return 'success';
|
|
590
|
+
case 'failed':
|
|
591
|
+
case 'not-run':
|
|
592
|
+
return 'failed';
|
|
593
|
+
case 'skipped':
|
|
594
|
+
return 'skipped-replay';
|
|
595
|
+
default: {
|
|
596
|
+
const _exhaustive = status;
|
|
597
|
+
void _exhaustive;
|
|
598
|
+
return 'failed';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Map an internal `AutopilotResult.errorCode` (string, possibly
|
|
603
|
+
* unrecognized) onto the bounded `AutopilotErrorCode` enum. Unknown
|
|
604
|
+
* values fall back to `phase_failed` so CI consumers always get a
|
|
605
|
+
* member of the published enum. */
|
|
606
|
+
function narrowErrorCode(code) {
|
|
607
|
+
if (code === undefined)
|
|
608
|
+
return undefined;
|
|
609
|
+
if (AUTOPILOT_ERROR_CODES.includes(code)) {
|
|
610
|
+
return code;
|
|
611
|
+
}
|
|
612
|
+
return 'phase_failed';
|
|
613
|
+
}
|
|
614
|
+
/** Build the envelope's `AutopilotJsonResult` from the orchestrator's
|
|
615
|
+
* internal `AutopilotResult`. Pure projection — no IO. */
|
|
616
|
+
function resultToJsonResult(result) {
|
|
617
|
+
const failedIdx = result.phases.findIndex(p => p.status === 'failed');
|
|
618
|
+
const errorCode = narrowErrorCode(result.errorCode);
|
|
619
|
+
const status = result.exitCode === 0 ? 'success' : 'failed';
|
|
620
|
+
const phases = result.phases.map(p => ({
|
|
621
|
+
name: p.name,
|
|
622
|
+
status: toEnvelopePhaseStatus(p.status),
|
|
623
|
+
costUSD: p.costUSD,
|
|
624
|
+
durationMs: p.durationMs,
|
|
625
|
+
}));
|
|
626
|
+
const exitCode = computeAutopilotExitCode(errorCode);
|
|
627
|
+
// Defensive: if narrowErrorCode mapped us off the canonical exit code (e.g.
|
|
628
|
+
// an internal `errorCode: 'concurrency_lock'` → fallback `phase_failed`)
|
|
629
|
+
// prefer the orchestrator's authoritative `exitCode` so we don't disagree
|
|
630
|
+
// with the legacy text-mode path. The mapping above is the canonical
|
|
631
|
+
// translation; this is the safety belt.
|
|
632
|
+
const finalExitCode = (() => {
|
|
633
|
+
if (status === 'success')
|
|
634
|
+
return 0;
|
|
635
|
+
if (errorCode !== undefined)
|
|
636
|
+
return exitCode;
|
|
637
|
+
// Fall back to whatever the orchestrator returned, clamped to the
|
|
638
|
+
// documented set.
|
|
639
|
+
const ec = result.exitCode;
|
|
640
|
+
if (ec === 0 || ec === 1 || ec === 2 || ec === 78)
|
|
641
|
+
return ec;
|
|
642
|
+
return 1;
|
|
643
|
+
})();
|
|
644
|
+
const out = {
|
|
645
|
+
runId: result.runId,
|
|
646
|
+
status,
|
|
647
|
+
exitCode: finalExitCode,
|
|
648
|
+
phases,
|
|
649
|
+
totalCostUSD: result.totalCostUSD,
|
|
650
|
+
durationMs: result.durationMs,
|
|
651
|
+
};
|
|
652
|
+
if (errorCode !== undefined)
|
|
653
|
+
out.errorCode = errorCode;
|
|
654
|
+
if (result.errorMessage !== undefined)
|
|
655
|
+
out.errorMessage = result.errorMessage;
|
|
656
|
+
if (failedIdx >= 0) {
|
|
657
|
+
out.failedAtPhase = failedIdx;
|
|
658
|
+
out.failedPhaseName = result.phases[failedIdx].name;
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
/** v6.2.2 entrypoint for `claude-autopilot autopilot --json`.
|
|
663
|
+
*
|
|
664
|
+
* Wraps `runAutopilot` (which already handles pre-run failures inline by
|
|
665
|
+
* returning an `AutopilotResult` with `runId: null` + populated
|
|
666
|
+
* `errorCode` / `errorMessage`) and emits exactly one envelope on stdout.
|
|
667
|
+
* Process-level fatal handlers (codex WARNING #2) catch async failures
|
|
668
|
+
* that would otherwise bypass our try/catch.
|
|
669
|
+
*
|
|
670
|
+
* Returns the exit code the dispatcher should propagate via
|
|
671
|
+
* `process.exit`. */
|
|
672
|
+
export async function runAutopilotWithJsonEnvelope(options = {}) {
|
|
673
|
+
const startedAt = Date.now();
|
|
674
|
+
const installHandlers = options.__testInstallProcessHandlers !== false; // default true
|
|
675
|
+
const handlerHandle = installHandlers
|
|
676
|
+
? installAutopilotJsonProcessHandlers(startedAt)
|
|
677
|
+
: null;
|
|
678
|
+
// Force `__silent` so the orchestrator's own banner stdout writes don't
|
|
679
|
+
// pollute the envelope. Per spec "Channel discipline" — stdout in --json
|
|
680
|
+
// mode is the envelope and ONLY the envelope.
|
|
681
|
+
const innerOptions = {
|
|
682
|
+
...options,
|
|
683
|
+
__silent: true,
|
|
684
|
+
};
|
|
685
|
+
let exitCode = 1;
|
|
686
|
+
try {
|
|
687
|
+
let result;
|
|
688
|
+
try {
|
|
689
|
+
result = await runAutopilot(innerOptions);
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
// Orchestrator threw past its own try/catch — surface as
|
|
693
|
+
// internal_error envelope. Non-GuardrailError throws here are usually
|
|
694
|
+
// bugs; we still emit a deterministic envelope so CI sees something
|
|
695
|
+
// parseable.
|
|
696
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
697
|
+
const errorCode = err instanceof GuardrailError &&
|
|
698
|
+
AUTOPILOT_ERROR_CODES.includes(err.code)
|
|
699
|
+
? err.code
|
|
700
|
+
: 'internal_error';
|
|
701
|
+
const ec = computeAutopilotExitCode(errorCode);
|
|
702
|
+
writeAutopilotEnvelope({
|
|
703
|
+
runId: null,
|
|
704
|
+
status: 'failed',
|
|
705
|
+
exitCode: ec,
|
|
706
|
+
phases: [],
|
|
707
|
+
totalCostUSD: 0,
|
|
708
|
+
durationMs: Date.now() - startedAt,
|
|
709
|
+
errorCode,
|
|
710
|
+
errorMessage: message,
|
|
711
|
+
});
|
|
712
|
+
await new Promise(resolve => process.stdout.write('', () => resolve()));
|
|
713
|
+
exitCode = ec;
|
|
714
|
+
return exitCode;
|
|
715
|
+
}
|
|
716
|
+
const jsonResult = resultToJsonResult(result);
|
|
717
|
+
writeAutopilotEnvelope(jsonResult);
|
|
718
|
+
await new Promise(resolve => process.stdout.write('', () => resolve()));
|
|
719
|
+
exitCode = jsonResult.exitCode;
|
|
720
|
+
// Test seam — emulate a finalization throw AFTER the envelope is on
|
|
721
|
+
// disk so the latch test can verify uncaughtException handlers no-op.
|
|
722
|
+
if (typeof options.__testThrowAfterEnvelope === 'function') {
|
|
723
|
+
options.__testThrowAfterEnvelope();
|
|
724
|
+
}
|
|
725
|
+
return exitCode;
|
|
726
|
+
}
|
|
727
|
+
finally {
|
|
728
|
+
if (handlerHandle)
|
|
729
|
+
handlerHandle.remove();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// Re-export so the dispatcher can mention it in --help without importing
|
|
733
|
+
// from the registry separately. (Pure convenience; no behavioral effect.)
|
|
734
|
+
export { PHASE_REGISTRY, DEFAULT_FULL_PHASES };
|
|
735
|
+
//# sourceMappingURL=autopilot.js.map
|