@delegance/claude-autopilot 5.5.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +935 -6
  2. package/README.md +55 -0
  3. package/dist/src/adapters/council/openai.js +12 -6
  4. package/dist/src/adapters/deploy/_http.d.ts +43 -0
  5. package/dist/src/adapters/deploy/_http.js +99 -0
  6. package/dist/src/adapters/deploy/fly.d.ts +206 -0
  7. package/dist/src/adapters/deploy/fly.js +696 -0
  8. package/dist/src/adapters/deploy/index.d.ts +2 -0
  9. package/dist/src/adapters/deploy/index.js +33 -0
  10. package/dist/src/adapters/deploy/render.d.ts +181 -0
  11. package/dist/src/adapters/deploy/render.js +550 -0
  12. package/dist/src/adapters/deploy/types.d.ts +67 -3
  13. package/dist/src/adapters/deploy/vercel.d.ts +17 -1
  14. package/dist/src/adapters/deploy/vercel.js +29 -49
  15. package/dist/src/adapters/pricing.d.ts +36 -0
  16. package/dist/src/adapters/pricing.js +40 -0
  17. package/dist/src/adapters/review-engine/codex.js +10 -7
  18. package/dist/src/cli/autopilot.d.ts +71 -0
  19. package/dist/src/cli/autopilot.js +735 -0
  20. package/dist/src/cli/brainstorm.d.ts +23 -0
  21. package/dist/src/cli/brainstorm.js +131 -0
  22. package/dist/src/cli/costs.d.ts +15 -1
  23. package/dist/src/cli/costs.js +99 -10
  24. package/dist/src/cli/deploy.d.ts +3 -3
  25. package/dist/src/cli/deploy.js +34 -9
  26. package/dist/src/cli/fix.d.ts +18 -0
  27. package/dist/src/cli/fix.js +105 -11
  28. package/dist/src/cli/help-text.d.ts +52 -0
  29. package/dist/src/cli/help-text.js +400 -0
  30. package/dist/src/cli/implement.d.ts +91 -0
  31. package/dist/src/cli/implement.js +196 -0
  32. package/dist/src/cli/index.js +719 -245
  33. package/dist/src/cli/json-envelope.d.ts +187 -0
  34. package/dist/src/cli/json-envelope.js +270 -0
  35. package/dist/src/cli/json-mode.d.ts +33 -0
  36. package/dist/src/cli/json-mode.js +201 -0
  37. package/dist/src/cli/migrate.d.ts +111 -0
  38. package/dist/src/cli/migrate.js +305 -0
  39. package/dist/src/cli/plan.d.ts +81 -0
  40. package/dist/src/cli/plan.js +149 -0
  41. package/dist/src/cli/pr.d.ts +106 -0
  42. package/dist/src/cli/pr.js +191 -19
  43. package/dist/src/cli/preflight.js +26 -0
  44. package/dist/src/cli/review.d.ts +27 -0
  45. package/dist/src/cli/review.js +126 -0
  46. package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
  47. package/dist/src/cli/runs-watch-renderer.js +275 -0
  48. package/dist/src/cli/runs-watch.d.ts +41 -0
  49. package/dist/src/cli/runs-watch.js +395 -0
  50. package/dist/src/cli/runs.d.ts +122 -0
  51. package/dist/src/cli/runs.js +902 -0
  52. package/dist/src/cli/scan.d.ts +93 -0
  53. package/dist/src/cli/scan.js +166 -40
  54. package/dist/src/cli/spec.d.ts +66 -0
  55. package/dist/src/cli/spec.js +132 -0
  56. package/dist/src/cli/validate.d.ts +29 -0
  57. package/dist/src/cli/validate.js +131 -0
  58. package/dist/src/core/config/schema.d.ts +9 -0
  59. package/dist/src/core/config/schema.js +7 -0
  60. package/dist/src/core/config/types.d.ts +11 -0
  61. package/dist/src/core/council/runner.d.ts +10 -1
  62. package/dist/src/core/council/runner.js +25 -3
  63. package/dist/src/core/council/types.d.ts +7 -0
  64. package/dist/src/core/errors.d.ts +1 -1
  65. package/dist/src/core/errors.js +11 -0
  66. package/dist/src/core/logging/redaction.d.ts +13 -0
  67. package/dist/src/core/logging/redaction.js +20 -0
  68. package/dist/src/core/migrate/schema-validator.js +15 -1
  69. package/dist/src/core/phases/static-rules.d.ts +5 -1
  70. package/dist/src/core/phases/static-rules.js +2 -5
  71. package/dist/src/core/run-state/budget.d.ts +88 -0
  72. package/dist/src/core/run-state/budget.js +141 -0
  73. package/dist/src/core/run-state/cli-internal.d.ts +21 -0
  74. package/dist/src/core/run-state/cli-internal.js +174 -0
  75. package/dist/src/core/run-state/events.d.ts +59 -0
  76. package/dist/src/core/run-state/events.js +504 -0
  77. package/dist/src/core/run-state/lock.d.ts +61 -0
  78. package/dist/src/core/run-state/lock.js +206 -0
  79. package/dist/src/core/run-state/phase-context.d.ts +60 -0
  80. package/dist/src/core/run-state/phase-context.js +108 -0
  81. package/dist/src/core/run-state/phase-registry.d.ts +137 -0
  82. package/dist/src/core/run-state/phase-registry.js +162 -0
  83. package/dist/src/core/run-state/phase-runner.d.ts +80 -0
  84. package/dist/src/core/run-state/phase-runner.js +447 -0
  85. package/dist/src/core/run-state/provider-readback.d.ts +130 -0
  86. package/dist/src/core/run-state/provider-readback.js +426 -0
  87. package/dist/src/core/run-state/replay-decision.d.ts +69 -0
  88. package/dist/src/core/run-state/replay-decision.js +144 -0
  89. package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
  90. package/dist/src/core/run-state/resolve-engine.js +190 -0
  91. package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
  92. package/dist/src/core/run-state/resume-preflight.js +116 -0
  93. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
  94. package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
  95. package/dist/src/core/run-state/runs.d.ts +57 -0
  96. package/dist/src/core/run-state/runs.js +288 -0
  97. package/dist/src/core/run-state/snapshot.d.ts +14 -0
  98. package/dist/src/core/run-state/snapshot.js +114 -0
  99. package/dist/src/core/run-state/state.d.ts +40 -0
  100. package/dist/src/core/run-state/state.js +164 -0
  101. package/dist/src/core/run-state/types.d.ts +278 -0
  102. package/dist/src/core/run-state/types.js +13 -0
  103. package/dist/src/core/run-state/ulid.d.ts +11 -0
  104. package/dist/src/core/run-state/ulid.js +95 -0
  105. package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
  106. package/dist/src/core/schema-alignment/extractor/index.js +2 -2
  107. package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
  108. package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
  109. package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
  110. package/dist/src/core/schema-alignment/git-history.js +53 -0
  111. package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
  112. package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
  113. package/package.json +2 -1
  114. package/scripts/autoregress.ts +1 -1
  115. package/skills/claude-autopilot.md +1 -1
  116. package/skills/make-interfaces-feel-better/SKILL.md +104 -0
  117. package/skills/simplify-ui/SKILL.md +103 -0
  118. package/skills/ui/SKILL.md +117 -0
  119. 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