@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.22
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/README.md +82 -26
- package/dist/action.d.mts +12 -6
- package/dist/action.mjs +17 -27
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-CQFA-o0v.d.mts → fan-out-n-00ppWr.d.mts} +308 -223
- package/dist/{github-DnenG3be.mjs → github-BgtP7Rdv.mjs} +652 -196
- package/dist/github-BgtP7Rdv.mjs.map +1 -0
- package/dist/index.d.mts +27 -54
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-BguZK4B_.mjs} +408 -134
- package/dist/providers-BguZK4B_.mjs.map +1 -0
- package/dist/testing.d.mts +10 -47
- package/dist/testing.mjs +29 -55
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -39
- package/src/index.ts +1 -0
- package/src/internal/anchors.ts +20 -0
- package/src/internal/coverage.ts +628 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +560 -255
- package/src/internal/profiles.ts +12 -0
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +51 -25
- package/src/internal/review-agent.ts +74 -25
- package/src/internal/review-state.ts +7 -5
- package/src/internal/review-units.ts +326 -32
- package/src/internal/run.ts +48 -41
- package/dist/github-DnenG3be.mjs.map +0 -1
- package/dist/providers-BE83_Tfo.mjs.map +0 -1
package/src/internal/fan-out.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Layer, Schema } from "effect";
|
|
2
2
|
import {
|
|
3
3
|
Agent,
|
|
4
4
|
AgentPolicy,
|
|
@@ -11,16 +11,14 @@ import {
|
|
|
11
11
|
} from "effect-agent";
|
|
12
12
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { anchorViolation } from "./anchors.ts";
|
|
15
|
+
import { ChangedFileStatus, ChangedPath } from "./diff.ts";
|
|
15
16
|
import {
|
|
16
17
|
clampMaxFindings,
|
|
17
18
|
CodeReview,
|
|
18
|
-
|
|
19
|
+
fileReviewEvidenceChunks,
|
|
20
|
+
MAX_PATCH_CHARS,
|
|
19
21
|
MAX_WALKTHROUGH_SUMMARY_CHARS,
|
|
20
|
-
ReadFile,
|
|
21
|
-
ReadFileDiff,
|
|
22
|
-
readFileDiffHandler,
|
|
23
|
-
readFileHandler,
|
|
24
22
|
REVIEW_TOOL_RESULT_MAX_BYTES,
|
|
25
23
|
ReviewConcern,
|
|
26
24
|
ReviewFinding,
|
|
@@ -29,85 +27,248 @@ import {
|
|
|
29
27
|
} from "./review-agent.ts";
|
|
30
28
|
import {
|
|
31
29
|
MAX_REVIEW_UNITS,
|
|
30
|
+
MAX_UNIT_EVIDENCE_SHARDS,
|
|
32
31
|
MAX_UNIT_FILES,
|
|
32
|
+
findingAnchorInUnitEvidence,
|
|
33
33
|
planReviewUnits,
|
|
34
|
+
ReviewEvidenceShardId,
|
|
35
|
+
ReviewPassId,
|
|
36
|
+
ReviewRiskCategory,
|
|
34
37
|
ReviewUnitId,
|
|
35
38
|
ReviewUnitPlan,
|
|
36
39
|
} from "./review-units.ts";
|
|
37
40
|
import { PullRequestSource, PullRequestSourceFailure } from "./source.ts";
|
|
38
41
|
|
|
39
42
|
// ---------------------------------------------------------------------------
|
|
40
|
-
// The fan-out reviewer
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
43
|
+
// The assured fan-out reviewer is a bounded, deterministic three-stage
|
|
44
|
+
// pipeline driven through attached S1 children:
|
|
45
|
+
//
|
|
46
|
+
// host plan -> independent discovery passes -> independent verification
|
|
47
|
+
//
|
|
48
|
+
// Host code owns partitioning, risk classification, bounded evidence, exact
|
|
49
|
+
// pass settlement, candidate provenance, and the final confirmed-candidate
|
|
50
|
+
// fold. The coordinator only schedules the declared work and writes prose.
|
|
48
51
|
// ---------------------------------------------------------------------------
|
|
49
52
|
|
|
50
|
-
/** One
|
|
51
|
-
export const MAX_CHILD_FINDINGS =
|
|
53
|
+
/** One discovery pass returns at most this many anchored candidates. */
|
|
54
|
+
export const MAX_CHILD_FINDINGS = 6;
|
|
52
55
|
|
|
53
|
-
/** One
|
|
56
|
+
/** One discovery pass returns at most this many non-anchored candidates. */
|
|
54
57
|
export const MAX_CHILD_CONCERNS = 3;
|
|
55
58
|
|
|
59
|
+
/** Every unit receives independent general and specialist discovery passes. */
|
|
60
|
+
export const MAX_UNIT_CANDIDATES = (MAX_CHILD_FINDINGS + MAX_CHILD_CONCERNS) * 2;
|
|
61
|
+
|
|
62
|
+
/** General + specialist discovery for every unit, then one verifier per unit. */
|
|
63
|
+
export const MAX_REVIEW_CHILDREN = MAX_REVIEW_UNITS * 3;
|
|
64
|
+
|
|
65
|
+
/** Structural minimum for a child that exposes no tools. */
|
|
66
|
+
export const MAX_FILE_REVIEW_TOOL_CALLS = 1;
|
|
67
|
+
|
|
68
|
+
export const ReviewWorkPhase = Schema.Literals(["discovery", "verification"]);
|
|
69
|
+
export type ReviewWorkPhase = typeof ReviewWorkPhase.Type;
|
|
70
|
+
|
|
71
|
+
export const ReviewWorkPerspective = Schema.Literals([
|
|
72
|
+
"general",
|
|
73
|
+
"risk-specialist",
|
|
74
|
+
"candidate-verification",
|
|
75
|
+
]);
|
|
76
|
+
export type ReviewWorkPerspective = typeof ReviewWorkPerspective.Type;
|
|
77
|
+
|
|
78
|
+
export const ReviewCandidateId = Schema.NonEmptyString.check(Schema.isMaxLength(96));
|
|
79
|
+
|
|
80
|
+
export class FindingCandidate extends Schema.TaggedClass<FindingCandidate>()("FindingCandidate", {
|
|
81
|
+
candidateId: ReviewCandidateId,
|
|
82
|
+
workId: ReviewPassId,
|
|
83
|
+
unitId: ReviewUnitId,
|
|
84
|
+
finding: ReviewFinding,
|
|
85
|
+
evidencePaths: Schema.Array(ChangedPath)
|
|
86
|
+
.check(Schema.isMinLength(1))
|
|
87
|
+
.check(Schema.isMaxLength(1)),
|
|
88
|
+
}) {}
|
|
89
|
+
|
|
90
|
+
export class ConcernCandidate extends Schema.TaggedClass<ConcernCandidate>()("ConcernCandidate", {
|
|
91
|
+
candidateId: ReviewCandidateId,
|
|
92
|
+
workId: ReviewPassId,
|
|
93
|
+
unitId: ReviewUnitId,
|
|
94
|
+
concern: ReviewConcern,
|
|
95
|
+
evidencePaths: Schema.Array(ChangedPath)
|
|
96
|
+
.check(Schema.isMinLength(1))
|
|
97
|
+
.check(Schema.isMaxLength(3)),
|
|
98
|
+
}) {}
|
|
99
|
+
|
|
100
|
+
export const ReviewCandidate = Schema.Union([FindingCandidate, ConcernCandidate]);
|
|
101
|
+
export type ReviewCandidate = typeof ReviewCandidate.Type;
|
|
102
|
+
|
|
103
|
+
/** Deterministic host equivalence for claims repeated across discovery passes. */
|
|
104
|
+
export const reviewCandidateSubjectKey = (candidate: ReviewCandidate): string =>
|
|
105
|
+
candidate._tag === "FindingCandidate"
|
|
106
|
+
? `finding:${JSON.stringify(Schema.encodeSync(ReviewFinding)(candidate.finding))}`
|
|
107
|
+
: `concern:${JSON.stringify(Schema.encodeSync(ReviewConcern)(candidate.concern))}`;
|
|
108
|
+
|
|
109
|
+
export class CandidateAssessment extends Schema.Class<CandidateAssessment>(
|
|
110
|
+
"@effect-agent/pr-review/CandidateAssessment",
|
|
111
|
+
)({
|
|
112
|
+
candidateId: ReviewCandidateId,
|
|
113
|
+
disposition: Schema.Literals(["confirmed", "rejected"]),
|
|
114
|
+
/**
|
|
115
|
+
* Exact suggestion settlement: required when the candidate finding carries
|
|
116
|
+
* a suggestion, forbidden otherwise. Untrusted child output cannot publish
|
|
117
|
+
* a GitHub replacement block by prompt compliance alone — the host keeps a
|
|
118
|
+
* confirmed finding's suggestion only on an exact "committable" settlement.
|
|
119
|
+
*/
|
|
120
|
+
suggestion: Schema.optionalKey(
|
|
121
|
+
Schema.Literals(["committable", "not-committable"]).annotate({
|
|
122
|
+
description:
|
|
123
|
+
'Required exactly when the candidate finding carries a suggestion: "committable" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else. Forbidden for candidates without a suggestion.',
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
|
|
127
|
+
}) {}
|
|
128
|
+
|
|
56
129
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
130
|
+
* Exact suggestion settlement shape: a carried suggestion must be settled and
|
|
131
|
+
* nothing else may be. Enforced identically by the live delegation projection
|
|
132
|
+
* and the independent host coverage fold.
|
|
59
133
|
*/
|
|
60
|
-
export const
|
|
134
|
+
export const assessmentSettlesSuggestionExactly = (
|
|
135
|
+
assessment: CandidateAssessment,
|
|
136
|
+
candidate: ReviewCandidate,
|
|
137
|
+
): boolean =>
|
|
138
|
+
candidate._tag === "FindingCandidate" && candidate.finding.suggestion !== undefined
|
|
139
|
+
? assessment.suggestion !== undefined
|
|
140
|
+
: assessment.suggestion === undefined;
|
|
61
141
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Fail-closed publication of a confirmed finding: only an exact "committable"
|
|
144
|
+
* settlement keeps the suggestion; anything else publishes the finding with
|
|
145
|
+
* the suggestion stripped so unverified text can never become a one-click
|
|
146
|
+
* GitHub replacement block.
|
|
147
|
+
*/
|
|
148
|
+
export const confirmedFindingForPublication = (
|
|
149
|
+
assessment: CandidateAssessment,
|
|
150
|
+
candidate: FindingCandidate,
|
|
151
|
+
): ReviewFinding => {
|
|
152
|
+
if (candidate.finding.suggestion === undefined || assessment.suggestion === "committable") {
|
|
153
|
+
return candidate.finding;
|
|
154
|
+
}
|
|
155
|
+
const { suggestion: _stripped, ...finding } = candidate.finding;
|
|
156
|
+
return ReviewFinding.make(finding);
|
|
157
|
+
};
|
|
70
158
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Concern candidates need explicit paths internally to bind the claim to
|
|
161
|
+
* scheduled evidence. The verifier receives the complete bounded unit so it
|
|
162
|
+
* can use neighboring evidence to falsify the claim. The public ReviewConcern
|
|
163
|
+
* remains path-free after the host confirms and projects it.
|
|
164
|
+
*/
|
|
165
|
+
export class DiscoveredConcern extends Schema.Class<DiscoveredConcern>(
|
|
166
|
+
"@effect-agent/pr-review/DiscoveredConcern",
|
|
167
|
+
)({
|
|
168
|
+
concern: ReviewConcern,
|
|
169
|
+
evidencePaths: Schema.Array(ChangedPath)
|
|
170
|
+
.check(Schema.isMinLength(1))
|
|
171
|
+
.check(Schema.isMaxLength(3)),
|
|
172
|
+
}) {}
|
|
75
173
|
|
|
76
174
|
const UnitPaths = Schema.Array(ChangedPath)
|
|
77
175
|
.check(Schema.isMinLength(1))
|
|
78
176
|
.check(Schema.isMaxLength(MAX_UNIT_FILES));
|
|
79
177
|
|
|
80
|
-
|
|
178
|
+
const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
|
|
179
|
+
const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES));
|
|
180
|
+
const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId)
|
|
181
|
+
.check(Schema.isMinLength(1))
|
|
182
|
+
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS));
|
|
183
|
+
|
|
184
|
+
/** Strict-object coordinator request for either discovery or verification. */
|
|
185
|
+
export class FileReviewRequest extends Schema.Class<FileReviewRequest>(
|
|
186
|
+
"@effect-agent/pr-review/FileReviewRequest",
|
|
187
|
+
)({
|
|
188
|
+
phase: ReviewWorkPhase,
|
|
189
|
+
workId: ReviewPassId,
|
|
190
|
+
unitId: ReviewUnitId,
|
|
191
|
+
paths: UnitPaths,
|
|
192
|
+
evidenceShardIds: EvidenceShardIds,
|
|
193
|
+
perspective: ReviewWorkPerspective,
|
|
194
|
+
riskCategories: RiskCategories,
|
|
195
|
+
/** Empty for discovery; the exact discovered set for unit verification. */
|
|
196
|
+
candidates: Candidates,
|
|
197
|
+
}) {}
|
|
198
|
+
|
|
199
|
+
/** One complete host-selected evidence shard supplied to a review child. */
|
|
200
|
+
export class FileReviewEvidence extends Schema.Class<FileReviewEvidence>(
|
|
201
|
+
"@effect-agent/pr-review/FileReviewEvidence",
|
|
202
|
+
)({
|
|
203
|
+
shardId: ReviewEvidenceShardId,
|
|
204
|
+
path: ChangedPath,
|
|
205
|
+
status: ChangedFileStatus,
|
|
206
|
+
reviewMode: Schema.Literals(["diff", "content", "unavailable"]),
|
|
207
|
+
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
208
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
209
|
+
annotatedPatch: Schema.String.check(Schema.isMaxLength(MAX_PATCH_CHARS)),
|
|
210
|
+
}) {}
|
|
211
|
+
|
|
212
|
+
/** Host-prepared child input with complete bounded diff/content evidence. */
|
|
81
213
|
export class FileReviewBrief extends Schema.Class<FileReviewBrief>(
|
|
82
214
|
"@effect-agent/pr-review/FileReviewBrief",
|
|
83
215
|
)({
|
|
216
|
+
phase: ReviewWorkPhase,
|
|
217
|
+
workId: ReviewPassId,
|
|
84
218
|
unitId: ReviewUnitId,
|
|
85
219
|
paths: UnitPaths,
|
|
86
|
-
|
|
220
|
+
evidenceShardIds: EvidenceShardIds,
|
|
221
|
+
perspective: ReviewWorkPerspective,
|
|
222
|
+
riskCategories: RiskCategories,
|
|
223
|
+
candidates: Candidates,
|
|
224
|
+
evidence: Schema.Array(FileReviewEvidence)
|
|
225
|
+
.check(Schema.isMinLength(1))
|
|
226
|
+
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS)),
|
|
87
227
|
}) {}
|
|
88
228
|
|
|
89
|
-
/**
|
|
229
|
+
/** Child output; phase-inapplicable collections must be empty. */
|
|
90
230
|
export class FileReviewReport extends Schema.Class<FileReviewReport>(
|
|
91
231
|
"@effect-agent/pr-review/FileReviewReport",
|
|
92
232
|
)({
|
|
233
|
+
phase: ReviewWorkPhase,
|
|
234
|
+
workId: ReviewPassId,
|
|
93
235
|
unitId: ReviewUnitId,
|
|
94
236
|
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(MAX_CHILD_FINDINGS)),
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
),
|
|
99
|
-
/** One-sentence per-file change summaries for the merged walkthrough. */
|
|
100
|
-
fileSummaries: Schema.optionalKey(
|
|
101
|
-
Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
102
|
-
),
|
|
237
|
+
concerns: Schema.Array(DiscoveredConcern).check(Schema.isMaxLength(MAX_CHILD_CONCERNS)),
|
|
238
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
239
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES)),
|
|
103
240
|
}) {}
|
|
104
241
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
242
|
+
/** Bounded coordinator-visible result with host-assigned candidate IDs. */
|
|
243
|
+
export class FileReviewUnitResult extends Schema.Class<FileReviewUnitResult>(
|
|
244
|
+
"@effect-agent/pr-review/FileReviewUnitResult",
|
|
245
|
+
)({
|
|
246
|
+
phase: ReviewWorkPhase,
|
|
247
|
+
workId: ReviewPassId,
|
|
248
|
+
unitId: ReviewUnitId,
|
|
249
|
+
candidates: Candidates,
|
|
250
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
251
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES)),
|
|
252
|
+
}) {}
|
|
253
|
+
|
|
254
|
+
export class FileReviewUnitFailed extends Schema.TaggedError<FileReviewUnitFailed>()(
|
|
255
|
+
"FileReviewUnitFailed",
|
|
256
|
+
{
|
|
257
|
+
childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
258
|
+
message: Schema.String.check(Schema.isMaxLength(400)),
|
|
259
|
+
},
|
|
260
|
+
) {}
|
|
261
|
+
|
|
262
|
+
export class FileReviewWorkRejected extends Schema.TaggedError<FileReviewWorkRejected>()(
|
|
263
|
+
"FileReviewWorkRejected",
|
|
264
|
+
{
|
|
265
|
+
workId: ReviewPassId,
|
|
266
|
+
reason: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
|
|
267
|
+
},
|
|
268
|
+
) {}
|
|
269
|
+
|
|
270
|
+
export const FileReviewFailure = Schema.Union([FileReviewUnitFailed, FileReviewWorkRejected]);
|
|
271
|
+
|
|
111
272
|
export interface FanOutInstructionOptions {
|
|
112
273
|
readonly guidance?: string | ReadonlyArray<string> | undefined;
|
|
113
274
|
}
|
|
@@ -120,115 +281,78 @@ const staticGuidanceLines = (
|
|
|
120
281
|
return lines.filter((line) => line.length > 0);
|
|
121
282
|
};
|
|
122
283
|
|
|
123
|
-
|
|
284
|
+
const evidenceInstructions = [
|
|
285
|
+
"The host placed complete bounded review evidence shards in the input evidence array. Treat every shard as required input; ordinal/total identifies multi-shard paths.",
|
|
286
|
+
"You have no tools and cannot roam outside this evidence. If it is insufficient for a candidate, reject or omit that candidate rather than guessing.",
|
|
287
|
+
"A diff marks new-version anchors as R<number>; only those lines may anchor findings. B/H content evidence is non-anchorable.",
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
/** Discovery and verification instructions share one child definition. */
|
|
124
291
|
export const makeFileReviewerInstructions =
|
|
125
292
|
(options: FanOutInstructionOptions = {}) =>
|
|
126
|
-
(brief: FileReviewBrief): string =>
|
|
127
|
-
[
|
|
128
|
-
`You are
|
|
293
|
+
(brief: FileReviewBrief): string => {
|
|
294
|
+
const common = [
|
|
295
|
+
`You are an attached review worker for ${brief.workId} in host-planned unit ${brief.unitId}: ${brief.paths.join(", ")}.`,
|
|
129
296
|
...staticGuidanceLines(options.guidance),
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
297
|
+
...evidenceInstructions,
|
|
298
|
+
];
|
|
299
|
+
if (brief.phase === "verification") {
|
|
300
|
+
return [
|
|
301
|
+
...common,
|
|
302
|
+
"Independently verify every candidate in the input. You did not receive another reviewer's transcript or reasoning; use only the candidate claim and bounded evidence.",
|
|
303
|
+
"The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
|
|
304
|
+
"For each candidate, try to falsify it first. Confirm only when the cited behavior is supported and actionable. Reject unsupported, speculative, duplicate, or non-actionable candidates.",
|
|
305
|
+
'Return ONLY JSON with phase "verification", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {"candidateId": <exact id>, "disposition": <"confirmed" | "rejected">, "suggestion": <"committable" | "not-committable", present exactly when the candidate finding carries a suggestion>, "rationale": <bounded evidence-based reason>}. Never add or omit an id.',
|
|
306
|
+
'Settle every carried suggestion independently of the claim: answer "committable" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else — it compiles in context and preserves the finding\'s intent, never prose describing a change. Otherwise answer "not-committable"; the host then publishes the confirmed finding without its suggestion. Omit the assessment "suggestion" field for candidates without one.',
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
309
|
+
const focus =
|
|
310
|
+
brief.perspective === "risk-specialist"
|
|
311
|
+
? brief.riskCategories.length > 0
|
|
312
|
+
? `This is a fresh specialist discovery pass. Concentrate on these host-classified risks without relying on another pass: ${brief.riskCategories.join(", ")}.`
|
|
313
|
+
: "This is a fresh specialist discovery pass. The host found no keyword-classified category, so independently scrutinize authentication/authorization, security boundaries, durability, concurrency, credentials, and external side effects rather than treating classification silence as low risk."
|
|
314
|
+
: "This is the general discovery pass. Review broadly for correctness, security, concurrency, resource, API, and error-handling defects.";
|
|
315
|
+
return [
|
|
316
|
+
...common,
|
|
317
|
+
focus,
|
|
318
|
+
"The discovery evidence array contains every complete shard in the unit. Review every entry and every shard of a multi-shard path. A later independent verifier, not you, decides which candidates publish.",
|
|
319
|
+
"When a non-anchored concern depends on one or more unit files, list 1-3 exact evidencePaths to bind the claim to scheduled evidence.",
|
|
320
|
+
`Return ONLY JSON with phase "discovery", the exact workId/unitId, up to ${MAX_CHILD_FINDINGS} findings, up to ${MAX_CHILD_CONCERNS} concerns shaped as {concern, evidencePaths}, one factual file summary per path (<= ${MAX_WALKTHROUGH_SUMMARY_CHARS} chars), and an empty assessments array. Empty candidate arrays are valid; do not invent defects.`,
|
|
321
|
+
'Each finding is {"path": <a unit file path>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "category": <OPTIONAL problem-kind label>, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement source code for exactly lines startLine..endLine, ready to commit>}.',
|
|
322
|
+
'Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement source for every line in the range and nothing else — never prose describing the change, which belongs in "body".',
|
|
139
323
|
].join("\n");
|
|
324
|
+
};
|
|
140
325
|
|
|
141
326
|
export const fileReviewerInstructions = makeFileReviewerInstructions();
|
|
142
327
|
|
|
143
|
-
|
|
328
|
+
export const FileReviewToolkit = Toolkit.empty;
|
|
329
|
+
|
|
330
|
+
/** Compatibility export: the evidence-only child has no handler requirements. */
|
|
331
|
+
export const FileReviewToolkitLayer = Layer.empty;
|
|
332
|
+
|
|
144
333
|
export const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
145
|
-
maxTurns:
|
|
334
|
+
maxTurns: 6,
|
|
146
335
|
maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
|
|
147
336
|
maxDuration: "6 minutes",
|
|
148
337
|
toolConcurrency: 2,
|
|
149
|
-
|
|
150
|
-
// model-visible results, and one parallel batch of out-of-unit probes must
|
|
151
|
-
// not kill the child before it has seen a single refusal.
|
|
152
|
-
repeatedFailureLimit: 12,
|
|
338
|
+
repeatedFailureLimit: 6,
|
|
153
339
|
tokenBudget: 200_000,
|
|
154
|
-
// Bound one live prompt independently from cumulative usage. The engine
|
|
155
|
-
// prunes old diff/file results before paying for a summary.
|
|
156
340
|
contextTokenLimit: 150_000,
|
|
157
341
|
toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// still emit schema-valid findings — laundering budget exhaustion into
|
|
161
|
-
// "reviewed". Until host-owned evidence proves every mandatory
|
|
162
|
-
// read_file_diff completed, an exhausted child fails typed and its unit
|
|
163
|
-
// stays honestly unreviewed (containment turns that into result data
|
|
164
|
-
// without failing the run).
|
|
342
|
+
// Discovery or verification that exhausts is unsettled work, never a
|
|
343
|
+
// schema-valid partial that can contribute to a green assurance claim.
|
|
165
344
|
onExhaustion: "fail",
|
|
166
345
|
});
|
|
167
346
|
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// The delegation: one Effect AI Tool per review unit, with explicit
|
|
170
|
-
// projections and finite bounds (SUB-009). `projectResult` is the
|
|
171
|
-
// declassification boundary — the parent sees the child's bounded findings,
|
|
172
|
-
// never its transcript or the diffs it read.
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
/** The model-decoded delegation parameters: which unit to review. */
|
|
176
|
-
export class FileReviewRequest extends Schema.Class<FileReviewRequest>(
|
|
177
|
-
"@effect-agent/pr-review/FileReviewRequest",
|
|
178
|
-
)({
|
|
179
|
-
unitId: ReviewUnitId,
|
|
180
|
-
paths: UnitPaths,
|
|
181
|
-
}) {}
|
|
182
|
-
|
|
183
|
-
/** The bounded parent-visible result of one delegated unit review. */
|
|
184
|
-
export class FileReviewUnitResult extends Schema.Class<FileReviewUnitResult>(
|
|
185
|
-
"@effect-agent/pr-review/FileReviewUnitResult",
|
|
186
|
-
)({
|
|
187
|
-
unitId: ReviewUnitId,
|
|
188
|
-
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(MAX_CHILD_FINDINGS)),
|
|
189
|
-
/** Unit-scoped concerns with no diff line to anchor to. */
|
|
190
|
-
concerns: Schema.optionalKey(
|
|
191
|
-
Schema.Array(ReviewConcern).check(Schema.isMaxLength(MAX_CHILD_CONCERNS)),
|
|
192
|
-
),
|
|
193
|
-
/** One-sentence per-file change summaries for the merged walkthrough. */
|
|
194
|
-
fileSummaries: Schema.optionalKey(
|
|
195
|
-
Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
196
|
-
),
|
|
197
|
-
}) {}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* One unit's review failed: the child Run ended in a typed failure (policy
|
|
201
|
-
* bound, output violation, model fault). The marker is bounded and carries no
|
|
202
|
-
* child transcript content beyond the failure tag and message.
|
|
203
|
-
*/
|
|
204
|
-
export class FileReviewUnitFailed extends Schema.TaggedError<FileReviewUnitFailed>()(
|
|
205
|
-
"FileReviewUnitFailed",
|
|
206
|
-
{
|
|
207
|
-
childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
208
|
-
message: Schema.String.check(Schema.isMaxLength(400)),
|
|
209
|
-
},
|
|
210
|
-
) {}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Finite per-invocation bounds (SUB-009), aligned with the child's own
|
|
214
|
-
* AgentPolicy: the child's policy is the limit that trips typed; the
|
|
215
|
-
* reservation mirrors it so parent-side accounting stays honest.
|
|
216
|
-
*/
|
|
217
347
|
export const fileReviewPolicy = SubagentPolicy.make({
|
|
218
|
-
maxChildren:
|
|
219
|
-
maxConcurrency:
|
|
220
|
-
maxTurns:
|
|
348
|
+
maxChildren: MAX_REVIEW_CHILDREN,
|
|
349
|
+
maxConcurrency: 4,
|
|
350
|
+
maxTurns: 6,
|
|
221
351
|
maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
|
|
222
352
|
maxDuration: "6 minutes",
|
|
353
|
+
maxResultBytes: 256 * 1024,
|
|
223
354
|
});
|
|
224
355
|
|
|
225
|
-
const delegationDescription =
|
|
226
|
-
"Delegate the review of one planned unit to a bounded file-reviewer child and return its line-anchored findings. Call it exactly once per unit from list_review_units; never retry a failed unit.";
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Total mapping from every expected child Run failure to the declared unit
|
|
230
|
-
* failure (SUB-028): the tag plus a bounded message, nothing else crosses.
|
|
231
|
-
*/
|
|
232
356
|
export const mapFileReviewChildFailure = (failure: {
|
|
233
357
|
readonly _tag: string;
|
|
234
358
|
readonly message?: string;
|
|
@@ -238,22 +362,284 @@ export const mapFileReviewChildFailure = (failure: {
|
|
|
238
362
|
message: (failure.message ?? "").slice(0, 400),
|
|
239
363
|
});
|
|
240
364
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
365
|
+
const sameStrings = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
|
|
366
|
+
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
367
|
+
|
|
368
|
+
const rejectWork = (workId: string, reason: string) =>
|
|
369
|
+
FileReviewWorkRejected.make({ workId, reason });
|
|
370
|
+
|
|
371
|
+
/** Validate coordinator scheduling against the current deterministic plan. */
|
|
372
|
+
const prepareReviewBrief = (request: FileReviewRequest) =>
|
|
373
|
+
Effect.gen(function* () {
|
|
374
|
+
const source = yield* PullRequestSource;
|
|
375
|
+
const mapSourceFailure = (failure: PullRequestSourceFailure) =>
|
|
376
|
+
rejectWork(
|
|
377
|
+
request.workId,
|
|
378
|
+
`pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600),
|
|
379
|
+
);
|
|
380
|
+
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
381
|
+
const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
|
|
382
|
+
const plan = planReviewUnits(files, { totalChangedFiles: metadata.totalChangedFiles });
|
|
383
|
+
const unit = plan.units.find((candidate) => candidate.unitId === request.unitId);
|
|
384
|
+
if (
|
|
385
|
+
unit === undefined ||
|
|
386
|
+
!sameStrings(request.paths, unit.paths) ||
|
|
387
|
+
!sameStrings(
|
|
388
|
+
request.evidenceShardIds,
|
|
389
|
+
unit.evidenceShards.map((shard) => shard.shardId),
|
|
390
|
+
)
|
|
391
|
+
) {
|
|
392
|
+
return yield* rejectWork(request.workId, "request does not match a host-planned unit");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (request.phase === "discovery") {
|
|
396
|
+
const pass = plan.discoveryPasses.find((candidate) => candidate.passId === request.workId);
|
|
397
|
+
if (
|
|
398
|
+
pass === undefined ||
|
|
399
|
+
pass.unitId !== request.unitId ||
|
|
400
|
+
!sameStrings(pass.paths, request.paths) ||
|
|
401
|
+
!sameStrings(pass.evidenceShardIds, request.evidenceShardIds) ||
|
|
402
|
+
pass.perspective !== request.perspective ||
|
|
403
|
+
!sameStrings(pass.riskCategories, request.riskCategories) ||
|
|
404
|
+
request.candidates.length !== 0
|
|
405
|
+
) {
|
|
406
|
+
return yield* rejectWork(request.workId, "discovery request does not match the host plan");
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
if (
|
|
410
|
+
request.workId !== `${request.unitId}-verification` ||
|
|
411
|
+
request.perspective !== "candidate-verification" ||
|
|
412
|
+
!sameStrings(request.riskCategories, unit.riskCategories) ||
|
|
413
|
+
request.candidates.length === 0
|
|
414
|
+
) {
|
|
415
|
+
return yield* rejectWork(
|
|
416
|
+
request.workId,
|
|
417
|
+
"verification request does not match the host-planned unit",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const candidateIds = new Set<string>();
|
|
421
|
+
const candidateSubjects = new Set<string>();
|
|
422
|
+
const allowed = new Set(unit.paths);
|
|
423
|
+
for (const candidate of request.candidates) {
|
|
424
|
+
const subjectKey = reviewCandidateSubjectKey(candidate);
|
|
425
|
+
if (
|
|
426
|
+
candidateIds.has(candidate.candidateId) ||
|
|
427
|
+
candidateSubjects.has(subjectKey) ||
|
|
428
|
+
candidate.unitId !== unit.unitId ||
|
|
429
|
+
candidate.evidencePaths.some((path) => !allowed.has(path)) ||
|
|
430
|
+
(candidate._tag === "FindingCandidate" && !allowed.has(candidate.finding.path))
|
|
431
|
+
) {
|
|
432
|
+
return yield* rejectWork(
|
|
433
|
+
request.workId,
|
|
434
|
+
"verification candidates are duplicated or outside the planned unit",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
candidateIds.add(candidate.candidateId);
|
|
438
|
+
candidateSubjects.add(subjectKey);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const byPath = new Map(files.map((file) => [file.path, file] as const));
|
|
443
|
+
const evidence: Array<FileReviewEvidence> = [];
|
|
444
|
+
for (const shard of unit.evidenceShards) {
|
|
445
|
+
const file = byPath.get(shard.path);
|
|
446
|
+
if (file === undefined) {
|
|
447
|
+
return yield* rejectWork(
|
|
448
|
+
request.workId,
|
|
449
|
+
`planned evidence path is unavailable: ${shard.path}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
453
|
+
const chunk = chunks[shard.ordinal - 1];
|
|
454
|
+
if (
|
|
455
|
+
chunk === undefined ||
|
|
456
|
+
chunks.length !== shard.total ||
|
|
457
|
+
chunk.annotatedPatch.length !== shard.evidenceChars
|
|
458
|
+
) {
|
|
459
|
+
return yield* rejectWork(
|
|
460
|
+
request.workId,
|
|
461
|
+
`planned evidence shard no longer matches source: ${shard.shardId}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
evidence.push(
|
|
465
|
+
FileReviewEvidence.make({
|
|
466
|
+
shardId: shard.shardId,
|
|
467
|
+
path: shard.path,
|
|
468
|
+
status: file.status,
|
|
469
|
+
reviewMode: chunk.reviewMode,
|
|
470
|
+
ordinal: shard.ordinal,
|
|
471
|
+
total: shard.total,
|
|
472
|
+
annotatedPatch: chunk.annotatedPatch,
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return FileReviewBrief.make({ ...request, evidence });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const candidateOrdinal = (index: number): string => String(index + 1).padStart(3, "0");
|
|
480
|
+
|
|
481
|
+
const projectReviewResult = (
|
|
482
|
+
report: FileReviewReport,
|
|
483
|
+
context: { readonly budgetExhausted: boolean },
|
|
484
|
+
request: FileReviewRequest,
|
|
485
|
+
) => {
|
|
486
|
+
if (context.budgetExhausted) {
|
|
487
|
+
return Effect.fail(
|
|
488
|
+
rejectWork(report.workId, "review work exhausted its budget before exact settlement"),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (
|
|
492
|
+
report.phase !== request.phase ||
|
|
493
|
+
report.workId !== request.workId ||
|
|
494
|
+
report.unitId !== request.unitId
|
|
495
|
+
) {
|
|
496
|
+
return Effect.fail(
|
|
497
|
+
rejectWork(request.workId, "review output identity does not match the scheduled request"),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (report.phase === "verification") {
|
|
501
|
+
if (
|
|
502
|
+
report.findings.length > 0 ||
|
|
503
|
+
report.concerns.length > 0 ||
|
|
504
|
+
report.fileSummaries.length > 0
|
|
505
|
+
) {
|
|
506
|
+
return Effect.fail(
|
|
507
|
+
rejectWork(report.workId, "verification output contained discovery-only fields"),
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const expectedById = new Map(
|
|
511
|
+
request.candidates.map((candidate) => [candidate.candidateId, candidate] as const),
|
|
512
|
+
);
|
|
513
|
+
const assessedIds = new Set<string>();
|
|
514
|
+
for (const assessment of report.assessments) {
|
|
515
|
+
const candidate = expectedById.get(assessment.candidateId);
|
|
516
|
+
if (candidate === undefined || assessedIds.has(assessment.candidateId)) {
|
|
517
|
+
return Effect.fail(
|
|
518
|
+
rejectWork(report.workId, "verification output did not assess the exact candidate set"),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (!assessmentSettlesSuggestionExactly(assessment, candidate)) {
|
|
522
|
+
return Effect.fail(
|
|
523
|
+
rejectWork(
|
|
524
|
+
report.workId,
|
|
525
|
+
"verification output did not settle suggestion publication exactly",
|
|
526
|
+
),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
assessedIds.add(assessment.candidateId);
|
|
530
|
+
}
|
|
531
|
+
if (assessedIds.size !== expectedById.size) {
|
|
532
|
+
return Effect.fail(
|
|
533
|
+
rejectWork(report.workId, "verification output did not assess the exact candidate set"),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return Effect.succeed(
|
|
537
|
+
FileReviewUnitResult.make({
|
|
538
|
+
phase: report.phase,
|
|
539
|
+
workId: report.workId,
|
|
540
|
+
unitId: report.unitId,
|
|
541
|
+
candidates: [],
|
|
542
|
+
fileSummaries: [],
|
|
543
|
+
assessments: report.assessments,
|
|
544
|
+
}),
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (report.assessments.length > 0) {
|
|
548
|
+
return Effect.fail(
|
|
549
|
+
rejectWork(report.workId, "discovery output contained verification-only assessments"),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const allowed = new Set(request.paths);
|
|
553
|
+
if (
|
|
554
|
+
report.findings.some((finding) => !allowed.has(finding.path)) ||
|
|
555
|
+
report.concerns.some((candidate) =>
|
|
556
|
+
candidate.evidencePaths.some((path) => !allowed.has(path)),
|
|
557
|
+
) ||
|
|
558
|
+
report.fileSummaries.some((entry) => !allowed.has(entry.path))
|
|
559
|
+
) {
|
|
560
|
+
return Effect.fail(
|
|
561
|
+
rejectWork(report.workId, "discovery output referenced evidence outside the scheduled unit"),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return Effect.gen(function* () {
|
|
565
|
+
const source = yield* PullRequestSource;
|
|
566
|
+
const mapSourceFailure = (failure: PullRequestSourceFailure) =>
|
|
567
|
+
rejectWork(
|
|
568
|
+
request.workId,
|
|
569
|
+
`pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600),
|
|
570
|
+
);
|
|
571
|
+
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
572
|
+
const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
|
|
573
|
+
const anchorFiles = yield* source.anchorFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
574
|
+
const unit = planReviewUnits(files, {
|
|
575
|
+
totalChangedFiles: metadata.totalChangedFiles,
|
|
576
|
+
}).units.find((candidate) => candidate.unitId === request.unitId);
|
|
577
|
+
if (unit === undefined) {
|
|
578
|
+
return yield* rejectWork(request.workId, "scheduled review unit is no longer available");
|
|
579
|
+
}
|
|
580
|
+
for (const finding of report.findings) {
|
|
581
|
+
const violation = anchorViolation(finding, anchorFiles);
|
|
582
|
+
if (violation !== undefined || !findingAnchorInUnitEvidence(finding, unit, files)) {
|
|
583
|
+
return yield* rejectWork(
|
|
584
|
+
request.workId,
|
|
585
|
+
`discovery finding has no valid anchor in its assigned evidence: ${violation ?? finding.path}`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const findingCandidates = report.findings.map((finding, index) =>
|
|
590
|
+
FindingCandidate.make({
|
|
591
|
+
candidateId: `${request.workId}:finding:${candidateOrdinal(index)}`,
|
|
592
|
+
workId: request.workId,
|
|
593
|
+
unitId: request.unitId,
|
|
594
|
+
finding,
|
|
595
|
+
evidencePaths: [finding.path],
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
598
|
+
const concernCandidates = report.concerns.map((candidate, index) =>
|
|
599
|
+
ConcernCandidate.make({
|
|
600
|
+
candidateId: `${request.workId}:concern:${candidateOrdinal(index)}`,
|
|
601
|
+
workId: request.workId,
|
|
602
|
+
unitId: request.unitId,
|
|
603
|
+
concern: candidate.concern,
|
|
604
|
+
evidencePaths: candidate.evidencePaths,
|
|
605
|
+
}),
|
|
606
|
+
);
|
|
607
|
+
return FileReviewUnitResult.make({
|
|
608
|
+
phase: report.phase,
|
|
609
|
+
workId: report.workId,
|
|
610
|
+
unitId: report.unitId,
|
|
611
|
+
candidates: [...findingCandidates, ...concernCandidates],
|
|
612
|
+
fileSummaries: report.fileSummaries,
|
|
613
|
+
assessments: [],
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const delegationDescription =
|
|
619
|
+
"Run exactly one host-planned discovery or candidate-verification child. Copy every plan field and candidate verbatim; never retry failed work.";
|
|
620
|
+
|
|
621
|
+
const makeFileReviewDelegation = (child: ReturnType<typeof makeFileReviewerDefinition>) =>
|
|
622
|
+
Subagent.define("delegate_file_review", {
|
|
623
|
+
description: delegationDescription,
|
|
624
|
+
target: child,
|
|
625
|
+
parameters: FileReviewRequest,
|
|
626
|
+
success: FileReviewUnitResult,
|
|
627
|
+
failure: FileReviewFailure,
|
|
628
|
+
failureMode: "return",
|
|
629
|
+
prepareInput: prepareReviewBrief,
|
|
630
|
+
projectResult: projectReviewResult,
|
|
631
|
+
policy: fileReviewPolicy,
|
|
632
|
+
});
|
|
246
633
|
|
|
247
634
|
export class ListReviewUnitsQuery extends Schema.Class<ListReviewUnitsQuery>(
|
|
248
635
|
"@effect-agent/pr-review/ListReviewUnitsQuery",
|
|
249
636
|
)({
|
|
250
|
-
/** Explicit constant keeps the zero-choice operation compatible with strict provider schemas. */
|
|
251
637
|
scope: Schema.Literal("all"),
|
|
252
638
|
}) {}
|
|
253
639
|
|
|
254
640
|
export const ListReviewUnits = Tool.make("list_review_units", {
|
|
255
641
|
description:
|
|
256
|
-
"List
|
|
642
|
+
"List deterministic bounded review units, explicit risk categories, every required discovery pass, and paths the pipeline cannot cover.",
|
|
257
643
|
parameters: ListReviewUnitsQuery,
|
|
258
644
|
success: ReviewUnitPlan,
|
|
259
645
|
failure: PullRequestSourceFailure,
|
|
@@ -273,64 +659,38 @@ export const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({
|
|
|
273
659
|
}),
|
|
274
660
|
});
|
|
275
661
|
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
// The coordinator Agent Definition: same mission input and CodeReview output
|
|
278
|
-
// contract as the flat reviewer, so planPublication and anchor validation
|
|
279
|
-
// apply unchanged.
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Build the coordinator's instructions. The same consumer guidance the
|
|
284
|
-
* children receive is injected between the mission framing and the procedure
|
|
285
|
-
* so the merged summary and verdict are shaped by the same review profile,
|
|
286
|
-
* and the configured findings bound reaches the merge step instead of only
|
|
287
|
-
* the host-side trim.
|
|
288
|
-
*/
|
|
289
662
|
export const makeFanOutReviewInstructions =
|
|
290
663
|
(options: FanOutInstructionOptions & { readonly maxFindings?: number | undefined } = {}) =>
|
|
291
664
|
(mission: ReviewMission): string => {
|
|
292
665
|
const maxFindings = clampMaxFindings(options.maxFindings);
|
|
293
666
|
return [
|
|
294
|
-
`You coordinate the review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}
|
|
295
|
-
mission.body.length > 0
|
|
296
|
-
? `Author description:\n${mission.body}`
|
|
297
|
-
: "The author provided no description.",
|
|
667
|
+
`You coordinate the bounded multi-pass review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}.`,
|
|
668
|
+
mission.body.length > 0 ? `Author description:\n${mission.body}` : "No author description.",
|
|
298
669
|
...staticGuidanceLines(options.guidance),
|
|
299
|
-
"
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`
|
|
304
|
-
|
|
305
|
-
"6. Merge the units' fileSummaries into one walkthrough: copy each entry verbatim, one entry per file, dropping duplicate paths.",
|
|
306
|
-
'7. 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, including every unreviewed unit or file>, "verdict": <"approve" | "comment" | "request-changes">, "findings": [{"path": <string>, "startLine": <integer>, "endLine": <integer>, "severity": <"blocking" | "important" | "nit">, "category": <string, OPTIONAL>, "title": <string, <= 120 chars>, "body": <string>, "suggestion": <string, OPTIONAL>}], "concerns": <array, OPTIONAL: [{"severity": <"blocking" | "important" | "nit">, "title": <string, <= 120 chars>, "body": <string>}], the merged unit concerns>, "walkthrough": <array, OPTIONAL: [{"path": <string>, "summary": <string>}], the merged fileSummaries>}. Copy findings (including "category" and "suggestion" when present), concerns, and walkthrough entries verbatim from the delegation results; never invent or edit anchors.',
|
|
307
|
-
'Use verdict "request-changes" only when at least one finding or concern is "blocking". An empty findings array with verdict "approve" is a valid review when every unit succeeded and found nothing.',
|
|
670
|
+
"1. Call list_review_units exactly once.",
|
|
671
|
+
'2. For EVERY discoveryPass, call delegate_file_review exactly once with phase "discovery", workId=passId, and the pass unitId/paths/evidenceShardIds/perspective/riskCategories verbatim; candidates must be []. Prefer one bounded parallel batch. Never retry.',
|
|
672
|
+
'3. Group candidates returned by all successful discovery passes by unit. Deterministically deduplicate byte-identical finding or concern payloads, retaining the first candidate in discoveryPass plan order. For every unit with at least one retained candidate, call delegate_file_review exactly once with phase "verification", workId "<unitId>-verification", perspective "candidate-verification", the unit paths/evidenceShardIds/riskCategories, and EVERY retained candidate copied byte-for-byte. Prefer one bounded parallel batch. Never retry.',
|
|
673
|
+
"4. Verification is authoritative: rejected candidates must not be reported. The host independently reconstructs publishable findings from exact confirmed assessments, so do not select, rewrite, downgrade, or invent findings.",
|
|
674
|
+
`5. Return ONLY CodeReview JSON. Write a concise summary of completed and failed stages. Set findings=[] and concerns=[]; the host injects exact confirmed candidates. Copy factual fileSummaries into walkthrough without invention. The host publication cap is ${maxFindings}.`,
|
|
675
|
+
"No configured pipeline can prove absence of defects. Describe settled work, never an exhaustive or defect-free review.",
|
|
308
676
|
].join("\n");
|
|
309
677
|
};
|
|
310
678
|
|
|
311
679
|
export const fanOutReviewInstructions = makeFanOutReviewInstructions();
|
|
312
680
|
|
|
313
|
-
/** The default fan-out coordinator execution bounds. */
|
|
314
681
|
export const defaultFanOutPolicy = AgentPolicy.make({
|
|
315
|
-
maxTurns:
|
|
316
|
-
maxToolCalls: 1 +
|
|
317
|
-
maxDuration: "
|
|
318
|
-
toolConcurrency:
|
|
319
|
-
// Contained unit failures (SUB-033) are ordinary successful Tool results,
|
|
320
|
-
// so they no longer fold into the repeated-failure counter; the default
|
|
321
|
-
// bound suffices.
|
|
682
|
+
maxTurns: 7,
|
|
683
|
+
maxToolCalls: 1 + MAX_REVIEW_CHILDREN,
|
|
684
|
+
maxDuration: "20 minutes",
|
|
685
|
+
toolConcurrency: 4,
|
|
322
686
|
repeatedFailureLimit: 3,
|
|
323
|
-
tokenBudget:
|
|
324
|
-
// Child reports can amplify the merge prompt; compact before the provider's
|
|
325
|
-
// 200k-class window becomes the failure boundary.
|
|
687
|
+
tokenBudget: 400_000,
|
|
326
688
|
contextTokenLimit: 150_000,
|
|
327
|
-
//
|
|
328
|
-
//
|
|
689
|
+
// Coordinator exhaustion cannot become an assured result; exact stage
|
|
690
|
+
// settlement, not this final prose, determines assurance.
|
|
329
691
|
onExhaustion: "final-answer",
|
|
330
692
|
});
|
|
331
693
|
|
|
332
|
-
/** Everything one fan-out configuration is made of, built as one unit so the
|
|
333
|
-
* delegation always targets exactly the child definition that will run. */
|
|
334
694
|
export interface FanOutReviewSuite {
|
|
335
695
|
readonly child: ReturnType<typeof makeFileReviewerDefinition>;
|
|
336
696
|
readonly parent: ReturnType<typeof makeFanOutReviewerDefinition>;
|
|
@@ -338,68 +698,22 @@ export interface FanOutReviewSuite {
|
|
|
338
698
|
}
|
|
339
699
|
|
|
340
700
|
const makeFileReviewerDefinition = (options: FanOutInstructionOptions = {}) =>
|
|
341
|
-
Agent.define("pr-
|
|
701
|
+
Agent.define("pr-review-worker", {
|
|
342
702
|
input: FileReviewBrief,
|
|
343
703
|
output: FileReviewReport,
|
|
344
704
|
instructions: makeFileReviewerInstructions(options),
|
|
345
705
|
toolkit: FileReviewToolkit,
|
|
346
706
|
policy: defaultFileReviewerPolicy,
|
|
347
707
|
description:
|
|
348
|
-
"
|
|
349
|
-
metadata: { deploymentClass: "E", surface: "read-only" },
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
/** Options for one coherent fan-out suite: shared guidance plus the merge bound. */
|
|
353
|
-
export interface FanOutSuiteOptions extends FanOutInstructionOptions {
|
|
354
|
-
readonly maxFindings?: number | undefined;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const makeFileReviewDelegation = (child: ReturnType<typeof makeFileReviewerDefinition>) =>
|
|
358
|
-
Subagent.define("delegate_file_review", {
|
|
359
|
-
description: delegationDescription,
|
|
360
|
-
target: child,
|
|
361
|
-
parameters: FileReviewRequest,
|
|
362
|
-
success: FileReviewUnitResult,
|
|
363
|
-
failure: FileReviewUnitFailed,
|
|
364
|
-
// First-party containment (SUB-033): a failed unit is model-visible
|
|
365
|
-
// result data instead of a parent-Run-fatal error, so the coordinator
|
|
366
|
-
// reports it honestly and keeps reviewing the other units. This retires
|
|
367
|
-
// the former same-name shadow-Tool workaround (FRICTION #7).
|
|
368
|
-
failureMode: "return",
|
|
369
|
-
prepareInput: (request) =>
|
|
370
|
-
Effect.succeed(
|
|
371
|
-
FileReviewBrief.make({
|
|
372
|
-
unitId: request.unitId,
|
|
373
|
-
paths: request.paths,
|
|
374
|
-
focus: "defects-first: correctness, security, concurrency, resources, error handling",
|
|
375
|
-
}),
|
|
376
|
-
),
|
|
377
|
-
// The explicit declassification boundary (SUB-015): exactly the bounded
|
|
378
|
-
// findings and concerns cross to the parent. Whether findings may anchor
|
|
379
|
-
// anywhere is decided host-side by planPublication against the real diff.
|
|
380
|
-
projectResult: (report) =>
|
|
381
|
-
Effect.succeed(
|
|
382
|
-
FileReviewUnitResult.make({
|
|
383
|
-
unitId: report.unitId,
|
|
384
|
-
findings: report.findings,
|
|
385
|
-
...(report.concerns !== undefined ? { concerns: report.concerns } : {}),
|
|
386
|
-
...(report.fileSummaries !== undefined ? { fileSummaries: report.fileSummaries } : {}),
|
|
387
|
-
}),
|
|
388
|
-
),
|
|
389
|
-
policy: fileReviewPolicy,
|
|
708
|
+
"Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
|
|
709
|
+
metadata: { deploymentClass: "E", surface: "read-only", stage: "discovery-verification" },
|
|
390
710
|
});
|
|
391
711
|
|
|
392
|
-
/**
|
|
393
|
-
* The coordinator-facing delegation Tool: the delegation's own first-party
|
|
394
|
-
* contained Tool plus the read-only execution class (the delegated child's
|
|
395
|
-
* whole tool surface is read-only). Effect AI resolves handlers by Tool name,
|
|
396
|
-
* so `SubagentRuntime.layer`'s handler serves this annotated copy unchanged.
|
|
397
|
-
*/
|
|
398
712
|
const delegationToolFor = (delegation: ReturnType<typeof makeFileReviewDelegation>) =>
|
|
399
713
|
delegation.tool.annotate(ToolExecutionClass, "readonly");
|
|
400
714
|
|
|
401
715
|
const makeFanOutReviewerDefinition = (
|
|
402
|
-
options:
|
|
716
|
+
options: FanOutInstructionOptions & { readonly maxFindings?: number | undefined },
|
|
403
717
|
delegation: ReturnType<typeof makeFileReviewDelegation>,
|
|
404
718
|
) =>
|
|
405
719
|
Agent.define("pr-fanout-reviewer", {
|
|
@@ -409,11 +723,19 @@ const makeFanOutReviewerDefinition = (
|
|
|
409
723
|
toolkit: Toolkit.make(ListReviewUnits, delegationToolFor(delegation)),
|
|
410
724
|
policy: defaultFanOutPolicy,
|
|
411
725
|
description:
|
|
412
|
-
"Coordinate
|
|
413
|
-
metadata: {
|
|
726
|
+
"Coordinate deterministic general/specialist discovery and independent candidate verification over bounded review units.",
|
|
727
|
+
metadata: {
|
|
728
|
+
deploymentClass: "E",
|
|
729
|
+
surface: "read-only",
|
|
730
|
+
delegation: "S1-attached",
|
|
731
|
+
assurance: "multi-pass",
|
|
732
|
+
},
|
|
414
733
|
});
|
|
415
734
|
|
|
416
|
-
|
|
735
|
+
export interface FanOutSuiteOptions extends FanOutInstructionOptions {
|
|
736
|
+
readonly maxFindings?: number | undefined;
|
|
737
|
+
}
|
|
738
|
+
|
|
417
739
|
export const makeFanOutReviewSuite = (options: FanOutSuiteOptions = {}): FanOutReviewSuite => {
|
|
418
740
|
const child = makeFileReviewerDefinition({ guidance: options.guidance });
|
|
419
741
|
const delegation = makeFileReviewDelegation(child);
|
|
@@ -426,29 +748,13 @@ export const makeFanOutReviewSuite = (options: FanOutSuiteOptions = {}): FanOutR
|
|
|
426
748
|
|
|
427
749
|
const defaultSuite = makeFanOutReviewSuite();
|
|
428
750
|
|
|
429
|
-
/** The default child Agent Definition. */
|
|
430
751
|
export const FileReviewer = defaultSuite.child;
|
|
431
|
-
|
|
432
|
-
/** The default coordinator Agent Definition. */
|
|
433
752
|
export const FanOutReviewer = defaultSuite.parent;
|
|
434
|
-
|
|
435
|
-
/** The default delegation over the default child. */
|
|
436
753
|
export const fileReviewDelegation = defaultSuite.delegation;
|
|
437
|
-
|
|
438
|
-
/** The default coordinator-facing delegation Tool (first-party contained mode). */
|
|
439
754
|
export const DelegateFileReview = delegationToolFor(fileReviewDelegation);
|
|
440
|
-
|
|
441
|
-
/** The default coordinator Toolkit. */
|
|
442
755
|
export const FanOutReviewToolkit = FanOutReviewer.toolkit;
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* The contained failure family the delegation can surface as result data
|
|
446
|
-
* (SUB-033), derived from the delegation itself so the coverage decoder can
|
|
447
|
-
* never diverge from what the runtime actually contains.
|
|
448
|
-
*/
|
|
449
756
|
export const FileReviewDelegationFailure = fileReviewDelegation.containedFailure;
|
|
450
757
|
|
|
451
|
-
/** Runtime wiring: one delegation plus one explicit child Binding. */
|
|
452
758
|
export const fanOutHandlersLayerFor =
|
|
453
759
|
(delegation: ReturnType<typeof makeFileReviewDelegation>) =>
|
|
454
760
|
<Provider, ModelProvides, ModelRequires>(
|
|
@@ -466,5 +772,4 @@ export const fanOutHandlersLayerFor =
|
|
|
466
772
|
mapChildFailure: mapFileReviewChildFailure,
|
|
467
773
|
});
|
|
468
774
|
|
|
469
|
-
/** Runtime wiring over the default delegation, mirroring the leaf example. */
|
|
470
775
|
export const fanOutHandlersLayer = fanOutHandlersLayerFor(fileReviewDelegation);
|