@plannotator/opencode 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/embedded.js CHANGED
@@ -1787,6 +1787,9 @@ function writeServerReadyMetadata(readyFile, metadata) {
1787
1787
  appendFileSync(readyFile, `${JSON.stringify(metadata)}
1788
1788
  `, "utf8");
1789
1789
  }
1790
+ function isCodexDesktopHost(env = process.env) {
1791
+ return env.__CFBundleIdentifier === "com.openai.codex";
1792
+ }
1790
1793
  async function handleServerReady(url, isRemote, port, options = {}) {
1791
1794
  const readyFile = options.readyFile ?? process.env.PLANNOTATOR_READY_FILE;
1792
1795
  if (readyFile) {
@@ -1802,6 +1805,12 @@ async function handleServerReady(url, isRemote, port, options = {}) {
1802
1805
  Plannotator session ready \u2014 open on your local machine (forward port ${port} if needed):
1803
1806
  ${url}
1804
1807
 
1808
+ `);
1809
+ } else if (isCodexDesktopHost()) {
1810
+ process.stderr.write(`
1811
+ Plannotator session ready:
1812
+ ${url}
1813
+
1805
1814
  `);
1806
1815
  }
1807
1816
  const skipBrowserOpen = options.skipBrowserOpen ?? process.env.PLANNOTATOR_SKIP_BROWSER_OPEN === "1";
@@ -40677,6 +40686,22 @@ function transformPlanInput(body3) {
40677
40686
  }
40678
40687
  return { annotations };
40679
40688
  }
40689
+ function classifyFindingPlacement(filePath, lineStart, lineEnd) {
40690
+ const hasFile = filePath.length > 0;
40691
+ const hasLine = typeof lineStart === "number";
40692
+ if (hasFile && hasLine) {
40693
+ return {
40694
+ scope: "line",
40695
+ filePath,
40696
+ lineStart,
40697
+ lineEnd: typeof lineEnd === "number" ? lineEnd : lineStart
40698
+ };
40699
+ }
40700
+ if (hasFile) {
40701
+ return { scope: "file", filePath, lineStart: 0, lineEnd: 0 };
40702
+ }
40703
+ return { scope: "general", filePath: "", lineStart: 0, lineEnd: 0 };
40704
+ }
40680
40705
  function transformReviewInput(body3) {
40681
40706
  const items = unwrapBody(body3);
40682
40707
  if ("error" in items)
@@ -40687,14 +40712,33 @@ function transformReviewInput(body3) {
40687
40712
  const source = requireString(obj, "source", i3);
40688
40713
  if (typeof source !== "string")
40689
40714
  return source;
40690
- const filePath = requireString(obj, "filePath", i3);
40691
- if (typeof filePath !== "string")
40692
- return filePath;
40693
- if (typeof obj.lineStart !== "number") {
40694
- return { error: `annotations[${i3}] missing required "lineStart" field` };
40715
+ const scope = typeof obj.scope === "string" ? obj.scope : "line";
40716
+ if (!VALID_SCOPES.includes(scope)) {
40717
+ return {
40718
+ error: `annotations[${i3}] invalid scope "${scope}". Must be one of: ${VALID_SCOPES.join(", ")}`
40719
+ };
40695
40720
  }
40696
- if (typeof obj.lineEnd !== "number") {
40697
- return { error: `annotations[${i3}] missing required "lineEnd" field` };
40721
+ let filePath = "";
40722
+ let lineStart = 0;
40723
+ let lineEnd = 0;
40724
+ if (scope !== "general") {
40725
+ const fp = requireString(obj, "filePath", i3);
40726
+ if (typeof fp !== "string")
40727
+ return fp;
40728
+ filePath = fp;
40729
+ if (scope === "line") {
40730
+ if (typeof obj.lineStart !== "number") {
40731
+ return { error: `annotations[${i3}] missing required "lineStart" field` };
40732
+ }
40733
+ if (typeof obj.lineEnd !== "number") {
40734
+ return { error: `annotations[${i3}] missing required "lineEnd" field` };
40735
+ }
40736
+ lineStart = obj.lineStart;
40737
+ lineEnd = obj.lineEnd;
40738
+ } else {
40739
+ lineStart = typeof obj.lineStart === "number" ? obj.lineStart : 0;
40740
+ lineEnd = typeof obj.lineEnd === "number" ? obj.lineEnd : 0;
40741
+ }
40698
40742
  }
40699
40743
  const side = typeof obj.side === "string" ? obj.side : "new";
40700
40744
  if (!VALID_SIDES.includes(side)) {
@@ -40708,12 +40752,6 @@ function transformReviewInput(body3) {
40708
40752
  error: `annotations[${i3}] invalid type "${type}". Must be one of: ${VALID_REVIEW_TYPES.join(", ")}`
40709
40753
  };
40710
40754
  }
40711
- const scope = typeof obj.scope === "string" ? obj.scope : "line";
40712
- if (!VALID_SCOPES.includes(scope)) {
40713
- return {
40714
- error: `annotations[${i3}] invalid scope "${scope}". Must be one of: ${VALID_SCOPES.join(", ")}`
40715
- };
40716
- }
40717
40755
  if (typeof obj.text !== "string" && typeof obj.suggestedCode !== "string") {
40718
40756
  return {
40719
40757
  error: `annotations[${i3}] must have at least one of: text, suggestedCode`
@@ -40724,8 +40762,8 @@ function transformReviewInput(body3) {
40724
40762
  type,
40725
40763
  scope,
40726
40764
  filePath,
40727
- lineStart: obj.lineStart,
40728
- lineEnd: obj.lineEnd,
40765
+ lineStart,
40766
+ lineEnd,
40729
40767
  side,
40730
40768
  text: typeof obj.text === "string" ? obj.text : undefined,
40731
40769
  suggestedCode: typeof obj.suggestedCode === "string" ? obj.suggestedCode : undefined,
@@ -40734,7 +40772,8 @@ function transformReviewInput(body3) {
40734
40772
  author: typeof obj.author === "string" ? obj.author : undefined,
40735
40773
  source,
40736
40774
  ...typeof obj.severity === "string" && { severity: obj.severity },
40737
- ...typeof obj.reasoning === "string" && { reasoning: obj.reasoning }
40775
+ ...typeof obj.reasoning === "string" && { reasoning: obj.reasoning },
40776
+ ...typeof obj.reviewProfileLabel === "string" && { reviewProfileLabel: obj.reviewProfileLabel }
40738
40777
  });
40739
40778
  }
40740
40779
  return { annotations };
@@ -40824,7 +40863,7 @@ var init_external_annotation = __esm(() => {
40824
40863
  VALID_PLAN_TYPES = ["DELETION", "COMMENT", "GLOBAL_COMMENT"];
40825
40864
  VALID_REVIEW_TYPES = ["comment", "suggestion", "concern"];
40826
40865
  VALID_SIDES = ["old", "new"];
40827
- VALID_SCOPES = ["line", "file"];
40866
+ VALID_SCOPES = ["line", "file", "general"];
40828
40867
  });
40829
40868
 
40830
40869
  // ../../packages/server/external-annotations.ts
@@ -69503,10 +69542,21 @@ var init_dist8 = __esm(() => {
69503
69542
  // ../../packages/ai/providers/codex-sdk.ts
69504
69543
  var exports_codex_sdk = {};
69505
69544
  __export(exports_codex_sdk, {
69545
+ shouldSkipGitRepoCheck: () => shouldSkipGitRepoCheck,
69506
69546
  mapCodexItem: () => mapCodexItem,
69507
69547
  mapCodexEvent: () => mapCodexEvent,
69508
69548
  CodexSDKProvider: () => CodexSDKProvider
69509
69549
  });
69550
+ import { execFile } from "child_process";
69551
+ import { promisify } from "util";
69552
+ async function shouldSkipGitRepoCheck(cwd, probe = execFileAsync) {
69553
+ try {
69554
+ const result = probe("git", ["-C", cwd, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8" });
69555
+ return String((await result).stdout ?? "").trim() !== "true";
69556
+ } catch {
69557
+ return true;
69558
+ }
69559
+ }
69510
69560
 
69511
69561
  class CodexSDKProvider {
69512
69562
  name = PROVIDER_NAME2;
@@ -69530,10 +69580,13 @@ class CodexSDKProvider {
69530
69580
  this.config = config;
69531
69581
  }
69532
69582
  async createSession(options) {
69583
+ const cwd = options.cwd ?? this.config.cwd ?? process.cwd();
69584
+ const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd);
69533
69585
  return new CodexSDKSession({
69534
69586
  ...this.baseConfig(options),
69535
69587
  systemPrompt: buildSystemPrompt(options.context),
69536
- cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
69588
+ cwd,
69589
+ skipGitRepoCheck,
69537
69590
  parentSessionId: null
69538
69591
  });
69539
69592
  }
@@ -69541,10 +69594,13 @@ class CodexSDKProvider {
69541
69594
  throw new Error("Codex does not support session forking. " + "The endpoint layer should fall back to createSession().");
69542
69595
  }
69543
69596
  async resumeSession(sessionId) {
69597
+ const cwd = this.config.cwd ?? process.cwd();
69598
+ const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd);
69544
69599
  return new CodexSDKSession({
69545
69600
  ...this.baseConfig(),
69546
69601
  systemPrompt: null,
69547
- cwd: this.config.cwd ?? process.cwd(),
69602
+ cwd,
69603
+ skipGitRepoCheck,
69548
69604
  parentSessionId: null,
69549
69605
  resumeThreadId: sessionId
69550
69606
  });
@@ -69701,10 +69757,11 @@ function mapCodexItem(event, itemTextOffsets) {
69701
69757
  return [{ type: "unknown", raw: { eventType, item } }];
69702
69758
  }
69703
69759
  }
69704
- var PROVIDER_NAME2 = "codex-sdk", DEFAULT_MODEL2 = "gpt-5.4", CodexClass = null, CodexSDKSession;
69760
+ var PROVIDER_NAME2 = "codex-sdk", DEFAULT_MODEL2 = "gpt-5.4", execFileAsync, CodexClass = null, CodexSDKSession;
69705
69761
  var init_codex_sdk = __esm(() => {
69706
69762
  init_base_session();
69707
69763
  init_provider();
69764
+ execFileAsync = promisify(execFile);
69708
69765
  CodexSDKSession = class CodexSDKSession extends BaseSession {
69709
69766
  config;
69710
69767
  _codexInstance = null;
@@ -69740,6 +69797,7 @@ var init_codex_sdk = __esm(() => {
69740
69797
  this._thread = this._codexInstance.resumeThread(this.config.resumeThreadId, {
69741
69798
  model: this.config.model,
69742
69799
  workingDirectory: this.config.cwd,
69800
+ skipGitRepoCheck: this.config.skipGitRepoCheck,
69743
69801
  sandboxMode: this.config.sandboxMode,
69744
69802
  ...this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }
69745
69803
  });
@@ -69747,6 +69805,7 @@ var init_codex_sdk = __esm(() => {
69747
69805
  this._thread = this._codexInstance.startThread({
69748
69806
  model: this.config.model,
69749
69807
  workingDirectory: this.config.cwd,
69808
+ skipGitRepoCheck: this.config.skipGitRepoCheck,
69750
69809
  sandboxMode: this.config.sandboxMode,
69751
69810
  ...this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }
69752
69811
  });
@@ -75390,6 +75449,69 @@ var init_pr_stack = __esm(() => {
75390
75449
  FULL_SHA_RE = /^[0-9a-f]{40,64}$/i;
75391
75450
  });
75392
75451
 
75452
+ // ../../packages/shared/agent-jobs.ts
75453
+ function serializeAgentSSEEvent(event) {
75454
+ return `data: ${JSON.stringify(event)}
75455
+
75456
+ `;
75457
+ }
75458
+ function isTerminalStatus(status) {
75459
+ return status === "done" || status === "failed" || status === "killed";
75460
+ }
75461
+ function jobSource(id) {
75462
+ return "agent-" + id.slice(0, 8);
75463
+ }
75464
+ function markJobReviewFailed(job, error) {
75465
+ job.status = "failed";
75466
+ job.error = error;
75467
+ }
75468
+ var AGENT_HEARTBEAT_COMMENT = `:
75469
+
75470
+ `, AGENT_HEARTBEAT_INTERVAL_MS = 30000, REVIEW_OUTPUT_FAILED = "Review finished but produced no usable findings.";
75471
+
75472
+ // ../../packages/shared/review-profiles.ts
75473
+ function profileHasCustomSection(profile) {
75474
+ return !!profile && profile.id !== BUILTIN_DEFAULT_ID && profile.instructions.trim().length > 0;
75475
+ }
75476
+ function composeReviewPrompt(systemPrompt, profile, userMessage) {
75477
+ if (profileHasCustomSection(profile)) {
75478
+ return profile.instructions.trim() + `
75479
+
75480
+ ` + REPORTING_INSTRUCTIONS + `
75481
+
75482
+ ---
75483
+
75484
+ ` + userMessage;
75485
+ }
75486
+ return systemPrompt + `
75487
+
75488
+ ---
75489
+
75490
+ ` + userMessage;
75491
+ }
75492
+ var BUILTIN_DEFAULT_ID = "builtin:default", BUILTIN_DEFAULT_PROFILE, REPORTING_INSTRUCTIONS = `## Returning your findings
75493
+
75494
+ Hand back what you found as separate findings, not as one combined report.
75495
+
75496
+ - One finding per issue. Don't merge unrelated points into a single entry.
75497
+ - Anchor each finding where it belongs:
75498
+ - About specific code? Give the file and the line(s), so it attaches to that spot in the diff.
75499
+ - About a whole file? Give the file and leave the line out.
75500
+ - A review-wide point? Leave out both the file and the line.
75501
+ - Always produce the code-specific findings first.
75502
+ - If your instructions also ask for a final verdict, summary, or overall judgment, add it as its own review-wide finding with no file and no line. The verdict is in addition to the specific findings, never a replacement for them.
75503
+
75504
+ If the instructions above told you to produce a particular report layout or document, that was for your own reasoning. For the final result, return findings in the structure described here: the specific code findings, plus any verdict as a separate review-wide finding.`;
75505
+ var init_review_profiles = __esm(() => {
75506
+ BUILTIN_DEFAULT_PROFILE = {
75507
+ id: BUILTIN_DEFAULT_ID,
75508
+ label: "Default",
75509
+ instructions: "",
75510
+ source: "builtin",
75511
+ default: true
75512
+ };
75513
+ });
75514
+
75393
75515
  // ../../packages/server/path-utils.ts
75394
75516
  import { relative as relative7 } from "path";
75395
75517
  function toRelativePath2(absolutePath, cwd) {
@@ -75402,7 +75524,36 @@ function toRelativePath2(absolutePath, cwd) {
75402
75524
  }
75403
75525
  var init_path_utils = () => {};
75404
75526
 
75527
+ // ../../packages/server/review-findings.ts
75528
+ function transformSeverityFindings(findings, source, author, cwd, pathTransform) {
75529
+ return findings.map((f9) => {
75530
+ const rawFile = typeof f9.file === "string" ? f9.file : "";
75531
+ const filePath = rawFile ? pathTransform ? pathTransform(toRelativePath2(rawFile, cwd)) : toRelativePath2(rawFile, cwd) : "";
75532
+ const placement = classifyFindingPlacement(filePath, f9.line, f9.end_line);
75533
+ return {
75534
+ source,
75535
+ filePath: placement.filePath,
75536
+ lineStart: placement.lineStart,
75537
+ lineEnd: placement.lineEnd,
75538
+ type: "comment",
75539
+ side: "new",
75540
+ scope: placement.scope,
75541
+ text: `[${f9.severity}] ${f9.description}`,
75542
+ severity: f9.severity,
75543
+ reasoning: f9.reasoning,
75544
+ author
75545
+ };
75546
+ });
75547
+ }
75548
+ var init_review_findings = __esm(() => {
75549
+ init_external_annotation();
75550
+ init_path_utils();
75551
+ });
75552
+
75405
75553
  // ../../packages/server/claude-review.ts
75554
+ function composeClaudeReviewPrompt(userMessage, reviewProfile) {
75555
+ return composeReviewPrompt(CLAUDE_REVIEW_PROMPT, reviewProfile, userMessage);
75556
+ }
75406
75557
  function buildClaudeCommand(prompt, model = "claude-opus-4-7", effort) {
75407
75558
  const allowedTools = [
75408
75559
  "Agent",
@@ -75512,19 +75663,7 @@ function parseClaudeStreamOutput(stdout) {
75512
75663
  return null;
75513
75664
  }
75514
75665
  function transformClaudeFindings(findings, source, cwd, pathTransform) {
75515
- return findings.filter((f9) => f9.file && typeof f9.line === "number").map((f9) => ({
75516
- source,
75517
- filePath: pathTransform ? pathTransform(toRelativePath2(f9.file, cwd)) : toRelativePath2(f9.file, cwd),
75518
- lineStart: f9.line,
75519
- lineEnd: f9.end_line ?? f9.line,
75520
- type: "comment",
75521
- side: "new",
75522
- scope: "line",
75523
- text: `[${f9.severity}] ${f9.description}`,
75524
- severity: f9.severity,
75525
- reasoning: f9.reasoning,
75526
- author: "Claude Code"
75527
- }));
75666
+ return transformSeverityFindings(findings, source, "Claude Code", cwd, pathTransform);
75528
75667
  }
75529
75668
  function formatClaudeLogEvent(line) {
75530
75669
  try {
@@ -75631,6 +75770,10 @@ Step 5: Deduplicate and rank
75631
75770
  - Within each severity, sort by file path and line number
75632
75771
 
75633
75772
  Step 6: Return structured JSON output matching the schema.
75773
+ Place each finding by how specific it is: give file and line for a line-level
75774
+ issue; give file and set line null for a whole-file issue; set file and line
75775
+ null for a general, review-level note. Never invent a line you are unsure of \u2014
75776
+ drop to a file or general placement instead of guessing.
75634
75777
  If no issues are found, return an empty findings array with zeroed summary.
75635
75778
 
75636
75779
  ## Hard constraints
@@ -75645,7 +75788,8 @@ Step 6: Return structured JSON output matching the schema.
75645
75788
  - Do NOT use gh pr comment or any commenting tool
75646
75789
  - Your only output is the structured JSON findings`;
75647
75790
  var init_claude_review = __esm(() => {
75648
- init_path_utils();
75791
+ init_review_profiles();
75792
+ init_review_findings();
75649
75793
  CLAUDE_REVIEW_SCHEMA_JSON = JSON.stringify({
75650
75794
  type: "object",
75651
75795
  properties: {
@@ -75655,9 +75799,9 @@ var init_claude_review = __esm(() => {
75655
75799
  type: "object",
75656
75800
  properties: {
75657
75801
  severity: { type: "string", enum: ["important", "nit", "pre_existing"] },
75658
- file: { type: "string" },
75659
- line: { type: "integer" },
75660
- end_line: { type: "integer" },
75802
+ file: { type: ["string", "null"] },
75803
+ line: { type: ["integer", "null"] },
75804
+ end_line: { type: ["integer", "null"] },
75661
75805
  description: { type: "string" },
75662
75806
  reasoning: { type: "string" }
75663
75807
  },
@@ -75681,23 +75825,513 @@ var init_claude_review = __esm(() => {
75681
75825
  });
75682
75826
  });
75683
75827
 
75684
- // ../../packages/shared/agent-jobs.ts
75685
- function serializeAgentSSEEvent(event) {
75686
- return `data: ${JSON.stringify(event)}
75687
-
75688
- `;
75828
+ // ../../packages/server/marker-review.ts
75829
+ function markerOpen(nonce) {
75830
+ return `<${MARKER_TAG}:${nonce}>`;
75689
75831
  }
75690
- function isTerminalStatus(status) {
75691
- return status === "done" || status === "failed" || status === "killed";
75832
+ function markerClose(nonce) {
75833
+ return `</${MARKER_TAG}:${nonce}>`;
75692
75834
  }
75693
- function jobSource(id) {
75694
- return "agent-" + id.slice(0, 8);
75835
+ function makeMarkerNonce() {
75836
+ return "pn" + crypto.randomUUID().replace(/-/g, "").slice(0, 12);
75695
75837
  }
75696
- var AGENT_HEARTBEAT_COMMENT = `:
75838
+ function extractMarkerNonce(prompt) {
75839
+ const m6 = new RegExp(`<${MARKER_TAG}:(pn[0-9a-f]{12})>`).exec(prompt);
75840
+ return m6 ? m6[1] : null;
75841
+ }
75842
+ function optionalNullableInteger(value) {
75843
+ if (value === undefined || value === null)
75844
+ return { ok: true, value: null };
75845
+ if (typeof value === "number" && Number.isInteger(value))
75846
+ return { ok: true, value };
75847
+ return { ok: false };
75848
+ }
75849
+ function optionalNullableString(value) {
75850
+ if (value === undefined || value === null)
75851
+ return { ok: true, value: null };
75852
+ if (typeof value === "string")
75853
+ return { ok: true, value };
75854
+ return { ok: false };
75855
+ }
75856
+ function validateMarkerReviewOutput(parsed) {
75857
+ if (!parsed || typeof parsed !== "object")
75858
+ return null;
75859
+ const obj = parsed;
75860
+ if (!Array.isArray(obj.findings))
75861
+ return null;
75862
+ const summary = obj.summary;
75863
+ if (!summary || typeof summary !== "object")
75864
+ return null;
75865
+ const s10 = summary;
75866
+ if (typeof s10.correctness !== "string")
75867
+ return null;
75868
+ if (typeof s10.explanation !== "string")
75869
+ return null;
75870
+ if (typeof s10.confidence !== "number" || !Number.isFinite(s10.confidence))
75871
+ return null;
75872
+ const findings = [];
75873
+ for (const raw2 of obj.findings) {
75874
+ if (!raw2 || typeof raw2 !== "object")
75875
+ return null;
75876
+ const f9 = raw2;
75877
+ if (typeof f9.severity !== "string" || !VALID_SEVERITIES.has(f9.severity))
75878
+ return null;
75879
+ if (typeof f9.description !== "string")
75880
+ return null;
75881
+ const file = optionalNullableString(f9.file);
75882
+ if (!file.ok)
75883
+ return null;
75884
+ const line = optionalNullableInteger(f9.line);
75885
+ if (!line.ok)
75886
+ return null;
75887
+ const endLine = optionalNullableInteger(f9.end_line);
75888
+ if (!endLine.ok)
75889
+ return null;
75890
+ findings.push({
75891
+ severity: f9.severity,
75892
+ file: file.value,
75893
+ line: line.value,
75894
+ end_line: endLine.value,
75895
+ description: f9.description,
75896
+ reasoning: typeof f9.reasoning === "string" ? f9.reasoning : ""
75897
+ });
75898
+ }
75899
+ return {
75900
+ findings,
75901
+ summary: {
75902
+ correctness: s10.correctness,
75903
+ explanation: s10.explanation,
75904
+ confidence: Math.max(0, Math.min(1, s10.confidence))
75905
+ }
75906
+ };
75907
+ }
75908
+ function cursorIsRealAssistantDelta(event) {
75909
+ return event.timestamp_ms !== undefined && event.model_call_id === undefined;
75910
+ }
75911
+ function cursorAssistantText(event) {
75912
+ if (typeof event.text === "string")
75913
+ return event.text;
75914
+ const message = event.message;
75915
+ const content = message?.content;
75916
+ if (typeof content === "string")
75917
+ return content;
75918
+ if (Array.isArray(content)) {
75919
+ return content.filter((p2) => !!p2 && typeof p2 === "object").filter((p2) => p2.type === "text" && typeof p2.text === "string").map((p2) => p2.text).join("");
75920
+ }
75921
+ return "";
75922
+ }
75923
+ function cursorExtractText(event) {
75924
+ if (event.type === "assistant") {
75925
+ if (event.timestamp_ms !== undefined) {
75926
+ if (cursorIsRealAssistantDelta(event))
75927
+ return cursorAssistantText(event);
75928
+ return null;
75929
+ }
75930
+ if (event.model_call_id === undefined)
75931
+ return cursorAssistantText(event);
75932
+ return null;
75933
+ }
75934
+ if (event.type === "result") {
75935
+ if (typeof event.result === "string")
75936
+ return event.result;
75937
+ if (typeof event.text === "string")
75938
+ return event.text;
75939
+ return null;
75940
+ }
75941
+ return null;
75942
+ }
75943
+ function cursorParseModels(stdout) {
75944
+ if (!stdout)
75945
+ return [];
75946
+ const models = [];
75947
+ const seen = new Set;
75948
+ for (const rawLine of stdout.split(`
75949
+ `)) {
75950
+ const line = rawLine.trim();
75951
+ const match = /^(\S+)\s+-\s+(.+)$/.exec(line);
75952
+ if (!match)
75953
+ continue;
75954
+ const id = match[1];
75955
+ const label = match[2].trim();
75956
+ if (!id || seen.has(id))
75957
+ continue;
75958
+ seen.add(id);
75959
+ models.push({ id, label });
75960
+ }
75961
+ return models;
75962
+ }
75963
+ function cursorFormatLogEvent(event) {
75964
+ switch (event.type) {
75965
+ case "system": {
75966
+ if (event.subtype === "init") {
75967
+ const model = typeof event.model === "string" ? event.model : undefined;
75968
+ const sessionId = typeof event.session_id === "string" ? event.session_id : undefined;
75969
+ const bits = ["[init]"];
75970
+ if (model)
75971
+ bits.push(`model=${model}`);
75972
+ if (sessionId)
75973
+ bits.push(`session=${sessionId}`);
75974
+ return bits.join(" ");
75975
+ }
75976
+ return null;
75977
+ }
75978
+ case "assistant": {
75979
+ if (!cursorIsRealAssistantDelta(event))
75980
+ return null;
75981
+ const text2 = cursorAssistantText(event);
75982
+ return text2 ? text2 : null;
75983
+ }
75984
+ case "tool_call": {
75985
+ const name = typeof event.name === "string" ? event.name : "tool";
75986
+ if (event.subtype === "completed") {
75987
+ return `[${name}] completed`;
75988
+ }
75989
+ const args = typeof event.args === "string" ? event.args.slice(0, 100) : event.args !== undefined ? JSON.stringify(event.args).slice(0, 100) : "";
75990
+ return `[${name}] ${args}`.trimEnd();
75991
+ }
75992
+ case "result": {
75993
+ const duration = typeof event.duration_ms === "number" ? `${event.duration_ms}ms` : undefined;
75994
+ const requestId = typeof event.request_id === "string" ? event.request_id : undefined;
75995
+ const bits = ["[result]"];
75996
+ if (duration)
75997
+ bits.push(duration);
75998
+ if (requestId)
75999
+ bits.push(`request=${requestId}`);
76000
+ return bits.length > 1 ? bits.join(" ") : null;
76001
+ }
76002
+ default:
76003
+ return null;
76004
+ }
76005
+ }
76006
+ function cursorBuildArgv(prompt, model, cwd) {
76007
+ const useModel = !!model && model.toLowerCase() !== "auto";
76008
+ return [
76009
+ "agent",
76010
+ "-p",
76011
+ "--mode",
76012
+ "ask",
76013
+ "--output-format",
76014
+ "stream-json",
76015
+ "--stream-partial-output",
76016
+ "--trust",
76017
+ ...cwd ? ["--workspace", cwd] : [],
76018
+ "--sandbox",
76019
+ "enabled",
76020
+ ...useModel ? ["--model", model] : [],
76021
+ prompt
76022
+ ];
76023
+ }
76024
+ function opencodeExtractText(event) {
76025
+ if (event.type === "text") {
76026
+ const part = event.part;
76027
+ if (typeof part?.text === "string")
76028
+ return part.text;
76029
+ }
76030
+ return null;
76031
+ }
76032
+ function opencodeParseModels(stdout) {
76033
+ if (!stdout)
76034
+ return [];
76035
+ const models = [];
76036
+ const seen = new Set;
76037
+ for (const rawLine of stdout.split(`
76038
+ `)) {
76039
+ const id = rawLine.trim();
76040
+ if (!id || /\s/.test(id) || !/^[^/\s]+\/\S+$/.test(id))
76041
+ continue;
76042
+ if (seen.has(id))
76043
+ continue;
76044
+ seen.add(id);
76045
+ models.push({ id, label: id });
76046
+ }
76047
+ return models;
76048
+ }
76049
+ function opencodeFormatLogEvent(event) {
76050
+ const part = event.part;
76051
+ switch (event.type) {
76052
+ case "text": {
76053
+ const text2 = typeof part?.text === "string" ? part.text.trim() : "";
76054
+ return text2 ? text2 : null;
76055
+ }
76056
+ case "tool_use": {
76057
+ const tool = typeof part?.tool === "string" ? part.tool : "tool";
76058
+ const status = typeof part?.state?.status === "string" ? part.state.status : "";
76059
+ return `[${tool}] ${status}`.trimEnd();
76060
+ }
76061
+ case "error": {
76062
+ return "[error] session error";
76063
+ }
76064
+ default:
76065
+ return null;
76066
+ }
76067
+ }
76068
+ function opencodeBuildArgv(prompt, model, cwd) {
76069
+ const useModel = !!model && model.trim().length > 0;
76070
+ return [
76071
+ "opencode",
76072
+ "run",
76073
+ "--format",
76074
+ "json",
76075
+ "--agent",
76076
+ "plan",
76077
+ ...useModel ? ["--model", model] : [],
76078
+ ...cwd ? ["--dir", cwd] : [],
76079
+ prompt
76080
+ ];
76081
+ }
76082
+ function reduceMarkerStream(stdout, engine) {
76083
+ let canonicalText = "";
76084
+ let recordCount = 0;
76085
+ if (!stdout)
76086
+ return { canonicalText, recordCount };
76087
+ for (const rawLine of stdout.split(`
76088
+ `)) {
76089
+ const line = rawLine.trim();
76090
+ if (!line)
76091
+ continue;
76092
+ let event;
76093
+ try {
76094
+ event = JSON.parse(line);
76095
+ } catch {
76096
+ continue;
76097
+ }
76098
+ recordCount++;
76099
+ const text2 = engine.extractText(event);
76100
+ if (text2)
76101
+ canonicalText += text2;
76102
+ }
76103
+ return { canonicalText, recordCount };
76104
+ }
76105
+ function extractLastMarkerBlock(text2, openTag, closeTag) {
76106
+ let result = null;
76107
+ let searchFrom = 0;
76108
+ while (true) {
76109
+ const open2 = text2.indexOf(openTag, searchFrom);
76110
+ if (open2 === -1)
76111
+ break;
76112
+ const contentStart = open2 + openTag.length;
76113
+ const close = text2.indexOf(closeTag, contentStart);
76114
+ if (close === -1)
76115
+ return null;
76116
+ result = text2.slice(contentStart, close);
76117
+ searchFrom = close + closeTag.length;
76118
+ }
76119
+ return result;
76120
+ }
76121
+ function parseMarkerStreamOutput(stdout, engine, nonce) {
76122
+ if (!stdout || !stdout.trim())
76123
+ return null;
76124
+ if (!nonce)
76125
+ return null;
76126
+ const { canonicalText } = reduceMarkerStream(stdout, engine);
76127
+ if (!canonicalText)
76128
+ return null;
76129
+ const block = extractLastMarkerBlock(canonicalText, markerOpen(nonce), markerClose(nonce));
76130
+ if (block === null)
76131
+ return null;
76132
+ let parsed;
76133
+ try {
76134
+ parsed = JSON.parse(block.trim());
76135
+ } catch {
76136
+ return null;
76137
+ }
76138
+ return validateMarkerReviewOutput(parsed);
76139
+ }
76140
+ function formatMarkerLogEvent(line, engine) {
76141
+ let event;
76142
+ try {
76143
+ event = JSON.parse(line);
76144
+ } catch {
76145
+ return null;
76146
+ }
76147
+ return engine.formatLogEvent(event);
76148
+ }
76149
+ function buildMarkerOutputContract(nonce) {
76150
+ return `## Output contract
76151
+ Your only machine-readable output is a single marker-delimited JSON block.
76152
+ Any natural-language commentary you write must come BEFORE the final marker
76153
+ block. Emit the block exactly once, as the last thing in your response. The
76154
+ opening and closing tags carry a session id (after the colon) \u2014 reproduce both
76155
+ tags EXACTLY as shown, including that id, or your findings will be discarded:
75697
76156
 
75698
- `, AGENT_HEARTBEAT_INTERVAL_MS = 30000;
76157
+ ${markerOpen(nonce)}
76158
+ {
76159
+ "findings": [
76160
+ {
76161
+ "file": "packages/server/review.ts",
76162
+ "line": 123,
76163
+ "end_line": 123,
76164
+ "severity": "important",
76165
+ "description": "The issue...",
76166
+ "reasoning": "Why this is a real issue..."
76167
+ }
76168
+ ],
76169
+ "summary": {
76170
+ "correctness": "Issues Found",
76171
+ "explanation": "One important issue was found.",
76172
+ "confidence": 0.85
76173
+ }
76174
+ }
76175
+ ${markerClose(nonce)}
76176
+
76177
+ Schema:
76178
+ - findings: array of objects, each with
76179
+ - file: string or null \u2014 path as shown in the diff. Use null for a general,
76180
+ review-level note that isn't about any one file.
76181
+ - line: integer or null \u2014 start line, post-change numbering. Give a line for a
76182
+ line-level issue; use null for a whole-file issue or a general note.
76183
+ - end_line: integer or null \u2014 end line; equal to line for a single line, null
76184
+ when line is null.
76185
+ - severity: one of "important", "nit", "pre_existing"
76186
+ - description: string (one paragraph) \u2014 state the IMPACT (what breaks, for
76187
+ whom) and the TRIGGER (when it happens); suggest a minimal fix if obvious
76188
+ - reasoning: string (how the issue was confirmed)
76189
+ - summary: object with
76190
+ - correctness: string ("Correct" or "Issues Found")
76191
+ - explanation: string (one sentence)
76192
+ - confidence: number between 0 and 1
76193
+
76194
+ Place each finding by how specific it is: give file AND line for a line-level
76195
+ issue; give file with line null for a whole-file issue; set both file and line
76196
+ null for a general, review-level note. Never invent a line you are unsure of \u2014
76197
+ drop to a file or general placement instead of guessing. Cite file/line from the
76198
+ new (post-change) code. One finding per distinct bug \u2014 do not stack unrelated
76199
+ issues. If no issues are found, return an empty "findings" array with a valid
76200
+ summary.`;
76201
+ }
76202
+ function composeMarkerReviewPrompt(profile, userMessage, nonce) {
76203
+ const head2 = profileHasCustomSection(profile) ? profile.instructions.trim() : MARKER_REVIEW_METHODOLOGY;
76204
+ return head2 + `
76205
+
76206
+ ` + buildMarkerOutputContract(nonce) + `
76207
+
76208
+ ---
76209
+
76210
+ ` + userMessage;
76211
+ }
76212
+ function buildMarkerCommand(engine, prompt, model, cwd) {
76213
+ return { command: engine.buildArgv(prompt, model, cwd) };
76214
+ }
76215
+ function transformMarkerFindings(findings, source, author, cwd, pathTransform) {
76216
+ return transformSeverityFindings(findings, source, author, cwd, pathTransform);
76217
+ }
76218
+ var MARKER_TAG = "plannotator-review-json", VALID_SEVERITIES, CURSOR_ENGINE, OPENCODE_ENGINE, MARKER_ENGINES, MARKER_REVIEW_METHODOLOGY = `# Code Review
76219
+
76220
+ ## Your role
76221
+ You are a senior engineer reviewing a code change. Find the bugs a maintainer
76222
+ would fix before merging \u2014 logic errors, regressions, broken edge cases,
76223
+ security holes with a real exploit path, data-loss risks. You are not a linter
76224
+ or style checker. Optimize for findings that get resolved, not for volume: a
76225
+ few real, well-evidenced bugs beat a long list of nits.
76226
+
76227
+ ## Method \u2014 investigate before you report
76228
+ You have tools. Use them. Do not judge the diff in isolation:
76229
+ - Read the full function and module around each change, not just the hunk.
76230
+ - Trace call sites and data flow to see how the changed code is actually used.
76231
+ - Read any repo guidance and honor it if present: AGENTS.md, CLAUDE.md,
76232
+ REVIEW.md, and any rules files at the repo root and in the directories
76233
+ you're reviewing. Treat these as authoritative for this repo and respect any
76234
+ skip/ignore rules they define.
76235
+ - Before reporting, check whether the issue is already handled elsewhere (a
76236
+ guard, try/catch, fallback, type guarantee).
76237
+ - Look at sibling code and tests: is the pattern you're flagging used safely
76238
+ elsewhere? Is the failure path you fear already covered?
76239
+
76240
+ ## What to look for
76241
+ - Correctness: logic errors, regressions, broken edge cases, off-by-one,
76242
+ wrong results, build/type breakage.
76243
+ - Robustness: unhandled errors on realistic paths, resource/process leaks,
76244
+ races with observable consequences, state left inconsistent on failure.
76245
+ - Security (only when the change introduces it): execution of
76246
+ user/client-controlled input, trusting unvalidated external or model
76247
+ output, weakened trust/read-only guarantees, newly exposed surfaces.
76248
+ - Contracts & attribution: wrong file/line/path mapping, scope confusion, or a
76249
+ change to one side of a contract (types, API shape, parallel
76250
+ implementations) without the matching change on the other side.
76251
+ - Clear, citable violations of the repo guidance files above.
76252
+
76253
+ ## Validate every finding
76254
+ Confirm each candidate is real by tracing the actual code path. If you can't
76255
+ show how it triggers, drop it \u2014 prefer silence over a false positive. The
76256
+ confirmation you write becomes the \`reasoning\` field: what triggers it, what
76257
+ breaks, and why it isn't already handled.
76258
+
76259
+ ## Severity \u2014 report what a maintainer would actually fix
76260
+ important \u2014 should be fixed before merge: data loss, silent wrong results,
76261
+ security with an exploit path, crashes/leaks, contract drift that breaks a
76262
+ real consumer, fail-open where it must fail-closed.
76263
+
76264
+ nit \u2014 minor and non-blocking: a real but low-impact edge case, missing
76265
+ handling on an unlikely path, a robustness gap with an easy workaround.
76266
+
76267
+ pre_existing \u2014 a genuine bug in surrounding code NOT introduced by this
76268
+ change. Only flag when it directly affects the changed code path.
76269
+
76270
+ ## Do NOT report
76271
+ - Formatting, naming, or style preferences (unless a repo guideline requires).
76272
+ - "Consider adding tests" \u2014 unless the change adds non-trivial logic with no
76273
+ test coverage at all.
76274
+ - Hypothetical problems needing exotic conditions with no real code path.
76275
+ - Speculative refactors or "this could be cleaner" outside the change's scope.
76276
+
76277
+ ## Hard constraints
76278
+ - This is a read-only review. Do NOT modify files.
76279
+ - Do NOT post any comments to GitHub or GitLab. Do NOT use gh pr comment, glab,
76280
+ or any commenting tool.
76281
+ - Never approve or block the change. Your only output is findings.`;
76282
+ var init_marker_review = __esm(() => {
76283
+ init_review_profiles();
76284
+ init_review_findings();
76285
+ VALID_SEVERITIES = new Set([
76286
+ "important",
76287
+ "nit",
76288
+ "pre_existing"
76289
+ ]);
76290
+ CURSOR_ENGINE = {
76291
+ id: "cursor",
76292
+ name: "Cursor CLI",
76293
+ binary: "agent",
76294
+ author: "Cursor",
76295
+ buildArgv: cursorBuildArgv,
76296
+ extractText: cursorExtractText,
76297
+ modelsArgv: ["models"],
76298
+ parseModels: cursorParseModels,
76299
+ formatLogEvent: cursorFormatLogEvent
76300
+ };
76301
+ OPENCODE_ENGINE = {
76302
+ id: "opencode",
76303
+ name: "OpenCode",
76304
+ binary: "opencode",
76305
+ author: "OpenCode",
76306
+ buildArgv: opencodeBuildArgv,
76307
+ extractText: opencodeExtractText,
76308
+ modelsArgv: ["models"],
76309
+ parseModels: opencodeParseModels,
76310
+ formatLogEvent: opencodeFormatLogEvent
76311
+ };
76312
+ MARKER_ENGINES = {
76313
+ cursor: CURSOR_ENGINE,
76314
+ opencode: OPENCODE_ENGINE
76315
+ };
76316
+ });
75699
76317
 
75700
76318
  // ../../packages/server/agent-jobs.ts
76319
+ async function discoverMarkerModels(engine) {
76320
+ try {
76321
+ const proc = Bun.spawn([engine.binary, ...engine.modelsArgv], {
76322
+ stdout: "pipe",
76323
+ stderr: "ignore",
76324
+ timeout: 5000
76325
+ });
76326
+ const stdout = await new Response(proc.stdout).text();
76327
+ const exitCode = await proc.exited;
76328
+ if (exitCode !== 0)
76329
+ return [];
76330
+ return engine.parseModels(stdout);
76331
+ } catch {
76332
+ return [];
76333
+ }
76334
+ }
75701
76335
  function createAgentJobHandler(options) {
75702
76336
  const { mode, getServerUrl, getCwd } = options;
75703
76337
  const jobs = new Map;
@@ -75710,11 +76344,28 @@ function createAgentJobHandler(options) {
75710
76344
  { id: "codex", name: "Codex CLI", available: !!Bun.which("codex") },
75711
76345
  { id: "tour", name: "Code Tour", available: !!Bun.which("claude") || !!Bun.which("codex") }
75712
76346
  ];
75713
- const capabilitiesResponse = {
75714
- mode,
75715
- providers: capabilities,
75716
- available: capabilities.some((c2) => c2.available)
75717
- };
76347
+ for (const engine of Object.values(MARKER_ENGINES)) {
76348
+ capabilities.push({
76349
+ id: engine.id,
76350
+ name: engine.name,
76351
+ available: mode === "review" && !!Bun.which(engine.binary)
76352
+ });
76353
+ }
76354
+ const markerModelsCache = new Map;
76355
+ async function buildCapabilitiesResponse() {
76356
+ const providers = await Promise.all(capabilities.map(async (c2) => {
76357
+ const engine = MARKER_ENGINES[c2.id];
76358
+ if (!engine || !c2.available)
76359
+ return c2;
76360
+ let models = markerModelsCache.get(engine.id);
76361
+ if (!models) {
76362
+ models = await discoverMarkerModels(engine);
76363
+ markerModelsCache.set(engine.id, models);
76364
+ }
76365
+ return { ...c2, models };
76366
+ }));
76367
+ return { mode, providers, available: providers.some((p2) => p2.available) };
76368
+ }
75718
76369
  function broadcast2(event) {
75719
76370
  version++;
75720
76371
  const data = encoder2.encode(serializeAgentSSEEvent(event));
@@ -75726,8 +76377,7 @@ function createAgentJobHandler(options) {
75726
76377
  }
75727
76378
  }
75728
76379
  }
75729
- function spawnJob(provider, command, label, outputPath, spawnOptions) {
75730
- const id = crypto.randomUUID();
76380
+ function spawnJob(id, provider, command, label, outputPath, spawnOptions) {
75731
76381
  const source = jobSource(id);
75732
76382
  const info = {
75733
76383
  id,
@@ -75745,7 +76395,9 @@ function createAgentJobHandler(options) {
75745
76395
  ...spawnOptions?.fastMode && { fastMode: spawnOptions.fastMode },
75746
76396
  ...spawnOptions?.prUrl && { prUrl: spawnOptions.prUrl },
75747
76397
  ...spawnOptions?.diffScope && { diffScope: spawnOptions.diffScope },
75748
- ...spawnOptions?.diffContext && { diffContext: spawnOptions.diffContext }
76398
+ ...spawnOptions?.diffContext && { diffContext: spawnOptions.diffContext },
76399
+ ...spawnOptions?.reviewProfileId && { reviewProfileId: spawnOptions.reviewProfileId },
76400
+ ...spawnOptions?.reviewProfileLabel && { reviewProfileLabel: spawnOptions.reviewProfileLabel }
75749
76401
  };
75750
76402
  let proc = null;
75751
76403
  try {
@@ -75812,33 +76464,46 @@ function createAgentJobHandler(options) {
75812
76464
  }
75813
76465
  let stdoutBuf = "";
75814
76466
  const stdoutDone = captureStdout && proc.stdout && typeof proc.stdout !== "number" ? (async () => {
76467
+ const emitLogLine = (line) => {
76468
+ if (!line.trim())
76469
+ return;
76470
+ if (provider === "claude" || spawnOptions?.engine === "claude") {
76471
+ const formatted = formatClaudeLogEvent(line);
76472
+ if (formatted !== null)
76473
+ broadcast2({ type: "job:log", jobId: id, delta: formatted + `
76474
+ ` });
76475
+ return;
76476
+ }
76477
+ const markerEngine = MARKER_ENGINES[provider];
76478
+ if (markerEngine) {
76479
+ const formatted = formatMarkerLogEvent(line, markerEngine);
76480
+ if (formatted !== null)
76481
+ broadcast2({ type: "job:log", jobId: id, delta: formatted + `
76482
+ ` });
76483
+ return;
76484
+ }
76485
+ try {
76486
+ const event = JSON.parse(line);
76487
+ if (event.type === "result")
76488
+ return;
76489
+ } catch {}
76490
+ broadcast2({ type: "job:log", jobId: id, delta: line + `
76491
+ ` });
76492
+ };
75815
76493
  try {
75816
76494
  const reader = proc.stdout;
76495
+ let logLineCarry = "";
75817
76496
  for await (const chunk of reader) {
75818
76497
  const text2 = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
75819
76498
  stdoutBuf += text2;
75820
- const lines = text2.split(`
76499
+ const lines = (logLineCarry + text2).split(`
75821
76500
  `);
75822
- for (const line of lines) {
75823
- if (!line.trim())
75824
- continue;
75825
- if (provider === "claude" || spawnOptions?.engine === "claude") {
75826
- const formatted = formatClaudeLogEvent(line);
75827
- if (formatted !== null) {
75828
- broadcast2({ type: "job:log", jobId: id, delta: formatted + `
75829
- ` });
75830
- }
75831
- continue;
75832
- }
75833
- try {
75834
- const event = JSON.parse(line);
75835
- if (event.type === "result")
75836
- continue;
75837
- } catch {}
75838
- broadcast2({ type: "job:log", jobId: id, delta: line + `
75839
- ` });
75840
- }
76501
+ logLineCarry = lines.pop() ?? "";
76502
+ for (const line of lines)
76503
+ emitLogLine(line);
75841
76504
  }
76505
+ if (logLineCarry)
76506
+ emitLogLine(logLineCarry);
75842
76507
  } catch {}
75843
76508
  })() : Promise.resolve();
75844
76509
  proc.exited.then(async (exitCode) => {
@@ -75861,7 +76526,12 @@ function createAgentJobHandler(options) {
75861
76526
  stdout: captureStdout ? stdoutBuf : undefined,
75862
76527
  cwd: jobCwd
75863
76528
  });
75864
- } catch {}
76529
+ } catch (err) {
76530
+ if (MARKER_ENGINES[provider]) {
76531
+ entry.info.status = "failed";
76532
+ entry.info.error = err instanceof Error ? err.message : `${provider} result ingestion failed`;
76533
+ }
76534
+ }
75865
76535
  }
75866
76536
  jobOutputPaths.delete(id);
75867
76537
  jobOutputPaths.delete(`${id}:cwd`);
@@ -75910,7 +76580,7 @@ function createAgentJobHandler(options) {
75910
76580
  killAll,
75911
76581
  async handle(req, url, handlerOptions) {
75912
76582
  if (url.pathname === CAPABILITIES && req.method === "GET") {
75913
- return Response.json(capabilitiesResponse);
76583
+ return Response.json(await buildCapabilitiesResponse());
75914
76584
  }
75915
76585
  if (url.pathname === JOBS_STREAM && req.method === "GET") {
75916
76586
  handlerOptions?.disableIdleTimeout?.();
@@ -75962,6 +76632,23 @@ function createAgentJobHandler(options) {
75962
76632
  if (url.pathname === JOBS && req.method === "POST") {
75963
76633
  try {
75964
76634
  const body3 = await req.json();
76635
+ const KNOWN_JOB_FIELDS = new Set([
76636
+ "provider",
76637
+ "command",
76638
+ "label",
76639
+ "engine",
76640
+ "model",
76641
+ "reasoningEffort",
76642
+ "effort",
76643
+ "fastMode",
76644
+ "reviewProfileId"
76645
+ ]);
76646
+ if (body3 && typeof body3 === "object") {
76647
+ const unknown2 = Object.keys(body3).filter((k4) => !KNOWN_JOB_FIELDS.has(k4));
76648
+ if (unknown2.length > 0) {
76649
+ return Response.json({ error: `Unknown field(s): ${unknown2.join(", ")}` }, { status: 400 });
76650
+ }
76651
+ }
75965
76652
  const provider = typeof body3.provider === "string" ? body3.provider : "";
75966
76653
  let rawCommand = Array.isArray(body3.command) ? body3.command : [];
75967
76654
  let command = rawCommand.filter((c2) => typeof c2 === "string");
@@ -75971,6 +76658,13 @@ function createAgentJobHandler(options) {
75971
76658
  if (!cap2 || !cap2.available) {
75972
76659
  return Response.json({ error: `Unknown or unavailable provider: ${provider}` }, { status: 400 });
75973
76660
  }
76661
+ const isServerBuiltProvider = SERVER_BUILT_PROVIDERS.has(provider);
76662
+ if (isServerBuiltProvider) {
76663
+ if (!options.buildCommand) {
76664
+ return Response.json({ error: `Provider ${provider} requires server-built command` }, { status: 400 });
76665
+ }
76666
+ command = [];
76667
+ }
75974
76668
  let captureStdout = false;
75975
76669
  let stdinPrompt;
75976
76670
  let spawnCwd;
@@ -75983,6 +76677,9 @@ function createAgentJobHandler(options) {
75983
76677
  let jobPrUrl;
75984
76678
  let jobDiffScope;
75985
76679
  let jobDiffContext;
76680
+ let jobReviewProfileId;
76681
+ let jobReviewProfileLabel;
76682
+ const jobId = crypto.randomUUID();
75986
76683
  if (options.buildCommand) {
75987
76684
  const config = {};
75988
76685
  if (typeof body3.engine === "string")
@@ -75995,6 +76692,8 @@ function createAgentJobHandler(options) {
75995
76692
  config.effort = body3.effort;
75996
76693
  if (body3.fastMode === true)
75997
76694
  config.fastMode = true;
76695
+ if (typeof body3.reviewProfileId === "string")
76696
+ config.reviewProfileId = body3.reviewProfileId;
75998
76697
  const built = await options.buildCommand(provider, Object.keys(config).length > 0 ? config : undefined);
75999
76698
  if (built) {
76000
76699
  command = built.command;
@@ -76013,12 +76712,14 @@ function createAgentJobHandler(options) {
76013
76712
  jobPrUrl = built.prUrl;
76014
76713
  jobDiffScope = built.diffScope;
76015
76714
  jobDiffContext = built.diffContext;
76715
+ jobReviewProfileId = built.reviewProfileId;
76716
+ jobReviewProfileLabel = built.reviewProfileLabel;
76016
76717
  }
76017
76718
  }
76018
76719
  if (command.length === 0) {
76019
76720
  return Response.json({ error: 'Missing "command" array' }, { status: 400 });
76020
76721
  }
76021
- const job = spawnJob(provider, command, label, outputPath, {
76722
+ const job = spawnJob(jobId, provider, command, label, outputPath, {
76022
76723
  captureStdout,
76023
76724
  stdinPrompt,
76024
76725
  cwd: spawnCwd,
@@ -76030,7 +76731,9 @@ function createAgentJobHandler(options) {
76030
76731
  fastMode: jobFastMode,
76031
76732
  prUrl: jobPrUrl,
76032
76733
  diffScope: jobDiffScope,
76033
- diffContext: jobDiffContext
76734
+ diffContext: jobDiffContext,
76735
+ reviewProfileId: jobReviewProfileId,
76736
+ reviewProfileLabel: jobReviewProfileLabel
76034
76737
  });
76035
76738
  return Response.json({ job }, { status: 201 });
76036
76739
  } catch (err) {
@@ -76060,12 +76763,20 @@ function createAgentJobHandler(options) {
76060
76763
  }
76061
76764
  };
76062
76765
  }
76063
- var BASE2 = "/api/agents", JOBS, JOBS_STREAM, CAPABILITIES;
76766
+ var BASE2 = "/api/agents", JOBS, JOBS_STREAM, CAPABILITIES, SERVER_BUILT_PROVIDERS;
76064
76767
  var init_agent_jobs = __esm(() => {
76065
76768
  init_claude_review();
76769
+ init_marker_review();
76066
76770
  JOBS = `${BASE2}/jobs`;
76067
76771
  JOBS_STREAM = `${JOBS}/stream`;
76068
76772
  CAPABILITIES = `${BASE2}/capabilities`;
76773
+ SERVER_BUILT_PROVIDERS = new Set([
76774
+ "claude",
76775
+ "codex",
76776
+ "tour",
76777
+ "cursor",
76778
+ "opencode"
76779
+ ]);
76069
76780
  });
76070
76781
 
76071
76782
  // ../../packages/server/codex-review.ts
@@ -76093,6 +76804,9 @@ async function ensureSchemaFile() {
76093
76804
  }
76094
76805
  return SCHEMA_FILE;
76095
76806
  }
76807
+ function composeCodexReviewPrompt(userMessage, reviewProfile) {
76808
+ return composeReviewPrompt(CODEX_REVIEW_SYSTEM_PROMPT, reviewProfile, userMessage);
76809
+ }
76096
76810
  async function buildCodexCommand(options) {
76097
76811
  const { cwd, outputPath, prompt, model, reasoningEffort, fastMode } = options;
76098
76812
  const schemaPath = await ensureSchemaFile();
@@ -76159,19 +76873,25 @@ async function parseCodexOutput(outputPath) {
76159
76873
  }
76160
76874
  }
76161
76875
  function transformReviewFindings(findings, source, cwd, author, pathTransform) {
76162
- const annotations = findings.filter((f9) => f9.code_location?.absolute_file_path && typeof f9.code_location?.line_range?.start === "number" && typeof f9.code_location?.line_range?.end === "number").map((f9) => ({
76163
- source,
76164
- filePath: pathTransform ? pathTransform(toRelativePath2(f9.code_location.absolute_file_path, cwd)) : toRelativePath2(f9.code_location.absolute_file_path, cwd),
76165
- lineStart: f9.code_location.line_range.start,
76166
- lineEnd: f9.code_location.line_range.end,
76167
- type: "comment",
76168
- side: "new",
76169
- scope: "line",
76170
- text: `${f9.title}
76876
+ const annotations = findings.map((f9) => {
76877
+ const loc = f9.code_location;
76878
+ const rawFile = loc && typeof loc.absolute_file_path === "string" ? loc.absolute_file_path : "";
76879
+ const filePath = rawFile ? pathTransform ? pathTransform(toRelativePath2(rawFile, cwd)) : toRelativePath2(rawFile, cwd) : "";
76880
+ const placement = classifyFindingPlacement(filePath, loc?.line_range?.start, loc?.line_range?.end);
76881
+ return {
76882
+ source,
76883
+ filePath: placement.filePath,
76884
+ lineStart: placement.lineStart,
76885
+ lineEnd: placement.lineEnd,
76886
+ type: "comment",
76887
+ side: "new",
76888
+ scope: placement.scope,
76889
+ text: `${f9.title}
76171
76890
 
76172
76891
  ${f9.body}`.trim(),
76173
- author: author ?? "Review Agent"
76174
- }));
76892
+ author: author ?? "Review Agent"
76893
+ };
76894
+ });
76175
76895
  debugLog("TRANSFORM_FINDINGS", {
76176
76896
  inputCount: findings.length,
76177
76897
  outputCount: annotations.length,
@@ -76235,6 +76955,8 @@ At the beginning of the finding title, tag the bug with priority level. For exam
76235
76955
 
76236
76956
  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.
76237
76957
 
76958
+ Place each finding by how specific it is. For a line-level issue, set code_location with the file and a line_range. For a whole-file issue, set code_location with the file path and line_range null. For a general, review-wide point, set code_location null. Do not invent a line range you are unsure of \u2014 drop to a whole-file or general placement instead.
76959
+
76238
76960
  At the end of your findings, output an "overall correctness" verdict of whether or not the patch should be considered "correct".
76239
76961
  Correct implies that existing code and tests will not break, and the patch is free of bugs and other blocking issues.
76240
76962
  Ignore non-blocking issues such as style, formatting, typos, documentation, and other nits.
@@ -76244,6 +76966,8 @@ The finding description should be one paragraph.`;
76244
76966
  var init_codex_review = __esm(() => {
76245
76967
  init_path_utils();
76246
76968
  init_data_dir();
76969
+ init_review_profiles();
76970
+ init_external_annotation();
76247
76971
  DATA_DIR3 = getPlannotatorDataDir();
76248
76972
  DEBUG_ENABLED = !!process.env.PLANNOTATOR_DEBUG;
76249
76973
  DEBUG_LOG_PATH = join16(DATA_DIR3, "codex-review-debug.log");
@@ -76260,11 +76984,11 @@ var init_codex_review = __esm(() => {
76260
76984
  confidence_score: { type: "number" },
76261
76985
  priority: { type: ["integer", "null"] },
76262
76986
  code_location: {
76263
- type: "object",
76987
+ type: ["object", "null"],
76264
76988
  properties: {
76265
76989
  absolute_file_path: { type: "string" },
76266
76990
  line_range: {
76267
- type: "object",
76991
+ type: ["object", "null"],
76268
76992
  properties: {
76269
76993
  start: { type: "integer" },
76270
76994
  end: { type: "integer" }
@@ -76318,20 +77042,20 @@ function buildWorkspacePromptContextLines(workspace, options = {}) {
76318
77042
  repoList
76319
77043
  ];
76320
77044
  }
76321
- function buildAgentReviewUserMessageForTarget(target) {
77045
+ function buildAgentReviewUserMessageForTarget(target, contextOnly = false) {
76322
77046
  if (target.kind === "workspace") {
76323
- return buildWorkspaceReviewUserMessage(target.patch, target.workspace);
77047
+ return buildWorkspaceReviewUserMessage(target.patch, target.workspace, contextOnly);
76324
77048
  }
76325
- return buildAgentReviewUserMessage(target.patch, target.diffType, target.options, target.kind === "pr" ? target.prMetadata : undefined);
77049
+ return buildAgentReviewUserMessage(target.patch, target.diffType, target.options, target.kind === "pr" ? target.prMetadata : undefined, contextOnly);
76326
77050
  }
76327
- function buildAgentReviewUserMessage(patch, diffType, options, prMetadata) {
77051
+ function buildAgentReviewUserMessage(patch, diffType, options, prMetadata, contextOnly = false) {
76328
77052
  if (prMetadata) {
76329
77053
  if (options?.prDiffScope === "full-stack") {
76330
77054
  return [
76331
- `Full-stack review of ${prMetadata.url}`,
77055
+ contextOnly ? prMetadata.url : `Full-stack review of ${prMetadata.url}`,
76332
77056
  "",
76333
77057
  "This is a stacked PR. The diff below shows ALL accumulated changes from the repository default branch through this PR's head (not just this PR's own layer).",
76334
- "Review the complete diff for issues that span the stack.",
77058
+ ...contextOnly ? [] : ["Review the complete diff for issues that span the stack."],
76335
77059
  "",
76336
77060
  "```diff",
76337
77061
  patch,
@@ -76353,10 +77077,14 @@ function buildAgentReviewUserMessage(patch, diffType, options, prMetadata) {
76353
77077
  }
76354
77078
  const instruction = getLocalDiffInstruction(diffType, options?.defaultBranch);
76355
77079
  if (instruction) {
77080
+ if (contextOnly) {
77081
+ return `Changeset: ${instruction.target}.
77082
+ ${instruction.inspect}`;
77083
+ }
76356
77084
  return `Review ${instruction.target}. ${instruction.inspect} Provide prioritized, actionable findings.`;
76357
77085
  }
76358
77086
  return [
76359
- "Review the following code changes and provide prioritized findings.",
77087
+ contextOnly ? "Code changes:" : "Review the following code changes and provide prioritized findings.",
76360
77088
  "",
76361
77089
  "```diff",
76362
77090
  patch,
@@ -76364,10 +77092,9 @@ function buildAgentReviewUserMessage(patch, diffType, options, prMetadata) {
76364
77092
  ].join(`
76365
77093
  `);
76366
77094
  }
76367
- function buildWorkspaceReviewUserMessage(patch, workspace) {
77095
+ function buildWorkspaceReviewUserMessage(patch, workspace, contextOnly = false) {
76368
77096
  return [
76369
- "Review the local workspace changes across multiple nested VCS repositories.",
76370
- "",
77097
+ ...contextOnly ? [] : ["Review the local workspace changes across multiple nested VCS repositories.", ""],
76371
77098
  ...buildWorkspacePromptContextLines(workspace, { includeReportingInstruction: true }),
76372
77099
  "",
76373
77100
  "```diff",
@@ -86852,9 +87579,231 @@ var init_code_nav2 = __esm(() => {
86852
87579
  };
86853
87580
  });
86854
87581
 
87582
+ // ../../packages/server/review-skill-loader.ts
87583
+ import {
87584
+ existsSync as existsSync16,
87585
+ mkdirSync as mkdirSync9,
87586
+ readdirSync as readdirSync4,
87587
+ readFileSync as readFileSync9,
87588
+ realpathSync as realpathSync5,
87589
+ statSync as statSync9,
87590
+ writeFileSync as writeFileSync6
87591
+ } from "fs";
87592
+ import { homedir as homedir3 } from "os";
87593
+ import { join as join19 } from "path";
87594
+ function resolveGlobalSkillRoots() {
87595
+ const home = process.env.HOME?.trim() || homedir3();
87596
+ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join19(home, ".claude");
87597
+ const codexHome = process.env.CODEX_HOME?.trim() || join19(home, ".codex");
87598
+ const configHome = process.env.XDG_CONFIG_HOME?.trim() || join19(home, ".config");
87599
+ const candidates = [
87600
+ { dir: join19(claudeHome, "skills"), root: "claude" },
87601
+ { dir: join19(codexHome, "skills"), root: "codex" },
87602
+ { dir: join19(home, ".agents", "skills"), root: "universal" },
87603
+ { dir: join19(configHome, "agents", "skills"), root: "universal" }
87604
+ ];
87605
+ const seen = new Set;
87606
+ const roots = [];
87607
+ for (const candidate of candidates) {
87608
+ let key2;
87609
+ try {
87610
+ key2 = realpathSync5(candidate.dir);
87611
+ } catch {
87612
+ key2 = candidate.dir;
87613
+ }
87614
+ if (seen.has(key2))
87615
+ continue;
87616
+ seen.add(key2);
87617
+ roots.push(candidate);
87618
+ }
87619
+ return roots;
87620
+ }
87621
+ function hasSkillMd(dir) {
87622
+ try {
87623
+ return statSync9(join19(dir, "SKILL.md")).isFile();
87624
+ } catch {
87625
+ return false;
87626
+ }
87627
+ }
87628
+ function listSubdirs(dir) {
87629
+ let entries;
87630
+ try {
87631
+ entries = readdirSync4(dir, { withFileTypes: true });
87632
+ } catch (err) {
87633
+ console.error(`[plannotator] Could not read skill root ${dir}: ${err instanceof Error ? err.message : String(err)}`);
87634
+ return [];
87635
+ }
87636
+ return entries.filter((e10) => e10.isDirectory() && !SKIP_DIRS.has(e10.name)).map((e10) => e10.name);
87637
+ }
87638
+ function discoverSkills() {
87639
+ const byName = new Map;
87640
+ const add = (dir, root3) => {
87641
+ const name = dir.replace(/^.*[\\/]/, "");
87642
+ if (byName.has(name))
87643
+ return;
87644
+ byName.set(name, {
87645
+ name,
87646
+ sourcePath: dir,
87647
+ skillMdPath: join19(dir, "SKILL.md"),
87648
+ root: root3
87649
+ });
87650
+ };
87651
+ for (const { dir: rootDir, root: root3 } of resolveGlobalSkillRoots()) {
87652
+ if (!existsSync16(rootDir))
87653
+ continue;
87654
+ for (const childName of listSubdirs(rootDir)) {
87655
+ const childDir = join19(rootDir, childName);
87656
+ if (hasSkillMd(childDir)) {
87657
+ add(childDir, root3);
87658
+ continue;
87659
+ }
87660
+ for (const grandName of listSubdirs(childDir)) {
87661
+ const grandDir = join19(childDir, grandName);
87662
+ if (hasSkillMd(grandDir))
87663
+ add(grandDir, root3);
87664
+ }
87665
+ }
87666
+ }
87667
+ return [...byName.values()];
87668
+ }
87669
+ function stripFrontmatter(raw2) {
87670
+ const text2 = raw2.replace(/^\uFEFF/, "");
87671
+ const match = text2.match(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/);
87672
+ if (!match)
87673
+ return text2;
87674
+ return text2.slice(match[0].length);
87675
+ }
87676
+ function skillHasExtraFiles(sourcePath) {
87677
+ let entries;
87678
+ try {
87679
+ entries = readdirSync4(sourcePath);
87680
+ } catch {
87681
+ return false;
87682
+ }
87683
+ return entries.some((name) => name !== "SKILL.md");
87684
+ }
87685
+ function skillFilesPointerLine(skillDir) {
87686
+ return `This review skill's files (references, scripts, assets) are at: ${skillDir}
87687
+ Resolve any relative paths in the instructions below (e.g. references/, scripts/, assets/) against that absolute directory \u2014 the working directory is the repository under review, not the skill directory.`;
87688
+ }
87689
+ function readCuratedSkillNames() {
87690
+ const path4 = join19(getPlannotatorDataDir(), "review-skills.json");
87691
+ if (!existsSync16(path4))
87692
+ return null;
87693
+ let parsed;
87694
+ try {
87695
+ parsed = JSON.parse(readFileSync9(path4, "utf-8"));
87696
+ } catch (err) {
87697
+ console.error(`[plannotator] Ignoring malformed review-skills.json: ${err instanceof Error ? err.message : String(err)}`);
87698
+ return null;
87699
+ }
87700
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
87701
+ console.error("[plannotator] Ignoring review-skills.json: not an object.");
87702
+ return null;
87703
+ }
87704
+ const { version, enabled } = parsed;
87705
+ if (version !== 1) {
87706
+ console.error("[plannotator] Ignoring review-skills.json: version must be 1.");
87707
+ return null;
87708
+ }
87709
+ if (!Array.isArray(enabled) || !enabled.every((n2) => typeof n2 === "string")) {
87710
+ console.error("[plannotator] Ignoring review-skills.json: `enabled` must be an array of strings.");
87711
+ return null;
87712
+ }
87713
+ return new Set(enabled);
87714
+ }
87715
+ function listAllSkills() {
87716
+ const enabled = readCuratedSkillNames() ?? new Set;
87717
+ return discoverSkills().map((s10) => ({
87718
+ name: s10.name,
87719
+ root: s10.root,
87720
+ sourcePath: s10.sourcePath,
87721
+ enabled: enabled.has(s10.name)
87722
+ }));
87723
+ }
87724
+ function enableReviewSkill(name) {
87725
+ const known = new Set(discoverSkills().map((s10) => s10.name));
87726
+ if (!known.has(name)) {
87727
+ throw new Error(`No skill named "${name}" found in any global skill root.`);
87728
+ }
87729
+ const current = readCuratedSkillNames() ?? new Set;
87730
+ current.add(name);
87731
+ const enabled = [...current];
87732
+ const dir = getPlannotatorDataDir();
87733
+ mkdirSync9(dir, { recursive: true });
87734
+ writeFileSync6(join19(dir, "review-skills.json"), JSON.stringify({ version: 1, enabled }, null, 2) + `
87735
+ `);
87736
+ return { enabled };
87737
+ }
87738
+ function resolveSkillProfile(skill) {
87739
+ let raw2;
87740
+ try {
87741
+ raw2 = readFileSync9(skill.skillMdPath, "utf-8");
87742
+ } catch (err) {
87743
+ console.error(`[plannotator] Skipping review skill ${skill.name}: could not read ${skill.skillMdPath}: ${err instanceof Error ? err.message : String(err)}`);
87744
+ return null;
87745
+ }
87746
+ const body3 = stripFrontmatter(raw2);
87747
+ if (!body3.trim()) {
87748
+ console.error(`[plannotator] Skipping review skill ${skill.name}: SKILL.md body is empty.`);
87749
+ return null;
87750
+ }
87751
+ if (body3.length > MAX_SKILL_BODY_LEN) {
87752
+ console.error(`[plannotator] Skipping review skill ${skill.name}: SKILL.md body exceeds ${MAX_SKILL_BODY_LEN} chars.`);
87753
+ return null;
87754
+ }
87755
+ const instructions = skillHasExtraFiles(skill.sourcePath) ? `${skillFilesPointerLine(skill.sourcePath)}
87756
+
87757
+ ${body3}` : body3;
87758
+ return {
87759
+ id: `skill:${skill.name}`,
87760
+ label: skill.name,
87761
+ instructions,
87762
+ source: "user",
87763
+ sourcePath: skill.sourcePath
87764
+ };
87765
+ }
87766
+ function discoverCuratedSkills() {
87767
+ const enabled = readCuratedSkillNames();
87768
+ if (!enabled || enabled.size === 0)
87769
+ return [];
87770
+ const discovered = discoverSkills();
87771
+ const byName = new Map(discovered.map((s10) => [s10.name, s10]));
87772
+ const curated = [];
87773
+ for (const name of enabled) {
87774
+ const skill = byName.get(name);
87775
+ if (skill) {
87776
+ curated.push(skill);
87777
+ } else {
87778
+ console.error(`[plannotator] Curated review skill "${name}" not found in any global skill root; skipping.`);
87779
+ }
87780
+ }
87781
+ return curated;
87782
+ }
87783
+ function resolveRequestedReviewProfile(requestedProfileId) {
87784
+ if (!requestedProfileId || requestedProfileId === BUILTIN_DEFAULT_PROFILE.id) {
87785
+ return BUILTIN_DEFAULT_PROFILE;
87786
+ }
87787
+ const skill = discoverCuratedSkills().find((s10) => `skill:${s10.name}` === requestedProfileId);
87788
+ if (!skill) {
87789
+ throw new Error(`Review "${requestedProfileId}" is not available \u2014 it may have been renamed or removed. Pick another review.`);
87790
+ }
87791
+ const resolved = resolveSkillProfile(skill);
87792
+ if (!resolved) {
87793
+ throw new Error(`Review "${skill.name}" could not be loaded \u2014 its SKILL.md is unreadable, empty, or too large. Fix the skill or pick another review.`);
87794
+ }
87795
+ return resolved;
87796
+ }
87797
+ var MAX_SKILL_BODY_LEN = 20000, SKIP_DIRS;
87798
+ var init_review_skill_loader = __esm(() => {
87799
+ init_data_dir();
87800
+ init_review_profiles();
87801
+ SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "__pycache__"]);
87802
+ });
87803
+
86855
87804
  // ../../packages/server/review.ts
86856
87805
  import { basename as basename6 } from "path";
86857
- import { existsSync as existsSync16 } from "fs";
87806
+ import { existsSync as existsSync17 } from "fs";
86858
87807
  async function startReviewServer(options) {
86859
87808
  const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady } = options;
86860
87809
  let prMetadata = options.prMetadata;
@@ -86893,7 +87842,7 @@ async function startReviewServer(options) {
86893
87842
  const detectedCompareTarget = () => gitContext?.defaultBranch || gitContext?.compareTarget?.fallback || "main";
86894
87843
  let currentBase = options.initialBase || detectedCompareTarget();
86895
87844
  let baseEverSwitched = false;
86896
- const agentCwdIfExists = () => options.agentCwd && existsSync16(options.agentCwd) ? options.agentCwd : undefined;
87845
+ const agentCwdIfExists = () => options.agentCwd && existsSync17(options.agentCwd) ? options.agentCwd : undefined;
86897
87846
  const resolvePRLocalCwd = (meta = prMetadata) => {
86898
87847
  const pool = options.worktreePool;
86899
87848
  if (pool && meta) {
@@ -87072,6 +88021,8 @@ async function startReviewServer(options) {
87072
88021
  const launchDiffType = currentDiffType;
87073
88022
  const launchBase = currentBase;
87074
88023
  const launchScope = currentPRDiffScope;
88024
+ const requestedProfileId = typeof config?.reviewProfileId === "string" ? config.reviewProfileId : undefined;
88025
+ const reviewProfile = resolveRequestedReviewProfile(requestedProfileId);
87075
88026
  let cwd;
87076
88027
  if (options.worktreePool && launchMetadata) {
87077
88028
  const checkout = await ensurePRLocalCwd(launchMetadata);
@@ -87107,37 +88058,38 @@ async function startReviewServer(options) {
87107
88058
  prMetadata: launchMetadata,
87108
88059
  config
87109
88060
  });
87110
- return built ? { ...built, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext } : built;
88061
+ return built ? { ...built, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext, reviewProfileId: reviewProfile.id, reviewProfileLabel: reviewProfile.label } : built;
87111
88062
  }
88063
+ const isCustomReview = reviewProfile.source === "user";
87112
88064
  const userMessage = workspacePrompt ? buildAgentReviewUserMessageForTarget({
87113
88065
  kind: "workspace",
87114
88066
  patch: launchPatch,
87115
88067
  workspace: workspacePrompt
87116
- }) : buildAgentReviewUserMessage(launchPatch, launchDiffType, userMessageOptions, launchMetadata);
88068
+ }, isCustomReview) : buildAgentReviewUserMessage(launchPatch, launchDiffType, userMessageOptions, launchMetadata, isCustomReview);
87117
88069
  const jobLabel = workspacePrompt ? "Workspace Review" : "Code Review";
87118
88070
  if (provider === "codex") {
87119
88071
  const model = typeof config?.model === "string" && config.model ? config.model : undefined;
87120
88072
  const reasoningEffort = typeof config?.reasoningEffort === "string" && config.reasoningEffort ? config.reasoningEffort : undefined;
87121
88073
  const fastMode = config?.fastMode === true;
87122
88074
  const outputPath = generateOutputPath();
87123
- const prompt = CODEX_REVIEW_SYSTEM_PROMPT + `
87124
-
87125
- ---
87126
-
87127
- ` + userMessage;
88075
+ const prompt = composeCodexReviewPrompt(userMessage, reviewProfile);
87128
88076
  const command = await buildCodexCommand({ cwd, outputPath, prompt, model, reasoningEffort, fastMode });
87129
- return { command, outputPath, prompt, cwd, label: jobLabel, model, reasoningEffort, fastMode: fastMode || undefined, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext };
88077
+ return { command, outputPath, prompt, cwd, label: jobLabel, model, reasoningEffort, fastMode: fastMode || undefined, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext, reviewProfileId: reviewProfile.id, reviewProfileLabel: reviewProfile.label };
87130
88078
  }
87131
88079
  if (provider === "claude") {
87132
88080
  const model = typeof config?.model === "string" && config.model ? config.model : undefined;
87133
88081
  const effort = typeof config?.effort === "string" && config.effort ? config.effort : undefined;
87134
- const prompt = CLAUDE_REVIEW_PROMPT + `
87135
-
87136
- ---
87137
-
87138
- ` + userMessage;
88082
+ const prompt = composeClaudeReviewPrompt(userMessage, reviewProfile);
87139
88083
  const { command, stdinPrompt } = buildClaudeCommand(prompt, model, effort);
87140
- return { command, stdinPrompt, prompt, cwd, label: jobLabel, captureStdout: true, model, effort, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext };
88084
+ return { command, stdinPrompt, prompt, cwd, label: jobLabel, captureStdout: true, model, effort, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext, reviewProfileId: reviewProfile.id, reviewProfileLabel: reviewProfile.label };
88085
+ }
88086
+ const markerEngine = MARKER_ENGINES[provider];
88087
+ if (markerEngine) {
88088
+ const model = typeof config?.model === "string" && config.model ? config.model : undefined;
88089
+ const nonce = makeMarkerNonce();
88090
+ const prompt = composeMarkerReviewPrompt(reviewProfile, userMessage, nonce);
88091
+ const { command } = buildMarkerCommand(markerEngine, prompt, model, cwd);
88092
+ return { command, prompt, cwd, label: jobLabel, captureStdout: true, model, prUrl: launchPrUrl, diffScope: launchDiffScope, diffContext, reviewProfileId: reviewProfile.id, reviewProfileLabel: reviewProfile.label };
87141
88093
  }
87142
88094
  return null;
87143
88095
  },
@@ -87152,41 +88104,77 @@ async function startReviewServer(options) {
87152
88104
  prTitle: jobPrMeta.title,
87153
88105
  prRepo: getDisplayRepo(jobPrMeta)
87154
88106
  } : jobPrUrl ? { prUrl: jobPrUrl } : {};
87155
- if (job.provider === "codex" && meta.outputPath) {
87156
- const output = await parseCodexOutput(meta.outputPath);
87157
- if (!output)
88107
+ const profileLabel = job.reviewProfileId && job.reviewProfileId !== BUILTIN_DEFAULT_PROFILE.id ? job.reviewProfileLabel : undefined;
88108
+ const ingest = (transformed, logTag) => {
88109
+ if (transformed.length === 0)
88110
+ return;
88111
+ const annotations = transformed.map((a2) => ({
88112
+ ...a2,
88113
+ ...jobPrContext,
88114
+ ...jobDiffScope && { diffScope: jobDiffScope },
88115
+ ...profileLabel && { reviewProfileLabel: profileLabel }
88116
+ }));
88117
+ const result = externalAnnotations.addAnnotations({ annotations });
88118
+ if ("error" in result)
88119
+ console.error(`[${logTag}] addAnnotations error:`, result.error);
88120
+ return result;
88121
+ };
88122
+ if (job.provider === "codex") {
88123
+ const output = meta.outputPath ? await parseCodexOutput(meta.outputPath) : null;
88124
+ if (!output) {
88125
+ markJobReviewFailed(job, REVIEW_OUTPUT_FAILED);
87158
88126
  return;
88127
+ }
87159
88128
  const hasBlockingFindings = output.findings.some((f9) => f9.priority !== null && f9.priority <= 1);
87160
88129
  job.summary = {
87161
88130
  correctness: hasBlockingFindings ? "Issues Found" : output.overall_correctness,
87162
88131
  explanation: output.overall_explanation,
87163
88132
  confidence: output.overall_confidence_score
87164
88133
  };
87165
- if (output.findings.length > 0) {
87166
- const annotations = transformReviewFindings(output.findings, job.source, cwd, "Codex", workspace ? (filePath) => workspace.normalizeAnnotationPath(filePath) : undefined).map((a2) => ({ ...a2, ...jobPrContext, ...jobDiffScope && { diffScope: jobDiffScope } }));
87167
- const result = externalAnnotations.addAnnotations({ annotations });
87168
- if ("error" in result)
87169
- console.error(`[codex-review] addAnnotations error:`, result.error);
88134
+ ingest(transformReviewFindings(output.findings, job.source, cwd, "Codex", workspace ? (filePath) => workspace.normalizeAnnotationPath(filePath) : undefined), "codex-review");
88135
+ return;
88136
+ }
88137
+ if (job.provider === "claude") {
88138
+ const stdout = meta.stdout ?? "";
88139
+ const output = parseClaudeStreamOutput(stdout);
88140
+ if (!output) {
88141
+ console.error(`[claude-review] Failed to parse output (${stdout.length} bytes, last 200: ${stdout.slice(-200)})`);
88142
+ markJobReviewFailed(job, REVIEW_OUTPUT_FAILED);
88143
+ return;
87170
88144
  }
88145
+ const transformed = transformClaudeFindings(output.findings, job.source, cwd, workspace ? (filePath) => workspace.normalizeAnnotationPath(filePath) : undefined);
88146
+ const counts = { important: 0, nit: 0, pre_existing: 0 };
88147
+ for (const a2 of transformed)
88148
+ counts[a2.severity]++;
88149
+ const total = counts.important + counts.nit + counts.pre_existing;
88150
+ job.summary = {
88151
+ correctness: counts.important === 0 ? "Correct" : "Issues Found",
88152
+ explanation: `${counts.important} important, ${counts.nit} nit, ${counts.pre_existing} pre-existing`,
88153
+ confidence: total === 0 ? 1 : Math.max(0, 1 - counts.important * 0.2)
88154
+ };
88155
+ ingest(transformed, "claude-review");
87171
88156
  return;
87172
88157
  }
87173
- if (job.provider === "claude" && meta.stdout) {
87174
- const output = parseClaudeStreamOutput(meta.stdout);
88158
+ const markerEngine = MARKER_ENGINES[job.provider];
88159
+ if (markerEngine) {
88160
+ const nonce = extractMarkerNonce(job.prompt ?? "");
88161
+ const output = nonce && meta.stdout ? parseMarkerStreamOutput(meta.stdout, markerEngine, nonce) : null;
87175
88162
  if (!output) {
87176
- console.error(`[claude-review] Failed to parse output (${meta.stdout.length} bytes, last 200: ${meta.stdout.slice(-200)})`);
88163
+ job.status = "failed";
88164
+ job.error = `${markerEngine.author} review output missing or unparseable (no valid marker JSON).`;
87177
88165
  return;
87178
88166
  }
87179
- const total = output.summary.important + output.summary.nit + output.summary.pre_existing;
88167
+ const hasImportant = output.findings.some((f9) => f9.severity === "important");
87180
88168
  job.summary = {
87181
- correctness: output.summary.important === 0 ? "Correct" : "Issues Found",
87182
- explanation: `${output.summary.important} important, ${output.summary.nit} nit, ${output.summary.pre_existing} pre-existing`,
87183
- confidence: total === 0 ? 1 : Math.max(0, 1 - output.summary.important * 0.2)
88169
+ correctness: hasImportant ? "Issues Found" : "Correct",
88170
+ explanation: output.summary.explanation,
88171
+ confidence: output.summary.confidence
87184
88172
  };
87185
- if (output.findings.length > 0) {
87186
- const annotations = transformClaudeFindings(output.findings, job.source, cwd, workspace ? (filePath) => workspace.normalizeAnnotationPath(filePath) : undefined).map((a2) => ({ ...a2, ...jobPrContext, ...jobDiffScope && { diffScope: jobDiffScope } }));
87187
- const result = externalAnnotations.addAnnotations({ annotations });
87188
- if ("error" in result)
87189
- console.error(`[claude-review] addAnnotations error:`, result.error);
88173
+ const result = ingest(transformMarkerFindings(output.findings, job.source, markerEngine.author, cwd, workspace ? (filePath) => workspace.normalizeAnnotationPath(filePath) : undefined), `${markerEngine.id}-review`);
88174
+ if (result && "error" in result) {
88175
+ job.status = "failed";
88176
+ job.error = `${markerEngine.author} annotation insertion failed: ${result.error}`;
88177
+ return;
87190
88178
  }
87191
88179
  return;
87192
88180
  }
@@ -87748,6 +88736,44 @@ async function startReviewServer(options) {
87748
88736
  if (url.pathname === "/api/agents") {
87749
88737
  return handleAgents(options.opencodeClient);
87750
88738
  }
88739
+ if (url.pathname === "/api/agents/review-profiles" && req.method === "GET") {
88740
+ const body3 = {
88741
+ profiles: [
88742
+ {
88743
+ id: BUILTIN_DEFAULT_PROFILE.id,
88744
+ label: BUILTIN_DEFAULT_PROFILE.label,
88745
+ source: BUILTIN_DEFAULT_PROFILE.source,
88746
+ default: BUILTIN_DEFAULT_PROFILE.default
88747
+ },
88748
+ ...discoverCuratedSkills().map((s10) => ({
88749
+ id: `skill:${s10.name}`,
88750
+ label: s10.name,
88751
+ source: "user",
88752
+ sourcePath: s10.sourcePath
88753
+ }))
88754
+ ]
88755
+ };
88756
+ return Response.json(body3);
88757
+ }
88758
+ if (url.pathname === "/api/agents/skills" && req.method === "GET") {
88759
+ return Response.json({ skills: listAllSkills() });
88760
+ }
88761
+ if (url.pathname === "/api/agents/review-skills" && req.method === "POST") {
88762
+ let name;
88763
+ try {
88764
+ ({ name } = await req.json());
88765
+ } catch {
88766
+ return Response.json({ error: "Invalid JSON" }, { status: 400 });
88767
+ }
88768
+ if (typeof name !== "string" || name.length === 0) {
88769
+ return Response.json({ error: "`name` is required." }, { status: 400 });
88770
+ }
88771
+ try {
88772
+ return Response.json(enableReviewSkill(name));
88773
+ } catch (err) {
88774
+ return Response.json({ error: err instanceof Error ? err.message : "Could not enable review." }, { status: 400 });
88775
+ }
88776
+ }
87751
88777
  if (url.pathname === "/api/draft") {
87752
88778
  if (req.method === "POST")
87753
88779
  return handleDraftSave(req, draftKey);
@@ -87926,18 +88952,21 @@ var init_review = __esm(() => {
87926
88952
  init_agent_review_message();
87927
88953
  init_claude_review();
87928
88954
  init_tour_review();
88955
+ init_marker_review();
87929
88956
  init_config2();
87930
88957
  init_pr();
87931
88958
  init_ai_runtime();
87932
88959
  init_browser();
87933
88960
  init_open_in();
87934
88961
  init_code_nav2();
88962
+ init_review_skill_loader();
88963
+ init_review_profiles();
87935
88964
  init_browser();
87936
88965
  init_shared_handlers();
87937
88966
  });
87938
88967
 
87939
88968
  // ../../packages/shared/annotate-reference-roots-node.ts
87940
- import { realpathSync as realpathSync5 } from "fs";
88969
+ import { realpathSync as realpathSync6 } from "fs";
87941
88970
  import { dirname as dirname6 } from "path";
87942
88971
  function getAnnotateReferenceRootPaths(options) {
87943
88972
  const roots = [];
@@ -87948,7 +88977,7 @@ function getAnnotateReferenceRootPaths(options) {
87948
88977
  if (!roots.includes(resolved))
87949
88978
  roots.push(resolved);
87950
88979
  try {
87951
- const real = realpathSync5(resolved);
88980
+ const real = realpathSync6(resolved);
87952
88981
  if (!roots.includes(real))
87953
88982
  roots.push(real);
87954
88983
  } catch {}
@@ -88253,11 +89282,11 @@ function shutdown() {
88253
89282
  var init_agent_terminal_node_sidecar = () => {};
88254
89283
 
88255
89284
  // ../../packages/server/agent-terminal-runtime.ts
88256
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
88257
- import { join as join19 } from "path";
89285
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
89286
+ import { join as join20 } from "path";
88258
89287
  import { fileURLToPath } from "url";
88259
89288
  function getAgentTerminalManagedRuntimeDir(dataDir = getPlannotatorDataDir()) {
88260
- return join19(dataDir, "vendor", "agent-terminal", `webtui-${AGENT_TERMINAL_WEBTUI_VERSION}`);
89289
+ return join20(dataDir, "vendor", "agent-terminal", `webtui-${AGENT_TERMINAL_WEBTUI_VERSION}`);
88261
89290
  }
88262
89291
  function isAgentTerminalRemoteEnabled(env = process.env) {
88263
89292
  return isTruthy(env.PLANNOTATOR_AGENT_TERMINAL_REMOTE);
@@ -88286,7 +89315,7 @@ function resolveBundledAgentTerminalSidecarPath(moduleUrl = import.meta.url) {
88286
89315
  const bundledSidecarPath = fileURLToPath(new URL("./agent-terminal-node-sidecar.mjs", moduleUrl));
88287
89316
  if (isBunVirtualPath(bundledSidecarPath))
88288
89317
  return null;
88289
- return existsSync17(bundledSidecarPath) ? bundledSidecarPath : null;
89318
+ return existsSync18(bundledSidecarPath) ? bundledSidecarPath : null;
88290
89319
  }
88291
89320
  async function resolveBundledAgentTerminalRuntime(nodePath, bundledSidecarPath) {
88292
89321
  const webtuiCoreUrl = resolveImportUrl("@plannotator/webtui/core");
@@ -88337,9 +89366,9 @@ async function resolveManagedAgentTerminalRuntime(nodePath) {
88337
89366
  };
88338
89367
  }
88339
89368
  function materializeAgentTerminalSidecar(runtimeDir) {
88340
- mkdirSync9(runtimeDir, { recursive: true });
88341
- const sidecarPath = join19(runtimeDir, "agent-terminal-node-sidecar.mjs");
88342
- writeFileSync6(sidecarPath, agent_terminal_node_sidecar_default, "utf8");
89369
+ mkdirSync10(runtimeDir, { recursive: true });
89370
+ const sidecarPath = join20(runtimeDir, "agent-terminal-node-sidecar.mjs");
89371
+ writeFileSync7(sidecarPath, agent_terminal_node_sidecar_default, "utf8");
88343
89372
  return sidecarPath;
88344
89373
  }
88345
89374
  function tryMaterializeAgentTerminalSidecar(runtimeDir) {
@@ -88354,11 +89383,11 @@ function tryMaterializeAgentTerminalSidecar(runtimeDir) {
88354
89383
  }
88355
89384
  }
88356
89385
  function readInstalledWebTuiVersion(runtimeDir) {
88357
- const packageJsonPath = join19(runtimeDir, "node_modules", "@plannotator", "webtui", "package.json");
88358
- if (!existsSync17(packageJsonPath))
89386
+ const packageJsonPath = join20(runtimeDir, "node_modules", "@plannotator", "webtui", "package.json");
89387
+ if (!existsSync18(packageJsonPath))
88359
89388
  return null;
88360
89389
  try {
88361
- const parsed = JSON.parse(readFileSync9(packageJsonPath, "utf8"));
89390
+ const parsed = JSON.parse(readFileSync10(packageJsonPath, "utf8"));
88362
89391
  return typeof parsed.version === "string" ? parsed.version : null;
88363
89392
  } catch {
88364
89393
  return null;
@@ -89480,7 +90509,7 @@ var init_agent_terminal = __esm(() => {
89480
90509
  });
89481
90510
 
89482
90511
  // ../../packages/server/annotate.ts
89483
- import { existsSync as existsSync18 } from "fs";
90512
+ import { existsSync as existsSync19 } from "fs";
89484
90513
  import { dirname as dirname8, resolve as resolvePath4 } from "path";
89485
90514
  async function startAnnotateServer(options) {
89486
90515
  warmFileListCache(process.cwd(), "code");
@@ -89728,7 +90757,7 @@ async function startAnnotateServer(options) {
89728
90757
  targetPath = capability.enabled ? capability.path : initialSingleFileSourcePath;
89729
90758
  } else if (mode === "annotate-folder" && folderPath && typeof body3.path === "string") {
89730
90759
  targetPath = body3.allowMissingBase ? resolveFolderSourceFileForSave(body3.path, folderPath) : resolveFolderSourceFile(body3.path, folderPath);
89731
- if (body3.allowMissingBase && targetPath && !existsSync18(targetPath) && !openedSourceFilePaths.has(targetPath)) {
90760
+ if (body3.allowMissingBase && targetPath && !existsSync19(targetPath) && !openedSourceFilePaths.has(targetPath)) {
89732
90761
  targetPath = null;
89733
90762
  }
89734
90763
  }
@@ -90439,7 +91468,7 @@ function parseDiffMetadataPathLines(lines) {
90439
91468
  }
90440
91469
 
90441
91470
  // ../../packages/shared/review-workspace-node.ts
90442
- import { existsSync as existsSync19, readdirSync as readdirSync4 } from "fs";
91471
+ import { existsSync as existsSync20, readdirSync as readdirSync5 } from "fs";
90443
91472
  import { basename as basename7, relative as relative9, resolve as resolve10 } from "path";
90444
91473
  function normalizeWorkspacePath(path4) {
90445
91474
  return path4.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -90543,12 +91572,12 @@ function resolveWorkspaceFilePath(repos, prefixedPath) {
90543
91572
  return null;
90544
91573
  }
90545
91574
  function hasVcsMarker(dirPath) {
90546
- return VCS_MARKERS.some((marker) => existsSync19(resolve10(dirPath, marker)));
91575
+ return VCS_MARKERS.some((marker) => existsSync20(resolve10(dirPath, marker)));
90547
91576
  }
90548
91577
  function collectWorkspaceRepos(root3, current, results) {
90549
91578
  let entries;
90550
91579
  try {
90551
- entries = readdirSync4(current, { withFileTypes: true });
91580
+ entries = readdirSync5(current, { withFileTypes: true });
90552
91581
  } catch {
90553
91582
  return;
90554
91583
  }
@@ -90559,7 +91588,7 @@ function collectWorkspaceRepos(root3, current, results) {
90559
91588
  for (const entry of entries) {
90560
91589
  if (!entry.isDirectory())
90561
91590
  continue;
90562
- if (SKIP_DIRS.has(entry.name))
91591
+ if (SKIP_DIRS2.has(entry.name))
90563
91592
  continue;
90564
91593
  collectWorkspaceRepos(root3, resolve10(current, entry.name), results);
90565
91594
  }
@@ -90607,10 +91636,10 @@ function aggregateWorkspacePatch(repos) {
90607
91636
  errors: repos.flatMap((repo) => repo.error ? [`${repo.label}: ${repo.error}`] : [])
90608
91637
  };
90609
91638
  }
90610
- var SKIP_DIRS, VCS_MARKERS;
91639
+ var SKIP_DIRS2, VCS_MARKERS;
90611
91640
  var init_review_workspace_node = __esm(() => {
90612
91641
  init_review_core();
90613
- SKIP_DIRS = new Set([
91642
+ SKIP_DIRS2 = new Set([
90614
91643
  ".git",
90615
91644
  ".jj",
90616
91645
  "node_modules",
@@ -90951,7 +91980,7 @@ __export(exports_commands, {
90951
91980
  handleAnnotateLastCommand: () => handleAnnotateLastCommand,
90952
91981
  handleAnnotateCommand: () => handleAnnotateCommand
90953
91982
  });
90954
- import { statSync as statSync9 } from "fs";
91983
+ import { statSync as statSync10 } from "fs";
90955
91984
  import path4 from "path";
90956
91985
  async function handleReviewCommand(event, deps) {
90957
91986
  const { client: client3, reviewHtmlContent, getSharingEnabled, getShareBaseUrl, directory } = deps;
@@ -91107,7 +92136,7 @@ async function handleAnnotateCommand(event, deps) {
91107
92136
  const projectRoot = agentCwd;
91108
92137
  const resolvedArg = resolveUserPath(filePath, projectRoot);
91109
92138
  try {
91110
- isFolder = statSync9(resolvedArg).isDirectory();
92139
+ isFolder = statSync10(resolvedArg).isDirectory();
91111
92140
  } catch {}
91112
92141
  if (isFolder) {
91113
92142
  if (!hasMarkdownFiles(resolvedArg, FILE_BROWSER_EXCLUDED, /\.(mdx?|txt|html?)$/i)) {
@@ -91121,7 +92150,7 @@ async function handleAnnotateCommand(event, deps) {
91121
92150
  client3.app.log({ level: "info", message: `Opening annotation UI for folder ${resolvedArg}...` });
91122
92151
  } else if (/\.html?$/i.test(resolvedArg)) {
91123
92152
  try {
91124
- statSync9(resolvedArg);
92153
+ statSync10(resolvedArg);
91125
92154
  } catch {
91126
92155
  client3.app.log({ level: "error", message: `File not found: ${filePath}` });
91127
92156
  return;