@plannotator/pi-extension 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/generated/agent-jobs.ts +24 -0
- package/generated/agent-review-message.ts +29 -8
- package/generated/ai/providers/codex-sdk.ts +37 -2
- package/generated/claude-review.ts +46 -44
- package/generated/codex-review.ts +49 -19
- package/generated/external-annotation.ts +78 -20
- package/generated/marker-review.ts +824 -0
- package/generated/review-findings.ts +75 -0
- package/generated/review-profiles.ts +144 -0
- package/generated/review-skill-loader.ts +467 -0
- package/package.json +1 -1
- package/plannotator.html +3 -3
- package/review-editor.html +200 -183
- package/server/agent-jobs.ts +161 -34
- package/server/serverReview.ts +203 -32
package/generated/agent-jobs.ts
CHANGED
|
@@ -76,12 +76,23 @@ export interface AgentJobInfo {
|
|
|
76
76
|
diffScope?: string;
|
|
77
77
|
/** Diff context at launch time (see AgentJobDiffContext). */
|
|
78
78
|
diffContext?: AgentJobDiffContext;
|
|
79
|
+
/** Resolved review profile id at launch time (e.g. "builtin:default", "user:security"). */
|
|
80
|
+
reviewProfileId?: string;
|
|
81
|
+
/** Resolved review profile label — rides on findings so the UI can show a profile tag. */
|
|
82
|
+
reviewProfileLabel?: string;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export interface AgentCapability {
|
|
82
86
|
id: string;
|
|
83
87
|
name: string;
|
|
84
88
|
available: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Provider-discovered model catalog (currently only Cursor). Best-effort and
|
|
91
|
+
* account-specific — populated from the provider CLI at capability-detection
|
|
92
|
+
* time, empty when discovery fails or the CLI is unauthenticated. The UI
|
|
93
|
+
* drives its model picker from this instead of a hardcoded list.
|
|
94
|
+
*/
|
|
95
|
+
models?: { id: string; label: string }[];
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
export interface AgentCapabilities {
|
|
@@ -131,3 +142,16 @@ export function isTerminalStatus(status: AgentJobStatus): boolean {
|
|
|
131
142
|
export function jobSource(id: string): string {
|
|
132
143
|
return "agent-" + id.slice(0, 8);
|
|
133
144
|
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Review ingestion completion semantics
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/** Calm, provider-neutral failure reason. Never leak schema/CLI internals. */
|
|
151
|
+
export const REVIEW_OUTPUT_FAILED = "Review finished but produced no usable findings.";
|
|
152
|
+
|
|
153
|
+
/** Flip a job to failed with a calm one-liner (Code Tour precedent). */
|
|
154
|
+
export function markJobReviewFailed(job: AgentJobInfo, error: string): void {
|
|
155
|
+
job.status = "failed";
|
|
156
|
+
job.error = error;
|
|
157
|
+
}
|
|
@@ -80,32 +80,45 @@ export function buildWorkspacePromptContextLines(
|
|
|
80
80
|
];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export function buildAgentReviewUserMessageForTarget(
|
|
83
|
+
export function buildAgentReviewUserMessageForTarget(
|
|
84
|
+
target: AgentReviewTarget,
|
|
85
|
+
contextOnly = false,
|
|
86
|
+
): string {
|
|
84
87
|
if (target.kind === "workspace") {
|
|
85
|
-
return buildWorkspaceReviewUserMessage(target.patch, target.workspace);
|
|
88
|
+
return buildWorkspaceReviewUserMessage(target.patch, target.workspace, contextOnly);
|
|
86
89
|
}
|
|
87
90
|
return buildAgentReviewUserMessage(
|
|
88
91
|
target.patch,
|
|
89
92
|
target.diffType,
|
|
90
93
|
target.options,
|
|
91
94
|
target.kind === "pr" ? target.prMetadata : undefined,
|
|
95
|
+
contextOnly,
|
|
92
96
|
);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Build the dynamic user message shared by local Claude and Codex review jobs.
|
|
101
|
+
*
|
|
102
|
+
* `contextOnly` strips the "Review… / provide findings" framing prose and keeps
|
|
103
|
+
* only the git/PR context the agent needs to locate the changes. Used by custom
|
|
104
|
+
* review skills, which carry their own instructions and must not inherit the
|
|
105
|
+
* default review's framing. With `contextOnly` off the output is byte-identical
|
|
106
|
+
* to today's prompt.
|
|
107
|
+
*/
|
|
96
108
|
export function buildAgentReviewUserMessage(
|
|
97
109
|
patch: string,
|
|
98
110
|
diffType: DiffType,
|
|
99
111
|
options?: AgentReviewUserMessageOptions,
|
|
100
112
|
prMetadata?: PRMetadata,
|
|
113
|
+
contextOnly = false,
|
|
101
114
|
): string {
|
|
102
115
|
if (prMetadata) {
|
|
103
116
|
if (options?.prDiffScope === "full-stack") {
|
|
104
117
|
return [
|
|
105
|
-
`Full-stack review of ${prMetadata.url}`,
|
|
118
|
+
contextOnly ? prMetadata.url : `Full-stack review of ${prMetadata.url}`,
|
|
106
119
|
"",
|
|
107
120
|
"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).",
|
|
108
|
-
"Review the complete diff for issues that span the stack.",
|
|
121
|
+
...(contextOnly ? [] : ["Review the complete diff for issues that span the stack."]),
|
|
109
122
|
"",
|
|
110
123
|
"```diff",
|
|
111
124
|
patch,
|
|
@@ -113,6 +126,9 @@ export function buildAgentReviewUserMessage(
|
|
|
113
126
|
].join("\n");
|
|
114
127
|
}
|
|
115
128
|
if (options?.hasLocalAccess) {
|
|
129
|
+
// Pure context already (where the checkout is, how to diff against the
|
|
130
|
+
// base) — no "Review… / provide findings" framing to strip, so this is
|
|
131
|
+
// identical for default and custom reviews regardless of contextOnly.
|
|
116
132
|
return [
|
|
117
133
|
prMetadata.url,
|
|
118
134
|
"",
|
|
@@ -126,11 +142,14 @@ export function buildAgentReviewUserMessage(
|
|
|
126
142
|
|
|
127
143
|
const instruction = getLocalDiffInstruction(diffType, options?.defaultBranch);
|
|
128
144
|
if (instruction) {
|
|
145
|
+
if (contextOnly) {
|
|
146
|
+
return `Changeset: ${instruction.target}.\n${instruction.inspect}`;
|
|
147
|
+
}
|
|
129
148
|
return `Review ${instruction.target}. ${instruction.inspect} Provide prioritized, actionable findings.`;
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
return [
|
|
133
|
-
"Review the following code changes and provide prioritized findings.",
|
|
152
|
+
contextOnly ? "Code changes:" : "Review the following code changes and provide prioritized findings.",
|
|
134
153
|
"",
|
|
135
154
|
"```diff",
|
|
136
155
|
patch,
|
|
@@ -141,10 +160,12 @@ export function buildAgentReviewUserMessage(
|
|
|
141
160
|
function buildWorkspaceReviewUserMessage(
|
|
142
161
|
patch: string,
|
|
143
162
|
workspace: WorkspaceReviewPromptContext,
|
|
163
|
+
contextOnly = false,
|
|
144
164
|
): string {
|
|
145
165
|
return [
|
|
146
|
-
|
|
147
|
-
|
|
166
|
+
...(contextOnly
|
|
167
|
+
? []
|
|
168
|
+
: ["Review the local workspace changes across multiple nested VCS repositories.", ""]),
|
|
148
169
|
...buildWorkspacePromptContextLines(workspace, { includeReportingInstruction: true }),
|
|
149
170
|
"",
|
|
150
171
|
"```diff",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import { buildSystemPrompt, buildEffectivePrompt } from "../context.ts";
|
|
15
15
|
import { BaseSession } from "../base-session.ts";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { promisify } from "node:util";
|
|
16
18
|
import type {
|
|
17
19
|
AIProvider,
|
|
18
20
|
AIProviderCapabilities,
|
|
@@ -29,6 +31,30 @@ import type {
|
|
|
29
31
|
const PROVIDER_NAME = "codex-sdk";
|
|
30
32
|
const DEFAULT_MODEL = "gpt-5.4";
|
|
31
33
|
|
|
34
|
+
type GitWorkTreeProbe = (
|
|
35
|
+
command: string,
|
|
36
|
+
args: string[],
|
|
37
|
+
options: { encoding: "utf8" },
|
|
38
|
+
) => Promise<{ stdout?: string | Buffer }>;
|
|
39
|
+
|
|
40
|
+
const execFileAsync = promisify(execFile);
|
|
41
|
+
|
|
42
|
+
export async function shouldSkipGitRepoCheck(
|
|
43
|
+
cwd: string,
|
|
44
|
+
probe: GitWorkTreeProbe = execFileAsync,
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
const result = probe(
|
|
48
|
+
"git",
|
|
49
|
+
["-C", cwd, "rev-parse", "--is-inside-work-tree"],
|
|
50
|
+
{ encoding: "utf8" },
|
|
51
|
+
);
|
|
52
|
+
return String((await result).stdout ?? "").trim() !== "true";
|
|
53
|
+
} catch {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
// ---------------------------------------------------------------------------
|
|
33
59
|
// Provider
|
|
34
60
|
// ---------------------------------------------------------------------------
|
|
@@ -58,10 +84,13 @@ export class CodexSDKProvider implements AIProvider {
|
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
async createSession(options: CreateSessionOptions): Promise<AISession> {
|
|
87
|
+
const cwd = options.cwd ?? this.config.cwd ?? process.cwd();
|
|
88
|
+
const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd);
|
|
61
89
|
return new CodexSDKSession({
|
|
62
90
|
...this.baseConfig(options),
|
|
63
91
|
systemPrompt: buildSystemPrompt(options.context),
|
|
64
|
-
cwd
|
|
92
|
+
cwd,
|
|
93
|
+
skipGitRepoCheck,
|
|
65
94
|
parentSessionId: null,
|
|
66
95
|
});
|
|
67
96
|
}
|
|
@@ -74,10 +103,13 @@ export class CodexSDKProvider implements AIProvider {
|
|
|
74
103
|
}
|
|
75
104
|
|
|
76
105
|
async resumeSession(sessionId: string): Promise<AISession> {
|
|
106
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
107
|
+
const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd);
|
|
77
108
|
return new CodexSDKSession({
|
|
78
109
|
...this.baseConfig(),
|
|
79
110
|
systemPrompt: null,
|
|
80
|
-
cwd
|
|
111
|
+
cwd,
|
|
112
|
+
skipGitRepoCheck,
|
|
81
113
|
parentSessionId: null,
|
|
82
114
|
resumeThreadId: sessionId,
|
|
83
115
|
});
|
|
@@ -124,6 +156,7 @@ interface SessionConfig {
|
|
|
124
156
|
maxTurns: number;
|
|
125
157
|
sandboxMode: "read-only" | "workspace-write" | "danger-full-access";
|
|
126
158
|
cwd: string;
|
|
159
|
+
skipGitRepoCheck: boolean;
|
|
127
160
|
parentSessionId: string | null;
|
|
128
161
|
resumeThreadId?: string;
|
|
129
162
|
codexExecutablePath?: string;
|
|
@@ -174,6 +207,7 @@ class CodexSDKSession extends BaseSession {
|
|
|
174
207
|
this._thread = this._codexInstance.resumeThread(this.config.resumeThreadId, {
|
|
175
208
|
model: this.config.model,
|
|
176
209
|
workingDirectory: this.config.cwd,
|
|
210
|
+
skipGitRepoCheck: this.config.skipGitRepoCheck,
|
|
177
211
|
sandboxMode: this.config.sandboxMode,
|
|
178
212
|
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
|
|
179
213
|
});
|
|
@@ -181,6 +215,7 @@ class CodexSDKSession extends BaseSession {
|
|
|
181
215
|
this._thread = this._codexInstance.startThread({
|
|
182
216
|
model: this.config.model,
|
|
183
217
|
workingDirectory: this.config.cwd,
|
|
218
|
+
skipGitRepoCheck: this.config.skipGitRepoCheck,
|
|
184
219
|
sandboxMode: this.config.sandboxMode,
|
|
185
220
|
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
|
|
186
221
|
});
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
// @generated — DO NOT EDIT. Source: packages/server/claude-review.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
composeReviewPrompt,
|
|
4
|
+
type ResolvedReviewProfile,
|
|
5
|
+
} from "./review-profiles.js";
|
|
6
|
+
import {
|
|
7
|
+
transformSeverityFindings,
|
|
8
|
+
type ReviewSeverity,
|
|
9
|
+
type ReviewFinding,
|
|
10
|
+
type ReviewAnnotationInput,
|
|
11
|
+
} from "./review-findings";
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* Claude Code Review Agent — prompt, command builder, and JSONL output parser.
|
|
@@ -17,16 +26,10 @@ import { toRelativePath } from "./path-utils.js";
|
|
|
17
26
|
// Types
|
|
18
27
|
// ---------------------------------------------------------------------------
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
file: string;
|
|
25
|
-
line: number;
|
|
26
|
-
end_line: number;
|
|
27
|
-
description: string;
|
|
28
|
-
reasoning: string;
|
|
29
|
-
}
|
|
29
|
+
// Claude findings ARE review findings — reuse the one shared shape from
|
|
30
|
+
// review-findings.ts rather than keeping a byte-identical copy that can drift.
|
|
31
|
+
export type ClaudeSeverity = ReviewSeverity;
|
|
32
|
+
export type ClaudeFinding = ReviewFinding;
|
|
30
33
|
|
|
31
34
|
export interface ClaudeReviewOutput {
|
|
32
35
|
findings: ClaudeFinding[];
|
|
@@ -50,9 +53,13 @@ export const CLAUDE_REVIEW_SCHEMA_JSON = JSON.stringify({
|
|
|
50
53
|
type: "object",
|
|
51
54
|
properties: {
|
|
52
55
|
severity: { type: "string", enum: ["important", "nit", "pre_existing"] },
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end_line
|
|
56
|
+
// Nullable, not omitted: keep every property in `required` so the
|
|
57
|
+
// schema is valid under strict structured-output validators too. A
|
|
58
|
+
// whole-file finding sets line/end_line null; a general finding also
|
|
59
|
+
// sets file null.
|
|
60
|
+
file: { type: ["string", "null"] },
|
|
61
|
+
line: { type: ["integer", "null"] },
|
|
62
|
+
end_line: { type: ["integer", "null"] },
|
|
56
63
|
description: { type: "string" },
|
|
57
64
|
reasoning: { type: "string" },
|
|
58
65
|
},
|
|
@@ -162,6 +169,10 @@ Step 5: Deduplicate and rank
|
|
|
162
169
|
- Within each severity, sort by file path and line number
|
|
163
170
|
|
|
164
171
|
Step 6: Return structured JSON output matching the schema.
|
|
172
|
+
Place each finding by how specific it is: give file and line for a line-level
|
|
173
|
+
issue; give file and set line null for a whole-file issue; set file and line
|
|
174
|
+
null for a general, review-level note. Never invent a line you are unsure of —
|
|
175
|
+
drop to a file or general placement instead of guessing.
|
|
165
176
|
If no issues are found, return an empty findings array with zeroed summary.
|
|
166
177
|
|
|
167
178
|
## Hard constraints
|
|
@@ -176,6 +187,23 @@ Step 6: Return structured JSON output matching the schema.
|
|
|
176
187
|
- Do NOT use gh pr comment or any commenting tool
|
|
177
188
|
- Your only output is the structured JSON findings`;
|
|
178
189
|
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Prompt composition
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compose Claude's review prompt: the immutable system prompt, the resolved
|
|
196
|
+
* profile's Custom Review Profile section (omitted for builtin:default), then
|
|
197
|
+
* the user review message. For builtin:default / no profile the output is
|
|
198
|
+
* byte-identical to today's `CLAUDE_REVIEW_PROMPT + "\n\n---\n\n" + userMessage`.
|
|
199
|
+
*/
|
|
200
|
+
export function composeClaudeReviewPrompt(
|
|
201
|
+
userMessage: string,
|
|
202
|
+
reviewProfile?: ResolvedReviewProfile,
|
|
203
|
+
): string {
|
|
204
|
+
return composeReviewPrompt(CLAUDE_REVIEW_PROMPT, reviewProfile, userMessage);
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
// ---------------------------------------------------------------------------
|
|
180
208
|
// Command builder
|
|
181
209
|
// ---------------------------------------------------------------------------
|
|
@@ -284,36 +312,10 @@ export function transformClaudeFindings(
|
|
|
284
312
|
source: string,
|
|
285
313
|
cwd?: string,
|
|
286
314
|
pathTransform?: (path: string) => string,
|
|
287
|
-
):
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
lineEnd: number;
|
|
292
|
-
type: string;
|
|
293
|
-
side: string;
|
|
294
|
-
scope: string;
|
|
295
|
-
text: string;
|
|
296
|
-
severity: ClaudeSeverity;
|
|
297
|
-
reasoning: string;
|
|
298
|
-
author: string;
|
|
299
|
-
}> {
|
|
300
|
-
return findings
|
|
301
|
-
.filter(f => f.file && typeof f.line === "number")
|
|
302
|
-
.map(f => ({
|
|
303
|
-
source,
|
|
304
|
-
filePath: pathTransform
|
|
305
|
-
? pathTransform(toRelativePath(f.file, cwd))
|
|
306
|
-
: toRelativePath(f.file, cwd),
|
|
307
|
-
lineStart: f.line,
|
|
308
|
-
lineEnd: f.end_line ?? f.line,
|
|
309
|
-
type: "comment",
|
|
310
|
-
side: "new",
|
|
311
|
-
scope: "line",
|
|
312
|
-
text: `[${f.severity}] ${f.description}`,
|
|
313
|
-
severity: f.severity,
|
|
314
|
-
reasoning: f.reasoning,
|
|
315
|
-
author: "Claude Code",
|
|
316
|
-
}));
|
|
315
|
+
): ReviewAnnotationInput[] {
|
|
316
|
+
// Routing (line / whole-file / general) is shared with the marker engines in
|
|
317
|
+
// review-findings.ts — nothing is dropped; only the author differs.
|
|
318
|
+
return transformSeverityFindings(findings, source, "Claude Code", cwd, pathTransform);
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
// ---------------------------------------------------------------------------
|
|
@@ -12,6 +12,11 @@ import { appendFile, mkdir, unlink, writeFile, readFile } from "node:fs/promises
|
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { toRelativePath } from "./path-utils.js";
|
|
14
14
|
import { getPlannotatorDataDir } from "./data-dir";
|
|
15
|
+
import {
|
|
16
|
+
composeReviewPrompt,
|
|
17
|
+
type ResolvedReviewProfile,
|
|
18
|
+
} from "./review-profiles.js";
|
|
19
|
+
import { classifyFindingPlacement } from "./external-annotation.js";
|
|
15
20
|
|
|
16
21
|
// ---------------------------------------------------------------------------
|
|
17
22
|
// Debug log — only active when PLANNOTATOR_DEBUG is set
|
|
@@ -39,7 +44,7 @@ async function debugLog(label: string, data?: unknown): Promise<void> {
|
|
|
39
44
|
// can't read, so we materialize the schema to a real file.
|
|
40
45
|
// ---------------------------------------------------------------------------
|
|
41
46
|
|
|
42
|
-
const CODEX_REVIEW_SCHEMA = JSON.stringify({
|
|
47
|
+
export const CODEX_REVIEW_SCHEMA = JSON.stringify({
|
|
43
48
|
type: "object",
|
|
44
49
|
properties: {
|
|
45
50
|
findings: {
|
|
@@ -51,12 +56,15 @@ const CODEX_REVIEW_SCHEMA = JSON.stringify({
|
|
|
51
56
|
body: { type: "string" },
|
|
52
57
|
confidence_score: { type: "number" },
|
|
53
58
|
priority: { type: ["integer", "null"] },
|
|
59
|
+
// Nullable, not omittable: OpenAI strict structured output requires
|
|
60
|
+
// every property to appear in `required`. A whole-file finding sets
|
|
61
|
+
// line_range to null; a general finding sets code_location to null.
|
|
54
62
|
code_location: {
|
|
55
|
-
type: "object",
|
|
63
|
+
type: ["object", "null"],
|
|
56
64
|
properties: {
|
|
57
65
|
absolute_file_path: { type: "string" },
|
|
58
66
|
line_range: {
|
|
59
|
-
type: "object",
|
|
67
|
+
type: ["object", "null"],
|
|
60
68
|
properties: {
|
|
61
69
|
start: { type: "integer" },
|
|
62
70
|
end: { type: "integer" },
|
|
@@ -152,6 +160,8 @@ At the beginning of the finding title, tag the bug with priority level. For exam
|
|
|
152
160
|
|
|
153
161
|
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.
|
|
154
162
|
|
|
163
|
+
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 — drop to a whole-file or general placement instead.
|
|
164
|
+
|
|
155
165
|
At the end of your findings, output an "overall correctness" verdict of whether or not the patch should be considered "correct".
|
|
156
166
|
Correct implies that existing code and tests will not break, and the patch is free of bugs and other blocking issues.
|
|
157
167
|
Ignore non-blocking issues such as style, formatting, typos, documentation, and other nits.
|
|
@@ -159,6 +169,24 @@ Ignore non-blocking issues such as style, formatting, typos, documentation, and
|
|
|
159
169
|
FORMATTING GUIDELINES:
|
|
160
170
|
The finding description should be one paragraph.`;
|
|
161
171
|
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Prompt composition
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Compose Codex's review prompt: the immutable system prompt, the resolved
|
|
178
|
+
* profile's Custom Review Profile section (omitted for builtin:default), then
|
|
179
|
+
* the user review message. For builtin:default / no profile the output is
|
|
180
|
+
* byte-identical to today's
|
|
181
|
+
* `CODEX_REVIEW_SYSTEM_PROMPT + "\n\n---\n\n" + userMessage`.
|
|
182
|
+
*/
|
|
183
|
+
export function composeCodexReviewPrompt(
|
|
184
|
+
userMessage: string,
|
|
185
|
+
reviewProfile?: ResolvedReviewProfile,
|
|
186
|
+
): string {
|
|
187
|
+
return composeReviewPrompt(CODEX_REVIEW_SYSTEM_PROMPT, reviewProfile, userMessage);
|
|
188
|
+
}
|
|
189
|
+
|
|
162
190
|
// ---------------------------------------------------------------------------
|
|
163
191
|
// Command builder
|
|
164
192
|
// ---------------------------------------------------------------------------
|
|
@@ -213,7 +241,7 @@ export function generateOutputPath(): string {
|
|
|
213
241
|
|
|
214
242
|
export interface CodexCodeLocation {
|
|
215
243
|
absolute_file_path: string;
|
|
216
|
-
line_range
|
|
244
|
+
line_range?: { start: number; end: number } | null; // null for a whole-file comment
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
export interface CodexFinding {
|
|
@@ -221,7 +249,7 @@ export interface CodexFinding {
|
|
|
221
249
|
body: string;
|
|
222
250
|
confidence_score: number;
|
|
223
251
|
priority: number | null;
|
|
224
|
-
code_location
|
|
252
|
+
code_location?: CodexCodeLocation | null; // null for a general (review-level) comment
|
|
225
253
|
}
|
|
226
254
|
|
|
227
255
|
export interface CodexReviewOutput {
|
|
@@ -296,25 +324,27 @@ export function transformReviewFindings(
|
|
|
296
324
|
author?: string,
|
|
297
325
|
pathTransform?: (path: string) => string,
|
|
298
326
|
): ReviewAnnotationInput[] {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
327
|
+
// Route every finding by what it carries — nothing is dropped. No usable file
|
|
328
|
+
// becomes a general comment; a file without a line range, a whole-file comment.
|
|
329
|
+
const annotations = findings.map((f) => {
|
|
330
|
+
const loc = f.code_location;
|
|
331
|
+
const rawFile = loc && typeof loc.absolute_file_path === "string" ? loc.absolute_file_path : "";
|
|
332
|
+
const filePath = rawFile
|
|
333
|
+
? (pathTransform ? pathTransform(toRelativePath(rawFile, cwd)) : toRelativePath(rawFile, cwd))
|
|
334
|
+
: "";
|
|
335
|
+
const placement = classifyFindingPlacement(filePath, loc?.line_range?.start, loc?.line_range?.end);
|
|
336
|
+
return {
|
|
306
337
|
source,
|
|
307
|
-
filePath:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
lineStart: f.code_location.line_range.start,
|
|
311
|
-
lineEnd: f.code_location.line_range.end,
|
|
338
|
+
filePath: placement.filePath,
|
|
339
|
+
lineStart: placement.lineStart,
|
|
340
|
+
lineEnd: placement.lineEnd,
|
|
312
341
|
type: "comment",
|
|
313
342
|
side: "new",
|
|
314
|
-
scope:
|
|
343
|
+
scope: placement.scope,
|
|
315
344
|
text: `${f.title}\n\n${f.body}`.trim(),
|
|
316
345
|
author: author ?? "Review Agent",
|
|
317
|
-
}
|
|
346
|
+
};
|
|
347
|
+
});
|
|
318
348
|
|
|
319
349
|
debugLog("TRANSFORM_FINDINGS", {
|
|
320
350
|
inputCount: findings.length,
|
|
@@ -191,14 +191,53 @@ interface ReviewAnnotation {
|
|
|
191
191
|
createdAt: number;
|
|
192
192
|
author?: string;
|
|
193
193
|
source?: string;
|
|
194
|
-
// Agent review metadata (optional — only set by
|
|
194
|
+
// Agent review metadata (optional — only set by agent review findings)
|
|
195
195
|
severity?: string; // "important" | "nit" | "pre_existing"
|
|
196
196
|
reasoning?: string; // Validation chain explaining how the issue was confirmed
|
|
197
|
+
reviewProfileLabel?: string; // Custom review profile that produced this finding
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
const VALID_REVIEW_TYPES = ["comment", "suggestion", "concern"];
|
|
200
201
|
const VALID_SIDES = ["old", "new"];
|
|
201
|
-
const VALID_SCOPES = ["line", "file"];
|
|
202
|
+
const VALID_SCOPES = ["line", "file", "general"];
|
|
203
|
+
|
|
204
|
+
/** A review finding's placement, derived from what it carries. */
|
|
205
|
+
export type FindingPlacement = {
|
|
206
|
+
scope: "line" | "file" | "general";
|
|
207
|
+
filePath: string;
|
|
208
|
+
lineStart: number;
|
|
209
|
+
lineEnd: number;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Classify an agent review finding by what it carries, so nothing is dropped:
|
|
214
|
+
* file + a usable line → a line comment
|
|
215
|
+
* file, no line → a whole-file comment
|
|
216
|
+
* neither → a general (review-level) comment
|
|
217
|
+
*
|
|
218
|
+
* For file and general placements the line is 0; for general the path is "".
|
|
219
|
+
* Consumers branch on `scope`, never on the sentinel values.
|
|
220
|
+
*/
|
|
221
|
+
export function classifyFindingPlacement(
|
|
222
|
+
filePath: string,
|
|
223
|
+
lineStart: number | null | undefined,
|
|
224
|
+
lineEnd: number | null | undefined,
|
|
225
|
+
): FindingPlacement {
|
|
226
|
+
const hasFile = filePath.length > 0;
|
|
227
|
+
const hasLine = typeof lineStart === "number";
|
|
228
|
+
if (hasFile && hasLine) {
|
|
229
|
+
return {
|
|
230
|
+
scope: "line",
|
|
231
|
+
filePath,
|
|
232
|
+
lineStart,
|
|
233
|
+
lineEnd: typeof lineEnd === "number" ? lineEnd : lineStart,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (hasFile) {
|
|
237
|
+
return { scope: "file", filePath, lineStart: 0, lineEnd: 0 };
|
|
238
|
+
}
|
|
239
|
+
return { scope: "general", filePath: "", lineStart: 0, lineEnd: 0 };
|
|
240
|
+
}
|
|
202
241
|
|
|
203
242
|
export function transformReviewInput(
|
|
204
243
|
body: unknown,
|
|
@@ -213,14 +252,40 @@ export function transformReviewInput(
|
|
|
213
252
|
const source = requireString(obj, "source", i);
|
|
214
253
|
if (typeof source !== "string") return source;
|
|
215
254
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
// scope: optional, defaults to "line"
|
|
256
|
+
const scope = typeof obj.scope === "string" ? obj.scope : "line";
|
|
257
|
+
if (!VALID_SCOPES.includes(scope)) {
|
|
258
|
+
return {
|
|
259
|
+
error: `annotations[${i}] invalid scope "${scope}". Must be one of: ${VALID_SCOPES.join(", ")}`,
|
|
260
|
+
};
|
|
221
261
|
}
|
|
222
|
-
|
|
223
|
-
|
|
262
|
+
|
|
263
|
+
// Location requirements depend on scope:
|
|
264
|
+
// line → filePath + lineStart + lineEnd required. A finding that claims
|
|
265
|
+
// a line must carry one, so a broken line finding is rejected
|
|
266
|
+
// rather than quietly passing as a vaguer comment.
|
|
267
|
+
// file → filePath required; line ignored (defaults to 0).
|
|
268
|
+
// general → no file, no line (review-level; defaults to "" / 0).
|
|
269
|
+
let filePath = "";
|
|
270
|
+
let lineStart = 0;
|
|
271
|
+
let lineEnd = 0;
|
|
272
|
+
if (scope !== "general") {
|
|
273
|
+
const fp = requireString(obj, "filePath", i);
|
|
274
|
+
if (typeof fp !== "string") return fp;
|
|
275
|
+
filePath = fp;
|
|
276
|
+
if (scope === "line") {
|
|
277
|
+
if (typeof obj.lineStart !== "number") {
|
|
278
|
+
return { error: `annotations[${i}] missing required "lineStart" field` };
|
|
279
|
+
}
|
|
280
|
+
if (typeof obj.lineEnd !== "number") {
|
|
281
|
+
return { error: `annotations[${i}] missing required "lineEnd" field` };
|
|
282
|
+
}
|
|
283
|
+
lineStart = obj.lineStart;
|
|
284
|
+
lineEnd = obj.lineEnd;
|
|
285
|
+
} else {
|
|
286
|
+
lineStart = typeof obj.lineStart === "number" ? obj.lineStart : 0;
|
|
287
|
+
lineEnd = typeof obj.lineEnd === "number" ? obj.lineEnd : 0;
|
|
288
|
+
}
|
|
224
289
|
}
|
|
225
290
|
|
|
226
291
|
// side: optional, defaults to "new"
|
|
@@ -239,14 +304,6 @@ export function transformReviewInput(
|
|
|
239
304
|
};
|
|
240
305
|
}
|
|
241
306
|
|
|
242
|
-
// scope: optional, defaults to "line"
|
|
243
|
-
const scope = typeof obj.scope === "string" ? obj.scope : "line";
|
|
244
|
-
if (!VALID_SCOPES.includes(scope)) {
|
|
245
|
-
return {
|
|
246
|
-
error: `annotations[${i}] invalid scope "${scope}". Must be one of: ${VALID_SCOPES.join(", ")}`,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
307
|
// Must have at least text or suggestedCode
|
|
251
308
|
if (typeof obj.text !== "string" && typeof obj.suggestedCode !== "string") {
|
|
252
309
|
return {
|
|
@@ -259,8 +316,8 @@ export function transformReviewInput(
|
|
|
259
316
|
type,
|
|
260
317
|
scope,
|
|
261
318
|
filePath,
|
|
262
|
-
lineStart
|
|
263
|
-
lineEnd
|
|
319
|
+
lineStart,
|
|
320
|
+
lineEnd,
|
|
264
321
|
side,
|
|
265
322
|
text: typeof obj.text === "string" ? obj.text : undefined,
|
|
266
323
|
suggestedCode: typeof obj.suggestedCode === "string" ? obj.suggestedCode : undefined,
|
|
@@ -268,9 +325,10 @@ export function transformReviewInput(
|
|
|
268
325
|
createdAt: Date.now(),
|
|
269
326
|
author: typeof obj.author === "string" ? obj.author : undefined,
|
|
270
327
|
source,
|
|
271
|
-
// Agent review metadata (optional — only set by
|
|
328
|
+
// Agent review metadata (optional — only set by agent review findings)
|
|
272
329
|
...(typeof obj.severity === "string" && { severity: obj.severity }),
|
|
273
330
|
...(typeof obj.reasoning === "string" && { reasoning: obj.reasoning }),
|
|
331
|
+
...(typeof obj.reviewProfileLabel === "string" && { reviewProfileLabel: obj.reviewProfileLabel }),
|
|
274
332
|
});
|
|
275
333
|
}
|
|
276
334
|
|