@delegance/claude-autopilot 5.5.2 → 6.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +935 -6
  2. package/README.md +55 -0
  3. package/dist/src/adapters/council/openai.js +12 -6
  4. package/dist/src/adapters/deploy/_http.d.ts +43 -0
  5. package/dist/src/adapters/deploy/_http.js +99 -0
  6. package/dist/src/adapters/deploy/fly.d.ts +206 -0
  7. package/dist/src/adapters/deploy/fly.js +696 -0
  8. package/dist/src/adapters/deploy/index.d.ts +2 -0
  9. package/dist/src/adapters/deploy/index.js +33 -0
  10. package/dist/src/adapters/deploy/render.d.ts +181 -0
  11. package/dist/src/adapters/deploy/render.js +550 -0
  12. package/dist/src/adapters/deploy/types.d.ts +67 -3
  13. package/dist/src/adapters/deploy/vercel.d.ts +17 -1
  14. package/dist/src/adapters/deploy/vercel.js +29 -49
  15. package/dist/src/adapters/pricing.d.ts +36 -0
  16. package/dist/src/adapters/pricing.js +40 -0
  17. package/dist/src/adapters/review-engine/codex.js +10 -7
  18. package/dist/src/cli/autopilot.d.ts +71 -0
  19. package/dist/src/cli/autopilot.js +735 -0
  20. package/dist/src/cli/brainstorm.d.ts +23 -0
  21. package/dist/src/cli/brainstorm.js +131 -0
  22. package/dist/src/cli/costs.d.ts +15 -1
  23. package/dist/src/cli/costs.js +99 -10
  24. package/dist/src/cli/deploy.d.ts +3 -3
  25. package/dist/src/cli/deploy.js +34 -9
  26. package/dist/src/cli/fix.d.ts +18 -0
  27. package/dist/src/cli/fix.js +105 -11
  28. package/dist/src/cli/help-text.d.ts +52 -0
  29. package/dist/src/cli/help-text.js +400 -0
  30. package/dist/src/cli/implement.d.ts +91 -0
  31. package/dist/src/cli/implement.js +196 -0
  32. package/dist/src/cli/index.js +719 -245
  33. package/dist/src/cli/json-envelope.d.ts +187 -0
  34. package/dist/src/cli/json-envelope.js +270 -0
  35. package/dist/src/cli/json-mode.d.ts +33 -0
  36. package/dist/src/cli/json-mode.js +201 -0
  37. package/dist/src/cli/migrate.d.ts +111 -0
  38. package/dist/src/cli/migrate.js +305 -0
  39. package/dist/src/cli/plan.d.ts +81 -0
  40. package/dist/src/cli/plan.js +149 -0
  41. package/dist/src/cli/pr.d.ts +106 -0
  42. package/dist/src/cli/pr.js +191 -19
  43. package/dist/src/cli/preflight.js +26 -0
  44. package/dist/src/cli/review.d.ts +27 -0
  45. package/dist/src/cli/review.js +126 -0
  46. package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
  47. package/dist/src/cli/runs-watch-renderer.js +275 -0
  48. package/dist/src/cli/runs-watch.d.ts +41 -0
  49. package/dist/src/cli/runs-watch.js +395 -0
  50. package/dist/src/cli/runs.d.ts +122 -0
  51. package/dist/src/cli/runs.js +902 -0
  52. package/dist/src/cli/scan.d.ts +93 -0
  53. package/dist/src/cli/scan.js +166 -40
  54. package/dist/src/cli/spec.d.ts +66 -0
  55. package/dist/src/cli/spec.js +132 -0
  56. package/dist/src/cli/validate.d.ts +29 -0
  57. package/dist/src/cli/validate.js +131 -0
  58. package/dist/src/core/config/schema.d.ts +9 -0
  59. package/dist/src/core/config/schema.js +7 -0
  60. package/dist/src/core/config/types.d.ts +11 -0
  61. package/dist/src/core/council/runner.d.ts +10 -1
  62. package/dist/src/core/council/runner.js +25 -3
  63. package/dist/src/core/council/types.d.ts +7 -0
  64. package/dist/src/core/errors.d.ts +1 -1
  65. package/dist/src/core/errors.js +11 -0
  66. package/dist/src/core/logging/redaction.d.ts +13 -0
  67. package/dist/src/core/logging/redaction.js +20 -0
  68. package/dist/src/core/migrate/schema-validator.js +15 -1
  69. package/dist/src/core/phases/static-rules.d.ts +5 -1
  70. package/dist/src/core/phases/static-rules.js +2 -5
  71. package/dist/src/core/run-state/budget.d.ts +88 -0
  72. package/dist/src/core/run-state/budget.js +141 -0
  73. package/dist/src/core/run-state/cli-internal.d.ts +21 -0
  74. package/dist/src/core/run-state/cli-internal.js +174 -0
  75. package/dist/src/core/run-state/events.d.ts +59 -0
  76. package/dist/src/core/run-state/events.js +504 -0
  77. package/dist/src/core/run-state/lock.d.ts +61 -0
  78. package/dist/src/core/run-state/lock.js +206 -0
  79. package/dist/src/core/run-state/phase-context.d.ts +60 -0
  80. package/dist/src/core/run-state/phase-context.js +108 -0
  81. package/dist/src/core/run-state/phase-registry.d.ts +137 -0
  82. package/dist/src/core/run-state/phase-registry.js +162 -0
  83. package/dist/src/core/run-state/phase-runner.d.ts +80 -0
  84. package/dist/src/core/run-state/phase-runner.js +447 -0
  85. package/dist/src/core/run-state/provider-readback.d.ts +130 -0
  86. package/dist/src/core/run-state/provider-readback.js +426 -0
  87. package/dist/src/core/run-state/replay-decision.d.ts +69 -0
  88. package/dist/src/core/run-state/replay-decision.js +144 -0
  89. package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
  90. package/dist/src/core/run-state/resolve-engine.js +190 -0
  91. package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
  92. package/dist/src/core/run-state/resume-preflight.js +116 -0
  93. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
  94. package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
  95. package/dist/src/core/run-state/runs.d.ts +57 -0
  96. package/dist/src/core/run-state/runs.js +288 -0
  97. package/dist/src/core/run-state/snapshot.d.ts +14 -0
  98. package/dist/src/core/run-state/snapshot.js +114 -0
  99. package/dist/src/core/run-state/state.d.ts +40 -0
  100. package/dist/src/core/run-state/state.js +164 -0
  101. package/dist/src/core/run-state/types.d.ts +278 -0
  102. package/dist/src/core/run-state/types.js +13 -0
  103. package/dist/src/core/run-state/ulid.d.ts +11 -0
  104. package/dist/src/core/run-state/ulid.js +95 -0
  105. package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
  106. package/dist/src/core/schema-alignment/extractor/index.js +2 -2
  107. package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
  108. package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
  109. package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
  110. package/dist/src/core/schema-alignment/git-history.js +53 -0
  111. package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
  112. package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
  113. package/package.json +2 -1
  114. package/scripts/autoregress.ts +1 -1
  115. package/skills/claude-autopilot.md +1 -1
  116. package/skills/make-interfaces-feel-better/SKILL.md +104 -0
  117. package/skills/simplify-ui/SKILL.md +103 -0
  118. package/skills/ui/SKILL.md +117 -0
  119. package/skills/ui-ux-pro-max/SKILL.md +90 -0
@@ -0,0 +1,23 @@
1
+ export interface BrainstormCommandOptions {
2
+ cwd?: string;
3
+ configPath?: string;
4
+ /**
5
+ * v6.0.3 — engine knob inputs. Same shape and precedence as scan / costs /
6
+ * fix (CLI > env > config > built-in default off in v6.0.x). The CLI
7
+ * dispatcher wires `cliEngine` from `--engine` / `--no-engine`;
8
+ * `envEngine` from `process.env.CLAUDE_AUTOPILOT_ENGINE`. An absent CLI
9
+ * flag + absent env value falls through to the loaded config and then to
10
+ * the built-in default.
11
+ */
12
+ cliEngine?: boolean;
13
+ envEngine?: string;
14
+ /**
15
+ * Test-only seam — when true, the phase body returns its result without
16
+ * printing the advisory banner. Lets engine-smoke tests assert the
17
+ * `state.json` + `events.ndjson` lifecycle without polluting stdout.
18
+ * Production callers (the CLI dispatcher) MUST NOT pass this.
19
+ */
20
+ __silent?: boolean;
21
+ }
22
+ export declare function runBrainstorm(options?: BrainstormCommandOptions): Promise<number>;
23
+ //# sourceMappingURL=brainstorm.d.ts.map
@@ -0,0 +1,131 @@
1
+ // src/cli/brainstorm.ts
2
+ //
3
+ // v6.0.3 — wrap the `brainstorm` pipeline phase through `runPhase`.
4
+ //
5
+ // `brainstorm` is the entry point of the autopilot pipeline. It is implemented
6
+ // primarily as a Claude Code skill (`/brainstorm` → `superpowers:brainstorming`),
7
+ // not as a standalone CLI subcommand. The CLI verb that ships in this binary is
8
+ // an advisory shim: it points the user at the Claude Code skill and the next
9
+ // pipeline verbs. There is no LLM call in the CLI verb body, and no provider
10
+ // side effects. The pure-LLM design dialogue happens in Claude Code; the spec
11
+ // markdown produced there lands at `docs/superpowers/specs/<slug>.md` (a local
12
+ // file write, not a platform-side-effect-free remote write — the recipe treats
13
+ // local file writes as acceptable inside the phase body, identical precedent to
14
+ // `fix.ts` editing local source files).
15
+ //
16
+ // Idempotency / side effects (deviation note vs. spec table):
17
+ // - The spec table at docs/specs/v6-run-state-engine.md says
18
+ // `idempotent: no` for `brainstorm` because re-running produces NEW LLM
19
+ // content each invocation. The recipe table at
20
+ // docs/v6/wrapping-pipeline-phases.md previously echoed that. v6.0.3
21
+ // declares `idempotent: true` to match the engine's actual semantics
22
+ // ("safe to retry without reconciliation"): the CLI verb itself is a
23
+ // printed advisory message that is byte-for-byte identical on every
24
+ // invocation, has no externalRefs to reconcile, and no provider state
25
+ // to roll back. The engine's idempotency check is "safe to replay,"
26
+ // not "produces byte-identical output." See the recipe section 2:
27
+ // `idempotent: true → phase output depends only on its input + project
28
+ // state, and re-running gives the same answer.` That holds here.
29
+ // - `hasSideEffects: false` — the CLI verb prints to stdout. No provider
30
+ // calls, no git push, no PR creation, no remote API write. Identical
31
+ // to costs.
32
+ import * as path from 'node:path';
33
+ import * as fs from 'node:fs';
34
+ import { loadConfig } from "../core/config/loader.js";
35
+ import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
36
+ const C = {
37
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
38
+ cyan: '\x1b[36m', red: '\x1b[31m',
39
+ };
40
+ const fmt = (c, t) => `${C[c]}${t}${C.reset}`;
41
+ export async function runBrainstorm(options = {}) {
42
+ const cwd = options.cwd ?? process.cwd();
43
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
44
+ let config = { configVersion: 1 };
45
+ if (fs.existsSync(configPath)) {
46
+ const loaded = await loadConfig(configPath);
47
+ if (loaded)
48
+ config = loaded;
49
+ }
50
+ const brainstormInput = { cwd, silent: options.__silent === true };
51
+ // The wrapped phase body. Pure: reads no files, makes no provider calls.
52
+ // Engine-off callers invoke `executeBrainstormPhase()` directly;
53
+ // engine-on callers route through `runPhase()`.
54
+ const phase = {
55
+ name: 'brainstorm',
56
+ // Pure-LLM design dialogue happens in the Claude Code skill, not here.
57
+ // The CLI verb is an advisory print with no externalRefs to reconcile
58
+ // and no provider state to roll back. Safe to retry. (Deviation from
59
+ // the spec table noted at the top of the file.)
60
+ idempotent: true,
61
+ // No provider calls, no git push, no PR creation. Identical to costs.
62
+ hasSideEffects: false,
63
+ run: async (input) => executeBrainstormPhase(input),
64
+ };
65
+ // v6.0.6 — lifecycle wiring lives in `runPhaseWithLifecycle`. The helper
66
+ // owns the engine-on/engine-off branch and the failure banner.
67
+ let output;
68
+ try {
69
+ const result = await runPhaseWithLifecycle({
70
+ cwd,
71
+ phase,
72
+ input: brainstormInput,
73
+ config,
74
+ cliEngine: options.cliEngine,
75
+ envEngine: options.envEngine,
76
+ runEngineOff: () => executeBrainstormPhase(brainstormInput),
77
+ });
78
+ output = result.output;
79
+ }
80
+ catch {
81
+ // Helper already printed the failure banner + emitted run.complete
82
+ // failed + refreshed state.json + released the lock.
83
+ return 1;
84
+ }
85
+ return renderBrainstormOutput(output, brainstormInput);
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Phase body — produce the advisory payload. Pure: no provider calls. By
89
+ // default does NOT print to stdout (the renderer handles that) so the engine
90
+ // path's idempotency isn't coupled to console output. Returns a
91
+ // JSON-serializable BrainstormOutput so the engine can persist it as
92
+ // `result` on the phase snapshot.
93
+ // ---------------------------------------------------------------------------
94
+ async function executeBrainstormPhase(_input) {
95
+ return {
96
+ kind: 'advisory',
97
+ nextActions: [
98
+ 'Invoke /brainstorm from Claude Code for interactive spec writing',
99
+ 'Then /autopilot to run the full pipeline from an approved spec',
100
+ ],
101
+ };
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Render — translate BrainstormOutput back to the legacy stdout advisory +
105
+ // exit code. Lives outside the wrapped phase because it's pure presentation;
106
+ // doing the rendering inside the phase would couple the engine path's
107
+ // idempotency to console output.
108
+ // ---------------------------------------------------------------------------
109
+ function renderBrainstormOutput(_output, input) {
110
+ if (input.silent)
111
+ return 0;
112
+ console.log(`
113
+ ${fmt('bold', '[brainstorm]')} The pipeline entry point is a Claude Code skill, not a CLI subcommand.
114
+
115
+ Invoke it from Claude Code:
116
+
117
+ ${fmt('cyan', '/brainstorm')} Interactive spec writing
118
+ ${fmt('cyan', '/autopilot')} Full pipeline from an approved spec
119
+ ${fmt('cyan', '/migrate')} Database migration phase (stack-dependent)
120
+
121
+ From the terminal, the CLI subset exposes only the individual review-phase subcommands:
122
+
123
+ ${fmt('cyan', 'claude-autopilot run --base main')} Just the review phase
124
+ ${fmt('cyan', 'claude-autopilot doctor')} Check prerequisites (incl. superpowers plugin)
125
+ ${fmt('cyan', 'claude-autopilot migrate-v4')} Codemod for v4 → v5 repo migration (not a pipeline phase)
126
+
127
+ Full pipeline docs: https://github.com/axledbetter/claude-autopilot#the-pipeline-phase-by-phase
128
+ `);
129
+ return 0;
130
+ }
131
+ //# sourceMappingURL=brainstorm.js.map
@@ -1,2 +1,16 @@
1
- export declare function runCosts(cwd?: string): Promise<number>;
1
+ export interface CostsCommandOptions {
2
+ cwd?: string;
3
+ configPath?: string;
4
+ /**
5
+ * v6.0.2 — engine knob inputs. Same shape and precedence as scan
6
+ * (CLI > env > config > built-in default off in v6.0.x). The CLI dispatcher
7
+ * wires `cliEngine` from `--engine` / `--no-engine`; `envEngine` from
8
+ * `process.env.CLAUDE_AUTOPILOT_ENGINE`. An absent CLI flag + absent env
9
+ * value falls through to the loaded config and then to the built-in
10
+ * default.
11
+ */
12
+ cliEngine?: boolean;
13
+ envEngine?: string;
14
+ }
15
+ export declare function runCosts(cwdOrOptions?: string | CostsCommandOptions): Promise<number>;
2
16
  //# sourceMappingURL=costs.d.ts.map
@@ -1,7 +1,11 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
1
3
  import { readCostLog } from "../core/persist/cost-log.js";
4
+ import { loadConfig } from "../core/config/loader.js";
5
+ import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
2
6
  const C = {
3
7
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
4
- green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m',
8
+ green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', red: '\x1b[31m',
5
9
  };
6
10
  const fmt = (c, t) => `${C[c]}${t}${C.reset}`;
7
11
  function formatDate(iso) {
@@ -19,13 +23,69 @@ function fmtUSD(n) {
19
23
  function fmtTokens(n) {
20
24
  return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
21
25
  }
22
- export async function runCosts(cwd = process.cwd()) {
23
- const log = readCostLog(cwd);
24
- if (log.length === 0) {
25
- console.log(fmt('yellow', `[costs] No run history found in ${cwd} run \`guardrail run\` first.`));
26
- console.log(fmt('dim', ` (Costs are scoped per-project. \`cd\` to the project before checking.)`));
27
- return 0;
26
+ export async function runCosts(cwdOrOptions = {}) {
27
+ // Back-compat early callers (tests, MCP) pass a bare `cwd: string`. The
28
+ // tests/costs.test.ts harness drives this shape directly. Promote both
29
+ // forms into a single options struct so the rest of the function can treat
30
+ // it uniformly.
31
+ const options = typeof cwdOrOptions === 'string'
32
+ ? { cwd: cwdOrOptions }
33
+ : cwdOrOptions;
34
+ const cwd = options.cwd ?? process.cwd();
35
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
36
+ let config = { configVersion: 1 };
37
+ if (fs.existsSync(configPath)) {
38
+ const loaded = await loadConfig(configPath);
39
+ if (loaded)
40
+ config = loaded;
41
+ }
42
+ const costsInput = { cwd };
43
+ // The wrapped phase body — pure read of the cost ledger + summary build.
44
+ // Extracted into a RunPhase so the engine-on path and the engine-off path
45
+ // share the exact same logic. Engine-off callers invoke this directly via
46
+ // `executeCostsPhase()`; engine-on callers route through `runPhase()`.
47
+ const phase = {
48
+ name: 'costs',
49
+ // Cost summary is a pure read of `.guardrail-cache/costs.jsonl` — re-running
50
+ // produces identical output for identical ledger contents. Always safe to
51
+ // retry.
52
+ idempotent: true,
53
+ // No provider calls, no git push, no PR comment, no file writes (the
54
+ // ledger is read-only on this path; the writer is `appendCostLog` called
55
+ // by other verbs). Replays are safe.
56
+ hasSideEffects: false,
57
+ run: async (input) => executeCostsPhase(input),
58
+ };
59
+ // v6.0.6 — lifecycle wiring lives in `runPhaseWithLifecycle`. The helper
60
+ // owns the engine-on/engine-off branch and the failure banner; the caller
61
+ // just supplies the phase, the input, and the engine-off escape hatch.
62
+ let output;
63
+ try {
64
+ const result = await runPhaseWithLifecycle({
65
+ cwd,
66
+ phase,
67
+ input: costsInput,
68
+ config,
69
+ cliEngine: options.cliEngine,
70
+ envEngine: options.envEngine,
71
+ runEngineOff: () => executeCostsPhase(costsInput),
72
+ });
73
+ output = result.output;
74
+ }
75
+ catch {
76
+ // Helper already printed the failure banner + emitted run.complete
77
+ // failed + refreshed state.json + released the lock.
78
+ return 1;
28
79
  }
80
+ return renderCostsOutput(output, costsInput);
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // Phase body — read the cost ledger and assemble the summary. Pure: no
84
+ // console output, no exit codes. Returns a JSON-serializable CostsOutput so
85
+ // the engine can persist it as `result` on the phase snapshot.
86
+ // ---------------------------------------------------------------------------
87
+ async function executeCostsPhase(input) {
88
+ const log = readCostLog(input.cwd);
29
89
  // 7-day window
30
90
  const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
31
91
  const recent = log.filter(e => new Date(e.timestamp).getTime() >= sevenDaysAgo);
@@ -34,12 +94,41 @@ export async function runCosts(cwd = process.cwd()) {
34
94
  const totalInput = log.reduce((s, e) => s + e.inputTokens, 0);
35
95
  const totalOutput = log.reduce((s, e) => s + e.outputTokens, 0);
36
96
  const recentCost = recent.reduce((s, e) => s + e.costUSD, 0);
97
+ return {
98
+ entryCount: log.length,
99
+ totals: {
100
+ runs: log.length,
101
+ inputTokens: totalInput,
102
+ outputTokens: totalOutput,
103
+ costUSD: totalCost,
104
+ },
105
+ recent: {
106
+ runs: recent.length,
107
+ costUSD: recentCost,
108
+ },
109
+ last10,
110
+ };
111
+ }
112
+ // ---------------------------------------------------------------------------
113
+ // Render — translate CostsOutput back to the legacy stdout summary + exit
114
+ // code. Lives outside the wrapped phase because it's pure presentation;
115
+ // doing the rendering inside the phase would couple the engine path's
116
+ // idempotency to console output.
117
+ // ---------------------------------------------------------------------------
118
+ function renderCostsOutput(output, input) {
119
+ const { cwd } = input;
120
+ const { entryCount, totals, recent, last10 } = output;
121
+ if (entryCount === 0) {
122
+ console.log(fmt('yellow', `[costs] No run history found in ${cwd} — run \`guardrail run\` first.`));
123
+ console.log(fmt('dim', ` (Costs are scoped per-project. \`cd\` to the project before checking.)`));
124
+ return 0;
125
+ }
37
126
  console.log(`\n${fmt('bold', '[costs]')} ${fmt('dim', cwd)}\n`);
38
127
  // Summary row
39
128
  console.log(fmt('bold', 'Summary'));
40
- console.log(` All-time runs: ${log.length}`);
41
- console.log(` All-time cost: ${fmtUSD(totalCost)} (${fmtTokens(totalInput)} in / ${fmtTokens(totalOutput)} out)`);
42
- console.log(` Last 7 days: ${fmtUSD(recentCost)} (${recent.length} run${recent.length !== 1 ? 's' : ''})`);
129
+ console.log(` All-time runs: ${totals.runs}`);
130
+ console.log(` All-time cost: ${fmtUSD(totals.costUSD)} (${fmtTokens(totals.inputTokens)} in / ${fmtTokens(totals.outputTokens)} out)`);
131
+ console.log(` Last 7 days: ${fmtUSD(recent.costUSD)} (${recent.runs} run${recent.runs !== 1 ? 's' : ''})`);
43
132
  console.log(fmt('dim', ` (per-project — scoped to ${cwd}/.guardrail-cache/costs.jsonl)`));
44
133
  console.log('');
45
134
  // Last 10 runs table
@@ -2,7 +2,7 @@ import type { DeployAdapter, DeployConfig } from '../adapters/deploy/types.ts';
2
2
  export interface RunDeployOptions {
3
3
  configPath?: string;
4
4
  /** When set, overrides `deploy.adapter` from config. */
5
- adapterOverride?: 'vercel' | 'generic';
5
+ adapterOverride?: 'vercel' | 'fly' | 'render' | 'generic';
6
6
  ref?: string;
7
7
  commitSha?: string;
8
8
  cwd?: string;
@@ -39,7 +39,7 @@ export interface RunDeployOptions {
39
39
  export declare function runDeploy(opts: RunDeployOptions): Promise<number>;
40
40
  export interface RunDeployRollbackOptions {
41
41
  configPath?: string;
42
- adapterOverride?: 'vercel' | 'generic';
42
+ adapterOverride?: 'vercel' | 'fly' | 'render' | 'generic';
43
43
  /** Specific deploy ID to roll back to. When omitted, the previous prod deploy is used. */
44
44
  to?: string;
45
45
  cwd?: string;
@@ -48,7 +48,7 @@ export interface RunDeployRollbackOptions {
48
48
  }
49
49
  export interface RunDeployStatusOptions {
50
50
  configPath?: string;
51
- adapterOverride?: 'vercel' | 'generic';
51
+ adapterOverride?: 'vercel' | 'fly' | 'render' | 'generic';
52
52
  cwd?: string;
53
53
  adapterFactory?: (config: DeployConfig) => DeployAdapter;
54
54
  }
@@ -55,7 +55,7 @@ async function loadDeployConfigAsync(opts) {
55
55
  const adapter = opts.adapterOverride ?? configBlock?.adapter;
56
56
  if (!adapter) {
57
57
  console.error('\x1b[31m[deploy] no deploy adapter configured\x1b[0m\n' +
58
- ' hint: set `deploy.adapter` in guardrail.config.yaml, or pass --adapter <vercel|generic>');
58
+ ' hint: set `deploy.adapter` in guardrail.config.yaml, or pass --adapter <vercel|fly|render|generic>');
59
59
  return { errorCode: 1 };
60
60
  }
61
61
  const merged = {
@@ -85,6 +85,16 @@ export async function runDeploy(opts) {
85
85
  let onDeployStart;
86
86
  if (opts.watch) {
87
87
  if (typeof deployAdapter.streamLogs === 'function') {
88
+ // Phase 3 of v5.6 — when an adapter advertises `streamMode: 'polling'`
89
+ // (currently only Render), surface a one-line stderr notice BEFORE
90
+ // iteration starts so users understand why their log lines arrive
91
+ // in batches with short gaps. Adapters with `streamMode: 'websocket'`
92
+ // (Vercel SSE, Fly WS) or `'none'`/undefined get no notice — their
93
+ // streaming behavior matches user expectations. Spec: § "Capability
94
+ // metadata".
95
+ if (deployAdapter.capabilities?.streamMode === 'polling') {
96
+ process.stderr.write(`[deploy] note: ${deployAdapter.name} uses 2s log polling — lines may arrive in batches and could include short gaps. See docs/deploy/adapters.md#log-streaming for details.\n`);
97
+ }
88
98
  streamController = new AbortController();
89
99
  const streamFn = deployAdapter.streamLogs.bind(deployAdapter);
90
100
  const ctrlSignal = streamController.signal;
@@ -142,12 +152,20 @@ export async function runDeploy(opts) {
142
152
  const wantRollback = triggers.includes('healthCheckFailure');
143
153
  if (wantRollback) {
144
154
  if (typeof deployAdapter.rollback === 'function') {
155
+ // BOUND: exactly one auto-rollback per deploy attempt (spec §
156
+ // "Health-check policy" → "After rollback completes (success
157
+ // or failure), the adapter returns; no second rollback
158
+ // attempt"). The single `rollback({})` call below is the only
159
+ // place this path is invoked; we do NOT loop. Result status
160
+ // becomes one of the two new terminal values:
161
+ // - `fail_rolled_back` — rollback returned `pass`
162
+ // - `fail_rollback_failed` — rollback returned non-pass OR threw
145
163
  try {
146
164
  const rb = await deployAdapter.rollback({});
147
165
  if (rb.status === 'pass') {
148
166
  result = {
149
167
  ...result,
150
- status: 'fail',
168
+ status: 'fail_rolled_back',
151
169
  rolledBackTo: rb.rolledBackTo ?? rb.deployId,
152
170
  output: `Deploy passed; health check failed (${healthOutcome.lastError}); auto-rolled back to ${rb.rolledBackTo ?? rb.deployId ?? '<unknown>'}.`,
153
171
  };
@@ -156,7 +174,7 @@ export async function runDeploy(opts) {
156
174
  else {
157
175
  result = {
158
176
  ...result,
159
- status: 'fail',
177
+ status: 'fail_rollback_failed',
160
178
  output: `Deploy passed; health check failed; auto-rollback ALSO failed: ${rb.output ?? '<no output>'}`,
161
179
  };
162
180
  printAutoRollbackFailed(rb.output ?? 'rollback returned non-pass');
@@ -166,7 +184,7 @@ export async function runDeploy(opts) {
166
184
  const msg = err?.message ?? String(err);
167
185
  result = {
168
186
  ...result,
169
- status: 'fail',
187
+ status: 'fail_rollback_failed',
170
188
  output: `Deploy passed; health check failed; auto-rollback ERRORED: ${msg}`,
171
189
  };
172
190
  printAutoRollbackFailed(msg);
@@ -364,15 +382,22 @@ function formatAge(createdAtMs) {
364
382
  const days = Math.floor(hours / 24);
365
383
  return `${days}d`;
366
384
  }
385
+ /** Per v5.6 spec § "Health-check policy" — cap retries at 5× with 6s backoff. */
386
+ const HEALTH_CHECK_MAX_ATTEMPTS = 5;
387
+ const HEALTH_CHECK_BACKOFF_MS = 6000;
367
388
  /**
368
- * Probe a URL up to 3 times with 2s backoff between attempts. 2xx → pass.
389
+ * Probe a URL up to {@link HEALTH_CHECK_MAX_ATTEMPTS} times with
390
+ * {@link HEALTH_CHECK_BACKOFF_MS} backoff between attempts. 2xx → pass.
369
391
  * Per-attempt timeout is 10s. Network errors are treated as failures and
370
392
  * retried.
393
+ *
394
+ * Total wall-clock budget: ~30s (5 attempts × 6s backoff between, minus
395
+ * the trailing skip — matches the spec's "max ~30s window").
371
396
  */
372
397
  async function runHealthCheck(opts) {
373
398
  const { url, fetchImpl, sleepImpl } = opts;
374
399
  let lastError = '';
375
- for (let attempt = 1; attempt <= 3; attempt += 1) {
400
+ for (let attempt = 1; attempt <= HEALTH_CHECK_MAX_ATTEMPTS; attempt += 1) {
376
401
  const ctrl = new AbortController();
377
402
  const timer = setTimeout(() => ctrl.abort(), 10_000);
378
403
  try {
@@ -387,8 +412,8 @@ async function runHealthCheck(opts) {
387
412
  clearTimeout(timer);
388
413
  lastError = err?.message ?? String(err);
389
414
  }
390
- if (attempt < 3)
391
- await sleepImpl(2000);
415
+ if (attempt < HEALTH_CHECK_MAX_ATTEMPTS)
416
+ await sleepImpl(HEALTH_CHECK_BACKOFF_MS);
392
417
  }
393
418
  return { status: 'fail', url, lastError };
394
419
  }
@@ -406,7 +431,7 @@ function printAutoRollback(adapter, hc, rb) {
406
431
  const reset = '\x1b[0m';
407
432
  const target = rb.rolledBackTo ?? rb.deployId ?? '<unknown>';
408
433
  console.log(`${yellow}🔄 [deploy] auto-rolled-back-to=${target} via=${adapter} health-check-url=${hc.url}${reset}`);
409
- console.log(`${dim} reason: health check failed 3x against ${hc.url} (${hc.lastError})${reset}`);
434
+ console.log(`${dim} reason: health check failed ${HEALTH_CHECK_MAX_ATTEMPTS}x against ${hc.url} (${hc.lastError})${reset}`);
410
435
  if (rb.deployUrl) {
411
436
  console.log(`${dim} current: ${rb.deployUrl}${reset}`);
412
437
  }
@@ -1,3 +1,4 @@
1
+ import type { ReviewEngine } from '../adapters/review-engine/types.ts';
1
2
  export interface FixCommandOptions {
2
3
  cwd?: string;
3
4
  configPath?: string;
@@ -5,6 +6,23 @@ export interface FixCommandOptions {
5
6
  dryRun?: boolean;
6
7
  yes?: boolean;
7
8
  noVerify?: boolean;
9
+ /**
10
+ * v6.0.2 — engine knob inputs. Same shape and precedence as scan / costs
11
+ * (CLI > env > config > built-in default off in v6.0.x). The CLI
12
+ * dispatcher wires `cliEngine` from `--engine` / `--no-engine`;
13
+ * `envEngine` from `process.env.CLAUDE_AUTOPILOT_ENGINE`. An absent CLI
14
+ * flag + absent env value falls through to the loaded config and then to
15
+ * the built-in default.
16
+ */
17
+ cliEngine?: boolean;
18
+ envEngine?: string;
19
+ /**
20
+ * Test-only seam — injects a pre-built ReviewEngine so tests can exercise
21
+ * the engine-wrap path without hitting `loadAdapter()` (and therefore
22
+ * without needing an LLM API key in the environment). Mirrors the seam
23
+ * in `scan.ts`. Production callers MUST NOT pass this.
24
+ */
25
+ __testReviewEngine?: ReviewEngine;
8
26
  }
9
27
  export declare function runFix(options?: FixCommandOptions): Promise<number>;
10
28
  //# sourceMappingURL=fix.d.ts.map
@@ -6,6 +6,7 @@ import { loadCachedFindings } from "../core/persist/findings-cache.js";
6
6
  import { loadConfig } from "../core/config/loader.js";
7
7
  import { loadAdapter } from "../adapters/loader.js";
8
8
  import { generateFix, buildUnifiedDiff } from "../core/fix/generator.js";
9
+ import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
9
10
  const C = {
10
11
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
11
12
  green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m',
@@ -90,14 +91,22 @@ export async function runFix(options = {}) {
90
91
  let loadedConfig = null;
91
92
  try {
92
93
  loadedConfig = fs.existsSync(configPath) ? await loadConfig(configPath) : null;
93
- const ref = loadedConfig
94
- ? (typeof loadedConfig.reviewEngine === 'string' ? loadedConfig.reviewEngine : (loadedConfig.reviewEngine?.adapter ?? 'auto'))
95
- : 'auto';
96
- engine = await loadAdapter({
97
- point: 'review-engine',
98
- ref,
99
- options: loadedConfig && typeof loadedConfig.reviewEngine === 'object' ? loadedConfig.reviewEngine.options : undefined,
100
- });
94
+ if (options.__testReviewEngine) {
95
+ // Test-only fast path skip the adapter loader (and therefore the
96
+ // implicit LLM key check inside the auto-loader). Same seam as scan's
97
+ // `__testReviewEngine`. Production callers do not pass this.
98
+ engine = options.__testReviewEngine;
99
+ }
100
+ else {
101
+ const ref = loadedConfig
102
+ ? (typeof loadedConfig.reviewEngine === 'string' ? loadedConfig.reviewEngine : (loadedConfig.reviewEngine?.adapter ?? 'auto'))
103
+ : 'auto';
104
+ engine = await loadAdapter({
105
+ point: 'review-engine',
106
+ ref,
107
+ options: loadedConfig && typeof loadedConfig.reviewEngine === 'object' ? loadedConfig.reviewEngine.options : undefined,
108
+ });
109
+ }
101
110
  }
102
111
  catch (err) {
103
112
  console.error(fmt('red', `[fix] Could not load review engine: ${err instanceof Error ? err.message : String(err)}`));
@@ -108,6 +117,81 @@ export async function runFix(options = {}) {
108
117
  if (shouldVerify) {
109
118
  console.log(fmt('dim', `[fix] Verified mode — running "${testCommand}" after each fix\n`));
110
119
  }
120
+ const fixInput = {
121
+ cwd,
122
+ fixable,
123
+ engine,
124
+ testCommand,
125
+ shouldVerify,
126
+ // The early-return above already exits when options.dryRun is true, so
127
+ // we're always entering the apply loop here with dryRun=false. Keep the
128
+ // field on FixInput for shape parity (renderFixOutput consumes it) and
129
+ // for future engine-resume scenarios where the snapshot is replayed.
130
+ dryRun: false,
131
+ yes: options.yes === true,
132
+ };
133
+ // The wrapped phase body — runs the apply loop with native readline +
134
+ // per-finding console output INSIDE the phase body. The recipe doc says
135
+ // "no console output" for phase bodies, but `fix` is fundamentally
136
+ // interactive: the user must see each diff and approve it. Same precedent
137
+ // as scan keeping its LLM call inside the phase body. Documented
138
+ // deviation, intentional.
139
+ const phase = {
140
+ name: 'fix',
141
+ // Same-input → same-output: the LLM fix generator is deterministic per
142
+ // (finding, file content) pair, and applied diffs are exact text
143
+ // replacements. Re-running against the same cached findings against an
144
+ // unchanged tree produces the same results.
145
+ idempotent: true,
146
+ // Local file edits only — no remote / git push / PR creation in the
147
+ // existing `fix` flow. Per the recipe table, "side effects" means
148
+ // platform-side mutations (PR comments, git push, deploy). Local file
149
+ // writes are inside the project tree and the engine treats them like
150
+ // findings-cache writes (already overwrite-style).
151
+ hasSideEffects: false,
152
+ run: async (input) => executeFixPhase(input),
153
+ };
154
+ // v6.0.6 — lifecycle wiring lives in `runPhaseWithLifecycle`. The helper
155
+ // owns the engine-on/engine-off branch and the failure banner; the caller
156
+ // just supplies the phase, the input, and the engine-off escape hatch.
157
+ // The fix phase body is interactive (readline + per-finding diff prints
158
+ // INSIDE executeFixPhase) — that deviation from "pure phase body" is
159
+ // documented in fix's executeFixPhase header comment and unaffected by
160
+ // the helper extract: the helper still calls phase.run, which IS
161
+ // executeFixPhase, exactly as before.
162
+ let output;
163
+ try {
164
+ const result = await runPhaseWithLifecycle({
165
+ cwd,
166
+ phase,
167
+ input: fixInput,
168
+ // The helper only consults `config.engine.enabled` — pass through
169
+ // `loadedConfig` if we have one, otherwise an empty default.
170
+ config: loadedConfig ?? { configVersion: 1 },
171
+ cliEngine: options.cliEngine,
172
+ envEngine: options.envEngine,
173
+ runEngineOff: () => executeFixPhase(fixInput),
174
+ });
175
+ output = result.output;
176
+ }
177
+ catch {
178
+ // Helper already printed the failure banner + emitted run.complete
179
+ // failed + refreshed state.json + released the lock.
180
+ return 1;
181
+ }
182
+ return renderFixOutput(output, fixInput);
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // Phase body — drive the apply loop. INTENTIONAL DEVIATION from the recipe:
186
+ // the loop emits per-finding console output and prompts via readline. Pure
187
+ // side-effect-free phase bodies are the recipe default; interactive verbs
188
+ // like `fix` are an explicit exception (same precedent as scan's LLM call
189
+ // inside its phase body). The summary banner + exit code logic still lives
190
+ // in `renderFixOutput` so the engine path's idempotency isn't coupled to
191
+ // the final stdout shape.
192
+ // ---------------------------------------------------------------------------
193
+ async function executeFixPhase(input) {
194
+ const { cwd, fixable, engine, testCommand, shouldVerify, dryRun, yes } = input;
111
195
  const results = [];
112
196
  let quit = false;
113
197
  for (const finding of fixable) {
@@ -135,7 +219,7 @@ export async function runFix(options = {}) {
135
219
  }
136
220
  // Show diff
137
221
  const diff = buildUnifiedDiff(result.originalLines, result.replacementLines, finding.file, result.startLine);
138
- if (options.dryRun) {
222
+ if (dryRun) {
139
223
  console.log('');
140
224
  console.log(diff);
141
225
  console.log(fmt('dim', ' (dry run — not applied)'));
@@ -143,7 +227,7 @@ export async function runFix(options = {}) {
143
227
  continue;
144
228
  }
145
229
  // Interactive confirmation (unless --yes)
146
- if (!options.yes) {
230
+ if (!yes) {
147
231
  const answer = await confirmFix(diff, finding);
148
232
  if (answer === 'quit') {
149
233
  quit = true;
@@ -197,12 +281,22 @@ export async function runFix(options = {}) {
197
281
  results.push({ file: finding.file, line: finding.line, findingMessage: finding.message, status: 'failed', reason: String(err) });
198
282
  }
199
283
  }
284
+ return { results, dryRun };
285
+ }
286
+ // ---------------------------------------------------------------------------
287
+ // Render — translate FixOutput back to the legacy stdout summary + exit
288
+ // code. Lives outside the wrapped phase so the engine path's idempotency
289
+ // isn't coupled to the final summary line shape.
290
+ // ---------------------------------------------------------------------------
291
+ function renderFixOutput(output, input) {
292
+ const { results, dryRun } = output;
293
+ const { fixable } = input;
200
294
  const fixed = results.filter(r => r.status === 'fixed').length;
201
295
  const rejected = results.filter(r => r.status === 'rejected').length;
202
296
  const failed = results.filter(r => r.status === 'failed').length;
203
297
  const skipped = results.filter(r => r.status === 'skipped').length;
204
298
  console.log('');
205
- if (options.dryRun) {
299
+ if (dryRun) {
206
300
  console.log(fmt('yellow', `[fix] Dry run complete — ${fixable.length} finding${fixable.length !== 1 ? 's' : ''} previewed, no files modified.\n`));
207
301
  }
208
302
  else {