@effect-agent/pr-review 0.1.0-beta.8 → 0.1.0-beta.80
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/NOTICE +26 -0
- package/README.md +170 -158
- package/dist/Review.d.mts +295 -0
- package/dist/Review.mjs +704 -0
- package/dist/Review.mjs.map +1 -0
- package/dist/ReviewRepository-Wd_4qCaO.d.mts +71 -0
- package/dist/ReviewRepository.d.mts +2 -0
- package/dist/ReviewRepository.mjs +15 -0
- package/dist/ReviewRepository.mjs.map +1 -0
- package/dist/index.d.mts +3 -716
- package/dist/index.mjs +3 -66
- package/dist/repository-BzSG74vX.mjs +101 -0
- package/dist/repository-BzSG74vX.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -54
- package/src/Review.ts +1058 -0
- package/src/ReviewRepository.ts +9 -0
- package/src/index.ts +2 -20
- package/src/internal/repository.ts +156 -0
- package/dist/action.d.mts +0 -185
- package/dist/action.mjs +0 -406
- package/dist/action.mjs.map +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -102
- package/dist/cli.mjs.map +0 -1
- package/dist/fan-out-BBEATQwc.d.mts +0 -997
- package/dist/github-BZNzmxao.mjs +0 -1372
- package/dist/github-BZNzmxao.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/providers-J6BKHyHe.mjs +0 -986
- package/dist/providers-J6BKHyHe.mjs.map +0 -1
- package/dist/testing.d.mts +0 -131
- package/dist/testing.mjs +0 -228
- package/dist/testing.mjs.map +0 -1
- package/src/action.ts +0 -666
- package/src/cli.ts +0 -213
- package/src/internal/action-entry.ts +0 -41
- package/src/internal/coverage.ts +0 -245
- package/src/internal/diff.ts +0 -134
- package/src/internal/effort.ts +0 -86
- package/src/internal/factory.ts +0 -374
- package/src/internal/fan-out-scripted.ts +0 -164
- package/src/internal/fan-out.ts +0 -450
- package/src/internal/fingerprint.ts +0 -74
- package/src/internal/fixtures.ts +0 -127
- package/src/internal/github-env.ts +0 -128
- package/src/internal/github.ts +0 -531
- package/src/internal/ignore.ts +0 -88
- package/src/internal/profiles.ts +0 -79
- package/src/internal/providers.ts +0 -91
- package/src/internal/render.ts +0 -428
- package/src/internal/review-agent.ts +0 -385
- package/src/internal/review-state.ts +0 -488
- package/src/internal/review-units.ts +0 -167
- package/src/internal/run.ts +0 -397
- package/src/internal/scripted.ts +0 -108
- package/src/internal/source.ts +0 -110
- package/src/testing.ts +0 -8
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
2
|
-
import { Agent, AgentPolicy, ToolExecutionClass } from "effect-agent";
|
|
3
|
-
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
|
-
|
|
5
|
-
import { annotatePatch, ChangedFileStatus, ChangedPath } from "./diff.ts";
|
|
6
|
-
import {
|
|
7
|
-
normalizeRepoRelativePath,
|
|
8
|
-
PullRequestSource,
|
|
9
|
-
PullRequestSourceFailure,
|
|
10
|
-
ReviewInputViolation,
|
|
11
|
-
} from "./source.ts";
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// The pull-request reviewer: a bounded, read-only agent. Every tool observes
|
|
15
|
-
// the pull request through the PullRequestSource port; nothing the model can
|
|
16
|
-
// call mutates anything. Publishing the review happens OUTSIDE the agent
|
|
17
|
-
// loop, after the finding anchors have been validated against the real diff
|
|
18
|
-
// (model output is untrusted input, AGENTS.md rule 11).
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
/** The hard findings bound carried by the CodeReview schema. */
|
|
22
|
-
export const MAX_FINDINGS = 20;
|
|
23
|
-
|
|
24
|
-
/** The hard non-anchored-concerns bound carried by the CodeReview schema. */
|
|
25
|
-
export const MAX_CONCERNS = 10;
|
|
26
|
-
|
|
27
|
-
/** Annotated patches larger than this are truncated with an explicit marker. */
|
|
28
|
-
const MAX_PATCH_CHARS = 60_000;
|
|
29
|
-
|
|
30
|
-
/** One `read_file` slice never exceeds this many lines. */
|
|
31
|
-
const MAX_SLICE_LINES = 1_000;
|
|
32
|
-
const DEFAULT_SLICE_LINES = 400;
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// Tool surface.
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
export class ChangedFileSummary extends Schema.Class<ChangedFileSummary>(
|
|
39
|
-
"@effect-agent/pr-review/ChangedFileSummary",
|
|
40
|
-
)({
|
|
41
|
-
path: ChangedPath,
|
|
42
|
-
status: ChangedFileStatus,
|
|
43
|
-
additions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
44
|
-
deletions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
45
|
-
hasTextualDiff: Schema.Boolean,
|
|
46
|
-
}) {}
|
|
47
|
-
|
|
48
|
-
export class ChangedFilesView extends Schema.Class<ChangedFilesView>(
|
|
49
|
-
"@effect-agent/pr-review/ChangedFilesView",
|
|
50
|
-
)({
|
|
51
|
-
totalFiles: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
52
|
-
/** True when the pull request has more changed files than are listed here. */
|
|
53
|
-
truncated: Schema.Boolean,
|
|
54
|
-
files: Schema.Array(ChangedFileSummary).check(Schema.isMaxLength(300)),
|
|
55
|
-
}) {}
|
|
56
|
-
|
|
57
|
-
export class ListChangedFilesQuery extends Schema.Class<ListChangedFilesQuery>(
|
|
58
|
-
"@effect-agent/pr-review/ListChangedFilesQuery",
|
|
59
|
-
)({
|
|
60
|
-
/** Explicit constant keeps the zero-choice operation compatible with strict provider schemas. */
|
|
61
|
-
scope: Schema.Literal("all"),
|
|
62
|
-
}) {}
|
|
63
|
-
|
|
64
|
-
export const ListChangedFiles = Tool.make("list_changed_files", {
|
|
65
|
-
description:
|
|
66
|
-
"List every file changed by this pull request with its status, line counts, and whether a textual diff is available.",
|
|
67
|
-
parameters: ListChangedFilesQuery,
|
|
68
|
-
success: ChangedFilesView,
|
|
69
|
-
failure: PullRequestSourceFailure,
|
|
70
|
-
failureMode: "error",
|
|
71
|
-
dependencies: [PullRequestSource],
|
|
72
|
-
}).annotate(ToolExecutionClass, "readonly");
|
|
73
|
-
|
|
74
|
-
export class FileDiffQuery extends Schema.Class<FileDiffQuery>(
|
|
75
|
-
"@effect-agent/pr-review/FileDiffQuery",
|
|
76
|
-
)({
|
|
77
|
-
path: ChangedPath,
|
|
78
|
-
}) {}
|
|
79
|
-
|
|
80
|
-
export class FileDiffView extends Schema.Class<FileDiffView>(
|
|
81
|
-
"@effect-agent/pr-review/FileDiffView",
|
|
82
|
-
)({
|
|
83
|
-
path: ChangedPath,
|
|
84
|
-
status: ChangedFileStatus,
|
|
85
|
-
/**
|
|
86
|
-
* The unified diff with explicit RIGHT-side line numbers: `R<n>` marks a
|
|
87
|
-
* line present in the new file version (only those may anchor findings);
|
|
88
|
-
* `-` marks removed lines. Empty when no textual diff exists.
|
|
89
|
-
*/
|
|
90
|
-
annotatedPatch: Schema.String,
|
|
91
|
-
truncated: Schema.Boolean,
|
|
92
|
-
}) {}
|
|
93
|
-
|
|
94
|
-
// Read failures stay model-visible results ("return"), never run-killers:
|
|
95
|
-
// a model asking for an out-of-changeset path is expected untrusted-input
|
|
96
|
-
// behavior, and the fail-closed answer is a typed refusal it can correct —
|
|
97
|
-
// aborting the whole review on one bad path guess would be fragility, not
|
|
98
|
-
// security (the run stays bounded by AgentPolicy regardless).
|
|
99
|
-
export const ReadFileDiff = Tool.make("read_file_diff", {
|
|
100
|
-
description:
|
|
101
|
-
"Read the annotated unified diff of one changed file. Lines marked R<number> exist in the new version and are the only valid finding anchors.",
|
|
102
|
-
parameters: FileDiffQuery,
|
|
103
|
-
success: FileDiffView,
|
|
104
|
-
failure: Schema.Union([PullRequestSourceFailure, ReviewInputViolation]),
|
|
105
|
-
failureMode: "return",
|
|
106
|
-
dependencies: [PullRequestSource],
|
|
107
|
-
}).annotate(ToolExecutionClass, "readonly");
|
|
108
|
-
|
|
109
|
-
export class FileSliceQuery extends Schema.Class<FileSliceQuery>(
|
|
110
|
-
"@effect-agent/pr-review/FileSliceQuery",
|
|
111
|
-
)({
|
|
112
|
-
path: ChangedPath,
|
|
113
|
-
/** 1-based first line to read; defaults to 1. */
|
|
114
|
-
startLine: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
115
|
-
/** Number of lines to read; defaults to 400, capped at 1000. */
|
|
116
|
-
maxLines: Schema.optionalKey(
|
|
117
|
-
Schema.Int.check(Schema.isGreaterThan(0)).check(Schema.isLessThanOrEqualTo(MAX_SLICE_LINES)),
|
|
118
|
-
),
|
|
119
|
-
}) {}
|
|
120
|
-
|
|
121
|
-
export class FileSlice extends Schema.Class<FileSlice>("@effect-agent/pr-review/FileSlice")({
|
|
122
|
-
path: ChangedPath,
|
|
123
|
-
startLine: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
124
|
-
endLine: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
125
|
-
totalLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
126
|
-
/** Slice content with each line prefixed by its 1-based line number. */
|
|
127
|
-
content: Schema.String,
|
|
128
|
-
}) {}
|
|
129
|
-
|
|
130
|
-
export const ReadFile = Tool.make("read_file", {
|
|
131
|
-
description:
|
|
132
|
-
"Read a numbered slice of the NEW (head) version of one changed file, for context around the diff. Only files in the changeset are readable.",
|
|
133
|
-
parameters: FileSliceQuery,
|
|
134
|
-
success: FileSlice,
|
|
135
|
-
failure: Schema.Union([PullRequestSourceFailure, ReviewInputViolation]),
|
|
136
|
-
failureMode: "return",
|
|
137
|
-
dependencies: [PullRequestSource],
|
|
138
|
-
}).annotate(ToolExecutionClass, "readonly");
|
|
139
|
-
|
|
140
|
-
export const ReviewToolkit = Toolkit.make(ListChangedFiles, ReadFileDiff, ReadFile);
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* The `list_changed_files` handler, shared by the flat reviewer's toolkit and
|
|
144
|
-
* any extended toolkit built by the configuration factory.
|
|
145
|
-
*/
|
|
146
|
-
export const listChangedFilesHandler = (_query: ListChangedFilesQuery) =>
|
|
147
|
-
Effect.gen(function* () {
|
|
148
|
-
const source = yield* PullRequestSource;
|
|
149
|
-
const files = yield* source.changedFiles;
|
|
150
|
-
const metadata = yield* source.metadata;
|
|
151
|
-
return ChangedFilesView.make({
|
|
152
|
-
totalFiles: metadata.totalChangedFiles,
|
|
153
|
-
truncated: files.length < metadata.totalChangedFiles,
|
|
154
|
-
files: files.map((file) =>
|
|
155
|
-
ChangedFileSummary.make({
|
|
156
|
-
path: file.path,
|
|
157
|
-
status: file.status,
|
|
158
|
-
additions: file.additions,
|
|
159
|
-
deletions: file.deletions,
|
|
160
|
-
hasTextualDiff: file.patch !== undefined,
|
|
161
|
-
}),
|
|
162
|
-
),
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The `read_file_diff` handler, shared verbatim by the flat reviewer's
|
|
168
|
-
* toolkit and the fan-out child's toolkit (fan-out.ts).
|
|
169
|
-
*/
|
|
170
|
-
export const readFileDiffHandler = (query: FileDiffQuery) =>
|
|
171
|
-
Effect.gen(function* () {
|
|
172
|
-
const source = yield* PullRequestSource;
|
|
173
|
-
const relative = yield* normalizeRepoRelativePath(query.path);
|
|
174
|
-
const files = yield* source.changedFiles;
|
|
175
|
-
const file = files.find((candidate) => candidate.path === relative);
|
|
176
|
-
if (file === undefined) {
|
|
177
|
-
return yield* ReviewInputViolation.make({
|
|
178
|
-
input: relative,
|
|
179
|
-
reason: "Path is not part of this pull request's changeset.",
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
const annotated = file.patch === undefined ? "" : annotatePatch(file.patch);
|
|
183
|
-
const truncated = annotated.length > MAX_PATCH_CHARS;
|
|
184
|
-
return FileDiffView.make({
|
|
185
|
-
path: file.path,
|
|
186
|
-
status: file.status,
|
|
187
|
-
annotatedPatch: truncated
|
|
188
|
-
? `${annotated.slice(0, MAX_PATCH_CHARS)}\n[diff truncated]`
|
|
189
|
-
: annotated,
|
|
190
|
-
truncated,
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* The `read_file` handler, shared verbatim by the flat reviewer's toolkit
|
|
196
|
-
* and the fan-out child's toolkit (fan-out.ts).
|
|
197
|
-
*/
|
|
198
|
-
export const readFileHandler = (query: FileSliceQuery) =>
|
|
199
|
-
Effect.gen(function* () {
|
|
200
|
-
const source = yield* PullRequestSource;
|
|
201
|
-
const relative = yield* normalizeRepoRelativePath(query.path);
|
|
202
|
-
const content = yield* source.readFile(relative);
|
|
203
|
-
const lines = content.split("\n");
|
|
204
|
-
const startLine = query.startLine ?? 1;
|
|
205
|
-
const maxLines = query.maxLines ?? DEFAULT_SLICE_LINES;
|
|
206
|
-
if (startLine > lines.length) {
|
|
207
|
-
return yield* ReviewInputViolation.make({
|
|
208
|
-
input: `${relative}:${startLine}`,
|
|
209
|
-
reason: `startLine is beyond the end of the file (${lines.length} lines).`,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
const slice = lines.slice(startLine - 1, startLine - 1 + maxLines);
|
|
213
|
-
const endLine = startLine + slice.length - 1;
|
|
214
|
-
return FileSlice.make({
|
|
215
|
-
path: relative,
|
|
216
|
-
startLine,
|
|
217
|
-
endLine,
|
|
218
|
-
totalLines: lines.length,
|
|
219
|
-
content: slice
|
|
220
|
-
.map((text, index) => `${String(startLine + index).padStart(5)} ${text}`)
|
|
221
|
-
.join("\n"),
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
export const ReviewToolkitLayer = ReviewToolkit.toLayer({
|
|
226
|
-
list_changed_files: listChangedFilesHandler,
|
|
227
|
-
read_file_diff: readFileDiffHandler,
|
|
228
|
-
read_file: readFileHandler,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
// Mission input and review output contracts.
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
export class ReviewMission extends Schema.Class<ReviewMission>(
|
|
236
|
-
"@effect-agent/pr-review/ReviewMission",
|
|
237
|
-
)({
|
|
238
|
-
repository: Schema.NonEmptyString.check(Schema.isMaxLength(200)),
|
|
239
|
-
number: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
240
|
-
title: Schema.String.check(Schema.isMaxLength(400)),
|
|
241
|
-
body: Schema.String.check(Schema.isMaxLength(20_000)),
|
|
242
|
-
baseRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
243
|
-
headRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
244
|
-
changedFileCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
245
|
-
}) {}
|
|
246
|
-
|
|
247
|
-
export const FindingSeverity = Schema.Literals(["blocking", "important", "nit"]);
|
|
248
|
-
export type FindingSeverity = typeof FindingSeverity.Type;
|
|
249
|
-
|
|
250
|
-
export class ReviewFinding extends Schema.Class<ReviewFinding>(
|
|
251
|
-
"@effect-agent/pr-review/ReviewFinding",
|
|
252
|
-
)({
|
|
253
|
-
path: ChangedPath,
|
|
254
|
-
/** 1-based line numbers in the NEW file version; must appear in the diff. */
|
|
255
|
-
startLine: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
256
|
-
endLine: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
257
|
-
severity: FindingSeverity,
|
|
258
|
-
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
259
|
-
body: Schema.NonEmptyString.check(Schema.isMaxLength(2_000)),
|
|
260
|
-
/** Replacement for exactly lines startLine..endLine; omit when unsure. */
|
|
261
|
-
suggestion: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2_000))),
|
|
262
|
-
}) {}
|
|
263
|
-
|
|
264
|
-
export const ReviewVerdict = Schema.Literals(["approve", "comment", "request-changes"]);
|
|
265
|
-
export type ReviewVerdict = typeof ReviewVerdict.Type;
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* A concern with no diff line to anchor to: a missing deletion or cleanup,
|
|
269
|
-
* rollout or migration sequencing, a coverage gap the diff implies but does
|
|
270
|
-
* not add, or a scope question only the author can answer. Rendered as a
|
|
271
|
-
* review-body section — never as an inline comment, so it needs no anchor and
|
|
272
|
-
* is never demoted.
|
|
273
|
-
*/
|
|
274
|
-
export class ReviewConcern extends Schema.Class<ReviewConcern>(
|
|
275
|
-
"@effect-agent/pr-review/ReviewConcern",
|
|
276
|
-
)({
|
|
277
|
-
severity: FindingSeverity,
|
|
278
|
-
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
279
|
-
body: Schema.NonEmptyString.check(Schema.isMaxLength(2_000)),
|
|
280
|
-
}) {}
|
|
281
|
-
|
|
282
|
-
export class CodeReview extends Schema.Class<CodeReview>("@effect-agent/pr-review/CodeReview")({
|
|
283
|
-
summary: Schema.NonEmptyString.check(Schema.isMaxLength(4_000)),
|
|
284
|
-
verdict: ReviewVerdict,
|
|
285
|
-
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(MAX_FINDINGS)),
|
|
286
|
-
/** Non-anchorable concerns; absent when the review raises none. */
|
|
287
|
-
concerns: Schema.optionalKey(Schema.Array(ReviewConcern).check(Schema.isMaxLength(MAX_CONCERNS))),
|
|
288
|
-
}) {}
|
|
289
|
-
|
|
290
|
-
// ---------------------------------------------------------------------------
|
|
291
|
-
// Instructions. Live models diverge wherever the contract is implicit, so the
|
|
292
|
-
// exact JSON shape, the anchor rule, and the suggestion rule are all spelled
|
|
293
|
-
// out with types. Consumer guidance is injected BETWEEN the mission framing
|
|
294
|
-
// and the procedure — it can widen what the reviewer pays attention to, but
|
|
295
|
-
// the machine contract (anchor rule, JSON shape, bounds) is always appended
|
|
296
|
-
// by this builder and cannot be edited out.
|
|
297
|
-
// ---------------------------------------------------------------------------
|
|
298
|
-
|
|
299
|
-
/** Consumer-supplied domain guidance: static lines or a function of the mission. */
|
|
300
|
-
export type ReviewGuidance =
|
|
301
|
-
| string
|
|
302
|
-
| ReadonlyArray<string>
|
|
303
|
-
| ((mission: ReviewMission) => string | ReadonlyArray<string>);
|
|
304
|
-
|
|
305
|
-
export const resolveGuidance = (
|
|
306
|
-
guidance: ReviewGuidance | undefined,
|
|
307
|
-
mission: ReviewMission,
|
|
308
|
-
): ReadonlyArray<string> => {
|
|
309
|
-
if (guidance === undefined) return [];
|
|
310
|
-
const value = typeof guidance === "function" ? guidance(mission) : guidance;
|
|
311
|
-
const lines = typeof value === "string" ? [value] : value;
|
|
312
|
-
return lines.filter((line) => line.length > 0);
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
export interface ReviewInstructionOptions {
|
|
316
|
-
readonly guidance?: ReviewGuidance | undefined;
|
|
317
|
-
/** Advertised findings bound; clamped to the CodeReview schema cap. */
|
|
318
|
-
readonly maxFindings?: number | undefined;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** Clamp a configured findings bound into the schema-supported range. */
|
|
322
|
-
export const clampMaxFindings = (maxFindings: number | undefined): number =>
|
|
323
|
-
maxFindings === undefined
|
|
324
|
-
? MAX_FINDINGS
|
|
325
|
-
: Math.min(MAX_FINDINGS, Math.max(1, Math.trunc(maxFindings)));
|
|
326
|
-
|
|
327
|
-
/** Build the flat reviewer's instructions with optional consumer guidance. */
|
|
328
|
-
export const makeReviewInstructions =
|
|
329
|
-
(options: ReviewInstructionOptions = {}) =>
|
|
330
|
-
(mission: ReviewMission): string => {
|
|
331
|
-
const maxFindings = clampMaxFindings(options.maxFindings);
|
|
332
|
-
return [
|
|
333
|
-
`You are a senior code reviewer for pull request #${mission.number} ("${mission.title}") in ${mission.repository}, merging ${mission.headRef} into ${mission.baseRef}. It changes ${mission.changedFileCount} file(s).`,
|
|
334
|
-
mission.body.length > 0
|
|
335
|
-
? `Author description:\n${mission.body}`
|
|
336
|
-
: "The author provided no description.",
|
|
337
|
-
...resolveGuidance(options.guidance, mission),
|
|
338
|
-
"Work in this order:",
|
|
339
|
-
"1. Call list_changed_files once to see the changeset.",
|
|
340
|
-
"2. Call read_file_diff for every file you review. In its output, only lines marked R<number> exist in the new version; those numbers are the only valid values for startLine and endLine. Never anchor a finding to a removed (-) line.",
|
|
341
|
-
"3. Call read_file when you need surrounding context the diff does not show. ONLY files in the changeset are readable: a request for any other path (an import, a neighbor, a config) returns a failed result — do not retry it; reason from the diff instead and note the gap honestly in your summary when it matters.",
|
|
342
|
-
"4. Review for real defects first: correctness, security, concurrency, resource leaks, error handling, API misuse. Style nits are least important. Do not praise; do not restate the diff.",
|
|
343
|
-
"When the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. The usual tell is a loose assertion standing where an exact one belongs — >= or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed.",
|
|
344
|
-
"Go shallow only when the diff has no behavioral surface at all: doc typos, formatting, lockfile or generated-code regeneration, a mechanical rename. Line count is not the signal — a one-line change to auth, money, SQL, a comparison operator, or a config default is not trivial.",
|
|
345
|
-
"Drop bloat-shaped findings before reporting: defensive checks for cases that cannot happen, abstractions used once, comments restating obvious code, tests asserting tautologies, just-in-case guards. A finding must be sound, correct, and worth acting on; prefer an explicit keep over an invented finding.",
|
|
346
|
-
'5. After collecting anchored findings, deliberately scan for concerns with NO line to point at: deletion or cleanup plans for code the diff replaces, rollout or migration sequencing, coverage gaps the diff implies but does not add, scope questions only the author can answer. Report each as a "concern", never as a finding with an invented anchor; report none when none exist.',
|
|
347
|
-
'6. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {"summary": <string, 1-3 paragraphs of overall assessment>, "verdict": <"approve" | "comment" | "request-changes">, "findings": [{"path": <string, a changed file path>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement text for exactly lines startLine..endLine, ready to commit>}], "concerns": <array, OPTIONAL: [{"severity": <"blocking" | "important" | "nit">, "title": <string, <= 120 chars>, "body": <string>}], only for step-5 concerns with no valid anchor — never duplicate a finding here>}.',
|
|
348
|
-
`Report at most ${maxFindings} findings and at most ${MAX_CONCERNS} concerns; prefer the most important ones. An empty findings array with verdict "approve" is a valid review. Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement for every line in the range and nothing else.`,
|
|
349
|
-
'Use verdict "request-changes" only when at least one finding or concern is "blocking". Line anchors you invent will be discarded, so copy R-numbers from read_file_diff output.',
|
|
350
|
-
].join("\n");
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
/** The default flat-reviewer instructions: no guidance, schema-cap findings. */
|
|
354
|
-
export const reviewInstructions = makeReviewInstructions();
|
|
355
|
-
|
|
356
|
-
/** The default flat-reviewer execution bounds. */
|
|
357
|
-
export const defaultReviewPolicy = AgentPolicy.make({
|
|
358
|
-
maxTurns: 12,
|
|
359
|
-
maxToolCalls: 24,
|
|
360
|
-
maxDuration: "8 minutes",
|
|
361
|
-
toolConcurrency: 2,
|
|
362
|
-
tokenBudget: 300_000,
|
|
363
|
-
// Keep enough output/summary headroom for the 200k-class provider window;
|
|
364
|
-
// tool-heavy histories prune before the engine spends a summarization call.
|
|
365
|
-
contextTokenLimit: 150_000,
|
|
366
|
-
// Budget soft landing (RUN-018): an exhausted reviewer returns its partial
|
|
367
|
-
// review on one final tool-free turn instead of failing the whole run.
|
|
368
|
-
onExhaustion: "final-answer",
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// ---------------------------------------------------------------------------
|
|
372
|
-
// Agent Definition: model-agnostic (D-027); bindings are created by callers
|
|
373
|
-
// or by the configuration factory.
|
|
374
|
-
// ---------------------------------------------------------------------------
|
|
375
|
-
|
|
376
|
-
export const PullRequestReviewer = Agent.define("pr-reviewer", {
|
|
377
|
-
input: ReviewMission,
|
|
378
|
-
output: CodeReview,
|
|
379
|
-
instructions: reviewInstructions,
|
|
380
|
-
toolkit: ReviewToolkit,
|
|
381
|
-
policy: defaultReviewPolicy,
|
|
382
|
-
description:
|
|
383
|
-
"Review one pull request read-only: list the changeset, read annotated diffs and head-file context, and return a structured, line-anchored code review.",
|
|
384
|
-
metadata: { deploymentClass: "E", surface: "read-only" },
|
|
385
|
-
});
|