@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
@@ -0,0 +1,65 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as path from 'node:path';
3
+ import { resolveWorkspace } from '../workspace.ts';
4
+ import { saveRun, checksumFile, pruneOldRuns } from '../run-store.ts';
5
+ import { runGuardrail } from '../../pipeline/run.ts';
6
+ import { resolveGitTouchedFiles } from '../../git/touched-files.ts';
7
+ import { loadRulesFromConfig } from '../../static-rules/registry.ts';
8
+ import { detectGitContext } from '../../detect/git-context.ts';
9
+ import type { ReviewEngine } from '../../../adapters/review-engine/types.ts';
10
+ import type { GuardrailConfig } from '../../config/types.ts';
11
+ import type { Finding } from '../../findings/types.ts';
12
+
13
+ export interface ReviewDiffResult {
14
+ schema_version: 1;
15
+ run_id: string;
16
+ findings: Finding[];
17
+ human_summary: string;
18
+ usage?: { costUSD?: number };
19
+ }
20
+
21
+ export async function handleReviewDiff(
22
+ input: { base?: string; cwd?: string; static_only?: boolean },
23
+ config: GuardrailConfig,
24
+ engine: ReviewEngine,
25
+ ): Promise<ReviewDiffResult> {
26
+ const workspace = resolveWorkspace(input.cwd);
27
+ pruneOldRuns(workspace, 24 * 60 * 60 * 1000);
28
+
29
+ const touchedFiles = resolveGitTouchedFiles({ cwd: workspace, base: input.base });
30
+ const staticRules = config.staticRules ? await loadRulesFromConfig(config.staticRules) : [];
31
+ const gitCtx = detectGitContext(workspace);
32
+
33
+ const result = await runGuardrail({
34
+ touchedFiles,
35
+ config,
36
+ reviewEngine: engine,
37
+ staticRules,
38
+ cwd: workspace,
39
+ gitSummary: gitCtx.summary ?? undefined,
40
+ base: input.base,
41
+ skipReview: input.static_only ?? false,
42
+ });
43
+
44
+ const run_id = crypto.randomUUID();
45
+ const fileChecksums: Record<string, string> = {};
46
+ for (const f of touchedFiles) {
47
+ const abs = path.isAbsolute(f) ? f : path.resolve(workspace, f);
48
+ fileChecksums[f] = checksumFile(abs);
49
+ }
50
+ saveRun(workspace, run_id, result.allFindings, fileChecksums);
51
+
52
+ const critCount = result.allFindings.filter(f => f.severity === 'critical').length;
53
+ const warnCount = result.allFindings.filter(f => f.severity === 'warning').length;
54
+ const human_summary = result.allFindings.length === 0
55
+ ? 'No findings — looks clean.'
56
+ : `${result.allFindings.length} finding${result.allFindings.length !== 1 ? 's' : ''}: ${critCount} critical, ${warnCount} warning.`;
57
+
58
+ return {
59
+ schema_version: 1,
60
+ run_id,
61
+ findings: result.allFindings,
62
+ human_summary,
63
+ usage: result.totalCostUSD !== undefined ? { costUSD: result.totalCostUSD } : undefined,
64
+ };
65
+ }
@@ -0,0 +1,65 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as path from 'node:path';
3
+ import { resolveWorkspace, assertInWorkspace } from '../workspace.ts';
4
+ import { saveRun, checksumFile, pruneOldRuns } from '../run-store.ts';
5
+ import { runReviewPhase } from '../../pipeline/review-phase.ts';
6
+ import { detectStack } from '../../detect/stack.ts';
7
+ import type { ReviewEngine } from '../../../adapters/review-engine/types.ts';
8
+ import type { GuardrailConfig } from '../../config/types.ts';
9
+ import type { Finding } from '../../findings/types.ts';
10
+
11
+ export interface ScanFilesResult {
12
+ schema_version: 1;
13
+ run_id: string;
14
+ findings: Finding[];
15
+ human_summary: string;
16
+ }
17
+
18
+ export async function handleScanFiles(
19
+ input: { files: string[]; cwd?: string; ask?: string },
20
+ config: GuardrailConfig,
21
+ engine: ReviewEngine,
22
+ ): Promise<ScanFilesResult> {
23
+ const workspace = resolveWorkspace(input.cwd);
24
+ pruneOldRuns(workspace, 24 * 60 * 60 * 1000);
25
+
26
+ // Validate all paths before any I/O
27
+ const resolvedFiles = input.files.map(f => assertInWorkspace(workspace, f));
28
+
29
+ const stack = detectStack(workspace) ?? config.stack;
30
+ const effectiveConfig: GuardrailConfig = input.ask
31
+ ? { ...config, stack: `${stack ?? 'unknown'}\n\nFocus: ${input.ask}` }
32
+ : config;
33
+
34
+ const result = await runReviewPhase({
35
+ touchedFiles: resolvedFiles,
36
+ config: effectiveConfig,
37
+ engine,
38
+ cwd: workspace,
39
+ });
40
+
41
+ const run_id = crypto.randomUUID();
42
+ const fileChecksums: Record<string, string> = {};
43
+ for (const f of resolvedFiles) {
44
+ // Store relative key so fix_finding's finding.file lookup matches
45
+ const relKey = path.relative(workspace, f);
46
+ fileChecksums[relKey] = checksumFile(f);
47
+ }
48
+ // Normalize finding.file to relative paths so downstream lookups against
49
+ // fileChecksums (keyed by relative paths) work regardless of whether the
50
+ // review engine echoed absolute or relative paths.
51
+ const normalizedFindings = result.findings.map(f => {
52
+ if (!f.file) return f;
53
+ const rel = path.isAbsolute(f.file) ? path.relative(workspace, f.file) : f.file;
54
+ return rel === f.file ? f : { ...f, file: rel };
55
+ });
56
+ saveRun(workspace, run_id, normalizedFindings, fileChecksums);
57
+
58
+ const critCount = result.findings.filter(f => f.severity === 'critical').length;
59
+ const warnCount = result.findings.filter(f => f.severity === 'warning').length;
60
+ const human_summary = result.findings.length === 0
61
+ ? 'No findings.'
62
+ : `${result.findings.length} finding${result.findings.length !== 1 ? 's' : ''}: ${critCount} critical, ${warnCount} warning.`;
63
+
64
+ return { schema_version: 1, run_id, findings: normalizedFindings, human_summary };
65
+ }
@@ -0,0 +1,41 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { resolveWorkspace } from '../workspace.ts';
3
+ import { withWriteLock } from '../concurrency.ts';
4
+ import type { GuardrailConfig } from '../../config/types.ts';
5
+
6
+ export interface ValidateFixResult {
7
+ schema_version: 1;
8
+ passed: boolean;
9
+ output: string;
10
+ durationMs: number;
11
+ }
12
+
13
+ export async function handleValidateFix(
14
+ input: { cwd?: string; files?: string[] },
15
+ config: GuardrailConfig,
16
+ ): Promise<ValidateFixResult> {
17
+ const workspace = resolveWorkspace(input.cwd);
18
+
19
+ if (!config.testCommand) {
20
+ return { schema_version: 1, passed: true, output: '', durationMs: 0 };
21
+ }
22
+
23
+ return withWriteLock(workspace, async () => {
24
+ const start = Date.now();
25
+ const result = spawnSync('/bin/sh', ['-c', config.testCommand!], {
26
+ cwd: workspace,
27
+ shell: false,
28
+ timeout: 120_000,
29
+ encoding: 'utf8',
30
+ });
31
+ const durationMs = Date.now() - start;
32
+ const raw = ((result.stdout ?? '') + (result.stderr ?? '')).slice(0, 4000);
33
+
34
+ return {
35
+ schema_version: 1 as const,
36
+ passed: result.status === 0,
37
+ output: raw,
38
+ durationMs,
39
+ };
40
+ });
41
+ }
@@ -0,0 +1,85 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
+ import type { Finding } from '../findings/types.ts';
5
+
6
+ const RUNS_DIR = '.guardrail-cache/runs';
7
+
8
+ export interface RunRecord {
9
+ run_id: string;
10
+ createdAt: string;
11
+ findings: Finding[];
12
+ fileChecksums: Record<string, string>;
13
+ }
14
+
15
+ function runsDir(cwd: string): string {
16
+ return path.join(cwd, RUNS_DIR);
17
+ }
18
+
19
+ // Reject any run_id that isn't a safe filename component — path separators,
20
+ // relative segments, hidden files, or empty strings.
21
+ // run_ids are generated server-side as UUIDs (crypto.randomUUID) so strict
22
+ // validation here is safe. MCP clients that fabricate run_ids get a clear
23
+ // rejection instead of silently reading outside RUNS_DIR.
24
+ const VALID_RUN_ID = /^[A-Za-z0-9_-]+$/;
25
+
26
+ function assertValidRunId(runId: string): void {
27
+ if (!runId || typeof runId !== 'string' || !VALID_RUN_ID.test(runId)) {
28
+ throw Object.assign(
29
+ new Error(`invalid run_id: "${runId}" (expected alphanumeric + dash/underscore)`),
30
+ { code: 'invalid_run_id' },
31
+ );
32
+ }
33
+ }
34
+
35
+ function runFilePath(cwd: string, runId: string): string {
36
+ assertValidRunId(runId);
37
+ return path.join(runsDir(cwd), `${runId}.json`);
38
+ }
39
+
40
+ export function saveRun(
41
+ cwd: string,
42
+ runId: string,
43
+ findings: Finding[],
44
+ fileChecksums: Record<string, string>,
45
+ ): void {
46
+ const dir = runsDir(cwd);
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ const record: RunRecord = { run_id: runId, createdAt: new Date().toISOString(), findings, fileChecksums };
49
+ const tmp = runFilePath(cwd, runId) + '.tmp';
50
+ fs.writeFileSync(tmp, JSON.stringify(record, null, 2), 'utf8');
51
+ fs.renameSync(tmp, runFilePath(cwd, runId));
52
+ }
53
+
54
+ export function loadRun(cwd: string, runId: string): RunRecord | null {
55
+ const p = runFilePath(cwd, runId);
56
+ if (!fs.existsSync(p)) return null;
57
+ try {
58
+ return JSON.parse(fs.readFileSync(p, 'utf8')) as RunRecord;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ export function checksumFile(filePath: string): string {
65
+ try {
66
+ const content = fs.readFileSync(filePath);
67
+ return crypto.createHash('sha256').update(content).digest('hex');
68
+ } catch {
69
+ return '';
70
+ }
71
+ }
72
+
73
+ export function pruneOldRuns(cwd: string, maxAgeMs: number): void {
74
+ const dir = runsDir(cwd);
75
+ if (!fs.existsSync(dir)) return;
76
+ const cutoff = Date.now() - maxAgeMs;
77
+ for (const entry of fs.readdirSync(dir)) {
78
+ if (!entry.endsWith('.json')) continue;
79
+ const full = path.join(dir, entry);
80
+ try {
81
+ const stat = fs.statSync(full);
82
+ if (stat.mtimeMs < cutoff) fs.unlinkSync(full);
83
+ } catch { /* ignore */ }
84
+ }
85
+ }
@@ -0,0 +1,35 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export function resolveWorkspace(cwd?: string): string {
5
+ return fs.realpathSync(cwd ?? process.cwd());
6
+ }
7
+
8
+ export function assertInWorkspace(workspace: string, filePath: string): string {
9
+ const resolvedWorkspace = fs.realpathSync(workspace);
10
+ const abs = path.isAbsolute(filePath)
11
+ ? filePath
12
+ : path.resolve(resolvedWorkspace, filePath);
13
+
14
+ let resolved: string;
15
+ try {
16
+ resolved = fs.realpathSync(abs);
17
+ } catch {
18
+ // File doesn't exist yet — check the directory
19
+ const dir = path.dirname(abs);
20
+ let resolvedDir: string;
21
+ try {
22
+ resolvedDir = fs.realpathSync(dir);
23
+ } catch {
24
+ // Parent directory doesn't exist — resolve what we can
25
+ resolvedDir = path.resolve(dir);
26
+ }
27
+ resolved = path.join(resolvedDir, path.basename(abs));
28
+ }
29
+
30
+ const root = resolvedWorkspace.endsWith(path.sep) ? resolvedWorkspace : resolvedWorkspace + path.sep;
31
+ if (!resolved.startsWith(root) && resolved !== resolvedWorkspace) {
32
+ throw new Error(`Path "${filePath}" is outside workspace "${workspace}"`);
33
+ }
34
+ return resolved;
35
+ }
@@ -0,0 +1,112 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type { Finding } from '../findings/types.ts';
4
+
5
+ const BASELINE_FILE = '.guardrail-baseline.json';
6
+
7
+ export interface BaselineEntry {
8
+ id: string;
9
+ file: string;
10
+ line?: number;
11
+ severity: string;
12
+ message: string;
13
+ pinnedAt: string;
14
+ note?: string;
15
+ }
16
+
17
+ export interface Baseline {
18
+ version: 1;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ note?: string;
22
+ entries: BaselineEntry[];
23
+ }
24
+
25
+ /** Stable key for matching a finding against a baseline entry. */
26
+ function baselineKey(f: { id: string; file: string; line?: number }): string {
27
+ return `${f.id}::${f.file}::${f.line ?? ''}`;
28
+ }
29
+
30
+ export function baselineFilePath(cwd: string, overridePath?: string): string {
31
+ return overridePath
32
+ ? path.isAbsolute(overridePath) ? overridePath : path.join(cwd, overridePath)
33
+ : path.join(cwd, BASELINE_FILE);
34
+ }
35
+
36
+ export function loadBaseline(cwd: string, overridePath?: string): Baseline | null {
37
+ const p = baselineFilePath(cwd, overridePath);
38
+ if (!fs.existsSync(p)) return null;
39
+ try {
40
+ return JSON.parse(fs.readFileSync(p, 'utf8')) as Baseline;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function saveBaseline(cwd: string, findings: Finding[], options: { note?: string; overridePath?: string } = {}): Baseline {
47
+ const existing = loadBaseline(cwd, options.overridePath);
48
+ const now = new Date().toISOString();
49
+ const baseline: Baseline = {
50
+ version: 1,
51
+ createdAt: existing?.createdAt ?? now,
52
+ updatedAt: now,
53
+ note: options.note ?? existing?.note,
54
+ entries: findings.map(f => ({
55
+ id: f.id,
56
+ file: f.file,
57
+ line: f.line,
58
+ severity: f.severity,
59
+ message: f.message,
60
+ pinnedAt: now,
61
+ })),
62
+ };
63
+ const p = baselineFilePath(cwd, options.overridePath);
64
+ const tmp = p + '.tmp';
65
+ fs.writeFileSync(tmp, JSON.stringify(baseline, null, 2), 'utf8');
66
+ fs.renameSync(tmp, p);
67
+ return baseline;
68
+ }
69
+
70
+ export function clearBaseline(cwd: string, overridePath?: string): void {
71
+ const p = baselineFilePath(cwd, overridePath);
72
+ if (fs.existsSync(p)) fs.unlinkSync(p);
73
+ }
74
+
75
+ export interface BaselineFilterResult {
76
+ newFindings: Finding[];
77
+ baselinedFindings: Finding[];
78
+ baselinedCount: number;
79
+ }
80
+
81
+ /** Returns findings NOT present in the baseline (new findings only). */
82
+ export function filterBaselined(findings: Finding[], baseline: Baseline): BaselineFilterResult {
83
+ const pinned = new Set(baseline.entries.map(baselineKey));
84
+ const newFindings: Finding[] = [];
85
+ const baselinedFindings: Finding[] = [];
86
+ for (const f of findings) {
87
+ if (pinned.has(baselineKey(f))) {
88
+ baselinedFindings.push(f);
89
+ } else {
90
+ newFindings.push(f);
91
+ }
92
+ }
93
+ return { newFindings, baselinedFindings, baselinedCount: baselinedFindings.length };
94
+ }
95
+
96
+ export interface BaselineDiff {
97
+ added: Finding[]; // in current but not in baseline
98
+ resolved: BaselineEntry[]; // in baseline but not in current
99
+ unchanged: Finding[]; // in both
100
+ }
101
+
102
+ /** Diff current findings against a baseline snapshot. */
103
+ export function diffAgainstBaseline(current: Finding[], baseline: Baseline): BaselineDiff {
104
+ const currentKeys = new Set(current.map(baselineKey));
105
+ const baselineKeys = new Set(baseline.entries.map(baselineKey));
106
+
107
+ return {
108
+ added: current.filter(f => !baselineKeys.has(baselineKey(f))),
109
+ resolved: baseline.entries.filter(e => !currentKeys.has(baselineKey(e))),
110
+ unchanged: current.filter(f => baselineKeys.has(baselineKey(f))),
111
+ };
112
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
 
4
- const CACHE_DIR = '.autopilot-cache';
4
+ const CACHE_DIR = '.guardrail-cache';
5
5
  const LOG_FILE = 'costs.jsonl';
6
6
 
7
7
  export interface CostLogEntry {
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import type { Finding } from '../findings/types.ts';
4
4
 
5
- const CACHE_DIR = '.autopilot-cache';
5
+ const CACHE_DIR = '.guardrail-cache';
6
6
  const CACHE_FILE = 'findings.json';
7
7
 
8
8
  function cacheFilePath(cwd: string): string {
@@ -0,0 +1,112 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type { Finding } from '../findings/types.ts';
4
+
5
+ const TRIAGE_FILE = '.guardrail-triage.json';
6
+
7
+ export type TriageState = 'accepted-risk' | 'false-positive';
8
+
9
+ export interface TriageEntry {
10
+ id: string;
11
+ file: string;
12
+ line?: number;
13
+ state: TriageState;
14
+ reason?: string;
15
+ triagedAt: string;
16
+ expiresAt?: string;
17
+ }
18
+
19
+ export interface TriageStore {
20
+ version: 1;
21
+ entries: TriageEntry[];
22
+ }
23
+
24
+ function triageFilePath(cwd: string): string {
25
+ return path.join(cwd, TRIAGE_FILE);
26
+ }
27
+
28
+ function entryKey(e: { id: string; file: string; line?: number }): string {
29
+ return `${e.id}::${e.file}::${e.line ?? ''}`;
30
+ }
31
+
32
+ export function loadTriage(cwd: string): TriageStore {
33
+ const p = triageFilePath(cwd);
34
+ if (!fs.existsSync(p)) return { version: 1, entries: [] };
35
+ try {
36
+ return JSON.parse(fs.readFileSync(p, 'utf8')) as TriageStore;
37
+ } catch {
38
+ return { version: 1, entries: [] };
39
+ }
40
+ }
41
+
42
+ export function saveTriage(cwd: string, store: TriageStore): void {
43
+ const p = triageFilePath(cwd);
44
+ const tmp = p + '.tmp';
45
+ fs.writeFileSync(tmp, JSON.stringify(store, null, 2), 'utf8');
46
+ fs.renameSync(tmp, p);
47
+ }
48
+
49
+ export function addTriageEntry(
50
+ cwd: string,
51
+ finding: Finding,
52
+ state: TriageState,
53
+ options: { reason?: string; expiresInDays?: number } = {},
54
+ ): void {
55
+ const store = loadTriage(cwd);
56
+ const key = entryKey(finding);
57
+ store.entries = store.entries.filter(e => entryKey(e) !== key);
58
+ const entry: TriageEntry = {
59
+ id: finding.id,
60
+ file: finding.file,
61
+ line: finding.line,
62
+ state,
63
+ reason: options.reason,
64
+ triagedAt: new Date().toISOString(),
65
+ };
66
+ if (options.expiresInDays !== undefined) {
67
+ const exp = new Date();
68
+ exp.setDate(exp.getDate() + options.expiresInDays);
69
+ entry.expiresAt = exp.toISOString();
70
+ }
71
+ store.entries.push(entry);
72
+ saveTriage(cwd, store);
73
+ }
74
+
75
+ export function removeTriageEntry(cwd: string, ids: string[]): number {
76
+ const store = loadTriage(cwd);
77
+ const before = store.entries.length;
78
+ store.entries = store.entries.filter(e => !ids.some(id => e.id === id || e.id.startsWith(id)));
79
+ saveTriage(cwd, store);
80
+ return before - store.entries.length;
81
+ }
82
+
83
+ export function clearExpiredEntries(cwd: string): number {
84
+ const store = loadTriage(cwd);
85
+ const now = new Date().toISOString();
86
+ const before = store.entries.length;
87
+ store.entries = store.entries.filter(e => !e.expiresAt || e.expiresAt > now);
88
+ saveTriage(cwd, store);
89
+ return before - store.entries.length;
90
+ }
91
+
92
+ export interface TriageFilterResult {
93
+ active: Finding[];
94
+ triaged: Finding[];
95
+ triageCount: number;
96
+ }
97
+
98
+ export function filterTriaged(findings: Finding[], store: TriageStore): TriageFilterResult {
99
+ const now = new Date().toISOString();
100
+ const activeKeys = new Set(
101
+ store.entries
102
+ .filter(e => !e.expiresAt || e.expiresAt > now)
103
+ .map(entryKey),
104
+ );
105
+ const active: Finding[] = [];
106
+ const triaged: Finding[] = [];
107
+ for (const f of findings) {
108
+ if (activeKeys.has(entryKey(f))) triaged.push(f);
109
+ else active.push(f);
110
+ }
111
+ return { active, triaged, triageCount: triaged.length };
112
+ }
@@ -1,16 +1,20 @@
1
1
  import type { Finding, FixAttempt, FixStatus } from '../findings/types.ts';
2
+ import type { GuardrailConfig } from '../config/types.ts';
3
+ import type { ReviewEngine } from '../../adapters/review-engine/types.ts';
2
4
  import { dedupFindings, findingContentKey } from '../findings/dedup.ts';
3
5
 
4
6
  export interface StaticRule {
5
7
  name: string;
6
8
  severity: 'critical' | 'warning' | 'note';
7
- check(touchedFiles: string[]): Promise<Finding[]>;
9
+ check(touchedFiles: string[], config?: Record<string, unknown>): Promise<Finding[]>;
8
10
  autofix?(finding: Finding): Promise<FixStatus>;
9
11
  }
10
12
 
11
13
  export interface StaticRulesPhaseInput {
12
14
  touchedFiles: string[];
13
15
  rules: StaticRule[];
16
+ config?: GuardrailConfig;
17
+ engine?: ReviewEngine;
14
18
  }
15
19
 
16
20
  export interface StaticRulesPhaseResult {
@@ -24,7 +28,7 @@ export interface StaticRulesPhaseResult {
24
28
  export async function runStaticRulesPhase(input: StaticRulesPhaseInput): Promise<StaticRulesPhaseResult> {
25
29
  const start = Date.now();
26
30
 
27
- const preFixFindings = dedupFindings(await runAllChecks(input.rules, input.touchedFiles));
31
+ const preFixFindings = dedupFindings(await runAllChecks(input.rules, input.touchedFiles, input.config, input.engine));
28
32
 
29
33
  const fixAttempts: FixAttempt[] = [];
30
34
  let anyFixApplied = false;
@@ -52,7 +56,7 @@ export async function runStaticRulesPhase(input: StaticRulesPhaseInput): Promise
52
56
  // findings always returns preFixFindings so callers have a complete record;
53
57
  // fixAttempts + re-check set-difference tells them what was resolved.
54
58
  const postFixFindings = anyFixApplied
55
- ? dedupFindings(await runAllChecks(input.rules, input.touchedFiles))
59
+ ? dedupFindings(await runAllChecks(input.rules, input.touchedFiles, input.config, input.engine))
56
60
  : preFixFindings;
57
61
 
58
62
  const postFixKeys = new Set(postFixFindings.map(findingContentKey));
@@ -69,9 +73,18 @@ export async function runStaticRulesPhase(input: StaticRulesPhaseInput): Promise
69
73
  return { phase: 'static-rules', status, findings: preFixFindings, fixAttempts, durationMs: Date.now() - start };
70
74
  }
71
75
 
72
- async function runAllChecks(rules: StaticRule[], files: string[]): Promise<Finding[]> {
76
+ async function runAllChecks(
77
+ rules: StaticRule[],
78
+ files: string[],
79
+ config?: GuardrailConfig,
80
+ engine?: ReviewEngine,
81
+ ): Promise<Finding[]> {
82
+ const ruleConfig: Record<string, unknown> = {
83
+ ...(config ? (config as unknown as Record<string, unknown>) : {}),
84
+ _engine: engine,
85
+ };
73
86
  const all: Finding[] = [];
74
- for (const rule of rules) all.push(...(await rule.check(files)));
87
+ for (const rule of rules) all.push(...(await rule.check(files, ruleConfig)));
75
88
  return all;
76
89
  }
77
90