@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
@@ -1,3 +1,6 @@
1
+ import type { ReviewEngine } from '../adapters/review-engine/types.ts';
2
+ import type { GuardrailConfig } from '../core/config/types.ts';
3
+ import { type RunPhase } from '../core/run-state/phase-runner.ts';
1
4
  export interface ScanCommandOptions {
2
5
  cwd?: string;
3
6
  configPath?: string;
@@ -6,6 +9,96 @@ export interface ScanCommandOptions {
6
9
  ask?: string;
7
10
  focus?: 'security' | 'logic' | 'performance' | 'brand' | 'all';
8
11
  dryRun?: boolean;
12
+ /**
13
+ * v6.0.1 — engine knob inputs. The CLI dispatcher gathers these and the
14
+ * resolver picks a winner against (cli > env > config > built-in default).
15
+ * Both fields are optional; an absent CLI flag + absent env value falls
16
+ * through to the loaded config and then to the built-in default (off in v6.0).
17
+ */
18
+ cliEngine?: boolean;
19
+ envEngine?: string;
20
+ /**
21
+ * Test-only seam — injects a pre-built ReviewEngine so tests can exercise
22
+ * the engine-wrap path without hitting `loadAdapter()` (and therefore
23
+ * without needing an LLM API key in the environment). Production callers
24
+ * MUST NOT pass this; the CLI dispatcher does not expose a flag that sets
25
+ * it. Underscore-prefixed to make the test-seam intent obvious in code
26
+ * search.
27
+ */
28
+ __testReviewEngine?: ReviewEngine;
9
29
  }
30
+ /**
31
+ * Input handed to the wrapped `RunPhase<ScanInput, ScanOutput>` body. Captures
32
+ * everything the phase needs that's already been resolved by the outer scope
33
+ * (file list, engine, focus hint, etc.) so the phase body itself is a thin
34
+ * await on `runReviewPhase` + finding post-processing.
35
+ *
36
+ * Exported (along with `ScanOutput`) so the v6.2.0 orchestrator's phase
37
+ * registry (`src/core/run-state/phase-registry.ts`) can carry the typed
38
+ * I/O shape on its `PhaseRegistration<ScanInput, ScanOutput>` slot.
39
+ */
40
+ export interface ScanInput {
41
+ files: string[];
42
+ relFiles: string[];
43
+ cwd: string;
44
+ config: GuardrailConfig;
45
+ engine: ReviewEngine;
46
+ focusHint: string;
47
+ ask?: string;
48
+ focusLabel: string | null;
49
+ all: boolean;
50
+ }
51
+ /** What the wrapped phase returns. The outer scope translates this back to
52
+ * the legacy stdout banner + exit code path. Keeping the shape JSON-
53
+ * serializable means runPhase persists it into phases/<name>.json so a
54
+ * future skip-already-applied can restore it without re-running the LLM. */
55
+ export interface ScanOutput {
56
+ /** Total files actually sent to the review engine. */
57
+ fileCount: number;
58
+ /** Findings after ignore-rule filtering. Tagged with severity so a JSON
59
+ * consumer can reason about pass/fail without re-reading the cache. */
60
+ findings: Array<{
61
+ severity: 'critical' | 'warning' | 'note';
62
+ message: string;
63
+ file?: string;
64
+ line?: number;
65
+ suggestion?: string;
66
+ }>;
67
+ costUSD?: number;
68
+ durationMs: number;
69
+ /** Pass-through of the LLM's raw text outputs when --ask returned prose
70
+ * rather than structured findings — replicates the legacy "Answer:" path. */
71
+ rawOutputs?: string[];
72
+ }
73
+ /**
74
+ * v6.2.0 — sentinel returned by `buildScanPhase` when the verb's pre-flight
75
+ * (no targets, no files found, dry-run, missing LLM key, …) decided it can
76
+ * exit early without running the engine. The CLI dispatcher treats this as
77
+ * "render-and-exit"; the orchestrator skips registry dispatch and returns
78
+ * the exit code straight through. Mirrors the legacy in-place `return N`
79
+ * branches inside `runScan` byte-for-byte.
80
+ */
81
+ export interface BuildScanPhaseEarlyExit {
82
+ kind: 'early-exit';
83
+ exitCode: number;
84
+ }
85
+ export interface BuildScanPhaseResult {
86
+ kind: 'phase';
87
+ phase: RunPhase<ScanInput, ScanOutput>;
88
+ input: ScanInput;
89
+ config: GuardrailConfig;
90
+ renderResult: (output: ScanOutput) => number;
91
+ }
92
+ /**
93
+ * v6.2.0 — extract the `RunPhase<ScanInput, ScanOutput>` construction out of
94
+ * `runScan(options)` so the new top-level `autopilot` orchestrator can drive
95
+ * `runPhase` itself with a shared `phaseIdx` against the same run dir.
96
+ *
97
+ * The legacy `runScan(options)` keeps calling this builder internally (then
98
+ * `runPhaseWithLifecycle` for single-phase runs) so direct CLI behavior is
99
+ * byte-for-byte identical to v6.1. Parity is asserted by
100
+ * `tests/cli/scan-builder-parity.test.ts`.
101
+ */
102
+ export declare function buildScanPhase(options: ScanCommandOptions): Promise<BuildScanPhaseResult | BuildScanPhaseEarlyExit>;
10
103
  export declare function runScan(options?: ScanCommandOptions): Promise<number>;
11
104
  //# sourceMappingURL=scan.d.ts.map
@@ -8,6 +8,8 @@ import { loadIgnoreRules, parseConfigIgnore, applyIgnoreRules } from "../core/ig
8
8
  import { saveCachedFindings } from "../core/persist/findings-cache.js";
9
9
  import { appendCostLog } from "../core/persist/cost-log.js";
10
10
  import { detectLLMKey, LLM_KEY_HINTS } from "../core/detect/llm-key.js";
11
+ import { resolveEngineEnabled } from "../core/run-state/resolve-engine.js";
12
+ import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
11
13
  const C = {
12
14
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
13
15
  green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m',
@@ -50,7 +52,17 @@ function collectFiles(target, cwd) {
50
52
  function collectAllFiles(cwd) {
51
53
  return collectFiles(cwd, cwd);
52
54
  }
53
- export async function runScan(options = {}) {
55
+ /**
56
+ * v6.2.0 — extract the `RunPhase<ScanInput, ScanOutput>` construction out of
57
+ * `runScan(options)` so the new top-level `autopilot` orchestrator can drive
58
+ * `runPhase` itself with a shared `phaseIdx` against the same run dir.
59
+ *
60
+ * The legacy `runScan(options)` keeps calling this builder internally (then
61
+ * `runPhaseWithLifecycle` for single-phase runs) so direct CLI behavior is
62
+ * byte-for-byte identical to v6.1. Parity is asserted by
63
+ * `tests/cli/scan-builder-parity.test.ts`.
64
+ */
65
+ export async function buildScanPhase(options) {
54
66
  const cwd = options.cwd ?? process.cwd();
55
67
  const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
56
68
  let config = { configVersion: 1 };
@@ -73,46 +85,52 @@ export async function runScan(options = {}) {
73
85
  console.error(fmt('dim', ' guardrail scan src/auth/'));
74
86
  console.error(fmt('dim', ' guardrail scan --all'));
75
87
  console.error(fmt('dim', ' guardrail scan --ask "is there SQL injection?" src/db/'));
76
- return 1;
88
+ return { kind: 'early-exit', exitCode: 1 };
77
89
  }
78
90
  // Deduplicate
79
91
  files = [...new Set(files)];
80
92
  if (files.length === 0) {
81
93
  console.log(fmt('yellow', '[scan] No code files found at the specified path(s)'));
82
- return 0;
94
+ return { kind: 'early-exit', exitCode: 0 };
83
95
  }
84
96
  if (options.dryRun) {
85
97
  console.log(fmt('bold', `[scan] Would scan ${files.length} file(s):`));
86
98
  for (const f of files)
87
99
  console.log(fmt('dim', ` ${path.relative(cwd, f)}`));
88
- return 0;
100
+ return { kind: 'early-exit', exitCode: 0 };
89
101
  }
90
102
  // Auto-detect stack if not in config
91
103
  if (!config.stack) {
92
104
  config = { ...config, stack: detectStack(cwd) ?? undefined };
93
105
  }
94
106
  // Build review engine
95
- if (!detectLLMKey().hasKey) {
96
- console.error(fmt('red', '[scan] No LLM API key — set one of:'));
97
- for (const { name, url, note } of LLM_KEY_HINTS) {
98
- const suffix = note ? ` (${note})` : '';
99
- console.error(fmt('dim', ` ${name.padEnd(18)} ${url}${suffix}`));
100
- }
101
- return 1;
102
- }
103
- const engineRef = typeof config.reviewEngine === 'string' ? config.reviewEngine
104
- : (config.reviewEngine?.adapter ?? 'auto');
105
107
  let engine;
106
- try {
107
- engine = await loadAdapter({
108
- point: 'review-engine',
109
- ref: engineRef,
110
- options: typeof config.reviewEngine === 'object' ? config.reviewEngine.options : undefined,
111
- });
108
+ if (options.__testReviewEngine) {
109
+ // Test-only fast path — skip the LLM key check and the adapter loader.
110
+ engine = options.__testReviewEngine;
112
111
  }
113
- catch (err) {
114
- console.error(fmt('red', `[scan] Could not load review engine: ${err instanceof Error ? err.message : String(err)}`));
115
- return 1;
112
+ else {
113
+ if (!detectLLMKey().hasKey) {
114
+ console.error(fmt('red', '[scan] No LLM API key — set one of:'));
115
+ for (const { name, url, note } of LLM_KEY_HINTS) {
116
+ const suffix = note ? ` (${note})` : '';
117
+ console.error(fmt('dim', ` ${name.padEnd(18)} ${url}${suffix}`));
118
+ }
119
+ return { kind: 'early-exit', exitCode: 1 };
120
+ }
121
+ const engineRef = typeof config.reviewEngine === 'string' ? config.reviewEngine
122
+ : (config.reviewEngine?.adapter ?? 'auto');
123
+ try {
124
+ engine = await loadAdapter({
125
+ point: 'review-engine',
126
+ ref: engineRef,
127
+ options: typeof config.reviewEngine === 'object' ? config.reviewEngine.options : undefined,
128
+ });
129
+ }
130
+ catch (err) {
131
+ console.error(fmt('red', `[scan] Could not load review engine: ${err instanceof Error ? err.message : String(err)}`));
132
+ return { kind: 'early-exit', exitCode: 1 };
133
+ }
116
134
  }
117
135
  const focusLabel = options.focus && options.focus !== 'all' ? options.focus : null;
118
136
  const relFiles = files.map(f => path.relative(cwd, f));
@@ -123,9 +141,94 @@ export async function runScan(options = {}) {
123
141
  console.log(fmt('dim', ` question: ${options.ask}`));
124
142
  if (focusLabel)
125
143
  console.log(fmt('dim', ` focus: ${focusLabel}`));
144
+ // Pre-flight resolution just for the banner; the helper re-resolves
145
+ // internally with identical precedence so the engine path stays
146
+ // deterministic. Keeping this inline preserves the legacy "engine: on
147
+ // (<source>)" banner that scan emitted before v6.0.6.
148
+ const engineBanner = resolveEngineEnabled({
149
+ ...(options.cliEngine !== undefined ? { cliEngine: options.cliEngine } : {}),
150
+ ...(options.envEngine !== undefined ? { envValue: options.envEngine } : {}),
151
+ ...(typeof config.engine?.enabled === 'boolean' ? { configEnabled: config.engine.enabled } : {}),
152
+ });
153
+ if (engineBanner.enabled) {
154
+ console.log(fmt('dim', ` engine: on (${engineBanner.source})`));
155
+ }
126
156
  console.log('');
127
157
  // Build a focused git summary / prompt context
128
158
  const focusHint = buildFocusHint(options.ask, focusLabel);
159
+ const scanInput = {
160
+ files,
161
+ relFiles,
162
+ cwd,
163
+ config,
164
+ engine,
165
+ focusHint,
166
+ ...(options.ask !== undefined ? { ask: options.ask } : {}),
167
+ focusLabel,
168
+ all: options.all === true,
169
+ };
170
+ // The wrapped phase body — pure call-the-LLM-and-process-findings work.
171
+ // Extracted into a RunPhase so the engine path and the legacy path share
172
+ // the exact same logic. Engine-off callers invoke this directly via
173
+ // `executeScanPhase()`; engine-on callers route through `runPhase()`.
174
+ const phase = {
175
+ name: 'scan',
176
+ // scan re-issues identical LLM queries against the same code — re-running
177
+ // is safe and cheap-ish to retry.
178
+ idempotent: true,
179
+ // No git push, no PR comment, no provider-side mutation. The cost-log
180
+ // append + findings-cache write are local file IO that's already
181
+ // overwrite-style; replays are safe.
182
+ hasSideEffects: false,
183
+ run: executeScanPhase,
184
+ };
185
+ return {
186
+ kind: 'phase',
187
+ phase,
188
+ input: scanInput,
189
+ config,
190
+ renderResult: (output) => renderScanOutput(output, scanInput),
191
+ };
192
+ }
193
+ export async function runScan(options = {}) {
194
+ const built = await buildScanPhase(options);
195
+ if (built.kind === 'early-exit')
196
+ return built.exitCode;
197
+ const { phase, input, config, renderResult } = built;
198
+ // v6.0.6 — lifecycle wiring (createRun → runPhase → run.complete + state
199
+ // snapshot + lock release) lives in `runPhaseWithLifecycle`. The helper
200
+ // owns the engine-on/engine-off branch and the failure banner; the caller
201
+ // just supplies the phase, the input, and the engine-off escape hatch.
202
+ let output;
203
+ try {
204
+ const result = await runPhaseWithLifecycle({
205
+ cwd: input.cwd,
206
+ phase,
207
+ input,
208
+ config,
209
+ cliEngine: options.cliEngine,
210
+ envEngine: options.envEngine,
211
+ runEngineOff: () => executeScanPhase(input),
212
+ });
213
+ output = result.output;
214
+ }
215
+ catch {
216
+ // Helper already printed the failure banner + emitted run.complete
217
+ // failed + refreshed state.json + released the lock. Surface the
218
+ // legacy non-zero exit so existing CI / scripts are unaffected.
219
+ return 1;
220
+ }
221
+ return renderResult(output);
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Phase body — the LLM call + finding processing + cost-log append + findings
225
+ // cache write. Extracted from runScan so the engine-on path can wrap it via
226
+ // `runPhase` and the engine-off path can call it directly. Returns a
227
+ // JSON-serializable ScanOutput so the engine can persist it as `result` on
228
+ // the phase snapshot.
229
+ // ---------------------------------------------------------------------------
230
+ async function executeScanPhase(input) {
231
+ const { files, relFiles, cwd, config, engine, focusHint, ask } = input;
129
232
  const result = await runReviewPhase({
130
233
  touchedFiles: relFiles,
131
234
  engine,
@@ -149,11 +252,47 @@ export async function runScan(options = {}) {
149
252
  // Apply ignore rules
150
253
  const ignoreRules = [...loadIgnoreRules(cwd), ...parseConfigIgnore(config.ignore)];
151
254
  const findings = applyIgnoreRules(result.findings, ignoreRules);
255
+ // Persist findings so `guardrail fix` can read them
256
+ saveCachedFindings(cwd, findings);
257
+ // Persist run to cost log so `claude-autopilot costs` reflects scans, not
258
+ // just full pipeline runs. Previously scan never wrote to the log, so the
259
+ // costs report stayed frozen at whatever the last `run` invocation produced.
260
+ appendCostLog(cwd, {
261
+ timestamp: new Date().toISOString(),
262
+ files: files.length,
263
+ inputTokens: result.usage?.input ?? 0,
264
+ outputTokens: result.usage?.output ?? 0,
265
+ costUSD: result.costUSD ?? 0,
266
+ durationMs: result.durationMs,
267
+ });
268
+ return {
269
+ fileCount: files.length,
270
+ findings: findings.map(f => ({
271
+ severity: f.severity,
272
+ message: f.message,
273
+ ...(f.file !== undefined ? { file: f.file } : {}),
274
+ ...(f.line !== undefined ? { line: f.line } : {}),
275
+ ...(f.suggestion !== undefined ? { suggestion: f.suggestion } : {}),
276
+ })),
277
+ ...(result.costUSD !== undefined ? { costUSD: result.costUSD } : {}),
278
+ durationMs: result.durationMs,
279
+ ...(result.rawOutputs !== undefined ? { rawOutputs: result.rawOutputs } : {}),
280
+ };
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Render — translate ScanOutput back to the legacy stdout banner + exit code.
284
+ // Lives outside the wrapped phase because it's pure presentation; doing the
285
+ // rendering inside the phase would couple the engine path's idempotency to
286
+ // console output, which we don't want.
287
+ // ---------------------------------------------------------------------------
288
+ function renderScanOutput(output, input) {
289
+ const { findings, costUSD, durationMs, rawOutputs } = output;
290
+ const { ask } = input;
152
291
  // Print results
153
- if (findings.length === 0 && options.ask && result.rawOutputs && result.rawOutputs.length > 0) {
292
+ if (findings.length === 0 && ask && rawOutputs && rawOutputs.length > 0) {
154
293
  // --ask returned prose rather than structured findings — surface raw response
155
294
  console.log(fmt('cyan', `Answer:`));
156
- for (const raw of result.rawOutputs) {
295
+ for (const raw of rawOutputs) {
157
296
  // Strip markdown fences and the ## Findings / ## Review Summary headers if present
158
297
  const cleaned = raw.replace(/^##\s+Review Summary\s*\n/gm, '').replace(/^##\s+Findings\s*\n/gm, '').trim();
159
298
  console.log(cleaned);
@@ -196,21 +335,8 @@ export async function runScan(options = {}) {
196
335
  console.log('');
197
336
  }
198
337
  }
199
- // Persist findings so `guardrail fix` can read them
200
- saveCachedFindings(cwd, findings);
201
- // Persist run to cost log so `claude-autopilot costs` reflects scans, not
202
- // just full pipeline runs. Previously scan never wrote to the log, so the
203
- // costs report stayed frozen at whatever the last `run` invocation produced.
204
- appendCostLog(cwd, {
205
- timestamp: new Date().toISOString(),
206
- files: files.length,
207
- inputTokens: result.usage?.input ?? 0,
208
- outputTokens: result.usage?.output ?? 0,
209
- costUSD: result.costUSD ?? 0,
210
- durationMs: result.durationMs,
211
- });
212
- if (result.costUSD !== undefined) {
213
- console.log(fmt('dim', ` $${result.costUSD.toFixed(4)} · ${result.durationMs}ms`));
338
+ if (costUSD !== undefined) {
339
+ console.log(fmt('dim', ` $${costUSD.toFixed(4)} · ${durationMs}ms`));
214
340
  }
215
341
  const fixable = findings.filter(f => f.severity === 'critical' || f.severity === 'warning');
216
342
  if (fixable.length > 0) {
@@ -0,0 +1,66 @@
1
+ import type { GuardrailConfig } from '../core/config/types.ts';
2
+ import { type RunPhase } from '../core/run-state/phase-runner.ts';
3
+ export interface SpecCommandOptions {
4
+ cwd?: string;
5
+ configPath?: string;
6
+ /**
7
+ * v6.0.3 — engine knob inputs. Same shape and precedence as scan / costs /
8
+ * fix / brainstorm (CLI > env > config > built-in default off in v6.0.x).
9
+ */
10
+ cliEngine?: boolean;
11
+ envEngine?: string;
12
+ /**
13
+ * Test-only seam — when true, the phase body returns its result without
14
+ * printing the advisory banner. Lets engine-smoke tests assert the
15
+ * `state.json` + `events.ndjson` lifecycle without polluting stdout.
16
+ * Production callers (the CLI dispatcher) MUST NOT pass this.
17
+ */
18
+ __silent?: boolean;
19
+ }
20
+ /**
21
+ * Phase input — minimal. The CLI verb body is a print-and-exit advisory.
22
+ * Captured as a struct so the engine path's phase body matches the
23
+ * engine-off path call signature.
24
+ *
25
+ * Exported so the v6.2.0 orchestrator's phase registry can carry the typed
26
+ * I/O shape on its `PhaseRegistration<SpecInput, SpecOutput>` slot.
27
+ */
28
+ export interface SpecInput {
29
+ cwd: string;
30
+ silent: boolean;
31
+ }
32
+ /**
33
+ * Phase output — JSON-serializable acknowledgment. Mirrors the shape of
34
+ * `BrainstormOutput`. Persisted as `result` on `phases/spec.json`.
35
+ */
36
+ export interface SpecOutput {
37
+ /** Always 'advisory' for v6.0.3 — the CLI verb is a Claude Code pointer. */
38
+ kind: 'advisory';
39
+ nextActions: string[];
40
+ }
41
+ /** v6.2.0 — see scan.ts for the kind='early-exit' rationale. Spec has no
42
+ * early-exit branches today (it just creates a config struct and returns
43
+ * an advisory) so this discriminant is included for shape parity with the
44
+ * other builders. */
45
+ export interface BuildSpecPhaseEarlyExit {
46
+ kind: 'early-exit';
47
+ exitCode: number;
48
+ }
49
+ export interface BuildSpecPhaseResult {
50
+ kind: 'phase';
51
+ phase: RunPhase<SpecInput, SpecOutput>;
52
+ input: SpecInput;
53
+ config: GuardrailConfig;
54
+ renderResult: (output: SpecOutput) => number;
55
+ }
56
+ /**
57
+ * v6.2.0 — extract the `RunPhase<SpecInput, SpecOutput>` construction out of
58
+ * `runSpec(options)` so the new top-level `autopilot` orchestrator can drive
59
+ * `runPhase` itself with a shared `phaseIdx` against the same run dir.
60
+ *
61
+ * Parity with `runSpec(options)` is asserted by
62
+ * `tests/cli/spec-builder-parity.test.ts`.
63
+ */
64
+ export declare function buildSpecPhase(options: SpecCommandOptions): Promise<BuildSpecPhaseResult | BuildSpecPhaseEarlyExit>;
65
+ export declare function runSpec(options?: SpecCommandOptions): Promise<number>;
66
+ //# sourceMappingURL=spec.d.ts.map
@@ -0,0 +1,132 @@
1
+ // src/cli/spec.ts
2
+ //
3
+ // v6.0.3 — wrap the `spec` pipeline phase through `runPhase`.
4
+ //
5
+ // `spec` is the second phase of the autopilot pipeline (brainstorm → spec →
6
+ // plan → implement → migrate → validate → pr → review). Like `brainstorm`, it
7
+ // is implemented primarily as a Claude Code skill, not as a standalone CLI
8
+ // subcommand. The CLI verb that ships in this binary is an advisory shim: it
9
+ // points the user at the Claude Code skill and the next pipeline verbs. There
10
+ // is no LLM call in the CLI verb body and no provider side effects. The
11
+ // pure-LLM spec writing happens in Claude Code; the spec markdown produced
12
+ // there lands at `docs/superpowers/specs/<slug>.md` (a local file write —
13
+ // the recipe treats local file writes as acceptable inside the phase body,
14
+ // identical precedent to `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 `spec` because re-running produces NEW LLM
19
+ // content each invocation. v6.0.3 declares `idempotent: true` to match
20
+ // the engine's actual semantics ("safe to retry without
21
+ // reconciliation"): the CLI verb itself is a printed advisory that is
22
+ // byte-for-byte identical on every invocation, has no externalRefs to
23
+ // reconcile, and no provider state to roll back. Same reasoning as the
24
+ // brainstorm wrap. See `src/cli/brainstorm.ts` for the longer
25
+ // deviation rationale.
26
+ // - `hasSideEffects: false` — the CLI verb prints to stdout. No provider
27
+ // calls, no git push, no PR creation, no remote API write.
28
+ import * as path from 'node:path';
29
+ import * as fs from 'node:fs';
30
+ import { loadConfig } from "../core/config/loader.js";
31
+ import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
32
+ const C = {
33
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
34
+ cyan: '\x1b[36m', red: '\x1b[31m',
35
+ };
36
+ const fmt = (c, t) => `${C[c]}${t}${C.reset}`;
37
+ /**
38
+ * v6.2.0 — extract the `RunPhase<SpecInput, SpecOutput>` construction out of
39
+ * `runSpec(options)` so the new top-level `autopilot` orchestrator can drive
40
+ * `runPhase` itself with a shared `phaseIdx` against the same run dir.
41
+ *
42
+ * Parity with `runSpec(options)` is asserted by
43
+ * `tests/cli/spec-builder-parity.test.ts`.
44
+ */
45
+ export async function buildSpecPhase(options) {
46
+ const cwd = options.cwd ?? process.cwd();
47
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
48
+ let config = { configVersion: 1 };
49
+ if (fs.existsSync(configPath)) {
50
+ const loaded = await loadConfig(configPath);
51
+ if (loaded)
52
+ config = loaded;
53
+ }
54
+ const specInput = { cwd, silent: options.__silent === true };
55
+ const phase = {
56
+ name: 'spec',
57
+ // Pure-LLM spec writing happens in the Claude Code skill, not here.
58
+ // The CLI verb is an advisory print with no externalRefs to reconcile
59
+ // and no provider state to roll back. Safe to retry. (Deviation from
60
+ // the spec table noted at the top of the file.)
61
+ idempotent: true,
62
+ // No provider calls, no git push, no PR creation. Identical to costs
63
+ // and brainstorm.
64
+ hasSideEffects: false,
65
+ run: async (input) => executeSpecPhase(input),
66
+ };
67
+ return {
68
+ kind: 'phase',
69
+ phase,
70
+ input: specInput,
71
+ config,
72
+ renderResult: (output) => renderSpecOutput(output, specInput),
73
+ };
74
+ }
75
+ export async function runSpec(options = {}) {
76
+ const built = await buildSpecPhase(options);
77
+ if (built.kind === 'early-exit')
78
+ return built.exitCode;
79
+ const { phase, input, config, renderResult } = built;
80
+ // v6.0.6 — lifecycle wiring lives in `runPhaseWithLifecycle`.
81
+ let output;
82
+ try {
83
+ const result = await runPhaseWithLifecycle({
84
+ cwd: input.cwd,
85
+ phase,
86
+ input,
87
+ config,
88
+ cliEngine: options.cliEngine,
89
+ envEngine: options.envEngine,
90
+ runEngineOff: () => executeSpecPhase(input),
91
+ });
92
+ output = result.output;
93
+ }
94
+ catch {
95
+ return 1;
96
+ }
97
+ return renderResult(output);
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Phase body — produce the advisory payload. Pure: no provider calls.
101
+ // ---------------------------------------------------------------------------
102
+ async function executeSpecPhase(_input) {
103
+ return {
104
+ kind: 'advisory',
105
+ nextActions: [
106
+ 'Approve a brainstorm output, then invoke /autopilot from Claude Code',
107
+ 'The autopilot skill writes the implementation plan + executes the pipeline',
108
+ ],
109
+ };
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Render — translate SpecOutput back to the stdout advisory + exit code.
113
+ // ---------------------------------------------------------------------------
114
+ function renderSpecOutput(_output, input) {
115
+ if (input.silent)
116
+ return 0;
117
+ console.log(`
118
+ ${fmt('bold', '[spec]')} Spec writing is a Claude Code skill, not a standalone CLI subcommand.
119
+
120
+ Invoke it from Claude Code:
121
+
122
+ ${fmt('cyan', '/brainstorm')} Interactive spec writing (entry point)
123
+ ${fmt('cyan', '/autopilot')} Full pipeline from an approved spec
124
+ ${fmt('cyan', '/migrate')} Database migration phase (stack-dependent)
125
+
126
+ Specs land at ${fmt('dim', 'docs/superpowers/specs/<slug>.md')} — once approved, /autopilot consumes them.
127
+
128
+ Full pipeline docs: https://github.com/axledbetter/claude-autopilot#the-pipeline-phase-by-phase
129
+ `);
130
+ return 0;
131
+ }
132
+ //# sourceMappingURL=spec.js.map
@@ -0,0 +1,29 @@
1
+ export interface ValidateCommandOptions {
2
+ cwd?: string;
3
+ configPath?: string;
4
+ /**
5
+ * Optional context note injected into the validate log. The actual
6
+ * validation work (static checks, auto-fix, tests, Codex review,
7
+ * bugbot triage) is owned by the Claude Code `/validate` skill; this
8
+ * CLI verb is the engine-wrap shell so v6 pipeline runs can checkpoint
9
+ * a `validate` phase entry alongside `plan` / `review`.
10
+ */
11
+ context?: string;
12
+ /**
13
+ * Where to write the validate log file. Defaults to
14
+ * `.guardrail-cache/validate/<timestamp>-validate.md` so it lands inside
15
+ * the cache that's already gitignored. The path is recorded on
16
+ * ValidateOutput so the engine path can persist it as `result` for
17
+ * replay.
18
+ */
19
+ outputPath?: string;
20
+ /**
21
+ * v6.0.5 — engine knob inputs. Same shape and precedence as scan / costs /
22
+ * fix / plan / review (CLI > env > config > built-in default off in
23
+ * v6.0.x).
24
+ */
25
+ cliEngine?: boolean;
26
+ envEngine?: string;
27
+ }
28
+ export declare function runValidate(options?: ValidateCommandOptions): Promise<number>;
29
+ //# sourceMappingURL=validate.d.ts.map