@delegance/claude-autopilot 5.5.2 → 7.2.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +1776 -6
  2. package/README.md +65 -1
  3. package/bin/_launcher.js +38 -23
  4. package/dist/src/adapters/council/openai.js +12 -6
  5. package/dist/src/adapters/deploy/_http.d.ts +43 -0
  6. package/dist/src/adapters/deploy/_http.js +99 -0
  7. package/dist/src/adapters/deploy/fly.d.ts +206 -0
  8. package/dist/src/adapters/deploy/fly.js +696 -0
  9. package/dist/src/adapters/deploy/index.d.ts +2 -0
  10. package/dist/src/adapters/deploy/index.js +33 -0
  11. package/dist/src/adapters/deploy/render.d.ts +181 -0
  12. package/dist/src/adapters/deploy/render.js +550 -0
  13. package/dist/src/adapters/deploy/types.d.ts +67 -3
  14. package/dist/src/adapters/deploy/vercel.d.ts +17 -1
  15. package/dist/src/adapters/deploy/vercel.js +29 -49
  16. package/dist/src/adapters/pricing.d.ts +36 -0
  17. package/dist/src/adapters/pricing.js +40 -0
  18. package/dist/src/adapters/review-engine/codex.js +10 -7
  19. package/dist/src/cli/autopilot.d.ts +75 -0
  20. package/dist/src/cli/autopilot.js +750 -0
  21. package/dist/src/cli/brainstorm.d.ts +23 -0
  22. package/dist/src/cli/brainstorm.js +131 -0
  23. package/dist/src/cli/costs.d.ts +15 -1
  24. package/dist/src/cli/costs.js +99 -10
  25. package/dist/src/cli/dashboard/index.d.ts +5 -0
  26. package/dist/src/cli/dashboard/index.js +49 -0
  27. package/dist/src/cli/dashboard/login.d.ts +22 -0
  28. package/dist/src/cli/dashboard/login.js +260 -0
  29. package/dist/src/cli/dashboard/logout.d.ts +12 -0
  30. package/dist/src/cli/dashboard/logout.js +45 -0
  31. package/dist/src/cli/dashboard/status.d.ts +30 -0
  32. package/dist/src/cli/dashboard/status.js +65 -0
  33. package/dist/src/cli/dashboard/upload.d.ts +16 -0
  34. package/dist/src/cli/dashboard/upload.js +48 -0
  35. package/dist/src/cli/deploy.d.ts +3 -3
  36. package/dist/src/cli/deploy.js +34 -9
  37. package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
  38. package/dist/src/cli/engine-flag-deprecation.js +20 -0
  39. package/dist/src/cli/fix.d.ts +18 -0
  40. package/dist/src/cli/fix.js +105 -11
  41. package/dist/src/cli/help-text.d.ts +52 -0
  42. package/dist/src/cli/help-text.js +416 -0
  43. package/dist/src/cli/implement.d.ts +91 -0
  44. package/dist/src/cli/implement.js +196 -0
  45. package/dist/src/cli/index.d.ts +2 -1
  46. package/dist/src/cli/index.js +774 -245
  47. package/dist/src/cli/json-envelope.d.ts +187 -0
  48. package/dist/src/cli/json-envelope.js +270 -0
  49. package/dist/src/cli/json-mode.d.ts +33 -0
  50. package/dist/src/cli/json-mode.js +201 -0
  51. package/dist/src/cli/migrate.d.ts +111 -0
  52. package/dist/src/cli/migrate.js +305 -0
  53. package/dist/src/cli/plan.d.ts +81 -0
  54. package/dist/src/cli/plan.js +149 -0
  55. package/dist/src/cli/pr.d.ts +106 -0
  56. package/dist/src/cli/pr.js +191 -19
  57. package/dist/src/cli/preflight.js +26 -0
  58. package/dist/src/cli/review.d.ts +27 -0
  59. package/dist/src/cli/review.js +126 -0
  60. package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
  61. package/dist/src/cli/runs-watch-renderer.js +275 -0
  62. package/dist/src/cli/runs-watch.d.ts +41 -0
  63. package/dist/src/cli/runs-watch.js +395 -0
  64. package/dist/src/cli/runs.d.ts +122 -0
  65. package/dist/src/cli/runs.js +902 -0
  66. package/dist/src/cli/scaffold.d.ts +39 -0
  67. package/dist/src/cli/scaffold.js +287 -0
  68. package/dist/src/cli/scan.d.ts +93 -0
  69. package/dist/src/cli/scan.js +166 -40
  70. package/dist/src/cli/setup.d.ts +30 -0
  71. package/dist/src/cli/setup.js +137 -0
  72. package/dist/src/cli/spec.d.ts +66 -0
  73. package/dist/src/cli/spec.js +132 -0
  74. package/dist/src/cli/validate.d.ts +29 -0
  75. package/dist/src/cli/validate.js +131 -0
  76. package/dist/src/core/config/schema.d.ts +9 -0
  77. package/dist/src/core/config/schema.js +7 -0
  78. package/dist/src/core/config/types.d.ts +11 -0
  79. package/dist/src/core/council/runner.d.ts +10 -1
  80. package/dist/src/core/council/runner.js +25 -3
  81. package/dist/src/core/council/types.d.ts +7 -0
  82. package/dist/src/core/errors.d.ts +1 -1
  83. package/dist/src/core/errors.js +11 -0
  84. package/dist/src/core/logging/redaction.d.ts +13 -0
  85. package/dist/src/core/logging/redaction.js +20 -0
  86. package/dist/src/core/migrate/schema-validator.js +15 -1
  87. package/dist/src/core/phases/static-rules.d.ts +5 -1
  88. package/dist/src/core/phases/static-rules.js +2 -5
  89. package/dist/src/core/run-state/budget.d.ts +88 -0
  90. package/dist/src/core/run-state/budget.js +141 -0
  91. package/dist/src/core/run-state/cli-internal.d.ts +21 -0
  92. package/dist/src/core/run-state/cli-internal.js +174 -0
  93. package/dist/src/core/run-state/events.d.ts +59 -0
  94. package/dist/src/core/run-state/events.js +512 -0
  95. package/dist/src/core/run-state/lock.d.ts +61 -0
  96. package/dist/src/core/run-state/lock.js +206 -0
  97. package/dist/src/core/run-state/phase-context.d.ts +60 -0
  98. package/dist/src/core/run-state/phase-context.js +108 -0
  99. package/dist/src/core/run-state/phase-registry.d.ts +137 -0
  100. package/dist/src/core/run-state/phase-registry.js +162 -0
  101. package/dist/src/core/run-state/phase-runner.d.ts +80 -0
  102. package/dist/src/core/run-state/phase-runner.js +447 -0
  103. package/dist/src/core/run-state/provider-readback.d.ts +130 -0
  104. package/dist/src/core/run-state/provider-readback.js +426 -0
  105. package/dist/src/core/run-state/replay-decision.d.ts +69 -0
  106. package/dist/src/core/run-state/replay-decision.js +144 -0
  107. package/dist/src/core/run-state/resolve-engine.d.ts +45 -0
  108. package/dist/src/core/run-state/resolve-engine.js +74 -0
  109. package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
  110. package/dist/src/core/run-state/resume-preflight.js +116 -0
  111. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +69 -0
  112. package/dist/src/core/run-state/run-phase-with-lifecycle.js +193 -0
  113. package/dist/src/core/run-state/runs.d.ts +57 -0
  114. package/dist/src/core/run-state/runs.js +288 -0
  115. package/dist/src/core/run-state/snapshot.d.ts +14 -0
  116. package/dist/src/core/run-state/snapshot.js +114 -0
  117. package/dist/src/core/run-state/state.d.ts +40 -0
  118. package/dist/src/core/run-state/state.js +164 -0
  119. package/dist/src/core/run-state/types.d.ts +284 -0
  120. package/dist/src/core/run-state/types.js +19 -0
  121. package/dist/src/core/run-state/ulid.d.ts +11 -0
  122. package/dist/src/core/run-state/ulid.js +95 -0
  123. package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
  124. package/dist/src/core/schema-alignment/extractor/index.js +2 -2
  125. package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
  126. package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
  127. package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
  128. package/dist/src/core/schema-alignment/git-history.js +53 -0
  129. package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
  130. package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
  131. package/dist/src/dashboard/auto-upload.d.ts +26 -0
  132. package/dist/src/dashboard/auto-upload.js +107 -0
  133. package/dist/src/dashboard/config.d.ts +22 -0
  134. package/dist/src/dashboard/config.js +109 -0
  135. package/dist/src/dashboard/upload/canonical.d.ts +3 -0
  136. package/dist/src/dashboard/upload/canonical.js +16 -0
  137. package/dist/src/dashboard/upload/chain.d.ts +9 -0
  138. package/dist/src/dashboard/upload/chain.js +27 -0
  139. package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
  140. package/dist/src/dashboard/upload/snapshot.js +66 -0
  141. package/dist/src/dashboard/upload/uploader.d.ts +54 -0
  142. package/dist/src/dashboard/upload/uploader.js +330 -0
  143. package/package.json +19 -3
  144. package/scripts/autoregress.ts +1 -1
  145. package/scripts/test-runner.mjs +4 -0
  146. package/skills/claude-autopilot.md +1 -1
  147. package/skills/make-interfaces-feel-better/SKILL.md +104 -0
  148. package/skills/simplify-ui/SKILL.md +103 -0
  149. package/skills/ui/SKILL.md +117 -0
  150. package/skills/ui-ux-pro-max/SKILL.md +90 -0
@@ -13,6 +13,7 @@
13
13
  import { runCommand } from "./run.js";
14
14
  import { runWatch } from "./watch.js";
15
15
  import { runSetup } from "./setup.js";
16
+ import { runScaffold } from "./scaffold.js";
16
17
  import { runDoctor } from "./preflight.js";
17
18
  import { runCi } from "./ci.js";
18
19
  import { runFix } from "./fix.js";
@@ -29,10 +30,12 @@ import { runCouncilCmd } from "./council.js";
29
30
  import { runMigrateV4 } from "./migrate-v4.js";
30
31
  import { runMigrateDoctor } from "./migrate-doctor.js";
31
32
  import { initMigrate, NoMigrationToolDetectedError } from "./init-migrate.js";
32
- import { dispatch as runMigrateDispatch } from "../core/migrate/dispatcher.js";
33
+ import { runMigrate } from "./migrate.js";
33
34
  import { runDeploy, runDeployRollback, runDeployStatus } from "./deploy.js";
34
35
  import { findPackageRoot } from "./_pkg-root.js";
35
36
  import { GuardrailError } from "../core/errors.js";
37
+ import { buildHelpText, buildCommandHelpText } from "./help-text.js";
38
+ import { runUnderJsonMode } from "./json-envelope.js";
36
39
  // Format unhandled errors as a one-line user-facing message instead of dumping a
37
40
  // Node stack trace. Auth/network failures are by far the most common path here
38
41
  // (bad/missing API key, rate limit, network blip) and surfacing the raw stack
@@ -106,6 +109,21 @@ const REVIEW_VERBS = new Set(['run', 'scan', 'ci', 'fix', 'baseline', 'explain',
106
109
  // `detector` is a library used by setup/run, not a CLI subcommand — leave it out.
107
110
  const ADVANCED_VERBS = new Set(['lsp', 'mcp', 'worker', 'autoregress', 'test-gen', 'hook', 'ignore']);
108
111
  if (args[0] === 'review') {
112
+ // v6.0.4 — `review` is BOTH a grouping prefix (legacy alpha.2) AND a flat
113
+ // verb (the new engine-wrapped `runReview`). Disambiguate based on args[1]:
114
+ //
115
+ // - missing → grouping-prefix help banner (legacy V16)
116
+ // - --help / -h → grouping-prefix help banner (legacy)
117
+ // - in REVIEW_VERBS → grouping prefix (shift, route to flat handler)
118
+ // - other flag (`--engine`,
119
+ // `--config`, etc.) → flat-verb invocation; let `case 'review':` handle it
120
+ // - anything else → reject with legacy "not a review-phase verb"
121
+ //
122
+ // The "missing → prefix help" branch preserves the V16 v4-compat test
123
+ // (`claude-autopilot review` alone prints the review-phase verb list);
124
+ // users who want the v6 flat-verb behavior must pass at least one flag
125
+ // (e.g. `--engine`, `--config`, `--context`). `help review` continues to
126
+ // surface the flat-verb Options block via buildCommandHelpText.
109
127
  const sub = args[1];
110
128
  if (!sub || sub === '--help' || sub === '-h') {
111
129
  console.log(`
@@ -123,16 +141,25 @@ Review-phase verbs:
123
141
 
124
142
  These are aliases for the flat subcommands — \`claude-autopilot run\` and
125
143
  \`claude-autopilot review run\` are equivalent.
144
+
145
+ The v6 \`review\` phase verb (engine-wrap shell) is invoked with any flag
146
+ present, e.g. \`claude-autopilot review --engine\`. See
147
+ \`claude-autopilot help review\` for its options.
126
148
  `);
127
149
  process.exit(0);
128
150
  }
129
- if (!REVIEW_VERBS.has(sub)) {
151
+ if (sub.startsWith('--')) {
152
+ // Flat-verb invocation — fall through; do not shift.
153
+ }
154
+ else if (!REVIEW_VERBS.has(sub)) {
130
155
  console.error(`\x1b[31m[claude-autopilot] "${sub}" is not a review-phase verb.\x1b[0m`);
131
156
  console.error(`\x1b[2m Valid: ${[...REVIEW_VERBS].join(', ')}\x1b[0m`);
132
157
  console.error(`\x1b[2m Did you mean: claude-autopilot ${sub} ...?\x1b[0m`);
133
158
  process.exit(1);
134
159
  }
135
- args.shift(); // drop 'review', leave the flat subcommand at args[0]
160
+ else {
161
+ args.shift(); // drop 'review', leave the flat subcommand at args[0]
162
+ }
136
163
  }
137
164
  if (args[0] === 'advanced') {
138
165
  const sub = args[1];
@@ -160,8 +187,17 @@ These are aliases for the flat subcommands; they still work without the 'advance
160
187
  }
161
188
  args.shift(); // drop 'advanced'
162
189
  }
163
- const SUBCOMMANDS = ['init', 'run', 'scan', 'report', 'explain', 'ignore', 'ci', 'pr', 'fix', 'costs', 'watch', 'hook', 'autoregress', 'baseline', 'triage', 'lsp', 'worker', 'mcp', 'test-gen', 'pr-desc', 'doctor', 'preflight', 'setup', 'council', 'migrate-v4', 'migrate', 'migrate-doctor', 'deploy', 'brainstorm', 'help', '--help', '-h'];
164
- const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce', 'ask', 'focus', 'fail-on', 'note', 'reason', 'expires', 'profile', 'severity', 'prompt', 'context-file', 'path', 'adapter', 'ref', 'sha'];
190
+ // `internal` is a hidden verb (v6 Phase 2): markdown-driven skills shell out
191
+ // to it to append typed events. Deliberately not in HELP_GROUPS / HELP_VERBS,
192
+ // not advertised in the welcome banner. Documented only via
193
+ // `claude-autopilot internal --help`.
194
+ //
195
+ // `runs` (plural) is the v6 Phase 3 umbrella verb — its sub-verbs (list, show,
196
+ // gc, delete, doctor) are dispatched inside its case block. The singular
197
+ // `run resume` form is handled BEFORE the default `run` -> review dispatch
198
+ // kicks in (see disambiguation block just below).
199
+ const SUBCOMMANDS = ['init', 'run', 'runs', 'scan', 'report', 'explain', 'ignore', 'ci', 'pr', 'fix', 'costs', 'watch', 'hook', 'autoregress', 'baseline', 'triage', 'lsp', 'worker', 'mcp', 'test-gen', 'pr-desc', 'doctor', 'preflight', 'setup', 'council', 'migrate-v4', 'migrate', 'migrate-doctor', 'deploy', 'brainstorm', 'spec', 'plan', 'implement', 'review', 'validate', 'autopilot', 'internal', 'help', '--help', '-h'];
200
+ const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce', 'ask', 'focus', 'fail-on', 'note', 'reason', 'expires', 'profile', 'severity', 'prompt', 'context-file', 'path', 'adapter', 'ref', 'sha', 'spec', 'context', 'mode', 'phases', 'budget'];
165
201
  // Bare invocation — no subcommand, no flags → show welcome guide
166
202
  if (args.length === 0) {
167
203
  const hasKey = !!(process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY ||
@@ -197,8 +233,16 @@ Run \x1b[36mclaude-autopilot --help\x1b[0m for full command reference.
197
233
  `);
198
234
  process.exit(0);
199
235
  }
200
- // Detect first non-flag arg as subcommand, default to 'run'
201
- const subcommand = (args[0] && !args[0].startsWith('--')) ? args[0] : 'run';
236
+ // Detect first non-flag arg as subcommand, default to 'run'.
237
+ //
238
+ // v6 Phase 3 disambiguation: `run resume <id>` is a v6 verb; the bare `run`
239
+ // remains the legacy review-phase entry point. We rewrite the head to a
240
+ // synthetic 'run-resume' subcommand so the existing 'run' case keeps doing
241
+ // `runReview` and we don't need to special-case it inside the review path.
242
+ let subcommand = (args[0] && !args[0].startsWith('--')) ? args[0] : 'run';
243
+ if (subcommand === 'run' && args[1] === 'resume') {
244
+ subcommand = 'run-resume';
245
+ }
202
246
  /** Returns value for --name <value>. Exits if value is missing (next token is another flag or absent). */
203
247
  function flag(name) {
204
248
  const idx = args.indexOf(`--${name}`);
@@ -214,129 +258,106 @@ function flag(name) {
214
258
  function boolFlag(name) {
215
259
  return args.includes(`--${name}`);
216
260
  }
261
+ /**
262
+ * v7.0 — `--no-engine` removed; `--engine` becomes a no-op shim with a
263
+ * one-shot per-process deprecation warning to stderr (codex pass-3
264
+ * NOTE #2). The engine is unconditionally on.
265
+ *
266
+ * Behavior:
267
+ * - `--no-engine` → exit 1 with `invalid_config` and a removal hint.
268
+ * - `--engine` → emit one stderr deprecation line per process; return.
269
+ * - neither → no-op; return.
270
+ */
271
+ let __engineDeprecationWarned = false;
272
+ import { ENGINE_FLAG_DEPRECATION_MESSAGE, ENGINE_OFF_REMOVED_MESSAGE, ENGINE_OFF_ENV_REMOVED_MESSAGE, } from "./engine-flag-deprecation.js";
273
+ function parseEngineCliFlag() {
274
+ if (args.includes('--no-engine')) {
275
+ console.error(`\x1b[31m[claude-autopilot] invalid_config: ${ENGINE_OFF_REMOVED_MESSAGE}\x1b[0m`);
276
+ process.exit(1);
277
+ }
278
+ if (args.includes('--engine') && !__engineDeprecationWarned) {
279
+ __engineDeprecationWarned = true;
280
+ process.stderr.write(`${ENGINE_FLAG_DEPRECATION_MESSAGE}\n`);
281
+ }
282
+ checkEngineOffEnvDeprecation();
283
+ // v7.0 — engine is always on; the resolver ignores cliEngine. We
284
+ // return undefined so `cliEngine` never gets spread into the
285
+ // resolver opts (keeps call sites identical and source-compatible).
286
+ return undefined;
287
+ }
288
+ // Test seam: reset the per-process deprecation latch so tests that drive
289
+ // the flag multiple times in one process can exercise the "warn once"
290
+ // behavior repeatably.
291
+ export function _resetEngineDeprecationLatchForTests() {
292
+ __engineDeprecationWarned = false;
293
+ }
294
+ /**
295
+ * v7.0 — `CLAUDE_AUTOPILOT_ENGINE=off` is softer than `--no-engine`:
296
+ * emit a one-shot warning + return undefined so the engine remains on.
297
+ * Per spec: env vars in CI are sticky and silently breaking every
298
+ * v6.x → v7 upgrade in CI on day one would burn user trust.
299
+ */
300
+ let __engineEnvOffWarned = false;
301
+ function checkEngineOffEnvDeprecation() {
302
+ const raw = process.env.CLAUDE_AUTOPILOT_ENGINE;
303
+ if (!raw)
304
+ return;
305
+ const normalized = raw.trim().toLowerCase();
306
+ if (normalized === 'off' || normalized === 'false' || normalized === '0' || normalized === 'no') {
307
+ if (!__engineEnvOffWarned) {
308
+ __engineEnvOffWarned = true;
309
+ process.stderr.write(`${ENGINE_OFF_ENV_REMOVED_MESSAGE}\n`);
310
+ }
311
+ }
312
+ }
313
+ export function _resetEngineEnvOffLatchForTests() {
314
+ __engineEnvOffWarned = false;
315
+ }
217
316
  /**
218
317
  * Run the migrate-doctor with shared CLI formatting and exit handling.
219
318
  *
220
319
  * Both `migrate doctor` (two-word) and `migrate-doctor` (single-verb alias)
221
320
  * resolve to this helper to keep their behavior locked together.
321
+ *
322
+ * Phase 5: also handles --json (envelope on stdout, no human banner).
222
323
  */
223
324
  async function runMigrateDoctorCLI() {
224
325
  const fix = args.includes('--fix');
225
- const result = await runMigrateDoctor({ repoRoot: process.cwd(), fix });
226
- for (const r of result.results) {
227
- const mark = r.result.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
228
- console.log(`${mark} ${r.name}${r.result.message ? ` — ${r.result.message}` : ''}`);
229
- if (!r.result.ok && r.result.fixHint) {
230
- console.log(` \x1b[2mhint: ${r.result.fixHint}\x1b[0m`);
326
+ const json = args.includes('--json');
327
+ let docResult = null;
328
+ const code = await runUnderJsonMode({
329
+ command: 'migrate-doctor',
330
+ active: json,
331
+ payload: () => docResult ? {
332
+ results: docResult.results,
333
+ mutations: docResult.mutations ?? [],
334
+ migrationReportPath: docResult.migrationReportPath,
335
+ allOk: docResult.allOk,
336
+ } : {},
337
+ statusFor: exit => exit === 0 ? 'pass' : 'fail',
338
+ }, async () => {
339
+ docResult = await runMigrateDoctor({ repoRoot: process.cwd(), fix });
340
+ for (const r of docResult.results) {
341
+ const mark = r.result.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
342
+ console.log(`${mark} ${r.name}${r.result.message ? ` — ${r.result.message}` : ''}`);
343
+ if (!r.result.ok && r.result.fixHint) {
344
+ console.log(` \x1b[2mhint: ${r.result.fixHint}\x1b[0m`);
345
+ }
231
346
  }
232
- }
233
- if (result.mutations && result.mutations.length > 0) {
234
- console.log(`\n\x1b[1mFixes applied:\x1b[0m`);
235
- for (const m of result.mutations)
236
- console.log(` - ${m}`);
237
- }
238
- if (result.migrationReportPath) {
239
- console.log(`\n\x1b[2mMigration report: ${result.migrationReportPath}\x1b[0m`);
240
- }
241
- process.exit(result.allOk ? 0 : 1);
347
+ if (docResult.mutations && docResult.mutations.length > 0) {
348
+ console.log(`\n\x1b[1mFixes applied:\x1b[0m`);
349
+ for (const m of docResult.mutations)
350
+ console.log(` - ${m}`);
351
+ }
352
+ if (docResult.migrationReportPath) {
353
+ console.log(`\n\x1b[2mMigration report: ${docResult.migrationReportPath}\x1b[0m`);
354
+ }
355
+ return docResult.allOk ? 0 : 1;
356
+ });
357
+ process.exit(code);
242
358
  }
243
359
  function printUsage() {
244
- console.log(`
245
- Usage: claude-autopilot <command> [options] (legacy alias: guardrail)
246
-
247
- Commands:
248
- run Review git-changed files (default)
249
- scan Review any path — no git required
250
- report Render cached findings as a markdown report
251
- explain Deep-dive explanation + remediation for a specific finding
252
- ignore Interactively add findings to .guardrail-ignore
253
- watch Watch for file changes and re-run on each save
254
- pr Review a specific PR by number (auto-detects if on PR branch)
255
- fix Auto-fix cached findings using the configured LLM
256
- costs Show per-run cost summary
257
- ci Opinionated CI entrypoint (post comments + SARIF)
258
- init Scaffold guardrail.config.yaml + auto-detect migrate stack (writes .autopilot/stack.md)
259
- migrate Run database migrations via the stack-aware dispatcher
260
- migrate doctor Validate .autopilot/stack.md and skill manifests (alias: migrate-doctor)
261
- deploy Deploy via configured adapter (vercel | generic) — also: rollback, status
262
- setup Auto-detect stack, write config, install pre-push hook
263
- doctor Check prerequisites (alias: preflight)
264
- preflight Check prerequisites (alias: doctor)
265
- hook Install / remove the pre-push git hook
266
- baseline Manage the committed findings baseline (create|update|show|delete)
267
- triage Mark individual findings as accepted/dismissed
268
- pr-desc Generate a PR title / summary / test plan from the current diff
269
- council Multi-model review — dispatch the diff to N models and synthesize consensus
270
- mcp MCP server for Claude / ChatGPT integration
271
- autoregress Snapshot regression tests (run|diff|update|generate)
272
- lsp Language server — publishes findings as LSP diagnostics (stdin/stdout)
273
- worker Persistent review daemon for multi-terminal parallel usage (start|stop|status)
274
- test-gen Detect uncovered exports and generate test cases using the LLM
275
-
276
- Options (run):
277
- --base <ref> Git base ref for diff (default: HEAD~1)
278
- --config <path> Path to config file (default: ./guardrail.config.yaml)
279
- --files <a,b,c> Explicit comma-separated file list (skips git detection)
280
- --dry-run Show what would run without executing
281
- --diff Send git diff hunks instead of full files (~70% fewer tokens)
282
- --delta Only report findings new since last run (suppress pre-existing)
283
- --inline-comments Post per-line review comments on the PR diff
284
- --post-comments Post/update a summary comment on the open PR
285
- --format <text|sarif> Output format (default: text)
286
- --output <path> Output file path (required with --format sarif)
287
-
288
- Options (scan):
289
- <path> [path...] Files or directories to scan (or --all for entire codebase)
290
- --all Scan entire codebase
291
- --ask <question> Targeted question to inject into the LLM review prompt
292
- --focus <type> security | logic | performance (default: all)
293
- --dry-run List files that would be scanned without running
294
- --config <path> Path to config file
295
-
296
- Options (pr):
297
- <number> PR number to review (optional if on a PR branch)
298
- --no-post-comments Skip posting/updating PR summary comment
299
- --no-inline-comments Skip posting per-line inline annotations
300
- --config <path> Path to config file
301
-
302
- Options (fix):
303
- --severity <critical|warning|all> Which findings to fix (default: critical)
304
- --dry-run Preview fixes without writing files
305
- --config <path> Path to config file
306
-
307
- Options (watch):
308
- --config <path> Path to config file (default: ./guardrail.config.yaml)
309
- --debounce <ms> Debounce delay in ms (default: 300)
310
-
311
- Options (autoregress):
312
- --all Run/diff all snapshots
313
- --since <ref> Git ref for changed-files detection
314
- --snapshot <slug> Target a single snapshot
315
- --files <a,b,c> Explicit file list for generate (skips git detection)
316
-
317
- Options (migrate):
318
- --env <name> Target environment from .autopilot/stack.md (default: dev)
319
- --dry-run Run skill in dry-run mode (no side effects)
320
- --yes Required to apply prod migrations in CI
321
-
322
- Options (migrate doctor / migrate-doctor):
323
- --fix Apply auto-fixable mutations (legacy stack.md, skills/migrate/, schema_version)
324
-
325
- Options (deploy):
326
- --adapter <vercel|generic> Override deploy.adapter from config
327
- --config <path> Path to config file
328
- --ref <ref> Git ref (branch / tag) to deploy
329
- --sha <commit> Specific commit SHA to deploy
330
- --watch Stream build logs to stderr in real time (Vercel only)
331
- --to <deploy-id> Target deploy ID for 'deploy rollback'
332
- --pr <n> Post upserting deploy summary comment on the PR
333
-
334
- Subcommands (deploy):
335
- deploy Deploy via configured adapter
336
- deploy rollback Roll back to previous prod deploy
337
- deploy rollback --to <id> Roll back to a specific deploy
338
- deploy status Show current prod + last 5 builds
339
- `);
360
+ process.stdout.write(buildHelpText());
340
361
  }
341
362
  switch (subcommand) {
342
363
  case 'scan': {
@@ -349,72 +370,113 @@ switch (subcommand) {
349
370
  }
350
371
  const dryRun = boolFlag('dry-run');
351
372
  const all = boolFlag('all');
373
+ const json = boolFlag('json');
374
+ // v6.0.1 — engine knob. CLI flag wins; env / config / default resolved
375
+ // inside runScan once it's loaded the config file.
376
+ const cliEngine = parseEngineCliFlag();
352
377
  // Remaining non-flag args after 'scan' are paths
353
378
  const targets = args.slice(1).filter(a => !a.startsWith('--') && a !== ask && a !== focusArg && a !== config);
354
- const code = await runScan({
379
+ const code = await runUnderJsonMode({ command: 'scan', active: json }, () => runScan({
355
380
  configPath: config,
356
381
  targets: targets.length > 0 ? targets : undefined,
357
382
  all,
358
383
  ask,
359
384
  focus: focusArg,
360
385
  dryRun,
361
- });
386
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
387
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
388
+ }));
362
389
  process.exit(code);
363
390
  break;
364
391
  }
365
392
  case 'init': {
366
393
  // `init` and `setup` are aliases. Keep both supported — no nag banner.
367
394
  const force = args.includes('--force');
368
- await runSetup({ force });
369
- // After the existing init/setup logic, sniff for a migration tool and write
370
- // .autopilot/stack.md. Non-interactive: high-confidence single matches are
371
- // auto-selected; ambiguity / no-match downgrades to a TODO 'none@1' shape so
372
- // we don't block the user. (Interactive prompts come from the autopilot skill,
373
- // not the CLI.)
374
- try {
375
- const result = await initMigrate({
376
- repoRoot: process.cwd(),
377
- force,
378
- });
379
- for (const ws of result.workspaces) {
380
- const rel = ws.workspace === process.cwd() ? '.' : ws.workspace;
381
- console.log(`\x1b[2m[init-migrate] ${ws.action} ${rel}/.autopilot/stack.md (skill: ${ws.skill})\x1b[0m`);
395
+ const json = boolFlag('json');
396
+ const code = await runUnderJsonMode({ command: 'init', active: json }, async () => {
397
+ await runSetup({ force });
398
+ // After the existing init/setup logic, sniff for a migration tool and write
399
+ // .autopilot/stack.md. Non-interactive: high-confidence single matches are
400
+ // auto-selected; ambiguity / no-match downgrades to a TODO 'none@1' shape so
401
+ // we don't block the user. (Interactive prompts come from the autopilot skill,
402
+ // not the CLI.)
403
+ try {
404
+ const result = await initMigrate({
405
+ repoRoot: process.cwd(),
406
+ force,
407
+ });
408
+ for (const ws of result.workspaces) {
409
+ const rel = ws.workspace === process.cwd() ? '.' : ws.workspace;
410
+ console.log(`\x1b[2m[init-migrate] ${ws.action} ${rel}/.autopilot/stack.md (skill: ${ws.skill})\x1b[0m`);
411
+ }
382
412
  }
383
- }
384
- catch (err) {
385
- if (err instanceof NoMigrationToolDetectedError) {
386
- // No high-confidence match fall back to skipMigrate shape so the user
387
- // can edit it later. This matches the auto-detection contract documented
388
- // in the v5.2.0 CHANGELOG.
389
- try {
390
- await initMigrate({
391
- repoRoot: process.cwd(),
392
- force,
393
- skipMigrate: true,
394
- });
395
- console.log(`\x1b[33m[init-migrate] No migration tool detected — wrote 'none@1' stack.md (edit .autopilot/stack.md to configure)\x1b[0m`);
413
+ catch (err) {
414
+ if (err instanceof NoMigrationToolDetectedError) {
415
+ // No high-confidence match — fall back to skipMigrate shape so the user
416
+ // can edit it later. This matches the auto-detection contract documented
417
+ // in the v5.2.0 CHANGELOG.
418
+ try {
419
+ await initMigrate({
420
+ repoRoot: process.cwd(),
421
+ force,
422
+ skipMigrate: true,
423
+ });
424
+ console.log(`\x1b[33m[init-migrate] No migration tool detected — wrote 'none@1' stack.md (edit .autopilot/stack.md to configure)\x1b[0m`);
425
+ }
426
+ catch (fallbackErr) {
427
+ console.error(`\x1b[31m[init-migrate] failed: ${fallbackErr.message}\x1b[0m`);
428
+ return 1;
429
+ }
396
430
  }
397
- catch (fallbackErr) {
398
- console.error(`\x1b[31m[init-migrate] failed: ${fallbackErr.message}\x1b[0m`);
431
+ else {
432
+ console.error(`\x1b[31m[init-migrate] failed: ${err.message}\x1b[0m`);
433
+ return 1;
399
434
  }
400
435
  }
401
- else {
402
- console.error(`\x1b[31m[init-migrate] failed: ${err.message}\x1b[0m`);
403
- }
404
- }
436
+ return 0;
437
+ });
438
+ if (json)
439
+ process.exit(code);
405
440
  break;
406
441
  }
407
442
  case 'doctor':
408
443
  case 'preflight': {
409
- const result = await runDoctor();
410
- process.exit(result.blockers > 0 ? 1 : 0);
444
+ const json = boolFlag('json');
445
+ let docResult = null;
446
+ const code = await runUnderJsonMode({
447
+ command: subcommand,
448
+ active: json,
449
+ payload: () => docResult ? {
450
+ blockers: docResult.blockers,
451
+ warnings: docResult.warnings,
452
+ } : {},
453
+ }, async () => {
454
+ docResult = await runDoctor();
455
+ return docResult.blockers > 0 ? 1 : 0;
456
+ });
457
+ process.exit(code);
411
458
  break;
412
459
  }
413
460
  case 'help':
414
461
  case '--help':
415
- case '-h':
462
+ case '-h': {
463
+ // `claude-autopilot help <command>` — focused per-command help. Falls back
464
+ // to the full two-level listing with an "unknown command" notice + exit 1
465
+ // when the named verb isn't documented.
466
+ const target = args[1];
467
+ if (target && !target.startsWith('-')) {
468
+ const focused = buildCommandHelpText(target);
469
+ if (focused !== null) {
470
+ process.stdout.write(focused);
471
+ process.exit(0);
472
+ }
473
+ process.stderr.write(`\x1b[31m[claude-autopilot] unknown command: "${target}"\x1b[0m\n`);
474
+ process.stdout.write(buildHelpText());
475
+ process.exit(1);
476
+ }
416
477
  printUsage();
417
478
  break;
479
+ }
418
480
  case 'watch': {
419
481
  const config = flag('config');
420
482
  const debounceArg = flag('debounce');
@@ -452,7 +514,8 @@ switch (subcommand) {
452
514
  process.exit(1);
453
515
  }
454
516
  const newOnly = boolFlag('new-only');
455
- const code = await runCommand({
517
+ const json = boolFlag('json');
518
+ const code = await runUnderJsonMode({ command: 'run', active: json }, () => runCommand({
456
519
  base,
457
520
  configPath: config,
458
521
  files: filesArg ? filesArg.split(',').map(f => f.trim()) : undefined,
@@ -466,7 +529,7 @@ switch (subcommand) {
466
529
  format: formatArg,
467
530
  outputPath,
468
531
  skipReview: staticOnly,
469
- });
532
+ }));
470
533
  process.exit(code);
471
534
  break;
472
535
  }
@@ -479,7 +542,8 @@ switch (subcommand) {
479
542
  const diff = boolFlag('diff');
480
543
  const newOnly = boolFlag('new-only');
481
544
  const failOnArg = flag('fail-on');
482
- const code = await runCi({
545
+ const json = boolFlag('json');
546
+ const code = await runUnderJsonMode({ command: 'ci', active: json }, () => runCi({
483
547
  configPath: config,
484
548
  base,
485
549
  sarifOutput: outputPath,
@@ -488,7 +552,7 @@ switch (subcommand) {
488
552
  diff,
489
553
  newOnly,
490
554
  failOn: failOnArg,
491
- });
555
+ }));
492
556
  process.exit(code);
493
557
  break;
494
558
  }
@@ -497,21 +561,35 @@ switch (subcommand) {
497
561
  const sub = args[1] ?? 'show';
498
562
  const note = flag('note');
499
563
  const config = flag('config');
500
- const code = await rb(sub, { cwd: process.cwd(), note, baselinePath: config });
564
+ const json = boolFlag('json');
565
+ const code = await runUnderJsonMode({ command: `baseline ${sub}`, active: json }, () => rb(sub, { cwd: process.cwd(), note, baselinePath: config }));
501
566
  process.exit(code);
502
567
  break;
503
568
  }
504
569
  case 'pr': {
570
+ // v6.0.9 — engine-wrap shell for the `pr` pipeline phase. Side-effecting
571
+ // (posts/updates a PR comment + inline review comments via the `gh` CLI
572
+ // inside runCommand). Declared `idempotent: false, hasSideEffects: true`
573
+ // with a `github-pr` externalRef recorded before the inner pipeline
574
+ // runs. See the long declaration note in src/cli/pr.ts for the
575
+ // per-call breakdown of what `gh` mutations happen and why the
576
+ // declaration matches the v6 spec table.
505
577
  const config = flag('config');
506
578
  const noPostComments = boolFlag('no-post-comments');
507
579
  const noInlineComments = boolFlag('no-inline-comments');
580
+ const json = boolFlag('json');
508
581
  const prNumber = args.slice(1).find(a => !a.startsWith('--') && /^\d+$/.test(a));
509
- const code = await runPr({
582
+ // v6.0.9 engine knob. CLI flag wins; env / config / default resolved
583
+ // inside runPr once it's loaded the config file.
584
+ const cliEngine = parseEngineCliFlag();
585
+ const code = await runUnderJsonMode({ command: 'pr', active: json }, () => runPr({
510
586
  configPath: config,
511
587
  prNumber,
512
588
  noPostComments,
513
589
  noInlineComments,
514
- });
590
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
591
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
592
+ }));
515
593
  process.exit(code);
516
594
  break;
517
595
  }
@@ -542,19 +620,26 @@ switch (subcommand) {
542
620
  }
543
621
  const dryRun = boolFlag('dry-run');
544
622
  const noVerify = boolFlag('no-verify');
545
- const code = await runFix({
623
+ const json = boolFlag('json');
624
+ // v6.0.2 — engine knob. CLI flag wins; env / config / default resolved
625
+ // inside runFix once it's loaded the config file.
626
+ const cliEngine = parseEngineCliFlag();
627
+ const code = await runUnderJsonMode({ command: 'fix', active: json }, () => runFix({
546
628
  configPath: config,
547
629
  severity: severityArg,
548
630
  dryRun,
549
631
  noVerify,
550
- });
632
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
633
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
634
+ }));
551
635
  process.exit(code);
552
636
  break;
553
637
  }
554
638
  case 'triage': {
555
639
  const sub = args[1];
556
640
  const rest = args.slice(2);
557
- const code = await runTriage(sub, rest);
641
+ const json = boolFlag('json');
642
+ const code = await runUnderJsonMode({ command: `triage${sub ? ` ${sub}` : ''}`, active: json }, () => runTriage(sub, rest));
558
643
  process.exit(code);
559
644
  break;
560
645
  }
@@ -563,15 +648,16 @@ switch (subcommand) {
563
648
  const base = flag('base');
564
649
  const dryRun = boolFlag('dry-run');
565
650
  const verify = boolFlag('verify');
651
+ const json = boolFlag('json');
566
652
  const targets = args.slice(1).filter(a => !a.startsWith('--') && a !== config && a !== base);
567
- const code = await runTestGen({
653
+ const code = await runUnderJsonMode({ command: 'test-gen', active: json }, () => runTestGen({
568
654
  cwd: process.cwd(),
569
655
  configPath: config,
570
656
  targets: targets.length > 0 ? targets : undefined,
571
657
  base,
572
658
  dryRun,
573
659
  verify,
574
- });
660
+ }));
575
661
  process.exit(code);
576
662
  break;
577
663
  }
@@ -581,12 +667,18 @@ switch (subcommand) {
581
667
  const base = baseIdx !== -1 ? args[baseIdx + 1] : undefined;
582
668
  const outputIdx = args.indexOf('--output');
583
669
  const output = outputIdx !== -1 ? args[outputIdx + 1] : undefined;
584
- await runPrDesc({
585
- base,
586
- post: args.includes('--post'),
587
- yes: args.includes('--yes'),
588
- output,
670
+ const json = boolFlag('json');
671
+ const code = await runUnderJsonMode({ command: 'pr-desc', active: json }, async () => {
672
+ await runPrDesc({
673
+ base,
674
+ post: args.includes('--post'),
675
+ yes: args.includes('--yes'),
676
+ output,
677
+ });
678
+ return 0;
589
679
  });
680
+ if (json)
681
+ process.exit(code);
590
682
  break;
591
683
  }
592
684
  case 'lsp': {
@@ -595,22 +687,217 @@ switch (subcommand) {
595
687
  }
596
688
  case 'costs': {
597
689
  const { runCosts } = await import("./costs.js");
598
- const code = await runCosts();
690
+ const json = boolFlag('json');
691
+ const config = flag('config');
692
+ // v6.0.2 — engine knob. CLI flag wins; env / config / default resolved
693
+ // inside runCosts once it's loaded the config file.
694
+ const cliEngine = parseEngineCliFlag();
695
+ const code = await runUnderJsonMode({ command: 'costs', active: json }, () => runCosts({
696
+ ...(config !== undefined ? { configPath: config } : {}),
697
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
698
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
699
+ }));
700
+ process.exit(code);
701
+ break;
702
+ }
703
+ case 'plan': {
704
+ // v6.0.4 — engine-wrap shell for the `plan` pipeline phase. The actual
705
+ // LLM-driven planning content is produced by the Claude Code
706
+ // superpowers:writing-plans skill; this CLI verb provides a
707
+ // checkpointable phase shell so v6 pipeline runs can record a `plan`
708
+ // entry. Mirrors the costs/scan/fix dispatcher shape.
709
+ const { runPlan } = await import("./plan.js");
710
+ const json = boolFlag('json');
711
+ const config = flag('config');
712
+ const specPath = flag('spec');
713
+ const outputPath = flag('output');
714
+ const cliEngine = parseEngineCliFlag();
715
+ const code = await runUnderJsonMode({ command: 'plan', active: json }, () => runPlan({
716
+ ...(config !== undefined ? { configPath: config } : {}),
717
+ ...(specPath !== undefined ? { specPath } : {}),
718
+ ...(outputPath !== undefined ? { outputPath } : {}),
719
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
720
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
721
+ }));
722
+ process.exit(code);
723
+ break;
724
+ }
725
+ case 'review': {
726
+ // v6.0.4 — engine-wrap shell for the `review` pipeline phase. The actual
727
+ // LLM-driven review content is produced by the Claude Code review skills
728
+ // (`/review`, `/review-2pass`, `pr-review-toolkit:review-pr`). PR-side
729
+ // comment posting lives in `claude-autopilot pr --inline-comments` /
730
+ // `--post-comments`; this verb does not post anywhere. See the long
731
+ // deviation note in src/cli/review.ts for the idempotent / hasSideEffects
732
+ // declaration rationale.
733
+ const { runReview } = await import("./review.js");
734
+ const json = boolFlag('json');
735
+ const config = flag('config');
736
+ const context = flag('context');
737
+ const outputPath = flag('output');
738
+ const cliEngine = parseEngineCliFlag();
739
+ const code = await runUnderJsonMode({ command: 'review', active: json }, () => runReview({
740
+ ...(config !== undefined ? { configPath: config } : {}),
741
+ ...(context !== undefined ? { context } : {}),
742
+ ...(outputPath !== undefined ? { outputPath } : {}),
743
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
744
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
745
+ }));
746
+ process.exit(code);
747
+ break;
748
+ }
749
+ case 'validate': {
750
+ // v6.0.5 — engine-wrap shell for the `validate` pipeline phase. The
751
+ // actual validation pipeline (static checks, auto-fix, tests, Codex
752
+ // review with auto-fix, bugbot triage) lives in the Claude Code
753
+ // `/validate` skill; this verb provides a checkpointable phase shell so
754
+ // v6 pipeline runs can record a `validate` entry. Mirrors the
755
+ // plan / review dispatcher shape. See the long deviation note in
756
+ // src/cli/validate.ts for the externalRefs / sarif-artifact
757
+ // declaration rationale.
758
+ const { runValidate } = await import("./validate.js");
759
+ const json = boolFlag('json');
760
+ const config = flag('config');
761
+ const context = flag('context');
762
+ const outputPath = flag('output');
763
+ const cliEngine = parseEngineCliFlag();
764
+ const code = await runUnderJsonMode({ command: 'validate', active: json }, () => runValidate({
765
+ ...(config !== undefined ? { configPath: config } : {}),
766
+ ...(context !== undefined ? { context } : {}),
767
+ ...(outputPath !== undefined ? { outputPath } : {}),
768
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
769
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
770
+ }));
771
+ process.exit(code);
772
+ break;
773
+ }
774
+ case 'implement': {
775
+ // v6.0.7 — engine-wrap shell for the `implement` pipeline phase. The
776
+ // actual implement loop (read plan → dispatch subagents one per plan
777
+ // phase via `subagent-driven-development` → write code → run tests →
778
+ // commit → optionally push via `commit-push-pr`) lives in the Claude
779
+ // Code `claude-autopilot` skill; this verb provides a checkpointable
780
+ // phase shell so v6 pipeline runs can record an `implement` entry.
781
+ // Mirrors the plan / review / validate dispatcher shape. See the long
782
+ // deviation note in src/cli/implement.ts for the idempotent /
783
+ // hasSideEffects / git-remote-push declaration rationale.
784
+ const { runImplement } = await import("./implement.js");
785
+ const json = boolFlag('json');
786
+ const config = flag('config');
787
+ const context = flag('context');
788
+ const plan = flag('plan');
789
+ const outputPath = flag('output');
790
+ const cliEngine = parseEngineCliFlag();
791
+ const code = await runUnderJsonMode({ command: 'implement', active: json }, () => runImplement({
792
+ ...(config !== undefined ? { configPath: config } : {}),
793
+ ...(context !== undefined ? { context } : {}),
794
+ ...(plan !== undefined ? { plan } : {}),
795
+ ...(outputPath !== undefined ? { outputPath } : {}),
796
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
797
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
798
+ }));
599
799
  process.exit(code);
600
800
  break;
601
801
  }
802
+ case 'autopilot': {
803
+ // v6.2.0 — multi-phase orchestrator. One runId across all phases.
804
+ // Engine-on REQUIRED (rejected at pre-flight if --no-engine / env=off
805
+ // / config=false). v6.2.0 ships --mode=full (scan → spec → plan →
806
+ // implement); v6.2.1 extends to scan → spec → plan → implement →
807
+ // migrate → pr; v6.2.2 adds the `--json` outer envelope.
808
+ const { runAutopilot, runAutopilotWithJsonEnvelope } = await import("./autopilot.js");
809
+ const json = boolFlag('json');
810
+ const modeArg = flag('mode');
811
+ if (modeArg !== undefined && modeArg !== 'full') {
812
+ // In --json mode emit the spec envelope instead of stderr text so
813
+ // CI consumers get a deterministic shape even on this synchronous
814
+ // pre-run validation failure.
815
+ if (json) {
816
+ const { writeAutopilotEnvelope } = await import("./json-envelope.js");
817
+ writeAutopilotEnvelope({
818
+ runId: null,
819
+ status: 'failed',
820
+ exitCode: 1,
821
+ phases: [],
822
+ totalCostUSD: 0,
823
+ durationMs: 0,
824
+ errorCode: 'invalid_config',
825
+ errorMessage: `--mode "${modeArg}" not supported (use --mode=full)`,
826
+ });
827
+ process.exit(1);
828
+ }
829
+ console.error(`\x1b[31m[claude-autopilot] invalid_config: --mode "${modeArg}" not supported (use --mode=full)\x1b[0m`);
830
+ console.error(`\x1b[2m --mode=fix and --mode=review land in v6.2.x+; use --phases=<csv> for custom lists\x1b[0m`);
831
+ process.exit(1);
832
+ }
833
+ const phasesArg = flag('phases');
834
+ const phases = phasesArg
835
+ ? phasesArg.split(',').map(s => s.trim()).filter(s => s.length > 0)
836
+ : undefined;
837
+ const budgetRaw = flag('budget');
838
+ let budgetUSD;
839
+ if (budgetRaw !== undefined) {
840
+ const parsed = Number.parseFloat(budgetRaw);
841
+ if (!Number.isFinite(parsed) || parsed <= 0) {
842
+ if (json) {
843
+ const { writeAutopilotEnvelope } = await import("./json-envelope.js");
844
+ writeAutopilotEnvelope({
845
+ runId: null,
846
+ status: 'failed',
847
+ exitCode: 1,
848
+ phases: [],
849
+ totalCostUSD: 0,
850
+ durationMs: 0,
851
+ errorCode: 'invalid_config',
852
+ errorMessage: `--budget must be a positive number, got "${budgetRaw}"`,
853
+ });
854
+ process.exit(1);
855
+ }
856
+ console.error(`\x1b[31m[claude-autopilot] invalid_config: --budget must be a positive number, got "${budgetRaw}"\x1b[0m`);
857
+ process.exit(1);
858
+ }
859
+ budgetUSD = parsed;
860
+ }
861
+ const cliEngine = parseEngineCliFlag();
862
+ const noUpload = boolFlag('no-upload');
863
+ if (json) {
864
+ const exitCode = await runAutopilotWithJsonEnvelope({
865
+ cwd: process.cwd(),
866
+ mode: 'full',
867
+ ...(phases !== undefined ? { phases } : {}),
868
+ ...(budgetUSD !== undefined ? { budgetUSD } : {}),
869
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
870
+ ...(noUpload ? { noUpload: true } : {}),
871
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
872
+ });
873
+ process.exit(exitCode);
874
+ }
875
+ const result = await runAutopilot({
876
+ cwd: process.cwd(),
877
+ mode: 'full',
878
+ ...(phases !== undefined ? { phases } : {}),
879
+ ...(budgetUSD !== undefined ? { budgetUSD } : {}),
880
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
881
+ ...(noUpload ? { noUpload: true } : {}),
882
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
883
+ });
884
+ process.exit(result.exitCode);
885
+ break;
886
+ }
602
887
  case 'report': {
603
888
  const outputPath = flag('output');
604
889
  const trend = boolFlag('trend');
605
- const code = await runReport({ output: outputPath, trend });
890
+ const json = boolFlag('json');
891
+ const code = await runUnderJsonMode({ command: 'report', active: json }, () => runReport({ output: outputPath, trend }));
606
892
  process.exit(code);
607
893
  break;
608
894
  }
609
895
  case 'explain': {
610
896
  const config = flag('config');
897
+ const json = boolFlag('json');
611
898
  // Target is the first non-flag arg after 'explain'
612
899
  const target = args.slice(1).find(a => !a.startsWith('--'));
613
- const code = await runExplain({ configPath: config, target });
900
+ const code = await runUnderJsonMode({ command: 'explain', active: json }, () => runExplain({ configPath: config, target }));
614
901
  process.exit(code);
615
902
  break;
616
903
  }
@@ -624,11 +911,17 @@ switch (subcommand) {
624
911
  case 'setup': {
625
912
  const force = args.includes('--force');
626
913
  const profileArg = flag('profile');
914
+ const json = boolFlag('json');
627
915
  if (profileArg && !['security-strict', 'team', 'solo'].includes(profileArg)) {
628
916
  console.error(`\x1b[31m[claude-autopilot] --profile must be "security-strict", "team", or "solo"\x1b[0m`);
629
917
  process.exit(1);
630
918
  }
631
- await runSetup({ force, profile: profileArg });
919
+ const code = await runUnderJsonMode({ command: 'setup', active: json }, async () => {
920
+ await runSetup({ force, profile: profileArg });
921
+ return 0;
922
+ });
923
+ if (json)
924
+ process.exit(code);
632
925
  break;
633
926
  }
634
927
  case 'worker': {
@@ -638,19 +931,33 @@ switch (subcommand) {
638
931
  process.exit(code);
639
932
  break;
640
933
  }
934
+ case 'scaffold': {
935
+ // v7.2.0 — `claude-autopilot scaffold --from-spec <path>`
936
+ const fromSpec = flag('from-spec');
937
+ const dryRun = boolFlag('dry-run');
938
+ if (!fromSpec) {
939
+ console.error(`\x1b[31m[claude-autopilot] scaffold requires --from-spec <path>\x1b[0m`);
940
+ console.error(` Example: claude-autopilot scaffold --from-spec docs/specs/foo.md`);
941
+ process.exit(1);
942
+ }
943
+ await runScaffold({ specPath: fromSpec, dryRun });
944
+ process.exit(0);
945
+ break;
946
+ }
641
947
  case 'council': {
642
948
  const config = flag('config');
643
949
  const prompt = flag('prompt');
644
950
  const contextFile = flag('context-file');
645
951
  const dryRun = boolFlag('dry-run');
646
952
  const noSynthesize = boolFlag('no-synthesize');
647
- const code = await runCouncilCmd({
953
+ const json = boolFlag('json');
954
+ const code = await runUnderJsonMode({ command: 'council', active: json }, () => runCouncilCmd({
648
955
  prompt,
649
956
  contextFile,
650
957
  configPath: config,
651
958
  dryRun,
652
959
  noSynthesize,
653
- });
960
+ }));
654
961
  process.exit(code);
655
962
  break;
656
963
  }
@@ -677,11 +984,12 @@ switch (subcommand) {
677
984
  break;
678
985
  }
679
986
  case 'migrate-v4': {
680
- const code = await runMigrateV4({
987
+ const json = boolFlag('json');
988
+ const code = await runUnderJsonMode({ command: 'migrate-v4', active: json }, () => runMigrateV4({
681
989
  cwd: flag('path') ?? process.cwd(),
682
990
  write: boolFlag('write'),
683
991
  undo: boolFlag('undo'),
684
- });
992
+ }));
685
993
  process.exit(code);
686
994
  break;
687
995
  }
@@ -695,41 +1003,49 @@ switch (subcommand) {
695
1003
  break;
696
1004
  }
697
1005
  // Plain `migrate [--env <name>] [--dry-run] [--yes]` → dispatcher.
1006
+ // v6.0.8: routed through `runMigrate` (src/cli/migrate.ts) which
1007
+ // wraps the dispatcher in a `RunPhase<MigrateInput, MigrateOutput>`
1008
+ // with `--engine` / `--no-engine` precedence. Engine-off is byte-for-
1009
+ // byte identical to v6.0.7 — same dispatch shape, same render lines.
698
1010
  const envName = flag('env') ?? 'dev';
699
1011
  const dryRun = boolFlag('dry-run');
700
1012
  const yesFlag = boolFlag('yes');
701
- // Read package version for the runtime handshake.
702
- const root = findPackageRoot(import.meta.url);
703
- let runtimeVersion = 'unknown';
704
- if (root) {
705
- try {
706
- const nodeFs = await import('node:fs');
707
- const nodePath = await import('node:path');
708
- const pkg = JSON.parse(nodeFs.readFileSync(nodePath.join(root, 'package.json'), 'utf8'));
709
- runtimeVersion = pkg.version;
710
- }
711
- catch {
712
- /* fall through with 'unknown' — handshake will fail closed */
713
- }
714
- }
715
- const result = await runMigrateDispatch({
716
- repoRoot: process.cwd(),
717
- env: envName,
718
- yesFlag,
719
- nonInteractive: !process.stdin.isTTY,
720
- currentRuntimeVersion: runtimeVersion,
721
- dryRun,
1013
+ const json = boolFlag('json');
1014
+ const cliEngine = parseEngineCliFlag();
1015
+ // Capture migrate result in an outer ref so the wrapper's payload
1016
+ // callback can surface its structured fields in --json mode.
1017
+ let migrateResult = null;
1018
+ const code = await runUnderJsonMode({
1019
+ command: 'migrate',
1020
+ active: json,
1021
+ payload: () => migrateResult ? {
1022
+ migrate: {
1023
+ status: migrateResult.status,
1024
+ reasonCode: migrateResult.reasonCode,
1025
+ appliedMigrations: migrateResult.appliedMigrations,
1026
+ nextActions: migrateResult.nextActions,
1027
+ },
1028
+ ...(migrateResult.nextActions.length > 0 ? { nextActions: migrateResult.nextActions } : {}),
1029
+ } : {},
1030
+ statusFor: exit => {
1031
+ if (!migrateResult)
1032
+ return exit === 0 ? 'pass' : 'fail';
1033
+ return migrateResult.status === 'applied' || migrateResult.status === 'skipped' ? 'pass' : 'fail';
1034
+ },
1035
+ }, async () => {
1036
+ const out = await runMigrate({
1037
+ cwd: process.cwd(),
1038
+ env: envName,
1039
+ dryRun,
1040
+ yesFlag,
1041
+ nonInteractive: json || !process.stdin.isTTY,
1042
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
1043
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
1044
+ });
1045
+ migrateResult = out.result;
1046
+ return out.exitCode;
722
1047
  });
723
- const ok = result.status === 'applied' || result.status === 'skipped';
724
- const color = ok ? '\x1b[32m' : '\x1b[31m';
725
- console.log(`${color}[migrate] status=${result.status} reason=${result.reasonCode}\x1b[0m`);
726
- if (result.appliedMigrations.length > 0) {
727
- console.log(` applied: ${result.appliedMigrations.join(', ')}`);
728
- }
729
- if (result.nextActions.length > 0) {
730
- console.log(` next: ${result.nextActions.join('; ')}`);
731
- }
732
- process.exit(ok ? 0 : 1);
1048
+ process.exit(code);
733
1049
  break;
734
1050
  }
735
1051
  case 'migrate-doctor': {
@@ -741,28 +1057,33 @@ switch (subcommand) {
741
1057
  case 'deploy': {
742
1058
  const config = flag('config');
743
1059
  const adapterArg = flag('adapter');
744
- if (adapterArg && !['vercel', 'generic'].includes(adapterArg)) {
745
- console.error(`\x1b[31m[claude-autopilot] --adapter must be "vercel" or "generic"\x1b[0m`);
1060
+ // Keep this list in sync with `DeployConfig.adapter` in
1061
+ // src/adapters/deploy/types.ts and the factory in
1062
+ // src/adapters/deploy/index.ts.
1063
+ const ADAPTER_NAMES = ['vercel', 'fly', 'render', 'generic'];
1064
+ if (adapterArg && !ADAPTER_NAMES.includes(adapterArg)) {
1065
+ console.error(`\x1b[31m[claude-autopilot] --adapter must be one of: ${ADAPTER_NAMES.join(', ')}\x1b[0m`);
746
1066
  process.exit(1);
747
1067
  }
748
1068
  // Phase 3 — `deploy rollback` and `deploy status` subverbs. The first
749
1069
  // non-flag positional after `deploy` selects the verb. The historic
750
1070
  // `claude-autopilot deploy` (no subverb) keeps calling runDeploy.
751
1071
  const subverb = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
1072
+ const json = boolFlag('json');
752
1073
  if (subverb === 'rollback') {
753
1074
  const to = flag('to');
754
- const code = await runDeployRollback({
1075
+ const code = await runUnderJsonMode({ command: 'deploy rollback', active: json }, () => runDeployRollback({
755
1076
  configPath: config,
756
1077
  adapterOverride: adapterArg,
757
1078
  to,
758
- });
1079
+ }));
759
1080
  process.exit(code);
760
1081
  }
761
1082
  if (subverb === 'status') {
762
- const code = await runDeployStatus({
1083
+ const code = await runUnderJsonMode({ command: 'deploy status', active: json }, () => runDeployStatus({
763
1084
  configPath: config,
764
1085
  adapterOverride: adapterArg,
765
- });
1086
+ }));
766
1087
  process.exit(code);
767
1088
  }
768
1089
  if (subverb !== undefined) {
@@ -782,42 +1103,250 @@ switch (subcommand) {
782
1103
  }
783
1104
  prNum = n;
784
1105
  }
785
- const code = await runDeploy({
1106
+ const code = await runUnderJsonMode({ command: 'deploy', active: json }, () => runDeploy({
786
1107
  configPath: config,
787
1108
  adapterOverride: adapterArg,
788
1109
  ref,
789
1110
  commitSha,
790
1111
  watch,
791
1112
  pr: prNum,
792
- });
1113
+ }));
793
1114
  process.exit(code);
794
1115
  break;
795
1116
  }
796
1117
  case 'brainstorm': {
797
- // `brainstorm` is the front of the pipeline and is implemented as a Claude
798
- // Code skill (superpowers:brainstorming autopilot), not a standalone CLI.
799
- // The welcome screen advertises `claude-autopilot brainstorm "..."` as the
800
- // primary quickstart, so users WILL land here. Give them clear instructions
801
- // instead of a generic "Unknown subcommand" rejection. Only reference CLI
802
- // subcommands that actually route (verified by the welcome regression test).
803
- console.log(`
804
- \x1b[1m[brainstorm]\x1b[0m The pipeline entry point is a Claude Code skill, not a CLI subcommand.
805
-
806
- Invoke it from Claude Code:
807
-
808
- \x1b[36m/brainstorm\x1b[0m Interactive spec writing
809
- \x1b[36m/autopilot\x1b[0m Full pipeline from an approved spec
810
- \x1b[36m/migrate\x1b[0m Database migration phase (stack-dependent)
811
-
812
- From the terminal, the CLI subset exposes only the individual review-phase subcommands:
813
-
814
- \x1b[36mclaude-autopilot run --base main\x1b[0m Just the review phase
815
- \x1b[36mclaude-autopilot doctor\x1b[0m Check prerequisites (incl. superpowers plugin)
816
- \x1b[36mclaude-autopilot migrate-v4\x1b[0m Codemod for v4 → v5 repo migration (not a pipeline phase)
817
-
818
- Full pipeline docs: https://github.com/axledbetter/claude-autopilot#the-pipeline-phase-by-phase
819
- `);
820
- process.exit(0);
1118
+ // v6.0.3 — `brainstorm` is wrapped through `runPhase`. Engine-off path
1119
+ // is byte-for-byte identical to v6.0.2 (advisory print pointing at the
1120
+ // Claude Code skill); engine-on path creates a run dir + emits
1121
+ // run.start/phase.start/phase.success/run.complete events. See
1122
+ // src/cli/brainstorm.ts for the deviation rationale on
1123
+ // `idempotent: true` vs. the spec table's `idempotent: no`.
1124
+ const { runBrainstorm } = await import("./brainstorm.js");
1125
+ const json = boolFlag('json');
1126
+ const config = flag('config');
1127
+ const cliEngine = parseEngineCliFlag();
1128
+ if (json) {
1129
+ // --json mode: surface the resume hint via nextActions, not a banner.
1130
+ // Mirror the v6.0.2 envelope shape so existing consumers (the
1131
+ // json-channel-discipline test, MCP wrappers) don't break. The phase
1132
+ // body itself runs in silent mode — engine-on still produces
1133
+ // run-state artifacts; engine-off short-circuits to 0.
1134
+ const code = await runUnderJsonMode({
1135
+ command: 'brainstorm',
1136
+ active: true,
1137
+ payload: () => ({
1138
+ note: 'brainstorm is a Claude Code skill, not a CLI subcommand',
1139
+ nextActions: [
1140
+ 'Invoke /brainstorm from Claude Code for interactive spec writing',
1141
+ 'Then /autopilot to run the full pipeline from an approved spec',
1142
+ ],
1143
+ }),
1144
+ }, () => runBrainstorm({
1145
+ ...(config !== undefined ? { configPath: config } : {}),
1146
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
1147
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
1148
+ __silent: true,
1149
+ }));
1150
+ process.exit(code);
1151
+ }
1152
+ const code = await runBrainstorm({
1153
+ ...(config !== undefined ? { configPath: config } : {}),
1154
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
1155
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
1156
+ });
1157
+ process.exit(code);
1158
+ break;
1159
+ }
1160
+ case 'spec': {
1161
+ // v6.0.3 — `spec` is wrapped through `runPhase`. Same shape as
1162
+ // brainstorm: engine-off prints an advisory pointing at the Claude
1163
+ // Code skill; engine-on creates a run dir + emits lifecycle events.
1164
+ // Like brainstorm, the deviation from the spec table's
1165
+ // `idempotent: no` is justified inline at the top of src/cli/spec.ts.
1166
+ const { runSpec } = await import("./spec.js");
1167
+ const json = boolFlag('json');
1168
+ const config = flag('config');
1169
+ const cliEngine = parseEngineCliFlag();
1170
+ if (json) {
1171
+ const code = await runUnderJsonMode({
1172
+ command: 'spec',
1173
+ active: true,
1174
+ payload: () => ({
1175
+ note: 'spec is a Claude Code skill, not a CLI subcommand',
1176
+ nextActions: [
1177
+ 'Approve a brainstorm output, then invoke /autopilot from Claude Code',
1178
+ 'The autopilot skill writes the implementation plan + executes the pipeline',
1179
+ ],
1180
+ }),
1181
+ }, () => runSpec({
1182
+ ...(config !== undefined ? { configPath: config } : {}),
1183
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
1184
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
1185
+ __silent: true,
1186
+ }));
1187
+ process.exit(code);
1188
+ }
1189
+ const code = await runSpec({
1190
+ ...(config !== undefined ? { configPath: config } : {}),
1191
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
1192
+ envEngine: process.env.CLAUDE_AUTOPILOT_ENGINE,
1193
+ });
1194
+ process.exit(code);
1195
+ break;
1196
+ }
1197
+ case 'runs': {
1198
+ // v6 Phase 3 — umbrella verb. Sub-verbs: list, show, gc, delete, doctor.
1199
+ const sub = args[1];
1200
+ const json = boolFlag('json');
1201
+ const cwd = process.cwd();
1202
+ if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
1203
+ const focused = (await import("./help-text.js")).buildCommandHelpText('runs');
1204
+ process.stdout.write(focused ?? buildHelpText());
1205
+ process.exit(0);
1206
+ }
1207
+ const { runRunsList, runRunsShow, runRunsGc, runRunsDelete, runRunsDoctor, } = await import("./runs.js");
1208
+ let result;
1209
+ switch (sub) {
1210
+ case 'list': {
1211
+ result = await runRunsList({ cwd, status: flag('status'), json });
1212
+ break;
1213
+ }
1214
+ case 'show': {
1215
+ const events = boolFlag('events');
1216
+ const tailRaw = flag('events-tail');
1217
+ const eventsTail = tailRaw ? parseInt(tailRaw, 10) : undefined;
1218
+ // Filter out value-flag *values* — without this, `runs show
1219
+ // --events-tail 5 <ULID>` would resolve runId to '5'. Same pattern
1220
+ // as the scan case above. Caught by Cursor Bugbot on PR #88
1221
+ // (MEDIUM).
1222
+ const runId = args.slice(2).find(a => !a.startsWith('--') && a !== tailRaw);
1223
+ result = await runRunsShow({
1224
+ runId: runId ?? '',
1225
+ cwd,
1226
+ events,
1227
+ ...(eventsTail !== undefined ? { eventsTail } : {}),
1228
+ json,
1229
+ });
1230
+ break;
1231
+ }
1232
+ case 'gc': {
1233
+ const dryRun = boolFlag('dry-run');
1234
+ const yes = boolFlag('yes');
1235
+ const olderRaw = flag('older-than-days');
1236
+ const olderThanDays = olderRaw ? parseInt(olderRaw, 10) : undefined;
1237
+ result = await runRunsGc({
1238
+ cwd,
1239
+ dryRun,
1240
+ yes,
1241
+ ...(olderThanDays !== undefined ? { olderThanDays } : {}),
1242
+ json,
1243
+ });
1244
+ break;
1245
+ }
1246
+ case 'delete': {
1247
+ const runId = args.slice(2).find(a => !a.startsWith('--'));
1248
+ const force = boolFlag('force');
1249
+ result = await runRunsDelete({ runId: runId ?? '', cwd, force, json });
1250
+ break;
1251
+ }
1252
+ case 'doctor': {
1253
+ const runId = args.slice(2).find(a => !a.startsWith('--'));
1254
+ const fix = boolFlag('fix');
1255
+ result = await runRunsDoctor({
1256
+ cwd,
1257
+ ...(runId ? { runId } : {}),
1258
+ fix,
1259
+ json,
1260
+ });
1261
+ break;
1262
+ }
1263
+ case 'watch': {
1264
+ // v6.1 — `runs watch <id>` tails events.ndjson with a live cost meter.
1265
+ // The other umbrella verbs use a unified `RunsCliResult` shape so the
1266
+ // dispatcher can treat them uniformly; `runs-watch.ts` returns the
1267
+ // same shape.
1268
+ const sinceRaw = flag('since');
1269
+ const since = sinceRaw !== undefined ? parseInt(sinceRaw, 10) : undefined;
1270
+ if (sinceRaw !== undefined && (Number.isNaN(since) || since < 0)) {
1271
+ process.stderr.write(`\x1b[31m[claude-autopilot] --since must be a non-negative integer\x1b[0m\n`);
1272
+ process.exit(1);
1273
+ }
1274
+ const noFollow = boolFlag('no-follow');
1275
+ const noColor = boolFlag('no-color');
1276
+ // Filter the value-flag value out of the positional lookup —
1277
+ // matches the same defensive pattern used in `runs show` (Bugbot
1278
+ // PR #88 MEDIUM). Without this, `runs watch --since 5 <ULID>`
1279
+ // would resolve runId to "5".
1280
+ const runId = args.slice(2).find(a => !a.startsWith('--') && a !== sinceRaw);
1281
+ const { runRunsWatch } = await import("./runs-watch.js");
1282
+ result = await runRunsWatch({
1283
+ runId: runId ?? '',
1284
+ cwd,
1285
+ ...(since !== undefined ? { since } : {}),
1286
+ noFollow,
1287
+ json,
1288
+ noColor,
1289
+ });
1290
+ break;
1291
+ }
1292
+ default: {
1293
+ process.stderr.write(`\x1b[31m[claude-autopilot] runs: unknown sub-verb "${sub}" — valid: list, show, gc, delete, doctor, watch\x1b[0m\n`);
1294
+ process.exit(1);
1295
+ }
1296
+ }
1297
+ for (const line of result.stdout)
1298
+ process.stdout.write(line.endsWith('\n') ? line : `${line}\n`);
1299
+ for (const line of result.stderr)
1300
+ process.stderr.write(line.endsWith('\n') ? line : `${line}\n`);
1301
+ process.exit(result.exit);
1302
+ break;
1303
+ }
1304
+ case 'run-resume': {
1305
+ // v6 Phase 3 — `run resume <id>`. Lookup-only: identifies the next phase
1306
+ // and decision rationale. Actual phase execution wires in Phase 6+.
1307
+ // The synthetic 'run-resume' subcommand was set above by the
1308
+ // disambiguation block; args[0]==='run', args[1]==='resume', args[2] is
1309
+ // optional run id.
1310
+ const json = boolFlag('json');
1311
+ const fromPhase = flag('from-phase') ?? flag('from');
1312
+ // Filter value-flag values out of positional lookup — see same comment
1313
+ // in the `runs show` case above. (Bugbot MEDIUM, PR #88.)
1314
+ const runId = args.slice(2).find(a => !a.startsWith('--') && a !== fromPhase);
1315
+ const { runRunResume } = await import("./runs.js");
1316
+ const result = await runRunResume({
1317
+ runId: runId ?? '',
1318
+ cwd: process.cwd(),
1319
+ ...(fromPhase ? { fromPhase } : {}),
1320
+ json,
1321
+ });
1322
+ for (const line of result.stdout)
1323
+ process.stdout.write(line.endsWith('\n') ? line : `${line}\n`);
1324
+ for (const line of result.stderr)
1325
+ process.stderr.write(line.endsWith('\n') ? line : `${line}\n`);
1326
+ process.exit(result.exit);
1327
+ break;
1328
+ }
1329
+ case 'internal': {
1330
+ // v6 Phase 2 — hidden verb. Markdown skills shell out to append typed
1331
+ // events into a run's events.ndjson. NOT advertised in the main help.
1332
+ const { runInternalCli } = await import("../core/run-state/cli-internal.js");
1333
+ const result = await runInternalCli({
1334
+ args: args.slice(1),
1335
+ cwd: process.cwd(),
1336
+ });
1337
+ for (const line of result.stdout)
1338
+ process.stdout.write(line.endsWith('\n') ? line : `${line}\n`);
1339
+ for (const line of result.stderr)
1340
+ process.stderr.write(line.endsWith('\n') ? line : `${line}\n`);
1341
+ process.exit(result.exit);
1342
+ break;
1343
+ }
1344
+ case 'dashboard': {
1345
+ // v7.0 Phase 2.3 — hosted dashboard verbs.
1346
+ // claude-autopilot dashboard {login,logout,status,upload <runId>}
1347
+ const { runDashboardVerb } = await import("./dashboard/index.js");
1348
+ const exit = await runDashboardVerb({ argv: args.slice(1) });
1349
+ process.exit(exit);
821
1350
  break;
822
1351
  }
823
1352
  default: