@delegance/claude-autopilot 1.0.0-alpha.6 → 1.0.0-alpha.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-alpha.8
4
+
5
+ ### Added
6
+
7
+ - **`autopilot autoregress`** — `autoregress run|diff|update|generate` now a first-class `autopilot` subcommand (no more raw `npx tsx scripts/autoregress.ts`)
8
+ - **GitHub Actions CI** — `.github/workflows/ci.yml` runs typecheck + tests on every PR; auto-publishes to npm on `v*` tags
9
+ - **README rewrite** — full feature documentation covering all alphas (all commands, config, GitHub Actions, snapshot regression, architecture)
10
+
11
+ ## 1.0.0-alpha.7
12
+
13
+ ### Added
14
+
15
+ - **`autopilot hook install`** — writes a `pre-push` git hook that runs `autoregress run` before every push; `hook uninstall` removes it; `hook status` shows current state; `--force` overwrites existing hook
16
+ - **`autoregress diff`** — colored snapshot viewer showing line-by-line JSON diffs between current output and baselines; exits 1 if any diffs found (never modifies baselines — use `update` for that)
17
+ - **`autoregress generate --files <list>`** — explicit comma-separated file list bypasses git detection; generates baselines for any src file on demand
18
+ - **Real baselines** — `tests/snapshots/*.snap.ts` + baselines for `serializer.ts`, `import-scanner.ts`, `impact-selector.ts`, and `sarif.ts` — alpha.6 infrastructure now self-testing via snapshots
19
+
3
20
  ## 1.0.0-alpha.6
4
21
 
5
22
  ### Added
package/README.md CHANGED
@@ -1,58 +1,159 @@
1
- # claude-autopilot
1
+ # @delegance/claude-autopilot
2
2
 
3
- End-to-end Claude Code pipeline: approved spec plan worktree implementation migrations validation PR review engine → review-bot triage.
3
+ Automated code review pipeline for Claude Code. Runs static rules, an optional LLM review engine, and impact-aware snapshot regression tests outputs SARIF for GitHub Code Scanning, inline PR annotations, and a pre-push hook for local enforcement.
4
4
 
5
- **Status: v1.0.0-alpha.1** — core architecture in place, ported adapters (codex, github, supabase, cursor), 1 preset (nextjs-supabase), 32 passing tests. API may change through alpha.
5
+ ## Install
6
6
 
7
- Full design spec: `docs/superpowers/specs/2026-04-20-claude-autopilot-v1-design.md`
8
- Implementation plans: `docs/superpowers/plans/`
7
+ ```bash
8
+ npm install --save-dev @delegance/claude-autopilot@alpha
9
+ ```
9
10
 
10
- ## Changes in v1.0 (alpha.1)
11
+ Requires Node 22+.
11
12
 
12
- - Four pluggable integration points (ReviewEngine, VcsHost, MigrationRunner, ReviewBotParser) with shared `AdapterBase`
13
- - YAML config (`autopilot.config.yaml`) replaces `.autopilot/stack.md`
14
- - Unified `Finding` type across validate + review-bot, with separate `TriageRecord[]` / `FixAttempt[]` history
15
- - Merged static-rules phase with global re-check after autofix
16
- - `AutopilotError` taxonomy with per-code retry policy
17
- - `apiVersion` + `getCapabilities()` on every adapter
18
- - Real tests phase — runs `testCommand` from config, emits critical finding on failure
19
- - NDJSON event log with secret redaction
13
+ ## Quick Start
20
14
 
21
- ## Prerequisites
15
+ ```bash
16
+ # Scaffold config
17
+ npx autopilot init
22
18
 
23
- - Node 22+
24
- - `gh` CLI authenticated
25
- - `OPENAI_API_KEY` in `.env.local`
19
+ # Run on changed files
20
+ npx autopilot run
26
21
 
27
- ## Install
22
+ # Watch mode (re-runs on every file save)
23
+ npx autopilot watch
24
+
25
+ # Install pre-push hook
26
+ npx autopilot hook install
27
+ ```
28
+
29
+ ## Commands
30
+
31
+ ### `autopilot run`
32
+
33
+ Runs the pipeline on git-changed files vs the base ref.
28
34
 
29
35
  ```bash
30
- npm install --save-dev @delegance/claude-autopilot@alpha
36
+ npx autopilot run # diff against HEAD~1
37
+ npx autopilot run --base main # diff against main
38
+ npx autopilot run --files src/foo.ts # explicit file list
39
+ npx autopilot run --format sarif --output results.sarif
40
+ npx autopilot run --dry-run # show what would run, no execution
41
+ ```
42
+
43
+ ### `autopilot watch`
44
+
45
+ Debounced re-run on every file save.
46
+
47
+ ```bash
48
+ npx autopilot watch
49
+ npx autopilot watch --debounce 500
31
50
  ```
32
51
 
33
- ## Usage (alpha.1)
52
+ ### `autopilot hook`
34
53
 
35
- CLI surface is limited to `preflight` in alpha.1. `run`, `init`, `validate`, `codex-pr-review`, `bugbot` land in alpha.4.
54
+ Manages a `pre-push` git hook that runs snapshot regression tests before every push.
36
55
 
37
56
  ```bash
38
- npx autopilot # runs preflight
57
+ npx autopilot hook install # write .git/hooks/pre-push
58
+ npx autopilot hook install --force # overwrite existing
59
+ npx autopilot hook uninstall # remove
60
+ npx autopilot hook status # show installed hook content
39
61
  ```
40
62
 
41
- ## Preset quick-start
63
+ Works in git worktrees (handles `.git` as a file pointer).
64
+
65
+ ### `autopilot autoregress`
66
+
67
+ Impact-aware snapshot regression testing. Only fires tests whose source modules (or one-hop importers) were touched by the current branch.
42
68
 
43
69
  ```bash
44
- cp presets/nextjs-supabase/autopilot.config.yaml .
45
- # Edit adapters / protectedPaths / testCommand as needed
46
- npx tsx src/cli/preflight.ts
70
+ npx autopilot autoregress run # impact-selected snapshots (default)
71
+ npx autopilot autoregress run --all # all snapshots
72
+ npx autopilot autoregress diff # show JSON diffs vs baselines
73
+ npx autopilot autoregress update # overwrite baselines with current output
74
+ npx autopilot autoregress generate # LLM-generate snapshot tests for changed files
75
+ npx autopilot autoregress generate --files src/foo.ts,src/bar.ts
47
76
  ```
48
77
 
49
- ## Roadmap
78
+ Requires `OPENAI_API_KEY` for `generate` mode.
79
+
80
+ ### `autopilot init`
81
+
82
+ Scaffolds `autopilot.config.yaml` from a preset.
83
+
84
+ ```bash
85
+ npx autopilot init
86
+ ```
87
+
88
+ Available presets: `nextjs-supabase`, `t3`, `python-fastapi`, `rails-postgres`, `go`.
89
+
90
+ ### `autopilot preflight`
91
+
92
+ Checks prerequisites (Node version, `gh` CLI auth, `OPENAI_API_KEY`).
93
+
94
+ ## GitHub Actions
95
+
96
+ Add to your workflow:
97
+
98
+ ```yaml
99
+ - uses: axledbetter/claude-autopilot@v1
100
+ with:
101
+ openai-api-key: ${{ secrets.OPENAI_API_KEY }}
102
+ ```
103
+
104
+ Runs the pipeline, uploads SARIF to GitHub Code Scanning, and annotates the PR diff inline.
105
+
106
+ ## SARIF Output
107
+
108
+ ```bash
109
+ npx autopilot run --format sarif --output autopilot.sarif
110
+ ```
111
+
112
+ Compatible with `github/codeql-action/upload-sarif@v3`.
113
+
114
+ ## Config (`autopilot.config.yaml`)
115
+
116
+ ```yaml
117
+ preset: nextjs-supabase # inherit a base config
118
+ reviewEngine:
119
+ adapter: codex
120
+ options:
121
+ model: gpt-5.3-codex
122
+ testCommand: npm test
123
+ protect:
124
+ - src/core/**
125
+ - data/deltas/**
126
+ ```
127
+
128
+ ## Snapshot Regression Testing
129
+
130
+ After each feature lands, generate behavioral baselines:
131
+
132
+ ```bash
133
+ npx autopilot autoregress generate
134
+ ```
135
+
136
+ Future PRs automatically fail if covered behavior diverges. The impact selector uses `git merge-base` diff + one-hop import graph expansion so only relevant snapshots run — keeping CI token-efficient.
137
+
138
+ High-impact paths (`src/core/pipeline/**`, `src/adapters/**`, `src/core/findings/**`, `src/core/config/**`) always trigger a full run.
139
+
140
+ ## Architecture
141
+
142
+ Four pluggable adapter points:
143
+
144
+ | Point | Built-in | Purpose |
145
+ |---|---|---|
146
+ | `review-engine` | `codex` | LLM code review |
147
+ | `vcs-host` | `github` | PR comments + SARIF upload |
148
+ | `migration-runner` | `supabase` | DB migration execution |
149
+ | `review-bot-parser` | `cursor` | Parse review bot comments |
150
+
151
+ ## Requirements
50
152
 
51
- - **alpha.2:** chunking, cost, cache, remaining adapters, 5 presets, 20 scenario tests
52
- - **alpha.3:** idempotency wiring, concurrency, adapter trust, 60 conformance + 13 safety tests
53
- - **alpha.4:** full CLI (init, install-github-action, run --resume, etc.) + programmatic API
54
- - **beta → 1.0.0:** dogfood + npm publish
153
+ - Node 22
154
+ - `OPENAI_API_KEY` (optional review engine and `autoregress generate` only)
155
+ - `gh` CLI authenticated (optional PR creation / vcs-host adapter)
55
156
 
56
157
  ## License
57
158
 
58
- MIT.
159
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delegance/claude-autopilot",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.8",
4
4
  "type": "module",
5
5
  "description": "Claude Code automation pipeline: spec → plan → implement → validate → PR",
6
6
  "keywords": ["claude", "autopilot", "ai", "pipeline", "code-review", "cli"],
@@ -2,6 +2,7 @@
2
2
  // scripts/autoregress.ts
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
+ import * as os from 'node:os';
5
6
  import { execSync, spawnSync } from 'node:child_process';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { selectSnapshots } from '../src/snapshots/impact-selector.ts';
@@ -19,6 +20,22 @@ function loadJson<T>(p: string, fallback: T): T {
19
20
  try { return JSON.parse(fs.readFileSync(p, 'utf8')) as T; } catch { return fallback; }
20
21
  }
21
22
 
23
+ export function diffBaselines(baselineJson: string, currentJson: string): string[] {
24
+ if (baselineJson === currentJson) return [];
25
+ const baselineLines = baselineJson.split('\n');
26
+ const currentLines = currentJson.split('\n');
27
+ const lines: string[] = [];
28
+ const maxLen = Math.max(baselineLines.length, currentLines.length);
29
+ for (let i = 0; i < maxLen; i++) {
30
+ const bLine = baselineLines[i];
31
+ const cLine = currentLines[i];
32
+ if (bLine === cLine) continue;
33
+ if (bLine !== undefined) lines.push(`- ${bLine}`);
34
+ if (cLine !== undefined) lines.push(`+ ${cLine}`);
35
+ }
36
+ return lines;
37
+ }
38
+
22
39
  function getChangedFiles(since?: string): string[] | null {
23
40
  try {
24
41
  const base = since
@@ -155,12 +172,15 @@ Write a snapshot test file. Requirements:
155
172
  5. Baseline loading pattern (use slug {slug}):
156
173
  const SLUG = '{slug}';
157
174
  import { fileURLToPath } from 'node:url';
175
+ import * as path from 'node:path';
158
176
  const baselineRaw = process.env.CAPTURE_BASELINE === '1' ? '{}' : fs.readFileSync(fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url)), 'utf8');
159
177
  const baseline = JSON.parse(baselineRaw);
160
178
  const captured: Record<string, unknown> = {};
161
179
  process.on('exit', () => {
162
180
  if (process.env.CAPTURE_BASELINE === '1') {
163
- const p = fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url));
181
+ const p = process.env.AUTOREGRESS_TEMP_BASELINE_DIR
182
+ ? path.join(process.env.AUTOREGRESS_TEMP_BASELINE_DIR, '{slug}.json')
183
+ : fileURLToPath(new URL('./baselines/{slug}.json', import.meta.url));
164
184
  fs.writeFileSync(p, JSON.stringify(captured, null, 2), 'utf8');
165
185
  }
166
186
  });
@@ -175,13 +195,24 @@ async function cmdGenerate(args: string[]): Promise<number> {
175
195
 
176
196
  const sinceIdx = args.indexOf('--since');
177
197
  const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
178
- const changed = getChangedFiles(since);
179
- if (!changed) { console.error('[autoregress generate] could not determine changed files'); return 1; }
180
-
181
- const srcFiles = changed.filter(f => f.startsWith('src/') && f.endsWith('.ts'));
182
- if (srcFiles.length === 0) {
183
- console.log('[autoregress generate] no src/*.ts files changed — nothing to generate');
184
- return 0;
198
+ const filesIdx = args.indexOf('--files');
199
+ const filesArg = filesIdx >= 0 ? args[filesIdx + 1] : undefined;
200
+
201
+ let srcFiles: string[];
202
+ if (filesArg) {
203
+ srcFiles = filesArg.split(',').map(f => f.trim()).filter(f => f.startsWith('src/') && f.endsWith('.ts'));
204
+ if (srcFiles.length === 0) {
205
+ console.error('[autoregress generate] --files must contain at least one src/*.ts path');
206
+ return 1;
207
+ }
208
+ } else {
209
+ const changed = getChangedFiles(since);
210
+ if (!changed) { console.error('[autoregress generate] could not determine changed files'); return 1; }
211
+ srcFiles = changed.filter(f => f.startsWith('src/') && f.endsWith('.ts'));
212
+ if (srcFiles.length === 0) {
213
+ console.log('[autoregress generate] no src/*.ts files changed — nothing to generate');
214
+ return 0;
215
+ }
185
216
  }
186
217
 
187
218
  console.log(`[autoregress generate] generating snapshots for ${srcFiles.length} file(s)`);
@@ -257,12 +288,98 @@ async function cmdGenerate(args: string[]): Promise<number> {
257
288
  return 0;
258
289
  }
259
290
 
260
- const [,, subcmd, ...rest] = process.argv;
261
- switch (subcmd) {
262
- case 'run': process.exit(cmdRun(rest)); break;
263
- case 'update': process.exit(cmdUpdate(rest)); break;
264
- case 'generate': process.exit(await cmdGenerate(rest)); break;
265
- default:
266
- console.error(`[autoregress] unknown subcommand: ${subcmd ?? '(none)'}`);
267
- process.exit(1);
291
+ function cmdDiff(args: string[]): number {
292
+ const runAll = args.includes('--all');
293
+ const snapIdx = args.indexOf('--snapshot');
294
+ const slug = snapIdx >= 0 ? args[snapIdx + 1] : undefined;
295
+ const sinceIdx = args.indexOf('--since');
296
+ const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
297
+
298
+ const index = loadJson<Record<string, string[]>>(INDEX_PATH, {});
299
+ const importMap = loadJson<Record<string, string[]>>(IMPORT_MAP_PATH, {});
300
+ const snapFiles = slug
301
+ ? [path.join('tests', 'snapshots', `${slug}.snap.ts`)]
302
+ : allSnapFiles();
303
+
304
+ let selected: string[];
305
+ if (runAll || slug || snapFiles.length === 0) {
306
+ selected = snapFiles;
307
+ } else {
308
+ const changed = getChangedFiles(since);
309
+ selected = changed ? selectSnapshots(changed, snapFiles, index, importMap).selected : snapFiles;
310
+ }
311
+
312
+ if (selected.length === 0) {
313
+ console.log('[autoregress diff] no snapshots to diff');
314
+ return 0;
315
+ }
316
+
317
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
318
+ const red = useColor ? '\x1b[31m' : '';
319
+ const green = useColor ? '\x1b[32m' : '';
320
+ const reset = useColor ? '\x1b[0m' : '';
321
+
322
+ let changedCount = 0;
323
+ for (const snap of selected) {
324
+ const slug_ = path.basename(snap, '.snap.ts');
325
+ const baselinePath = path.join(BASELINES_DIR, `${slug_}.json`);
326
+
327
+ if (!fs.existsSync(baselinePath)) {
328
+ console.log(` ${snap} — ${red}no baseline${reset}`);
329
+ continue;
330
+ }
331
+
332
+ const tmpBaselinesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ar-diff-'));
333
+ const tmpBaselinePath = path.join(tmpBaselinesDir, `${slug_}.json`);
334
+ fs.copyFileSync(baselinePath, tmpBaselinePath);
335
+
336
+ const absSnap = path.join(ROOT, snap);
337
+ const captureResult = spawnSync('node', ['--test', '--import', 'tsx', absSnap], {
338
+ stdio: ['ignore', 'pipe', 'pipe'],
339
+ cwd: ROOT,
340
+ env: { ...process.env, CAPTURE_BASELINE: '1', AUTOREGRESS_TEMP_BASELINE_DIR: tmpBaselinesDir },
341
+ });
342
+
343
+ const baselineJson = fs.readFileSync(baselinePath, 'utf8');
344
+ const captureOk = fs.existsSync(tmpBaselinePath);
345
+ const currentJson = captureOk ? fs.readFileSync(tmpBaselinePath, 'utf8') : null;
346
+ if (!captureOk && captureResult.status !== 0) {
347
+ const stderr = captureResult.stderr?.toString().trim();
348
+ if (stderr) console.error(` ${stderr.slice(0, 120)}`);
349
+ }
350
+ fs.rmSync(tmpBaselinesDir, { recursive: true, force: true });
351
+
352
+ if (!currentJson) {
353
+ console.log(` ${snap} — ${red}capture failed${reset}`);
354
+ continue;
355
+ }
356
+
357
+ const diffLines = diffBaselines(baselineJson, currentJson);
358
+ if (diffLines.length === 0) {
359
+ console.log(` ${snap} — ${green}✓ no changes${reset}`);
360
+ } else {
361
+ changedCount++;
362
+ console.log(` ${snap}`);
363
+ for (const line of diffLines) {
364
+ if (line.startsWith('-')) console.log(` ${red}${line}${reset}`);
365
+ else if (line.startsWith('+')) console.log(` ${green}${line}${reset}`);
366
+ else console.log(` ${line}`);
367
+ }
368
+ }
369
+ }
370
+
371
+ return changedCount > 0 ? 1 : 0;
372
+ }
373
+
374
+ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
375
+ const [,, subcmd, ...rest] = process.argv;
376
+ switch (subcmd) {
377
+ case 'run': process.exit(cmdRun(rest)); break;
378
+ case 'update': process.exit(cmdUpdate(rest)); break;
379
+ case 'generate': process.exit(await cmdGenerate(rest)); break;
380
+ case 'diff': process.exit(cmdDiff(rest)); break;
381
+ default:
382
+ console.error(`[autoregress] unknown subcommand: ${subcmd ?? '(none)'}`);
383
+ process.exit(1);
384
+ }
268
385
  }
@@ -0,0 +1,30 @@
1
+ // src/cli/autoregress-bridge.ts
2
+ import * as path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const SCRIPT = path.resolve(__dirname, '../../scripts/autoregress.ts');
8
+
9
+ const VALID_MODES = ['run', 'update', 'generate', 'diff'];
10
+
11
+ export function buildAutoregressArgs(args: string[]): string[] {
12
+ const mode = args[0] && VALID_MODES.includes(args[0]) ? args[0] : 'run';
13
+ const rest = args[0] && VALID_MODES.includes(args[0]) ? args.slice(1) : args;
14
+ return [mode, ...rest];
15
+ }
16
+
17
+ export function runAutoregress(args: string[]): number {
18
+ const resolvedArgs = buildAutoregressArgs(args);
19
+ const result = spawnSync(
20
+ process.execPath,
21
+ ['--import', 'tsx', SCRIPT, ...resolvedArgs],
22
+ { stdio: 'inherit', cwd: process.cwd() },
23
+ );
24
+ if (result.error) {
25
+ console.error(`[autoregress] failed to launch: ${result.error.message}`);
26
+ console.error(` script: ${SCRIPT}`);
27
+ return 1;
28
+ }
29
+ return result.status ?? 1;
30
+ }
@@ -0,0 +1,79 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ const HOOK_CONTENT = `#!/bin/sh
5
+ # autopilot pre-push hook — runs impact-selected snapshots before push
6
+ npx tsx scripts/autoregress.ts run
7
+ `;
8
+
9
+ function findGitDir(cwd: string): string | null {
10
+ let dir = path.resolve(cwd);
11
+ for (let i = 0; i < 10; i++) {
12
+ const candidate = path.join(dir, '.git');
13
+ if (fs.existsSync(candidate)) {
14
+ const stat = fs.statSync(candidate);
15
+ if (stat.isDirectory()) return candidate;
16
+ if (stat.isFile()) {
17
+ const content = fs.readFileSync(candidate, 'utf8');
18
+ const match = content.match(/^gitdir:\s*(.+)/m);
19
+ if (match) return path.resolve(dir, match[1]!.trim());
20
+ }
21
+ }
22
+ const parent = path.dirname(dir);
23
+ if (parent === dir) return null;
24
+ dir = parent;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export async function runHook(
30
+ sub: string,
31
+ options: { cwd?: string; force?: boolean } = {},
32
+ ): Promise<number> {
33
+ const cwd = options.cwd ?? process.cwd();
34
+ const gitDir = findGitDir(cwd);
35
+
36
+ if (!gitDir) {
37
+ console.error('[hook] not inside a git repository');
38
+ return 1;
39
+ }
40
+
41
+ const hookPath = path.join(gitDir, 'hooks', 'pre-push');
42
+
43
+ switch (sub) {
44
+ case 'install': {
45
+ if (fs.existsSync(hookPath) && !options.force) {
46
+ console.error(`[hook] pre-push hook already exists at ${hookPath}`);
47
+ console.error(' Use --force to overwrite.');
48
+ return 1;
49
+ }
50
+ fs.mkdirSync(path.dirname(hookPath), { recursive: true });
51
+ fs.writeFileSync(hookPath, HOOK_CONTENT, 'utf8');
52
+ fs.chmodSync(hookPath, 0o755);
53
+ console.log(`[hook] installed pre-push hook at ${hookPath}`);
54
+ return 0;
55
+ }
56
+ case 'uninstall': {
57
+ if (!fs.existsSync(hookPath)) {
58
+ console.log('[hook] no pre-push hook installed');
59
+ return 0;
60
+ }
61
+ fs.rmSync(hookPath);
62
+ console.log(`[hook] removed ${hookPath}`);
63
+ return 0;
64
+ }
65
+ case 'status': {
66
+ if (fs.existsSync(hookPath)) {
67
+ console.log(`[hook] installed at ${hookPath}`);
68
+ console.log(fs.readFileSync(hookPath, 'utf8'));
69
+ } else {
70
+ console.log('[hook] not installed');
71
+ }
72
+ return 0;
73
+ }
74
+ default:
75
+ console.error(`[hook] unknown subcommand: ${sub}`);
76
+ console.error('Usage: autopilot hook <install|uninstall|status> [--force]');
77
+ return 1;
78
+ }
79
+ }
package/src/cli/index.ts CHANGED
@@ -16,7 +16,7 @@ import { runWatch } from './watch.ts';
16
16
 
17
17
  const args = process.argv.slice(2);
18
18
 
19
- const SUBCOMMANDS = ['init', 'run', 'preflight', 'help', '--help', '-h'] as const;
19
+ const SUBCOMMANDS = ['init', 'run', 'watch', 'hook', 'autoregress', 'preflight', 'help', '--help', '-h'] as const;
20
20
  const VALUE_FLAGS = ['base', 'config', 'files', 'format', 'output', 'debounce'];
21
21
 
22
22
  // Detect first non-flag arg as subcommand, default to 'run'
@@ -47,6 +47,7 @@ Commands:
47
47
  watch Watch for file changes and re-run pipeline on each save
48
48
  init Scaffold autopilot.config.yaml from a preset
49
49
  preflight Check prerequisites
50
+ autoregress Run snapshot regression tests (run|diff|update|generate)
50
51
 
51
52
  Options (run):
52
53
  --base <ref> Git base ref for diff (default: HEAD~1)
@@ -59,6 +60,12 @@ Options (run):
59
60
  Options (watch):
60
61
  --config <path> Path to config file (default: ./autopilot.config.yaml)
61
62
  --debounce <ms> Debounce delay in ms (default: 300)
63
+
64
+ Options (autoregress):
65
+ --all Run/diff all snapshots
66
+ --since <ref> Git ref for changed-files detection
67
+ --snapshot <slug> Target a single snapshot
68
+ --files <a,b,c> Explicit file list for generate (skips git detection)
62
69
  `);
63
70
  }
64
71
 
@@ -118,6 +125,22 @@ switch (subcommand) {
118
125
  break;
119
126
  }
120
127
 
128
+ case 'hook': {
129
+ const { runHook } = await import('./hook.ts');
130
+ const hookSub = args[1] ?? 'status';
131
+ const force = boolFlag('force');
132
+ const code = await runHook(hookSub, { force });
133
+ process.exit(code);
134
+ break;
135
+ }
136
+
137
+ case 'autoregress': {
138
+ const { runAutoregress } = await import('./autoregress-bridge.ts');
139
+ const code = runAutoregress(args.slice(1));
140
+ process.exit(code);
141
+ break;
142
+ }
143
+
121
144
  default:
122
145
  console.error(`\x1b[31m[autopilot] Unknown subcommand: "${subcommand}"\x1b[0m`);
123
146
  printUsage();