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