@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,233 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import { loadConfig } from '../core/config/loader.ts';
4
+ import { loadAdapter } from '../adapters/loader.ts';
5
+ import type { ReviewEngine } from '../adapters/review-engine/types.ts';
6
+ import { runReviewPhase } from '../core/pipeline/review-phase.ts';
7
+ import { detectStack } from '../core/detect/stack.ts';
8
+ import { loadIgnoreRules, parseConfigIgnore, applyIgnoreRules } from '../core/ignore/index.ts';
9
+ import { saveCachedFindings } from '../core/persist/findings-cache.ts';
10
+ import type { GuardrailConfig } from '../core/config/types.ts';
11
+ import { detectLLMKey, LLM_KEY_HINTS } from '../core/detect/llm-key.ts';
12
+
13
+ const C = {
14
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
15
+ green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m',
16
+ };
17
+ const fmt = (c: keyof typeof C, t: string) => `${C[c]}${t}${C.reset}`;
18
+
19
+ const IGNORED_DIRS = new Set([
20
+ 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'coverage',
21
+ '.guardrail-cache', '.autopilot', '__pycache__', '.venv', 'vendor',
22
+ ]);
23
+
24
+ const CODE_EXTS = new Set([
25
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
26
+ '.py', '.go', '.rb', '.rs', '.java', '.kt', '.swift',
27
+ '.c', '.cpp', '.h', '.cs', '.php',
28
+ '.sql', '.sh', '.bash', '.yaml', '.yml', '.json', '.toml',
29
+ ]);
30
+
31
+ function collectFiles(target: string, cwd: string): string[] {
32
+ const abs = path.isAbsolute(target) ? target : path.resolve(cwd, target);
33
+ if (!fs.existsSync(abs)) return [];
34
+
35
+ const stat = fs.statSync(abs);
36
+ if (stat.isFile()) return [abs];
37
+
38
+ const results: string[] = [];
39
+ function walk(dir: string) {
40
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
41
+ if (IGNORED_DIRS.has(entry.name)) continue;
42
+ const full = path.join(dir, entry.name);
43
+ if (entry.isDirectory()) {
44
+ walk(full);
45
+ } else if (entry.isFile() && CODE_EXTS.has(path.extname(entry.name))) {
46
+ results.push(full);
47
+ }
48
+ }
49
+ }
50
+ walk(abs);
51
+ return results;
52
+ }
53
+
54
+ function collectAllFiles(cwd: string): string[] {
55
+ return collectFiles(cwd, cwd);
56
+ }
57
+
58
+ export interface ScanCommandOptions {
59
+ cwd?: string;
60
+ configPath?: string;
61
+ targets?: string[]; // explicit paths/dirs to scan
62
+ all?: boolean; // scan entire codebase
63
+ ask?: string; // targeted question to inject into review prompt
64
+ focus?: 'security' | 'logic' | 'performance' | 'brand' | 'all';
65
+ dryRun?: boolean;
66
+ }
67
+
68
+ export async function runScan(options: ScanCommandOptions = {}): Promise<number> {
69
+ const cwd = options.cwd ?? process.cwd();
70
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
71
+
72
+ let config: GuardrailConfig = { configVersion: 1 };
73
+ if (fs.existsSync(configPath)) {
74
+ const loaded = await loadConfig(configPath);
75
+ if (loaded) config = loaded;
76
+ }
77
+
78
+ // Collect files
79
+ let files: string[];
80
+ if (options.all) {
81
+ files = collectAllFiles(cwd);
82
+ } else if (options.targets && options.targets.length > 0) {
83
+ files = options.targets.flatMap(t => collectFiles(t, cwd));
84
+ } else {
85
+ console.error(fmt('red', '[scan] Specify a path, --all, or use `guardrail run` for git-changed files'));
86
+ console.error(fmt('dim', ' Examples:'));
87
+ console.error(fmt('dim', ' guardrail scan src/auth/'));
88
+ console.error(fmt('dim', ' guardrail scan --all'));
89
+ console.error(fmt('dim', ' guardrail scan --ask "is there SQL injection?" src/db/'));
90
+ return 1;
91
+ }
92
+
93
+ // Deduplicate
94
+ files = [...new Set(files)];
95
+
96
+ if (files.length === 0) {
97
+ console.log(fmt('yellow', '[scan] No code files found at the specified path(s)'));
98
+ return 0;
99
+ }
100
+
101
+ if (options.dryRun) {
102
+ console.log(fmt('bold', `[scan] Would scan ${files.length} file(s):`));
103
+ for (const f of files) console.log(fmt('dim', ` ${path.relative(cwd, f)}`));
104
+ return 0;
105
+ }
106
+
107
+ // Auto-detect stack if not in config
108
+ if (!config.stack) {
109
+ config = { ...config, stack: detectStack(cwd) ?? undefined };
110
+ }
111
+
112
+ // Build review engine
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 1;
120
+ }
121
+ const engineRef = typeof config.reviewEngine === 'string' ? config.reviewEngine
122
+ : (config.reviewEngine?.adapter ?? 'auto');
123
+ let engine: ReviewEngine;
124
+ try {
125
+ engine = await loadAdapter<ReviewEngine>({
126
+ point: 'review-engine',
127
+ ref: engineRef,
128
+ options: typeof config.reviewEngine === 'object' ? config.reviewEngine.options as Record<string, unknown> : undefined,
129
+ });
130
+ } catch (err) {
131
+ console.error(fmt('red', `[scan] Could not load review engine: ${err instanceof Error ? err.message : String(err)}`));
132
+ return 1;
133
+ }
134
+
135
+ const focusLabel = options.focus && options.focus !== 'all' ? options.focus : null;
136
+ const relFiles = files.map(f => path.relative(cwd, f));
137
+
138
+ console.log('');
139
+ const scopeDesc = options.all ? 'entire codebase' : relFiles.slice(0, 3).join(', ') + (relFiles.length > 3 ? ` +${relFiles.length - 3} more` : '');
140
+ console.log(fmt('bold', `[guardrail scan]`) + fmt('dim', ` ${files.length} file(s) — ${scopeDesc}`));
141
+ if (options.ask) console.log(fmt('dim', ` question: ${options.ask}`));
142
+ if (focusLabel) console.log(fmt('dim', ` focus: ${focusLabel}`));
143
+ console.log('');
144
+
145
+ // Build a focused git summary / prompt context
146
+ const focusHint = buildFocusHint(options.ask, focusLabel);
147
+
148
+ const result = await runReviewPhase({
149
+ touchedFiles: relFiles,
150
+ engine,
151
+ config,
152
+ cwd,
153
+ gitSummary: focusHint,
154
+ });
155
+
156
+ // Apply ignore rules
157
+ const ignoreRules = [...loadIgnoreRules(cwd), ...parseConfigIgnore(config.ignore)];
158
+ const findings = applyIgnoreRules(result.findings, ignoreRules);
159
+
160
+ // Print results
161
+ if (findings.length === 0 && options.ask && result.rawOutputs && result.rawOutputs.length > 0) {
162
+ // --ask returned prose rather than structured findings — surface raw response
163
+ console.log(fmt('cyan', `Answer:`));
164
+ for (const raw of result.rawOutputs) {
165
+ // Strip markdown fences and the ## Findings / ## Review Summary headers if present
166
+ const cleaned = raw.replace(/^##\s+Review Summary\s*\n/gm, '').replace(/^##\s+Findings\s*\n/gm, '').trim();
167
+ console.log(cleaned);
168
+ }
169
+ console.log('');
170
+ } else if (findings.length === 0) {
171
+ console.log(fmt('green', '✓ No findings'));
172
+ } else {
173
+ const critical = findings.filter(f => f.severity === 'critical');
174
+ const warnings = findings.filter(f => f.severity === 'warning');
175
+ const notes = findings.filter(f => f.severity === 'note');
176
+
177
+ if (critical.length > 0) {
178
+ console.log(fmt('red', `🚨 ${critical.length} critical`));
179
+ for (const f of critical) {
180
+ const loc = f.file && f.file !== '<unspecified>' ? fmt('dim', `${f.file}${f.line ? `:${f.line}` : ''}`) + ' ' : '';
181
+ console.log(` ${loc}${f.message}`);
182
+ if (f.suggestion) console.log(fmt('dim', ` → ${f.suggestion}`));
183
+ }
184
+ console.log('');
185
+ }
186
+ if (warnings.length > 0) {
187
+ console.log(fmt('yellow', `⚠ ${warnings.length} warning${warnings.length !== 1 ? 's' : ''}`));
188
+ for (const f of warnings) {
189
+ const loc = f.file && f.file !== '<unspecified>' ? fmt('dim', `${f.file}${f.line ? `:${f.line}` : ''}`) + ' ' : '';
190
+ console.log(` ${loc}${f.message}`);
191
+ if (f.suggestion) console.log(fmt('dim', ` → ${f.suggestion}`));
192
+ }
193
+ console.log('');
194
+ }
195
+ if (notes.length > 0) {
196
+ console.log(fmt('dim', `ℹ ${notes.length} note${notes.length !== 1 ? 's' : ''}`));
197
+ for (const f of notes) {
198
+ const loc = f.file && f.file !== '<unspecified>' ? `${f.file}${f.line ? `:${f.line}` : ''} ` : '';
199
+ console.log(fmt('dim', ` ${loc}${f.message}`));
200
+ }
201
+ console.log('');
202
+ }
203
+ }
204
+
205
+ // Persist findings so `guardrail fix` can read them
206
+ saveCachedFindings(cwd, findings);
207
+
208
+ if (result.costUSD !== undefined) {
209
+ console.log(fmt('dim', ` $${result.costUSD.toFixed(4)} · ${result.durationMs}ms`));
210
+ }
211
+
212
+ const fixable = findings.filter(f => f.severity === 'critical' || f.severity === 'warning');
213
+ if (fixable.length > 0) {
214
+ console.log(fmt('dim', ` → run \`guardrail fix\` to auto-fix ${fixable.length} finding${fixable.length !== 1 ? 's' : ''}`));
215
+ }
216
+
217
+ return findings.some(f => f.severity === 'critical') ? 1 : 0;
218
+ }
219
+
220
+ function buildFocusHint(ask: string | undefined, focus: string | null): string {
221
+ const parts: string[] = [];
222
+ if (ask) {
223
+ parts.push(
224
+ `TARGETED QUESTION (required): The reviewer specifically wants to know: "${ask}". ` +
225
+ `You MUST answer this question using the structured findings format. ` +
226
+ `Even if no issues are found, output at least one ### [NOTE] finding that directly answers the question.`,
227
+ );
228
+ }
229
+ if (focus === 'security') parts.push('Focus: security vulnerabilities, auth issues, injection risks, data exposure');
230
+ if (focus === 'logic') parts.push('Focus: logic bugs, incorrect behavior, edge cases, null handling, async errors');
231
+ if (focus === 'performance') parts.push('Focus: performance issues, N+1 queries, blocking I/O, memory leaks');
232
+ return parts.join(' | ');
233
+ }
package/src/cli/setup.ts CHANGED
@@ -5,9 +5,13 @@ import { fileURLToPath } from 'node:url';
5
5
  import { detectProject } from './detector.ts';
6
6
  import { runHook } from './hook.ts';
7
7
  import { runDoctor } from './preflight.ts';
8
+ import { detectLLMKey, LLM_KEY_NAMES } from '../core/detect/llm-key.ts';
8
9
 
9
10
  const PASS = '\x1b[32m✓\x1b[0m';
10
11
  const WARN = '\x1b[33m!\x1b[0m';
12
+ const DIM = (t: string) => `\x1b[2m${t}\x1b[0m`;
13
+ const BOLD = (t: string) => `\x1b[1m${t}\x1b[0m`;
14
+ const CYAN = (t: string) => `\x1b[36m${t}\x1b[0m`;
11
15
 
12
16
  const PRESET_LABELS: Record<string, string> = {
13
17
  'nextjs-supabase': 'Next.js + Supabase',
@@ -15,19 +19,69 @@ const PRESET_LABELS: Record<string, string> = {
15
19
  'rails-postgres': 'Ruby on Rails + PostgreSQL',
16
20
  'python-fastapi': 'Python FastAPI',
17
21
  'go': 'Go + PostgreSQL',
22
+ 'generic': 'Generic (no stack-specific assumptions)',
23
+ };
24
+
25
+ export type ProfileName = 'security-strict' | 'team' | 'solo';
26
+
27
+ const PROFILES: Record<ProfileName, { label: string; overlay: string }> = {
28
+ 'security-strict': {
29
+ label: 'Security Strict',
30
+ overlay: [
31
+ 'staticRules:',
32
+ ' - hardcoded-secrets',
33
+ ' - npm-audit',
34
+ ' - package-lock-sync',
35
+ ' - sql-injection',
36
+ ' - missing-auth',
37
+ ' - ssrf',
38
+ ' - insecure-redirect',
39
+ 'policy:',
40
+ ' failOn: warning',
41
+ ' newOnly: false',
42
+ ].join('\n'),
43
+ },
44
+ 'team': {
45
+ label: 'Team',
46
+ overlay: [
47
+ 'staticRules:',
48
+ ' - hardcoded-secrets',
49
+ ' - npm-audit',
50
+ ' - package-lock-sync',
51
+ ' - sql-injection',
52
+ ' - missing-auth',
53
+ ' - ssrf',
54
+ ' - insecure-redirect',
55
+ 'policy:',
56
+ ' failOn: critical',
57
+ ' newOnly: false',
58
+ ].join('\n'),
59
+ },
60
+ 'solo': {
61
+ label: 'Solo Dev',
62
+ overlay: [
63
+ 'staticRules:',
64
+ ' - hardcoded-secrets',
65
+ ' - npm-audit',
66
+ 'policy:',
67
+ ' failOn: critical',
68
+ ' newOnly: false',
69
+ ].join('\n'),
70
+ },
18
71
  };
19
72
 
20
73
  export interface SetupOptions {
21
74
  cwd?: string;
22
75
  force?: boolean;
23
76
  skipHook?: boolean;
77
+ profile?: ProfileName;
24
78
  }
25
79
 
26
80
  function presetSearchPaths(name: string, cwd: string): string[] {
27
81
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
28
82
  return [
29
- path.join(pkgRoot, 'presets', name, 'autopilot.config.yaml'),
30
- path.join(cwd, 'node_modules', '@delegance', 'claude-autopilot', 'presets', name, 'autopilot.config.yaml'),
83
+ path.join(pkgRoot, 'presets', name, 'guardrail.config.yaml'),
84
+ path.join(cwd, 'node_modules', '@delegance', 'claude-autopilot', 'presets', name, 'guardrail.config.yaml'),
31
85
  ];
32
86
  }
33
87
 
@@ -40,24 +94,35 @@ function findPresetConfig(name: string, cwd: string): string | null {
40
94
 
41
95
  export async function runSetup(options: SetupOptions = {}): Promise<void> {
42
96
  const cwd = options.cwd ?? process.cwd();
43
- const dest = path.join(cwd, 'autopilot.config.yaml');
97
+ const dest = path.join(cwd, 'guardrail.config.yaml');
44
98
 
45
99
  if (fs.existsSync(dest) && !options.force) {
46
- throw new Error('autopilot.config.yaml already exists — use --force to overwrite');
100
+ throw new Error('guardrail.config.yaml already exists — use --force to overwrite');
47
101
  }
48
102
 
49
- console.log('\n[setup] Detecting project type...');
103
+ console.log(`\n${BOLD('[guardrail setup]')} ${DIM(cwd)}\n`);
104
+ console.log(`${BOLD('Detecting project…')}\n`);
50
105
 
51
106
  const detection = detectProject(cwd);
52
107
  const label = PRESET_LABELS[detection.preset] ?? detection.preset;
53
108
 
54
109
  if (detection.confidence === 'high') {
55
- console.log(` ${PASS} ${label} (${detection.evidence})`);
110
+ console.log(` ${PASS} Stack: ${label}`);
111
+ console.log(` ${PASS} Evidence: ${DIM(detection.evidence)}`);
112
+ } else {
113
+ console.log(` ${WARN} Stack: ${label} ${DIM('(low confidence — fallback preset)')}`);
114
+ console.log(` ${DIM(detection.evidence)}`);
115
+ console.log(` ${DIM('Edit guardrail.config.yaml to switch presets if needed')}`);
116
+ }
117
+ console.log(` ${PASS} Test command: ${DIM(detection.testCommand)}`);
118
+
119
+ const { hasKey, preferred } = detectLLMKey();
120
+ if (hasKey) {
121
+ console.log(` ${PASS} LLM API key: detected (${preferred})`);
56
122
  } else {
57
- console.log(` ${WARN} ${label} no strong signals found, defaulted to ${detection.preset}`);
58
- console.log(` \x1b[2mEdit autopilot.config.yaml to switch presets if needed\x1b[0m`);
123
+ console.log(` ${WARN} LLM API key: not found`);
124
+ console.log(` ${DIM(`Set one of: ${LLM_KEY_NAMES.join(', ')}`)}`);
59
125
  }
60
- console.log(` ${PASS} Test command: ${detection.testCommand}`);
61
126
 
62
127
  const presetConfigPath = findPresetConfig(detection.preset, cwd);
63
128
  if (!presetConfigPath) {
@@ -66,20 +131,61 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
66
131
 
67
132
  let presetContent = await fsAsync.readFile(presetConfigPath, 'utf8');
68
133
  presetContent = presetContent.trimEnd() + `\ntestCommand: "${detection.testCommand}"\n`;
134
+
135
+ // Apply profile overlay if specified
136
+ if (options.profile) {
137
+ const profile = PROFILES[options.profile];
138
+ if (profile) {
139
+ console.log(` ${PASS} Profile: ${profile.label}`);
140
+ // Profile overlay replaces staticRules + policy sections from preset
141
+ presetContent = presetContent
142
+ .replace(/^staticRules:.*?(?=^\w|\z)/ms, '')
143
+ .replace(/^policy:.*?(?=^\w|\z)/ms, '');
144
+ presetContent = presetContent.trimEnd() + `\n${profile.overlay}\n`;
145
+ }
146
+ }
147
+
69
148
  await fsAsync.writeFile(dest, presetContent, 'utf8');
70
- console.log(` ${PASS} Created autopilot.config.yaml`);
71
149
 
150
+ console.log(`\n${BOLD('Config written to guardrail.config.yaml:')}\n`);
151
+ for (const line of presetContent.trimEnd().split('\n')) {
152
+ console.log(` ${DIM(line)}`);
153
+ }
154
+
155
+ let hookInstalled = false;
72
156
  if (!options.skipHook) {
73
157
  const hookCode = await runHook('install', { cwd, silent: true });
74
- if (hookCode === 0) {
75
- console.log(` ${PASS} Installed pre-push git hook`);
158
+ hookInstalled = hookCode === 0;
159
+ if (hookInstalled) {
160
+ console.log(`\n ${PASS} Pre-push git hook installed`);
76
161
  } else {
77
- console.log(` ${WARN} Hook install failed (not fatal — run: npx autopilot hook install)`);
162
+ console.log(`\n ${WARN} Hook install failed (run: npx guardrail hook install)`);
78
163
  }
79
164
  }
80
165
 
81
- console.log('\n[setup] Checking prerequisites...');
166
+ console.log('\nChecking prerequisites');
82
167
  await runDoctor();
83
168
 
84
- console.log('\n[setup] Done. Run: npx autopilot run\n');
169
+ console.log(`\n${BOLD('Next steps:')}\n`);
170
+ if (!hasKey) {
171
+ console.log(` 1. ${CYAN('Set an LLM API key')} — guardrail needs one to review code:`);
172
+ console.log(` export ANTHROPIC_API_KEY=sk-ant-... # https://console.anthropic.com/`);
173
+ console.log(` export OPENAI_API_KEY=sk-... # https://platform.openai.com/api-keys`);
174
+ console.log(` export GROQ_API_KEY=gsk_... # https://console.groq.com/keys (free)\n`);
175
+ console.log(` 2. ${CYAN('Review your changes:')}`);
176
+ console.log(` npx guardrail run --base main\n`);
177
+ console.log(` 3. ${CYAN('Scan any path directly:')}`);
178
+ console.log(` npx guardrail scan src/auth/\n`);
179
+ } else {
180
+ console.log(` ${CYAN('Review git-changed files:')}`);
181
+ console.log(` npx guardrail run --base main\n`);
182
+ console.log(` ${CYAN('Scan any path (no git needed):')}`);
183
+ console.log(` npx guardrail scan src/auth/\n`);
184
+ console.log(` ${CYAN('Ask a targeted question:')}`);
185
+ console.log(` npx guardrail scan --ask "is there SQL injection here?" src/db/\n`);
186
+ if (!hookInstalled && !options.skipHook) {
187
+ console.log(` ${CYAN('Install pre-push hook (auto-runs before git push):')}`);
188
+ console.log(` npx guardrail hook install\n`);
189
+ }
190
+ }
85
191
  }
@@ -0,0 +1,125 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { loadConfig } from '../core/config/loader.ts';
5
+ import { loadAdapter } from '../adapters/loader.ts';
6
+ import type { ReviewEngine } from '../adapters/review-engine/types.ts';
7
+ import { findCoverageGaps } from '../core/test-gen/coverage-analyzer.ts';
8
+ import { detectTestFramework } from '../core/test-gen/framework-detector.ts';
9
+ import { writeGeneratedTest, buildGenerationPrompt } from '../core/test-gen/test-writer.ts';
10
+
11
+ const C = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', dim: '\x1b[2m', bold: '\x1b[1m', cyan: '\x1b[36m' };
12
+
13
+ export interface TestGenOptions {
14
+ cwd?: string;
15
+ configPath?: string;
16
+ targets?: string[];
17
+ base?: string;
18
+ dryRun?: boolean;
19
+ verify?: boolean;
20
+ }
21
+
22
+ export async function runTestGen(options: TestGenOptions = {}): Promise<number> {
23
+ const cwd = options.cwd ?? process.cwd();
24
+ const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
25
+
26
+ let config = { configVersion: 1 as const, testCommand: null as string | null };
27
+ if (fs.existsSync(configPath)) {
28
+ try {
29
+ const loaded = await loadConfig(configPath);
30
+ if (loaded) config = loaded as typeof config;
31
+ } catch {
32
+ // proceed with defaults if config fails to load
33
+ }
34
+ }
35
+
36
+ // Collect files to analyze
37
+ let files: string[];
38
+ if (options.targets && options.targets.length > 0) {
39
+ files = options.targets.map(t => path.isAbsolute(t) ? t : path.resolve(cwd, t));
40
+ } else {
41
+ // Fall back to git-changed files
42
+ try {
43
+ const base = options.base ?? 'HEAD~1';
44
+ const out = execFileSync('git', ['diff', '--name-only', base, 'HEAD'], { cwd, encoding: 'utf8' });
45
+ files = out.trim().split('\n').filter(Boolean).map(f => path.resolve(cwd, f));
46
+ } catch {
47
+ console.error(`${C.red}[test-gen] No targets specified and git diff failed. Pass a path: guardrail test-gen src/${C.reset}`);
48
+ return 1;
49
+ }
50
+ }
51
+
52
+ console.log(`${C.bold}[test-gen]${C.reset} Analyzing ${files.length} file(s)...`);
53
+ const gaps = findCoverageGaps(files);
54
+
55
+ if (gaps.length === 0) {
56
+ console.log(`${C.green}[test-gen] No coverage gaps found${C.reset}`);
57
+ return 0;
58
+ }
59
+
60
+ for (const gap of gaps) {
61
+ const rel = path.relative(cwd, gap.file);
62
+ const covered = gap.exports.length;
63
+ console.log(` ${C.cyan}${rel}${C.reset} ${covered} uncovered export(s): ${gap.exports.join(', ')}`);
64
+ }
65
+
66
+ if (options.dryRun) {
67
+ console.log(`\n${C.yellow}[test-gen] Dry run — not generating tests${C.reset}`);
68
+ return 0;
69
+ }
70
+
71
+ // Load review engine for generation
72
+ const engineRef = (config as { reviewEngine?: unknown }).reviewEngine ?? 'auto';
73
+ let engine: Awaited<ReturnType<typeof loadAdapter>>;
74
+ try {
75
+ engine = await loadAdapter({ point: 'review-engine', ref: engineRef as string });
76
+ } catch (err) {
77
+ console.error(`${C.red}[test-gen] Could not load review engine: ${err}${C.reset}`);
78
+ return 1;
79
+ }
80
+
81
+ const framework = detectTestFramework(cwd);
82
+ const written: string[] = [];
83
+
84
+ for (const gap of gaps) {
85
+ let sourceContent: string;
86
+ try { sourceContent = fs.readFileSync(gap.file, 'utf8'); } catch { continue; }
87
+
88
+ const prompt = buildGenerationPrompt(gap, sourceContent, framework);
89
+
90
+ process.stdout.write(` Generating ${path.relative(cwd, gap.testFile)}... `);
91
+ try {
92
+ const result = await (engine as unknown as ReviewEngine).review({ content: prompt, kind: 'spec', context: { cwd } });
93
+
94
+ // Extract code block if wrapped in markdown
95
+ let code = result.rawOutput.trim();
96
+ const fenceMatch = code.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
97
+ if (fenceMatch) code = fenceMatch[1]!.trim();
98
+
99
+ const testPath = writeGeneratedTest(gap, code);
100
+ written.push(testPath);
101
+ console.log(`${C.green}done${C.reset}`);
102
+
103
+ // Verify mode
104
+ if (options.verify && config.testCommand) {
105
+ try {
106
+ const [cmd, ...cmdArgs] = config.testCommand.split(/\s+/);
107
+ execFileSync(cmd!, cmdArgs, { cwd, stdio: 'ignore', timeout: 60_000 });
108
+ } catch {
109
+ fs.unlinkSync(testPath);
110
+ written.pop();
111
+ console.log(` ${C.yellow} ↳ tests failed — reverted${C.reset}`);
112
+ }
113
+ }
114
+ } catch (err) {
115
+ console.log(`${C.red}failed: ${err}${C.reset}`);
116
+ }
117
+ }
118
+
119
+ if (written.length > 0) {
120
+ console.log(`\n${C.green}[test-gen] Generated ${written.length} test file(s):${C.reset}`);
121
+ for (const f of written) console.log(` ${path.relative(cwd, f)}`);
122
+ }
123
+
124
+ return 0;
125
+ }