@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 +1192 -163
- package/package.json +2 -2
- package/plannotator.html +3 -3
- package/review-editor.html +200 -183
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
|
|
40691
|
-
if (
|
|
40692
|
-
return
|
|
40693
|
-
|
|
40694
|
-
|
|
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
|
-
|
|
40697
|
-
|
|
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
|
|
40728
|
-
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
75685
|
-
function
|
|
75686
|
-
return
|
|
75687
|
-
|
|
75688
|
-
`;
|
|
75828
|
+
// ../../packages/server/marker-review.ts
|
|
75829
|
+
function markerOpen(nonce) {
|
|
75830
|
+
return `<${MARKER_TAG}:${nonce}>`;
|
|
75689
75831
|
}
|
|
75690
|
-
function
|
|
75691
|
-
return
|
|
75832
|
+
function markerClose(nonce) {
|
|
75833
|
+
return `</${MARKER_TAG}:${nonce}>`;
|
|
75692
75834
|
}
|
|
75693
|
-
function
|
|
75694
|
-
return "
|
|
75835
|
+
function makeMarkerNonce() {
|
|
75836
|
+
return "pn" + crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
75695
75837
|
}
|
|
75696
|
-
|
|
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
|
-
|
|
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
|
|
75714
|
-
|
|
75715
|
-
|
|
75716
|
-
|
|
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
|
-
|
|
75823
|
-
|
|
75824
|
-
|
|
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(
|
|
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.
|
|
76163
|
-
|
|
76164
|
-
|
|
76165
|
-
|
|
76166
|
-
|
|
76167
|
-
|
|
76168
|
-
|
|
76169
|
-
|
|
76170
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
87156
|
-
|
|
87157
|
-
if (
|
|
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
|
-
|
|
87166
|
-
|
|
87167
|
-
|
|
87168
|
-
|
|
87169
|
-
|
|
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
|
-
|
|
87174
|
-
|
|
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
|
-
|
|
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
|
|
88167
|
+
const hasImportant = output.findings.some((f9) => f9.severity === "important");
|
|
87180
88168
|
job.summary = {
|
|
87181
|
-
correctness:
|
|
87182
|
-
explanation:
|
|
87183
|
-
confidence:
|
|
88169
|
+
correctness: hasImportant ? "Issues Found" : "Correct",
|
|
88170
|
+
explanation: output.summary.explanation,
|
|
88171
|
+
confidence: output.summary.confidence
|
|
87184
88172
|
};
|
|
87185
|
-
|
|
87186
|
-
|
|
87187
|
-
|
|
87188
|
-
|
|
87189
|
-
|
|
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
|
|
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 =
|
|
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
|
|
88257
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
-
|
|
88341
|
-
const sidecarPath =
|
|
88342
|
-
|
|
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 =
|
|
88358
|
-
if (!
|
|
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(
|
|
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
|
|
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 && !
|
|
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
|
|
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) =>
|
|
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 =
|
|
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 (
|
|
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
|
|
91639
|
+
var SKIP_DIRS2, VCS_MARKERS;
|
|
90611
91640
|
var init_review_workspace_node = __esm(() => {
|
|
90612
91641
|
init_review_core();
|
|
90613
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
92153
|
+
statSync10(resolvedArg);
|
|
91125
92154
|
} catch {
|
|
91126
92155
|
client3.app.log({ level: "error", message: `File not found: ${filePath}` });
|
|
91127
92156
|
return;
|