@mrkaran/hodor 0.7.0 → 0.7.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/dist/{chunk-W7ZUJIFT.js → chunk-DALI4QRT.js} +21 -1
- package/dist/{chunk-W7ZUJIFT.js.map → chunk-DALI4QRT.js.map} +1 -1
- package/dist/{chunk-Q5AQKNSE.js → chunk-YKB3BRJM.js} +161 -35
- package/dist/chunk-YKB3BRJM.js.map +1 -0
- package/dist/cli.js +36 -16
- package/dist/cli.js.map +1 -1
- package/dist/{gitlab-L3VGOT6Q.js → gitlab-JSVU4YFQ.js} +4 -2
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/templates/review-task.md +1 -7
- package/dist/chunk-Q5AQKNSE.js.map +0 -1
- /package/dist/{gitlab-L3VGOT6Q.js.map → gitlab-JSVU4YFQ.js.map} +0 -0
|
@@ -502,6 +502,25 @@ async function bulkPublishGitlabDraftNotes(owner, repo, mrNumber, host) {
|
|
|
502
502
|
throw new GitLabAPIError(`Failed to bulk publish draft notes for MR !${mrNumber}: ${msg}`);
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
|
+
async function publishGitlabDraftNote(owner, repo, mrNumber, draftNoteId, host) {
|
|
506
|
+
const encoded = encodedProjectPath(owner, repo);
|
|
507
|
+
const env = glabEnv(host);
|
|
508
|
+
try {
|
|
509
|
+
await exec(
|
|
510
|
+
"glab",
|
|
511
|
+
[
|
|
512
|
+
"api",
|
|
513
|
+
`projects/${encoded}/merge_requests/${mrNumber}/draft_notes/${draftNoteId}/publish`,
|
|
514
|
+
"--method",
|
|
515
|
+
"PUT"
|
|
516
|
+
],
|
|
517
|
+
{ env }
|
|
518
|
+
);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
521
|
+
throw new GitLabAPIError(`Failed to publish draft note ${draftNoteId} for MR !${mrNumber}: ${msg}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
505
524
|
async function postGitlabCommitStatus(owner, repo, sha, state, host, opts) {
|
|
506
525
|
const allowedStates = /* @__PURE__ */ new Set([
|
|
507
526
|
"pending",
|
|
@@ -647,8 +666,9 @@ export {
|
|
|
647
666
|
getGitlabMrDiffRefs,
|
|
648
667
|
createGitlabDraftNote,
|
|
649
668
|
bulkPublishGitlabDraftNotes,
|
|
669
|
+
publishGitlabDraftNote,
|
|
650
670
|
postGitlabCommitStatus,
|
|
651
671
|
listHodorDiscussions,
|
|
652
672
|
resolveGitlabDiscussions
|
|
653
673
|
};
|
|
654
|
-
//# sourceMappingURL=chunk-
|
|
674
|
+
//# sourceMappingURL=chunk-DALI4QRT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/exec.ts","../src/utils/logger.ts","../src/render.ts","../src/gitlab.ts"],"sourcesContent":["import { execFile, spawn } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n}\n\nexport interface ExecOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n input?: string;\n}\n\nexport async function exec(\n cmd: string,\n args: string[],\n opts?: ExecOptions,\n): Promise<ExecResult> {\n if (typeof opts?.input === \"string\") {\n return new Promise<ExecResult>((resolve, reject) => {\n const child = spawn(cmd, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.stderr.on(\"data\", (chunk: Buffer | string) => {\n stderr += chunk.toString();\n });\n child.on(\"error\", (error) => {\n reject(error);\n });\n child.on(\"close\", (code, signal) => {\n if (code === 0) {\n resolve({ stdout, stderr });\n return;\n }\n\n const parts: string[] = [`Command failed: ${cmd} ${args.join(\" \")}`];\n if (stderr.trim()) {\n parts.push(`stderr:\\n${stderr.trim()}`);\n }\n if (stdout.trim()) {\n parts.push(`stdout:\\n${stdout.trim()}`);\n }\n if (signal) {\n parts.push(`signal: ${signal}`);\n }\n const error = new Error(parts.join(\"\\n\"));\n reject(error);\n });\n\n child.stdin.write(opts.input);\n child.stdin.end();\n });\n }\n\n const { stdout, stderr } = await execFileAsync(cmd, args, {\n cwd: opts?.cwd,\n env: opts?.env ?? process.env,\n maxBuffer: 50 * 1024 * 1024, // 50MB\n });\n return { stdout, stderr };\n}\n\nexport async function execJson<T = Record<string, unknown>>(\n cmd: string,\n args: string[],\n opts?: ExecOptions,\n): Promise<T> {\n const { stdout } = await exec(cmd, args, opts);\n return JSON.parse(stdout.trim()) as T;\n}\n","import chalk from \"chalk\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nlet currentLevel: LogLevel = \"warn\";\n\nconst LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nexport function setLogLevel(level: LogLevel): void {\n currentLevel = level;\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVELS[level] >= LEVELS[currentLevel];\n}\n\nfunction timestamp(): string {\n return new Date().toISOString();\n}\n\nexport const logger = {\n debug(msg: string): void {\n if (shouldLog(\"debug\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.gray(\"DEBUG\")} ${msg}\\n`);\n }\n },\n info(msg: string): void {\n if (shouldLog(\"info\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.blue(\"INFO\")} ${msg}\\n`);\n }\n },\n warn(msg: string): void {\n if (shouldLog(\"warn\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.yellow(\"WARN\")} ${msg}\\n`);\n }\n },\n error(msg: string): void {\n if (shouldLog(\"error\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.red(\"ERROR\")} ${msg}\\n`);\n }\n },\n};\n","/**\n * Render structured review output into clean markdown for PR/MR comments.\n */\n\nimport type { ReviewFinding, ReviewOutput } from \"./types.js\";\n\nexport const HODOR_REVIEW_MARKER = \"<!-- hodor-review -->\";\n\n/**\n * Render a ReviewOutput into clean markdown for posting as a PR/MR comment.\n */\nexport function renderMarkdown(review: ReviewOutput): string {\n const lines: string[] = [HODOR_REVIEW_MARKER];\n\n // Group findings by priority\n const critical: ReviewFinding[] = []; // P0, P1\n const important: ReviewFinding[] = []; // P2\n const minor: ReviewFinding[] = []; // P3\n\n for (const f of review.findings) {\n const p = f.priority;\n if (p <= 1) critical.push(f);\n else if (p === 2) important.push(f);\n else minor.push(f);\n }\n\n lines.push(\"### Issues Found\");\n lines.push(\"\");\n\n if (review.findings.length === 0) {\n lines.push(\"No issues found.\");\n lines.push(\"\");\n }\n\n if (critical.length > 0) {\n lines.push(\"**Critical (P0/P1)**\");\n for (const f of critical) {\n lines.push(formatFinding(f));\n }\n lines.push(\"\");\n }\n\n if (important.length > 0) {\n lines.push(\"**Important (P2)**\");\n for (const f of important) {\n lines.push(formatFinding(f));\n }\n lines.push(\"\");\n }\n\n if (minor.length > 0) {\n lines.push(\"**Minor (P3)**\");\n for (const f of minor) {\n lines.push(formatFinding(f));\n }\n lines.push(\"\");\n }\n\n // Summary\n lines.push(\"### Summary\");\n lines.push(\n `Total issues: ${critical.length} critical, ${important.length} important, ${minor.length} minor.`,\n );\n lines.push(\"\");\n\n // Overall verdict\n lines.push(\"### Overall Verdict\");\n const isCorrect = review.overall_correctness === \"patch is correct\";\n lines.push(\n `**Status**: ${isCorrect ? \"Patch is correct\" : \"Patch has blocking issues\"}`,\n );\n lines.push(\"\");\n if (review.overall_explanation) {\n lines.push(`**Explanation**: ${review.overall_explanation}`);\n }\n\n return lines.join(\"\\n\").trimEnd() + \"\\n\";\n}\n\nexport function renderSummaryMarkdown(review: ReviewOutput): string {\n const lines: string[] = [HODOR_REVIEW_MARKER];\n\n const counts = { critical: 0, important: 0, minor: 0 };\n for (const f of review.findings) {\n if (f.priority <= 1) counts.critical++;\n else if (f.priority === 2) counts.important++;\n else counts.minor++;\n }\n\n lines.push(\"\");\n lines.push(\"| Category | Count |\");\n lines.push(\"| --- | ---: |\");\n lines.push(`| Critical (P0/P1) | ${counts.critical} |`);\n lines.push(`| Important (P2) | ${counts.important} |`);\n lines.push(`| Minor (P3) | ${counts.minor} |`);\n\n const isCorrect = review.overall_correctness === \"patch is correct\";\n lines.push(\"\");\n lines.push(`**Overall verdict**: ${isCorrect ? \"Patch is correct\" : \"Patch has blocking issues\"}`);\n lines.push(\"\");\n lines.push(`**Explanation**: ${review.overall_explanation}`);\n\n if (review.findings.length > 0) {\n lines.push(\"\");\n lines.push(\"| Finding | Location | Priority |\");\n lines.push(\"| --- | --- | --- |\");\n for (const f of review.findings) {\n const loc = formatLocation(f.code_location);\n const safeTitle = f.title.replace(/\\|/g, \"\\\\|\");\n lines.push(`| ${safeTitle} | \\`${loc}\\` | P${f.priority} |`);\n }\n }\n\n return lines.join(\"\\n\").trimEnd() + \"\\n\";\n}\n\nfunction formatFinding(f: ReviewFinding): string {\n const loc = ` (\\`${formatLocation(f.code_location)}\\`)`;\n const title = `- **${f.title}**${loc}`;\n const body = ` - ${f.body}`;\n return `${title}\\n${body}`;\n}\n\nfunction formatLocation(loc: {\n absolute_file_path: string;\n line_range: { start: number; end: number };\n}): string {\n // Strip common workspace prefixes to get a clean relative path\n let filePath = loc.absolute_file_path;\n\n // GitLab CI: /builds/owner/repo/src/file.ts → src/file.ts\n const buildsMatch = filePath.match(/\\/builds\\/[^/]+\\/[^/]+\\/(.+)/);\n if (buildsMatch) {\n filePath = buildsMatch[1];\n }\n // GitHub Actions / generic workspace\n else if (filePath.includes(\"/workspace/\")) {\n filePath = filePath.slice(filePath.indexOf(\"/workspace/\") + \"/workspace/\".length);\n }\n // Temp review dirs: /tmp/hodor-review-<id>/src/file.ts → src/file.ts\n else {\n filePath = filePath.replace(/^.*\\/hodor-review-[^/]+\\//, \"\");\n }\n\n const { start, end } = loc.line_range;\n return start === end ? `${filePath}:${start}` : `${filePath}:${start}-${end}`;\n}\n","import { exec, execJson } from \"./utils/exec.js\";\nimport { logger } from \"./utils/logger.js\";\nimport type { MrMetadata, NoteEntry } from \"./types.js\";\nimport { HODOR_REVIEW_MARKER } from \"./render.js\";\n\nexport { HODOR_REVIEW_MARKER };\n\nexport interface DiffRefs {\n base_sha: string;\n head_sha: string;\n start_sha: string;\n}\n\nconst DEFAULT_GITLAB_HOST = \"gitlab.com\";\n\n/**\n * Match notes Hodor itself created. The body must begin with a hodor-owned HTML\n * comment (either `<!-- hodor-review -->` or a sibling like `<!-- hodor:sha:... -->`\n * that hodor prepends to summary comments). Anchoring at the start avoids deleting\n * human notes that quote the marker incidentally (e.g., a code block discussing hodor).\n */\nconst HODOR_NOTE_PREFIX_RE = /^\\s*<!--\\s*hodor[-:]/;\nconst HODOR_CACHE_MARKER_RE = /<!--\\s*hodor:cache:v1:[A-Za-z0-9_-]+\\s*-->\\s*/g;\n\nfunction isHodorNote(body: unknown, marker = HODOR_REVIEW_MARKER): boolean {\n if (typeof body !== \"string\") return false;\n // Default fast-path: body starts with the canonical marker (allowing leading whitespace).\n if (body.trimStart().startsWith(marker)) return true;\n // Accept hodor's own SHA prefix, e.g. `<!-- hodor:sha:abc -->\\n<!-- hodor-review -->\\n...`\n if (marker === HODOR_REVIEW_MARKER && HODOR_NOTE_PREFIX_RE.test(body)) {\n // Require the canonical marker to appear somewhere in the body so we don't\n // resolve unrelated `<!-- hodor:foo -->` notes that aren't review summaries.\n return body.includes(HODOR_REVIEW_MARKER);\n }\n return false;\n}\n\nexport function isHodorGeneratedNote(body: unknown): boolean {\n return isHodorNote(body);\n}\n\n/**\n * Parse concatenated JSON arrays from `glab api --paginate`.\n * glab outputs `[...][...][...]` — one array per page, no delimiter.\n * We track bracket depth (respecting strings/escapes) to find each\n * top-level array, parse them individually, and merge with flat().\n */\nexport function parseGlabPaginatedJson(raw: string): Array<Record<string, unknown>> {\n const trimmed = raw.trim();\n if (!trimmed) return [];\n\n const chunks: string[] = [];\n let depth = 0;\n let inString = false;\n let escaped = false;\n let start = -1;\n\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (escaped) {\n escaped = false;\n continue;\n }\n if (ch === \"\\\\\" && inString) {\n escaped = true;\n continue;\n }\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n if (ch === \"[\") {\n if (depth === 0) start = i;\n depth++;\n } else if (ch === \"]\") {\n depth--;\n if (depth === 0 && start >= 0) {\n chunks.push(trimmed.slice(start, i + 1));\n start = -1;\n }\n }\n }\n\n const results: Array<Record<string, unknown>> = [];\n for (const chunk of chunks) {\n try {\n const parsed = JSON.parse(chunk) as Array<Record<string, unknown>>;\n if (Array.isArray(parsed)) results.push(...parsed);\n } catch (err) {\n logger.warn(\n `Skipping malformed glab pagination chunk: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n return results;\n}\n\nexport class GitLabAPIError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"GitLabAPIError\";\n }\n}\n\nfunction normalizeBaseUrl(host?: string | null): string {\n const candidate =\n host ||\n process.env.GITLAB_HOST ||\n process.env.CI_SERVER_URL ||\n DEFAULT_GITLAB_HOST;\n const trimmed = candidate.trim() || DEFAULT_GITLAB_HOST;\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed.replace(/\\/+$/, \"\");\n }\n return `https://${trimmed}`.replace(/\\/+$/, \"\");\n}\n\nfunction encodedProjectPath(owner: string, repo: string): string {\n const projectPath = [owner.replace(/^\\/+|\\/+$/g, \"\"), repo.replace(/^\\/+|\\/+$/g, \"\")]\n .filter(Boolean)\n .join(\"/\");\n return encodeURIComponent(projectPath);\n}\n\nfunction glabEnv(host?: string | null): NodeJS.ProcessEnv {\n const env = { ...process.env };\n // Ensure glab knows which host to talk to\n const baseUrl = normalizeBaseUrl(host);\n const hostname = baseUrl.replace(/^https?:\\/\\//, \"\");\n env.GITLAB_HOST = hostname;\n return env;\n}\n\n/**\n * Fetch merge request metadata using glab api.\n */\nexport async function fetchGitlabMrInfo(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n options?: { includeComments?: boolean },\n): Promise<MrMetadata> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let mrData: Record<string, unknown>;\n try {\n mrData = await execJson<Record<string, unknown>>(\n \"glab\",\n [\"api\", `projects/${encoded}/merge_requests/${mrNumber}`],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to fetch MR !${mrNumber}: ${msg}`);\n }\n\n const metadata: MrMetadata = {\n title: mrData.title as string | undefined,\n description: (mrData.description as string) ?? \"\",\n source_branch: mrData.source_branch as string | undefined,\n target_branch: mrData.target_branch as string | undefined,\n changes_count: mrData.changes_count as number | undefined,\n labels: mrData.labels as string[] | undefined,\n author: mrData.author as { username?: string; name?: string } | undefined,\n pipeline: mrData.pipeline as { status?: string; web_url?: string } | undefined,\n state: mrData.state as string | undefined,\n };\n\n if (options?.includeComments) {\n try {\n // glab --paginate concatenates JSON arrays across pages (e.g., `[...][...]`).\n // Parse each top-level array separately and merge, avoiding regex on raw JSON\n // which could corrupt string values containing `][`.\n const { stdout: rawNotes } = await exec(\n \"glab\",\n [\"api\", `projects/${encoded}/merge_requests/${mrNumber}/notes`, \"--paginate\"],\n { env },\n );\n const notes = parseGlabPaginatedJson(rawNotes);\n metadata.Notes = notes.map((n) => ({\n body: (n.body as string) ?? \"\",\n author: n.author as { username?: string; name?: string } | undefined,\n created_at: n.created_at as string | undefined,\n system: n.system as boolean | undefined,\n }));\n } catch (err) {\n logger.warn(`Failed to fetch MR notes: ${err instanceof Error ? err.message : err}`);\n }\n }\n\n return metadata;\n}\n\n/**\n * Post a comment on a GitLab merge request using glab api.\n */\nexport async function postGitlabMrComment(\n owner: string,\n repo: string,\n mrNumber: number | string,\n body: string,\n host?: string | null,\n): Promise<void> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/notes`,\n \"--method\",\n \"POST\",\n \"-H\",\n \"Content-Type: application/json\",\n \"--input\",\n \"-\",\n ],\n { env, input: JSON.stringify({ body }) },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to post comment to MR !${mrNumber}: ${msg}`);\n }\n}\n\n/**\n * Summarize GitLab notes into a human-readable bullet list.\n */\nexport function summarizeGitlabNotes(\n notes: NoteEntry[] | undefined | null,\n maxEntries = 5,\n): string {\n return summarizeNotes(notes, maxEntries, (note) => !isHodorNote(note.body));\n}\n\nexport function summarizeHodorNotes(\n notes: NoteEntry[] | undefined | null,\n maxEntries = 5,\n): string {\n return summarizeNotes(notes, maxEntries, (note) => isHodorNote(note.body));\n}\n\nfunction summarizeNotes(\n notes: NoteEntry[] | undefined | null,\n maxEntries: number,\n include: (note: NoteEntry) => boolean,\n): string {\n if (!notes || notes.length === 0) return \"\";\n\n const trivialPatterns = new Set([\n \"lgtm\",\n \"+1\",\n \"-1\",\n \"👍\",\n \"👎\",\n \"thanks\",\n \"thank you\",\n \"looks good\",\n \"approved\",\n \"🚀\",\n \"✅\",\n \"❌\",\n ]);\n\n const filtered: Array<{ username: string; body: string; createdAt: string }> = [];\n for (const note of notes) {\n if (!include(note)) continue;\n // Cache payloads are machine-only and can be large. Never feed their\n // compressed representation back into reviewer context.\n const body = (note.body ?? \"\").replace(HODOR_CACHE_MARKER_RE, \"\").trim();\n if (!body) continue;\n if (note.system) continue;\n if (body.length < 20) continue;\n\n const bodyLower = body.toLowerCase();\n let isTrivial = false;\n for (const pattern of trivialPatterns) {\n if (bodyLower.includes(pattern) && body.length < 50) {\n isTrivial = true;\n break;\n }\n }\n if (isTrivial) continue;\n\n const username =\n note.author?.username ?? note.author?.name ?? \"unknown\";\n filtered.push({ username, body, createdAt: note.created_at ?? \"\" });\n }\n\n // Sort oldest first\n filtered.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\n // Take most recent\n const recent = filtered.slice(-maxEntries);\n\n const lines: string[] = [];\n for (const { username, body, createdAt } of recent) {\n let timestampStr = \"\";\n if (createdAt) {\n try {\n const dt = new Date(createdAt);\n timestampStr = dt.toISOString().replace(\"T\", \" \").slice(0, 16);\n } catch {\n timestampStr = createdAt.slice(0, 10);\n }\n }\n\n const header = timestampStr\n ? `- ${timestampStr} @${username}:`\n : `- @${username}:`;\n const boundedBody = body.length > 2_000 ? `${body.slice(0, 1_999).trimEnd()}…` : body;\n const indentedBody = boundedBody.split(\"\\n\").join(\"\\n \");\n lines.push(`${header}\\n ${indentedBody}`);\n }\n\n return lines.join(\"\\n\");\n}\n\nexport async function getGitlabMrDiffRefs(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n): Promise<DiffRefs> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let mrData: Record<string, unknown>;\n try {\n mrData = await execJson<Record<string, unknown>>(\n \"glab\",\n [\"api\", `projects/${encoded}/merge_requests/${mrNumber}`],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to fetch diff refs for MR !${mrNumber}: ${msg}`);\n }\n\n const diffRefs = mrData.diff_refs as Record<string, unknown> | undefined;\n const base_sha = diffRefs?.base_sha;\n const head_sha = diffRefs?.head_sha;\n const start_sha = diffRefs?.start_sha;\n\n if (\n typeof base_sha !== \"string\" ||\n typeof head_sha !== \"string\" ||\n typeof start_sha !== \"string\" ||\n !base_sha ||\n !head_sha ||\n !start_sha\n ) {\n throw new GitLabAPIError(`MR !${mrNumber} has missing or incomplete diff_refs`);\n }\n\n return { base_sha, head_sha, start_sha };\n}\n\nexport async function createGitlabDraftNote(\n owner: string,\n repo: string,\n mrNumber: number | string,\n body: string,\n host?: string | null,\n opts?: { filePath?: string; line?: number; diffRefs?: DiffRefs },\n): Promise<Record<string, unknown>> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n const endpoint = `projects/${encoded}/merge_requests/${mrNumber}/draft_notes`;\n\n const payload: Record<string, unknown> = {\n note: body,\n };\n\n if (opts?.filePath && typeof opts.line === \"number\" && opts.diffRefs) {\n payload.position = {\n base_sha: opts.diffRefs.base_sha,\n head_sha: opts.diffRefs.head_sha,\n start_sha: opts.diffRefs.start_sha,\n position_type: \"text\",\n old_path: opts.filePath,\n new_path: opts.filePath,\n new_line: opts.line,\n };\n }\n\n try {\n return await execJson<Record<string, unknown>>(\n \"glab\",\n [\"api\", endpoint, \"--method\", \"POST\", \"-H\", \"Content-Type: application/json\", \"--input\", \"-\"],\n {\n env,\n input: JSON.stringify(payload),\n },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to create draft note for MR !${mrNumber}: ${msg}`);\n }\n}\n\nexport async function bulkPublishGitlabDraftNotes(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n): Promise<void> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/draft_notes/bulk_publish`,\n \"--method\",\n \"POST\",\n ],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to bulk publish draft notes for MR !${mrNumber}: ${msg}`);\n }\n}\n\ntype GitlabCommitStatusState = \"pending\" | \"running\" | \"success\" | \"failed\" | \"canceled\";\n\nexport async function postGitlabCommitStatus(\n owner: string,\n repo: string,\n sha: string,\n state: GitlabCommitStatusState,\n host?: string | null,\n opts?: { name?: string; description?: string; targetUrl?: string },\n): Promise<void> {\n const allowedStates = new Set<GitlabCommitStatusState>([\n \"pending\",\n \"running\",\n \"success\",\n \"failed\",\n \"canceled\",\n ]);\n if (!allowedStates.has(state)) {\n throw new GitLabAPIError(`Invalid GitLab commit status state: ${state}`);\n }\n\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n const endpoint = `projects/${encoded}/statuses/${sha}`;\n const payload: Record<string, unknown> = {\n state,\n name: opts?.name ?? \"hodor\",\n };\n\n if (opts?.description) {\n payload.description = opts.description;\n }\n if (opts?.targetUrl) {\n payload.target_url = opts.targetUrl;\n }\n\n try {\n await exec(\n \"glab\",\n [\"api\", endpoint, \"--method\", \"POST\", \"-H\", \"Content-Type: application/json\", \"--input\", \"-\"],\n {\n env,\n input: JSON.stringify(payload),\n },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to post commit status for ${sha}: ${msg}`);\n }\n}\n\nexport async function listHodorDiscussions(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n marker = HODOR_REVIEW_MARKER,\n): Promise<\n Array<{\n discussionId: string;\n noteId: number;\n body: string;\n resolved: boolean;\n filePath?: string;\n line?: number;\n }>\n> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let discussions: Array<Record<string, unknown>>;\n try {\n const { stdout: rawDiscussions } = await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/discussions?per_page=100`,\n \"--paginate\",\n ],\n { env },\n );\n discussions = parseGlabPaginatedJson(rawDiscussions);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to list discussions for MR !${mrNumber}: ${msg}`);\n }\n\n const results: Array<{\n discussionId: string;\n noteId: number;\n body: string;\n resolved: boolean;\n filePath?: string;\n line?: number;\n }> = [];\n\n for (const discussion of discussions) {\n const discussionId = discussion.id;\n if (typeof discussionId !== \"string\") {\n continue;\n }\n\n const notes = discussion.notes;\n if (!Array.isArray(notes)) {\n continue;\n }\n\n for (const note of notes) {\n if (!note || typeof note !== \"object\") {\n continue;\n }\n const noteObj = note as Record<string, unknown>;\n const noteId = noteObj.id;\n const body = noteObj.body;\n if (typeof noteId !== \"number\" || typeof body !== \"string\" || !isHodorNote(body, marker)) {\n continue;\n }\n\n const position =\n noteObj.position && typeof noteObj.position === \"object\"\n ? (noteObj.position as Record<string, unknown>)\n : undefined;\n\n const filePath = typeof position?.new_path === \"string\" ? position.new_path : undefined;\n const line = typeof position?.new_line === \"number\" ? position.new_line : undefined;\n\n // Skip non-resolvable threads. GitLab wraps the summary-comment note in a\n // discussion envelope with `resolvable: false`; PUT resolved=true on those\n // returns 403, independent of the caller's project role. Only diff/review\n // threads (resolvable: true) belong in the resolver's queue.\n if (noteObj.resolvable !== true) {\n continue;\n }\n\n results.push({\n discussionId,\n noteId,\n body,\n resolved: Boolean(noteObj.resolved),\n filePath,\n line,\n });\n }\n }\n\n return results;\n}\n\nexport async function resolveGitlabDiscussions(\n owner: string,\n repo: string,\n mrNumber: number | string,\n discussionIds: string[],\n host?: string | null,\n): Promise<number> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let resolvedCount = 0;\n\n for (const discussionId of discussionIds) {\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/discussions/${discussionId}`,\n \"--method\",\n \"PUT\",\n \"-H\",\n \"Content-Type: application/json\",\n \"--input\",\n \"-\",\n ],\n {\n env,\n input: JSON.stringify({ resolved: true }),\n },\n );\n resolvedCount += 1;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.warn(`Failed to resolve discussion ${discussionId} on MR !${mrNumber}: ${msg}`);\n }\n }\n\n return resolvedCount;\n}\n"],"mappings":"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","names":["stdout","stderr"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/exec.ts","../src/utils/logger.ts","../src/render.ts","../src/gitlab.ts"],"sourcesContent":["import { execFile, spawn } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n}\n\nexport interface ExecOptions {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n input?: string;\n}\n\nexport async function exec(\n cmd: string,\n args: string[],\n opts?: ExecOptions,\n): Promise<ExecResult> {\n if (typeof opts?.input === \"string\") {\n return new Promise<ExecResult>((resolve, reject) => {\n const child = spawn(cmd, args, {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer | string) => {\n stdout += chunk.toString();\n });\n child.stderr.on(\"data\", (chunk: Buffer | string) => {\n stderr += chunk.toString();\n });\n child.on(\"error\", (error) => {\n reject(error);\n });\n child.on(\"close\", (code, signal) => {\n if (code === 0) {\n resolve({ stdout, stderr });\n return;\n }\n\n const parts: string[] = [`Command failed: ${cmd} ${args.join(\" \")}`];\n if (stderr.trim()) {\n parts.push(`stderr:\\n${stderr.trim()}`);\n }\n if (stdout.trim()) {\n parts.push(`stdout:\\n${stdout.trim()}`);\n }\n if (signal) {\n parts.push(`signal: ${signal}`);\n }\n const error = new Error(parts.join(\"\\n\"));\n reject(error);\n });\n\n child.stdin.write(opts.input);\n child.stdin.end();\n });\n }\n\n const { stdout, stderr } = await execFileAsync(cmd, args, {\n cwd: opts?.cwd,\n env: opts?.env ?? process.env,\n maxBuffer: 50 * 1024 * 1024, // 50MB\n });\n return { stdout, stderr };\n}\n\nexport async function execJson<T = Record<string, unknown>>(\n cmd: string,\n args: string[],\n opts?: ExecOptions,\n): Promise<T> {\n const { stdout } = await exec(cmd, args, opts);\n return JSON.parse(stdout.trim()) as T;\n}\n","import chalk from \"chalk\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nlet currentLevel: LogLevel = \"warn\";\n\nconst LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nexport function setLogLevel(level: LogLevel): void {\n currentLevel = level;\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVELS[level] >= LEVELS[currentLevel];\n}\n\nfunction timestamp(): string {\n return new Date().toISOString();\n}\n\nexport const logger = {\n debug(msg: string): void {\n if (shouldLog(\"debug\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.gray(\"DEBUG\")} ${msg}\\n`);\n }\n },\n info(msg: string): void {\n if (shouldLog(\"info\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.blue(\"INFO\")} ${msg}\\n`);\n }\n },\n warn(msg: string): void {\n if (shouldLog(\"warn\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.yellow(\"WARN\")} ${msg}\\n`);\n }\n },\n error(msg: string): void {\n if (shouldLog(\"error\")) {\n process.stderr.write(`${chalk.gray(timestamp())} ${chalk.red(\"ERROR\")} ${msg}\\n`);\n }\n },\n};\n","/**\n * Render structured review output into clean markdown for PR/MR comments.\n */\n\nimport type { ReviewFinding, ReviewOutput } from \"./types.js\";\n\nexport const HODOR_REVIEW_MARKER = \"<!-- hodor-review -->\";\n\n/**\n * Render a ReviewOutput into clean markdown for posting as a PR/MR comment.\n */\nexport function renderMarkdown(review: ReviewOutput): string {\n const lines: string[] = [HODOR_REVIEW_MARKER];\n\n // Group findings by priority\n const critical: ReviewFinding[] = []; // P0, P1\n const important: ReviewFinding[] = []; // P2\n const minor: ReviewFinding[] = []; // P3\n\n for (const f of review.findings) {\n const p = f.priority;\n if (p <= 1) critical.push(f);\n else if (p === 2) important.push(f);\n else minor.push(f);\n }\n\n lines.push(\"### Issues Found\");\n lines.push(\"\");\n\n if (review.findings.length === 0) {\n lines.push(\"No issues found.\");\n lines.push(\"\");\n }\n\n if (critical.length > 0) {\n lines.push(\"**Critical (P0/P1)**\");\n for (const f of critical) {\n lines.push(formatFinding(f));\n }\n lines.push(\"\");\n }\n\n if (important.length > 0) {\n lines.push(\"**Important (P2)**\");\n for (const f of important) {\n lines.push(formatFinding(f));\n }\n lines.push(\"\");\n }\n\n if (minor.length > 0) {\n lines.push(\"**Minor (P3)**\");\n for (const f of minor) {\n lines.push(formatFinding(f));\n }\n lines.push(\"\");\n }\n\n // Summary\n lines.push(\"### Summary\");\n lines.push(\n `Total issues: ${critical.length} critical, ${important.length} important, ${minor.length} minor.`,\n );\n lines.push(\"\");\n\n // Overall verdict\n lines.push(\"### Overall Verdict\");\n const isCorrect = review.overall_correctness === \"patch is correct\";\n lines.push(\n `**Status**: ${isCorrect ? \"Patch is correct\" : \"Patch has blocking issues\"}`,\n );\n lines.push(\"\");\n if (review.overall_explanation) {\n lines.push(`**Explanation**: ${review.overall_explanation}`);\n }\n\n return lines.join(\"\\n\").trimEnd() + \"\\n\";\n}\n\nexport function renderSummaryMarkdown(review: ReviewOutput): string {\n const lines: string[] = [HODOR_REVIEW_MARKER];\n\n const counts = { critical: 0, important: 0, minor: 0 };\n for (const f of review.findings) {\n if (f.priority <= 1) counts.critical++;\n else if (f.priority === 2) counts.important++;\n else counts.minor++;\n }\n\n lines.push(\"\");\n lines.push(\"| Category | Count |\");\n lines.push(\"| --- | ---: |\");\n lines.push(`| Critical (P0/P1) | ${counts.critical} |`);\n lines.push(`| Important (P2) | ${counts.important} |`);\n lines.push(`| Minor (P3) | ${counts.minor} |`);\n\n const isCorrect = review.overall_correctness === \"patch is correct\";\n lines.push(\"\");\n lines.push(`**Overall verdict**: ${isCorrect ? \"Patch is correct\" : \"Patch has blocking issues\"}`);\n lines.push(\"\");\n lines.push(`**Explanation**: ${review.overall_explanation}`);\n\n if (review.findings.length > 0) {\n lines.push(\"\");\n lines.push(\"| Finding | Location | Priority |\");\n lines.push(\"| --- | --- | --- |\");\n for (const f of review.findings) {\n const loc = formatLocation(f.code_location);\n const safeTitle = f.title.replace(/\\|/g, \"\\\\|\");\n lines.push(`| ${safeTitle} | \\`${loc}\\` | P${f.priority} |`);\n }\n }\n\n return lines.join(\"\\n\").trimEnd() + \"\\n\";\n}\n\nfunction formatFinding(f: ReviewFinding): string {\n const loc = ` (\\`${formatLocation(f.code_location)}\\`)`;\n const title = `- **${f.title}**${loc}`;\n const body = ` - ${f.body}`;\n return `${title}\\n${body}`;\n}\n\nfunction formatLocation(loc: {\n absolute_file_path: string;\n line_range: { start: number; end: number };\n}): string {\n // Strip common workspace prefixes to get a clean relative path\n let filePath = loc.absolute_file_path;\n\n // GitLab CI: /builds/owner/repo/src/file.ts → src/file.ts\n const buildsMatch = filePath.match(/\\/builds\\/[^/]+\\/[^/]+\\/(.+)/);\n if (buildsMatch) {\n filePath = buildsMatch[1];\n }\n // GitHub Actions / generic workspace\n else if (filePath.includes(\"/workspace/\")) {\n filePath = filePath.slice(filePath.indexOf(\"/workspace/\") + \"/workspace/\".length);\n }\n // Temp review dirs: /tmp/hodor-review-<id>/src/file.ts → src/file.ts\n else {\n filePath = filePath.replace(/^.*\\/hodor-review-[^/]+\\//, \"\");\n }\n\n const { start, end } = loc.line_range;\n return start === end ? `${filePath}:${start}` : `${filePath}:${start}-${end}`;\n}\n","import { exec, execJson } from \"./utils/exec.js\";\nimport { logger } from \"./utils/logger.js\";\nimport type { MrMetadata, NoteEntry } from \"./types.js\";\nimport { HODOR_REVIEW_MARKER } from \"./render.js\";\n\nexport { HODOR_REVIEW_MARKER };\n\nexport interface DiffRefs {\n base_sha: string;\n head_sha: string;\n start_sha: string;\n}\n\nconst DEFAULT_GITLAB_HOST = \"gitlab.com\";\n\n/**\n * Match notes Hodor itself created. The body must begin with a hodor-owned HTML\n * comment (either `<!-- hodor-review -->` or a sibling like `<!-- hodor:sha:... -->`\n * that hodor prepends to summary comments). Anchoring at the start avoids deleting\n * human notes that quote the marker incidentally (e.g., a code block discussing hodor).\n */\nconst HODOR_NOTE_PREFIX_RE = /^\\s*<!--\\s*hodor[-:]/;\nconst HODOR_CACHE_MARKER_RE = /<!--\\s*hodor:cache:v1:[A-Za-z0-9_-]+\\s*-->\\s*/g;\n\nfunction isHodorNote(body: unknown, marker = HODOR_REVIEW_MARKER): boolean {\n if (typeof body !== \"string\") return false;\n // Default fast-path: body starts with the canonical marker (allowing leading whitespace).\n if (body.trimStart().startsWith(marker)) return true;\n // Accept hodor's own SHA prefix, e.g. `<!-- hodor:sha:abc -->\\n<!-- hodor-review -->\\n...`\n if (marker === HODOR_REVIEW_MARKER && HODOR_NOTE_PREFIX_RE.test(body)) {\n // Require the canonical marker to appear somewhere in the body so we don't\n // resolve unrelated `<!-- hodor:foo -->` notes that aren't review summaries.\n return body.includes(HODOR_REVIEW_MARKER);\n }\n return false;\n}\n\nexport function isHodorGeneratedNote(body: unknown): boolean {\n return isHodorNote(body);\n}\n\n/**\n * Parse concatenated JSON arrays from `glab api --paginate`.\n * glab outputs `[...][...][...]` — one array per page, no delimiter.\n * We track bracket depth (respecting strings/escapes) to find each\n * top-level array, parse them individually, and merge with flat().\n */\nexport function parseGlabPaginatedJson(raw: string): Array<Record<string, unknown>> {\n const trimmed = raw.trim();\n if (!trimmed) return [];\n\n const chunks: string[] = [];\n let depth = 0;\n let inString = false;\n let escaped = false;\n let start = -1;\n\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (escaped) {\n escaped = false;\n continue;\n }\n if (ch === \"\\\\\" && inString) {\n escaped = true;\n continue;\n }\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n if (inString) continue;\n\n if (ch === \"[\") {\n if (depth === 0) start = i;\n depth++;\n } else if (ch === \"]\") {\n depth--;\n if (depth === 0 && start >= 0) {\n chunks.push(trimmed.slice(start, i + 1));\n start = -1;\n }\n }\n }\n\n const results: Array<Record<string, unknown>> = [];\n for (const chunk of chunks) {\n try {\n const parsed = JSON.parse(chunk) as Array<Record<string, unknown>>;\n if (Array.isArray(parsed)) results.push(...parsed);\n } catch (err) {\n logger.warn(\n `Skipping malformed glab pagination chunk: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n return results;\n}\n\nexport class GitLabAPIError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"GitLabAPIError\";\n }\n}\n\nfunction normalizeBaseUrl(host?: string | null): string {\n const candidate =\n host ||\n process.env.GITLAB_HOST ||\n process.env.CI_SERVER_URL ||\n DEFAULT_GITLAB_HOST;\n const trimmed = candidate.trim() || DEFAULT_GITLAB_HOST;\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed.replace(/\\/+$/, \"\");\n }\n return `https://${trimmed}`.replace(/\\/+$/, \"\");\n}\n\nfunction encodedProjectPath(owner: string, repo: string): string {\n const projectPath = [owner.replace(/^\\/+|\\/+$/g, \"\"), repo.replace(/^\\/+|\\/+$/g, \"\")]\n .filter(Boolean)\n .join(\"/\");\n return encodeURIComponent(projectPath);\n}\n\nfunction glabEnv(host?: string | null): NodeJS.ProcessEnv {\n const env = { ...process.env };\n // Ensure glab knows which host to talk to\n const baseUrl = normalizeBaseUrl(host);\n const hostname = baseUrl.replace(/^https?:\\/\\//, \"\");\n env.GITLAB_HOST = hostname;\n return env;\n}\n\n/**\n * Fetch merge request metadata using glab api.\n */\nexport async function fetchGitlabMrInfo(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n options?: { includeComments?: boolean },\n): Promise<MrMetadata> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let mrData: Record<string, unknown>;\n try {\n mrData = await execJson<Record<string, unknown>>(\n \"glab\",\n [\"api\", `projects/${encoded}/merge_requests/${mrNumber}`],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to fetch MR !${mrNumber}: ${msg}`);\n }\n\n const metadata: MrMetadata = {\n title: mrData.title as string | undefined,\n description: (mrData.description as string) ?? \"\",\n source_branch: mrData.source_branch as string | undefined,\n target_branch: mrData.target_branch as string | undefined,\n changes_count: mrData.changes_count as number | undefined,\n labels: mrData.labels as string[] | undefined,\n author: mrData.author as { username?: string; name?: string } | undefined,\n pipeline: mrData.pipeline as { status?: string; web_url?: string } | undefined,\n state: mrData.state as string | undefined,\n };\n\n if (options?.includeComments) {\n try {\n // glab --paginate concatenates JSON arrays across pages (e.g., `[...][...]`).\n // Parse each top-level array separately and merge, avoiding regex on raw JSON\n // which could corrupt string values containing `][`.\n const { stdout: rawNotes } = await exec(\n \"glab\",\n [\"api\", `projects/${encoded}/merge_requests/${mrNumber}/notes`, \"--paginate\"],\n { env },\n );\n const notes = parseGlabPaginatedJson(rawNotes);\n metadata.Notes = notes.map((n) => ({\n body: (n.body as string) ?? \"\",\n author: n.author as { username?: string; name?: string } | undefined,\n created_at: n.created_at as string | undefined,\n system: n.system as boolean | undefined,\n }));\n } catch (err) {\n logger.warn(`Failed to fetch MR notes: ${err instanceof Error ? err.message : err}`);\n }\n }\n\n return metadata;\n}\n\n/**\n * Post a comment on a GitLab merge request using glab api.\n */\nexport async function postGitlabMrComment(\n owner: string,\n repo: string,\n mrNumber: number | string,\n body: string,\n host?: string | null,\n): Promise<void> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/notes`,\n \"--method\",\n \"POST\",\n \"-H\",\n \"Content-Type: application/json\",\n \"--input\",\n \"-\",\n ],\n { env, input: JSON.stringify({ body }) },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to post comment to MR !${mrNumber}: ${msg}`);\n }\n}\n\n/**\n * Summarize GitLab notes into a human-readable bullet list.\n */\nexport function summarizeGitlabNotes(\n notes: NoteEntry[] | undefined | null,\n maxEntries = 5,\n): string {\n return summarizeNotes(notes, maxEntries, (note) => !isHodorNote(note.body));\n}\n\nexport function summarizeHodorNotes(\n notes: NoteEntry[] | undefined | null,\n maxEntries = 5,\n): string {\n return summarizeNotes(notes, maxEntries, (note) => isHodorNote(note.body));\n}\n\nfunction summarizeNotes(\n notes: NoteEntry[] | undefined | null,\n maxEntries: number,\n include: (note: NoteEntry) => boolean,\n): string {\n if (!notes || notes.length === 0) return \"\";\n\n const trivialPatterns = new Set([\n \"lgtm\",\n \"+1\",\n \"-1\",\n \"👍\",\n \"👎\",\n \"thanks\",\n \"thank you\",\n \"looks good\",\n \"approved\",\n \"🚀\",\n \"✅\",\n \"❌\",\n ]);\n\n const filtered: Array<{ username: string; body: string; createdAt: string }> = [];\n for (const note of notes) {\n if (!include(note)) continue;\n // Cache payloads are machine-only and can be large. Never feed their\n // compressed representation back into reviewer context.\n const body = (note.body ?? \"\").replace(HODOR_CACHE_MARKER_RE, \"\").trim();\n if (!body) continue;\n if (note.system) continue;\n if (body.length < 20) continue;\n\n const bodyLower = body.toLowerCase();\n let isTrivial = false;\n for (const pattern of trivialPatterns) {\n if (bodyLower.includes(pattern) && body.length < 50) {\n isTrivial = true;\n break;\n }\n }\n if (isTrivial) continue;\n\n const username =\n note.author?.username ?? note.author?.name ?? \"unknown\";\n filtered.push({ username, body, createdAt: note.created_at ?? \"\" });\n }\n\n // Sort oldest first\n filtered.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n\n // Take most recent\n const recent = filtered.slice(-maxEntries);\n\n const lines: string[] = [];\n for (const { username, body, createdAt } of recent) {\n let timestampStr = \"\";\n if (createdAt) {\n try {\n const dt = new Date(createdAt);\n timestampStr = dt.toISOString().replace(\"T\", \" \").slice(0, 16);\n } catch {\n timestampStr = createdAt.slice(0, 10);\n }\n }\n\n const header = timestampStr\n ? `- ${timestampStr} @${username}:`\n : `- @${username}:`;\n const boundedBody = body.length > 2_000 ? `${body.slice(0, 1_999).trimEnd()}…` : body;\n const indentedBody = boundedBody.split(\"\\n\").join(\"\\n \");\n lines.push(`${header}\\n ${indentedBody}`);\n }\n\n return lines.join(\"\\n\");\n}\n\nexport async function getGitlabMrDiffRefs(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n): Promise<DiffRefs> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let mrData: Record<string, unknown>;\n try {\n mrData = await execJson<Record<string, unknown>>(\n \"glab\",\n [\"api\", `projects/${encoded}/merge_requests/${mrNumber}`],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to fetch diff refs for MR !${mrNumber}: ${msg}`);\n }\n\n const diffRefs = mrData.diff_refs as Record<string, unknown> | undefined;\n const base_sha = diffRefs?.base_sha;\n const head_sha = diffRefs?.head_sha;\n const start_sha = diffRefs?.start_sha;\n\n if (\n typeof base_sha !== \"string\" ||\n typeof head_sha !== \"string\" ||\n typeof start_sha !== \"string\" ||\n !base_sha ||\n !head_sha ||\n !start_sha\n ) {\n throw new GitLabAPIError(`MR !${mrNumber} has missing or incomplete diff_refs`);\n }\n\n return { base_sha, head_sha, start_sha };\n}\n\nexport async function createGitlabDraftNote(\n owner: string,\n repo: string,\n mrNumber: number | string,\n body: string,\n host?: string | null,\n opts?: { filePath?: string; line?: number; diffRefs?: DiffRefs },\n): Promise<Record<string, unknown>> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n const endpoint = `projects/${encoded}/merge_requests/${mrNumber}/draft_notes`;\n\n const payload: Record<string, unknown> = {\n note: body,\n };\n\n if (opts?.filePath && typeof opts.line === \"number\" && opts.diffRefs) {\n payload.position = {\n base_sha: opts.diffRefs.base_sha,\n head_sha: opts.diffRefs.head_sha,\n start_sha: opts.diffRefs.start_sha,\n position_type: \"text\",\n old_path: opts.filePath,\n new_path: opts.filePath,\n new_line: opts.line,\n };\n }\n\n try {\n return await execJson<Record<string, unknown>>(\n \"glab\",\n [\"api\", endpoint, \"--method\", \"POST\", \"-H\", \"Content-Type: application/json\", \"--input\", \"-\"],\n {\n env,\n input: JSON.stringify(payload),\n },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to create draft note for MR !${mrNumber}: ${msg}`);\n }\n}\n\nexport async function bulkPublishGitlabDraftNotes(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n): Promise<void> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/draft_notes/bulk_publish`,\n \"--method\",\n \"POST\",\n ],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to bulk publish draft notes for MR !${mrNumber}: ${msg}`);\n }\n}\n\nexport async function publishGitlabDraftNote(\n owner: string,\n repo: string,\n mrNumber: number | string,\n draftNoteId: number | string,\n host?: string | null,\n): Promise<void> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/draft_notes/${draftNoteId}/publish`,\n \"--method\",\n \"PUT\",\n ],\n { env },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to publish draft note ${draftNoteId} for MR !${mrNumber}: ${msg}`);\n }\n}\n\ntype GitlabCommitStatusState = \"pending\" | \"running\" | \"success\" | \"failed\" | \"canceled\";\n\nexport async function postGitlabCommitStatus(\n owner: string,\n repo: string,\n sha: string,\n state: GitlabCommitStatusState,\n host?: string | null,\n opts?: { name?: string; description?: string; targetUrl?: string },\n): Promise<void> {\n const allowedStates = new Set<GitlabCommitStatusState>([\n \"pending\",\n \"running\",\n \"success\",\n \"failed\",\n \"canceled\",\n ]);\n if (!allowedStates.has(state)) {\n throw new GitLabAPIError(`Invalid GitLab commit status state: ${state}`);\n }\n\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n const endpoint = `projects/${encoded}/statuses/${sha}`;\n const payload: Record<string, unknown> = {\n state,\n name: opts?.name ?? \"hodor\",\n };\n\n if (opts?.description) {\n payload.description = opts.description;\n }\n if (opts?.targetUrl) {\n payload.target_url = opts.targetUrl;\n }\n\n try {\n await exec(\n \"glab\",\n [\"api\", endpoint, \"--method\", \"POST\", \"-H\", \"Content-Type: application/json\", \"--input\", \"-\"],\n {\n env,\n input: JSON.stringify(payload),\n },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to post commit status for ${sha}: ${msg}`);\n }\n}\n\nexport async function listHodorDiscussions(\n owner: string,\n repo: string,\n mrNumber: number | string,\n host?: string | null,\n marker = HODOR_REVIEW_MARKER,\n): Promise<\n Array<{\n discussionId: string;\n noteId: number;\n body: string;\n resolved: boolean;\n filePath?: string;\n line?: number;\n }>\n> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let discussions: Array<Record<string, unknown>>;\n try {\n const { stdout: rawDiscussions } = await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/discussions?per_page=100`,\n \"--paginate\",\n ],\n { env },\n );\n discussions = parseGlabPaginatedJson(rawDiscussions);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new GitLabAPIError(`Failed to list discussions for MR !${mrNumber}: ${msg}`);\n }\n\n const results: Array<{\n discussionId: string;\n noteId: number;\n body: string;\n resolved: boolean;\n filePath?: string;\n line?: number;\n }> = [];\n\n for (const discussion of discussions) {\n const discussionId = discussion.id;\n if (typeof discussionId !== \"string\") {\n continue;\n }\n\n const notes = discussion.notes;\n if (!Array.isArray(notes)) {\n continue;\n }\n\n for (const note of notes) {\n if (!note || typeof note !== \"object\") {\n continue;\n }\n const noteObj = note as Record<string, unknown>;\n const noteId = noteObj.id;\n const body = noteObj.body;\n if (typeof noteId !== \"number\" || typeof body !== \"string\" || !isHodorNote(body, marker)) {\n continue;\n }\n\n const position =\n noteObj.position && typeof noteObj.position === \"object\"\n ? (noteObj.position as Record<string, unknown>)\n : undefined;\n\n const filePath = typeof position?.new_path === \"string\" ? position.new_path : undefined;\n const line = typeof position?.new_line === \"number\" ? position.new_line : undefined;\n\n // Skip non-resolvable threads. GitLab wraps the summary-comment note in a\n // discussion envelope with `resolvable: false`; PUT resolved=true on those\n // returns 403, independent of the caller's project role. Only diff/review\n // threads (resolvable: true) belong in the resolver's queue.\n if (noteObj.resolvable !== true) {\n continue;\n }\n\n results.push({\n discussionId,\n noteId,\n body,\n resolved: Boolean(noteObj.resolved),\n filePath,\n line,\n });\n }\n }\n\n return results;\n}\n\nexport async function resolveGitlabDiscussions(\n owner: string,\n repo: string,\n mrNumber: number | string,\n discussionIds: string[],\n host?: string | null,\n): Promise<number> {\n const encoded = encodedProjectPath(owner, repo);\n const env = glabEnv(host);\n\n let resolvedCount = 0;\n\n for (const discussionId of discussionIds) {\n try {\n await exec(\n \"glab\",\n [\n \"api\",\n `projects/${encoded}/merge_requests/${mrNumber}/discussions/${discussionId}`,\n \"--method\",\n \"PUT\",\n \"-H\",\n \"Content-Type: application/json\",\n \"--input\",\n \"-\",\n ],\n {\n env,\n input: JSON.stringify({ resolved: true }),\n },\n );\n resolvedCount += 1;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.warn(`Failed to resolve discussion ${discussionId} on MR !${mrNumber}: ${msg}`);\n }\n }\n\n return resolvedCount;\n}\n"],"mappings":"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","names":["stdout","stderr"]}
|
|
@@ -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-
|
|
19
|
+
} from "./chunk-DALI4QRT.js";
|
|
19
20
|
import {
|
|
20
21
|
relativizeWorkspacePath
|
|
21
22
|
} from "./chunk-AMUK6GDX.js";
|
|
@@ -42,7 +43,8 @@ function buildPrReviewPrompt(opts) {
|
|
|
42
43
|
previousReviewSha,
|
|
43
44
|
reviewDiffMode,
|
|
44
45
|
changedFiles = [],
|
|
45
|
-
localMode = false
|
|
46
|
+
localMode = false,
|
|
47
|
+
singleTurn = false
|
|
46
48
|
} = opts;
|
|
47
49
|
let templateText;
|
|
48
50
|
try {
|
|
@@ -92,22 +94,24 @@ function buildPrReviewPrompt(opts) {
|
|
|
92
94
|
diffExplanation = `**Three-dot syntax** shows ONLY changes introduced on the source branch, excluding changes already on \`${targetBranch}\`.`;
|
|
93
95
|
}
|
|
94
96
|
const { contextSection, notesSection, reminderSection } = buildMrSections(mrMetadata);
|
|
97
|
+
const oneTurn = singleTurn && Boolean(embeddedDiff);
|
|
95
98
|
let incrementalSection = "";
|
|
96
99
|
if (previousReviewSha) {
|
|
97
100
|
incrementalSection = `## ${reviewDiffMode === "snapshot" ? "Snapshot Delta" : "Incremental Review"} Mode
|
|
98
101
|
|
|
99
|
-
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.\
|
|
102
|
+
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";
|
|
100
103
|
}
|
|
101
104
|
let embeddedDiffSection;
|
|
102
105
|
let diffFetchInstructions;
|
|
103
106
|
let reviewProcessSection;
|
|
104
107
|
let startInstruction;
|
|
108
|
+
let runtimeToolsSection;
|
|
105
109
|
if (embeddedDiff) {
|
|
106
110
|
const changedFileManifest = changedFiles.length > 0 ? `
|
|
107
111
|
Changed files (${changedFiles.length}):
|
|
108
112
|
${changedFiles.map((file) => `- \`${file}\``).join("\n")}
|
|
109
113
|
` : "";
|
|
110
|
-
embeddedDiffSection = "## Full Diff (Pre-fetched)\n\nThe complete diff for this PR is provided below. Analyze it directly. Do not run another command to list changed files. Use `read` or `grep` only if you need additional file context beyond what the diff shows.\n" + changedFileManifest + "\n````diff\n" + embeddedDiff + "\n````\n";
|
|
114
|
+
embeddedDiffSection = "## Full Diff (Pre-fetched)\n\nThe complete diff for this PR is provided below. Analyze it directly. Do not run another command to list changed files. " + (oneTurn ? "This diff is small and self-contained; it is the complete basis for your review.\n" : "Use `read` or `grep` only if you need additional file context beyond what the diff shows.\n") + changedFileManifest + "\n````diff\n" + embeddedDiff + "\n````\n";
|
|
111
115
|
diffFetchInstructions = `## Review the Diff Above
|
|
112
116
|
|
|
113
117
|
### Critical Rules
|
|
@@ -118,8 +122,13 @@ ${changedFiles.map((file) => `- \`${file}\``).join("\n")}
|
|
|
118
122
|
- NEVER flag "dependency version downgrade" (branch not rebased)
|
|
119
123
|
- NEVER compare entire codebase to ${targetBranch} - DIFF ONLY
|
|
120
124
|
`;
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
if (oneTurn) {
|
|
126
|
+
reviewProcessSection = "## Review Process\n\n1. Analyze the embedded diff above thoroughly\n2. Call `submit_review` in this same turn with your findings\n\nNo file-inspection tools are available for this review. Base every finding on the diff above. If a potential issue cannot be established from the diff alone, do not report it.\n";
|
|
127
|
+
startInstruction = "Analyze the diff above and call `submit_review` now, in this turn.";
|
|
128
|
+
} else {
|
|
129
|
+
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 = 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`.";
|
|
131
|
+
}
|
|
123
132
|
} else {
|
|
124
133
|
embeddedDiffSection = "";
|
|
125
134
|
diffFetchInstructions = "## Step 1: List Changed Files (MANDATORY FIRST STEP)\n\n**Run this command FIRST to get the list of changed files:**\n```bash\n" + prDiffCmd + "\n```\n\nThis lists ONLY the filenames changed in this PR. **Do NOT dump the entire diff here** - you'll inspect each file individually in Step 2. Only review files that appear in this output.\n\n## Step 2: Review Changed Files Only\n\n### Critical Rules\n- ONLY review files that appear in the diff from Step 1\n- ONLY analyze actual code changes (+ and - lines in the diff)\n- Use the most reliable diff command: `" + gitDiffCmd + `\`
|
|
@@ -146,7 +155,15 @@ ${changedFiles.map((file) => `- \`${file}\``).join("\n")}
|
|
|
146
155
|
`;
|
|
147
156
|
startInstruction = `Start by running \`${prDiffCmd}\` to list the changed files, then analyze each file individually using \`${gitDiffCmd} -- path/to/file\`.`;
|
|
148
157
|
}
|
|
149
|
-
|
|
158
|
+
runtimeToolsSection = oneTurn ? "## Runtime Tools\n\n- `submit_review` submits the completed review. It is the only tool available for this review.\n" : `## Runtime Tools
|
|
159
|
+
|
|
160
|
+
- \`${prDiffCmd}\` lists the changed files when a diff is not embedded.
|
|
161
|
+
- \`${gitDiffCmd} -- path/to/file\` shows the delta for one changed file.
|
|
162
|
+
- \`read\` provides bounded surrounding context.
|
|
163
|
+
- \`grep\` searches for directly relevant code and contracts.
|
|
164
|
+
- \`submit_review\` submits the completed review.
|
|
165
|
+
`;
|
|
166
|
+
return templateText.replace(/\{pr_url\}/g, prUrl).replace(/\{pr_diff_cmd\}/g, prDiffCmd).replace(/\{git_diff_cmd\}/g, gitDiffCmd).replace(/\{mr_context_section\}/g, contextSection).replace(/\{mr_notes_section\}/g, notesSection).replace(/\{mr_reminder_section\}/g, reminderSection).replace(/\{incremental_section\}/g, incrementalSection).replace(/\{embedded_diff_section\}/g, embeddedDiffSection).replace(/\{diff_fetch_instructions\}/g, diffFetchInstructions).replace(/\{runtime_tools_section\}/g, runtimeToolsSection).replace(/\{review_process_section\}/g, reviewProcessSection).replace(/\{start_instruction\}/g, startInstruction);
|
|
150
167
|
}
|
|
151
168
|
function buildMrSections(mrMetadata) {
|
|
152
169
|
if (!mrMetadata) {
|
|
@@ -265,6 +282,17 @@ function parseModelString(model) {
|
|
|
265
282
|
if (modelId.startsWith("converse/")) {
|
|
266
283
|
modelId = modelId.slice("converse/".length);
|
|
267
284
|
}
|
|
285
|
+
const separator = modelId.indexOf("@");
|
|
286
|
+
if (separator !== -1) {
|
|
287
|
+
const baseModelId = modelId.slice(separator + 1).trim();
|
|
288
|
+
modelId = modelId.slice(0, separator).trim();
|
|
289
|
+
if (!modelId || !baseModelId) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Invalid bedrock model "${trimmed}". Use "bedrock/<arn>@<base-model-id>", for example "bedrock/arn:aws:bedrock:ap-south-1:123:application-inference-profile/abc@global.anthropic.claude-opus-5".`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return { provider, modelId, baseModelId };
|
|
295
|
+
}
|
|
268
296
|
return { provider, modelId };
|
|
269
297
|
}
|
|
270
298
|
if (knownProviders.has(provider)) {
|
|
@@ -286,6 +314,36 @@ function parseModelString(model) {
|
|
|
286
314
|
}
|
|
287
315
|
return { provider: "anthropic", modelId: trimmed };
|
|
288
316
|
}
|
|
317
|
+
function extractBedrockArnRegion(arn) {
|
|
318
|
+
const parts = arn.split(":");
|
|
319
|
+
return parts.length >= 4 && parts[3] ? parts[3] : "us-east-1";
|
|
320
|
+
}
|
|
321
|
+
function buildBedrockArnModel(opts) {
|
|
322
|
+
const region = opts.region ?? extractBedrockArnRegion(opts.arn);
|
|
323
|
+
const baseUrl = `https://bedrock-runtime.${region}.amazonaws.com`;
|
|
324
|
+
if (opts.baseModel) {
|
|
325
|
+
return {
|
|
326
|
+
...opts.baseModel,
|
|
327
|
+
id: opts.arn,
|
|
328
|
+
// Keep the registry id as the name so pi-ai's capability matchers still
|
|
329
|
+
// see the vendor and model version behind the ARN.
|
|
330
|
+
name: opts.baseModel.id,
|
|
331
|
+
baseUrl
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
id: opts.arn,
|
|
336
|
+
name: opts.arn,
|
|
337
|
+
api: "bedrock-converse-stream",
|
|
338
|
+
provider: "amazon-bedrock",
|
|
339
|
+
baseUrl,
|
|
340
|
+
reasoning: false,
|
|
341
|
+
input: ["text"],
|
|
342
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
343
|
+
contextWindow: 2e5,
|
|
344
|
+
maxTokens: 16384
|
|
345
|
+
};
|
|
346
|
+
}
|
|
289
347
|
function mapReasoningEffort(effort) {
|
|
290
348
|
if (!effort) return void 0;
|
|
291
349
|
switch (effort.toLowerCase()) {
|
|
@@ -315,16 +373,26 @@ function getDefaultReasoningEffortForModel(model) {
|
|
|
315
373
|
}
|
|
316
374
|
var HIGH_RISK_PATH_RE = /(?:^|\/)(?:migrations?|schema|auth|security|permissions?|crypto|iam)(?:\/|\.|$)|\.tf$/im;
|
|
317
375
|
var HIGH_RISK_CHANGE_RE = /^\+.*\b(?:authorization|authentication|permission|transaction|mutex|semaphore|encrypt|decrypt|credential|secret)\b/im;
|
|
376
|
+
function isHighRiskDiff(diff) {
|
|
377
|
+
return HIGH_RISK_PATH_RE.test(diff) || HIGH_RISK_CHANGE_RE.test(diff);
|
|
378
|
+
}
|
|
379
|
+
var SINGLE_TURN_MAX_FILES = 5;
|
|
380
|
+
var SINGLE_TURN_MAX_CHANGED_LINES = 50;
|
|
381
|
+
function qualifiesForSingleTurnReview(opts) {
|
|
382
|
+
if (!opts.embedded || !opts.stats) return false;
|
|
383
|
+
const { files, additions, deletions } = opts.stats;
|
|
384
|
+
if (files === 0 || files > SINGLE_TURN_MAX_FILES) return false;
|
|
385
|
+
if (additions + deletions > SINGLE_TURN_MAX_CHANGED_LINES) return false;
|
|
386
|
+
if (opts.diff && isHighRiskDiff(opts.diff)) return false;
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
318
389
|
function selectReasoningEffort(opts) {
|
|
319
390
|
const requested = mapReasoningEffort(opts.requested);
|
|
320
391
|
if (requested) return requested;
|
|
321
392
|
const modelDefault = opts.modelDefault;
|
|
322
393
|
if (modelDefault !== "xhigh") return modelDefault;
|
|
323
394
|
if (opts.forcedFull) return modelDefault;
|
|
324
|
-
|
|
325
|
-
opts.diff && (HIGH_RISK_PATH_RE.test(opts.diff) || HIGH_RISK_CHANGE_RE.test(opts.diff))
|
|
326
|
-
);
|
|
327
|
-
if (risky) return modelDefault;
|
|
395
|
+
if (opts.diff && isHighRiskDiff(opts.diff)) return modelDefault;
|
|
328
396
|
if (opts.mode === "incremental" || opts.mode === "snapshot") return "high";
|
|
329
397
|
const changedLines = (opts.stats?.additions ?? 0) + (opts.stats?.deletions ?? 0);
|
|
330
398
|
if (opts.stats && opts.stats.files <= 10 && changedLines <= 500) return "high";
|
|
@@ -466,6 +534,9 @@ async function pushMetrics(opts) {
|
|
|
466
534
|
`# HELP hodor_review_reused Whether an existing identical-HEAD review was reused`,
|
|
467
535
|
`# TYPE hodor_review_reused gauge`,
|
|
468
536
|
`hodor_review_reused${labelSuffix} ${metrics.reused ? 1 : 0}`,
|
|
537
|
+
`# HELP hodor_review_fast_path Whether the single-turn tiny-diff fast path gated tools to submit_review`,
|
|
538
|
+
`# TYPE hodor_review_fast_path gauge`,
|
|
539
|
+
`hodor_review_fast_path${labelSuffix} ${metrics.fastPath ? 1 : 0}`,
|
|
469
540
|
""
|
|
470
541
|
];
|
|
471
542
|
const body = lines.join("\n");
|
|
@@ -1074,6 +1145,7 @@ async function postReviewStructured(opts) {
|
|
|
1074
1145
|
let inlineCreated = 0;
|
|
1075
1146
|
let inlineFailed = 0;
|
|
1076
1147
|
let inlineDeduplicated = 0;
|
|
1148
|
+
const draftNoteIds = [];
|
|
1077
1149
|
for (const finding of review.findings) {
|
|
1078
1150
|
const fingerprint = getFindingFingerprint(finding, workspacePath);
|
|
1079
1151
|
if (existingByFingerprint.has(fingerprint)) {
|
|
@@ -1100,7 +1172,7 @@ ${finding.suggestion}
|
|
|
1100
1172
|
\`\`\``;
|
|
1101
1173
|
}
|
|
1102
1174
|
try {
|
|
1103
|
-
await createGitlabDraftNote(
|
|
1175
|
+
const draftNote = await createGitlabDraftNote(
|
|
1104
1176
|
parsed.owner,
|
|
1105
1177
|
parsed.repo,
|
|
1106
1178
|
parsed.prNumber,
|
|
@@ -1112,6 +1184,9 @@ ${finding.suggestion}
|
|
|
1112
1184
|
diffRefs
|
|
1113
1185
|
}
|
|
1114
1186
|
);
|
|
1187
|
+
if (typeof draftNote.id === "number" || typeof draftNote.id === "string") {
|
|
1188
|
+
draftNoteIds.push(draftNote.id);
|
|
1189
|
+
}
|
|
1115
1190
|
inlineCreated++;
|
|
1116
1191
|
} catch (error) {
|
|
1117
1192
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1135,8 +1210,32 @@ ${finding.suggestion}
|
|
|
1135
1210
|
draftsPublished = true;
|
|
1136
1211
|
} catch (error) {
|
|
1137
1212
|
const message = error instanceof Error ? error.message : String(error);
|
|
1138
|
-
errors.push(`draft publish: ${message}`);
|
|
1139
1213
|
logger.warn(`Failed to bulk publish draft notes: ${message}`);
|
|
1214
|
+
if (draftNoteIds.length === inlineCreated) {
|
|
1215
|
+
let individuallyPublished = 0;
|
|
1216
|
+
for (const draftNoteId of draftNoteIds) {
|
|
1217
|
+
try {
|
|
1218
|
+
await publishGitlabDraftNote(
|
|
1219
|
+
parsed.owner,
|
|
1220
|
+
parsed.repo,
|
|
1221
|
+
parsed.prNumber,
|
|
1222
|
+
draftNoteId,
|
|
1223
|
+
parsed.host
|
|
1224
|
+
);
|
|
1225
|
+
individuallyPublished++;
|
|
1226
|
+
} catch (publishError) {
|
|
1227
|
+
const publishMessage = publishError instanceof Error ? publishError.message : String(publishError);
|
|
1228
|
+
errors.push(`draft publish: ${publishMessage}`);
|
|
1229
|
+
logger.warn(`Failed to publish draft note ${draftNoteId}: ${publishMessage}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
draftsPublished = individuallyPublished === inlineCreated;
|
|
1233
|
+
if (draftsPublished) {
|
|
1234
|
+
logger.info(`Published ${individuallyPublished} draft note(s) individually`);
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
errors.push(`draft publish: ${message}`);
|
|
1238
|
+
}
|
|
1140
1239
|
}
|
|
1141
1240
|
}
|
|
1142
1241
|
let summaryPosted = false;
|
|
@@ -2102,7 +2201,8 @@ async function reviewPr(opts) {
|
|
|
2102
2201
|
localMode = false,
|
|
2103
2202
|
diffAgainst,
|
|
2104
2203
|
full = false,
|
|
2105
|
-
targetBranchOverride
|
|
2204
|
+
targetBranchOverride,
|
|
2205
|
+
tinyDiffFastPath = false
|
|
2106
2206
|
} = opts;
|
|
2107
2207
|
const effectiveReviewInstructions = reviewInstructions == null ? loadDefaultReviewInstructions() : validateReviewInstructions(reviewInstructions, "review instructions");
|
|
2108
2208
|
const effectiveAdditionalInstructions = additionalInstructions == null ? null : validateReviewInstructions(additionalInstructions, "additional instructions");
|
|
@@ -2137,24 +2237,26 @@ async function reviewPr(opts) {
|
|
|
2137
2237
|
}
|
|
2138
2238
|
let piModel = modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
2139
2239
|
if (parsed.modelId.startsWith("arn:")) {
|
|
2140
|
-
const
|
|
2141
|
-
const region = arnParts.length >= 4 ? arnParts[3] : "us-east-1";
|
|
2240
|
+
const region = extractBedrockArnRegion(parsed.modelId);
|
|
2142
2241
|
if (!process.env.AWS_REGION && !process.env.AWS_DEFAULT_REGION) {
|
|
2143
2242
|
process.env.AWS_REGION = region;
|
|
2144
2243
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2244
|
+
const baseModel = parsed.baseModelId ? modelRuntime.getModel(parsed.provider, parsed.baseModelId) : void 0;
|
|
2245
|
+
if (parsed.baseModelId && !baseModel) {
|
|
2246
|
+
throw new Error(
|
|
2247
|
+
`Base model "${parsed.baseModelId}" for Bedrock ARN "${parsed.modelId}" was not found in the installed pi-ai registry.`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
piModel = buildBedrockArnModel({ arn: parsed.modelId, baseModel, region });
|
|
2251
|
+
if (baseModel) {
|
|
2252
|
+
logger.info(
|
|
2253
|
+
`Custom bedrock ARN model \u2014 region: ${region}, capabilities from ${baseModel.id}`
|
|
2254
|
+
);
|
|
2255
|
+
} else {
|
|
2256
|
+
logger.warn(
|
|
2257
|
+
`Custom bedrock ARN model \u2014 region: ${region}, no base model given. Prompt caching, reasoning, and cost reporting are disabled. Append "@<base-model-id>" to the model string (e.g. "${model}@global.anthropic.claude-opus-5") to restore them.`
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2158
2260
|
} else if (!piModel) {
|
|
2159
2261
|
if (parsed.provider === "openrouter") {
|
|
2160
2262
|
piModel = {
|
|
@@ -2315,13 +2417,19 @@ async function reviewPr(opts) {
|
|
|
2315
2417
|
diffAdditions: 0,
|
|
2316
2418
|
diffDeletions: 0,
|
|
2317
2419
|
diffBytes: 0,
|
|
2318
|
-
reused: true
|
|
2420
|
+
reused: true,
|
|
2421
|
+
fastPath: false
|
|
2319
2422
|
};
|
|
2320
2423
|
logger.info(`Review telemetry: ${JSON.stringify({
|
|
2424
|
+
project: `${owner}/${repo}`,
|
|
2425
|
+
mr: prNumber,
|
|
2426
|
+
headSha: headSha.slice(0, 12),
|
|
2427
|
+
model,
|
|
2428
|
+
outcome: "reused",
|
|
2321
2429
|
reviewMode: metrics2.reviewMode,
|
|
2322
2430
|
reasoningEffort: metrics2.reasoningEffort,
|
|
2431
|
+
fastPath: false,
|
|
2323
2432
|
reused: true,
|
|
2324
|
-
headSha: headSha.slice(0, 12),
|
|
2325
2433
|
findings: cachedReview.findings.length
|
|
2326
2434
|
})}`);
|
|
2327
2435
|
printMetrics(metrics2);
|
|
@@ -2380,6 +2488,16 @@ async function reviewPr(opts) {
|
|
|
2380
2488
|
if (thinkingLevel) {
|
|
2381
2489
|
logger.info(`Reasoning effort for ${piModel.name}: ${thinkingLevel}${reasoningEffort ? " (explicit)" : " (adaptive)"}`);
|
|
2382
2490
|
}
|
|
2491
|
+
const singleTurn = tinyDiffFastPath && qualifiesForSingleTurnReview({
|
|
2492
|
+
diff: reviewDiff,
|
|
2493
|
+
stats: diffStats,
|
|
2494
|
+
embedded: embeddedDiff != null
|
|
2495
|
+
});
|
|
2496
|
+
if (singleTurn) {
|
|
2497
|
+
logger.info(
|
|
2498
|
+
`Single-turn fast path: tiny low-risk diff (${diffStats?.files} file(s), ${(diffStats?.additions ?? 0) + (diffStats?.deletions ?? 0)} changed line(s)); exposing only submit_review`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2383
2501
|
const prompt = buildPrReviewPrompt({
|
|
2384
2502
|
prUrl: prUrl ?? `local diff (against ${targetBranch})`,
|
|
2385
2503
|
platform,
|
|
@@ -2390,7 +2508,8 @@ async function reviewPr(opts) {
|
|
|
2390
2508
|
previousReviewSha,
|
|
2391
2509
|
reviewDiffMode: reviewMode,
|
|
2392
2510
|
changedFiles,
|
|
2393
|
-
localMode
|
|
2511
|
+
localMode,
|
|
2512
|
+
singleTurn
|
|
2394
2513
|
});
|
|
2395
2514
|
const startTime = Date.now();
|
|
2396
2515
|
const settingsManager = SettingsManager.inMemory({
|
|
@@ -2469,7 +2588,7 @@ async function reviewPr(opts) {
|
|
|
2469
2588
|
// (see _refreshToolRegistry in @earendil-works/pi-coding-agent's
|
|
2470
2589
|
// agent-session.ts). The submit_review custom tool must be named here
|
|
2471
2590
|
// or the LLM never sees it and the agent loop exits without calling it.
|
|
2472
|
-
tools: ["read", "bash", "grep", "find", "ls", "submit_review"],
|
|
2591
|
+
tools: singleTurn ? ["submit_review"] : ["read", "bash", "grep", "find", "ls", "submit_review"],
|
|
2473
2592
|
customTools: [submitReviewTool],
|
|
2474
2593
|
modelRuntime,
|
|
2475
2594
|
sessionManager: SessionManager.inMemory(),
|
|
@@ -2627,11 +2746,18 @@ async function reviewPr(opts) {
|
|
|
2627
2746
|
diffAdditions: diffStats?.additions ?? 0,
|
|
2628
2747
|
diffDeletions: diffStats?.deletions ?? 0,
|
|
2629
2748
|
diffBytes: diffStats?.bytes ?? 0,
|
|
2630
|
-
reused: false
|
|
2749
|
+
reused: false,
|
|
2750
|
+
fastPath: singleTurn
|
|
2631
2751
|
};
|
|
2632
2752
|
logger.info(`Review telemetry: ${JSON.stringify({
|
|
2753
|
+
project: localMode ? null : `${owner}/${repo}`,
|
|
2754
|
+
mr: localMode ? null : prNumber,
|
|
2755
|
+
headSha: headSha?.slice(0, 12) ?? null,
|
|
2756
|
+
model,
|
|
2757
|
+
outcome: "reviewed",
|
|
2633
2758
|
reviewMode: metrics.reviewMode,
|
|
2634
2759
|
reasoningEffort: metrics.reasoningEffort,
|
|
2760
|
+
fastPath: singleTurn,
|
|
2635
2761
|
reused: false,
|
|
2636
2762
|
diffFiles: metrics.diffFiles,
|
|
2637
2763
|
diffAdditions: metrics.diffAdditions,
|
|
@@ -2699,4 +2825,4 @@ export {
|
|
|
2699
2825
|
postReviewStructured,
|
|
2700
2826
|
reviewPr
|
|
2701
2827
|
};
|
|
2702
|
-
//# sourceMappingURL=chunk-
|
|
2828
|
+
//# sourceMappingURL=chunk-YKB3BRJM.js.map
|