@delegance/claude-autopilot 2.5.0 → 5.0.0-alpha.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 (129) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +169 -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 +23 -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 +2 -2
  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 +173 -111
  36. package/src/cli/hook.ts +72 -27
  37. package/src/cli/ignore-helper.ts +116 -0
  38. package/src/cli/index.ts +355 -31
  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 +109 -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 +10 -10
  58. package/src/core/config/preset-resolver.ts +6 -6
  59. package/src/core/config/schema.ts +103 -2
  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
@@ -1,8 +1,9 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
- import { AutopilotError } from '../../core/errors.ts';
2
+ import { GuardrailError } from '../../core/errors.ts';
3
3
  import type { Capabilities } from '../base.ts';
4
4
  import type { ReviewEngine, ReviewInput, ReviewOutput } from './types.ts';
5
5
  import { parseReviewOutput } from './parse-output.ts';
6
+ import { buildSystemPrompt, classifyError } from './prompt-builder.ts';
6
7
 
7
8
  const DEFAULT_MODEL = 'claude-opus-4-7';
8
9
  const MAX_OUTPUT_TOKENS = 4096;
@@ -14,7 +15,7 @@ const COST_PER_M_OUTPUT = 75.0;
14
15
  const SYSTEM_PROMPT_TEMPLATE = `You are a senior software architect reviewing code changes for quality, security, and correctness.
15
16
 
16
17
  The codebase context:
17
- {STACK}{GIT_CONTEXT}
18
+ {STACK}{GIT_CONTEXT}{DESIGN_SCHEMA}
18
19
 
19
20
  Provide structured feedback in exactly this format:
20
21
 
@@ -51,13 +52,11 @@ export const claudeAdapter: ReviewEngine = {
51
52
  async review(input: ReviewInput): Promise<ReviewOutput> {
52
53
  const apiKey = process.env.ANTHROPIC_API_KEY;
53
54
  if (!apiKey) {
54
- throw new AutopilotError('ANTHROPIC_API_KEY not set', { code: 'auth', provider: 'claude' });
55
+ throw new GuardrailError('ANTHROPIC_API_KEY not set', { code: 'auth', provider: 'claude' });
55
56
  }
56
57
 
57
58
  const model = (input.context as Record<string, unknown> | undefined)?.['model'] as string | undefined ?? DEFAULT_MODEL;
58
- const stack = input.context?.stack ?? 'A web application — stack details unspecified.';
59
- const gitCtx = input.context?.gitSummary ? `\n\nChange context: ${input.context.gitSummary}` : '';
60
- const systemPrompt = SYSTEM_PROMPT_TEMPLATE.replace('{STACK}', stack).replace('{GIT_CONTEXT}', gitCtx);
59
+ const systemPrompt = buildSystemPrompt(input, SYSTEM_PROMPT_TEMPLATE);
61
60
 
62
61
  const client = new Anthropic({ apiKey });
63
62
  let response: Anthropic.Message;
@@ -70,12 +69,11 @@ export const claudeAdapter: ReviewEngine = {
70
69
  });
71
70
  } catch (err) {
72
71
  const message = err instanceof Error ? err.message : String(err);
73
- const isRateLimit = /rate.limit|429|overloaded/i.test(message);
74
- const isAuth = /unauthorized|401|invalid.api.key|authentication/i.test(message);
75
- throw new AutopilotError(`Claude review call failed: ${message}`, {
76
- code: isAuth ? 'auth' : isRateLimit ? 'rate_limit' : 'transient_network',
72
+ const code = classifyError(message);
73
+ throw new GuardrailError(`Claude review call failed: ${message}`, {
74
+ code,
77
75
  provider: 'claude',
78
- retryable: isRateLimit,
76
+ retryable: code === 'rate_limit',
79
77
  });
80
78
  }
81
79
 
@@ -1,8 +1,9 @@
1
1
  import OpenAI from 'openai';
2
2
  import { parseReviewOutput } from './parse-output.ts';
3
- import { AutopilotError } from '../../core/errors.ts';
3
+ import { GuardrailError } from '../../core/errors.ts';
4
4
  import type { Capabilities } from '../base.ts';
5
5
  import type { ReviewEngine, ReviewInput, ReviewOutput } from './types.ts';
6
+ import { buildSystemPrompt, classifyError } from './prompt-builder.ts';
6
7
 
7
8
  const DEFAULT_MODEL = process.env.CODEX_MODEL ?? 'gpt-5.3-codex';
8
9
  const MAX_OUTPUT_TOKENS = 4096;
@@ -10,7 +11,7 @@ const MAX_OUTPUT_TOKENS = 4096;
10
11
  const SYSTEM_PROMPT_TEMPLATE = `You are a senior software architect providing feedback on designs, proposals, and ideas.
11
12
 
12
13
  The codebase context:
13
- {STACK}{GIT_CONTEXT}
14
+ {STACK}{GIT_CONTEXT}{DESIGN_SCHEMA}
14
15
 
15
16
  Provide structured feedback in exactly this format:
16
17
 
@@ -46,11 +47,9 @@ export const codexAdapter: ReviewEngine = {
46
47
  async review(input: ReviewInput): Promise<ReviewOutput> {
47
48
  const apiKey = process.env.OPENAI_API_KEY;
48
49
  if (!apiKey) {
49
- throw new AutopilotError('OPENAI_API_KEY not set', { code: 'auth', provider: 'codex' });
50
+ throw new GuardrailError('OPENAI_API_KEY not set', { code: 'auth', provider: 'codex' });
50
51
  }
51
- const stack = input.context?.stack ?? 'A web application — stack details unspecified.';
52
- const gitCtx = input.context?.gitSummary ? `\n\nChange context: ${input.context.gitSummary}` : '';
53
- const systemPrompt = SYSTEM_PROMPT_TEMPLATE.replace('{STACK}', stack).replace('{GIT_CONTEXT}', gitCtx);
52
+ const systemPrompt = buildSystemPrompt(input, SYSTEM_PROMPT_TEMPLATE);
54
53
 
55
54
  const client = new OpenAI({ apiKey });
56
55
  let response;
@@ -63,12 +62,11 @@ export const codexAdapter: ReviewEngine = {
63
62
  });
64
63
  } catch (err) {
65
64
  const message = err instanceof Error ? err.message : String(err);
66
- const isRateLimit = /rate.limit|429/i.test(message);
67
- const isAuth = /unauthorized|401|invalid.api.key/i.test(message);
68
- throw new AutopilotError(`Codex review call failed: ${message}`, {
69
- code: isAuth ? 'auth' : isRateLimit ? 'rate_limit' : 'transient_network',
65
+ const code = classifyError(message);
66
+ throw new GuardrailError(`Codex review call failed: ${message}`, {
67
+ code,
70
68
  provider: 'codex',
71
- retryable: isRateLimit,
69
+ retryable: code === 'rate_limit',
72
70
  });
73
71
  }
74
72
 
@@ -1,8 +1,9 @@
1
1
  import { GoogleGenerativeAI } from '@google/generative-ai';
2
2
  import { parseReviewOutput } from './parse-output.ts';
3
- import { AutopilotError } from '../../core/errors.ts';
3
+ import { GuardrailError } from '../../core/errors.ts';
4
4
  import type { Capabilities } from '../base.ts';
5
5
  import type { ReviewEngine, ReviewInput, ReviewOutput } from './types.ts';
6
+ import { buildSystemPrompt, classifyError } from './prompt-builder.ts';
6
7
 
7
8
  const DEFAULT_MODEL = 'gemini-2.5-pro-preview-05-06';
8
9
  const MAX_OUTPUT_TOKENS = 4096;
@@ -14,7 +15,7 @@ const COST_PER_M_OUTPUT = 10.0;
14
15
  const PROMPT_TEMPLATE = `You are a senior software architect reviewing code changes for quality, security, and correctness.
15
16
 
16
17
  The codebase context:
17
- {STACK}{GIT_CONTEXT}
18
+ {STACK}{GIT_CONTEXT}{DESIGN_SCHEMA}
18
19
 
19
20
  Please review the following:
20
21
 
@@ -59,13 +60,11 @@ export const geminiAdapter: ReviewEngine = {
59
60
  async review(input: ReviewInput): Promise<ReviewOutput> {
60
61
  const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
61
62
  if (!apiKey) {
62
- throw new AutopilotError('GEMINI_API_KEY (or GOOGLE_API_KEY) not set', { code: 'auth', provider: 'gemini' });
63
+ throw new GuardrailError('GEMINI_API_KEY (or GOOGLE_API_KEY) not set', { code: 'auth', provider: 'gemini' });
63
64
  }
64
65
 
65
66
  const model = (input.context as Record<string, unknown> | undefined)?.['model'] as string | undefined ?? DEFAULT_MODEL;
66
- const stack = input.context?.stack ?? 'A web application — stack details unspecified.';
67
- const gitCtx = input.context?.gitSummary ? `\n\nChange context: ${input.context.gitSummary}` : '';
68
- const prompt = PROMPT_TEMPLATE.replace('{STACK}', stack).replace('{GIT_CONTEXT}', gitCtx).replace('{CONTENT}', input.content);
67
+ const prompt = buildSystemPrompt(input, PROMPT_TEMPLATE).replace('{CONTENT}', input.content);
69
68
 
70
69
  const genAI = new GoogleGenerativeAI(apiKey);
71
70
  const genModel = genAI.getGenerativeModel({
@@ -78,12 +77,11 @@ export const geminiAdapter: ReviewEngine = {
78
77
  result = await genModel.generateContent(prompt);
79
78
  } catch (err) {
80
79
  const message = err instanceof Error ? err.message : String(err);
81
- const isRateLimit = /rate.limit|429|quota/i.test(message);
82
- const isAuth = /api.key|unauthorized|403/i.test(message);
83
- throw new AutopilotError(`Gemini review call failed: ${message}`, {
84
- code: isAuth ? 'auth' : isRateLimit ? 'rate_limit' : 'transient_network',
80
+ const code = classifyError(message);
81
+ throw new GuardrailError(`Gemini review call failed: ${message}`, {
82
+ code,
85
83
  provider: 'gemini',
86
- retryable: isRateLimit,
84
+ retryable: code === 'rate_limit',
87
85
  });
88
86
  }
89
87
 
@@ -1,15 +1,16 @@
1
1
  import OpenAI from 'openai';
2
2
  import { parseReviewOutput } from './parse-output.ts';
3
- import { AutopilotError } from '../../core/errors.ts';
3
+ import { GuardrailError } from '../../core/errors.ts';
4
4
  import type { Capabilities } from '../base.ts';
5
5
  import type { ReviewEngine, ReviewInput, ReviewOutput } from './types.ts';
6
+ import { buildSystemPrompt, classifyError } from './prompt-builder.ts';
6
7
 
7
8
  const MAX_OUTPUT_TOKENS = 4096;
8
9
 
9
10
  const SYSTEM_PROMPT_TEMPLATE = `You are a senior software architect reviewing code changes for quality, security, and correctness.
10
11
 
11
12
  The codebase context:
12
- {STACK}{GIT_CONTEXT}
13
+ {STACK}{GIT_CONTEXT}{DESIGN_SCHEMA}
13
14
 
14
15
  Provide structured feedback in exactly this format:
15
16
 
@@ -56,15 +57,13 @@ export const openaiCompatibleAdapter: ReviewEngine = {
56
57
 
57
58
  const model = opts['model'] as string | undefined;
58
59
  if (!model) {
59
- throw new AutopilotError(
60
- 'openai-compatible adapter requires options.model to be set in autopilot.config.yaml',
60
+ throw new GuardrailError(
61
+ 'openai-compatible adapter requires options.model to be set in guardrail.config.yaml',
61
62
  { code: 'invalid_config', provider: 'openai-compatible' },
62
63
  );
63
64
  }
64
65
 
65
- const stack = input.context?.stack ?? 'A web application — stack details unspecified.';
66
- const gitCtx = input.context?.gitSummary ? `\n\nChange context: ${input.context.gitSummary}` : '';
67
- const systemPrompt = SYSTEM_PROMPT_TEMPLATE.replace('{STACK}', stack).replace('{GIT_CONTEXT}', gitCtx);
66
+ const systemPrompt = buildSystemPrompt(input, SYSTEM_PROMPT_TEMPLATE);
68
67
  const client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
69
68
 
70
69
  let response: OpenAI.Chat.ChatCompletion;
@@ -79,12 +78,11 @@ export const openaiCompatibleAdapter: ReviewEngine = {
79
78
  });
80
79
  } catch (err) {
81
80
  const message = err instanceof Error ? err.message : String(err);
82
- const isRateLimit = /rate.limit|429/i.test(message);
83
- const isAuth = /unauthorized|401|invalid.api.key/i.test(message);
84
- throw new AutopilotError(`openai-compatible review call failed: ${message}`, {
85
- code: isAuth ? 'auth' : isRateLimit ? 'rate_limit' : 'transient_network',
81
+ const code = classifyError(message);
82
+ throw new GuardrailError(`openai-compatible review call failed: ${message}`, {
83
+ code,
86
84
  provider: 'openai-compatible',
87
- retryable: isRateLimit,
85
+ retryable: code === 'rate_limit',
88
86
  });
89
87
  }
90
88
 
@@ -15,16 +15,29 @@ function extractFileRef(text: string): { file: string; line?: number } {
15
15
  return { file: raw, line };
16
16
  }
17
17
 
18
+ // Accepts any of: `### [CRITICAL] title`, `### CRITICAL title`, `### **CRITICAL** title`,
19
+ // `### **[CRITICAL]** title`. Severity capture works across variants.
20
+ const FINDING_REGEX =
21
+ /### (?:\*\*)?\[?(CRITICAL|WARNING|NOTE)\]?(?:\*\*)?\s*(.+?)(?=\n### (?:\*\*)?\[?(?:CRITICAL|WARNING|NOTE)\]?|## Review Summary|$)/gs;
22
+
23
+ // "Substantive" output = enough non-whitespace chars to be a real LLM response, not
24
+ // an empty/placeholder string. Anything past this with zero parsed findings is likely
25
+ // format drift we should warn about.
26
+ const NONTRIVIAL_OUTPUT_THRESHOLD = 40;
27
+
18
28
  /**
19
- * Parses the structured [CRITICAL|WARNING|NOTE] markdown format
20
- * produced by all review engine adapters. Extracts file:line references
21
- * from the finding body when present.
29
+ * Parses the structured CRITICAL|WARNING|NOTE markdown format produced by all review
30
+ * engine adapters. Extracts file:line references from the finding body when present.
31
+ *
32
+ * Tolerates common LLM format drift (missing brackets, bold wrappers) because the prompt
33
+ * alone doesn't guarantee literal `### [CRITICAL]` — models routinely emit
34
+ * `### CRITICAL` or `### **CRITICAL**`. A strict parser silently returns zero findings
35
+ * on otherwise-valid output, which is exactly the silent-failure mode this file exists to
36
+ * prevent.
22
37
  */
23
38
  export function parseReviewOutput(output: string, idPrefix: string): Finding[] {
24
39
  const findings: Finding[] = [];
25
- const regex = /### \[(CRITICAL|WARNING|NOTE)\]\s*(.+?)(?=\n### \[|## Review Summary|$)/gs;
26
- let match: RegExpExecArray | null;
27
- while ((match = regex.exec(output)) !== null) {
40
+ for (const match of output.matchAll(FINDING_REGEX)) {
28
41
  const severity = match[1]!.toLowerCase() as Finding['severity'];
29
42
  const body = match[2]!.trim();
30
43
  const titleEnd = body.indexOf('\n');
@@ -44,5 +57,18 @@ export function parseReviewOutput(output: string, idPrefix: string): Finding[] {
44
57
  createdAt: new Date().toISOString(),
45
58
  });
46
59
  }
60
+
61
+ if (findings.length === 0) {
62
+ const nonWhitespace = output.replace(/\s/g, '').length;
63
+ if (nonWhitespace >= NONTRIVIAL_OUTPUT_THRESHOLD) {
64
+ const preview = output.slice(0, 200).replace(/\s+/g, ' ').trim();
65
+ // eslint-disable-next-line no-console
66
+ console.warn(
67
+ `[parseReviewOutput] LLM returned ${output.length} chars but no findings parsed. ` +
68
+ `Expected '### [CRITICAL|WARNING|NOTE] …'. Preview: ${preview}${output.length > 200 ? '…' : ''}`,
69
+ );
70
+ }
71
+ }
72
+
47
73
  return findings;
48
74
  }
@@ -0,0 +1,19 @@
1
+ import type { ReviewInput } from './types.ts';
2
+
3
+ const DEFAULT_STACK = 'A web application — stack details unspecified.';
4
+
5
+ export function buildSystemPrompt(input: ReviewInput, template: string): string {
6
+ const stack = input.context?.stack ?? DEFAULT_STACK;
7
+ const gitCtx = input.context?.gitSummary ? `\n\nChange context: ${input.context.gitSummary}` : '';
8
+ const designBlock = input.context?.designSchema ? `\n\n${input.context.designSchema}` : '';
9
+ return template
10
+ .replace('{STACK}', stack)
11
+ .replace('{GIT_CONTEXT}', gitCtx)
12
+ .replace('{DESIGN_SCHEMA}', designBlock);
13
+ }
14
+
15
+ export function classifyError(message: string): 'auth' | 'rate_limit' | 'transient_network' {
16
+ if (/unauthorized|401|invalid.api.key|authentication|api.key|403/i.test(message)) return 'auth';
17
+ if (/rate.limit|429|overloaded|quota/i.test(message)) return 'rate_limit';
18
+ return 'transient_network';
19
+ }
@@ -4,7 +4,7 @@ import type { Finding } from '../../core/findings/types.ts';
4
4
  export interface ReviewInput {
5
5
  content: string;
6
6
  kind: 'spec' | 'pr-diff' | 'file-batch';
7
- context?: { spec?: string; plan?: string; stack?: string; cwd?: string; gitSummary?: string };
7
+ context?: { spec?: string; plan?: string; stack?: string; cwd?: string; gitSummary?: string; designSchema?: string };
8
8
  }
9
9
 
10
10
  export interface ReviewOutput {
@@ -0,0 +1,39 @@
1
+ import { runSafe } from '../../core/shell.ts';
2
+
3
+ export type CommitState = 'pending' | 'success' | 'failure' | 'error';
4
+
5
+ export interface CommitStatusOptions {
6
+ sha: string;
7
+ state: CommitState;
8
+ description?: string;
9
+ context?: string;
10
+ targetUrl?: string;
11
+ cwd?: string;
12
+ }
13
+
14
+ function getCurrentSha(cwd: string): string | null {
15
+ return runSafe('git', ['rev-parse', 'HEAD'], { cwd })?.trim() ?? null;
16
+ }
17
+
18
+ export function resolveCommitSha(cwd: string, envSha?: string): string | null {
19
+ return envSha
20
+ ?? process.env.GITHUB_SHA
21
+ ?? getCurrentSha(cwd);
22
+ }
23
+
24
+ export function postCommitStatus(opts: CommitStatusOptions): boolean {
25
+ const payload = JSON.stringify({
26
+ state: opts.state,
27
+ description: (opts.description ?? '').slice(0, 140),
28
+ context: opts.context ?? 'guardrail',
29
+ ...(opts.targetUrl ? { target_url: opts.targetUrl } : {}),
30
+ });
31
+
32
+ const result = runSafe('gh', [
33
+ 'api', `repos/{owner}/{repo}/statuses/${opts.sha}`,
34
+ '--method', 'POST',
35
+ '--input', '-',
36
+ ], { cwd: opts.cwd, input: payload });
37
+
38
+ return result !== null;
39
+ }
@@ -1,5 +1,5 @@
1
1
  import { runSafe, runThrowing } from '../../core/shell.ts';
2
- import { AutopilotError } from '../../core/errors.ts';
2
+ import { GuardrailError } from '../../core/errors.ts';
3
3
  import type { Capabilities } from '../base.ts';
4
4
  import type { VcsHost, GenericComment, PrMetadata, CreatePrOptions, CreatePrResult } from './types.ts';
5
5
 
@@ -13,7 +13,7 @@ export const githubAdapter: VcsHost = {
13
13
 
14
14
  async getPrDiff(pr: number | string): Promise<string> {
15
15
  const result = runSafe('gh', ['pr', 'diff', String(pr)]);
16
- if (result === null) throw new AutopilotError(`Failed to get diff for PR ${pr}`, { code: 'transient_network' });
16
+ if (result === null) throw new GuardrailError(`Failed to get diff for PR ${pr}`, { code: 'transient_network' });
17
17
  return result;
18
18
  },
19
19
 
@@ -0,0 +1,125 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import {
4
+ loadBaseline, saveBaseline, clearBaseline, diffAgainstBaseline,
5
+ baselineFilePath,
6
+ } from '../core/persist/baseline.ts';
7
+ import { loadCachedFindings } from '../core/persist/findings-cache.ts';
8
+
9
+ const C = {
10
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
11
+ green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m',
12
+ };
13
+ const fmt = (c: keyof typeof C, t: string) => `${C[c]}${t}${C.reset}`;
14
+
15
+ export interface BaselineCommandOptions {
16
+ cwd?: string;
17
+ note?: string;
18
+ baselinePath?: string;
19
+ }
20
+
21
+ export async function runBaseline(sub: string, options: BaselineCommandOptions = {}): Promise<number> {
22
+ const cwd = options.cwd ?? process.cwd();
23
+ const bPath = baselineFilePath(cwd, options.baselinePath);
24
+ const relPath = path.relative(cwd, bPath);
25
+
26
+ switch (sub) {
27
+ case 'create': {
28
+ if (fs.existsSync(bPath)) {
29
+ console.log(fmt('yellow', `[baseline] ${relPath} already exists — use \`guardrail baseline update\` to refresh, or \`guardrail baseline clear\` to reset`));
30
+ return 1;
31
+ }
32
+ return createOrUpdate(cwd, bPath, relPath, options.note, 'Created');
33
+ }
34
+
35
+ case 'update': {
36
+ return createOrUpdate(cwd, bPath, relPath, options.note, 'Updated');
37
+ }
38
+
39
+ case 'show': {
40
+ const baseline = loadBaseline(cwd, options.baselinePath);
41
+ if (!baseline) {
42
+ console.log(fmt('yellow', `[baseline] No baseline found at ${relPath}`));
43
+ console.log(fmt('dim', ' Run: guardrail baseline create'));
44
+ return 0;
45
+ }
46
+ console.log(`\n${fmt('bold', '[guardrail baseline]')} ${fmt('dim', relPath)}`);
47
+ console.log(fmt('dim', ` Created: ${baseline.createdAt} Updated: ${baseline.updatedAt}`));
48
+ if (baseline.note) console.log(fmt('dim', ` Note: ${baseline.note}`));
49
+ console.log(` ${baseline.entries.length} pinned finding${baseline.entries.length !== 1 ? 's' : ''}\n`);
50
+ for (const e of baseline.entries) {
51
+ const sev = e.severity === 'critical' ? fmt('red', 'CRIT') : e.severity === 'warning' ? fmt('yellow', 'WARN') : fmt('dim', 'NOTE');
52
+ console.log(` [${sev}] ${fmt('dim', `${e.file}${e.line ? `:${e.line}` : ''}`)} ${e.message.slice(0, 70)}`);
53
+ }
54
+ console.log('');
55
+ return 0;
56
+ }
57
+
58
+ case 'diff': {
59
+ const baseline = loadBaseline(cwd, options.baselinePath);
60
+ if (!baseline) {
61
+ console.log(fmt('yellow', `[baseline] No baseline found — run: guardrail baseline create`));
62
+ return 1;
63
+ }
64
+ const current = loadCachedFindings(cwd);
65
+ if (current.length === 0) {
66
+ console.log(fmt('yellow', '[baseline] No cached findings — run `guardrail run` or `guardrail scan` first'));
67
+ return 1;
68
+ }
69
+ const diff = diffAgainstBaseline(current, baseline);
70
+ console.log(`\n${fmt('bold', '[guardrail baseline diff]')} vs ${fmt('dim', relPath)}\n`);
71
+
72
+ if (diff.added.length > 0) {
73
+ console.log(fmt('red', ` ${diff.added.length} new finding${diff.added.length !== 1 ? 's' : ''} (not in baseline):`));
74
+ for (const f of diff.added) {
75
+ const sev = f.severity === 'critical' ? fmt('red', 'CRIT') : f.severity === 'warning' ? fmt('yellow', 'WARN') : fmt('dim', 'NOTE');
76
+ console.log(` [${sev}] ${fmt('dim', `${f.file}${f.line ? `:${f.line}` : ''}`)} ${f.message.slice(0, 70)}`);
77
+ }
78
+ console.log('');
79
+ }
80
+ if (diff.resolved.length > 0) {
81
+ console.log(fmt('green', ` ${diff.resolved.length} resolved (in baseline but not in current):`));
82
+ for (const e of diff.resolved) {
83
+ console.log(` ${fmt('dim', `${e.file}${e.line ? `:${e.line}` : ''}`)} ${e.message.slice(0, 70)}`);
84
+ }
85
+ console.log('');
86
+ }
87
+ if (diff.added.length === 0 && diff.resolved.length === 0) {
88
+ console.log(fmt('green', ` ✓ No changes vs baseline (${diff.unchanged.length} pinned findings unchanged)\n`));
89
+ } else {
90
+ console.log(fmt('dim', ` ${diff.unchanged.length} unchanged · run \`guardrail baseline update\` to pin new state\n`));
91
+ }
92
+ return diff.added.some(f => f.severity === 'critical') ? 1 : 0;
93
+ }
94
+
95
+ case 'clear': {
96
+ if (!fs.existsSync(bPath)) {
97
+ console.log(fmt('dim', `[baseline] No baseline at ${relPath} — nothing to clear`));
98
+ return 0;
99
+ }
100
+ clearBaseline(cwd, options.baselinePath);
101
+ console.log(fmt('green', `[baseline] Cleared ${relPath}`));
102
+ return 0;
103
+ }
104
+
105
+ default:
106
+ console.error(fmt('red', `[baseline] Unknown subcommand: "${sub}"`));
107
+ console.error(fmt('dim', ' Usage: guardrail baseline <create|update|show|diff|clear> [--note "..."]'));
108
+ return 1;
109
+ }
110
+ }
111
+
112
+ function createOrUpdate(cwd: string, bPath: string, relPath: string, note: string | undefined, verb: string): number {
113
+ const findings = loadCachedFindings(cwd);
114
+ if (findings.length === 0) {
115
+ console.log(fmt('yellow', '[baseline] No cached findings to snapshot — run `guardrail run` or `guardrail scan` first'));
116
+ return 1;
117
+ }
118
+ const baseline = saveBaseline(cwd, findings, { note, overridePath: bPath === path.join(cwd, '.guardrail-baseline.json') ? undefined : bPath });
119
+ console.log(`\n${fmt('green', `[baseline] ${verb}`)} ${fmt('dim', relPath)}`);
120
+ console.log(` ${baseline.entries.length} finding${baseline.entries.length !== 1 ? 's' : ''} pinned as accepted baseline`);
121
+ if (note) console.log(` Note: ${note}`);
122
+ console.log(fmt('dim', `\n Commit this file to share the baseline with your team:`));
123
+ console.log(fmt('cyan', ` git add ${relPath} && git commit -m "chore: update guardrail baseline"\n`));
124
+ return 0;
125
+ }
package/src/cli/ci.ts CHANGED
@@ -8,18 +8,19 @@ export interface CiCommandOptions {
8
8
  sarifOutput?: string;
9
9
  diff?: boolean;
10
10
  inlineComments?: boolean;
11
+ newOnly?: boolean;
12
+ failOn?: 'critical' | 'warning' | 'note' | 'none';
11
13
  }
12
14
 
13
15
  /**
14
- * `autopilot ci` — opinionated single-command CI entrypoint.
15
- *
16
- * Equivalent to:
17
- * autopilot run --base <ref> --post-comments --format sarif --output <path>
16
+ * `guardrail ci` — opinionated single-command CI entrypoint.
18
17
  *
19
18
  * Defaults:
20
- * base GITHUB_BASE_REF → HEAD~1
21
- * output autopilot.sarif
22
- * post-comments true (skip if no PR detected — run.ts handles gracefully)
19
+ * base GITHUB_BASE_REF → HEAD~1
20
+ * output guardrail.sarif
21
+ * post-comments true
22
+ * fail-on critical (or policy.failOn from config)
23
+ * new-only false (or policy.newOnly from config)
23
24
  */
24
25
  export async function runCi(options: CiCommandOptions = {}): Promise<number> {
25
26
  const base = options.base
@@ -27,7 +28,7 @@ export async function runCi(options: CiCommandOptions = {}): Promise<number> {
27
28
  ?? process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME // GitLab
28
29
  ?? 'HEAD~1';
29
30
 
30
- const sarifOutput = options.sarifOutput ?? 'autopilot.sarif';
31
+ const sarifOutput = options.sarifOutput ?? 'guardrail.sarif';
31
32
 
32
33
  return runCommand({
33
34
  cwd: options.cwd,
@@ -38,5 +39,7 @@ export async function runCi(options: CiCommandOptions = {}): Promise<number> {
38
39
  outputPath: sarifOutput,
39
40
  diff: options.diff,
40
41
  inlineComments: options.inlineComments ?? true,
42
+ newOnly: options.newOnly,
43
+ failOn: options.failOn,
41
44
  });
42
45
  }
package/src/cli/costs.ts CHANGED
@@ -26,7 +26,7 @@ export async function runCosts(cwd = process.cwd()): Promise<number> {
26
26
  const log = readCostLog(cwd);
27
27
 
28
28
  if (log.length === 0) {
29
- console.log(fmt('yellow', '[costs] No run history found — run `autopilot run` first.'));
29
+ console.log(fmt('yellow', '[costs] No run history found — run `guardrail run` first.'));
30
30
  return 0;
31
31
  }
32
32
 
@@ -40,7 +40,7 @@ export async function runCosts(cwd = process.cwd()): Promise<number> {
40
40
  const totalOutput = log.reduce((s, e) => s + e.outputTokens, 0);
41
41
  const recentCost = recent.reduce((s, e) => s + e.costUSD, 0);
42
42
 
43
- console.log(`\n${fmt('bold', '[autopilot costs]')}\n`);
43
+ console.log(`\n${fmt('bold', '[guardrail costs]')}\n`);
44
44
 
45
45
  // Summary row
46
46
  console.log(fmt('bold', 'Summary'));
@@ -0,0 +1,96 @@
1
+ // src/cli/council.ts
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { loadConfig } from '../core/config/loader.ts';
5
+ import { parseCouncilConfig } from '../core/council/config.ts';
6
+ import { runCouncil } from '../core/council/runner.ts';
7
+ import { makeClaudeCouncilAdapter } from '../adapters/council/claude.ts';
8
+ import { makeOpenAICouncilAdapter } from '../adapters/council/openai.ts';
9
+ import type { CouncilAdapter } from '../adapters/council/types.ts';
10
+ import type { CouncilModelEntry } from '../core/council/types.ts';
11
+ import { GuardrailError } from '../core/errors.ts';
12
+
13
+ function makeAdapter(entry: CouncilModelEntry): CouncilAdapter {
14
+ switch (entry.adapter) {
15
+ case 'claude': return makeClaudeCouncilAdapter(entry.model, entry.label);
16
+ case 'openai': return makeOpenAICouncilAdapter(entry.model, entry.label);
17
+ }
18
+ }
19
+
20
+ export async function runCouncilCmd(opts: {
21
+ prompt?: string;
22
+ contextFile?: string;
23
+ configPath?: string;
24
+ dryRun?: boolean;
25
+ noSynthesize?: boolean;
26
+ }): Promise<number> {
27
+ const cwd = process.cwd();
28
+ const configPath = opts.configPath ?? path.join(cwd, 'guardrail.config.yaml');
29
+
30
+ let config;
31
+ try {
32
+ config = await loadConfig(configPath);
33
+ } catch (err) {
34
+ console.error(err instanceof GuardrailError ? err.message : String(err));
35
+ return 1;
36
+ }
37
+
38
+ if (!config.council) {
39
+ console.error('[council] No "council" section in guardrail.config.yaml — add council.models and council.synthesizer');
40
+ return 1;
41
+ }
42
+
43
+ let councilConfig;
44
+ try {
45
+ councilConfig = parseCouncilConfig(config.council as Record<string, unknown>);
46
+ } catch (err) {
47
+ console.error(err instanceof GuardrailError ? err.message : String(err));
48
+ return 1;
49
+ }
50
+
51
+ if (opts.dryRun) {
52
+ process.stdout.write(JSON.stringify({ schema_version: 1, status: 'dry_run', config: councilConfig }, null, 2) + '\n');
53
+ return 0;
54
+ }
55
+
56
+ if (!opts.prompt) {
57
+ console.error('[council] --prompt is required');
58
+ return 1;
59
+ }
60
+ if (!opts.contextFile) {
61
+ console.error('[council] --context-file is required');
62
+ return 1;
63
+ }
64
+
65
+ let contextDoc: string;
66
+ try {
67
+ contextDoc = fs.readFileSync(opts.contextFile, 'utf8');
68
+ } catch {
69
+ console.error(`[council] Cannot read context file: ${opts.contextFile}`);
70
+ return 1;
71
+ }
72
+
73
+ const adapters = councilConfig.models.map(makeAdapter);
74
+ const synthesizer = opts.noSynthesize
75
+ ? { label: 'none', consult: async () => '' } as CouncilAdapter
76
+ : makeAdapter(councilConfig.synthesizer);
77
+
78
+ const result = await runCouncil(
79
+ councilConfig,
80
+ adapters,
81
+ synthesizer,
82
+ opts.prompt,
83
+ contextDoc,
84
+ );
85
+
86
+ // When no-synthesize, clear the empty synthesis object
87
+ if (opts.noSynthesize && result.synthesis?.text === '') {
88
+ delete (result as unknown as Record<string, unknown>)['synthesis'];
89
+ }
90
+
91
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
92
+
93
+ if (result.status === 'failed') return 2;
94
+ if (result.status === 'partial') return 1;
95
+ return 0;
96
+ }