@plannotator/opencode 0.16.7 → 0.17.1
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 +2 -2
- package/dist/index.js +947 -37
- package/package.json +2 -2
- package/plannotator.html +78 -77
- package/review-editor.html +596 -273
package/dist/index.js
CHANGED
|
@@ -24374,16 +24374,29 @@ var init_opencode_sdk = __esm(() => {
|
|
|
24374
24374
|
|
|
24375
24375
|
// index.ts
|
|
24376
24376
|
import { tool } from "@opencode-ai/plugin";
|
|
24377
|
-
import { existsSync as
|
|
24377
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
24378
24378
|
import path3 from "path";
|
|
24379
24379
|
|
|
24380
24380
|
// ../../packages/server/remote.ts
|
|
24381
24381
|
var DEFAULT_REMOTE_PORT = 19432;
|
|
24382
|
-
function
|
|
24382
|
+
function getRemoteOverride() {
|
|
24383
24383
|
const remote = process.env.PLANNOTATOR_REMOTE;
|
|
24384
|
+
if (remote === undefined) {
|
|
24385
|
+
return null;
|
|
24386
|
+
}
|
|
24384
24387
|
if (remote === "1" || remote?.toLowerCase() === "true") {
|
|
24385
24388
|
return true;
|
|
24386
24389
|
}
|
|
24390
|
+
if (remote === "0" || remote?.toLowerCase() === "false") {
|
|
24391
|
+
return false;
|
|
24392
|
+
}
|
|
24393
|
+
return null;
|
|
24394
|
+
}
|
|
24395
|
+
function isRemoteSession() {
|
|
24396
|
+
const remoteOverride = getRemoteOverride();
|
|
24397
|
+
if (remoteOverride !== null) {
|
|
24398
|
+
return remoteOverride;
|
|
24399
|
+
}
|
|
24387
24400
|
if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
|
24388
24401
|
return true;
|
|
24389
24402
|
}
|
|
@@ -24927,10 +24940,15 @@ var {$: $3 } = globalThis.Bun;
|
|
|
24927
24940
|
function parseRemoteUrl(url) {
|
|
24928
24941
|
if (!url)
|
|
24929
24942
|
return null;
|
|
24930
|
-
const
|
|
24931
|
-
if (
|
|
24932
|
-
return
|
|
24933
|
-
|
|
24943
|
+
const sshPortMatch = url.match(/^ssh:\/\/[^/]+(?::\d+)?\/(.+?)(?:\.git)?$/);
|
|
24944
|
+
if (sshPortMatch)
|
|
24945
|
+
return sshPortMatch[1];
|
|
24946
|
+
if (!url.includes("://")) {
|
|
24947
|
+
const sshMatch = url.match(/:([^/][^:]*?)(?:\.git)?$/);
|
|
24948
|
+
if (sshMatch)
|
|
24949
|
+
return sshMatch[1];
|
|
24950
|
+
}
|
|
24951
|
+
const httpsMatch = url.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
24934
24952
|
if (httpsMatch)
|
|
24935
24953
|
return httpsMatch[1];
|
|
24936
24954
|
return null;
|
|
@@ -25077,9 +25095,18 @@ async function isWSL() {
|
|
|
25077
25095
|
} catch {}
|
|
25078
25096
|
return false;
|
|
25079
25097
|
}
|
|
25080
|
-
|
|
25098
|
+
function shouldTryRemoteBrowserFallback(isRemote) {
|
|
25099
|
+
return isRemote && !process.env.PLANNOTATOR_BROWSER && !process.env.BROWSER;
|
|
25100
|
+
}
|
|
25101
|
+
async function openBrowser(url, options) {
|
|
25081
25102
|
try {
|
|
25082
25103
|
const browser = process.env.PLANNOTATOR_BROWSER || process.env.BROWSER;
|
|
25104
|
+
if (shouldTryRemoteBrowserFallback(options?.isRemote ?? false)) {
|
|
25105
|
+
const openedViaIpc = await tryVscodeIpc(url);
|
|
25106
|
+
if (openedViaIpc) {
|
|
25107
|
+
return true;
|
|
25108
|
+
}
|
|
25109
|
+
}
|
|
25083
25110
|
const platform = process.platform;
|
|
25084
25111
|
const wsl = await isWSL();
|
|
25085
25112
|
if (browser) {
|
|
@@ -25293,7 +25320,7 @@ function handleFavicon() {
|
|
|
25293
25320
|
});
|
|
25294
25321
|
}
|
|
25295
25322
|
async function handleServerReady(url, isRemote, _port) {
|
|
25296
|
-
await openBrowser(url);
|
|
25323
|
+
await openBrowser(url, { isRemote });
|
|
25297
25324
|
}
|
|
25298
25325
|
|
|
25299
25326
|
// ../../packages/server/reference-handlers.ts
|
|
@@ -25409,6 +25436,17 @@ function resolveAbsolutePath(input, platform = process.platform) {
|
|
|
25409
25436
|
function isSearchableMarkdownPath(input) {
|
|
25410
25437
|
return MARKDOWN_PATH_REGEX.test(input.trim());
|
|
25411
25438
|
}
|
|
25439
|
+
function stripWrappingQuotes(input) {
|
|
25440
|
+
if (input.length < 2) {
|
|
25441
|
+
return input;
|
|
25442
|
+
}
|
|
25443
|
+
const first = input[0];
|
|
25444
|
+
const last = input[input.length - 1];
|
|
25445
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
25446
|
+
return input.slice(1, -1);
|
|
25447
|
+
}
|
|
25448
|
+
return input;
|
|
25449
|
+
}
|
|
25412
25450
|
function hasWindowsDriveLetter(input) {
|
|
25413
25451
|
return /^[a-zA-Z]:[/\\]/.test(input);
|
|
25414
25452
|
}
|
|
@@ -25458,8 +25496,7 @@ function isAbsoluteMarkdownPath(input, platform = process.platform) {
|
|
|
25458
25496
|
}
|
|
25459
25497
|
return platform === "win32" ? win32.isAbsolute(normalizedInput) : isAbsolute(normalizedInput);
|
|
25460
25498
|
}
|
|
25461
|
-
function
|
|
25462
|
-
input = input.trim();
|
|
25499
|
+
function resolveMarkdownFileCore(input, projectRoot) {
|
|
25463
25500
|
const normalizedInput = normalizeMarkdownPathInput(input);
|
|
25464
25501
|
const searchInput = normalizeSeparators(normalizedInput);
|
|
25465
25502
|
const isBareFilename = !searchInput.includes("/");
|
|
@@ -25501,6 +25538,32 @@ function resolveMarkdownFile(input, projectRoot) {
|
|
|
25501
25538
|
}
|
|
25502
25539
|
return { kind: "not_found", input };
|
|
25503
25540
|
}
|
|
25541
|
+
function resolveMarkdownFile(input, projectRoot) {
|
|
25542
|
+
const originalInput = input.trim();
|
|
25543
|
+
const unquotedInput = stripWrappingQuotes(originalInput);
|
|
25544
|
+
const primary = resolveMarkdownFileCore(unquotedInput, projectRoot);
|
|
25545
|
+
if (primary.kind === "found") {
|
|
25546
|
+
return primary;
|
|
25547
|
+
}
|
|
25548
|
+
if (primary.kind === "ambiguous") {
|
|
25549
|
+
return { ...primary, input: originalInput };
|
|
25550
|
+
}
|
|
25551
|
+
if (!unquotedInput.startsWith("@")) {
|
|
25552
|
+
return { kind: "not_found", input: originalInput };
|
|
25553
|
+
}
|
|
25554
|
+
const normalizedInput = unquotedInput.replace(/^@+/, "");
|
|
25555
|
+
if (!normalizedInput) {
|
|
25556
|
+
return { kind: "not_found", input: originalInput };
|
|
25557
|
+
}
|
|
25558
|
+
const fallback = resolveMarkdownFileCore(normalizedInput, projectRoot);
|
|
25559
|
+
if (fallback.kind === "found") {
|
|
25560
|
+
return fallback;
|
|
25561
|
+
}
|
|
25562
|
+
if (fallback.kind === "ambiguous") {
|
|
25563
|
+
return { ...fallback, input: originalInput };
|
|
25564
|
+
}
|
|
25565
|
+
return { kind: "not_found", input: originalInput };
|
|
25566
|
+
}
|
|
25504
25567
|
|
|
25505
25568
|
// ../../packages/server/reference-handlers.ts
|
|
25506
25569
|
async function handleDoc(req) {
|
|
@@ -25828,7 +25891,9 @@ function transformReviewInput(body) {
|
|
|
25828
25891
|
originalCode: typeof obj.originalCode === "string" ? obj.originalCode : undefined,
|
|
25829
25892
|
createdAt: Date.now(),
|
|
25830
25893
|
author: typeof obj.author === "string" ? obj.author : undefined,
|
|
25831
|
-
source
|
|
25894
|
+
source,
|
|
25895
|
+
...typeof obj.severity === "string" && { severity: obj.severity },
|
|
25896
|
+
...typeof obj.reasoning === "string" && { reasoning: obj.reasoning }
|
|
25832
25897
|
});
|
|
25833
25898
|
}
|
|
25834
25899
|
return { annotations };
|
|
@@ -25931,6 +25996,13 @@ function createExternalAnnotationHandler(mode) {
|
|
|
25931
25996
|
}
|
|
25932
25997
|
});
|
|
25933
25998
|
return {
|
|
25999
|
+
addAnnotations(body) {
|
|
26000
|
+
const parsed = transform(body);
|
|
26001
|
+
if ("error" in parsed)
|
|
26002
|
+
return { error: parsed.error };
|
|
26003
|
+
const created = store.add(parsed.annotations);
|
|
26004
|
+
return { ids: created.map((a) => a.id) };
|
|
26005
|
+
},
|
|
25934
26006
|
async handle(req, url, options) {
|
|
25935
26007
|
if (url.pathname === STREAM && req.method === "GET") {
|
|
25936
26008
|
options?.disableIdleTimeout?.();
|
|
@@ -26436,6 +26508,7 @@ async function getGitContext(runtime, cwd) {
|
|
|
26436
26508
|
];
|
|
26437
26509
|
if (currentBranch !== defaultBranch) {
|
|
26438
26510
|
diffOptions.push({ id: "branch", label: `vs ${defaultBranch}` });
|
|
26511
|
+
diffOptions.push({ id: "merge-base", label: `Current PR Diff` });
|
|
26439
26512
|
}
|
|
26440
26513
|
const [worktrees, currentTreePathResult] = await Promise.all([
|
|
26441
26514
|
getWorktrees(runtime, cwd),
|
|
@@ -26577,6 +26650,21 @@ async function runGitDiff(runtime, diffType, defaultBranch = "main", externalCwd
|
|
|
26577
26650
|
label = `Changes vs ${defaultBranch}`;
|
|
26578
26651
|
break;
|
|
26579
26652
|
}
|
|
26653
|
+
case "merge-base": {
|
|
26654
|
+
const mergeBaseResult = assertGitSuccess(await runtime.runGit(["merge-base", defaultBranch, "HEAD"], { cwd }), ["merge-base", defaultBranch, "HEAD"]);
|
|
26655
|
+
const mergeBase = mergeBaseResult.stdout.trim();
|
|
26656
|
+
const mergeBaseDiffArgs = [
|
|
26657
|
+
"diff",
|
|
26658
|
+
"--no-ext-diff",
|
|
26659
|
+
`${mergeBase}..HEAD`,
|
|
26660
|
+
"--src-prefix=a/",
|
|
26661
|
+
"--dst-prefix=b/"
|
|
26662
|
+
];
|
|
26663
|
+
const mergeBaseDiff = assertGitSuccess(await runtime.runGit(mergeBaseDiffArgs, { cwd }), mergeBaseDiffArgs);
|
|
26664
|
+
patch = mergeBaseDiff.stdout;
|
|
26665
|
+
label = `PR diff vs ${defaultBranch}`;
|
|
26666
|
+
break;
|
|
26667
|
+
}
|
|
26580
26668
|
default:
|
|
26581
26669
|
return { patch: "", label: "Unknown diff type" };
|
|
26582
26670
|
}
|
|
@@ -26644,6 +26732,14 @@ async function getFileContentsForDiff(runtime, diffType, defaultBranch, filePath
|
|
|
26644
26732
|
oldContent: await gitShow(defaultBranch, oldFilePath),
|
|
26645
26733
|
newContent: await gitShow("HEAD", filePath)
|
|
26646
26734
|
};
|
|
26735
|
+
case "merge-base": {
|
|
26736
|
+
const mbResult = await runtime.runGit(["merge-base", defaultBranch, "HEAD"], { cwd });
|
|
26737
|
+
const mb = mbResult.exitCode === 0 ? mbResult.stdout.trim() : defaultBranch;
|
|
26738
|
+
return {
|
|
26739
|
+
oldContent: await gitShow(mb, oldFilePath),
|
|
26740
|
+
newContent: await gitShow("HEAD", filePath)
|
|
26741
|
+
};
|
|
26742
|
+
}
|
|
26647
26743
|
default:
|
|
26648
26744
|
return { oldContent: null, newContent: null };
|
|
26649
26745
|
}
|
|
@@ -26994,7 +27090,7 @@ async function getP4FileContentsForDiff(diffType, filePath, cwd) {
|
|
|
26994
27090
|
}
|
|
26995
27091
|
|
|
26996
27092
|
// ../../packages/server/vcs.ts
|
|
26997
|
-
var GIT_DIFF_TYPES = new Set(["uncommitted", "staged", "unstaged", "last-commit", "branch"]);
|
|
27093
|
+
var GIT_DIFF_TYPES = new Set(["uncommitted", "staged", "unstaged", "last-commit", "branch", "merge-base"]);
|
|
26998
27094
|
var gitProvider = {
|
|
26999
27095
|
id: "git",
|
|
27000
27096
|
async detect(cwd) {
|
|
@@ -27082,6 +27178,284 @@ function resolveVcsCwd(diffType, fallbackCwd) {
|
|
|
27082
27178
|
return provider.resolveCwd?.(diffType, fallbackCwd) ?? fallbackCwd;
|
|
27083
27179
|
}
|
|
27084
27180
|
|
|
27181
|
+
// ../../packages/server/path-utils.ts
|
|
27182
|
+
import { relative } from "path";
|
|
27183
|
+
function toRelativePath2(absolutePath, cwd) {
|
|
27184
|
+
if (!cwd)
|
|
27185
|
+
return absolutePath;
|
|
27186
|
+
const rel = relative(cwd, absolutePath);
|
|
27187
|
+
if (rel.startsWith(".."))
|
|
27188
|
+
return absolutePath;
|
|
27189
|
+
return rel.replace(/\\/g, "/");
|
|
27190
|
+
}
|
|
27191
|
+
|
|
27192
|
+
// ../../packages/server/claude-review.ts
|
|
27193
|
+
var CLAUDE_REVIEW_SCHEMA_JSON = JSON.stringify({
|
|
27194
|
+
type: "object",
|
|
27195
|
+
properties: {
|
|
27196
|
+
findings: {
|
|
27197
|
+
type: "array",
|
|
27198
|
+
items: {
|
|
27199
|
+
type: "object",
|
|
27200
|
+
properties: {
|
|
27201
|
+
severity: { type: "string", enum: ["important", "nit", "pre_existing"] },
|
|
27202
|
+
file: { type: "string" },
|
|
27203
|
+
line: { type: "integer" },
|
|
27204
|
+
end_line: { type: "integer" },
|
|
27205
|
+
description: { type: "string" },
|
|
27206
|
+
reasoning: { type: "string" }
|
|
27207
|
+
},
|
|
27208
|
+
required: ["severity", "file", "line", "end_line", "description", "reasoning"],
|
|
27209
|
+
additionalProperties: false
|
|
27210
|
+
}
|
|
27211
|
+
},
|
|
27212
|
+
summary: {
|
|
27213
|
+
type: "object",
|
|
27214
|
+
properties: {
|
|
27215
|
+
important: { type: "integer" },
|
|
27216
|
+
nit: { type: "integer" },
|
|
27217
|
+
pre_existing: { type: "integer" }
|
|
27218
|
+
},
|
|
27219
|
+
required: ["important", "nit", "pre_existing"],
|
|
27220
|
+
additionalProperties: false
|
|
27221
|
+
}
|
|
27222
|
+
},
|
|
27223
|
+
required: ["findings", "summary"],
|
|
27224
|
+
additionalProperties: false
|
|
27225
|
+
});
|
|
27226
|
+
var CLAUDE_REVIEW_PROMPT = `# Claude Code Review System Prompt
|
|
27227
|
+
|
|
27228
|
+
## Identity
|
|
27229
|
+
You are a code review system. Your job is to find bugs that would break
|
|
27230
|
+
production. You are not a linter, formatter, or style checker unless
|
|
27231
|
+
project guidance files explicitly expand your scope.
|
|
27232
|
+
|
|
27233
|
+
## Pipeline
|
|
27234
|
+
|
|
27235
|
+
Step 1: Gather context
|
|
27236
|
+
- Retrieve the PR diff (gh pr diff or git diff)
|
|
27237
|
+
- Read CLAUDE.md and REVIEW.md at the repo root and in every directory
|
|
27238
|
+
containing modified files
|
|
27239
|
+
- Build a map of which rules apply to which file paths
|
|
27240
|
+
- Identify any skip rules (paths, patterns, or file types to ignore)
|
|
27241
|
+
|
|
27242
|
+
Step 2: Launch 4 parallel review agents
|
|
27243
|
+
|
|
27244
|
+
Agent 1 \u2014 Bug + Regression (Opus-level reasoning)
|
|
27245
|
+
Scan for logic errors, regressions, broken edge cases, build failures,
|
|
27246
|
+
and code that will produce wrong results. Focus on the diff but read
|
|
27247
|
+
surrounding code to understand call sites and data flow. Flag only
|
|
27248
|
+
issues where the code is demonstrably wrong \u2014 not stylistic concerns,
|
|
27249
|
+
not missing tests, not "could be cleaner."
|
|
27250
|
+
|
|
27251
|
+
Agent 2 \u2014 Security + Deep Analysis (Opus-level reasoning)
|
|
27252
|
+
Look for security vulnerabilities with concrete exploit paths, race
|
|
27253
|
+
conditions, incorrect assumptions about trust boundaries, and subtle
|
|
27254
|
+
issues in introduced code. Read surrounding code for context. Do not
|
|
27255
|
+
flag theoretical risks without a plausible path to harm.
|
|
27256
|
+
|
|
27257
|
+
Agent 3 \u2014 Code Quality + Reusability (Sonnet-level reasoning)
|
|
27258
|
+
Look for code smells, unnecessary duplication, missed opportunities to
|
|
27259
|
+
reuse existing utilities or patterns in the codebase, overly complex
|
|
27260
|
+
implementations that could be simpler, and elegance issues. Read the
|
|
27261
|
+
surrounding codebase to understand existing patterns before flagging.
|
|
27262
|
+
Only flag issues a senior engineer would care about.
|
|
27263
|
+
|
|
27264
|
+
Agent 4 \u2014 Guideline Compliance (Haiku-level reasoning)
|
|
27265
|
+
Audit changes against rules from CLAUDE.md and REVIEW.md gathered in
|
|
27266
|
+
Step 1. Only flag clear, unambiguous violations where you can cite the
|
|
27267
|
+
exact rule broken. If a PR makes a CLAUDE.md statement outdated, flag
|
|
27268
|
+
that the docs need updating. Respect all skip rules \u2014 never flag files
|
|
27269
|
+
or patterns that guidance says to ignore.
|
|
27270
|
+
|
|
27271
|
+
All agents:
|
|
27272
|
+
- Do not duplicate each other's findings
|
|
27273
|
+
- Do not flag issues in paths excluded by guidance files
|
|
27274
|
+
- Provide file, line number, and a concise description for each candidate
|
|
27275
|
+
|
|
27276
|
+
Step 3: Validate each candidate finding
|
|
27277
|
+
For each candidate, launch a validation agent. The validator:
|
|
27278
|
+
- Traces the actual code path to confirm the issue is real
|
|
27279
|
+
- Checks whether the issue is handled elsewhere (try/catch, upstream
|
|
27280
|
+
guard, fallback logic, type system guarantees)
|
|
27281
|
+
- Confirms the finding is not a false positive with high confidence
|
|
27282
|
+
- If validation fails, drop the finding silently
|
|
27283
|
+
- If validation passes, write a clear reasoning chain explaining how
|
|
27284
|
+
the issue was confirmed \u2014 this becomes the \`reasoning\` field
|
|
27285
|
+
|
|
27286
|
+
Step 4: Classify each validated finding
|
|
27287
|
+
Assign exactly one severity:
|
|
27288
|
+
|
|
27289
|
+
important \u2014 A bug that should be fixed before merging. Build failures,
|
|
27290
|
+
clear logic errors, security vulnerabilities with exploit paths, data
|
|
27291
|
+
loss risks, race conditions with observable consequences.
|
|
27292
|
+
|
|
27293
|
+
nit \u2014 A minor issue worth fixing but non-blocking. Style deviations
|
|
27294
|
+
from project guidelines, code quality concerns, edge cases that are
|
|
27295
|
+
unlikely but worth noting, convention violations that don't affect
|
|
27296
|
+
correctness.
|
|
27297
|
+
|
|
27298
|
+
pre_existing \u2014 A bug that exists in the surrounding codebase but was
|
|
27299
|
+
NOT introduced by this PR. Only flag when directly relevant to the
|
|
27300
|
+
changed code path.
|
|
27301
|
+
|
|
27302
|
+
Step 5: Deduplicate and rank
|
|
27303
|
+
- Merge findings that describe the same underlying issue from different
|
|
27304
|
+
agents \u2014 keep the most specific description and the highest severity
|
|
27305
|
+
- Sort by severity: important \u2192 nit \u2192 pre_existing
|
|
27306
|
+
- Within each severity, sort by file path and line number
|
|
27307
|
+
|
|
27308
|
+
Step 6: Return structured JSON output matching the schema.
|
|
27309
|
+
If no issues are found, return an empty findings array with zeroed summary.
|
|
27310
|
+
|
|
27311
|
+
## Hard constraints
|
|
27312
|
+
- Never approve or block the PR
|
|
27313
|
+
- Never comment on formatting or code style unless guidance files say to
|
|
27314
|
+
- Never flag missing test coverage unless guidance files say to
|
|
27315
|
+
- Never invent rules \u2014 only enforce what CLAUDE.md or REVIEW.md state
|
|
27316
|
+
- Never flag issues in skipped paths or generated files unless guidance
|
|
27317
|
+
explicitly includes them
|
|
27318
|
+
- Prefer silence over false positives \u2014 when in doubt, drop the finding
|
|
27319
|
+
- Do NOT post any comments to GitHub or GitLab
|
|
27320
|
+
- Do NOT use gh pr comment or any commenting tool
|
|
27321
|
+
- Your only output is the structured JSON findings`;
|
|
27322
|
+
function buildClaudeCommand(prompt) {
|
|
27323
|
+
const allowedTools = [
|
|
27324
|
+
"Agent",
|
|
27325
|
+
"Read",
|
|
27326
|
+
"Glob",
|
|
27327
|
+
"Grep",
|
|
27328
|
+
"Bash(gh pr view:*)",
|
|
27329
|
+
"Bash(gh pr diff:*)",
|
|
27330
|
+
"Bash(gh pr list:*)",
|
|
27331
|
+
"Bash(gh issue view:*)",
|
|
27332
|
+
"Bash(gh issue list:*)",
|
|
27333
|
+
"Bash(gh api repos/*/*/pulls/*)",
|
|
27334
|
+
"Bash(gh api repos/*/*/pulls/*/files*)",
|
|
27335
|
+
"Bash(gh api repos/*/*/pulls/*/comments*)",
|
|
27336
|
+
"Bash(gh api repos/*/*/issues/*/comments*)",
|
|
27337
|
+
"Bash(glab mr view:*)",
|
|
27338
|
+
"Bash(glab mr diff:*)",
|
|
27339
|
+
"Bash(glab mr list:*)",
|
|
27340
|
+
"Bash(glab api:*)",
|
|
27341
|
+
"Bash(git status:*)",
|
|
27342
|
+
"Bash(git diff:*)",
|
|
27343
|
+
"Bash(git log:*)",
|
|
27344
|
+
"Bash(git show:*)",
|
|
27345
|
+
"Bash(git blame:*)",
|
|
27346
|
+
"Bash(git branch:*)",
|
|
27347
|
+
"Bash(git grep:*)",
|
|
27348
|
+
"Bash(git ls-remote:*)",
|
|
27349
|
+
"Bash(git ls-tree:*)",
|
|
27350
|
+
"Bash(git merge-base:*)",
|
|
27351
|
+
"Bash(git remote:*)",
|
|
27352
|
+
"Bash(git rev-parse:*)",
|
|
27353
|
+
"Bash(git show-ref:*)",
|
|
27354
|
+
"Bash(wc:*)"
|
|
27355
|
+
].join(",");
|
|
27356
|
+
const disallowedTools = [
|
|
27357
|
+
"Edit",
|
|
27358
|
+
"Write",
|
|
27359
|
+
"NotebookEdit",
|
|
27360
|
+
"WebFetch",
|
|
27361
|
+
"WebSearch",
|
|
27362
|
+
"Bash(python:*)",
|
|
27363
|
+
"Bash(python3:*)",
|
|
27364
|
+
"Bash(node:*)",
|
|
27365
|
+
"Bash(npx:*)",
|
|
27366
|
+
"Bash(bun:*)",
|
|
27367
|
+
"Bash(bunx:*)",
|
|
27368
|
+
"Bash(sh:*)",
|
|
27369
|
+
"Bash(bash:*)",
|
|
27370
|
+
"Bash(zsh:*)",
|
|
27371
|
+
"Bash(curl:*)",
|
|
27372
|
+
"Bash(wget:*)"
|
|
27373
|
+
].join(",");
|
|
27374
|
+
return {
|
|
27375
|
+
command: [
|
|
27376
|
+
"claude",
|
|
27377
|
+
"-p",
|
|
27378
|
+
"--permission-mode",
|
|
27379
|
+
"dontAsk",
|
|
27380
|
+
"--output-format",
|
|
27381
|
+
"stream-json",
|
|
27382
|
+
"--verbose",
|
|
27383
|
+
"--json-schema",
|
|
27384
|
+
CLAUDE_REVIEW_SCHEMA_JSON,
|
|
27385
|
+
"--no-session-persistence",
|
|
27386
|
+
"--model",
|
|
27387
|
+
"sonnet",
|
|
27388
|
+
"--tools",
|
|
27389
|
+
"Agent,Bash,Read,Glob,Grep",
|
|
27390
|
+
"--allowedTools",
|
|
27391
|
+
allowedTools,
|
|
27392
|
+
"--disallowedTools",
|
|
27393
|
+
disallowedTools
|
|
27394
|
+
],
|
|
27395
|
+
stdinPrompt: prompt
|
|
27396
|
+
};
|
|
27397
|
+
}
|
|
27398
|
+
function parseClaudeStreamOutput(stdout) {
|
|
27399
|
+
if (!stdout.trim())
|
|
27400
|
+
return null;
|
|
27401
|
+
const lines = stdout.trim().split(`
|
|
27402
|
+
`);
|
|
27403
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
27404
|
+
const line = lines[i].trim();
|
|
27405
|
+
if (!line)
|
|
27406
|
+
continue;
|
|
27407
|
+
try {
|
|
27408
|
+
const event = JSON.parse(line);
|
|
27409
|
+
if (event.type === "result") {
|
|
27410
|
+
if (event.is_error)
|
|
27411
|
+
return null;
|
|
27412
|
+
const output = event.structured_output;
|
|
27413
|
+
if (!output || !Array.isArray(output.findings))
|
|
27414
|
+
return null;
|
|
27415
|
+
return output;
|
|
27416
|
+
}
|
|
27417
|
+
} catch {}
|
|
27418
|
+
}
|
|
27419
|
+
return null;
|
|
27420
|
+
}
|
|
27421
|
+
function transformClaudeFindings(findings, source, cwd) {
|
|
27422
|
+
return findings.filter((f) => f.file && typeof f.line === "number").map((f) => ({
|
|
27423
|
+
source,
|
|
27424
|
+
filePath: toRelativePath2(f.file, cwd),
|
|
27425
|
+
lineStart: f.line,
|
|
27426
|
+
lineEnd: f.end_line ?? f.line,
|
|
27427
|
+
type: "comment",
|
|
27428
|
+
side: "new",
|
|
27429
|
+
scope: "line",
|
|
27430
|
+
text: `[${f.severity}] ${f.description}`,
|
|
27431
|
+
severity: f.severity,
|
|
27432
|
+
reasoning: f.reasoning,
|
|
27433
|
+
author: "Claude Code"
|
|
27434
|
+
}));
|
|
27435
|
+
}
|
|
27436
|
+
function formatClaudeLogEvent(line) {
|
|
27437
|
+
try {
|
|
27438
|
+
const event = JSON.parse(line);
|
|
27439
|
+
if (event.type === "result")
|
|
27440
|
+
return null;
|
|
27441
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
27442
|
+
const parts = Array.isArray(event.message.content) ? event.message.content : [event.message.content];
|
|
27443
|
+
const texts = parts.filter((p) => p.type === "text" && p.text).map((p) => p.text);
|
|
27444
|
+
if (texts.length > 0)
|
|
27445
|
+
return texts.join(`
|
|
27446
|
+
`);
|
|
27447
|
+
const tools = parts.filter((p) => p.type === "tool_use");
|
|
27448
|
+
if (tools.length > 0) {
|
|
27449
|
+
return tools.map((t) => `[${t.name}] ${typeof t.input === "string" ? t.input.slice(0, 100) : JSON.stringify(t.input).slice(0, 100)}`).join(`
|
|
27450
|
+
`);
|
|
27451
|
+
}
|
|
27452
|
+
}
|
|
27453
|
+
return null;
|
|
27454
|
+
} catch {
|
|
27455
|
+
return null;
|
|
27456
|
+
}
|
|
27457
|
+
}
|
|
27458
|
+
|
|
27085
27459
|
// ../../packages/shared/agent-jobs.ts
|
|
27086
27460
|
var AGENT_HEARTBEAT_COMMENT = `:
|
|
27087
27461
|
|
|
@@ -27107,13 +27481,13 @@ var CAPABILITIES = `${BASE2}/capabilities`;
|
|
|
27107
27481
|
function createAgentJobHandler(options) {
|
|
27108
27482
|
const { mode, getServerUrl, getCwd } = options;
|
|
27109
27483
|
const jobs = new Map;
|
|
27484
|
+
const jobOutputPaths = new Map;
|
|
27110
27485
|
const subscribers = new Set;
|
|
27111
27486
|
const encoder = new TextEncoder;
|
|
27112
27487
|
let version = 0;
|
|
27113
27488
|
const capabilities = [
|
|
27114
27489
|
{ id: "claude", name: "Claude Code", available: !!Bun.which("claude") },
|
|
27115
|
-
{ id: "codex", name: "Codex CLI", available: !!Bun.which("codex") }
|
|
27116
|
-
{ id: "shell", name: "Shell Command", available: true }
|
|
27490
|
+
{ id: "codex", name: "Codex CLI", available: !!Bun.which("codex") }
|
|
27117
27491
|
];
|
|
27118
27492
|
const capabilitiesResponse = {
|
|
27119
27493
|
mode,
|
|
@@ -27131,7 +27505,7 @@ function createAgentJobHandler(options) {
|
|
|
27131
27505
|
}
|
|
27132
27506
|
}
|
|
27133
27507
|
}
|
|
27134
|
-
function spawnJob(provider, command, label) {
|
|
27508
|
+
function spawnJob(provider, command, label, outputPath, spawnOptions) {
|
|
27135
27509
|
const id = crypto.randomUUID();
|
|
27136
27510
|
const source = jobSource(id);
|
|
27137
27511
|
const info = {
|
|
@@ -27141,13 +27515,18 @@ function createAgentJobHandler(options) {
|
|
|
27141
27515
|
label,
|
|
27142
27516
|
status: "starting",
|
|
27143
27517
|
startedAt: Date.now(),
|
|
27144
|
-
command
|
|
27518
|
+
command,
|
|
27519
|
+
cwd: getCwd()
|
|
27145
27520
|
};
|
|
27146
27521
|
let proc = null;
|
|
27147
27522
|
try {
|
|
27523
|
+
const spawnCwd = spawnOptions?.cwd ?? getCwd();
|
|
27524
|
+
const captureStdout = spawnOptions?.captureStdout ?? false;
|
|
27525
|
+
const hasStdinPrompt = !!spawnOptions?.stdinPrompt;
|
|
27148
27526
|
proc = Bun.spawn(command, {
|
|
27149
|
-
cwd:
|
|
27150
|
-
|
|
27527
|
+
cwd: spawnCwd,
|
|
27528
|
+
stdin: hasStdinPrompt ? "pipe" : undefined,
|
|
27529
|
+
stdout: captureStdout ? "pipe" : "ignore",
|
|
27151
27530
|
stderr: "pipe",
|
|
27152
27531
|
env: {
|
|
27153
27532
|
...process.env,
|
|
@@ -27155,10 +27534,24 @@ function createAgentJobHandler(options) {
|
|
|
27155
27534
|
PLANNOTATOR_API_URL: getServerUrl()
|
|
27156
27535
|
}
|
|
27157
27536
|
});
|
|
27537
|
+
if (hasStdinPrompt && proc.stdin) {
|
|
27538
|
+
const sink = proc.stdin;
|
|
27539
|
+
sink.write(spawnOptions.stdinPrompt);
|
|
27540
|
+
sink.end();
|
|
27541
|
+
}
|
|
27158
27542
|
info.status = "running";
|
|
27543
|
+
info.cwd = spawnCwd;
|
|
27544
|
+
if (spawnOptions?.prompt)
|
|
27545
|
+
info.prompt = spawnOptions.prompt;
|
|
27159
27546
|
jobs.set(id, { info, proc });
|
|
27547
|
+
if (outputPath)
|
|
27548
|
+
jobOutputPaths.set(id, outputPath);
|
|
27549
|
+
if (spawnOptions?.cwd)
|
|
27550
|
+
jobOutputPaths.set(`${id}:cwd`, spawnOptions.cwd);
|
|
27160
27551
|
broadcast({ type: "job:started", job: { ...info } });
|
|
27161
27552
|
let stderrBuf = "";
|
|
27553
|
+
let logPending = "";
|
|
27554
|
+
let logFlushTimer = null;
|
|
27162
27555
|
if (proc.stderr && typeof proc.stderr !== "number") {
|
|
27163
27556
|
(async () => {
|
|
27164
27557
|
try {
|
|
@@ -27166,11 +27559,61 @@ function createAgentJobHandler(options) {
|
|
|
27166
27559
|
for await (const chunk of reader) {
|
|
27167
27560
|
const text = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
27168
27561
|
stderrBuf = (stderrBuf + text).slice(-500);
|
|
27562
|
+
logPending += text;
|
|
27563
|
+
if (!logFlushTimer) {
|
|
27564
|
+
logFlushTimer = setTimeout(() => {
|
|
27565
|
+
if (logPending) {
|
|
27566
|
+
broadcast({ type: "job:log", jobId: id, delta: logPending });
|
|
27567
|
+
logPending = "";
|
|
27568
|
+
}
|
|
27569
|
+
logFlushTimer = null;
|
|
27570
|
+
}, 200);
|
|
27571
|
+
}
|
|
27572
|
+
}
|
|
27573
|
+
if (logFlushTimer) {
|
|
27574
|
+
clearTimeout(logFlushTimer);
|
|
27575
|
+
logFlushTimer = null;
|
|
27576
|
+
}
|
|
27577
|
+
if (logPending) {
|
|
27578
|
+
broadcast({ type: "job:log", jobId: id, delta: logPending });
|
|
27579
|
+
logPending = "";
|
|
27169
27580
|
}
|
|
27170
27581
|
} catch {}
|
|
27171
27582
|
})();
|
|
27172
27583
|
}
|
|
27173
|
-
|
|
27584
|
+
let stdoutBuf = "";
|
|
27585
|
+
const stdoutDone = captureStdout && proc.stdout && typeof proc.stdout !== "number" ? (async () => {
|
|
27586
|
+
try {
|
|
27587
|
+
const reader = proc.stdout;
|
|
27588
|
+
for await (const chunk of reader) {
|
|
27589
|
+
const text = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
27590
|
+
stdoutBuf += text;
|
|
27591
|
+
const lines = text.split(`
|
|
27592
|
+
`);
|
|
27593
|
+
for (const line of lines) {
|
|
27594
|
+
if (!line.trim())
|
|
27595
|
+
continue;
|
|
27596
|
+
if (provider === "claude") {
|
|
27597
|
+
const formatted = formatClaudeLogEvent(line);
|
|
27598
|
+
if (formatted !== null) {
|
|
27599
|
+
broadcast({ type: "job:log", jobId: id, delta: formatted + `
|
|
27600
|
+
` });
|
|
27601
|
+
}
|
|
27602
|
+
continue;
|
|
27603
|
+
}
|
|
27604
|
+
try {
|
|
27605
|
+
const event = JSON.parse(line);
|
|
27606
|
+
if (event.type === "result")
|
|
27607
|
+
continue;
|
|
27608
|
+
} catch {}
|
|
27609
|
+
broadcast({ type: "job:log", jobId: id, delta: line + `
|
|
27610
|
+
` });
|
|
27611
|
+
}
|
|
27612
|
+
}
|
|
27613
|
+
} catch {}
|
|
27614
|
+
})() : Promise.resolve();
|
|
27615
|
+
proc.exited.then(async (exitCode) => {
|
|
27616
|
+
await Promise.race([stdoutDone, new Promise((r) => setTimeout(r, 2000))]);
|
|
27174
27617
|
const entry = jobs.get(id);
|
|
27175
27618
|
if (!entry || isTerminalStatus(entry.info.status))
|
|
27176
27619
|
return;
|
|
@@ -27180,8 +27623,21 @@ function createAgentJobHandler(options) {
|
|
|
27180
27623
|
if (exitCode !== 0 && stderrBuf) {
|
|
27181
27624
|
entry.info.error = stderrBuf;
|
|
27182
27625
|
}
|
|
27626
|
+
const outputPath2 = jobOutputPaths.get(id);
|
|
27627
|
+
const jobCwd = jobOutputPaths.get(`${id}:cwd`);
|
|
27628
|
+
if (exitCode === 0 && options.onJobComplete) {
|
|
27629
|
+
try {
|
|
27630
|
+
await options.onJobComplete(entry.info, {
|
|
27631
|
+
outputPath: outputPath2,
|
|
27632
|
+
stdout: captureStdout ? stdoutBuf : undefined,
|
|
27633
|
+
cwd: jobCwd
|
|
27634
|
+
});
|
|
27635
|
+
} catch {}
|
|
27636
|
+
}
|
|
27637
|
+
jobOutputPaths.delete(id);
|
|
27638
|
+
jobOutputPaths.delete(`${id}:cwd`);
|
|
27183
27639
|
broadcast({ type: "job:completed", job: { ...entry.info } });
|
|
27184
|
-
});
|
|
27640
|
+
}).catch(() => {});
|
|
27185
27641
|
} catch (err) {
|
|
27186
27642
|
jobs.set(id, { info, proc: null });
|
|
27187
27643
|
broadcast({ type: "job:started", job: { ...info } });
|
|
@@ -27203,6 +27659,8 @@ function createAgentJobHandler(options) {
|
|
|
27203
27659
|
}
|
|
27204
27660
|
entry.info.status = "killed";
|
|
27205
27661
|
entry.info.endedAt = Date.now();
|
|
27662
|
+
jobOutputPaths.delete(id);
|
|
27663
|
+
jobOutputPaths.delete(`${id}:cwd`);
|
|
27206
27664
|
broadcast({ type: "job:completed", job: { ...entry.info } });
|
|
27207
27665
|
return true;
|
|
27208
27666
|
}
|
|
@@ -27275,18 +27733,41 @@ function createAgentJobHandler(options) {
|
|
|
27275
27733
|
if (url.pathname === JOBS && req.method === "POST") {
|
|
27276
27734
|
try {
|
|
27277
27735
|
const body = await req.json();
|
|
27278
|
-
const provider = typeof body.provider === "string" ? body.provider : "
|
|
27279
|
-
|
|
27280
|
-
|
|
27281
|
-
|
|
27736
|
+
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
27737
|
+
let rawCommand = Array.isArray(body.command) ? body.command : [];
|
|
27738
|
+
let command = rawCommand.filter((c) => typeof c === "string");
|
|
27739
|
+
let label = typeof body.label === "string" ? body.label : `${provider} agent`;
|
|
27740
|
+
let outputPath;
|
|
27282
27741
|
const cap = capabilities.find((c) => c.id === provider);
|
|
27283
27742
|
if (!cap || !cap.available) {
|
|
27284
27743
|
return Response.json({ error: `Unknown or unavailable provider: ${provider}` }, { status: 400 });
|
|
27285
27744
|
}
|
|
27745
|
+
let captureStdout = false;
|
|
27746
|
+
let stdinPrompt;
|
|
27747
|
+
let spawnCwd;
|
|
27748
|
+
let promptText;
|
|
27749
|
+
if (options.buildCommand) {
|
|
27750
|
+
const built = await options.buildCommand(provider);
|
|
27751
|
+
if (built) {
|
|
27752
|
+
command = built.command;
|
|
27753
|
+
outputPath = built.outputPath;
|
|
27754
|
+
captureStdout = built.captureStdout ?? false;
|
|
27755
|
+
stdinPrompt = built.stdinPrompt;
|
|
27756
|
+
spawnCwd = built.cwd;
|
|
27757
|
+
promptText = built.prompt;
|
|
27758
|
+
if (built.label)
|
|
27759
|
+
label = built.label;
|
|
27760
|
+
}
|
|
27761
|
+
}
|
|
27286
27762
|
if (command.length === 0) {
|
|
27287
27763
|
return Response.json({ error: 'Missing "command" array' }, { status: 400 });
|
|
27288
27764
|
}
|
|
27289
|
-
const job = spawnJob(provider, command, label
|
|
27765
|
+
const job = spawnJob(provider, command, label, outputPath, {
|
|
27766
|
+
captureStdout,
|
|
27767
|
+
stdinPrompt,
|
|
27768
|
+
cwd: spawnCwd,
|
|
27769
|
+
prompt: promptText
|
|
27770
|
+
});
|
|
27290
27771
|
return Response.json({ job }, { status: 201 });
|
|
27291
27772
|
} catch {
|
|
27292
27773
|
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
@@ -27312,6 +27793,262 @@ function createAgentJobHandler(options) {
|
|
|
27312
27793
|
};
|
|
27313
27794
|
}
|
|
27314
27795
|
|
|
27796
|
+
// ../../packages/server/codex-review.ts
|
|
27797
|
+
import { join as join8 } from "path";
|
|
27798
|
+
import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
|
|
27799
|
+
import { appendFile, mkdir, unlink, writeFile, readFile } from "fs/promises";
|
|
27800
|
+
import { existsSync as existsSync8 } from "fs";
|
|
27801
|
+
var DEBUG_ENABLED = !!process.env.PLANNOTATOR_DEBUG;
|
|
27802
|
+
var DEBUG_LOG_PATH = join8(homedir4(), ".plannotator", "codex-review-debug.log");
|
|
27803
|
+
async function debugLog(label, data) {
|
|
27804
|
+
if (!DEBUG_ENABLED)
|
|
27805
|
+
return;
|
|
27806
|
+
try {
|
|
27807
|
+
await mkdir(join8(homedir4(), ".plannotator"), { recursive: true });
|
|
27808
|
+
const timestamp = new Date().toISOString();
|
|
27809
|
+
const line = data !== undefined ? `[${timestamp}] ${label}: ${typeof data === "string" ? data : JSON.stringify(data, null, 2)}
|
|
27810
|
+
` : `[${timestamp}] ${label}
|
|
27811
|
+
`;
|
|
27812
|
+
await appendFile(DEBUG_LOG_PATH, line);
|
|
27813
|
+
} catch {}
|
|
27814
|
+
}
|
|
27815
|
+
var CODEX_REVIEW_SCHEMA = JSON.stringify({
|
|
27816
|
+
type: "object",
|
|
27817
|
+
properties: {
|
|
27818
|
+
findings: {
|
|
27819
|
+
type: "array",
|
|
27820
|
+
items: {
|
|
27821
|
+
type: "object",
|
|
27822
|
+
properties: {
|
|
27823
|
+
title: { type: "string" },
|
|
27824
|
+
body: { type: "string" },
|
|
27825
|
+
confidence_score: { type: "number" },
|
|
27826
|
+
priority: { type: ["integer", "null"] },
|
|
27827
|
+
code_location: {
|
|
27828
|
+
type: "object",
|
|
27829
|
+
properties: {
|
|
27830
|
+
absolute_file_path: { type: "string" },
|
|
27831
|
+
line_range: {
|
|
27832
|
+
type: "object",
|
|
27833
|
+
properties: {
|
|
27834
|
+
start: { type: "integer" },
|
|
27835
|
+
end: { type: "integer" }
|
|
27836
|
+
},
|
|
27837
|
+
required: ["start", "end"],
|
|
27838
|
+
additionalProperties: false
|
|
27839
|
+
}
|
|
27840
|
+
},
|
|
27841
|
+
required: ["absolute_file_path", "line_range"],
|
|
27842
|
+
additionalProperties: false
|
|
27843
|
+
}
|
|
27844
|
+
},
|
|
27845
|
+
required: ["title", "body", "confidence_score", "priority", "code_location"],
|
|
27846
|
+
additionalProperties: false
|
|
27847
|
+
}
|
|
27848
|
+
},
|
|
27849
|
+
overall_correctness: { type: "string" },
|
|
27850
|
+
overall_explanation: { type: "string" },
|
|
27851
|
+
overall_confidence_score: { type: "number" }
|
|
27852
|
+
},
|
|
27853
|
+
required: ["findings", "overall_correctness", "overall_explanation", "overall_confidence_score"],
|
|
27854
|
+
additionalProperties: false
|
|
27855
|
+
});
|
|
27856
|
+
var SCHEMA_DIR = join8(homedir4(), ".plannotator");
|
|
27857
|
+
var SCHEMA_FILE = join8(SCHEMA_DIR, "codex-review-schema.json");
|
|
27858
|
+
var schemaMaterialized = false;
|
|
27859
|
+
async function ensureSchemaFile() {
|
|
27860
|
+
if (!schemaMaterialized) {
|
|
27861
|
+
await mkdir(SCHEMA_DIR, { recursive: true });
|
|
27862
|
+
await writeFile(SCHEMA_FILE, CODEX_REVIEW_SCHEMA);
|
|
27863
|
+
schemaMaterialized = true;
|
|
27864
|
+
}
|
|
27865
|
+
return SCHEMA_FILE;
|
|
27866
|
+
}
|
|
27867
|
+
var CODEX_REVIEW_SYSTEM_PROMPT = `# Review guidelines:
|
|
27868
|
+
|
|
27869
|
+
You are acting as a reviewer for a proposed code change made by another engineer.
|
|
27870
|
+
|
|
27871
|
+
Below are some default guidelines for determining whether the original author would appreciate the issue being flagged.
|
|
27872
|
+
|
|
27873
|
+
These are not the final word in determining whether an issue is a bug. In many cases, you will encounter other, more specific guidelines. These may be present elsewhere in a developer message, a user message, a file, or even elsewhere in this system message.
|
|
27874
|
+
Those guidelines should be considered to override these general instructions.
|
|
27875
|
+
|
|
27876
|
+
Here are the general guidelines for determining whether something is a bug and should be flagged.
|
|
27877
|
+
|
|
27878
|
+
1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
27879
|
+
2. The bug is discrete and actionable (i.e. not a general issue with the codebase or a combination of multiple issues).
|
|
27880
|
+
3. Fixing the bug does not demand a level of rigor that is not present in the rest of the codebase (e.g. one doesn't need very detailed comments and input validation in a repository of one-off scripts in personal projects)
|
|
27881
|
+
4. The bug was introduced in the commit (pre-existing bugs should not be flagged).
|
|
27882
|
+
5. The author of the original PR would likely fix the issue if they were made aware of it.
|
|
27883
|
+
6. The bug does not rely on unstated assumptions about the codebase or author's intent.
|
|
27884
|
+
7. It is not enough to speculate that a change may disrupt another part of the codebase, to be considered a bug, one must identify the other parts of the code that are provably affected.
|
|
27885
|
+
8. The bug is clearly not just an intentional change by the original author.
|
|
27886
|
+
|
|
27887
|
+
When flagging a bug, you will also provide an accompanying comment. Once again, these guidelines are not the final word on how to construct a comment -- defer to any subsequent guidelines that you encounter.
|
|
27888
|
+
|
|
27889
|
+
1. The comment should be clear about why the issue is a bug.
|
|
27890
|
+
2. The comment should appropriately communicate the severity of the issue. It should not claim that an issue is more severe than it actually is.
|
|
27891
|
+
3. The comment should be brief. The body should be at most 1 paragraph. It should not introduce line breaks within the natural language flow unless it is necessary for the code fragment.
|
|
27892
|
+
4. The comment should not include any chunks of code longer than 3 lines. Any code chunks should be wrapped in markdown inline code tags or a code block.
|
|
27893
|
+
5. The comment should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors.
|
|
27894
|
+
6. The comment's tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer.
|
|
27895
|
+
7. The comment should be written such that the original author can immediately grasp the idea without close reading.
|
|
27896
|
+
8. The comment should avoid excessive flattery and comments that are not helpful to the original author. The comment should avoid phrasing like "Great job ...", "Thanks for ...".
|
|
27897
|
+
|
|
27898
|
+
Below are some more detailed guidelines that you should apply to this specific review.
|
|
27899
|
+
|
|
27900
|
+
HOW MANY FINDINGS TO RETURN:
|
|
27901
|
+
|
|
27902
|
+
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.
|
|
27903
|
+
|
|
27904
|
+
GUIDELINES:
|
|
27905
|
+
|
|
27906
|
+
- Ignore trivial style unless it obscures meaning or violates documented standards.
|
|
27907
|
+
- Use one comment per distinct issue (or a multi-line range if necessary).
|
|
27908
|
+
- Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
|
|
27909
|
+
- In every \`\`\`suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
|
|
27910
|
+
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.
|
|
27911
|
+
|
|
27912
|
+
The comments will be presented in the code review as inline comments. You should avoid providing unnecessary location details in the comment body. Always keep the line range as short as possible for interpreting the issue. Avoid ranges longer than 5\u201310 lines; instead, choose the most suitable subrange that pinpoints the problem.
|
|
27913
|
+
|
|
27914
|
+
At the beginning of the finding title, tag the bug with priority level. For example "[P1] Un-padding slices along wrong tensor dimensions". [P0] \u2013 Drop everything to fix. Blocking release, operations, or major usage. Only use for universal issues that do not depend on any assumptions about the inputs. \xB7 [P1] \u2013 Urgent. Should be addressed in the next cycle \xB7 [P2] \u2013 Normal. To be fixed eventually \xB7 [P3] \u2013 Low. Nice to have.
|
|
27915
|
+
|
|
27916
|
+
Additionally, include a numeric priority field in the JSON output for each finding: set "priority" to 0 for P0, 1 for P1, 2 for P2, or 3 for P3. If a priority cannot be determined, omit the field or use null.
|
|
27917
|
+
|
|
27918
|
+
At the end of your findings, output an "overall correctness" verdict of whether or not the patch should be considered "correct".
|
|
27919
|
+
Correct implies that existing code and tests will not break, and the patch is free of bugs and other blocking issues.
|
|
27920
|
+
Ignore non-blocking issues such as style, formatting, typos, documentation, and other nits.
|
|
27921
|
+
|
|
27922
|
+
FORMATTING GUIDELINES:
|
|
27923
|
+
The finding description should be one paragraph.`;
|
|
27924
|
+
function buildCodexReviewUserMessage(patch, diffType, options, prMetadata) {
|
|
27925
|
+
if (prMetadata) {
|
|
27926
|
+
if (options?.hasLocalAccess) {
|
|
27927
|
+
return [
|
|
27928
|
+
prMetadata.url,
|
|
27929
|
+
"",
|
|
27930
|
+
"You are in a local worktree checked out at the PR head. The code is available locally.",
|
|
27931
|
+
`To see the PR changes, diff against the remote base branch: git diff origin/${prMetadata.baseBranch}...HEAD`,
|
|
27932
|
+
"Do NOT diff against the local `main` branch \u2014 it may be stale. Always use origin/."
|
|
27933
|
+
].join(`
|
|
27934
|
+
`);
|
|
27935
|
+
}
|
|
27936
|
+
return prMetadata.url;
|
|
27937
|
+
}
|
|
27938
|
+
const effectiveDiffType = diffType.startsWith("worktree:") ? diffType.split(":").pop() || "uncommitted" : diffType;
|
|
27939
|
+
switch (effectiveDiffType) {
|
|
27940
|
+
case "uncommitted":
|
|
27941
|
+
return "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.";
|
|
27942
|
+
case "staged":
|
|
27943
|
+
return "Review the currently staged code changes (`git diff --staged`) and provide prioritized findings.";
|
|
27944
|
+
case "unstaged":
|
|
27945
|
+
return "Review the unstaged code changes (tracked modifications and untracked files) and provide prioritized findings.";
|
|
27946
|
+
case "last-commit":
|
|
27947
|
+
return "Review the code changes introduced in the last commit (`git diff HEAD~1..HEAD`) and provide prioritized findings.";
|
|
27948
|
+
case "branch": {
|
|
27949
|
+
const base = options?.defaultBranch || "main";
|
|
27950
|
+
return `Review the code changes against the base branch '${base}'. Run \`git diff ${base}..HEAD\` to inspect the changes. Provide prioritized, actionable findings.`;
|
|
27951
|
+
}
|
|
27952
|
+
default:
|
|
27953
|
+
return [
|
|
27954
|
+
"Review the following code changes and provide prioritized findings.",
|
|
27955
|
+
"",
|
|
27956
|
+
"```diff",
|
|
27957
|
+
patch,
|
|
27958
|
+
"```"
|
|
27959
|
+
].join(`
|
|
27960
|
+
`);
|
|
27961
|
+
}
|
|
27962
|
+
}
|
|
27963
|
+
async function buildCodexCommand(options) {
|
|
27964
|
+
const { cwd, outputPath, prompt } = options;
|
|
27965
|
+
const schemaPath = await ensureSchemaFile();
|
|
27966
|
+
const command = [
|
|
27967
|
+
"codex",
|
|
27968
|
+
"exec",
|
|
27969
|
+
"--output-schema",
|
|
27970
|
+
schemaPath,
|
|
27971
|
+
"-o",
|
|
27972
|
+
outputPath,
|
|
27973
|
+
"--full-auto",
|
|
27974
|
+
"--ephemeral",
|
|
27975
|
+
"-C",
|
|
27976
|
+
cwd,
|
|
27977
|
+
prompt
|
|
27978
|
+
];
|
|
27979
|
+
debugLog("BUILD_COMMAND", {
|
|
27980
|
+
cwd,
|
|
27981
|
+
outputPath,
|
|
27982
|
+
schemaPath,
|
|
27983
|
+
promptLength: prompt.length,
|
|
27984
|
+
command: command.map((c, i) => i === command.length - 1 ? `<prompt: ${c.length} chars>` : c)
|
|
27985
|
+
});
|
|
27986
|
+
return command;
|
|
27987
|
+
}
|
|
27988
|
+
function generateOutputPath() {
|
|
27989
|
+
return join8(tmpdir2(), `plannotator-codex-${crypto.randomUUID()}.json`);
|
|
27990
|
+
}
|
|
27991
|
+
async function parseCodexOutput(outputPath) {
|
|
27992
|
+
await debugLog("PARSE_OUTPUT_START", { outputPath });
|
|
27993
|
+
try {
|
|
27994
|
+
if (!existsSync8(outputPath)) {
|
|
27995
|
+
await debugLog("PARSE_OUTPUT_FILE_MISSING", outputPath);
|
|
27996
|
+
return null;
|
|
27997
|
+
}
|
|
27998
|
+
const text = await readFile(outputPath, "utf-8");
|
|
27999
|
+
try {
|
|
28000
|
+
await unlink(outputPath);
|
|
28001
|
+
} catch {}
|
|
28002
|
+
if (!text.trim()) {
|
|
28003
|
+
await debugLog("PARSE_OUTPUT_EMPTY");
|
|
28004
|
+
return null;
|
|
28005
|
+
}
|
|
28006
|
+
const parsed = JSON.parse(text);
|
|
28007
|
+
if (!parsed || !Array.isArray(parsed.findings)) {
|
|
28008
|
+
await debugLog("PARSE_OUTPUT_INVALID_SHAPE", { hasFindings: !!parsed?.findings });
|
|
28009
|
+
return null;
|
|
28010
|
+
}
|
|
28011
|
+
await debugLog("PARSE_OUTPUT_SUCCESS", {
|
|
28012
|
+
findingsCount: parsed.findings.length,
|
|
28013
|
+
overall_correctness: parsed.overall_correctness,
|
|
28014
|
+
overall_confidence_score: parsed.overall_confidence_score
|
|
28015
|
+
});
|
|
28016
|
+
return parsed;
|
|
28017
|
+
} catch (err) {
|
|
28018
|
+
await debugLog("PARSE_OUTPUT_ERROR", err instanceof Error ? err.message : String(err));
|
|
28019
|
+
try {
|
|
28020
|
+
await unlink(outputPath);
|
|
28021
|
+
} catch {}
|
|
28022
|
+
return null;
|
|
28023
|
+
}
|
|
28024
|
+
}
|
|
28025
|
+
function transformReviewFindings(findings, source, cwd, author) {
|
|
28026
|
+
const annotations = findings.filter((f) => f.code_location?.absolute_file_path && typeof f.code_location?.line_range?.start === "number" && typeof f.code_location?.line_range?.end === "number").map((f) => ({
|
|
28027
|
+
source,
|
|
28028
|
+
filePath: toRelativePath2(f.code_location.absolute_file_path, cwd),
|
|
28029
|
+
lineStart: f.code_location.line_range.start,
|
|
28030
|
+
lineEnd: f.code_location.line_range.end,
|
|
28031
|
+
type: "comment",
|
|
28032
|
+
side: "new",
|
|
28033
|
+
scope: "line",
|
|
28034
|
+
text: `${f.title}
|
|
28035
|
+
|
|
28036
|
+
${f.body}`.trim(),
|
|
28037
|
+
author: author ?? "Review Agent"
|
|
28038
|
+
}));
|
|
28039
|
+
debugLog("TRANSFORM_FINDINGS", {
|
|
28040
|
+
inputCount: findings.length,
|
|
28041
|
+
outputCount: annotations.length,
|
|
28042
|
+
annotations: annotations.map((a) => ({
|
|
28043
|
+
filePath: a.filePath,
|
|
28044
|
+
lineStart: a.lineStart,
|
|
28045
|
+
lineEnd: a.lineEnd,
|
|
28046
|
+
textPreview: a.text.slice(0, 80)
|
|
28047
|
+
}))
|
|
28048
|
+
});
|
|
28049
|
+
return annotations;
|
|
28050
|
+
}
|
|
28051
|
+
|
|
27315
28052
|
// ../../packages/shared/pr-github.ts
|
|
27316
28053
|
function repoFlag(ref) {
|
|
27317
28054
|
if (ref.host !== "github.com") {
|
|
@@ -27372,6 +28109,18 @@ async function fetchGhPR(runtime2, ref) {
|
|
|
27372
28109
|
throw new Error(`Failed to fetch PR metadata: ${viewResult.stderr.trim() || `exit code ${viewResult.exitCode}`}`);
|
|
27373
28110
|
}
|
|
27374
28111
|
const raw = JSON.parse(viewResult.stdout);
|
|
28112
|
+
let mergeBaseSha;
|
|
28113
|
+
try {
|
|
28114
|
+
const compareResult = await runtime2.runCommand("gh", hostnameArgs(ref.host, [
|
|
28115
|
+
"api",
|
|
28116
|
+
`repos/${ref.owner}/${ref.repo}/compare/${raw.baseRefOid}...${raw.headRefOid}`,
|
|
28117
|
+
"--jq",
|
|
28118
|
+
".merge_base_commit.sha"
|
|
28119
|
+
]));
|
|
28120
|
+
if (compareResult.exitCode === 0 && compareResult.stdout.trim()) {
|
|
28121
|
+
mergeBaseSha = compareResult.stdout.trim();
|
|
28122
|
+
}
|
|
28123
|
+
} catch {}
|
|
27375
28124
|
const metadata = {
|
|
27376
28125
|
platform: "github",
|
|
27377
28126
|
host: ref.host,
|
|
@@ -27385,6 +28134,7 @@ async function fetchGhPR(runtime2, ref) {
|
|
|
27385
28134
|
headBranch: raw.headRefName,
|
|
27386
28135
|
baseSha: raw.baseRefOid,
|
|
27387
28136
|
headSha: raw.headRefOid,
|
|
28137
|
+
mergeBaseSha,
|
|
27388
28138
|
url: raw.url
|
|
27389
28139
|
};
|
|
27390
28140
|
return { metadata, rawPatch: diffResult.stdout };
|
|
@@ -27429,8 +28179,10 @@ function parseGhPRContext(raw) {
|
|
|
27429
28179
|
author: login(r?.author),
|
|
27430
28180
|
state: str(r?.state),
|
|
27431
28181
|
body: str(r?.body),
|
|
27432
|
-
submittedAt: str(r?.submittedAt)
|
|
28182
|
+
submittedAt: str(r?.submittedAt),
|
|
28183
|
+
...r?.url ? { url: str(r.url) } : {}
|
|
27433
28184
|
})),
|
|
28185
|
+
reviewThreads: [],
|
|
27434
28186
|
checks: arr(raw.statusCheckRollup).map((c) => ({
|
|
27435
28187
|
name: str(c?.name),
|
|
27436
28188
|
status: str(c?.status),
|
|
@@ -27460,7 +28212,78 @@ async function fetchGhPRContext(runtime2, ref) {
|
|
|
27460
28212
|
throw new Error(`Failed to fetch PR context: ${result.stderr.trim() || `exit code ${result.exitCode}`}`);
|
|
27461
28213
|
}
|
|
27462
28214
|
const raw = JSON.parse(result.stdout);
|
|
27463
|
-
|
|
28215
|
+
const context = parseGhPRContext(raw);
|
|
28216
|
+
try {
|
|
28217
|
+
context.reviewThreads = await fetchGhReviewThreads(runtime2, ref);
|
|
28218
|
+
} catch {
|
|
28219
|
+
context.reviewThreads = [];
|
|
28220
|
+
}
|
|
28221
|
+
return context;
|
|
28222
|
+
}
|
|
28223
|
+
var REVIEW_THREADS_QUERY = `
|
|
28224
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
28225
|
+
repository(owner: $owner, name: $repo) {
|
|
28226
|
+
pullRequest(number: $number) {
|
|
28227
|
+
reviewThreads(first: 100) {
|
|
28228
|
+
nodes {
|
|
28229
|
+
id
|
|
28230
|
+
isResolved
|
|
28231
|
+
isOutdated
|
|
28232
|
+
line
|
|
28233
|
+
startLine
|
|
28234
|
+
path
|
|
28235
|
+
diffSide
|
|
28236
|
+
comments(first: 50) {
|
|
28237
|
+
nodes {
|
|
28238
|
+
id
|
|
28239
|
+
body
|
|
28240
|
+
author { login }
|
|
28241
|
+
createdAt
|
|
28242
|
+
url
|
|
28243
|
+
diffHunk
|
|
28244
|
+
}
|
|
28245
|
+
}
|
|
28246
|
+
}
|
|
28247
|
+
}
|
|
28248
|
+
}
|
|
28249
|
+
}
|
|
28250
|
+
}`;
|
|
28251
|
+
async function fetchGhReviewThreads(runtime2, ref) {
|
|
28252
|
+
const result = await runtime2.runCommand("gh", hostnameArgs(ref.host, [
|
|
28253
|
+
"api",
|
|
28254
|
+
"graphql",
|
|
28255
|
+
"-f",
|
|
28256
|
+
`query=${REVIEW_THREADS_QUERY}`,
|
|
28257
|
+
"-f",
|
|
28258
|
+
`owner=${ref.owner}`,
|
|
28259
|
+
"-f",
|
|
28260
|
+
`repo=${ref.repo}`,
|
|
28261
|
+
"-F",
|
|
28262
|
+
`number=${ref.number}`
|
|
28263
|
+
]));
|
|
28264
|
+
if (result.exitCode !== 0)
|
|
28265
|
+
return [];
|
|
28266
|
+
const data = JSON.parse(result.stdout);
|
|
28267
|
+
const threads = data?.data?.repository?.pullRequest?.reviewThreads?.nodes;
|
|
28268
|
+
if (!Array.isArray(threads))
|
|
28269
|
+
return [];
|
|
28270
|
+
return threads.map((t) => ({
|
|
28271
|
+
id: String(t.id ?? ""),
|
|
28272
|
+
isResolved: t.isResolved === true,
|
|
28273
|
+
isOutdated: t.isOutdated === true,
|
|
28274
|
+
path: String(t.path ?? ""),
|
|
28275
|
+
line: typeof t.line === "number" ? t.line : null,
|
|
28276
|
+
startLine: typeof t.startLine === "number" ? t.startLine : null,
|
|
28277
|
+
diffSide: t.diffSide === "LEFT" || t.diffSide === "RIGHT" ? t.diffSide : null,
|
|
28278
|
+
comments: Array.isArray(t.comments?.nodes) ? t.comments.nodes.map((c) => ({
|
|
28279
|
+
id: String(c.id ?? ""),
|
|
28280
|
+
author: c.author?.login ? String(c.author.login) : "",
|
|
28281
|
+
body: String(c.body ?? ""),
|
|
28282
|
+
createdAt: String(c.createdAt ?? ""),
|
|
28283
|
+
url: String(c.url ?? ""),
|
|
28284
|
+
...c.diffHunk ? { diffHunk: String(c.diffHunk) } : {}
|
|
28285
|
+
})) : []
|
|
28286
|
+
}));
|
|
27464
28287
|
}
|
|
27465
28288
|
async function fetchGhPRFileContent(runtime2, ref, sha, filePath) {
|
|
27466
28289
|
const result = await runtime2.runCommand("gh", hostnameArgs(ref.host, [
|
|
@@ -27822,6 +28645,7 @@ async function fetchGlMRContext(runtime2, ref) {
|
|
|
27822
28645
|
mergeStateStatus,
|
|
27823
28646
|
comments: notes,
|
|
27824
28647
|
reviews,
|
|
28648
|
+
reviewThreads: [],
|
|
27825
28649
|
checks,
|
|
27826
28650
|
linkedIssues
|
|
27827
28651
|
};
|
|
@@ -28322,6 +29146,7 @@ var RETRY_DELAY_MS2 = 500;
|
|
|
28322
29146
|
async function startReviewServer(options) {
|
|
28323
29147
|
const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady, prMetadata } = options;
|
|
28324
29148
|
const isPRMode = !!prMetadata;
|
|
29149
|
+
const hasLocalAccess = !!gitContext;
|
|
28325
29150
|
const draftKey = contentHash(options.rawPatch);
|
|
28326
29151
|
const editorAnnotations = createEditorAnnotationHandler();
|
|
28327
29152
|
const externalAnnotations = createExternalAnnotationHandler("review");
|
|
@@ -28334,7 +29159,75 @@ async function startReviewServer(options) {
|
|
|
28334
29159
|
mode: "review",
|
|
28335
29160
|
getServerUrl: () => serverUrl,
|
|
28336
29161
|
getCwd: () => {
|
|
29162
|
+
if (options.agentCwd)
|
|
29163
|
+
return options.agentCwd;
|
|
28337
29164
|
return resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd();
|
|
29165
|
+
},
|
|
29166
|
+
async buildCommand(provider) {
|
|
29167
|
+
const cwd = options.agentCwd ?? resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd();
|
|
29168
|
+
const hasAgentLocalAccess = !!options.agentCwd || !!gitContext;
|
|
29169
|
+
const userMessage = buildCodexReviewUserMessage(currentPatch, currentDiffType, { defaultBranch: gitContext?.defaultBranch, hasLocalAccess: hasAgentLocalAccess }, prMetadata);
|
|
29170
|
+
if (provider === "codex") {
|
|
29171
|
+
const outputPath = generateOutputPath();
|
|
29172
|
+
const prompt = CODEX_REVIEW_SYSTEM_PROMPT + `
|
|
29173
|
+
|
|
29174
|
+
---
|
|
29175
|
+
|
|
29176
|
+
` + userMessage;
|
|
29177
|
+
const command = await buildCodexCommand({ cwd, outputPath, prompt });
|
|
29178
|
+
return { command, outputPath, prompt, label: "Codex Review" };
|
|
29179
|
+
}
|
|
29180
|
+
if (provider === "claude") {
|
|
29181
|
+
const prompt = CLAUDE_REVIEW_PROMPT + `
|
|
29182
|
+
|
|
29183
|
+
---
|
|
29184
|
+
|
|
29185
|
+
` + userMessage;
|
|
29186
|
+
const { command, stdinPrompt } = buildClaudeCommand(prompt);
|
|
29187
|
+
return { command, stdinPrompt, prompt, cwd, label: "Claude Code Review", captureStdout: true };
|
|
29188
|
+
}
|
|
29189
|
+
return null;
|
|
29190
|
+
},
|
|
29191
|
+
async onJobComplete(job, meta) {
|
|
29192
|
+
const cwd = options.agentCwd ?? resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd();
|
|
29193
|
+
if (job.provider === "codex" && meta.outputPath) {
|
|
29194
|
+
const output = await parseCodexOutput(meta.outputPath);
|
|
29195
|
+
if (!output)
|
|
29196
|
+
return;
|
|
29197
|
+
const hasBlockingFindings = output.findings.some((f7) => f7.priority !== null && f7.priority <= 1);
|
|
29198
|
+
job.summary = {
|
|
29199
|
+
correctness: hasBlockingFindings ? "Issues Found" : output.overall_correctness,
|
|
29200
|
+
explanation: output.overall_explanation,
|
|
29201
|
+
confidence: output.overall_confidence_score
|
|
29202
|
+
};
|
|
29203
|
+
if (output.findings.length > 0) {
|
|
29204
|
+
const annotations = transformReviewFindings(output.findings, job.source, cwd, "Codex");
|
|
29205
|
+
const result = externalAnnotations.addAnnotations({ annotations });
|
|
29206
|
+
if ("error" in result)
|
|
29207
|
+
console.error(`[codex-review] addAnnotations error:`, result.error);
|
|
29208
|
+
}
|
|
29209
|
+
return;
|
|
29210
|
+
}
|
|
29211
|
+
if (job.provider === "claude" && meta.stdout) {
|
|
29212
|
+
const output = parseClaudeStreamOutput(meta.stdout);
|
|
29213
|
+
if (!output) {
|
|
29214
|
+
console.error(`[claude-review] Failed to parse output (${meta.stdout.length} bytes, last 200: ${meta.stdout.slice(-200)})`);
|
|
29215
|
+
return;
|
|
29216
|
+
}
|
|
29217
|
+
const total = output.summary.important + output.summary.nit + output.summary.pre_existing;
|
|
29218
|
+
job.summary = {
|
|
29219
|
+
correctness: output.summary.important === 0 ? "Correct" : "Issues Found",
|
|
29220
|
+
explanation: `${output.summary.important} important, ${output.summary.nit} nit, ${output.summary.pre_existing} pre-existing`,
|
|
29221
|
+
confidence: total === 0 ? 1 : Math.max(0, 1 - output.summary.important * 0.2)
|
|
29222
|
+
};
|
|
29223
|
+
if (output.findings.length > 0) {
|
|
29224
|
+
const annotations = transformClaudeFindings(output.findings, job.source, cwd);
|
|
29225
|
+
const result = externalAnnotations.addAnnotations({ annotations });
|
|
29226
|
+
if ("error" in result)
|
|
29227
|
+
console.error(`[claude-review] addAnnotations error:`, result.error);
|
|
29228
|
+
}
|
|
29229
|
+
return;
|
|
29230
|
+
}
|
|
28338
29231
|
}
|
|
28339
29232
|
});
|
|
28340
29233
|
const aiRegistry = new ProviderRegistry;
|
|
@@ -28395,6 +29288,8 @@ async function startReviewServer(options) {
|
|
|
28395
29288
|
registry: aiRegistry,
|
|
28396
29289
|
sessionManager: aiSessionManager,
|
|
28397
29290
|
getCwd: () => {
|
|
29291
|
+
if (options.agentCwd)
|
|
29292
|
+
return options.agentCwd;
|
|
28398
29293
|
return resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd();
|
|
28399
29294
|
}
|
|
28400
29295
|
});
|
|
@@ -28429,12 +29324,13 @@ async function startReviewServer(options) {
|
|
|
28429
29324
|
rawPatch: currentPatch,
|
|
28430
29325
|
gitRef: currentGitRef,
|
|
28431
29326
|
origin,
|
|
28432
|
-
diffType:
|
|
28433
|
-
gitContext:
|
|
29327
|
+
diffType: hasLocalAccess ? currentDiffType : undefined,
|
|
29328
|
+
gitContext: hasLocalAccess ? gitContext : undefined,
|
|
28434
29329
|
sharingEnabled,
|
|
28435
29330
|
shareBaseUrl,
|
|
28436
29331
|
repoInfo,
|
|
28437
29332
|
isWSL: wslFlag,
|
|
29333
|
+
...options.agentCwd && { agentCwd: options.agentCwd },
|
|
28438
29334
|
...isPRMode && { prMetadata, platformUser },
|
|
28439
29335
|
...isPRMode && initialViewedFiles.length > 0 && { viewedFiles: initialViewedFiles },
|
|
28440
29336
|
...currentError && { error: currentError },
|
|
@@ -28442,8 +29338,8 @@ async function startReviewServer(options) {
|
|
|
28442
29338
|
});
|
|
28443
29339
|
}
|
|
28444
29340
|
if (url.pathname === "/api/diff/switch" && req.method === "POST") {
|
|
28445
|
-
if (
|
|
28446
|
-
return Response.json({ error: "Not available
|
|
29341
|
+
if (!hasLocalAccess) {
|
|
29342
|
+
return Response.json({ error: "Not available without local file access" }, { status: 400 });
|
|
28447
29343
|
}
|
|
28448
29344
|
try {
|
|
28449
29345
|
const body = await req.json();
|
|
@@ -28499,17 +29395,21 @@ async function startReviewServer(options) {
|
|
|
28499
29395
|
return Response.json({ error: "Invalid path" }, { status: 400 });
|
|
28500
29396
|
}
|
|
28501
29397
|
}
|
|
29398
|
+
if (hasLocalAccess) {
|
|
29399
|
+
const defaultBranch = gitContext?.defaultBranch || "main";
|
|
29400
|
+
const defaultCwd = gitContext?.cwd;
|
|
29401
|
+
const result = await getVcsFileContentsForDiff(currentDiffType, defaultBranch, filePath, oldPath, defaultCwd);
|
|
29402
|
+
return Response.json(result);
|
|
29403
|
+
}
|
|
28502
29404
|
if (isPRMode) {
|
|
29405
|
+
const oldSha = prMetadata.mergeBaseSha ?? prMetadata.baseSha;
|
|
28503
29406
|
const [oldContent, newContent] = await Promise.all([
|
|
28504
|
-
fetchPRFileContent2(prRef,
|
|
29407
|
+
fetchPRFileContent2(prRef, oldSha, oldPath || filePath),
|
|
28505
29408
|
fetchPRFileContent2(prRef, prMetadata.headSha, filePath)
|
|
28506
29409
|
]);
|
|
28507
29410
|
return Response.json({ oldContent, newContent });
|
|
28508
29411
|
}
|
|
28509
|
-
|
|
28510
|
-
const defaultCwd = gitContext?.cwd;
|
|
28511
|
-
const result = await getVcsFileContentsForDiff(currentDiffType, defaultBranch, filePath, oldPath, defaultCwd);
|
|
28512
|
-
return Response.json(result);
|
|
29412
|
+
return Response.json({ error: "No file access available" }, { status: 400 });
|
|
28513
29413
|
}
|
|
28514
29414
|
if (url.pathname === "/api/git-add" && req.method === "POST") {
|
|
28515
29415
|
if (isPRMode || !canStageFiles(currentDiffType)) {
|
|
@@ -28598,10 +29498,13 @@ async function startReviewServer(options) {
|
|
|
28598
29498
|
}
|
|
28599
29499
|
try {
|
|
28600
29500
|
const body = await req.json();
|
|
29501
|
+
console.error(`[pr-action] ${body.action} with ${body.fileComments.length} file comment(s), headSha=${prMetadata.headSha}`);
|
|
28601
29502
|
await submitPRReview2(prRef, prMetadata.headSha, body.action, body.body, body.fileComments);
|
|
29503
|
+
console.error(`[pr-action] Success`);
|
|
28602
29504
|
return Response.json({ ok: true, prUrl: prMetadata.url });
|
|
28603
29505
|
} catch (err) {
|
|
28604
29506
|
const message = err instanceof Error ? err.message : "Failed to submit PR review";
|
|
29507
|
+
console.error(`[pr-action] Failed: ${message}`);
|
|
28605
29508
|
return Response.json({ error: message }, { status: 500 });
|
|
28606
29509
|
}
|
|
28607
29510
|
}
|
|
@@ -28674,6 +29577,13 @@ async function startReviewServer(options) {
|
|
|
28674
29577
|
aiSessionManager.disposeAll();
|
|
28675
29578
|
aiRegistry.disposeAll();
|
|
28676
29579
|
server2.stop();
|
|
29580
|
+
if (options.onCleanup) {
|
|
29581
|
+
try {
|
|
29582
|
+
const result = options.onCleanup();
|
|
29583
|
+
if (result instanceof Promise)
|
|
29584
|
+
result.catch(() => {});
|
|
29585
|
+
} catch {}
|
|
29586
|
+
}
|
|
28677
29587
|
}
|
|
28678
29588
|
};
|
|
28679
29589
|
}
|
|
@@ -29084,7 +29994,7 @@ function getReviewHtml() {
|
|
|
29084
29994
|
}
|
|
29085
29995
|
var DEFAULT_PLAN_TIMEOUT_SECONDS = 345600;
|
|
29086
29996
|
function isFilePath(value) {
|
|
29087
|
-
return path3.isAbsolute(value) && value.endsWith(".md") &&
|
|
29997
|
+
return path3.isAbsolute(value) && value.endsWith(".md") && existsSync10(value);
|
|
29088
29998
|
}
|
|
29089
29999
|
function resolvePlanContent(plan) {
|
|
29090
30000
|
if (isFilePath(plan)) {
|