@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/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 existsSync9, readFileSync as readFileSync6 } from "fs";
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 isRemoteSession() {
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
- async function openBrowser(url) {
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 resolveMarkdownFile(input, projectRoot) {
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: getCwd(),
27150
- stdout: "ignore",
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
- proc.exited.then((exitCode) => {
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 : "shell";
27279
- const rawCommand = Array.isArray(body.command) ? body.command : [];
27280
- const command = rawCommand.filter((c) => typeof c === "string");
27281
- const label = typeof body.label === "string" ? body.label : `${provider} agent`;
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
- return parseGhPRContext(raw);
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: isPRMode ? undefined : currentDiffType,
28433
- gitContext: isPRMode ? undefined : 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 (isPRMode) {
28446
- return Response.json({ error: "Not available for PR reviews" }, { status: 400 });
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, prMetadata.baseSha, oldPath || filePath),
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
- const defaultBranch = gitContext?.defaultBranch || "main";
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") && existsSync9(value);
29992
+ return path3.isAbsolute(value) && value.endsWith(".md") && existsSync10(value);
29088
29993
  }
29089
29994
  function resolvePlanContent(plan) {
29090
29995
  if (isFilePath(plan)) {