@effect-agent/pr-review 0.1.0-beta.19 → 0.1.0-beta.21
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 +10 -4
- package/dist/action.mjs +17 -27
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +6 -6
- package/dist/cli.mjs.map +1 -1
- package/dist/{fan-out-CQFA-o0v.d.mts → fan-out-D5xrmadQ.d.mts} +288 -223
- package/dist/{github-DnenG3be.mjs → github-DSqZp3Ce.mjs} +622 -195
- package/dist/github-DSqZp3Ce.mjs.map +1 -0
- package/dist/index.d.mts +26 -53
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-CblG1G9b.mjs} +403 -134
- package/dist/providers-CblG1G9b.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 +7 -7
- package/src/action.ts +40 -39
- package/src/cli.ts +4 -0
- package/src/index.ts +1 -0
- package/src/internal/anchors.ts +20 -0
- package/src/internal/coverage.ts +612 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +505 -256
- 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 +68 -24
- 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,206 @@ 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
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
* maximum-size unit. Keep the child and delegation reservation aligned.
|
|
59
|
-
*/
|
|
60
|
-
export const MAX_FILE_REVIEW_TOOL_CALLS = MAX_UNIT_FILES * 2;
|
|
59
|
+
/** Every unit receives independent general and specialist discovery passes. */
|
|
60
|
+
export const MAX_UNIT_CANDIDATES = (MAX_CHILD_FINDINGS + MAX_CHILD_CONCERNS) * 2;
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// smaller than the flat reviewer's — diff and head-file reads only, no
|
|
65
|
-
// changeset listing — so a child can never roam beyond its briefed unit
|
|
66
|
-
// despite its observation surface being the whole changeset port.
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
62
|
+
/** General + specialist discovery for every unit, then one verifier per unit. */
|
|
63
|
+
export const MAX_REVIEW_CHILDREN = MAX_REVIEW_UNITS * 3;
|
|
68
64
|
|
|
69
|
-
|
|
65
|
+
/** Structural minimum for a child that exposes no tools. */
|
|
66
|
+
export const MAX_FILE_REVIEW_TOOL_CALLS = 1;
|
|
70
67
|
|
|
71
|
-
export const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
|
|
115
|
+
}) {}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Concern candidates need explicit paths internally to bind the claim to
|
|
119
|
+
* scheduled evidence. The verifier receives the complete bounded unit so it
|
|
120
|
+
* can use neighboring evidence to falsify the claim. The public ReviewConcern
|
|
121
|
+
* remains path-free after the host confirms and projects it.
|
|
122
|
+
*/
|
|
123
|
+
export class DiscoveredConcern extends Schema.Class<DiscoveredConcern>(
|
|
124
|
+
"@effect-agent/pr-review/DiscoveredConcern",
|
|
125
|
+
)({
|
|
126
|
+
concern: ReviewConcern,
|
|
127
|
+
evidencePaths: Schema.Array(ChangedPath)
|
|
128
|
+
.check(Schema.isMinLength(1))
|
|
129
|
+
.check(Schema.isMaxLength(3)),
|
|
130
|
+
}) {}
|
|
75
131
|
|
|
76
132
|
const UnitPaths = Schema.Array(ChangedPath)
|
|
77
133
|
.check(Schema.isMinLength(1))
|
|
78
134
|
.check(Schema.isMaxLength(MAX_UNIT_FILES));
|
|
79
135
|
|
|
80
|
-
|
|
136
|
+
const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
|
|
137
|
+
const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES));
|
|
138
|
+
const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId)
|
|
139
|
+
.check(Schema.isMinLength(1))
|
|
140
|
+
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS));
|
|
141
|
+
|
|
142
|
+
/** Strict-object coordinator request for either discovery or verification. */
|
|
143
|
+
export class FileReviewRequest extends Schema.Class<FileReviewRequest>(
|
|
144
|
+
"@effect-agent/pr-review/FileReviewRequest",
|
|
145
|
+
)({
|
|
146
|
+
phase: ReviewWorkPhase,
|
|
147
|
+
workId: ReviewPassId,
|
|
148
|
+
unitId: ReviewUnitId,
|
|
149
|
+
paths: UnitPaths,
|
|
150
|
+
evidenceShardIds: EvidenceShardIds,
|
|
151
|
+
perspective: ReviewWorkPerspective,
|
|
152
|
+
riskCategories: RiskCategories,
|
|
153
|
+
/** Empty for discovery; the exact discovered set for unit verification. */
|
|
154
|
+
candidates: Candidates,
|
|
155
|
+
}) {}
|
|
156
|
+
|
|
157
|
+
/** One complete host-selected evidence shard supplied to a review child. */
|
|
158
|
+
export class FileReviewEvidence extends Schema.Class<FileReviewEvidence>(
|
|
159
|
+
"@effect-agent/pr-review/FileReviewEvidence",
|
|
160
|
+
)({
|
|
161
|
+
shardId: ReviewEvidenceShardId,
|
|
162
|
+
path: ChangedPath,
|
|
163
|
+
status: ChangedFileStatus,
|
|
164
|
+
reviewMode: Schema.Literals(["diff", "content", "unavailable"]),
|
|
165
|
+
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
166
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
167
|
+
annotatedPatch: Schema.String.check(Schema.isMaxLength(MAX_PATCH_CHARS)),
|
|
168
|
+
}) {}
|
|
169
|
+
|
|
170
|
+
/** Host-prepared child input with complete bounded diff/content evidence. */
|
|
81
171
|
export class FileReviewBrief extends Schema.Class<FileReviewBrief>(
|
|
82
172
|
"@effect-agent/pr-review/FileReviewBrief",
|
|
83
173
|
)({
|
|
174
|
+
phase: ReviewWorkPhase,
|
|
175
|
+
workId: ReviewPassId,
|
|
84
176
|
unitId: ReviewUnitId,
|
|
85
177
|
paths: UnitPaths,
|
|
86
|
-
|
|
178
|
+
evidenceShardIds: EvidenceShardIds,
|
|
179
|
+
perspective: ReviewWorkPerspective,
|
|
180
|
+
riskCategories: RiskCategories,
|
|
181
|
+
candidates: Candidates,
|
|
182
|
+
evidence: Schema.Array(FileReviewEvidence)
|
|
183
|
+
.check(Schema.isMinLength(1))
|
|
184
|
+
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS)),
|
|
87
185
|
}) {}
|
|
88
186
|
|
|
89
|
-
/**
|
|
187
|
+
/** Child output; phase-inapplicable collections must be empty. */
|
|
90
188
|
export class FileReviewReport extends Schema.Class<FileReviewReport>(
|
|
91
189
|
"@effect-agent/pr-review/FileReviewReport",
|
|
92
190
|
)({
|
|
191
|
+
phase: ReviewWorkPhase,
|
|
192
|
+
workId: ReviewPassId,
|
|
93
193
|
unitId: ReviewUnitId,
|
|
94
194
|
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
|
-
),
|
|
195
|
+
concerns: Schema.Array(DiscoveredConcern).check(Schema.isMaxLength(MAX_CHILD_CONCERNS)),
|
|
196
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
197
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES)),
|
|
103
198
|
}) {}
|
|
104
199
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
200
|
+
/** Bounded coordinator-visible result with host-assigned candidate IDs. */
|
|
201
|
+
export class FileReviewUnitResult extends Schema.Class<FileReviewUnitResult>(
|
|
202
|
+
"@effect-agent/pr-review/FileReviewUnitResult",
|
|
203
|
+
)({
|
|
204
|
+
phase: ReviewWorkPhase,
|
|
205
|
+
workId: ReviewPassId,
|
|
206
|
+
unitId: ReviewUnitId,
|
|
207
|
+
candidates: Candidates,
|
|
208
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
209
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES)),
|
|
210
|
+
}) {}
|
|
211
|
+
|
|
212
|
+
export class FileReviewUnitFailed extends Schema.TaggedError<FileReviewUnitFailed>()(
|
|
213
|
+
"FileReviewUnitFailed",
|
|
214
|
+
{
|
|
215
|
+
childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
216
|
+
message: Schema.String.check(Schema.isMaxLength(400)),
|
|
217
|
+
},
|
|
218
|
+
) {}
|
|
219
|
+
|
|
220
|
+
export class FileReviewWorkRejected extends Schema.TaggedError<FileReviewWorkRejected>()(
|
|
221
|
+
"FileReviewWorkRejected",
|
|
222
|
+
{
|
|
223
|
+
workId: ReviewPassId,
|
|
224
|
+
reason: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
|
|
225
|
+
},
|
|
226
|
+
) {}
|
|
227
|
+
|
|
228
|
+
export const FileReviewFailure = Schema.Union([FileReviewUnitFailed, FileReviewWorkRejected]);
|
|
229
|
+
|
|
111
230
|
export interface FanOutInstructionOptions {
|
|
112
231
|
readonly guidance?: string | ReadonlyArray<string> | undefined;
|
|
113
232
|
}
|
|
@@ -120,115 +239,75 @@ const staticGuidanceLines = (
|
|
|
120
239
|
return lines.filter((line) => line.length > 0);
|
|
121
240
|
};
|
|
122
241
|
|
|
123
|
-
|
|
242
|
+
const evidenceInstructions = [
|
|
243
|
+
"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.",
|
|
244
|
+
"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.",
|
|
245
|
+
"A diff marks new-version anchors as R<number>; only those lines may anchor findings. B/H content evidence is non-anchorable.",
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
/** Discovery and verification instructions share one child definition. */
|
|
124
249
|
export const makeFileReviewerInstructions =
|
|
125
250
|
(options: FanOutInstructionOptions = {}) =>
|
|
126
|
-
(brief: FileReviewBrief): string =>
|
|
127
|
-
[
|
|
128
|
-
`You are
|
|
251
|
+
(brief: FileReviewBrief): string => {
|
|
252
|
+
const common = [
|
|
253
|
+
`You are an attached review worker for ${brief.workId} in host-planned unit ${brief.unitId}: ${brief.paths.join(", ")}.`,
|
|
129
254
|
...staticGuidanceLines(options.guidance),
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
255
|
+
...evidenceInstructions,
|
|
256
|
+
];
|
|
257
|
+
if (brief.phase === "verification") {
|
|
258
|
+
return [
|
|
259
|
+
...common,
|
|
260
|
+
"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.",
|
|
261
|
+
"The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
|
|
262
|
+
"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.",
|
|
263
|
+
'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">, "rationale": <bounded evidence-based reason>}. Never add or omit an id.',
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
const focus =
|
|
267
|
+
brief.perspective === "risk-specialist"
|
|
268
|
+
? brief.riskCategories.length > 0
|
|
269
|
+
? `This is a fresh specialist discovery pass. Concentrate on these host-classified risks without relying on another pass: ${brief.riskCategories.join(", ")}.`
|
|
270
|
+
: "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."
|
|
271
|
+
: "This is the general discovery pass. Review broadly for correctness, security, concurrency, resource, API, and error-handling defects.";
|
|
272
|
+
return [
|
|
273
|
+
...common,
|
|
274
|
+
focus,
|
|
275
|
+
"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.",
|
|
276
|
+
"When a non-anchored concern depends on one or more unit files, list 1-3 exact evidencePaths to bind the claim to scheduled evidence.",
|
|
277
|
+
`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.`,
|
|
139
278
|
].join("\n");
|
|
279
|
+
};
|
|
140
280
|
|
|
141
281
|
export const fileReviewerInstructions = makeFileReviewerInstructions();
|
|
142
282
|
|
|
143
|
-
|
|
283
|
+
export const FileReviewToolkit = Toolkit.empty;
|
|
284
|
+
|
|
285
|
+
/** Compatibility export: the evidence-only child has no handler requirements. */
|
|
286
|
+
export const FileReviewToolkitLayer = Layer.empty;
|
|
287
|
+
|
|
144
288
|
export const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
145
|
-
maxTurns:
|
|
289
|
+
maxTurns: 6,
|
|
146
290
|
maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
|
|
147
291
|
maxDuration: "6 minutes",
|
|
148
292
|
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,
|
|
293
|
+
repeatedFailureLimit: 6,
|
|
153
294
|
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
295
|
contextTokenLimit: 150_000,
|
|
157
296
|
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).
|
|
297
|
+
// Discovery or verification that exhausts is unsettled work, never a
|
|
298
|
+
// schema-valid partial that can contribute to a green assurance claim.
|
|
165
299
|
onExhaustion: "fail",
|
|
166
300
|
});
|
|
167
301
|
|
|
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
302
|
export const fileReviewPolicy = SubagentPolicy.make({
|
|
218
|
-
maxChildren:
|
|
219
|
-
maxConcurrency:
|
|
220
|
-
maxTurns:
|
|
303
|
+
maxChildren: MAX_REVIEW_CHILDREN,
|
|
304
|
+
maxConcurrency: 4,
|
|
305
|
+
maxTurns: 6,
|
|
221
306
|
maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
|
|
222
307
|
maxDuration: "6 minutes",
|
|
308
|
+
maxResultBytes: 256 * 1024,
|
|
223
309
|
});
|
|
224
310
|
|
|
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
311
|
export const mapFileReviewChildFailure = (failure: {
|
|
233
312
|
readonly _tag: string;
|
|
234
313
|
readonly message?: string;
|
|
@@ -238,22 +317,273 @@ export const mapFileReviewChildFailure = (failure: {
|
|
|
238
317
|
message: (failure.message ?? "").slice(0, 400),
|
|
239
318
|
});
|
|
240
319
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
320
|
+
const sameStrings = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
|
|
321
|
+
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
322
|
+
|
|
323
|
+
const rejectWork = (workId: string, reason: string) =>
|
|
324
|
+
FileReviewWorkRejected.make({ workId, reason });
|
|
325
|
+
|
|
326
|
+
/** Validate coordinator scheduling against the current deterministic plan. */
|
|
327
|
+
const prepareReviewBrief = (request: FileReviewRequest) =>
|
|
328
|
+
Effect.gen(function* () {
|
|
329
|
+
const source = yield* PullRequestSource;
|
|
330
|
+
const mapSourceFailure = (failure: PullRequestSourceFailure) =>
|
|
331
|
+
rejectWork(
|
|
332
|
+
request.workId,
|
|
333
|
+
`pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600),
|
|
334
|
+
);
|
|
335
|
+
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
336
|
+
const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
|
|
337
|
+
const plan = planReviewUnits(files, { totalChangedFiles: metadata.totalChangedFiles });
|
|
338
|
+
const unit = plan.units.find((candidate) => candidate.unitId === request.unitId);
|
|
339
|
+
if (
|
|
340
|
+
unit === undefined ||
|
|
341
|
+
!sameStrings(request.paths, unit.paths) ||
|
|
342
|
+
!sameStrings(
|
|
343
|
+
request.evidenceShardIds,
|
|
344
|
+
unit.evidenceShards.map((shard) => shard.shardId),
|
|
345
|
+
)
|
|
346
|
+
) {
|
|
347
|
+
return yield* rejectWork(request.workId, "request does not match a host-planned unit");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (request.phase === "discovery") {
|
|
351
|
+
const pass = plan.discoveryPasses.find((candidate) => candidate.passId === request.workId);
|
|
352
|
+
if (
|
|
353
|
+
pass === undefined ||
|
|
354
|
+
pass.unitId !== request.unitId ||
|
|
355
|
+
!sameStrings(pass.paths, request.paths) ||
|
|
356
|
+
!sameStrings(pass.evidenceShardIds, request.evidenceShardIds) ||
|
|
357
|
+
pass.perspective !== request.perspective ||
|
|
358
|
+
!sameStrings(pass.riskCategories, request.riskCategories) ||
|
|
359
|
+
request.candidates.length !== 0
|
|
360
|
+
) {
|
|
361
|
+
return yield* rejectWork(request.workId, "discovery request does not match the host plan");
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
if (
|
|
365
|
+
request.workId !== `${request.unitId}-verification` ||
|
|
366
|
+
request.perspective !== "candidate-verification" ||
|
|
367
|
+
!sameStrings(request.riskCategories, unit.riskCategories) ||
|
|
368
|
+
request.candidates.length === 0
|
|
369
|
+
) {
|
|
370
|
+
return yield* rejectWork(
|
|
371
|
+
request.workId,
|
|
372
|
+
"verification request does not match the host-planned unit",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const candidateIds = new Set<string>();
|
|
376
|
+
const candidateSubjects = new Set<string>();
|
|
377
|
+
const allowed = new Set(unit.paths);
|
|
378
|
+
for (const candidate of request.candidates) {
|
|
379
|
+
const subjectKey = reviewCandidateSubjectKey(candidate);
|
|
380
|
+
if (
|
|
381
|
+
candidateIds.has(candidate.candidateId) ||
|
|
382
|
+
candidateSubjects.has(subjectKey) ||
|
|
383
|
+
candidate.unitId !== unit.unitId ||
|
|
384
|
+
candidate.evidencePaths.some((path) => !allowed.has(path)) ||
|
|
385
|
+
(candidate._tag === "FindingCandidate" && !allowed.has(candidate.finding.path))
|
|
386
|
+
) {
|
|
387
|
+
return yield* rejectWork(
|
|
388
|
+
request.workId,
|
|
389
|
+
"verification candidates are duplicated or outside the planned unit",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
candidateIds.add(candidate.candidateId);
|
|
393
|
+
candidateSubjects.add(subjectKey);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const byPath = new Map(files.map((file) => [file.path, file] as const));
|
|
398
|
+
const evidence: Array<FileReviewEvidence> = [];
|
|
399
|
+
for (const shard of unit.evidenceShards) {
|
|
400
|
+
const file = byPath.get(shard.path);
|
|
401
|
+
if (file === undefined) {
|
|
402
|
+
return yield* rejectWork(
|
|
403
|
+
request.workId,
|
|
404
|
+
`planned evidence path is unavailable: ${shard.path}`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
408
|
+
const chunk = chunks[shard.ordinal - 1];
|
|
409
|
+
if (
|
|
410
|
+
chunk === undefined ||
|
|
411
|
+
chunks.length !== shard.total ||
|
|
412
|
+
chunk.annotatedPatch.length !== shard.evidenceChars
|
|
413
|
+
) {
|
|
414
|
+
return yield* rejectWork(
|
|
415
|
+
request.workId,
|
|
416
|
+
`planned evidence shard no longer matches source: ${shard.shardId}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
evidence.push(
|
|
420
|
+
FileReviewEvidence.make({
|
|
421
|
+
shardId: shard.shardId,
|
|
422
|
+
path: shard.path,
|
|
423
|
+
status: file.status,
|
|
424
|
+
reviewMode: chunk.reviewMode,
|
|
425
|
+
ordinal: shard.ordinal,
|
|
426
|
+
total: shard.total,
|
|
427
|
+
annotatedPatch: chunk.annotatedPatch,
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return FileReviewBrief.make({ ...request, evidence });
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const candidateOrdinal = (index: number): string => String(index + 1).padStart(3, "0");
|
|
435
|
+
|
|
436
|
+
const projectReviewResult = (
|
|
437
|
+
report: FileReviewReport,
|
|
438
|
+
context: { readonly budgetExhausted: boolean },
|
|
439
|
+
request: FileReviewRequest,
|
|
440
|
+
) => {
|
|
441
|
+
if (context.budgetExhausted) {
|
|
442
|
+
return Effect.fail(
|
|
443
|
+
rejectWork(report.workId, "review work exhausted its budget before exact settlement"),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
report.phase !== request.phase ||
|
|
448
|
+
report.workId !== request.workId ||
|
|
449
|
+
report.unitId !== request.unitId
|
|
450
|
+
) {
|
|
451
|
+
return Effect.fail(
|
|
452
|
+
rejectWork(request.workId, "review output identity does not match the scheduled request"),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (report.phase === "verification") {
|
|
456
|
+
if (
|
|
457
|
+
report.findings.length > 0 ||
|
|
458
|
+
report.concerns.length > 0 ||
|
|
459
|
+
report.fileSummaries.length > 0
|
|
460
|
+
) {
|
|
461
|
+
return Effect.fail(
|
|
462
|
+
rejectWork(report.workId, "verification output contained discovery-only fields"),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const expectedIds = new Set(request.candidates.map((candidate) => candidate.candidateId));
|
|
466
|
+
const assessedIds = new Set<string>();
|
|
467
|
+
for (const assessment of report.assessments) {
|
|
468
|
+
if (!expectedIds.has(assessment.candidateId) || assessedIds.has(assessment.candidateId)) {
|
|
469
|
+
return Effect.fail(
|
|
470
|
+
rejectWork(report.workId, "verification output did not assess the exact candidate set"),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
assessedIds.add(assessment.candidateId);
|
|
474
|
+
}
|
|
475
|
+
if (assessedIds.size !== expectedIds.size) {
|
|
476
|
+
return Effect.fail(
|
|
477
|
+
rejectWork(report.workId, "verification output did not assess the exact candidate set"),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return Effect.succeed(
|
|
481
|
+
FileReviewUnitResult.make({
|
|
482
|
+
phase: report.phase,
|
|
483
|
+
workId: report.workId,
|
|
484
|
+
unitId: report.unitId,
|
|
485
|
+
candidates: [],
|
|
486
|
+
fileSummaries: [],
|
|
487
|
+
assessments: report.assessments,
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (report.assessments.length > 0) {
|
|
492
|
+
return Effect.fail(
|
|
493
|
+
rejectWork(report.workId, "discovery output contained verification-only assessments"),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
const allowed = new Set(request.paths);
|
|
497
|
+
if (
|
|
498
|
+
report.findings.some((finding) => !allowed.has(finding.path)) ||
|
|
499
|
+
report.concerns.some((candidate) =>
|
|
500
|
+
candidate.evidencePaths.some((path) => !allowed.has(path)),
|
|
501
|
+
) ||
|
|
502
|
+
report.fileSummaries.some((entry) => !allowed.has(entry.path))
|
|
503
|
+
) {
|
|
504
|
+
return Effect.fail(
|
|
505
|
+
rejectWork(report.workId, "discovery output referenced evidence outside the scheduled unit"),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return Effect.gen(function* () {
|
|
509
|
+
const source = yield* PullRequestSource;
|
|
510
|
+
const mapSourceFailure = (failure: PullRequestSourceFailure) =>
|
|
511
|
+
rejectWork(
|
|
512
|
+
request.workId,
|
|
513
|
+
`pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600),
|
|
514
|
+
);
|
|
515
|
+
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
516
|
+
const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
|
|
517
|
+
const anchorFiles = yield* source.anchorFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
518
|
+
const unit = planReviewUnits(files, {
|
|
519
|
+
totalChangedFiles: metadata.totalChangedFiles,
|
|
520
|
+
}).units.find((candidate) => candidate.unitId === request.unitId);
|
|
521
|
+
if (unit === undefined) {
|
|
522
|
+
return yield* rejectWork(request.workId, "scheduled review unit is no longer available");
|
|
523
|
+
}
|
|
524
|
+
for (const finding of report.findings) {
|
|
525
|
+
const violation = anchorViolation(finding, anchorFiles);
|
|
526
|
+
if (violation !== undefined || !findingAnchorInUnitEvidence(finding, unit, files)) {
|
|
527
|
+
return yield* rejectWork(
|
|
528
|
+
request.workId,
|
|
529
|
+
`discovery finding has no valid anchor in its assigned evidence: ${violation ?? finding.path}`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const findingCandidates = report.findings.map((finding, index) =>
|
|
534
|
+
FindingCandidate.make({
|
|
535
|
+
candidateId: `${request.workId}:finding:${candidateOrdinal(index)}`,
|
|
536
|
+
workId: request.workId,
|
|
537
|
+
unitId: request.unitId,
|
|
538
|
+
finding,
|
|
539
|
+
evidencePaths: [finding.path],
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
const concernCandidates = report.concerns.map((candidate, index) =>
|
|
543
|
+
ConcernCandidate.make({
|
|
544
|
+
candidateId: `${request.workId}:concern:${candidateOrdinal(index)}`,
|
|
545
|
+
workId: request.workId,
|
|
546
|
+
unitId: request.unitId,
|
|
547
|
+
concern: candidate.concern,
|
|
548
|
+
evidencePaths: candidate.evidencePaths,
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
return FileReviewUnitResult.make({
|
|
552
|
+
phase: report.phase,
|
|
553
|
+
workId: report.workId,
|
|
554
|
+
unitId: report.unitId,
|
|
555
|
+
candidates: [...findingCandidates, ...concernCandidates],
|
|
556
|
+
fileSummaries: report.fileSummaries,
|
|
557
|
+
assessments: [],
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const delegationDescription =
|
|
563
|
+
"Run exactly one host-planned discovery or candidate-verification child. Copy every plan field and candidate verbatim; never retry failed work.";
|
|
564
|
+
|
|
565
|
+
const makeFileReviewDelegation = (child: ReturnType<typeof makeFileReviewerDefinition>) =>
|
|
566
|
+
Subagent.define("delegate_file_review", {
|
|
567
|
+
description: delegationDescription,
|
|
568
|
+
target: child,
|
|
569
|
+
parameters: FileReviewRequest,
|
|
570
|
+
success: FileReviewUnitResult,
|
|
571
|
+
failure: FileReviewFailure,
|
|
572
|
+
failureMode: "return",
|
|
573
|
+
prepareInput: prepareReviewBrief,
|
|
574
|
+
projectResult: projectReviewResult,
|
|
575
|
+
policy: fileReviewPolicy,
|
|
576
|
+
});
|
|
246
577
|
|
|
247
578
|
export class ListReviewUnitsQuery extends Schema.Class<ListReviewUnitsQuery>(
|
|
248
579
|
"@effect-agent/pr-review/ListReviewUnitsQuery",
|
|
249
580
|
)({
|
|
250
|
-
/** Explicit constant keeps the zero-choice operation compatible with strict provider schemas. */
|
|
251
581
|
scope: Schema.Literal("all"),
|
|
252
582
|
}) {}
|
|
253
583
|
|
|
254
584
|
export const ListReviewUnits = Tool.make("list_review_units", {
|
|
255
585
|
description:
|
|
256
|
-
"List
|
|
586
|
+
"List deterministic bounded review units, explicit risk categories, every required discovery pass, and paths the pipeline cannot cover.",
|
|
257
587
|
parameters: ListReviewUnitsQuery,
|
|
258
588
|
success: ReviewUnitPlan,
|
|
259
589
|
failure: PullRequestSourceFailure,
|
|
@@ -273,64 +603,38 @@ export const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({
|
|
|
273
603
|
}),
|
|
274
604
|
});
|
|
275
605
|
|
|
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
606
|
export const makeFanOutReviewInstructions =
|
|
290
607
|
(options: FanOutInstructionOptions & { readonly maxFindings?: number | undefined } = {}) =>
|
|
291
608
|
(mission: ReviewMission): string => {
|
|
292
609
|
const maxFindings = clampMaxFindings(options.maxFindings);
|
|
293
610
|
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.",
|
|
611
|
+
`You coordinate the bounded multi-pass review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}.`,
|
|
612
|
+
mission.body.length > 0 ? `Author description:\n${mission.body}` : "No author description.",
|
|
298
613
|
...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.',
|
|
614
|
+
"1. Call list_review_units exactly once.",
|
|
615
|
+
'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.',
|
|
616
|
+
'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.',
|
|
617
|
+
"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.",
|
|
618
|
+
`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}.`,
|
|
619
|
+
"No configured pipeline can prove absence of defects. Describe settled work, never an exhaustive or defect-free review.",
|
|
308
620
|
].join("\n");
|
|
309
621
|
};
|
|
310
622
|
|
|
311
623
|
export const fanOutReviewInstructions = makeFanOutReviewInstructions();
|
|
312
624
|
|
|
313
|
-
/** The default fan-out coordinator execution bounds. */
|
|
314
625
|
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.
|
|
626
|
+
maxTurns: 7,
|
|
627
|
+
maxToolCalls: 1 + MAX_REVIEW_CHILDREN,
|
|
628
|
+
maxDuration: "20 minutes",
|
|
629
|
+
toolConcurrency: 4,
|
|
322
630
|
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.
|
|
631
|
+
tokenBudget: 400_000,
|
|
326
632
|
contextTokenLimit: 150_000,
|
|
327
|
-
//
|
|
328
|
-
//
|
|
633
|
+
// Coordinator exhaustion cannot become an assured result; exact stage
|
|
634
|
+
// settlement, not this final prose, determines assurance.
|
|
329
635
|
onExhaustion: "final-answer",
|
|
330
636
|
});
|
|
331
637
|
|
|
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
638
|
export interface FanOutReviewSuite {
|
|
335
639
|
readonly child: ReturnType<typeof makeFileReviewerDefinition>;
|
|
336
640
|
readonly parent: ReturnType<typeof makeFanOutReviewerDefinition>;
|
|
@@ -338,68 +642,22 @@ export interface FanOutReviewSuite {
|
|
|
338
642
|
}
|
|
339
643
|
|
|
340
644
|
const makeFileReviewerDefinition = (options: FanOutInstructionOptions = {}) =>
|
|
341
|
-
Agent.define("pr-
|
|
645
|
+
Agent.define("pr-review-worker", {
|
|
342
646
|
input: FileReviewBrief,
|
|
343
647
|
output: FileReviewReport,
|
|
344
648
|
instructions: makeFileReviewerInstructions(options),
|
|
345
649
|
toolkit: FileReviewToolkit,
|
|
346
650
|
policy: defaultFileReviewerPolicy,
|
|
347
651
|
description:
|
|
348
|
-
"
|
|
349
|
-
metadata: { deploymentClass: "E", surface: "read-only" },
|
|
652
|
+
"Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
|
|
653
|
+
metadata: { deploymentClass: "E", surface: "read-only", stage: "discovery-verification" },
|
|
350
654
|
});
|
|
351
655
|
|
|
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,
|
|
390
|
-
});
|
|
391
|
-
|
|
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
656
|
const delegationToolFor = (delegation: ReturnType<typeof makeFileReviewDelegation>) =>
|
|
399
657
|
delegation.tool.annotate(ToolExecutionClass, "readonly");
|
|
400
658
|
|
|
401
659
|
const makeFanOutReviewerDefinition = (
|
|
402
|
-
options:
|
|
660
|
+
options: FanOutInstructionOptions & { readonly maxFindings?: number | undefined },
|
|
403
661
|
delegation: ReturnType<typeof makeFileReviewDelegation>,
|
|
404
662
|
) =>
|
|
405
663
|
Agent.define("pr-fanout-reviewer", {
|
|
@@ -409,11 +667,19 @@ const makeFanOutReviewerDefinition = (
|
|
|
409
667
|
toolkit: Toolkit.make(ListReviewUnits, delegationToolFor(delegation)),
|
|
410
668
|
policy: defaultFanOutPolicy,
|
|
411
669
|
description:
|
|
412
|
-
"Coordinate
|
|
413
|
-
metadata: {
|
|
670
|
+
"Coordinate deterministic general/specialist discovery and independent candidate verification over bounded review units.",
|
|
671
|
+
metadata: {
|
|
672
|
+
deploymentClass: "E",
|
|
673
|
+
surface: "read-only",
|
|
674
|
+
delegation: "S1-attached",
|
|
675
|
+
assurance: "multi-pass",
|
|
676
|
+
},
|
|
414
677
|
});
|
|
415
678
|
|
|
416
|
-
|
|
679
|
+
export interface FanOutSuiteOptions extends FanOutInstructionOptions {
|
|
680
|
+
readonly maxFindings?: number | undefined;
|
|
681
|
+
}
|
|
682
|
+
|
|
417
683
|
export const makeFanOutReviewSuite = (options: FanOutSuiteOptions = {}): FanOutReviewSuite => {
|
|
418
684
|
const child = makeFileReviewerDefinition({ guidance: options.guidance });
|
|
419
685
|
const delegation = makeFileReviewDelegation(child);
|
|
@@ -426,29 +692,13 @@ export const makeFanOutReviewSuite = (options: FanOutSuiteOptions = {}): FanOutR
|
|
|
426
692
|
|
|
427
693
|
const defaultSuite = makeFanOutReviewSuite();
|
|
428
694
|
|
|
429
|
-
/** The default child Agent Definition. */
|
|
430
695
|
export const FileReviewer = defaultSuite.child;
|
|
431
|
-
|
|
432
|
-
/** The default coordinator Agent Definition. */
|
|
433
696
|
export const FanOutReviewer = defaultSuite.parent;
|
|
434
|
-
|
|
435
|
-
/** The default delegation over the default child. */
|
|
436
697
|
export const fileReviewDelegation = defaultSuite.delegation;
|
|
437
|
-
|
|
438
|
-
/** The default coordinator-facing delegation Tool (first-party contained mode). */
|
|
439
698
|
export const DelegateFileReview = delegationToolFor(fileReviewDelegation);
|
|
440
|
-
|
|
441
|
-
/** The default coordinator Toolkit. */
|
|
442
699
|
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
700
|
export const FileReviewDelegationFailure = fileReviewDelegation.containedFailure;
|
|
450
701
|
|
|
451
|
-
/** Runtime wiring: one delegation plus one explicit child Binding. */
|
|
452
702
|
export const fanOutHandlersLayerFor =
|
|
453
703
|
(delegation: ReturnType<typeof makeFileReviewDelegation>) =>
|
|
454
704
|
<Provider, ModelProvides, ModelRequires>(
|
|
@@ -466,5 +716,4 @@ export const fanOutHandlersLayerFor =
|
|
|
466
716
|
mapChildFailure: mapFileReviewChildFailure,
|
|
467
717
|
});
|
|
468
718
|
|
|
469
|
-
/** Runtime wiring over the default delegation, mirroring the leaf example. */
|
|
470
719
|
export const fanOutHandlersLayer = fanOutHandlersLayerFor(fileReviewDelegation);
|