@mrkaran/hodor 0.7.2 → 0.7.3
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/dist/{chunk-YKB3BRJM.js → chunk-GISFKKMM.js} +253 -147
- package/dist/chunk-GISFKKMM.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-YKB3BRJM.js.map +0 -1
|
@@ -31,6 +31,135 @@ function getTemplatePath(name) {
|
|
|
31
31
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", name);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/review-diff.ts
|
|
35
|
+
var HODOR_REVIEW_SHA_RE = /^\s*<!--\s*hodor:sha:([a-f0-9]{40})\s*-->/i;
|
|
36
|
+
function getReviewDiffArgs(options) {
|
|
37
|
+
const {
|
|
38
|
+
platform,
|
|
39
|
+
targetBranch,
|
|
40
|
+
diffBaseSha,
|
|
41
|
+
previousReviewSha,
|
|
42
|
+
reviewDiffMode,
|
|
43
|
+
localMode = false
|
|
44
|
+
} = options;
|
|
45
|
+
const rebasedGitlabReview = platform === "gitlab" && reviewDiffMode === "snapshot";
|
|
46
|
+
if (previousReviewSha && !rebasedGitlabReview) {
|
|
47
|
+
return reviewDiffMode === "snapshot" ? ["--no-pager", "diff", previousReviewSha, "HEAD"] : ["--no-pager", "diff", `${previousReviewSha}...HEAD`];
|
|
48
|
+
}
|
|
49
|
+
if (localMode) return ["--no-pager", "diff", targetBranch];
|
|
50
|
+
if (diffBaseSha) return ["--no-pager", "diff", diffBaseSha, "HEAD"];
|
|
51
|
+
return ["--no-pager", "diff", `origin/${targetBranch}...HEAD`];
|
|
52
|
+
}
|
|
53
|
+
function getHodorReviewShaCandidates(notes) {
|
|
54
|
+
if (!notes || notes.length === 0) return [];
|
|
55
|
+
const candidates = [];
|
|
56
|
+
for (const [index, note] of notes.entries()) {
|
|
57
|
+
const match = note.body?.match(HODOR_REVIEW_SHA_RE);
|
|
58
|
+
if (!match) continue;
|
|
59
|
+
const createdAtMs = Date.parse(note.created_at ?? "");
|
|
60
|
+
candidates.push({
|
|
61
|
+
sha: match[1],
|
|
62
|
+
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
|
|
63
|
+
index
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
candidates.sort((a, b) => {
|
|
67
|
+
if (a.createdAtMs != null && b.createdAtMs != null && a.createdAtMs !== b.createdAtMs) {
|
|
68
|
+
return b.createdAtMs - a.createdAtMs;
|
|
69
|
+
}
|
|
70
|
+
if (a.createdAtMs != null && b.createdAtMs == null) return -1;
|
|
71
|
+
if (a.createdAtMs == null && b.createdAtMs != null) return 1;
|
|
72
|
+
return a.index - b.index;
|
|
73
|
+
});
|
|
74
|
+
return [...new Set(candidates.map(({ sha }) => sha))];
|
|
75
|
+
}
|
|
76
|
+
async function findLatestReviewBase(notes, workspacePath) {
|
|
77
|
+
const candidates = getHodorReviewShaCandidates(notes);
|
|
78
|
+
if (candidates.length === 0) return null;
|
|
79
|
+
logger.info(`Found ${candidates.length} previous Hodor review marker(s)`);
|
|
80
|
+
for (const sha of candidates) {
|
|
81
|
+
try {
|
|
82
|
+
let objectType;
|
|
83
|
+
try {
|
|
84
|
+
({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
|
|
85
|
+
cwd: workspacePath
|
|
86
|
+
}));
|
|
87
|
+
} catch {
|
|
88
|
+
await exec("git", ["fetch", "--quiet", "origin", sha], {
|
|
89
|
+
cwd: workspacePath
|
|
90
|
+
});
|
|
91
|
+
({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
|
|
92
|
+
cwd: workspacePath
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
if (objectType.trim() !== "commit") throw new Error("not a commit");
|
|
96
|
+
try {
|
|
97
|
+
await exec("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
|
|
98
|
+
cwd: workspacePath
|
|
99
|
+
});
|
|
100
|
+
return { sha, mode: "incremental" };
|
|
101
|
+
} catch {
|
|
102
|
+
logger.info(
|
|
103
|
+
`Previous review SHA ${sha.slice(0, 8)} is not an ancestor; using snapshot delta`
|
|
104
|
+
);
|
|
105
|
+
return { sha, mode: "snapshot" };
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
logger.info(
|
|
109
|
+
`Skipping previous review SHA ${sha.slice(0, 8)}; commit is unavailable`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function getDiffStats(diff) {
|
|
116
|
+
let files = 0;
|
|
117
|
+
let additions = 0;
|
|
118
|
+
let deletions = 0;
|
|
119
|
+
for (const line of diff.split("\n")) {
|
|
120
|
+
if (line.startsWith("diff --git ")) files++;
|
|
121
|
+
else if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
122
|
+
else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
files,
|
|
126
|
+
additions,
|
|
127
|
+
deletions,
|
|
128
|
+
bytes: Buffer.byteLength(diff, "utf-8")
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function getChangedFiles(diff) {
|
|
132
|
+
const files = [];
|
|
133
|
+
for (const match of diff.matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm)) {
|
|
134
|
+
files.push(match[2]);
|
|
135
|
+
}
|
|
136
|
+
return [...new Set(files)];
|
|
137
|
+
}
|
|
138
|
+
var DIFF_SKIP_PATTERNS = [
|
|
139
|
+
/(?:^|\/)testdata\//,
|
|
140
|
+
/(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.sum|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/,
|
|
141
|
+
/\.mdx?$/
|
|
142
|
+
];
|
|
143
|
+
function filterEmbeddedDiff(rawDiff) {
|
|
144
|
+
const skippedFiles = [];
|
|
145
|
+
const sections = rawDiff.split(/(?=^diff --git )/m);
|
|
146
|
+
const kept = [];
|
|
147
|
+
for (const section of sections) {
|
|
148
|
+
const match = section.match(/^diff --git a\/(.*?) b\//);
|
|
149
|
+
if (!match) {
|
|
150
|
+
kept.push(section);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const filePath = match[1];
|
|
154
|
+
if (DIFF_SKIP_PATTERNS.some((pattern) => pattern.test(filePath))) {
|
|
155
|
+
skippedFiles.push(filePath);
|
|
156
|
+
} else {
|
|
157
|
+
kept.push(section);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { filtered: kept.join(""), skippedFiles };
|
|
161
|
+
}
|
|
162
|
+
|
|
34
163
|
// src/prompt.ts
|
|
35
164
|
function buildPrReviewPrompt(opts) {
|
|
36
165
|
const {
|
|
@@ -46,6 +175,9 @@ function buildPrReviewPrompt(opts) {
|
|
|
46
175
|
localMode = false,
|
|
47
176
|
singleTurn = false
|
|
48
177
|
} = opts;
|
|
178
|
+
const rebasedGitlabReview = platform === "gitlab" && reviewDiffMode === "snapshot";
|
|
179
|
+
const hasPreviousReviewDelta = Boolean(previousReviewSha && !rebasedGitlabReview);
|
|
180
|
+
const previousReviewShaText = previousReviewSha ?? "";
|
|
49
181
|
let templateText;
|
|
50
182
|
try {
|
|
51
183
|
templateText = readFileSync(getTemplatePath("review-task.md"), "utf-8");
|
|
@@ -62,44 +194,36 @@ function buildPrReviewPrompt(opts) {
|
|
|
62
194
|
if (previousReviewSha && !/^[a-f0-9]{40}$/.test(previousReviewSha)) {
|
|
63
195
|
throw new Error(`Invalid previous review SHA: ${previousReviewSha}`);
|
|
64
196
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (diffBaseSha) {
|
|
80
|
-
prDiffCmd = `git --no-pager diff ${diffBaseSha} HEAD --name-only`;
|
|
81
|
-
gitDiffCmd = `git --no-pager diff ${diffBaseSha} HEAD`;
|
|
82
|
-
logger.info(`Using GitLab CI_MERGE_REQUEST_DIFF_BASE_SHA: ${diffBaseSha.slice(0, 8)}`);
|
|
83
|
-
} else {
|
|
84
|
-
prDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD --name-only`;
|
|
85
|
-
gitDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD`;
|
|
86
|
-
}
|
|
197
|
+
const diffArgs = getReviewDiffArgs({
|
|
198
|
+
platform,
|
|
199
|
+
targetBranch,
|
|
200
|
+
diffBaseSha,
|
|
201
|
+
previousReviewSha,
|
|
202
|
+
reviewDiffMode,
|
|
203
|
+
localMode
|
|
204
|
+
});
|
|
205
|
+
const gitDiffCmd = `git ${diffArgs.join(" ")}`;
|
|
206
|
+
const prDiffCmd = `${gitDiffCmd} --name-only`;
|
|
207
|
+
if (hasPreviousReviewDelta) {
|
|
208
|
+
logger.info(`${reviewDiffMode === "snapshot" ? "Snapshot" : "Incremental"} review: diffing from ${previousReviewSha?.slice(0, 8)} to HEAD`);
|
|
209
|
+
} else if (rebasedGitlabReview) {
|
|
210
|
+
logger.info("Rebased GitLab review: diffing from the current MR base to HEAD");
|
|
87
211
|
}
|
|
88
212
|
let diffExplanation;
|
|
89
|
-
if (
|
|
90
|
-
diffExplanation = reviewDiffMode === "snapshot" ? `**Snapshot delta mode**: The MR history was rewritten. This directly compares the last reviewed snapshot (commit \`${
|
|
213
|
+
if (hasPreviousReviewDelta) {
|
|
214
|
+
diffExplanation = reviewDiffMode === "snapshot" ? `**Snapshot delta mode**: The MR history was rewritten. This directly compares the last reviewed snapshot (commit \`${previousReviewShaText.slice(0, 8)}\`) with the current HEAD; it does not imply ancestry.` : `**Incremental mode**: Showing only changes since the last hodor review (commit \`${previousReviewShaText.slice(0, 8)}\`).`;
|
|
91
215
|
} else if (diffBaseSha) {
|
|
92
|
-
diffExplanation = `**GitLab CI Advantage**: This uses
|
|
216
|
+
diffExplanation = `**GitLab CI Advantage**: This uses the merge base resolved from the current target branch, which matches the current GitLab MR diff after force pushes and rebases.`;
|
|
93
217
|
} else {
|
|
94
218
|
diffExplanation = `**Three-dot syntax** shows ONLY changes introduced on the source branch, excluding changes already on \`${targetBranch}\`.`;
|
|
95
219
|
}
|
|
96
220
|
const { contextSection, notesSection, reminderSection } = buildMrSections(mrMetadata);
|
|
97
221
|
const oneTurn = singleTurn && Boolean(embeddedDiff);
|
|
98
222
|
let incrementalSection = "";
|
|
99
|
-
if (
|
|
223
|
+
if (hasPreviousReviewDelta) {
|
|
100
224
|
incrementalSection = `## ${reviewDiffMode === "snapshot" ? "Snapshot Delta" : "Incremental Review"} Mode
|
|
101
225
|
|
|
102
|
-
This is a follow-up review. A previous hodor review was done at commit \`${
|
|
226
|
+
This is a follow-up review. A previous hodor review was done at commit \`${previousReviewShaText.slice(0, 8)}\`. ` + (reviewDiffMode === "snapshot" ? "The branch history was rewritten, so the diff below compares that reviewed snapshot directly with the current HEAD. " : "The diff below shows ONLY changes since that review. ") + "Your job is to review that delta, not the whole MR again.\n\nRules for incremental reviews:\n1. Only report findings introduced or still affected by the new delta.\n2. Do not re-report issues that are already mentioned in existing notes unless the new delta changes the same code and the issue remains newly relevant.\n3. If the delta is small and self-contained, decide from the embedded diff and submit the review without broad repository exploration.\n" + (oneTurn ? "4. No file-inspection tools are available; if a mechanical change like a route/path/string rename leaves a compatibility question you cannot settle from the diff, do not report it.\n" : "4. For mechanical changes like route/path/string renames, verify the direct call sites or tests only when the diff itself leaves a concrete compatibility question.\n") + "5. If the delta does not produce a qualifying finding under the selected review instructions, submit no findings.\n\n";
|
|
103
227
|
}
|
|
104
228
|
let embeddedDiffSection;
|
|
105
229
|
let diffFetchInstructions;
|
|
@@ -127,7 +251,7 @@ ${changedFiles.map((file) => `- \`${file}\``).join("\n")}
|
|
|
127
251
|
startInstruction = "Analyze the diff above and call `submit_review` now, in this turn.";
|
|
128
252
|
} else {
|
|
129
253
|
reviewProcessSection = "## Review Process\n\n1. Analyze the embedded diff above thoroughly\n2. Use `grep` to search for patterns when needed\n3. Use bounded line-range reads when surrounding context is essential; avoid reading entire large files\n4. Do not repeat a diff, grep, or read operation whose result is already in context\n5. Submit your review using `submit_review`\n";
|
|
130
|
-
startInstruction =
|
|
254
|
+
startInstruction = hasPreviousReviewDelta ? "Analyze only the incremental diff provided above. If it is self-contained, submit your review without extra tool calls." : "Analyze the diff provided above, then submit your review using `submit_review`.";
|
|
131
255
|
}
|
|
132
256
|
} else {
|
|
133
257
|
embeddedDiffSection = "";
|
|
@@ -314,6 +438,31 @@ function parseModelString(model) {
|
|
|
314
438
|
}
|
|
315
439
|
return { provider: "anthropic", modelId: trimmed };
|
|
316
440
|
}
|
|
441
|
+
var BEDROCK_REGIONAL_PREFIXES = ["global", "us", "eu", "apac", "in", "jp", "au", "ca"];
|
|
442
|
+
function stripBedrockRegionalPrefix(modelId) {
|
|
443
|
+
const dot = modelId.indexOf(".");
|
|
444
|
+
if (dot <= 0) return null;
|
|
445
|
+
const prefix = modelId.slice(0, dot).toLowerCase();
|
|
446
|
+
if (!BEDROCK_REGIONAL_PREFIXES.includes(prefix)) return null;
|
|
447
|
+
return modelId.slice(dot + 1);
|
|
448
|
+
}
|
|
449
|
+
function isOpenAiBedrockModel(model) {
|
|
450
|
+
if (model.provider !== "amazon-bedrock") return false;
|
|
451
|
+
return [model.id, model.name].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes("openai"));
|
|
452
|
+
}
|
|
453
|
+
function addOpenAiBedrockReasoning(payload, effort) {
|
|
454
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
|
|
455
|
+
const request = payload;
|
|
456
|
+
const existingFields = request.additionalModelRequestFields;
|
|
457
|
+
const additionalModelRequestFields = existingFields && typeof existingFields === "object" && !Array.isArray(existingFields) ? existingFields : {};
|
|
458
|
+
return {
|
|
459
|
+
...request,
|
|
460
|
+
additionalModelRequestFields: {
|
|
461
|
+
...additionalModelRequestFields,
|
|
462
|
+
reasoning: { effort }
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
317
466
|
function extractBedrockArnRegion(arn) {
|
|
318
467
|
const parts = arn.split(":");
|
|
319
468
|
return parts.length >= 4 && parts[3] ? parts[3] : "us-east-1";
|
|
@@ -1486,6 +1635,31 @@ async function detectCiWorkspace(owner, repo) {
|
|
|
1486
1635
|
}
|
|
1487
1636
|
return { path: null, targetBranch: null, diffBaseSha: null };
|
|
1488
1637
|
}
|
|
1638
|
+
async function resolveGitlabDiffBaseSha(workspace, targetBranch, fallbackSha) {
|
|
1639
|
+
if (!targetBranch) return fallbackSha;
|
|
1640
|
+
try {
|
|
1641
|
+
await exec("git", ["fetch", "--no-tags", "origin", targetBranch], { cwd: workspace });
|
|
1642
|
+
const { stdout } = await exec("git", ["merge-base", "HEAD", "FETCH_HEAD"], { cwd: workspace });
|
|
1643
|
+
const mergeBase = stdout.trim();
|
|
1644
|
+
if (mergeBase) {
|
|
1645
|
+
logger.info(`Calculated current GitLab MR diff base: ${mergeBase.slice(0, 8)}`);
|
|
1646
|
+
return mergeBase;
|
|
1647
|
+
}
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
logger.warn(`Could not calculate current GitLab MR diff base: ${err}`);
|
|
1650
|
+
}
|
|
1651
|
+
try {
|
|
1652
|
+
const { stdout } = await exec("git", ["merge-base", "HEAD", `origin/${targetBranch}`], { cwd: workspace });
|
|
1653
|
+
const mergeBase = stdout.trim();
|
|
1654
|
+
if (mergeBase) {
|
|
1655
|
+
logger.info(`Calculated GitLab MR diff base from origin/${targetBranch}: ${mergeBase.slice(0, 8)}`);
|
|
1656
|
+
return mergeBase;
|
|
1657
|
+
}
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
if (fallbackSha) logger.warn(`Falling back to CI_MERGE_REQUEST_DIFF_BASE_SHA: ${fallbackSha.slice(0, 8)}`);
|
|
1661
|
+
return fallbackSha;
|
|
1662
|
+
}
|
|
1489
1663
|
function normalizeGitRemotePath(remoteUrl) {
|
|
1490
1664
|
const trimmed = remoteUrl.trim().replace(/\.git$/, "");
|
|
1491
1665
|
try {
|
|
@@ -1700,11 +1874,18 @@ async function setupWorkspace(opts) {
|
|
|
1700
1874
|
try {
|
|
1701
1875
|
const ci = await detectCiWorkspace(owner, repo);
|
|
1702
1876
|
let detectedTargetBranch = ci.targetBranch;
|
|
1703
|
-
|
|
1877
|
+
let detectedDiffBaseSha = ci.diffBaseSha;
|
|
1704
1878
|
let workspace;
|
|
1705
1879
|
let isTemporary = false;
|
|
1706
1880
|
if (ci.path) {
|
|
1707
1881
|
workspace = ci.path;
|
|
1882
|
+
if (platform === "gitlab" && ci.targetBranch) {
|
|
1883
|
+
detectedDiffBaseSha = await resolveGitlabDiffBaseSha(
|
|
1884
|
+
workspace,
|
|
1885
|
+
ci.targetBranch,
|
|
1886
|
+
detectedDiffBaseSha
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1708
1889
|
if (platform === "github" && !detectedTargetBranch) {
|
|
1709
1890
|
detectedTargetBranch = await getGithubBaseBranch(workspace, prNumber);
|
|
1710
1891
|
}
|
|
@@ -1954,118 +2135,6 @@ function resolveReviewLocations(review, opts) {
|
|
|
1954
2135
|
return { review: { ...review, findings }, stats };
|
|
1955
2136
|
}
|
|
1956
2137
|
|
|
1957
|
-
// src/review-diff.ts
|
|
1958
|
-
var HODOR_REVIEW_SHA_RE = /^\s*<!--\s*hodor:sha:([a-f0-9]{40})\s*-->/i;
|
|
1959
|
-
function getHodorReviewShaCandidates(notes) {
|
|
1960
|
-
if (!notes || notes.length === 0) return [];
|
|
1961
|
-
const candidates = [];
|
|
1962
|
-
for (const [index, note] of notes.entries()) {
|
|
1963
|
-
const match = note.body?.match(HODOR_REVIEW_SHA_RE);
|
|
1964
|
-
if (!match) continue;
|
|
1965
|
-
const createdAtMs = Date.parse(note.created_at ?? "");
|
|
1966
|
-
candidates.push({
|
|
1967
|
-
sha: match[1],
|
|
1968
|
-
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
|
|
1969
|
-
index
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
candidates.sort((a, b) => {
|
|
1973
|
-
if (a.createdAtMs != null && b.createdAtMs != null && a.createdAtMs !== b.createdAtMs) {
|
|
1974
|
-
return b.createdAtMs - a.createdAtMs;
|
|
1975
|
-
}
|
|
1976
|
-
if (a.createdAtMs != null && b.createdAtMs == null) return -1;
|
|
1977
|
-
if (a.createdAtMs == null && b.createdAtMs != null) return 1;
|
|
1978
|
-
return a.index - b.index;
|
|
1979
|
-
});
|
|
1980
|
-
return [...new Set(candidates.map(({ sha }) => sha))];
|
|
1981
|
-
}
|
|
1982
|
-
async function findLatestReviewBase(notes, workspacePath) {
|
|
1983
|
-
const candidates = getHodorReviewShaCandidates(notes);
|
|
1984
|
-
if (candidates.length === 0) return null;
|
|
1985
|
-
logger.info(`Found ${candidates.length} previous Hodor review marker(s)`);
|
|
1986
|
-
for (const sha of candidates) {
|
|
1987
|
-
try {
|
|
1988
|
-
let objectType;
|
|
1989
|
-
try {
|
|
1990
|
-
({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
|
|
1991
|
-
cwd: workspacePath
|
|
1992
|
-
}));
|
|
1993
|
-
} catch {
|
|
1994
|
-
await exec("git", ["fetch", "--quiet", "origin", sha], {
|
|
1995
|
-
cwd: workspacePath
|
|
1996
|
-
});
|
|
1997
|
-
({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
|
|
1998
|
-
cwd: workspacePath
|
|
1999
|
-
}));
|
|
2000
|
-
}
|
|
2001
|
-
if (objectType.trim() !== "commit") throw new Error("not a commit");
|
|
2002
|
-
try {
|
|
2003
|
-
await exec("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
|
|
2004
|
-
cwd: workspacePath
|
|
2005
|
-
});
|
|
2006
|
-
return { sha, mode: "incremental" };
|
|
2007
|
-
} catch {
|
|
2008
|
-
logger.info(
|
|
2009
|
-
`Previous review SHA ${sha.slice(0, 8)} is not an ancestor; using snapshot delta`
|
|
2010
|
-
);
|
|
2011
|
-
return { sha, mode: "snapshot" };
|
|
2012
|
-
}
|
|
2013
|
-
} catch {
|
|
2014
|
-
logger.info(
|
|
2015
|
-
`Skipping previous review SHA ${sha.slice(0, 8)}; commit is unavailable`
|
|
2016
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
return null;
|
|
2020
|
-
}
|
|
2021
|
-
function getDiffStats(diff) {
|
|
2022
|
-
let files = 0;
|
|
2023
|
-
let additions = 0;
|
|
2024
|
-
let deletions = 0;
|
|
2025
|
-
for (const line of diff.split("\n")) {
|
|
2026
|
-
if (line.startsWith("diff --git ")) files++;
|
|
2027
|
-
else if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
|
2028
|
-
else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
|
|
2029
|
-
}
|
|
2030
|
-
return {
|
|
2031
|
-
files,
|
|
2032
|
-
additions,
|
|
2033
|
-
deletions,
|
|
2034
|
-
bytes: Buffer.byteLength(diff, "utf-8")
|
|
2035
|
-
};
|
|
2036
|
-
}
|
|
2037
|
-
function getChangedFiles(diff) {
|
|
2038
|
-
const files = [];
|
|
2039
|
-
for (const match of diff.matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm)) {
|
|
2040
|
-
files.push(match[2]);
|
|
2041
|
-
}
|
|
2042
|
-
return [...new Set(files)];
|
|
2043
|
-
}
|
|
2044
|
-
var DIFF_SKIP_PATTERNS = [
|
|
2045
|
-
/(?:^|\/)testdata\//,
|
|
2046
|
-
/(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.sum|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/,
|
|
2047
|
-
/\.mdx?$/
|
|
2048
|
-
];
|
|
2049
|
-
function filterEmbeddedDiff(rawDiff) {
|
|
2050
|
-
const skippedFiles = [];
|
|
2051
|
-
const sections = rawDiff.split(/(?=^diff --git )/m);
|
|
2052
|
-
const kept = [];
|
|
2053
|
-
for (const section of sections) {
|
|
2054
|
-
const match = section.match(/^diff --git a\/(.*?) b\//);
|
|
2055
|
-
if (!match) {
|
|
2056
|
-
kept.push(section);
|
|
2057
|
-
continue;
|
|
2058
|
-
}
|
|
2059
|
-
const filePath = match[1];
|
|
2060
|
-
if (DIFF_SKIP_PATTERNS.some((pattern) => pattern.test(filePath))) {
|
|
2061
|
-
skippedFiles.push(filePath);
|
|
2062
|
-
} else {
|
|
2063
|
-
kept.push(section);
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
return { filtered: kept.join(""), skippedFiles };
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
2138
|
// src/review-cache.ts
|
|
2070
2139
|
import { createHash as createHash2 } from "crypto";
|
|
2071
2140
|
import { gzipSync, gunzipSync } from "zlib";
|
|
@@ -2258,7 +2327,22 @@ async function reviewPr(opts) {
|
|
|
2258
2327
|
);
|
|
2259
2328
|
}
|
|
2260
2329
|
} else if (!piModel) {
|
|
2261
|
-
if (parsed.provider === "
|
|
2330
|
+
if (parsed.provider === "amazon-bedrock") {
|
|
2331
|
+
const inferredBaseModelId = stripBedrockRegionalPrefix(parsed.modelId);
|
|
2332
|
+
const baseModelId = parsed.baseModelId ?? inferredBaseModelId;
|
|
2333
|
+
const baseModel = baseModelId ? modelRuntime.getModel(parsed.provider, baseModelId) : void 0;
|
|
2334
|
+
if (!baseModel) {
|
|
2335
|
+
const hint = parsed.baseModelId ? `Base model "${parsed.baseModelId}" was not found in the installed pi-ai registry.` : `Append "@<base-model-id>" if this is a custom inference profile.`;
|
|
2336
|
+
throw new Error(
|
|
2337
|
+
`Unsupported Bedrock model "${parsed.modelId}". ${hint}`
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1";
|
|
2341
|
+
piModel = buildBedrockArnModel({ arn: parsed.modelId, baseModel, region });
|
|
2342
|
+
logger.info(
|
|
2343
|
+
`Regional bedrock model, region: ${region}, capabilities from ${baseModel.id}`
|
|
2344
|
+
);
|
|
2345
|
+
} else if (parsed.provider === "openrouter") {
|
|
2262
2346
|
piModel = {
|
|
2263
2347
|
id: parsed.modelId,
|
|
2264
2348
|
name: parsed.modelId,
|
|
@@ -2459,7 +2543,14 @@ async function reviewPr(opts) {
|
|
|
2459
2543
|
let diffStats = null;
|
|
2460
2544
|
let changedFiles = [];
|
|
2461
2545
|
try {
|
|
2462
|
-
const diffArgs =
|
|
2546
|
+
const diffArgs = getReviewDiffArgs({
|
|
2547
|
+
platform,
|
|
2548
|
+
targetBranch,
|
|
2549
|
+
diffBaseSha,
|
|
2550
|
+
previousReviewSha,
|
|
2551
|
+
reviewDiffMode: previousReviewBase?.mode,
|
|
2552
|
+
localMode
|
|
2553
|
+
});
|
|
2463
2554
|
const { stdout: rawDiff } = await exec("git", diffArgs, { cwd: workspacePath });
|
|
2464
2555
|
const { filtered: filteredDiff, skippedFiles } = filterEmbeddedDiff(rawDiff);
|
|
2465
2556
|
if (skippedFiles.length > 0) {
|
|
@@ -2596,14 +2687,29 @@ async function reviewPr(opts) {
|
|
|
2596
2687
|
resourceLoader
|
|
2597
2688
|
});
|
|
2598
2689
|
activeSession = session;
|
|
2599
|
-
|
|
2690
|
+
const openAiReasoning = thinkingLevel && isOpenAiBedrockModel(piModel) ? thinkingLevel : void 0;
|
|
2691
|
+
if (parsed.provider === "amazon-bedrock" && (bedrockTags || openAiReasoning)) {
|
|
2600
2692
|
const agent = session.agent;
|
|
2601
2693
|
const originalStreamFn = agent.streamFn;
|
|
2602
2694
|
agent.streamFn = (...args) => {
|
|
2603
2695
|
const options = args[2] ?? {};
|
|
2604
|
-
|
|
2696
|
+
const originalOnPayload = options.onPayload;
|
|
2697
|
+
const onPayload = openAiReasoning ? async (payload, model2) => {
|
|
2698
|
+
const transformed = originalOnPayload ? await originalOnPayload(payload, model2) : void 0;
|
|
2699
|
+
return addOpenAiBedrockReasoning(
|
|
2700
|
+
transformed === void 0 ? payload : transformed,
|
|
2701
|
+
openAiReasoning
|
|
2702
|
+
);
|
|
2703
|
+
} : originalOnPayload;
|
|
2704
|
+
return originalStreamFn(args[0], args[1], {
|
|
2705
|
+
...options,
|
|
2706
|
+
...bedrockTags ? { requestMetadata: bedrockTags } : {},
|
|
2707
|
+
...onPayload ? { onPayload } : {}
|
|
2708
|
+
});
|
|
2605
2709
|
};
|
|
2606
|
-
|
|
2710
|
+
if (bedrockTags) {
|
|
2711
|
+
logger.info(`Bedrock cost allocation tags: ${JSON.stringify(bedrockTags)}`);
|
|
2712
|
+
}
|
|
2607
2713
|
}
|
|
2608
2714
|
let turnCount = 0;
|
|
2609
2715
|
let toolCallCount = 0;
|
|
@@ -2825,4 +2931,4 @@ export {
|
|
|
2825
2931
|
postReviewStructured,
|
|
2826
2932
|
reviewPr
|
|
2827
2933
|
};
|
|
2828
|
-
//# sourceMappingURL=chunk-
|
|
2934
|
+
//# sourceMappingURL=chunk-GISFKKMM.js.map
|