@mrkaran/hodor 0.5.0 → 0.6.2-rc.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.
- package/README.md +149 -21
- package/dist/chunk-AMUK6GDX.js +23 -0
- package/dist/chunk-AMUK6GDX.js.map +1 -0
- package/dist/chunk-DVJVQTVW.js +731 -0
- package/dist/chunk-DVJVQTVW.js.map +1 -0
- package/dist/{chunk-QGUJENIG.js → chunk-MRMLGIXO.js} +1100 -472
- package/dist/chunk-MRMLGIXO.js.map +1 -0
- package/dist/cli.js +111 -22
- package/dist/cli.js.map +1 -1
- package/dist/codequality-DTJK2LGF.js +42 -0
- package/dist/codequality-DTJK2LGF.js.map +1 -0
- package/dist/gitlab-5TWHYHEP.js +33 -0
- package/dist/gitlab-5TWHYHEP.js.map +1 -0
- package/dist/index.d.ts +24 -13
- package/dist/index.js +5 -2
- package/package.json +13 -13
- package/templates/tool-review.md +41 -5
- package/dist/chunk-QGUJENIG.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
detectPlatform,
|
|
4
|
+
parsePrUrl,
|
|
5
|
+
postGitlabReviewCommitStatus,
|
|
4
6
|
postReviewComment,
|
|
7
|
+
postReviewStructured,
|
|
5
8
|
pushMetrics,
|
|
9
|
+
reviewPr
|
|
10
|
+
} from "./chunk-MRMLGIXO.js";
|
|
11
|
+
import {
|
|
6
12
|
renderMarkdown,
|
|
7
|
-
reviewPr,
|
|
8
13
|
setLogLevel
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-DVJVQTVW.js";
|
|
15
|
+
import "./chunk-AMUK6GDX.js";
|
|
10
16
|
|
|
11
17
|
// src/cli.ts
|
|
12
18
|
import { Command } from "commander";
|
|
@@ -14,14 +20,14 @@ import chalk from "chalk";
|
|
|
14
20
|
import "dotenv/config";
|
|
15
21
|
var program = new Command();
|
|
16
22
|
program.name("hodor").description(
|
|
17
|
-
"AI-powered code review agent for GitHub PRs, GitLab MRs, and local diffs.\n\nHodor uses an AI agent that clones the repository, checks out the PR branch,\nand analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\n\nFor local reviews, use --local with --diff-against to review changes in your current git repository."
|
|
18
|
-
).version("0.
|
|
23
|
+
"AI-powered code review agent for GitHub PRs, GitLab MRs, Gitea/Forgejo PRs, and local diffs.\n\nHodor uses an AI agent that clones the repository, checks out the PR branch,\nand analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\n\nFor local reviews, use --local with --diff-against to review changes in your current git repository."
|
|
24
|
+
).version("0.6.1").argument("[pr-url]", "URL of the GitHub PR, GitLab MR, or Gitea/Forgejo PR to review (optional with --local)").option(
|
|
19
25
|
"--model <model>",
|
|
20
|
-
"LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929,
|
|
26
|
+
"LLM model to use as provider/model-id (e.g., anthropic/claude-sonnet-4-5-20250929, openrouter/moonshotai/kimi-k2.6)",
|
|
21
27
|
"anthropic/claude-sonnet-4-5-20250929"
|
|
22
28
|
).option(
|
|
23
29
|
"--reasoning-effort <level>",
|
|
24
|
-
"Reasoning effort level: low, medium, high, xhigh"
|
|
30
|
+
"Reasoning effort level: minimal, low, medium, high, xhigh"
|
|
25
31
|
).option("-v, --verbose", "Enable verbose logging", false).option(
|
|
26
32
|
"--post",
|
|
27
33
|
"Post the review directly to the PR/MR as a comment",
|
|
@@ -32,6 +38,17 @@ program.name("hodor").description(
|
|
|
32
38
|
).option(
|
|
33
39
|
"--workspace <dir>",
|
|
34
40
|
"Workspace directory (creates temp dir if not specified)"
|
|
41
|
+
).option(
|
|
42
|
+
"--review-style <style>",
|
|
43
|
+
"How to post reviews on GitLab: summary (single comment), inline (diff comments), hybrid (both). Default: hybrid.",
|
|
44
|
+
"hybrid"
|
|
45
|
+
).option(
|
|
46
|
+
"--code-quality <path>",
|
|
47
|
+
"Write a gl-code-quality-report.json CodeClimate artifact to this path"
|
|
48
|
+
).option(
|
|
49
|
+
"--commit-status",
|
|
50
|
+
"Post a pass/fail commit status to the MR head SHA",
|
|
51
|
+
false
|
|
35
52
|
).option(
|
|
36
53
|
"--ultrathink",
|
|
37
54
|
"Enable maximum reasoning effort with extended thinking budget",
|
|
@@ -58,6 +75,9 @@ program.name("hodor").description(
|
|
|
58
75
|
const prompt = cmdOpts.prompt;
|
|
59
76
|
const promptFile = cmdOpts.promptFile;
|
|
60
77
|
const workspace = cmdOpts.workspace;
|
|
78
|
+
const reviewStyle = cmdOpts.reviewStyle;
|
|
79
|
+
const codeQuality = cmdOpts.codeQuality;
|
|
80
|
+
const commitStatus = cmdOpts.commitStatus;
|
|
61
81
|
const ultrathink = cmdOpts.ultrathink;
|
|
62
82
|
const bedrockTagsRaw = cmdOpts.bedrockTags;
|
|
63
83
|
const prometheusPush = cmdOpts.prometheusPush;
|
|
@@ -71,11 +91,15 @@ program.name("hodor").description(
|
|
|
71
91
|
console.error(chalk.red("Error: --post is not supported in --local mode (no remote to post to)"));
|
|
72
92
|
process.exit(1);
|
|
73
93
|
}
|
|
74
|
-
|
|
94
|
+
if (!["summary", "inline", "hybrid"].includes(reviewStyle ?? "hybrid")) {
|
|
95
|
+
console.error(chalk.red("Error: --review-style must be one of: summary, inline, hybrid"));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS || process.env.GITEA_ACTIONS || process.env.FORGEJO_ACTIONS);
|
|
75
99
|
if (verbose) setLogLevel("debug");
|
|
76
100
|
else if (isCI) setLogLevel("info");
|
|
77
101
|
if (ultrathink) {
|
|
78
|
-
reasoningEffort = "
|
|
102
|
+
reasoningEffort = "xhigh";
|
|
79
103
|
}
|
|
80
104
|
let bedrockTags = null;
|
|
81
105
|
if (bedrockTagsRaw) {
|
|
@@ -157,8 +181,13 @@ program.name("hodor").description(
|
|
|
157
181
|
}
|
|
158
182
|
try {
|
|
159
183
|
let platform = "local";
|
|
184
|
+
let metricsProject;
|
|
185
|
+
let metricsMrIid;
|
|
160
186
|
if (!localMode && prUrl) {
|
|
161
187
|
platform = detectPlatform(prUrl);
|
|
188
|
+
const parsedPr = parsePrUrl(prUrl);
|
|
189
|
+
metricsProject = `${parsedPr.owner}/${parsedPr.repo}`;
|
|
190
|
+
metricsMrIid = String(parsedPr.prNumber);
|
|
162
191
|
const githubToken = process.env.GITHUB_TOKEN;
|
|
163
192
|
const gitlabToken = process.env.GITLAB_TOKEN ?? process.env.GITLAB_PRIVATE_TOKEN ?? process.env.CI_JOB_TOKEN;
|
|
164
193
|
if (platform === "github" && !githubToken) {
|
|
@@ -167,6 +196,12 @@ program.name("hodor").description(
|
|
|
167
196
|
} else if (platform === "gitlab" && !gitlabToken) {
|
|
168
197
|
console.error(chalk.yellow("Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope)."));
|
|
169
198
|
console.error(chalk.dim(" Export GITLAB_TOKEN and optionally GITLAB_HOST.\n"));
|
|
199
|
+
} else if (platform === "gitea") {
|
|
200
|
+
const giteaToken = process.env.GITEA_TOKEN ?? process.env.FORGEJO_TOKEN;
|
|
201
|
+
if (!giteaToken) {
|
|
202
|
+
console.error(chalk.yellow("Warning: No Gitea/Forgejo token detected. Set GITEA_TOKEN for authentication."));
|
|
203
|
+
console.error(chalk.dim(" Export GITEA_TOKEN (or FORGEJO_TOKEN) for API access.\n"));
|
|
204
|
+
}
|
|
170
205
|
}
|
|
171
206
|
}
|
|
172
207
|
log(`
|
|
@@ -185,7 +220,7 @@ ${chalk.bold.cyan("Hodor - AI Code Review Agent")}`);
|
|
|
185
220
|
}
|
|
186
221
|
log();
|
|
187
222
|
streamLog(chalk.dim("\u25B6 Setting up workspace..."));
|
|
188
|
-
const { review, metricsFooter, headSha, metrics } = await reviewPr({
|
|
223
|
+
const { review, metricsFooter, headSha, metrics, workspacePath } = await reviewPr({
|
|
189
224
|
prUrl: localMode ? void 0 : prUrl,
|
|
190
225
|
model,
|
|
191
226
|
reasoningEffort,
|
|
@@ -201,18 +236,68 @@ ${chalk.bold.cyan("Hodor - AI Code Review Agent")}`);
|
|
|
201
236
|
});
|
|
202
237
|
const reviewText = renderMarkdown(review);
|
|
203
238
|
streamLog(chalk.green("\u2714 Review complete!"));
|
|
239
|
+
if (codeQuality) {
|
|
240
|
+
try {
|
|
241
|
+
const { formatCodeQualityReport } = await import("./codequality-DTJK2LGF.js");
|
|
242
|
+
const { writeFileSync } = await import("fs");
|
|
243
|
+
writeFileSync(
|
|
244
|
+
codeQuality,
|
|
245
|
+
formatCodeQualityReport(review, process.env.CI_PROJECT_DIR ?? workspacePath),
|
|
246
|
+
"utf-8"
|
|
247
|
+
);
|
|
248
|
+
log(chalk.dim(`Wrote code quality report to ${codeQuality}`));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
log(chalk.yellow(`Failed to write code quality report: ${err}`));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
204
253
|
if (post && prUrl) {
|
|
205
254
|
log(chalk.cyan("\nPosting review to PR/MR..."));
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
255
|
+
const platform2 = detectPlatform(prUrl);
|
|
256
|
+
const useStructured = platform2 === "gitlab" && reviewStyle !== "summary";
|
|
257
|
+
let result;
|
|
258
|
+
if (useStructured) {
|
|
259
|
+
result = await postReviewStructured({
|
|
260
|
+
prUrl,
|
|
261
|
+
review,
|
|
262
|
+
model,
|
|
263
|
+
metricsFooter,
|
|
264
|
+
reviewStyle: reviewStyle ?? "hybrid",
|
|
265
|
+
commitStatus,
|
|
266
|
+
headSha,
|
|
267
|
+
workspacePath
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
result = await postReviewComment({
|
|
271
|
+
prUrl,
|
|
272
|
+
reviewText,
|
|
273
|
+
model,
|
|
274
|
+
metricsFooter,
|
|
275
|
+
headSha
|
|
276
|
+
});
|
|
277
|
+
if (platform2 === "gitlab" && commitStatus) {
|
|
278
|
+
try {
|
|
279
|
+
const { getGitlabMrDiffRefs } = await import("./gitlab-5TWHYHEP.js");
|
|
280
|
+
const parsed = parsePrUrl(prUrl);
|
|
281
|
+
const diffRefs = await getGitlabMrDiffRefs(
|
|
282
|
+
parsed.owner,
|
|
283
|
+
parsed.repo,
|
|
284
|
+
parsed.prNumber,
|
|
285
|
+
parsed.host
|
|
286
|
+
);
|
|
287
|
+
await postGitlabReviewCommitStatus(parsed, review, diffRefs);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
result = {
|
|
290
|
+
success: false,
|
|
291
|
+
platform: "gitlab",
|
|
292
|
+
mrNumber: parsePrUrl(prUrl).prNumber,
|
|
293
|
+
error: `Failed to post commit status: ${err instanceof Error ? err.message : err}`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
213
298
|
if (result.success) {
|
|
214
299
|
log(chalk.bold.green("Review posted successfully!"));
|
|
215
|
-
log(chalk.dim(` ${
|
|
300
|
+
log(chalk.dim(` ${platform2 === "gitlab" ? "MR" : "PR"}: ${prUrl}`));
|
|
216
301
|
} else {
|
|
217
302
|
log(chalk.bold.red(`Failed to post review: ${result.error}`));
|
|
218
303
|
log(chalk.yellow("\nReview output:\n"));
|
|
@@ -226,14 +311,18 @@ ${chalk.bold.cyan("Hodor - AI Code Review Agent")}`);
|
|
|
226
311
|
}
|
|
227
312
|
}
|
|
228
313
|
if (prometheusPush) {
|
|
314
|
+
const labels = {
|
|
315
|
+
platform,
|
|
316
|
+
model,
|
|
317
|
+
verdict: review.overall_correctness === "patch is correct" ? "correct" : "incorrect"
|
|
318
|
+
};
|
|
319
|
+
if (metricsProject) labels.project = metricsProject;
|
|
320
|
+
if (metricsMrIid) labels.mr_iid = metricsMrIid;
|
|
229
321
|
await pushMetrics({
|
|
230
322
|
pushgatewayUrl: prometheusPush,
|
|
231
323
|
metrics,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
model,
|
|
235
|
-
verdict: review.overall_correctness === "patch is correct" ? "correct" : "incorrect"
|
|
236
|
-
}
|
|
324
|
+
findings: review.findings,
|
|
325
|
+
labels
|
|
237
326
|
});
|
|
238
327
|
}
|
|
239
328
|
} catch (err) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport \"dotenv/config\";\n\nimport { detectPlatform, postReviewComment, reviewPr } from \"./agent.js\";\nimport type { AgentProgressEvent } from \"./agent.js\";\nimport { renderMarkdown } from \"./render.js\";\nimport { pushMetrics } from \"./metrics.js\";\nimport { setLogLevel } from \"./utils/logger.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"hodor\")\n .description(\n \"AI-powered code review agent for GitHub PRs, GitLab MRs, and local diffs.\\n\\n\" +\n \"Hodor uses an AI agent that clones the repository, checks out the PR branch,\\n\" +\n \"and analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\\n\\n\" +\n \"For local reviews, use --local with --diff-against to review changes in your current git repository.\",\n )\n .version(\"0.4.1\")\n .argument(\"[pr-url]\", \"URL of the GitHub PR or GitLab MR to review (optional with --local)\")\n .option(\n \"--model <model>\",\n \"LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-5)\",\n \"anthropic/claude-sonnet-4-5-20250929\",\n )\n .option(\n \"--reasoning-effort <level>\",\n \"Reasoning effort level: low, medium, high, xhigh\",\n )\n .option(\"-v, --verbose\", \"Enable verbose logging\", false)\n .option(\n \"--post\",\n \"Post the review directly to the PR/MR as a comment\",\n false,\n )\n .option(\"--prompt <text>\", \"Custom inline prompt text\")\n .option(\n \"--prompt-file <path>\",\n \"Path to file containing custom prompt instructions\",\n )\n .option(\n \"--workspace <dir>\",\n \"Workspace directory (creates temp dir if not specified)\",\n )\n .option(\n \"--ultrathink\",\n \"Enable maximum reasoning effort with extended thinking budget\",\n false,\n )\n .option(\n \"--bedrock-tags <json>\",\n \"JSON object of cost allocation tags for Bedrock requests (e.g., '{\\\"team\\\":\\\"platform\\\"}')\",\n )\n .option(\n \"--prometheus-push <url>\",\n \"Push review metrics to a Prometheus Pushgateway URL\",\n )\n .option(\n \"--local\",\n \"Review local changes in the current directory (no PR URL required)\",\n false,\n )\n .option(\n \"--diff-against <ref>\",\n \"Git ref to diff against in local mode (e.g., origin/main, HEAD~1)\",\n \"origin/main\",\n )\n .action(async (prUrl: string | undefined, cmdOpts: Record<string, unknown>) => {\n const verbose = cmdOpts.verbose as boolean;\n const post = cmdOpts.post as boolean;\n const model = cmdOpts.model as string;\n let reasoningEffort = cmdOpts.reasoningEffort as string | undefined;\n const prompt = cmdOpts.prompt as string | undefined;\n const promptFile = cmdOpts.promptFile as string | undefined;\n const workspace = cmdOpts.workspace as string | undefined;\n const ultrathink = cmdOpts.ultrathink as boolean;\n const bedrockTagsRaw = cmdOpts.bedrockTags as string | undefined;\n const prometheusPush = cmdOpts.prometheusPush as string | undefined;\n const localMode = cmdOpts.local as boolean;\n const diffAgainst = cmdOpts.diffAgainst as string;\n\n if (!localMode && !prUrl) {\n console.error(chalk.red(\"Error: pr-url is required unless --local is specified\"));\n process.exit(1);\n }\n if (localMode && post) {\n console.error(chalk.red(\"Error: --post is not supported in --local mode (no remote to post to)\"));\n process.exit(1);\n }\n\n // Auto-detect CI environment\n const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS);\n\n if (verbose) setLogLevel(\"debug\");\n else if (isCI) setLogLevel(\"info\");\n\n // Handle ultrathink\n if (ultrathink) {\n reasoningEffort = \"high\";\n }\n\n // Parse Bedrock cost allocation tags\n let bedrockTags: Record<string, string> | null = null;\n if (bedrockTagsRaw) {\n try {\n bedrockTags = JSON.parse(bedrockTagsRaw) as Record<string, string>;\n } catch {\n console.error(chalk.red(\"Error: --bedrock-tags must be valid JSON\"));\n process.exit(1);\n }\n }\n\n const log = console.log;\n const logStream = process.stdout;\n\n const toolIcons: Record<string, string> = {\n bash: \"$\",\n read: \"cat\",\n grep: \"grep\",\n find: \"find\",\n ls: \"ls\",\n };\n\n /** Write a line to the log stream */\n function streamLog(msg: string): void {\n logStream.write(`${msg}\\n`);\n }\n\n /** Write inline text (no newline) for streaming deltas */\n function streamWrite(text: string): void {\n process.stderr.write(text);\n }\n\n function handleEvent(event: AgentProgressEvent): void {\n switch (event.type) {\n case \"agent_start\":\n streamLog(chalk.dim(\"▶ Agent started\"));\n break;\n case \"turn_start\":\n streamLog(chalk.dim(`\\n── Turn ${event.turnIndex ?? \"?\"} ──`));\n break;\n case \"tool_start\": {\n const icon = toolIcons[event.toolName ?? \"\"] ?? event.toolName;\n const preview = event.toolArgs ? ` ${event.toolArgs}` : \"\";\n const maxLen = 160;\n const truncated = preview.length > maxLen ? preview.slice(0, maxLen) + \"…\" : preview;\n streamLog(chalk.green(` ${icon}${truncated}`));\n break;\n }\n case \"tool_end\": {\n if (event.isError) {\n streamLog(chalk.red(` ✗ error`));\n }\n if (event.result) {\n const lines = event.result.split(\"\\n\");\n const maxLines = verbose ? 15 : 6;\n const maxChars = verbose ? 400 : 200;\n let chars = 0;\n for (let i = 0; i < Math.min(lines.length, maxLines); i++) {\n const line = lines[i];\n if (chars + line.length > maxChars) {\n streamLog(chalk.dim(` …(${lines.length - i} more lines)`));\n break;\n }\n streamLog(chalk.dim(` ${line}`));\n chars += line.length;\n }\n }\n break;\n }\n case \"text_delta\":\n if (verbose && event.delta) {\n streamWrite(event.delta);\n }\n break;\n case \"thinking_delta\":\n // Only show reasoning in verbose mode\n if (verbose && event.delta) {\n streamWrite(chalk.dim(event.delta));\n }\n break;\n case \"agent_end\":\n streamLog(chalk.dim(\"\\n▶ Extracting review...\"));\n break;\n }\n }\n\n try {\n // Detect platform and warn about missing tokens\n let platform: string = \"local\";\n if (!localMode && prUrl) {\n platform = detectPlatform(prUrl);\n const githubToken = process.env.GITHUB_TOKEN;\n const gitlabToken =\n process.env.GITLAB_TOKEN ??\n process.env.GITLAB_PRIVATE_TOKEN ??\n process.env.CI_JOB_TOKEN;\n\n if (platform === \"github\" && !githubToken) {\n console.error(chalk.yellow(\"Warning: GITHUB_TOKEN not set. You may encounter rate limits.\"));\n console.error(chalk.dim(\" Set GITHUB_TOKEN or run: gh auth login\\n\"));\n } else if (platform === \"gitlab\" && !gitlabToken) {\n console.error(chalk.yellow(\"Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope).\"));\n console.error(chalk.dim(\" Export GITLAB_TOKEN and optionally GITLAB_HOST.\\n\"));\n }\n }\n\n log(`\\n${chalk.bold.cyan(\"Hodor - AI Code Review Agent\")}`);\n if (localMode) {\n log(chalk.dim(`Mode: Local diff review`));\n log(chalk.dim(`Diff against: ${diffAgainst}`));\n log(chalk.dim(`Workspace: ${workspace ?? process.cwd()}`));\n } else {\n log(chalk.dim(`Platform: ${platform.toUpperCase()}`));\n log(chalk.dim(`PR URL: ${prUrl}`));\n }\n log(chalk.dim(`Model: ${model}`));\n if (reasoningEffort) {\n log(chalk.dim(`Reasoning Effort: ${reasoningEffort}`));\n }\n log();\n\n streamLog(chalk.dim(\"▶ Setting up workspace...\"));\n const { review, metricsFooter, headSha, metrics } = await reviewPr({\n prUrl: localMode ? undefined : prUrl,\n model,\n reasoningEffort,\n customPrompt: prompt,\n promptFile,\n cleanup: !workspace,\n workspaceDir: workspace,\n includeMetricsFooter: post && !localMode,\n onEvent: handleEvent,\n bedrockTags,\n localMode,\n diffAgainst,\n });\n const reviewText = renderMarkdown(review);\n\n streamLog(chalk.green(\"✔ Review complete!\"));\n\n if (post && prUrl) {\n log(chalk.cyan(\"\\nPosting review to PR/MR...\"));\n\n const result = await postReviewComment({\n prUrl,\n reviewText,\n model,\n metricsFooter,\n headSha,\n });\n\n if (result.success) {\n log(chalk.bold.green(\"Review posted successfully!\"));\n log(chalk.dim(` ${platform === \"github\" ? \"PR\" : \"MR\"}: ${prUrl}`));\n } else {\n log(chalk.bold.red(`Failed to post review: ${result.error}`));\n log(chalk.yellow(\"\\nReview output:\\n\"));\n console.log(reviewText);\n }\n } else {\n log(chalk.bold.green(\"Review Complete\\n\"));\n console.log(reviewText);\n if (!localMode) {\n log(chalk.dim(\"\\nTip: Use --post to automatically post this review to the PR/MR\"));\n }\n }\n\n // Push metrics to Prometheus Pushgateway (best-effort, never fails the run)\n if (prometheusPush) {\n await pushMetrics({\n pushgatewayUrl: prometheusPush,\n metrics,\n labels: {\n platform,\n model,\n verdict: review.overall_correctness === \"patch is correct\" ? \"correct\" : \"incorrect\",\n },\n });\n }\n } catch (err) {\n streamLog(chalk.red(\"✗ Review failed\"));\n console.error(\n chalk.bold.red(`\\nError: ${err instanceof Error ? err.message : err}`),\n );\n if (verbose && err instanceof Error && err.stack) {\n console.error(chalk.dim(err.stack));\n }\n process.exit(1);\n }\n });\n\nprogram.parse();\n"],"mappings":";;;;;;;;;;;AAEA,SAAS,eAAe;AACxB,OAAO,WAAW;AAClB,OAAO;AAQP,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,OAAO,EACZ;AAAA,EACC;AAIF,EACC,QAAQ,OAAO,EACf,SAAS,YAAY,qEAAqE,EAC1F;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,iBAAiB,0BAA0B,KAAK,EACvD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC,OAAO,mBAAmB,2BAA2B,EACrD;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA,EACA;AACF,EACC,OAAO,OAAO,OAA2B,YAAqC;AAC7E,QAAM,UAAU,QAAQ;AACxB,QAAM,OAAO,QAAQ;AACrB,QAAM,QAAQ,QAAQ;AACtB,MAAI,kBAAkB,QAAQ;AAC9B,QAAM,SAAS,QAAQ;AACvB,QAAM,aAAa,QAAQ;AAC3B,QAAM,YAAY,QAAQ;AAC1B,QAAM,aAAa,QAAQ;AAC3B,QAAM,iBAAiB,QAAQ;AAC/B,QAAM,iBAAiB,QAAQ;AAC/B,QAAM,YAAY,QAAQ;AAC1B,QAAM,cAAc,QAAQ;AAE5B,MAAI,CAAC,aAAa,CAAC,OAAO;AACxB,YAAQ,MAAM,MAAM,IAAI,uDAAuD,CAAC;AAChF,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,MAAI,aAAa,MAAM;AACrB,YAAQ,MAAM,MAAM,IAAI,uEAAuE,CAAC;AAChG,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,OAAO,CAAC,EAAE,QAAQ,IAAI,MAAM,QAAQ,IAAI,aAAa,QAAQ,IAAI;AAEvE,MAAI,QAAS,aAAY,OAAO;AAAA,WACvB,KAAM,aAAY,MAAM;AAGjC,MAAI,YAAY;AACd,sBAAkB;AAAA,EACpB;AAGA,MAAI,cAA6C;AACjD,MAAI,gBAAgB;AAClB,QAAI;AACF,oBAAc,KAAK,MAAM,cAAc;AAAA,IACzC,QAAQ;AACN,cAAQ,MAAM,MAAM,IAAI,0CAA0C,CAAC;AACnE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ;AACpB,QAAM,YAAY,QAAQ;AAE1B,QAAM,YAAoC;AAAA,IACxC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,IAAI;AAAA,EACN;AAGA,WAAS,UAAU,KAAmB;AACpC,cAAU,MAAM,GAAG,GAAG;AAAA,CAAI;AAAA,EAC5B;AAGA,WAAS,YAAY,MAAoB;AACvC,YAAQ,OAAO,MAAM,IAAI;AAAA,EAC3B;AAEA,WAAS,YAAY,OAAiC;AACpD,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK;AACH,kBAAU,MAAM,IAAI,sBAAiB,CAAC;AACtC;AAAA,MACF,KAAK;AACH,kBAAU,MAAM,IAAI;AAAA,oBAAa,MAAM,aAAa,GAAG,eAAK,CAAC;AAC7D;AAAA,MACF,KAAK,cAAc;AACjB,cAAM,OAAO,UAAU,MAAM,YAAY,EAAE,KAAK,MAAM;AACtD,cAAM,UAAU,MAAM,WAAW,IAAI,MAAM,QAAQ,KAAK;AACxD,cAAM,SAAS;AACf,cAAM,YAAY,QAAQ,SAAS,SAAS,QAAQ,MAAM,GAAG,MAAM,IAAI,WAAM;AAC7E,kBAAU,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS,EAAE,CAAC;AAC9C;AAAA,MACF;AAAA,MACA,KAAK,YAAY;AACf,YAAI,MAAM,SAAS;AACjB,oBAAU,MAAM,IAAI,gBAAW,CAAC;AAAA,QAClC;AACA,YAAI,MAAM,QAAQ;AAChB,gBAAM,QAAQ,MAAM,OAAO,MAAM,IAAI;AACrC,gBAAM,WAAW,UAAU,KAAK;AAChC,gBAAM,WAAW,UAAU,MAAM;AACjC,cAAI,QAAQ;AACZ,mBAAS,IAAI,GAAG,IAAI,KAAK,IAAI,MAAM,QAAQ,QAAQ,GAAG,KAAK;AACzD,kBAAM,OAAO,MAAM,CAAC;AACpB,gBAAI,QAAQ,KAAK,SAAS,UAAU;AAClC,wBAAU,MAAM,IAAI,cAAS,MAAM,SAAS,CAAC,cAAc,CAAC;AAC5D;AAAA,YACF;AACA,sBAAU,MAAM,IAAI,OAAO,IAAI,EAAE,CAAC;AAClC,qBAAS,KAAK;AAAA,UAChB;AAAA,QACF;AACA;AAAA,MACF;AAAA,MACA,KAAK;AACH,YAAI,WAAW,MAAM,OAAO;AAC1B,sBAAY,MAAM,KAAK;AAAA,QACzB;AACA;AAAA,MACF,KAAK;AAEH,YAAI,WAAW,MAAM,OAAO;AAC1B,sBAAY,MAAM,IAAI,MAAM,KAAK,CAAC;AAAA,QACpC;AACA;AAAA,MACF,KAAK;AACH,kBAAU,MAAM,IAAI,+BAA0B,CAAC;AAC/C;AAAA,IACJ;AAAA,EACF;AAEA,MAAI;AAEF,QAAI,WAAmB;AACvB,QAAI,CAAC,aAAa,OAAO;AACvB,iBAAW,eAAe,KAAK;AAC/B,YAAM,cAAc,QAAQ,IAAI;AAChC,YAAM,cACJ,QAAQ,IAAI,gBACZ,QAAQ,IAAI,wBACZ,QAAQ,IAAI;AAEd,UAAI,aAAa,YAAY,CAAC,aAAa;AACzC,gBAAQ,MAAM,MAAM,OAAO,+DAA+D,CAAC;AAC3F,gBAAQ,MAAM,MAAM,IAAI,4CAA4C,CAAC;AAAA,MACvE,WAAW,aAAa,YAAY,CAAC,aAAa;AAChD,gBAAQ,MAAM,MAAM,OAAO,kEAAkE,CAAC;AAC9F,gBAAQ,MAAM,MAAM,IAAI,qDAAqD,CAAC;AAAA,MAChF;AAAA,IACF;AAEA,QAAI;AAAA,EAAK,MAAM,KAAK,KAAK,8BAA8B,CAAC,EAAE;AAC1D,QAAI,WAAW;AACb,UAAI,MAAM,IAAI,yBAAyB,CAAC;AACxC,UAAI,MAAM,IAAI,iBAAiB,WAAW,EAAE,CAAC;AAC7C,UAAI,MAAM,IAAI,cAAc,aAAa,QAAQ,IAAI,CAAC,EAAE,CAAC;AAAA,IAC3D,OAAO;AACL,UAAI,MAAM,IAAI,aAAa,SAAS,YAAY,CAAC,EAAE,CAAC;AACpD,UAAI,MAAM,IAAI,WAAW,KAAK,EAAE,CAAC;AAAA,IACnC;AACA,QAAI,MAAM,IAAI,UAAU,KAAK,EAAE,CAAC;AAChC,QAAI,iBAAiB;AACnB,UAAI,MAAM,IAAI,qBAAqB,eAAe,EAAE,CAAC;AAAA,IACvD;AACA,QAAI;AAEJ,cAAU,MAAM,IAAI,gCAA2B,CAAC;AAChD,UAAM,EAAE,QAAQ,eAAe,SAAS,QAAQ,IAAI,MAAM,SAAS;AAAA,MACjE,OAAO,YAAY,SAAY;AAAA,MAC/B;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd;AAAA,MACA,SAAS,CAAC;AAAA,MACV,cAAc;AAAA,MACd,sBAAsB,QAAQ,CAAC;AAAA,MAC/B,SAAS;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,aAAa,eAAe,MAAM;AAExC,cAAU,MAAM,MAAM,yBAAoB,CAAC;AAE3C,QAAI,QAAQ,OAAO;AACjB,UAAI,MAAM,KAAK,8BAA8B,CAAC;AAE9C,YAAM,SAAS,MAAM,kBAAkB;AAAA,QACrC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,UAAI,OAAO,SAAS;AAClB,YAAI,MAAM,KAAK,MAAM,6BAA6B,CAAC;AACnD,YAAI,MAAM,IAAI,KAAK,aAAa,WAAW,OAAO,IAAI,KAAK,KAAK,EAAE,CAAC;AAAA,MACrE,OAAO;AACL,YAAI,MAAM,KAAK,IAAI,0BAA0B,OAAO,KAAK,EAAE,CAAC;AAC5D,YAAI,MAAM,OAAO,oBAAoB,CAAC;AACtC,gBAAQ,IAAI,UAAU;AAAA,MACxB;AAAA,IACF,OAAO;AACL,UAAI,MAAM,KAAK,MAAM,mBAAmB,CAAC;AACzC,cAAQ,IAAI,UAAU;AACtB,UAAI,CAAC,WAAW;AACd,YAAI,MAAM,IAAI,kEAAkE,CAAC;AAAA,MACnF;AAAA,IACF;AAGA,QAAI,gBAAgB;AAClB,YAAM,YAAY;AAAA,QAChB,gBAAgB;AAAA,QAChB;AAAA,QACA,QAAQ;AAAA,UACN;AAAA,UACA;AAAA,UACA,SAAS,OAAO,wBAAwB,qBAAqB,YAAY;AAAA,QAC3E;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,cAAU,MAAM,IAAI,sBAAiB,CAAC;AACtC,YAAQ;AAAA,MACN,MAAM,KAAK,IAAI;AAAA,SAAY,eAAe,QAAQ,IAAI,UAAU,GAAG,EAAE;AAAA,IACvE;AACA,QAAI,WAAW,eAAe,SAAS,IAAI,OAAO;AAChD,cAAQ,MAAM,MAAM,IAAI,IAAI,KAAK,CAAC;AAAA,IACpC;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,QAAQ,MAAM;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport \"dotenv/config\";\n\nimport { detectPlatform, parsePrUrl, postGitlabReviewCommitStatus, postReviewComment, postReviewStructured, reviewPr } from \"./agent.js\";\nimport type { AgentProgressEvent } from \"./agent.js\";\nimport type { PostCommentResult } from \"./types.js\";\nimport { renderMarkdown } from \"./render.js\";\nimport { pushMetrics } from \"./metrics.js\";\nimport { setLogLevel } from \"./utils/logger.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"hodor\")\n .description(\n \"AI-powered code review agent for GitHub PRs, GitLab MRs, Gitea/Forgejo PRs, and local diffs.\\n\\n\" +\n \"Hodor uses an AI agent that clones the repository, checks out the PR branch,\\n\" +\n \"and analyzes the code using tools (gh, git, glab) for metadata fetching and comment posting.\\n\\n\" +\n \"For local reviews, use --local with --diff-against to review changes in your current git repository.\",\n )\n .version(\"0.6.1\")\n .argument(\"[pr-url]\", \"URL of the GitHub PR, GitLab MR, or Gitea/Forgejo PR to review (optional with --local)\")\n .option(\n \"--model <model>\",\n \"LLM model to use as provider/model-id (e.g., anthropic/claude-sonnet-4-5-20250929, openrouter/moonshotai/kimi-k2.6)\",\n \"anthropic/claude-sonnet-4-5-20250929\",\n )\n .option(\n \"--reasoning-effort <level>\",\n \"Reasoning effort level: minimal, low, medium, high, xhigh\",\n )\n .option(\"-v, --verbose\", \"Enable verbose logging\", false)\n .option(\n \"--post\",\n \"Post the review directly to the PR/MR as a comment\",\n false,\n )\n .option(\"--prompt <text>\", \"Custom inline prompt text\")\n .option(\n \"--prompt-file <path>\",\n \"Path to file containing custom prompt instructions\",\n )\n .option(\n \"--workspace <dir>\",\n \"Workspace directory (creates temp dir if not specified)\",\n )\n .option(\n \"--review-style <style>\",\n \"How to post reviews on GitLab: summary (single comment), inline (diff comments), hybrid (both). Default: hybrid.\",\n \"hybrid\",\n )\n .option(\n \"--code-quality <path>\",\n \"Write a gl-code-quality-report.json CodeClimate artifact to this path\",\n )\n .option(\n \"--commit-status\",\n \"Post a pass/fail commit status to the MR head SHA\",\n false,\n )\n .option(\n \"--ultrathink\",\n \"Enable maximum reasoning effort with extended thinking budget\",\n false,\n )\n .option(\n \"--bedrock-tags <json>\",\n \"JSON object of cost allocation tags for Bedrock requests (e.g., '{\\\"team\\\":\\\"platform\\\"}')\",\n )\n .option(\n \"--prometheus-push <url>\",\n \"Push review metrics to a Prometheus Pushgateway URL\",\n )\n .option(\n \"--local\",\n \"Review local changes in the current directory (no PR URL required)\",\n false,\n )\n .option(\n \"--diff-against <ref>\",\n \"Git ref to diff against in local mode (e.g., origin/main, HEAD~1)\",\n \"origin/main\",\n )\n .action(async (prUrl: string | undefined, cmdOpts: Record<string, unknown>) => {\n const verbose = cmdOpts.verbose as boolean;\n const post = cmdOpts.post as boolean;\n const model = cmdOpts.model as string;\n let reasoningEffort = cmdOpts.reasoningEffort as string | undefined;\n const prompt = cmdOpts.prompt as string | undefined;\n const promptFile = cmdOpts.promptFile as string | undefined;\n const workspace = cmdOpts.workspace as string | undefined;\n const reviewStyle = cmdOpts.reviewStyle as \"summary\" | \"inline\" | \"hybrid\" | undefined;\n const codeQuality = cmdOpts.codeQuality as string | undefined;\n const commitStatus = cmdOpts.commitStatus as boolean;\n const ultrathink = cmdOpts.ultrathink as boolean;\n const bedrockTagsRaw = cmdOpts.bedrockTags as string | undefined;\n const prometheusPush = cmdOpts.prometheusPush as string | undefined;\n const localMode = cmdOpts.local as boolean;\n const diffAgainst = cmdOpts.diffAgainst as string;\n\n if (!localMode && !prUrl) {\n console.error(chalk.red(\"Error: pr-url is required unless --local is specified\"));\n process.exit(1);\n }\n if (localMode && post) {\n console.error(chalk.red(\"Error: --post is not supported in --local mode (no remote to post to)\"));\n process.exit(1);\n }\n if (![\"summary\", \"inline\", \"hybrid\"].includes(reviewStyle ?? \"hybrid\")) {\n console.error(chalk.red(\"Error: --review-style must be one of: summary, inline, hybrid\"));\n process.exit(1);\n }\n\n // Auto-detect CI environment\n const isCI = !!(process.env.CI || process.env.GITLAB_CI || process.env.GITHUB_ACTIONS || process.env.GITEA_ACTIONS || process.env.FORGEJO_ACTIONS);\n\n if (verbose) setLogLevel(\"debug\");\n else if (isCI) setLogLevel(\"info\");\n\n // Handle ultrathink\n if (ultrathink) {\n reasoningEffort = \"xhigh\";\n }\n\n // Parse Bedrock cost allocation tags\n let bedrockTags: Record<string, string> | null = null;\n if (bedrockTagsRaw) {\n try {\n bedrockTags = JSON.parse(bedrockTagsRaw) as Record<string, string>;\n } catch {\n console.error(chalk.red(\"Error: --bedrock-tags must be valid JSON\"));\n process.exit(1);\n }\n }\n\n const log = console.log;\n const logStream = process.stdout;\n\n const toolIcons: Record<string, string> = {\n bash: \"$\",\n read: \"cat\",\n grep: \"grep\",\n find: \"find\",\n ls: \"ls\",\n };\n\n /** Write a line to the log stream */\n function streamLog(msg: string): void {\n logStream.write(`${msg}\\n`);\n }\n\n /** Write inline text (no newline) for streaming deltas */\n function streamWrite(text: string): void {\n process.stderr.write(text);\n }\n\n function handleEvent(event: AgentProgressEvent): void {\n switch (event.type) {\n case \"agent_start\":\n streamLog(chalk.dim(\"▶ Agent started\"));\n break;\n case \"turn_start\":\n streamLog(chalk.dim(`\\n── Turn ${event.turnIndex ?? \"?\"} ──`));\n break;\n case \"tool_start\": {\n const icon = toolIcons[event.toolName ?? \"\"] ?? event.toolName;\n const preview = event.toolArgs ? ` ${event.toolArgs}` : \"\";\n const maxLen = 160;\n const truncated = preview.length > maxLen ? preview.slice(0, maxLen) + \"…\" : preview;\n streamLog(chalk.green(` ${icon}${truncated}`));\n break;\n }\n case \"tool_end\": {\n if (event.isError) {\n streamLog(chalk.red(` ✗ error`));\n }\n if (event.result) {\n const lines = event.result.split(\"\\n\");\n const maxLines = verbose ? 15 : 6;\n const maxChars = verbose ? 400 : 200;\n let chars = 0;\n for (let i = 0; i < Math.min(lines.length, maxLines); i++) {\n const line = lines[i];\n if (chars + line.length > maxChars) {\n streamLog(chalk.dim(` …(${lines.length - i} more lines)`));\n break;\n }\n streamLog(chalk.dim(` ${line}`));\n chars += line.length;\n }\n }\n break;\n }\n case \"text_delta\":\n if (verbose && event.delta) {\n streamWrite(event.delta);\n }\n break;\n case \"thinking_delta\":\n // Only show reasoning in verbose mode\n if (verbose && event.delta) {\n streamWrite(chalk.dim(event.delta));\n }\n break;\n case \"agent_end\":\n streamLog(chalk.dim(\"\\n▶ Extracting review...\"));\n break;\n }\n }\n\n try {\n // Detect platform and warn about missing tokens\n let platform: string = \"local\";\n let metricsProject: string | undefined;\n let metricsMrIid: string | undefined;\n if (!localMode && prUrl) {\n platform = detectPlatform(prUrl);\n const parsedPr = parsePrUrl(prUrl);\n metricsProject = `${parsedPr.owner}/${parsedPr.repo}`;\n metricsMrIid = String(parsedPr.prNumber);\n const githubToken = process.env.GITHUB_TOKEN;\n const gitlabToken =\n process.env.GITLAB_TOKEN ??\n process.env.GITLAB_PRIVATE_TOKEN ??\n process.env.CI_JOB_TOKEN;\n\n if (platform === \"github\" && !githubToken) {\n console.error(chalk.yellow(\"Warning: GITHUB_TOKEN not set. You may encounter rate limits.\"));\n console.error(chalk.dim(\" Set GITHUB_TOKEN or run: gh auth login\\n\"));\n } else if (platform === \"gitlab\" && !gitlabToken) {\n console.error(chalk.yellow(\"Warning: No GitLab token detected. Set GITLAB_TOKEN (api scope).\"));\n console.error(chalk.dim(\" Export GITLAB_TOKEN and optionally GITLAB_HOST.\\n\"));\n } else if (platform === \"gitea\") {\n const giteaToken = process.env.GITEA_TOKEN ?? process.env.FORGEJO_TOKEN;\n if (!giteaToken) {\n console.error(chalk.yellow(\"Warning: No Gitea/Forgejo token detected. Set GITEA_TOKEN for authentication.\"));\n console.error(chalk.dim(\" Export GITEA_TOKEN (or FORGEJO_TOKEN) for API access.\\n\"));\n }\n }\n }\n\n log(`\\n${chalk.bold.cyan(\"Hodor - AI Code Review Agent\")}`);\n if (localMode) {\n log(chalk.dim(`Mode: Local diff review`));\n log(chalk.dim(`Diff against: ${diffAgainst}`));\n log(chalk.dim(`Workspace: ${workspace ?? process.cwd()}`));\n } else {\n log(chalk.dim(`Platform: ${platform.toUpperCase()}`));\n log(chalk.dim(`PR URL: ${prUrl}`));\n }\n log(chalk.dim(`Model: ${model}`));\n if (reasoningEffort) {\n log(chalk.dim(`Reasoning Effort: ${reasoningEffort}`));\n }\n log();\n\n streamLog(chalk.dim(\"▶ Setting up workspace...\"));\n const { review, metricsFooter, headSha, metrics, workspacePath } = await reviewPr({\n prUrl: localMode ? undefined : prUrl,\n model,\n reasoningEffort,\n customPrompt: prompt,\n promptFile,\n cleanup: !workspace,\n workspaceDir: workspace,\n includeMetricsFooter: post && !localMode,\n onEvent: handleEvent,\n bedrockTags,\n localMode,\n diffAgainst,\n });\n const reviewText = renderMarkdown(review);\n\n streamLog(chalk.green(\"✔ Review complete!\"));\n\n if (codeQuality) {\n try {\n const { formatCodeQualityReport } = await import(\"./codequality.js\");\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(\n codeQuality,\n formatCodeQualityReport(review, process.env.CI_PROJECT_DIR ?? workspacePath),\n \"utf-8\",\n );\n log(chalk.dim(`Wrote code quality report to ${codeQuality}`));\n } catch (err) {\n log(chalk.yellow(`Failed to write code quality report: ${err}`));\n }\n }\n\n if (post && prUrl) {\n log(chalk.cyan(\"\\nPosting review to PR/MR...\"));\n\n const platform = detectPlatform(prUrl);\n const useStructured = platform === \"gitlab\" && reviewStyle !== \"summary\";\n\n let result: PostCommentResult;\n if (useStructured) {\n result = await postReviewStructured({\n prUrl,\n review,\n model,\n metricsFooter,\n reviewStyle: reviewStyle ?? \"hybrid\",\n commitStatus,\n headSha,\n workspacePath,\n });\n } else {\n result = await postReviewComment({\n prUrl,\n reviewText,\n model,\n metricsFooter,\n headSha,\n });\n\n if (platform === \"gitlab\" && commitStatus) {\n try {\n const { getGitlabMrDiffRefs } = await import(\"./gitlab.js\");\n const parsed = parsePrUrl(prUrl);\n const diffRefs = await getGitlabMrDiffRefs(\n parsed.owner,\n parsed.repo,\n parsed.prNumber,\n parsed.host,\n );\n await postGitlabReviewCommitStatus(parsed, review, diffRefs);\n } catch (err) {\n result = {\n success: false,\n platform: \"gitlab\",\n mrNumber: parsePrUrl(prUrl).prNumber,\n error: `Failed to post commit status: ${err instanceof Error ? err.message : err}`,\n };\n }\n }\n }\n\n if (result.success) {\n log(chalk.bold.green(\"Review posted successfully!\"));\n log(chalk.dim(` ${platform === \"gitlab\" ? \"MR\" : \"PR\"}: ${prUrl}`));\n } else {\n log(chalk.bold.red(`Failed to post review: ${result.error}`));\n log(chalk.yellow(\"\\nReview output:\\n\"));\n console.log(reviewText);\n }\n } else {\n log(chalk.bold.green(\"Review Complete\\n\"));\n console.log(reviewText);\n if (!localMode) {\n log(chalk.dim(\"\\nTip: Use --post to automatically post this review to the PR/MR\"));\n }\n }\n\n // Push metrics to Prometheus Pushgateway (best-effort, never fails the run)\n if (prometheusPush) {\n const labels: Record<string, string> = {\n platform,\n model,\n verdict: review.overall_correctness === \"patch is correct\" ? \"correct\" : \"incorrect\",\n };\n if (metricsProject) labels.project = metricsProject;\n if (metricsMrIid) labels.mr_iid = metricsMrIid;\n\n await pushMetrics({\n pushgatewayUrl: prometheusPush,\n metrics,\n findings: review.findings,\n labels,\n });\n }\n } catch (err) {\n streamLog(chalk.red(\"✗ Review failed\"));\n console.error(\n chalk.bold.red(`\\nError: ${err instanceof Error ? err.message : err}`),\n );\n if (verbose && err instanceof Error && err.stack) {\n console.error(chalk.dim(err.stack));\n }\n process.exit(1);\n }\n });\n\nprogram.parse();\n"],"mappings":"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","names":["platform"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
relativizeWorkspacePath
|
|
3
|
+
} from "./chunk-AMUK6GDX.js";
|
|
4
|
+
|
|
5
|
+
// src/codequality.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
var PRIORITY_TO_SEVERITY = {
|
|
8
|
+
0: "critical",
|
|
9
|
+
1: "major",
|
|
10
|
+
2: "minor",
|
|
11
|
+
3: "info"
|
|
12
|
+
};
|
|
13
|
+
function fingerprint(finding, relativePath) {
|
|
14
|
+
const input = `${finding.title}:${relativePath}:${finding.code_location.line_range.start}`;
|
|
15
|
+
return createHash("md5").update(input).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
function formatCodeQualityReport(review, workspacePrefix) {
|
|
18
|
+
const issues = review.findings.map((finding) => {
|
|
19
|
+
const relPath = relativizeWorkspacePath(finding.code_location.absolute_file_path, workspacePrefix);
|
|
20
|
+
return {
|
|
21
|
+
type: "issue",
|
|
22
|
+
check_name: `hodor/P${finding.priority}`,
|
|
23
|
+
description: finding.title,
|
|
24
|
+
content: { body: finding.body },
|
|
25
|
+
categories: ["Bug Risk"],
|
|
26
|
+
severity: PRIORITY_TO_SEVERITY[finding.priority] ?? "info",
|
|
27
|
+
location: {
|
|
28
|
+
path: relPath,
|
|
29
|
+
lines: {
|
|
30
|
+
begin: finding.code_location.line_range.start,
|
|
31
|
+
end: finding.code_location.line_range.end
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
fingerprint: fingerprint(finding, relPath)
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
return JSON.stringify(issues, null, 2);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
formatCodeQualityReport
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=codequality-DTJK2LGF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/codequality.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { ReviewOutput, ReviewFinding, ReviewPriority } from \"./types.js\";\nimport { relativizeWorkspacePath } from \"./utils/path.js\";\n\nconst PRIORITY_TO_SEVERITY: Record<ReviewPriority, string> = {\n 0: \"critical\",\n 1: \"major\",\n 2: \"minor\",\n 3: \"info\",\n};\n\nfunction fingerprint(finding: ReviewFinding, relativePath: string): string {\n const input = `${finding.title}:${relativePath}:${finding.code_location.line_range.start}`;\n return createHash(\"md5\").update(input).digest(\"hex\");\n}\n\nexport function formatCodeQualityReport(\n review: ReviewOutput,\n workspacePrefix?: string,\n): string {\n const issues = review.findings.map((finding) => {\n const relPath = relativizeWorkspacePath(finding.code_location.absolute_file_path, workspacePrefix);\n return {\n type: \"issue\",\n check_name: `hodor/P${finding.priority}`,\n description: finding.title,\n content: { body: finding.body },\n categories: [\"Bug Risk\"],\n severity: PRIORITY_TO_SEVERITY[finding.priority] ?? \"info\",\n location: {\n path: relPath,\n lines: {\n begin: finding.code_location.line_range.start,\n end: finding.code_location.line_range.end,\n },\n },\n fingerprint: fingerprint(finding, relPath),\n };\n });\n\n return JSON.stringify(issues, null, 2);\n}\n"],"mappings":";;;;;AAAA,SAAS,kBAAkB;AAI3B,IAAM,uBAAuD;AAAA,EAC3D,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AAEA,SAAS,YAAY,SAAwB,cAA8B;AACzE,QAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI,YAAY,IAAI,QAAQ,cAAc,WAAW,KAAK;AACxF,SAAO,WAAW,KAAK,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACrD;AAEO,SAAS,wBACd,QACA,iBACQ;AACR,QAAM,SAAS,OAAO,SAAS,IAAI,CAAC,YAAY;AAC9C,UAAM,UAAU,wBAAwB,QAAQ,cAAc,oBAAoB,eAAe;AACjG,WAAO;AAAA,MACL,MAAM;AAAA,MACN,YAAY,UAAU,QAAQ,QAAQ;AAAA,MACtC,aAAa,QAAQ;AAAA,MACrB,SAAS,EAAE,MAAM,QAAQ,KAAK;AAAA,MAC9B,YAAY,CAAC,UAAU;AAAA,MACvB,UAAU,qBAAqB,QAAQ,QAAQ,KAAK;AAAA,MACpD,UAAU;AAAA,QACR,MAAM;AAAA,QACN,OAAO;AAAA,UACL,OAAO,QAAQ,cAAc,WAAW;AAAA,UACxC,KAAK,QAAQ,cAAc,WAAW;AAAA,QACxC;AAAA,MACF;AAAA,MACA,aAAa,YAAY,SAAS,OAAO;AAAA,IAC3C;AAAA,EACF,CAAC;AAED,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;","names":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GitLabAPIError,
|
|
3
|
+
HODOR_REVIEW_MARKER,
|
|
4
|
+
bulkPublishGitlabDraftNotes,
|
|
5
|
+
cleanupHodorComments,
|
|
6
|
+
createGitlabDraftNote,
|
|
7
|
+
fetchGitlabMrInfo,
|
|
8
|
+
getGitlabMrDiffRefs,
|
|
9
|
+
listHodorDiscussions,
|
|
10
|
+
parseGlabPaginatedJson,
|
|
11
|
+
postGitlabCommitStatus,
|
|
12
|
+
postGitlabInlineComment,
|
|
13
|
+
postGitlabMrComment,
|
|
14
|
+
resolveGitlabDiscussions,
|
|
15
|
+
summarizeGitlabNotes
|
|
16
|
+
} from "./chunk-DVJVQTVW.js";
|
|
17
|
+
export {
|
|
18
|
+
GitLabAPIError,
|
|
19
|
+
HODOR_REVIEW_MARKER,
|
|
20
|
+
bulkPublishGitlabDraftNotes,
|
|
21
|
+
cleanupHodorComments,
|
|
22
|
+
createGitlabDraftNote,
|
|
23
|
+
fetchGitlabMrInfo,
|
|
24
|
+
getGitlabMrDiffRefs,
|
|
25
|
+
listHodorDiscussions,
|
|
26
|
+
parseGlabPaginatedJson,
|
|
27
|
+
postGitlabCommitStatus,
|
|
28
|
+
postGitlabInlineComment,
|
|
29
|
+
postGitlabMrComment,
|
|
30
|
+
resolveGitlabDiscussions,
|
|
31
|
+
summarizeGitlabNotes
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=gitlab-5TWHYHEP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { ThinkingLevel } from '@earendil-works/pi-ai';
|
|
2
|
+
|
|
3
|
+
type Platform = "github" | "gitlab" | "gitea";
|
|
2
4
|
interface ParsedPrUrl {
|
|
3
5
|
owner: string;
|
|
4
6
|
repo: string;
|
|
@@ -61,6 +63,10 @@ interface ReviewFinding {
|
|
|
61
63
|
end: number;
|
|
62
64
|
};
|
|
63
65
|
};
|
|
66
|
+
/** Verbatim copy of the source lines the finding refers to, used to resolve
|
|
67
|
+
* line_range against the on-disk file. Optional; falls back to line_range. */
|
|
68
|
+
existing_code?: string;
|
|
69
|
+
suggestion?: string;
|
|
64
70
|
}
|
|
65
71
|
interface ReviewOutput {
|
|
66
72
|
findings: ReviewFinding[];
|
|
@@ -75,6 +81,15 @@ interface PostCommentResult {
|
|
|
75
81
|
error?: string;
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Render structured review output into clean markdown for PR/MR comments.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render a ReviewOutput into clean markdown for posting as a PR/MR comment.
|
|
90
|
+
*/
|
|
91
|
+
declare function renderMarkdown(review: ReviewOutput): string;
|
|
92
|
+
|
|
78
93
|
interface AgentProgressEvent {
|
|
79
94
|
type: "tool_start" | "tool_end" | "thinking" | "turn_start" | "turn_end" | "agent_start" | "agent_end" | "text_delta" | "thinking_delta" | "tool_result";
|
|
80
95
|
toolName?: string;
|
|
@@ -111,6 +126,7 @@ declare function reviewPr(opts: {
|
|
|
111
126
|
metricsFooter: string | null;
|
|
112
127
|
headSha: string | null;
|
|
113
128
|
metrics: ReviewMetrics;
|
|
129
|
+
workspacePath: string;
|
|
114
130
|
}>;
|
|
115
131
|
|
|
116
132
|
declare function buildPrReviewPrompt(opts: {
|
|
@@ -139,19 +155,22 @@ declare function parseModelString(model: string): ParsedModel;
|
|
|
139
155
|
* Map reasoning effort strings to pi-ai thinking levels.
|
|
140
156
|
* Returns undefined for no reasoning.
|
|
141
157
|
*/
|
|
142
|
-
declare function mapReasoningEffort(effort: string | undefined):
|
|
158
|
+
declare function mapReasoningEffort(effort: string | undefined): ThinkingLevel | undefined;
|
|
143
159
|
/**
|
|
144
160
|
* Get API key with provider-aware selection.
|
|
145
161
|
*
|
|
146
162
|
* Priority:
|
|
147
163
|
* 1. LLM_API_KEY (universal override)
|
|
148
|
-
* 2. Provider-specific key (ANTHROPIC_API_KEY, OPENAI_API_KEY
|
|
149
|
-
*
|
|
164
|
+
* 2. Provider-specific key known by pi-ai (ANTHROPIC_API_KEY, OPENAI_API_KEY,
|
|
165
|
+
* OPENROUTER_API_KEY, etc.)
|
|
150
166
|
*
|
|
151
167
|
* Returns null for bedrock (uses AWS credentials).
|
|
152
168
|
*/
|
|
153
169
|
declare function getApiKey(model?: string): string | null;
|
|
154
170
|
|
|
171
|
+
type FindingPriority = {
|
|
172
|
+
priority: number;
|
|
173
|
+
};
|
|
155
174
|
declare function formatMetricsMarkdown(metrics: ReviewMetrics): string;
|
|
156
175
|
declare function printMetrics(metrics: ReviewMetrics, stream?: NodeJS.WritableStream): void;
|
|
157
176
|
/**
|
|
@@ -161,18 +180,10 @@ declare function printMetrics(metrics: ReviewMetrics, stream?: NodeJS.WritableSt
|
|
|
161
180
|
declare function pushMetrics(opts: {
|
|
162
181
|
pushgatewayUrl: string;
|
|
163
182
|
metrics: ReviewMetrics;
|
|
183
|
+
findings?: FindingPriority[];
|
|
164
184
|
labels?: Record<string, string>;
|
|
165
185
|
}): Promise<void>;
|
|
166
186
|
|
|
167
187
|
declare function validateReviewOutput(review: ReviewOutput): ReviewOutput;
|
|
168
188
|
|
|
169
|
-
/**
|
|
170
|
-
* Render structured review output into clean markdown for PR/MR comments.
|
|
171
|
-
*/
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Render a ReviewOutput into clean markdown for posting as a PR/MR comment.
|
|
175
|
-
*/
|
|
176
|
-
declare function renderMarkdown(review: ReviewOutput): string;
|
|
177
|
-
|
|
178
189
|
export { type AgentProgressEvent, type MrMetadata, type NoteEntry, type ParsedPrUrl, type Platform, type PostCommentResult, type ReviewCorrectness, type ReviewFinding, type ReviewMetrics, type ReviewOutput, type ReviewPriority, buildPrReviewPrompt, detectPlatform, formatMetricsMarkdown, getApiKey, mapReasoningEffort, parseModelString, parsePrUrl, postReviewComment, printMetrics, pushMetrics, renderMarkdown, reviewPr, validateReviewOutput };
|
package/dist/index.js
CHANGED
|
@@ -9,10 +9,13 @@ import {
|
|
|
9
9
|
postReviewComment,
|
|
10
10
|
printMetrics,
|
|
11
11
|
pushMetrics,
|
|
12
|
-
renderMarkdown,
|
|
13
12
|
reviewPr,
|
|
14
13
|
validateReviewOutput
|
|
15
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-MRMLGIXO.js";
|
|
15
|
+
import {
|
|
16
|
+
renderMarkdown
|
|
17
|
+
} from "./chunk-DVJVQTVW.js";
|
|
18
|
+
import "./chunk-AMUK6GDX.js";
|
|
16
19
|
export {
|
|
17
20
|
buildPrReviewPrompt,
|
|
18
21
|
detectPlatform,
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.2-rc.2",
|
|
7
7
|
"description": "AI-powered code review agent that finds bugs, security issues, and logic errors in pull requests",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "dist/index.js",
|
|
@@ -43,19 +43,19 @@
|
|
|
43
43
|
"node": ">=22"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@sinclair/typebox": "^0.34.
|
|
47
|
-
"@anthropic-ai/sdk": "^0.
|
|
48
|
-
"@
|
|
49
|
-
"@
|
|
50
|
-
"chalk": "^5.
|
|
51
|
-
"commander": "^
|
|
52
|
-
"dotenv": "^
|
|
46
|
+
"@sinclair/typebox": "^0.34.49",
|
|
47
|
+
"@anthropic-ai/sdk": "^0.95.2",
|
|
48
|
+
"@earendil-works/pi-ai": "^0.74.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
50
|
+
"chalk": "^5.6.2",
|
|
51
|
+
"commander": "^14.0.3",
|
|
52
|
+
"dotenv": "^17.4.2"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@types/node": "^
|
|
56
|
-
"tsup": "^8.
|
|
57
|
-
"tsx": "^4.
|
|
58
|
-
"typescript": "^
|
|
59
|
-
"vitest": "^
|
|
55
|
+
"@types/node": "^25.7.0",
|
|
56
|
+
"tsup": "^8.5.1",
|
|
57
|
+
"tsx": "^4.21.0",
|
|
58
|
+
"typescript": "^6.0.3",
|
|
59
|
+
"vitest": "^4.1.6"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/templates/tool-review.md
CHANGED
|
@@ -72,6 +72,16 @@ Always include the matching numeric priority field in the `submit_review` payloa
|
|
|
72
72
|
|
|
73
73
|
Output all findings that the original author would fix if they knew about it. If there is no finding that a person would definitely love to see and fix, prefer outputting no findings. Do not stop at the first qualifying finding. Continue until you've listed every qualifying finding.
|
|
74
74
|
|
|
75
|
+
### Contract Trace Checklist
|
|
76
|
+
|
|
77
|
+
For changes that introduce or modify routes, handlers, API parameters, auth/session/token logic, database schema or queries, cache keys, config contracts, or public interfaces:
|
|
78
|
+
|
|
79
|
+
1. Trace each externally supplied value through the layers it crosses: route/query/body/header → handler extraction and parsing → service method signature → DB/query/cache key → tests or mocks.
|
|
80
|
+
2. Compare semantic identity, not just variable names. Examples: public `user_id` string vs internal integer primary key, client/account ID vs database UID, token key vs full token value, app ID vs API key, enum label vs stored code, timestamp units/timezones, paise vs rupees.
|
|
81
|
+
3. Read the minimal adjacent convention needed when a changed file depends on it: nearby routes for the same resource, model/schema definitions, query files, auth middleware, or changed tests.
|
|
82
|
+
4. Treat a semantic mismatch as a concrete bug when production input will pass one kind of value but the changed code stores, queries, authorizes, or tests a different kind.
|
|
83
|
+
5. Keep this scoped to the diff. Do not browse unrelated code unless it defines a contract that the changed lines directly depend on.
|
|
84
|
+
|
|
75
85
|
### Additional Guidelines
|
|
76
86
|
|
|
77
87
|
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
@@ -82,10 +92,30 @@ Output all findings that the original author would fix if they knew about it. If
|
|
|
82
92
|
|
|
83
93
|
{review_process_section}
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
### Conditional Review Lenses
|
|
96
|
+
|
|
97
|
+
After identifying the changed files and diff hunks, classify which lenses apply. Apply only the relevant lenses below; do not run a broad style review just because a lens exists.
|
|
98
|
+
|
|
99
|
+
- **Silent failure / error handling lens**: If the diff changes `try`/`catch`, rescue/recover blocks, fallback logic, retries, default values after failures, logging, metrics, or error returns, verify that failures are not swallowed, fallbacks are explicit and safe, and operators/users get enough context to debug the failing operation. Flag only concrete silent failures or misleading success paths.
|
|
100
|
+
- **Critical test gap lens**: If the diff adds behavior, changes edge-case handling, alters async/concurrent behavior, or fixes a bug, compare changed behavior against changed or existing tests. Flag missing tests only when the gap is likely to let a meaningful regression through; do not require line coverage or tests for trivial code.
|
|
101
|
+
- **Comment/documentation accuracy lens**: If the diff adds or modifies comments, docstrings, README snippets, API docs, or examples, verify the prose matches the changed code and commands. Flag comments or docs that are factually wrong, misleading, or likely to cause misuse; ignore harmless wording preferences.
|
|
102
|
+
- **Type/API invariant lens**: If the diff adds or changes types, schemas, DTOs, request/response shapes, config objects, database models, enums, or public interfaces, check whether invalid states became representable, required validation moved out of the boundary, or consumers can now pass semantically wrong values. Prefer concrete invariant or compatibility bugs over abstract design advice.
|
|
103
|
+
- **Simplification lens**: If the diff introduces complex branching, duplicated logic, clever one-liners, or unnecessary abstraction, mention it only when the complexity creates a plausible bug, hides an important invariant, or makes future fixes risky. Do not suggest cosmetic refactors.
|
|
104
|
+
|
|
105
|
+
### Attack Surface Analysis
|
|
106
|
+
|
|
107
|
+
When the diff touches the areas below, investigate them specifically — do not skip a category unless it is provably inapplicable to the changed code:
|
|
108
|
+
|
|
109
|
+
- **Race conditions / TOCTOU**: Shared mutable state, non-atomic check-then-act sequences, missing locks or transactions
|
|
110
|
+
- **Off-by-one / boundary errors**: Loop bounds, slice indices, size comparisons (`<` vs `<=`), pagination math, overflow
|
|
111
|
+
- **Schema / contract drift**: Renamed fields, changed types, enum values, units (paise vs rupees, ms vs s), encoding assumptions, wire format changes
|
|
112
|
+
- **Auth / permission gaps**: New routes or handlers missing auth middleware, privilege escalation paths, token or session misuse, missing ownership checks
|
|
113
|
+
- **Rollback safety**: Migrations that cannot be rolled back cleanly, stateful side effects committed before DB transactions complete, partial-write failure modes
|
|
114
|
+
- **Data loss / silent discard**: Early returns or swallowed errors that drop user data, leave state inconsistent, or suppress failures silently
|
|
115
|
+
- **Observability gaps**: New error paths unreachable from logs or metrics, silent failures, missing structured context on errors
|
|
116
|
+
- **Input validation**: Unvalidated user-supplied values crossing trust boundaries — SQL injection, shell injection, path traversal, template injection, deserialization
|
|
117
|
+
|
|
118
|
+
Default to skepticism: a change that looks mechanical (rename, refactor, constant tweak) may still introduce one of these. If a category is provably inapplicable, skip it — but verify, don't assume.
|
|
89
119
|
|
|
90
120
|
## Final Submission
|
|
91
121
|
|
|
@@ -103,7 +133,9 @@ When you are done, call `submit_review` exactly once with the final structured r
|
|
|
103
133
|
"code_location": {
|
|
104
134
|
"absolute_file_path": "<absolute file path>",
|
|
105
135
|
"line_range": {"start": <int>, "end": <int>}
|
|
106
|
-
}
|
|
136
|
+
},
|
|
137
|
+
"existing_code": "<verbatim copy of the exact current source lines this finding covers>",
|
|
138
|
+
"suggestion": "<optional: exact replacement code for the flagged line range>"
|
|
107
139
|
}
|
|
108
140
|
],
|
|
109
141
|
"overall_correctness": "patch is correct" | "patch is incorrect",
|
|
@@ -117,9 +149,13 @@ When you are done, call `submit_review` exactly once with the final structured r
|
|
|
117
149
|
* Do not print the review as normal assistant text.
|
|
118
150
|
* Do not wrap the payload in markdown fences when calling the tool.
|
|
119
151
|
* If there are no findings, submit `"findings": []`.
|
|
152
|
+
* If `findings` is non-empty, `overall_correctness` must be `"patch is incorrect"`.
|
|
153
|
+
* If `findings` is empty, `overall_correctness` must be `"patch is correct"`.
|
|
120
154
|
* Every finding must include `title`, `body`, `priority`, and `code_location`.
|
|
155
|
+
* Always include `existing_code`: copy the exact current source lines the finding refers to, verbatim and contiguous, from the file or the diff's context/added lines. Do NOT include diff `+`/`-` markers, line numbers, or markdown fences. These lines are matched mechanically against the file to pinpoint the comment location, so they must be an exact quote covering the same lines as `line_range`.
|
|
121
156
|
* Use absolute file paths (for example, `/workspace/path/to/file.py`) not relative paths.
|
|
122
157
|
* The title must start with a priority tag: `[P0]`, `[P1]`, `[P2]`, or `[P3]`.
|
|
123
158
|
* `overall_correctness` must be exactly `"patch is correct"` or `"patch is incorrect"`.
|
|
159
|
+
* If you can suggest a specific code fix for a finding, include the replacement code in the `suggestion` field. This should be the exact code to replace the lines in `line_range`, without markdown fences or extra context. Omit `suggestion` if no specific fix is available.
|
|
124
160
|
|
|
125
161
|
{start_instruction}
|