@plannotator/opencode 0.16.7 → 0.17.1

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