@delegance/claude-autopilot 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/cli/ci.ts +2 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/run.ts +7 -0
- package/src/core/chunking/index.ts +21 -1
- package/src/core/config/types.ts +1 -1
- package/src/core/git/diff-hunks.ts +86 -0
- package/src/core/pipeline/review-phase.ts +2 -0
- package/src/core/pipeline/run.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0] — 2026-04-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`reviewStrategy: diff`** — new chunking strategy that sends `git diff` unified hunks instead of full file contents; typically ~70% fewer tokens and more focused findings (LLM sees exactly what changed)
|
|
7
|
+
- **`--diff` flag** on `run` and `ci` subcommands — shorthand to activate diff strategy without editing config
|
|
8
|
+
- **`src/core/git/diff-hunks.ts`** — `getFileDiffs()`, `parseUnifiedDiff()`, `formatDiffContent()`; per-file diff sections in fenced code blocks; files that exceed `maxChars` are omitted with a count notice
|
|
9
|
+
- `BuildChunksInput.base` / `ReviewPhaseInput.base` / `RunInput.base` — threads git base ref through pipeline to diff engine
|
|
10
|
+
- 9 new tests for `parseUnifiedDiff` and `formatDiffContent` — **233 total**
|
|
11
|
+
|
|
3
12
|
## [2.1.0] — 2026-04-22
|
|
4
13
|
|
|
5
14
|
### Added
|
package/package.json
CHANGED
package/src/cli/ci.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface CiCommandOptions {
|
|
|
6
6
|
base?: string;
|
|
7
7
|
postComments?: boolean;
|
|
8
8
|
sarifOutput?: string;
|
|
9
|
+
diff?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -34,5 +35,6 @@ export async function runCi(options: CiCommandOptions = {}): Promise<number> {
|
|
|
34
35
|
postComments: options.postComments ?? true,
|
|
35
36
|
format: 'sarif',
|
|
36
37
|
outputPath: sarifOutput,
|
|
38
|
+
diff: options.diff,
|
|
37
39
|
});
|
|
38
40
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ Options (run):
|
|
|
66
66
|
--config <path> Path to config file (default: ./autopilot.config.yaml)
|
|
67
67
|
--files <a,b,c> Explicit comma-separated file list (skips git detection)
|
|
68
68
|
--dry-run Show what would run without executing
|
|
69
|
+
--diff Send git diff hunks instead of full files (~70% fewer tokens)
|
|
69
70
|
--post-comments Post/update a summary comment on the open PR
|
|
70
71
|
--format <text|sarif> Output format (default: text)
|
|
71
72
|
--output <path> Output file path (required with --format sarif)
|
|
@@ -120,6 +121,7 @@ switch (subcommand) {
|
|
|
120
121
|
const config = flag('config');
|
|
121
122
|
const filesArg = flag('files');
|
|
122
123
|
const dryRun = boolFlag('dry-run');
|
|
124
|
+
const diff = boolFlag('diff');
|
|
123
125
|
const postComments = boolFlag('post-comments');
|
|
124
126
|
const formatArg = flag('format');
|
|
125
127
|
const outputPath = flag('output');
|
|
@@ -138,6 +140,7 @@ switch (subcommand) {
|
|
|
138
140
|
configPath: config,
|
|
139
141
|
files: filesArg ? filesArg.split(',').map(f => f.trim()) : undefined,
|
|
140
142
|
dryRun,
|
|
143
|
+
diff,
|
|
141
144
|
postComments,
|
|
142
145
|
format: formatArg as 'text' | 'sarif' | undefined,
|
|
143
146
|
outputPath,
|
|
@@ -151,11 +154,13 @@ switch (subcommand) {
|
|
|
151
154
|
const config = flag('config');
|
|
152
155
|
const outputPath = flag('output');
|
|
153
156
|
const noPostComments = boolFlag('no-post-comments');
|
|
157
|
+
const diff = boolFlag('diff');
|
|
154
158
|
const code = await runCi({
|
|
155
159
|
configPath: config,
|
|
156
160
|
base,
|
|
157
161
|
sarifOutput: outputPath,
|
|
158
162
|
postComments: noPostComments ? false : undefined,
|
|
163
|
+
diff,
|
|
159
164
|
});
|
|
160
165
|
process.exit(code);
|
|
161
166
|
break;
|
package/src/cli/run.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface RunCommandOptions {
|
|
|
64
64
|
base?: string; // git base ref (default HEAD~1)
|
|
65
65
|
files?: string[]; // explicit file list (skips git detection)
|
|
66
66
|
dryRun?: boolean; // skip review, print what would run
|
|
67
|
+
diff?: boolean; // use diff strategy (send git hunks instead of full files)
|
|
67
68
|
format?: 'text' | 'sarif';
|
|
68
69
|
outputPath?: string;
|
|
69
70
|
postComments?: boolean; // post/update summary comment on the open PR
|
|
@@ -167,6 +168,11 @@ export async function runCommand(options: RunCommandOptions = {}): Promise<numbe
|
|
|
167
168
|
? await loadRulesFromConfig(config.staticRules)
|
|
168
169
|
: [];
|
|
169
170
|
|
|
171
|
+
// Apply --diff flag: override reviewStrategy to 'diff'
|
|
172
|
+
if (options.diff && config.reviewStrategy !== 'diff') {
|
|
173
|
+
config = { ...config, reviewStrategy: 'diff' };
|
|
174
|
+
}
|
|
175
|
+
|
|
170
176
|
// Execute pipeline
|
|
171
177
|
const input: RunInput = {
|
|
172
178
|
touchedFiles,
|
|
@@ -175,6 +181,7 @@ export async function runCommand(options: RunCommandOptions = {}): Promise<numbe
|
|
|
175
181
|
staticRules,
|
|
176
182
|
cwd,
|
|
177
183
|
gitSummary: gitCtx.summary ?? undefined,
|
|
184
|
+
base: options.base,
|
|
178
185
|
};
|
|
179
186
|
|
|
180
187
|
console.log('');
|
|
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import type { ReviewEngine, ReviewInput } from '../../adapters/review-engine/types.ts';
|
|
4
4
|
import type { AutopilotConfig } from '../config/types.ts';
|
|
5
5
|
import { rankByRisk } from './risk-ranker.ts';
|
|
6
|
+
import { getFileDiffs, formatDiffContent } from '../git/diff-hunks.ts';
|
|
6
7
|
|
|
7
8
|
export interface ReviewChunk {
|
|
8
9
|
content: string;
|
|
@@ -12,11 +13,12 @@ export interface ReviewChunk {
|
|
|
12
13
|
|
|
13
14
|
export interface BuildChunksInput {
|
|
14
15
|
touchedFiles: string[];
|
|
15
|
-
strategy: 'auto' | 'single-pass' | 'file-level';
|
|
16
|
+
strategy: 'auto' | 'single-pass' | 'file-level' | 'diff';
|
|
16
17
|
chunking?: AutopilotConfig['chunking'];
|
|
17
18
|
engine: ReviewEngine;
|
|
18
19
|
cwd?: string;
|
|
19
20
|
protectedPaths?: string[];
|
|
21
|
+
base?: string; // git base ref — required for 'diff' strategy
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const DEFAULT_SMALL_TIER_TOKENS = 8000;
|
|
@@ -26,6 +28,11 @@ export async function buildReviewChunks(input: BuildChunksInput): Promise<Review
|
|
|
26
28
|
const smallMax = input.chunking?.smallTierMaxTokens ?? DEFAULT_SMALL_TIER_TOKENS;
|
|
27
29
|
const fileMax = input.chunking?.perFileMaxTokens ?? DEFAULT_FILE_TIER_TOKENS;
|
|
28
30
|
|
|
31
|
+
// Diff strategy: send unified diff hunks instead of full file contents
|
|
32
|
+
if (input.strategy === 'diff') {
|
|
33
|
+
return buildDiffChunks(input);
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
const ranked = rankByRisk(input.touchedFiles, { protectedPaths: input.protectedPaths });
|
|
30
37
|
const fileContents = await readFiles(ranked, input.cwd);
|
|
31
38
|
|
|
@@ -51,6 +58,19 @@ export async function buildReviewChunks(input: BuildChunksInput): Promise<Review
|
|
|
51
58
|
return chunks;
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
function buildDiffChunks(input: BuildChunksInput): ReviewChunk[] {
|
|
62
|
+
const cwd = input.cwd ?? process.cwd();
|
|
63
|
+
const base = input.base ?? 'HEAD~1';
|
|
64
|
+
const ranked = rankByRisk(input.touchedFiles, { protectedPaths: input.protectedPaths });
|
|
65
|
+
const diffs = getFileDiffs(cwd, base, ranked);
|
|
66
|
+
|
|
67
|
+
if (diffs.length === 0) return [];
|
|
68
|
+
|
|
69
|
+
// Single chunk — diff content is already compact; truncation handled in formatDiffContent
|
|
70
|
+
const content = formatDiffContent(diffs);
|
|
71
|
+
return [{ content, kind: 'file-batch', files: diffs.map(d => d.file) }];
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
async function readFiles(touchedFiles: string[], cwd?: string): Promise<Map<string, string>> {
|
|
55
75
|
const result = new Map<string, string>();
|
|
56
76
|
for (const f of touchedFiles) {
|
package/src/core/config/types.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface AutopilotConfig {
|
|
|
27
27
|
maxCodexRetries?: number;
|
|
28
28
|
maxBugbotRounds?: number;
|
|
29
29
|
};
|
|
30
|
-
reviewStrategy?: 'auto' | 'single-pass' | 'file-level';
|
|
30
|
+
reviewStrategy?: 'auto' | 'single-pass' | 'file-level' | 'diff';
|
|
31
31
|
chunking?: {
|
|
32
32
|
smallTierMaxTokens?: number;
|
|
33
33
|
partialReviewTokens?: number;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { runSafe } from '../shell.ts';
|
|
2
|
+
|
|
3
|
+
export interface FileDiff {
|
|
4
|
+
file: string;
|
|
5
|
+
hunks: string; // unified diff content for this file (header + hunks)
|
|
6
|
+
additions: number;
|
|
7
|
+
deletions: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns per-file unified diffs for the given files between base and HEAD.
|
|
12
|
+
* Falls back to working-tree diff (unstaged) when base diff is empty for a file.
|
|
13
|
+
*/
|
|
14
|
+
export function getFileDiffs(cwd: string, base: string, files: string[]): FileDiff[] {
|
|
15
|
+
if (files.length === 0) return [];
|
|
16
|
+
|
|
17
|
+
// Get full diff in one shot — more efficient than per-file calls
|
|
18
|
+
const raw = runSafe('git', ['diff', base, 'HEAD', '--unified=3', '--', ...files], { cwd })
|
|
19
|
+
?? runSafe('git', ['diff', 'HEAD', '--unified=3', '--', ...files], { cwd })
|
|
20
|
+
?? '';
|
|
21
|
+
|
|
22
|
+
return parseUnifiedDiff(raw, files);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses unified diff output into per-file FileDiff entries.
|
|
27
|
+
* Only returns files that actually have diff content.
|
|
28
|
+
*/
|
|
29
|
+
export function parseUnifiedDiff(raw: string, requestedFiles: string[]): FileDiff[] {
|
|
30
|
+
if (!raw.trim()) return [];
|
|
31
|
+
|
|
32
|
+
const results: FileDiff[] = [];
|
|
33
|
+
const sections = raw.split(/^(?=diff --git )/m).filter(Boolean);
|
|
34
|
+
|
|
35
|
+
const requested = new Set(requestedFiles.map(f => f.replace(/\\/g, '/')));
|
|
36
|
+
|
|
37
|
+
for (const section of sections) {
|
|
38
|
+
// Extract b/ filename from diff header: diff --git a/src/foo.ts b/src/foo.ts
|
|
39
|
+
const headerMatch = section.match(/^diff --git a\/.+ b\/(.+)$/m);
|
|
40
|
+
if (!headerMatch) continue;
|
|
41
|
+
const file = headerMatch[1]!.trim();
|
|
42
|
+
if (!requested.has(file)) continue;
|
|
43
|
+
|
|
44
|
+
// Strip the git binary/index header lines, keep hunk content
|
|
45
|
+
const hunkStart = section.indexOf('@@');
|
|
46
|
+
const hunks = hunkStart >= 0 ? section.slice(hunkStart) : '';
|
|
47
|
+
if (!hunks.trim()) continue;
|
|
48
|
+
|
|
49
|
+
let additions = 0;
|
|
50
|
+
let deletions = 0;
|
|
51
|
+
for (const line of hunks.split('\n')) {
|
|
52
|
+
if (line.startsWith('+') && !line.startsWith('+++')) additions++;
|
|
53
|
+
if (line.startsWith('-') && !line.startsWith('---')) deletions++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
results.push({ file, hunks: hunks.trimEnd(), additions, deletions });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Formats FileDiff entries into a review-ready string.
|
|
64
|
+
* Total size is bounded by maxChars (default 120K chars ≈ 30K tokens).
|
|
65
|
+
*/
|
|
66
|
+
export function formatDiffContent(diffs: FileDiff[], maxChars = 120_000): string {
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
let total = 0;
|
|
69
|
+
let skipped = 0;
|
|
70
|
+
|
|
71
|
+
for (const d of diffs) {
|
|
72
|
+
const section = `## ${d.file} (+${d.additions}/-${d.deletions})\n\`\`\`diff\n${d.hunks}\n\`\`\``;
|
|
73
|
+
if (total + section.length > maxChars) {
|
|
74
|
+
skipped++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
parts.push(section);
|
|
78
|
+
total += section.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (skipped > 0) {
|
|
82
|
+
parts.push(`[${skipped} file${skipped !== 1 ? 's' : ''} omitted — diff exceeded size limit]`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join('\n\n');
|
|
86
|
+
}
|
|
@@ -19,6 +19,7 @@ export interface ReviewPhaseInput {
|
|
|
19
19
|
cwd?: string;
|
|
20
20
|
gitSummary?: string;
|
|
21
21
|
budgetRemainingUSD?: number;
|
|
22
|
+
base?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export async function runReviewPhase(input: ReviewPhaseInput): Promise<ReviewPhaseResult> {
|
|
@@ -35,6 +36,7 @@ export async function runReviewPhase(input: ReviewPhaseInput): Promise<ReviewPha
|
|
|
35
36
|
engine: input.engine,
|
|
36
37
|
cwd: input.cwd,
|
|
37
38
|
protectedPaths: input.config.protectedPaths,
|
|
39
|
+
base: input.base,
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
const allFindings: Finding[] = [];
|
package/src/core/pipeline/run.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface RunInput {
|
|
|
18
18
|
staticRules?: StaticRule[];
|
|
19
19
|
cwd?: string;
|
|
20
20
|
gitSummary?: string;
|
|
21
|
+
base?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface RunResult {
|
|
@@ -62,6 +63,7 @@ export async function runAutopilot(input: RunInput): Promise<RunResult> {
|
|
|
62
63
|
cwd: input.cwd,
|
|
63
64
|
gitSummary: input.gitSummary,
|
|
64
65
|
budgetRemainingUSD: budgetUSD,
|
|
66
|
+
base: input.base,
|
|
65
67
|
});
|
|
66
68
|
phases.push(reviewResult);
|
|
67
69
|
if (reviewResult.costUSD !== undefined) {
|