@delegance/claude-autopilot 2.4.0 → 5.0.0-alpha.1

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 (129) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +164 -106
  3. package/bin/_launcher.js +77 -0
  4. package/bin/claude-autopilot.js +3 -0
  5. package/bin/guardrail.js +3 -0
  6. package/package.json +15 -9
  7. package/presets/generic/guardrail.config.yaml +35 -0
  8. package/presets/generic/stack.md +40 -0
  9. package/presets/nextjs-supabase/{autopilot.config.yaml → guardrail.config.yaml} +7 -0
  10. package/scripts/autoregress.ts +27 -11
  11. package/skills/autopilot/SKILL.md +170 -0
  12. package/skills/claude-autopilot.md +80 -0
  13. package/skills/guardrail.md +39 -0
  14. package/skills/migrate/SKILL.md +83 -0
  15. package/src/adapters/council/claude.ts +41 -0
  16. package/src/adapters/council/openai.ts +40 -0
  17. package/src/adapters/council/types.ts +7 -0
  18. package/src/adapters/loader.ts +7 -7
  19. package/src/adapters/review-engine/auto.ts +2 -2
  20. package/src/adapters/review-engine/claude.ts +9 -11
  21. package/src/adapters/review-engine/codex.ts +9 -11
  22. package/src/adapters/review-engine/gemini.ts +9 -11
  23. package/src/adapters/review-engine/openai-compatible.ts +10 -12
  24. package/src/adapters/review-engine/parse-output.ts +32 -6
  25. package/src/adapters/review-engine/prompt-builder.ts +19 -0
  26. package/src/adapters/review-engine/types.ts +1 -1
  27. package/src/adapters/vcs-host/commit-status.ts +39 -0
  28. package/src/adapters/vcs-host/github.ts +2 -2
  29. package/src/cli/baseline.ts +125 -0
  30. package/src/cli/ci.ts +11 -8
  31. package/src/cli/costs.ts +80 -0
  32. package/src/cli/council.ts +96 -0
  33. package/src/cli/detector.ts +21 -5
  34. package/src/cli/explain.ts +197 -0
  35. package/src/cli/fix.ts +249 -0
  36. package/src/cli/hook.ts +72 -27
  37. package/src/cli/ignore-helper.ts +116 -0
  38. package/src/cli/index.ts +302 -28
  39. package/src/cli/init.ts +12 -12
  40. package/src/cli/lsp.ts +200 -0
  41. package/src/cli/mcp.ts +206 -0
  42. package/src/cli/pr-comment.ts +5 -5
  43. package/src/cli/pr-desc.ts +168 -0
  44. package/src/cli/pr-review-comments.ts +3 -3
  45. package/src/cli/pr.ts +76 -0
  46. package/src/cli/preflight.ts +15 -32
  47. package/src/cli/report.ts +186 -0
  48. package/src/cli/run.ts +140 -36
  49. package/src/cli/scan.ts +233 -0
  50. package/src/cli/setup.ts +121 -15
  51. package/src/cli/test-gen.ts +125 -0
  52. package/src/cli/triage.ts +137 -0
  53. package/src/cli/watch.ts +52 -31
  54. package/src/cli/worker.ts +109 -0
  55. package/src/core/cache/review-cache.ts +2 -2
  56. package/src/core/chunking/index.ts +2 -2
  57. package/src/core/config/loader.ts +24 -12
  58. package/src/core/config/preset-resolver.ts +6 -6
  59. package/src/core/config/schema.ts +121 -3
  60. package/src/core/config/types.ts +57 -2
  61. package/src/core/council/config.ts +71 -0
  62. package/src/core/council/context.ts +17 -0
  63. package/src/core/council/runner.ts +83 -0
  64. package/src/core/council/types.ts +45 -0
  65. package/src/core/detect/llm-key.ts +89 -0
  66. package/src/core/detect/workspaces.ts +103 -0
  67. package/src/core/errors.ts +4 -4
  68. package/src/core/fix/generator.ts +149 -0
  69. package/src/core/ignore/index.ts +4 -4
  70. package/src/core/mcp/concurrency.ts +16 -0
  71. package/src/core/mcp/handlers/fix-finding.ts +126 -0
  72. package/src/core/mcp/handlers/get-capabilities.ts +62 -0
  73. package/src/core/mcp/handlers/get-findings.ts +36 -0
  74. package/src/core/mcp/handlers/review-diff.ts +65 -0
  75. package/src/core/mcp/handlers/scan-files.ts +65 -0
  76. package/src/core/mcp/handlers/validate-fix.ts +41 -0
  77. package/src/core/mcp/run-store.ts +85 -0
  78. package/src/core/mcp/workspace.ts +35 -0
  79. package/src/core/persist/baseline.ts +112 -0
  80. package/src/core/persist/cost-log.ts +1 -1
  81. package/src/core/persist/findings-cache.ts +1 -1
  82. package/src/core/persist/triage.ts +112 -0
  83. package/src/core/phases/static-rules.ts +18 -5
  84. package/src/core/pipeline/review-phase.ts +65 -26
  85. package/src/core/pipeline/run.ts +42 -10
  86. package/src/core/runtime/lock.ts +2 -2
  87. package/src/core/runtime/state.ts +2 -2
  88. package/src/core/schema-alignment/detector.ts +59 -0
  89. package/src/core/schema-alignment/extractor/index.ts +24 -0
  90. package/src/core/schema-alignment/extractor/prisma.ts +21 -0
  91. package/src/core/schema-alignment/extractor/sql.ts +99 -0
  92. package/src/core/schema-alignment/llm-check.ts +91 -0
  93. package/src/core/schema-alignment/scanner.ts +107 -0
  94. package/src/core/schema-alignment/types.ts +43 -0
  95. package/src/core/shell.ts +3 -3
  96. package/src/core/static-rules/registry.ts +17 -8
  97. package/src/core/static-rules/rules/brand-tokens.ts +145 -0
  98. package/src/core/static-rules/rules/hardcoded-secrets.ts +27 -1
  99. package/src/core/static-rules/rules/insecure-redirect.ts +67 -0
  100. package/src/core/static-rules/rules/missing-auth.ts +70 -0
  101. package/src/core/static-rules/rules/schema-alignment.ts +132 -0
  102. package/src/core/static-rules/rules/sql-injection.ts +71 -0
  103. package/src/core/static-rules/rules/ssrf.ts +63 -0
  104. package/src/core/static-rules/tailwind-extractor.ts +38 -0
  105. package/src/core/test-gen/coverage-analyzer.ts +93 -0
  106. package/src/core/test-gen/framework-detector.ts +21 -0
  107. package/src/core/test-gen/test-writer.ts +33 -0
  108. package/src/core/ui/design-context-loader.ts +87 -0
  109. package/src/core/worker/client.ts +46 -0
  110. package/src/core/worker/lockfile.ts +38 -0
  111. package/src/core/worker/server.ts +81 -0
  112. package/src/formatters/junit.ts +52 -0
  113. package/src/formatters/sarif.ts +2 -2
  114. package/src/index.ts +1 -2
  115. package/tests/snapshots/baselines/src-formatters-sarif.json +4 -4
  116. package/tests/snapshots/index.json +3 -3
  117. package/tests/snapshots/src-formatters-sarif.snap.ts +1 -1
  118. package/tests/snapshots/src-snapshots-impact-selector.snap.ts +3 -3
  119. package/tests/snapshots/src-snapshots-import-scanner.snap.ts +3 -3
  120. package/tests/snapshots/src-snapshots-serializer.snap.ts +2 -2
  121. package/bin/autopilot.js +0 -20
  122. package/skills/autopilot.md +0 -157
  123. /package/presets/go/{autopilot.config.yaml → guardrail.config.yaml} +0 -0
  124. /package/presets/python-fastapi/{autopilot.config.yaml → guardrail.config.yaml} +0 -0
  125. /package/presets/rails-postgres/{autopilot.config.yaml → guardrail.config.yaml} +0 -0
  126. /package/presets/t3/{autopilot.config.yaml → guardrail.config.yaml} +0 -0
  127. /package/{src → scripts}/snapshots/impact-selector.ts +0 -0
  128. /package/{src → scripts}/snapshots/import-scanner.ts +0 -0
  129. /package/{src → scripts}/snapshots/serializer.ts +0 -0
@@ -0,0 +1,137 @@
1
+ import { loadCachedFindings } from '../core/persist/findings-cache.ts';
2
+ import {
3
+ loadTriage, saveTriage, addTriageEntry, removeTriageEntry, clearExpiredEntries,
4
+ type TriageState,
5
+ } from '../core/persist/triage.ts';
6
+
7
+ const C = {
8
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
9
+ green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m',
10
+ };
11
+ const fmt = (c: keyof typeof C, t: string) => `${C[c]}${t}${C.reset}`;
12
+
13
+ export interface TriageCommandOptions {
14
+ cwd?: string;
15
+ }
16
+
17
+ function parseTriageArgs(rest: string[]): { reason?: string; expiresInDays?: number; positional: string[] } {
18
+ let reason: string | undefined;
19
+ let expiresInDays: number | undefined;
20
+ const positional: string[] = [];
21
+ for (let i = 0; i < rest.length; i++) {
22
+ if (rest[i] === '--reason' && rest[i + 1]) { reason = rest[++i]; }
23
+ else if (rest[i] === '--expires' && rest[i + 1]) { expiresInDays = parseInt(rest[++i]!, 10); }
24
+ else if (!rest[i]!.startsWith('--')) { positional.push(rest[i]!); }
25
+ }
26
+ return { reason, expiresInDays, positional };
27
+ }
28
+
29
+ export async function runTriage(
30
+ subcommand: string | undefined,
31
+ rest: string[],
32
+ options: TriageCommandOptions = {},
33
+ ): Promise<number> {
34
+ const cwd = options.cwd ?? process.cwd();
35
+
36
+ if (subcommand === 'list' || subcommand === 'show') {
37
+ return cmdList(cwd);
38
+ }
39
+
40
+ if (subcommand === 'clear') {
41
+ const { positional } = parseTriageArgs(rest);
42
+ return cmdClear(cwd, positional, rest.includes('--expired'));
43
+ }
44
+
45
+ // Default: triage <finding-id> <state>
46
+ const findingId = subcommand;
47
+ const { reason, expiresInDays, positional } = parseTriageArgs(rest);
48
+ const stateArg = positional[0];
49
+
50
+ if (!findingId || !stateArg) {
51
+ printUsage();
52
+ return 1;
53
+ }
54
+
55
+ if (stateArg !== 'accepted-risk' && stateArg !== 'false-positive') {
56
+ console.error(fmt('red', `[triage] State must be "accepted-risk" or "false-positive", got: "${stateArg}"`));
57
+ return 1;
58
+ }
59
+
60
+ const findings = loadCachedFindings(cwd);
61
+ const finding = findings.find(f => f.id === findingId || f.id.startsWith(findingId));
62
+ if (!finding) {
63
+ console.error(fmt('red', `[triage] Finding not found: "${findingId}"`));
64
+ console.error(fmt('dim', ' Run `guardrail run` or `guardrail scan` first, then `guardrail report` to list IDs'));
65
+ return 1;
66
+ }
67
+
68
+ addTriageEntry(cwd, finding, stateArg as TriageState, { reason, expiresInDays });
69
+
70
+ const expNote = expiresInDays !== undefined ? fmt('dim', ` (expires in ${expiresInDays} days)`) : '';
71
+ console.log(`${fmt('green', '✓')} ${fmt('bold', stateArg)} ${finding.file}${finding.line ? `:${finding.line}` : ''} — ${finding.message}${expNote}`);
72
+ if (reason) console.log(fmt('dim', ` Reason: ${reason}`));
73
+ console.log(fmt('dim', ' Suppressed from future runs. Commit .guardrail-triage.json to share with team.'));
74
+ return 0;
75
+ }
76
+
77
+ function cmdList(cwd: string): number {
78
+ const store = loadTriage(cwd);
79
+ const now = new Date().toISOString();
80
+ const active = store.entries.filter(e => !e.expiresAt || e.expiresAt > now);
81
+ const expired = store.entries.filter(e => e.expiresAt && e.expiresAt <= now);
82
+
83
+ if (store.entries.length === 0) {
84
+ console.log(fmt('dim', '[triage] No triaged findings.'));
85
+ return 0;
86
+ }
87
+
88
+ console.log(`\n${fmt('bold', '[guardrail triage]')} ${active.length} active, ${expired.length} expired\n`);
89
+ for (const e of active) {
90
+ const tag = e.state === 'false-positive'
91
+ ? fmt('dim', 'false-positive ')
92
+ : fmt('yellow', 'accepted-risk ');
93
+ const exp = e.expiresAt ? fmt('dim', ` expires ${e.expiresAt.slice(0, 10)}`) : '';
94
+ console.log(` [${tag}] ${fmt('dim', `${e.file}${e.line ? `:${e.line}` : ''}`)} — ${e.id}${exp}`);
95
+ if (e.reason) console.log(fmt('dim', ` Reason: ${e.reason}`));
96
+ }
97
+ if (expired.length > 0) {
98
+ console.log(fmt('dim', `\n ${expired.length} expired — run \`guardrail triage clear --expired\` to remove`));
99
+ }
100
+ console.log('');
101
+ return 0;
102
+ }
103
+
104
+ function cmdClear(cwd: string, ids: string[], expired: boolean): number {
105
+ if (expired) {
106
+ const removed = clearExpiredEntries(cwd);
107
+ console.log(fmt('dim', `[triage] Cleared ${removed} expired entr${removed === 1 ? 'y' : 'ies'}`));
108
+ return 0;
109
+ }
110
+ if (ids.length === 0) {
111
+ console.error(fmt('red', '[triage] clear requires a finding ID or --expired'));
112
+ return 1;
113
+ }
114
+ const removed = removeTriageEntry(cwd, ids);
115
+ console.log(fmt('dim', `[triage] Cleared ${removed} entr${removed === 1 ? 'y' : 'ies'}`));
116
+ return 0;
117
+ }
118
+
119
+ function printUsage(): void {
120
+ console.error(`
121
+ ${fmt('bold', 'Usage:')}
122
+ guardrail triage <finding-id> accepted-risk|false-positive [options]
123
+ guardrail triage list
124
+ guardrail triage clear <finding-id> [<id>...]
125
+ guardrail triage clear --expired
126
+
127
+ ${fmt('bold', 'Options:')}
128
+ --reason <text> Explain why this finding was triaged
129
+ --expires <days> Auto-expire triage after N days
130
+
131
+ ${fmt('bold', 'States:')}
132
+ accepted-risk Known issue, risk accepted — suppress without fixing
133
+ false-positive Finding is incorrect — suppress permanently (or with expiry)
134
+
135
+ ${fmt('dim', 'Finding IDs come from `guardrail report` or the run output.')}
136
+ `);
137
+ }
package/src/cli/watch.ts CHANGED
@@ -3,9 +3,9 @@ import * as path from 'node:path';
3
3
  import { loadConfig } from '../core/config/loader.ts';
4
4
  import { resolvePreset, mergeConfigs } from '../core/config/preset-resolver.ts';
5
5
  import { loadAdapter } from '../adapters/loader.ts';
6
- import { runAutopilot } from '../core/pipeline/run.ts';
6
+ import { runGuardrail } from '../core/pipeline/run.ts';
7
7
  import type { ReviewEngine } from '../adapters/review-engine/types.ts';
8
- import type { AutopilotConfig } from '../core/config/types.ts';
8
+ import type { GuardrailConfig } from '../core/config/types.ts';
9
9
 
10
10
  const C = {
11
11
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
@@ -17,7 +17,7 @@ const fmt = (c: keyof typeof C, t: string) => `${C[c]}${t}${C.reset}`;
17
17
  export const IGNORED_PATTERNS: readonly RegExp[] = [
18
18
  /(^|[/\\])node_modules([/\\]|$)/,
19
19
  /(^|[/\\])\.git([/\\]|$)/,
20
- /(^|[/\\])\.autopilot-cache([/\\]|$)/,
20
+ /(^|[/\\])\.guardrail-cache([/\\]|$)/,
21
21
  /\.(log|tmp|swp|swo|DS_Store)$/,
22
22
  /~$/,
23
23
  ];
@@ -29,10 +29,12 @@ export function isIgnored(p: string): boolean {
29
29
  /**
30
30
  * Pure debounce accumulator — returned functions are the testable core of watch logic.
31
31
  * schedule(file) → adds file, starts/resets timer; when debounce fires, calls flush(batch).
32
+ * onSchedule (optional) is called on every schedule() with the file and current queue size.
32
33
  */
33
34
  export function makeDebouncer(
34
35
  flushFn: (batch: string[]) => void,
35
36
  debounceMs: number,
37
+ onSchedule?: (file: string, queueSize: number) => void,
36
38
  ): { schedule: (file: string) => void; pending: () => string[] } {
37
39
  const pending = new Set<string>();
38
40
  let timer: ReturnType<typeof setTimeout> | null = null;
@@ -46,6 +48,7 @@ export function makeDebouncer(
46
48
  timer = null;
47
49
  flushFn(batch);
48
50
  }, debounceMs);
51
+ onSchedule?.(file, pending.size);
49
52
  },
50
53
  pending() { return [...pending]; },
51
54
  };
@@ -59,47 +62,55 @@ export interface WatchOptions {
59
62
 
60
63
  export async function runWatch(options: WatchOptions = {}): Promise<void> {
61
64
  const cwd = options.cwd ?? process.cwd();
62
- const configPath = options.configPath ?? path.join(cwd, 'autopilot.config.yaml');
65
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
63
66
  const debounceMs = options.debounceMs ?? 300;
64
67
 
68
+ // Zero-config fallback — same as `run`
69
+ let config: GuardrailConfig;
65
70
  if (!fs.existsSync(configPath)) {
66
- console.error(fmt('red', `[watch] autopilot.config.yaml not found run: npx autopilot init`));
67
- process.exit(1);
71
+ config = { configVersion: 1, reviewEngine: { adapter: 'auto' }, testCommand: null };
72
+ } else {
73
+ try {
74
+ const userConfig = await loadConfig(configPath);
75
+ config = userConfig.preset
76
+ ? mergeConfigs((await resolvePreset(userConfig.preset)).config, userConfig)
77
+ : userConfig;
78
+ } catch (err) {
79
+ console.error(fmt('red', `[watch] Config error: ${err instanceof Error ? err.message : String(err)}`));
80
+ process.exit(1);
81
+ }
68
82
  }
69
83
 
70
- let config: AutopilotConfig;
71
- try {
72
- const userConfig = await loadConfig(configPath);
73
- config = userConfig.preset
74
- ? mergeConfigs((await resolvePreset(userConfig.preset)).config, userConfig)
75
- : userConfig;
76
- } catch (err) {
77
- console.error(fmt('red', `[watch] Config error: ${err instanceof Error ? err.message : String(err)}`));
78
- process.exit(1);
79
- }
84
+ const hasAnyKey = !!(process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY ||
85
+ process.env.GOOGLE_API_KEY || process.env.OPENAI_API_KEY || process.env.GROQ_API_KEY);
80
86
 
81
87
  let reviewEngine: ReviewEngine | undefined;
82
- if (config.reviewEngine) {
88
+ if (config.reviewEngine && hasAnyKey) {
83
89
  const ref = typeof config.reviewEngine === 'string' ? config.reviewEngine : config.reviewEngine.adapter;
84
- if (process.env.OPENAI_API_KEY) {
85
- try {
86
- reviewEngine = await loadAdapter<ReviewEngine>({
87
- point: 'review-engine', ref,
88
- options: typeof config.reviewEngine === 'string' ? undefined : config.reviewEngine.options,
89
- });
90
- } catch { /* skip */ }
91
- }
90
+ try {
91
+ reviewEngine = await loadAdapter<ReviewEngine>({
92
+ point: 'review-engine', ref,
93
+ options: typeof config.reviewEngine === 'string' ? undefined : config.reviewEngine.options,
94
+ });
95
+ } catch { /* skip — static rules still run */ }
92
96
  }
93
97
 
94
- console.log(`\n${fmt('bold', '[autopilot watch]')} ${fmt('dim', cwd)}`);
95
- console.log(fmt('dim', ` debounce: ${debounceMs}ms | Ctrl+C to exit\n`));
98
+ const keyStatus = hasAnyKey
99
+ ? fmt('green', '✓ LLM review enabled')
100
+ : fmt('yellow', '! No API key — static rules only (set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or GROQ_API_KEY)');
101
+
102
+ console.log(`\n${fmt('bold', '[guardrail watch]')} ${fmt('dim', cwd)}`);
103
+ console.log(fmt('dim', ` debounce: ${debounceMs}ms | Ctrl+C to exit`));
104
+ console.log(` ${keyStatus}\n`);
105
+ console.log(fmt('dim', ' Watching for changes…\n'));
96
106
 
97
107
  let running = false;
98
108
  const nextPending = new Set<string>();
109
+ let debounceLineShown = false;
99
110
 
100
111
  const runBatch = async (batch: string[]) => {
112
+ debounceLineShown = false;
101
113
  if (running) {
102
- // Queue these files for the next run after the current one completes
103
114
  for (const f of batch) nextPending.add(f);
104
115
  return;
105
116
  }
@@ -111,7 +122,7 @@ export async function runWatch(options: WatchOptions = {}): Promise<void> {
111
122
  console.log(fmt('dim', ` changed: ${rel.slice(0, 4).join(', ')}${rel.length > 4 ? ` +${rel.length - 4} more` : ''}`));
112
123
 
113
124
  try {
114
- const result = await runAutopilot({ touchedFiles: rel, config, reviewEngine, cwd });
125
+ const result = await runGuardrail({ touchedFiles: rel, config, reviewEngine, cwd });
115
126
 
116
127
  for (const phase of result.phases) {
117
128
  const icon = phase.status === 'pass' ? fmt('green', '✓')
@@ -137,7 +148,8 @@ export async function runWatch(options: WatchOptions = {}): Promise<void> {
137
148
  }
138
149
 
139
150
  running = false;
140
- // Flush anything that accumulated while we were running
151
+ console.log(fmt('dim', '\n Watching for changes…'));
152
+
141
153
  if (nextPending.size > 0) {
142
154
  const queued = [...nextPending];
143
155
  nextPending.clear();
@@ -145,7 +157,16 @@ export async function runWatch(options: WatchOptions = {}): Promise<void> {
145
157
  }
146
158
  };
147
159
 
148
- const debouncer = makeDebouncer(batch => { runBatch(batch); }, debounceMs);
160
+ const debouncer = makeDebouncer(
161
+ batch => { runBatch(batch); },
162
+ debounceMs,
163
+ (_file, queueSize) => {
164
+ if (!debounceLineShown && !running) {
165
+ process.stdout.write(fmt('dim', ` ⋯ ${queueSize} file(s) queued — reviewing in ${debounceMs}ms…\r`));
166
+ debounceLineShown = true;
167
+ }
168
+ },
169
+ );
149
170
 
150
171
  const onEvent = (_event: string, filename: string | null) => {
151
172
  if (!filename) return;
@@ -0,0 +1,109 @@
1
+ import { readLock, writeLock, deleteLock, isWorkerAlive } from '../core/worker/lockfile.ts';
2
+ import { stopWorker, getWorkerStatus } from '../core/worker/client.ts';
3
+ import { startWorkerServer } from '../core/worker/server.ts';
4
+ import { loadConfig } from '../core/config/loader.ts';
5
+ import type { ReviewEngine } from '../adapters/review-engine/types.ts';
6
+ import * as path from 'node:path';
7
+ import * as fs from 'node:fs';
8
+
9
+ const C = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', dim: '\x1b[2m', bold: '\x1b[1m' };
10
+
11
+ export async function runWorker(sub: string | undefined, options: { cwd?: string; configPath?: string } = {}): Promise<number> {
12
+ const cwd = options.cwd ?? process.cwd();
13
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
14
+
15
+ switch (sub) {
16
+ case 'start':
17
+ return workerStart(cwd, configPath);
18
+ case 'stop':
19
+ return workerStop(cwd);
20
+ case 'status':
21
+ return workerStatus(cwd);
22
+ default:
23
+ console.error(`${C.red}[guardrail worker] Unknown subcommand: "${sub ?? ''}". Use start|stop|status${C.reset}`);
24
+ return 1;
25
+ }
26
+ }
27
+
28
+ async function workerStart(cwd: string, configPath: string): Promise<number> {
29
+ const existing = readLock(cwd);
30
+ if (existing && isWorkerAlive(existing)) {
31
+ console.log(`${C.yellow}[worker] Already running — pid ${existing.pid} port ${existing.port}${C.reset}`);
32
+ return 0;
33
+ }
34
+
35
+ let config = { configVersion: 1 as const };
36
+ if (fs.existsSync(configPath)) {
37
+ const loaded = await loadConfig(configPath);
38
+ if (loaded) config = loaded;
39
+ }
40
+
41
+ // Lazy import to avoid loading review engine at CLI startup
42
+ const { loadAdapter } = await import('../adapters/loader.ts');
43
+ const { runReviewPhase } = await import('../core/pipeline/review-phase.ts');
44
+
45
+ const engineRef = (config as { reviewEngine?: unknown }).reviewEngine;
46
+ const ref = typeof engineRef === 'string' ? engineRef : (engineRef as { adapter?: string })?.adapter ?? 'auto';
47
+ const engineOptions = typeof engineRef === 'object' && engineRef !== null
48
+ ? (engineRef as { options?: Record<string, unknown> }).options
49
+ : undefined;
50
+
51
+ const engine = await loadAdapter({
52
+ point: 'review-engine',
53
+ ref,
54
+ options: engineOptions,
55
+ });
56
+
57
+ const server = await startWorkerServer({
58
+ cwd,
59
+ onReview: async (files, cfg) => {
60
+ const result = await runReviewPhase({ touchedFiles: files, config: cfg, engine: engine as unknown as ReviewEngine });
61
+ return { findings: result.findings, usage: result.costUSD !== undefined ? { costUSD: result.costUSD } : undefined };
62
+ },
63
+ });
64
+
65
+ writeLock(cwd, { pid: process.pid, port: server.port, startedAt: new Date().toISOString() });
66
+
67
+ const cleanup = () => { deleteLock(cwd); server.close().then(() => process.exit(0)); };
68
+ process.on('SIGTERM', cleanup);
69
+ process.on('SIGINT', cleanup);
70
+
71
+ console.log(`${C.green}[worker] Started — pid ${process.pid} port ${server.port}${C.reset}`);
72
+ console.log(`${C.dim} guardrail run --use-worker # dispatch review chunks to this worker${C.reset}`);
73
+
74
+ await new Promise(() => {}); // keep alive
75
+ return 0;
76
+ }
77
+
78
+ async function workerStop(cwd: string): Promise<number> {
79
+ const lock = readLock(cwd);
80
+ if (!lock) { console.log('[worker] No worker running'); return 0; }
81
+ if (!isWorkerAlive(lock)) { deleteLock(cwd); console.log('[worker] Stale lockfile removed'); return 0; }
82
+ await stopWorker(lock);
83
+ // Give it 3s to exit, then SIGTERM
84
+ await new Promise(r => setTimeout(r, 1000));
85
+ if (isWorkerAlive(lock)) {
86
+ try { process.kill(lock.pid, 'SIGTERM'); } catch { /* already dead */ }
87
+ }
88
+ deleteLock(cwd);
89
+ console.log(`${C.green}[worker] Stopped${C.reset}`);
90
+ return 0;
91
+ }
92
+
93
+ async function workerStatus(cwd: string): Promise<number> {
94
+ const lock = readLock(cwd);
95
+ if (!lock) { console.log('[worker] Not running'); return 1; }
96
+ if (!isWorkerAlive(lock)) { console.log(`[worker] Dead (stale lock — pid ${lock.pid})`); return 1; }
97
+ try {
98
+ const status = await getWorkerStatus(lock);
99
+ console.log(`[worker] Running`);
100
+ console.log(` pid: ${status.pid}`);
101
+ console.log(` port: ${status.port}`);
102
+ console.log(` jobs processed: ${status.jobsProcessed}`);
103
+ console.log(` uptime: ${Math.round(status.uptimeMs / 1000)}s`);
104
+ return 0;
105
+ } catch {
106
+ console.log(`[worker] Running (pid ${lock.pid} port ${lock.port}) — status endpoint unreachable`);
107
+ return 0;
108
+ }
109
+ }
@@ -17,10 +17,10 @@ export interface ReviewCacheOptions {
17
17
  }
18
18
 
19
19
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
20
- // Prefer env override, then ~/.autopilot-cache to survive across cwd changes and container restarts
20
+ // Prefer env override, then ~/.guardrail-cache to survive across cwd changes and container restarts
21
21
  const DEFAULT_CACHE_DIR = process.env.AUTOPILOT_CACHE_DIR
22
22
  ? path.join(process.env.AUTOPILOT_CACHE_DIR, 'reviews')
23
- : path.join(os.homedir(), '.autopilot-cache', 'reviews');
23
+ : path.join(os.homedir(), '.guardrail-cache', 'reviews');
24
24
 
25
25
  export class ReviewCache {
26
26
  private readonly cacheDir: string;
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import type { ReviewEngine, ReviewInput } from '../../adapters/review-engine/types.ts';
4
- import type { AutopilotConfig } from '../config/types.ts';
4
+ import type { GuardrailConfig } from '../config/types.ts';
5
5
  import { rankByRisk } from './risk-ranker.ts';
6
6
  import { getFileDiffs, formatDiffContent } from '../git/diff-hunks.ts';
7
7
 
@@ -14,7 +14,7 @@ export interface ReviewChunk {
14
14
  export interface BuildChunksInput {
15
15
  touchedFiles: string[];
16
16
  strategy: 'auto' | 'single-pass' | 'file-level' | 'diff' | 'auto-diff';
17
- chunking?: AutopilotConfig['chunking'];
17
+ chunking?: GuardrailConfig['chunking'];
18
18
  engine: ReviewEngine;
19
19
  cwd?: string;
20
20
  protectedPaths?: string[];
@@ -1,19 +1,19 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as yaml from 'js-yaml';
3
3
  import Ajv from 'ajv';
4
- import { AutopilotError } from '../errors.ts';
5
- import type { AutopilotConfig } from './types.ts';
6
- import { AUTOPILOT_CONFIG_SCHEMA } from './schema.ts';
4
+ import { GuardrailError } from '../errors.ts';
5
+ import type { GuardrailConfig } from './types.ts';
6
+ import { GUARDRAIL_CONFIG_SCHEMA } from './schema.ts';
7
7
 
8
8
  const ajv = new Ajv({ allErrors: true, strict: false });
9
- const validate = ajv.compile(AUTOPILOT_CONFIG_SCHEMA);
9
+ const validate = ajv.compile(GUARDRAIL_CONFIG_SCHEMA);
10
10
 
11
- export async function loadConfig(path: string): Promise<AutopilotConfig> {
11
+ export async function loadConfig(path: string): Promise<GuardrailConfig> {
12
12
  let content: string;
13
13
  try {
14
14
  content = await fs.readFile(path, 'utf8');
15
15
  } catch (err) {
16
- throw new AutopilotError(`Config file not found: ${path}`, {
16
+ throw new GuardrailError(`Config file not found: ${path}`, {
17
17
  code: 'user_input',
18
18
  details: { path, cause: err instanceof Error ? err.message : String(err) },
19
19
  });
@@ -23,19 +23,31 @@ export async function loadConfig(path: string): Promise<AutopilotConfig> {
23
23
  try {
24
24
  parsed = yaml.load(content);
25
25
  } catch (err) {
26
- throw new AutopilotError(`Invalid YAML in ${path}`, {
26
+ throw new GuardrailError(`Invalid YAML in ${path}`, {
27
27
  code: 'invalid_config',
28
28
  details: { path, cause: err instanceof Error ? err.message : String(err) },
29
29
  });
30
30
  }
31
31
 
32
32
  if (!validate(parsed)) {
33
- const errors = (validate.errors ?? []).map(e => `${e.instancePath || '<root>'}: ${e.message}`);
34
- throw new AutopilotError('Config schema validation failed', {
35
- code: 'invalid_config',
36
- details: { path, errors },
33
+ const errors = (validate.errors ?? []).map(e => {
34
+ const loc = e.instancePath ? e.instancePath.replace(/^\//, '').replace(/\//g, '.') : '<root>';
35
+ // enum errors: list allowed values
36
+ if (e.keyword === 'enum' && Array.isArray(e.params?.allowedValues)) {
37
+ return `${loc}: must be one of ${(e.params.allowedValues as unknown[]).map(v => JSON.stringify(v)).join(', ')}`;
38
+ }
39
+ // additionalProperties: name the unexpected key
40
+ if (e.keyword === 'additionalProperties' && e.params?.additionalProperty) {
41
+ return `${loc}: unexpected key "${e.params.additionalProperty as string}"`;
42
+ }
43
+ return `${loc}: ${e.message ?? 'invalid'}`;
37
44
  });
45
+ const summary = errors.slice(0, 5).join('\n ');
46
+ throw new GuardrailError(
47
+ `guardrail.config.yaml is invalid:\n ${summary}${errors.length > 5 ? `\n …and ${errors.length - 5} more` : ''}`,
48
+ { code: 'invalid_config', details: { path, errors } },
49
+ );
38
50
  }
39
51
 
40
- return parsed as AutopilotConfig;
52
+ return parsed as GuardrailConfig;
41
53
  }
@@ -1,14 +1,14 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { loadConfig } from './loader.ts';
4
- import { AutopilotError } from '../errors.ts';
5
- import type { AutopilotConfig } from './types.ts';
4
+ import { GuardrailError } from '../errors.ts';
5
+ import type { GuardrailConfig } from './types.ts';
6
6
 
7
7
  const PRESET_ROOT = path.resolve(process.cwd(), 'presets');
8
8
 
9
9
  export interface ResolvedPreset {
10
10
  name: string;
11
- config: AutopilotConfig;
11
+ config: GuardrailConfig;
12
12
  stack: string;
13
13
  }
14
14
 
@@ -17,13 +17,13 @@ export async function resolvePreset(name: string): Promise<ResolvedPreset> {
17
17
  try {
18
18
  await fs.stat(presetDir);
19
19
  } catch {
20
- throw new AutopilotError(`Preset not found: ${name}`, {
20
+ throw new GuardrailError(`Preset not found: ${name}`, {
21
21
  code: 'invalid_config',
22
22
  details: { name, presetDir },
23
23
  });
24
24
  }
25
25
 
26
- const config = await loadConfig(path.join(presetDir, 'autopilot.config.yaml'));
26
+ const config = await loadConfig(path.join(presetDir, 'guardrail.config.yaml'));
27
27
  let stack = '';
28
28
  try {
29
29
  stack = await fs.readFile(path.join(presetDir, 'stack.md'), 'utf8');
@@ -33,7 +33,7 @@ export async function resolvePreset(name: string): Promise<ResolvedPreset> {
33
33
  return { name, config, stack };
34
34
  }
35
35
 
36
- export function mergeConfigs(preset: AutopilotConfig, user: AutopilotConfig): AutopilotConfig {
36
+ export function mergeConfigs(preset: GuardrailConfig, user: GuardrailConfig): GuardrailConfig {
37
37
  return {
38
38
  ...preset,
39
39
  ...user,