@delegance/claude-autopilot 5.2.2 → 6.2.2

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