@mrkaran/hodor 0.7.1 → 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.
@@ -10,12 +10,13 @@ import {
10
10
  logger,
11
11
  postGitlabCommitStatus,
12
12
  postGitlabMrComment,
13
+ publishGitlabDraftNote,
13
14
  renderMarkdown,
14
15
  renderSummaryMarkdown,
15
16
  resolveGitlabDiscussions,
16
17
  summarizeGitlabNotes,
17
18
  summarizeHodorNotes
18
- } from "./chunk-W7ZUJIFT.js";
19
+ } from "./chunk-DALI4QRT.js";
19
20
  import {
20
21
  relativizeWorkspacePath
21
22
  } from "./chunk-AMUK6GDX.js";
@@ -30,6 +31,135 @@ function getTemplatePath(name) {
30
31
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates", name);
31
32
  }
32
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
+
33
163
  // src/prompt.ts
34
164
  function buildPrReviewPrompt(opts) {
35
165
  const {
@@ -45,6 +175,9 @@ function buildPrReviewPrompt(opts) {
45
175
  localMode = false,
46
176
  singleTurn = false
47
177
  } = opts;
178
+ const rebasedGitlabReview = platform === "gitlab" && reviewDiffMode === "snapshot";
179
+ const hasPreviousReviewDelta = Boolean(previousReviewSha && !rebasedGitlabReview);
180
+ const previousReviewShaText = previousReviewSha ?? "";
48
181
  let templateText;
49
182
  try {
50
183
  templateText = readFileSync(getTemplatePath("review-task.md"), "utf-8");
@@ -61,44 +194,36 @@ function buildPrReviewPrompt(opts) {
61
194
  if (previousReviewSha && !/^[a-f0-9]{40}$/.test(previousReviewSha)) {
62
195
  throw new Error(`Invalid previous review SHA: ${previousReviewSha}`);
63
196
  }
64
- let prDiffCmd;
65
- let gitDiffCmd;
66
- if (previousReviewSha) {
67
- const separator = reviewDiffMode === "snapshot" ? " " : "...";
68
- prDiffCmd = `git --no-pager diff ${previousReviewSha}${separator}HEAD --name-only`;
69
- gitDiffCmd = `git --no-pager diff ${previousReviewSha}${separator}HEAD`;
70
- logger.info(`${reviewDiffMode === "snapshot" ? "Snapshot" : "Incremental"} review: diffing from ${previousReviewSha.slice(0, 8)} to HEAD`);
71
- } else if (localMode) {
72
- prDiffCmd = `git --no-pager diff ${targetBranch} --name-only`;
73
- gitDiffCmd = `git --no-pager diff ${targetBranch}`;
74
- } else if (platform === "github" || platform === "gitea") {
75
- prDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD --name-only`;
76
- gitDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD`;
77
- } else {
78
- if (diffBaseSha) {
79
- prDiffCmd = `git --no-pager diff ${diffBaseSha} HEAD --name-only`;
80
- gitDiffCmd = `git --no-pager diff ${diffBaseSha} HEAD`;
81
- logger.info(`Using GitLab CI_MERGE_REQUEST_DIFF_BASE_SHA: ${diffBaseSha.slice(0, 8)}`);
82
- } else {
83
- prDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD --name-only`;
84
- gitDiffCmd = `git --no-pager diff origin/${targetBranch}...HEAD`;
85
- }
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");
86
211
  }
87
212
  let diffExplanation;
88
- if (previousReviewSha) {
89
- diffExplanation = reviewDiffMode === "snapshot" ? `**Snapshot delta mode**: The MR history was rewritten. This directly compares the last reviewed snapshot (commit \`${previousReviewSha.slice(0, 8)}\`) with the current HEAD; it does not imply ancestry.` : `**Incremental mode**: Showing only changes since the last hodor review (commit \`${previousReviewSha.slice(0, 8)}\`).`;
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)}\`).`;
90
215
  } else if (diffBaseSha) {
91
- diffExplanation = `**GitLab CI Advantage**: This uses GitLab's pre-calculated merge base SHA (\`CI_MERGE_REQUEST_DIFF_BASE_SHA\`), which matches exactly what the GitLab UI shows. This is more reliable than three-dot syntax because it handles force pushes, rebases, and messy histories correctly.`;
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.`;
92
217
  } else {
93
218
  diffExplanation = `**Three-dot syntax** shows ONLY changes introduced on the source branch, excluding changes already on \`${targetBranch}\`.`;
94
219
  }
95
220
  const { contextSection, notesSection, reminderSection } = buildMrSections(mrMetadata);
96
221
  const oneTurn = singleTurn && Boolean(embeddedDiff);
97
222
  let incrementalSection = "";
98
- if (previousReviewSha) {
223
+ if (hasPreviousReviewDelta) {
99
224
  incrementalSection = `## ${reviewDiffMode === "snapshot" ? "Snapshot Delta" : "Incremental Review"} Mode
100
225
 
101
- This is a follow-up review. A previous hodor review was done at commit \`${previousReviewSha.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";
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";
102
227
  }
103
228
  let embeddedDiffSection;
104
229
  let diffFetchInstructions;
@@ -126,7 +251,7 @@ ${changedFiles.map((file) => `- \`${file}\``).join("\n")}
126
251
  startInstruction = "Analyze the diff above and call `submit_review` now, in this turn.";
127
252
  } else {
128
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";
129
- startInstruction = previousReviewSha ? "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`.";
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`.";
130
255
  }
131
256
  } else {
132
257
  embeddedDiffSection = "";
@@ -313,6 +438,31 @@ function parseModelString(model) {
313
438
  }
314
439
  return { provider: "anthropic", modelId: trimmed };
315
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
+ }
316
466
  function extractBedrockArnRegion(arn) {
317
467
  const parts = arn.split(":");
318
468
  return parts.length >= 4 && parts[3] ? parts[3] : "us-east-1";
@@ -1144,6 +1294,7 @@ async function postReviewStructured(opts) {
1144
1294
  let inlineCreated = 0;
1145
1295
  let inlineFailed = 0;
1146
1296
  let inlineDeduplicated = 0;
1297
+ const draftNoteIds = [];
1147
1298
  for (const finding of review.findings) {
1148
1299
  const fingerprint = getFindingFingerprint(finding, workspacePath);
1149
1300
  if (existingByFingerprint.has(fingerprint)) {
@@ -1170,7 +1321,7 @@ ${finding.suggestion}
1170
1321
  \`\`\``;
1171
1322
  }
1172
1323
  try {
1173
- await createGitlabDraftNote(
1324
+ const draftNote = await createGitlabDraftNote(
1174
1325
  parsed.owner,
1175
1326
  parsed.repo,
1176
1327
  parsed.prNumber,
@@ -1182,6 +1333,9 @@ ${finding.suggestion}
1182
1333
  diffRefs
1183
1334
  }
1184
1335
  );
1336
+ if (typeof draftNote.id === "number" || typeof draftNote.id === "string") {
1337
+ draftNoteIds.push(draftNote.id);
1338
+ }
1185
1339
  inlineCreated++;
1186
1340
  } catch (error) {
1187
1341
  const message = error instanceof Error ? error.message : String(error);
@@ -1205,8 +1359,32 @@ ${finding.suggestion}
1205
1359
  draftsPublished = true;
1206
1360
  } catch (error) {
1207
1361
  const message = error instanceof Error ? error.message : String(error);
1208
- errors.push(`draft publish: ${message}`);
1209
1362
  logger.warn(`Failed to bulk publish draft notes: ${message}`);
1363
+ if (draftNoteIds.length === inlineCreated) {
1364
+ let individuallyPublished = 0;
1365
+ for (const draftNoteId of draftNoteIds) {
1366
+ try {
1367
+ await publishGitlabDraftNote(
1368
+ parsed.owner,
1369
+ parsed.repo,
1370
+ parsed.prNumber,
1371
+ draftNoteId,
1372
+ parsed.host
1373
+ );
1374
+ individuallyPublished++;
1375
+ } catch (publishError) {
1376
+ const publishMessage = publishError instanceof Error ? publishError.message : String(publishError);
1377
+ errors.push(`draft publish: ${publishMessage}`);
1378
+ logger.warn(`Failed to publish draft note ${draftNoteId}: ${publishMessage}`);
1379
+ }
1380
+ }
1381
+ draftsPublished = individuallyPublished === inlineCreated;
1382
+ if (draftsPublished) {
1383
+ logger.info(`Published ${individuallyPublished} draft note(s) individually`);
1384
+ }
1385
+ } else {
1386
+ errors.push(`draft publish: ${message}`);
1387
+ }
1210
1388
  }
1211
1389
  }
1212
1390
  let summaryPosted = false;
@@ -1457,6 +1635,31 @@ async function detectCiWorkspace(owner, repo) {
1457
1635
  }
1458
1636
  return { path: null, targetBranch: null, diffBaseSha: null };
1459
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
+ }
1460
1663
  function normalizeGitRemotePath(remoteUrl) {
1461
1664
  const trimmed = remoteUrl.trim().replace(/\.git$/, "");
1462
1665
  try {
@@ -1671,11 +1874,18 @@ async function setupWorkspace(opts) {
1671
1874
  try {
1672
1875
  const ci = await detectCiWorkspace(owner, repo);
1673
1876
  let detectedTargetBranch = ci.targetBranch;
1674
- const detectedDiffBaseSha = ci.diffBaseSha;
1877
+ let detectedDiffBaseSha = ci.diffBaseSha;
1675
1878
  let workspace;
1676
1879
  let isTemporary = false;
1677
1880
  if (ci.path) {
1678
1881
  workspace = ci.path;
1882
+ if (platform === "gitlab" && ci.targetBranch) {
1883
+ detectedDiffBaseSha = await resolveGitlabDiffBaseSha(
1884
+ workspace,
1885
+ ci.targetBranch,
1886
+ detectedDiffBaseSha
1887
+ );
1888
+ }
1679
1889
  if (platform === "github" && !detectedTargetBranch) {
1680
1890
  detectedTargetBranch = await getGithubBaseBranch(workspace, prNumber);
1681
1891
  }
@@ -1925,118 +2135,6 @@ function resolveReviewLocations(review, opts) {
1925
2135
  return { review: { ...review, findings }, stats };
1926
2136
  }
1927
2137
 
1928
- // src/review-diff.ts
1929
- var HODOR_REVIEW_SHA_RE = /^\s*<!--\s*hodor:sha:([a-f0-9]{40})\s*-->/i;
1930
- function getHodorReviewShaCandidates(notes) {
1931
- if (!notes || notes.length === 0) return [];
1932
- const candidates = [];
1933
- for (const [index, note] of notes.entries()) {
1934
- const match = note.body?.match(HODOR_REVIEW_SHA_RE);
1935
- if (!match) continue;
1936
- const createdAtMs = Date.parse(note.created_at ?? "");
1937
- candidates.push({
1938
- sha: match[1],
1939
- createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
1940
- index
1941
- });
1942
- }
1943
- candidates.sort((a, b) => {
1944
- if (a.createdAtMs != null && b.createdAtMs != null && a.createdAtMs !== b.createdAtMs) {
1945
- return b.createdAtMs - a.createdAtMs;
1946
- }
1947
- if (a.createdAtMs != null && b.createdAtMs == null) return -1;
1948
- if (a.createdAtMs == null && b.createdAtMs != null) return 1;
1949
- return a.index - b.index;
1950
- });
1951
- return [...new Set(candidates.map(({ sha }) => sha))];
1952
- }
1953
- async function findLatestReviewBase(notes, workspacePath) {
1954
- const candidates = getHodorReviewShaCandidates(notes);
1955
- if (candidates.length === 0) return null;
1956
- logger.info(`Found ${candidates.length} previous Hodor review marker(s)`);
1957
- for (const sha of candidates) {
1958
- try {
1959
- let objectType;
1960
- try {
1961
- ({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
1962
- cwd: workspacePath
1963
- }));
1964
- } catch {
1965
- await exec("git", ["fetch", "--quiet", "origin", sha], {
1966
- cwd: workspacePath
1967
- });
1968
- ({ stdout: objectType } = await exec("git", ["cat-file", "-t", sha], {
1969
- cwd: workspacePath
1970
- }));
1971
- }
1972
- if (objectType.trim() !== "commit") throw new Error("not a commit");
1973
- try {
1974
- await exec("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
1975
- cwd: workspacePath
1976
- });
1977
- return { sha, mode: "incremental" };
1978
- } catch {
1979
- logger.info(
1980
- `Previous review SHA ${sha.slice(0, 8)} is not an ancestor; using snapshot delta`
1981
- );
1982
- return { sha, mode: "snapshot" };
1983
- }
1984
- } catch {
1985
- logger.info(
1986
- `Skipping previous review SHA ${sha.slice(0, 8)}; commit is unavailable`
1987
- );
1988
- }
1989
- }
1990
- return null;
1991
- }
1992
- function getDiffStats(diff) {
1993
- let files = 0;
1994
- let additions = 0;
1995
- let deletions = 0;
1996
- for (const line of diff.split("\n")) {
1997
- if (line.startsWith("diff --git ")) files++;
1998
- else if (line.startsWith("+") && !line.startsWith("+++")) additions++;
1999
- else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
2000
- }
2001
- return {
2002
- files,
2003
- additions,
2004
- deletions,
2005
- bytes: Buffer.byteLength(diff, "utf-8")
2006
- };
2007
- }
2008
- function getChangedFiles(diff) {
2009
- const files = [];
2010
- for (const match of diff.matchAll(/^diff --git a\/(.*?) b\/(.*?)$/gm)) {
2011
- files.push(match[2]);
2012
- }
2013
- return [...new Set(files)];
2014
- }
2015
- var DIFF_SKIP_PATTERNS = [
2016
- /(?:^|\/)testdata\//,
2017
- /(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|go\.sum|Cargo\.lock|poetry\.lock|Gemfile\.lock|composer\.lock)$/,
2018
- /\.mdx?$/
2019
- ];
2020
- function filterEmbeddedDiff(rawDiff) {
2021
- const skippedFiles = [];
2022
- const sections = rawDiff.split(/(?=^diff --git )/m);
2023
- const kept = [];
2024
- for (const section of sections) {
2025
- const match = section.match(/^diff --git a\/(.*?) b\//);
2026
- if (!match) {
2027
- kept.push(section);
2028
- continue;
2029
- }
2030
- const filePath = match[1];
2031
- if (DIFF_SKIP_PATTERNS.some((pattern) => pattern.test(filePath))) {
2032
- skippedFiles.push(filePath);
2033
- } else {
2034
- kept.push(section);
2035
- }
2036
- }
2037
- return { filtered: kept.join(""), skippedFiles };
2038
- }
2039
-
2040
2138
  // src/review-cache.ts
2041
2139
  import { createHash as createHash2 } from "crypto";
2042
2140
  import { gzipSync, gunzipSync } from "zlib";
@@ -2229,7 +2327,22 @@ async function reviewPr(opts) {
2229
2327
  );
2230
2328
  }
2231
2329
  } else if (!piModel) {
2232
- if (parsed.provider === "openrouter") {
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") {
2233
2346
  piModel = {
2234
2347
  id: parsed.modelId,
2235
2348
  name: parsed.modelId,
@@ -2430,7 +2543,14 @@ async function reviewPr(opts) {
2430
2543
  let diffStats = null;
2431
2544
  let changedFiles = [];
2432
2545
  try {
2433
- const diffArgs = previousReviewSha ? previousReviewBase?.mode === "snapshot" ? ["--no-pager", "diff", previousReviewSha, "HEAD"] : ["--no-pager", "diff", `${previousReviewSha}...HEAD`] : diffBaseSha ? ["--no-pager", "diff", diffBaseSha, "HEAD"] : localMode ? ["--no-pager", "diff", targetBranch] : ["--no-pager", "diff", `origin/${targetBranch}...HEAD`];
2546
+ const diffArgs = getReviewDiffArgs({
2547
+ platform,
2548
+ targetBranch,
2549
+ diffBaseSha,
2550
+ previousReviewSha,
2551
+ reviewDiffMode: previousReviewBase?.mode,
2552
+ localMode
2553
+ });
2434
2554
  const { stdout: rawDiff } = await exec("git", diffArgs, { cwd: workspacePath });
2435
2555
  const { filtered: filteredDiff, skippedFiles } = filterEmbeddedDiff(rawDiff);
2436
2556
  if (skippedFiles.length > 0) {
@@ -2567,14 +2687,29 @@ async function reviewPr(opts) {
2567
2687
  resourceLoader
2568
2688
  });
2569
2689
  activeSession = session;
2570
- if (bedrockTags && parsed.provider === "amazon-bedrock") {
2690
+ const openAiReasoning = thinkingLevel && isOpenAiBedrockModel(piModel) ? thinkingLevel : void 0;
2691
+ if (parsed.provider === "amazon-bedrock" && (bedrockTags || openAiReasoning)) {
2571
2692
  const agent = session.agent;
2572
2693
  const originalStreamFn = agent.streamFn;
2573
2694
  agent.streamFn = (...args) => {
2574
2695
  const options = args[2] ?? {};
2575
- return originalStreamFn(args[0], args[1], { ...options, requestMetadata: bedrockTags });
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
+ });
2576
2709
  };
2577
- logger.info(`Bedrock cost allocation tags: ${JSON.stringify(bedrockTags)}`);
2710
+ if (bedrockTags) {
2711
+ logger.info(`Bedrock cost allocation tags: ${JSON.stringify(bedrockTags)}`);
2712
+ }
2578
2713
  }
2579
2714
  let turnCount = 0;
2580
2715
  let toolCallCount = 0;
@@ -2796,4 +2931,4 @@ export {
2796
2931
  postReviewStructured,
2797
2932
  reviewPr
2798
2933
  };
2799
- //# sourceMappingURL=chunk-VERO4XYJ.js.map
2934
+ //# sourceMappingURL=chunk-GISFKKMM.js.map