@navels/neal 0.2.0 → 0.3.1
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 +206 -209
- package/SECURITY.md +16 -19
- package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
- package/dist/neal/adjudicator/execute.js +33 -33
- package/dist/neal/adjudicator/final-completion.js +2 -31
- package/dist/neal/adjudicator/planning.js +230 -38
- package/dist/neal/agents/prompts.js +6 -17
- package/dist/neal/agents/rounds.js +43 -73
- package/dist/neal/agents/schemas.js +47 -13
- package/dist/neal/blocked-guidance.js +16 -9
- package/dist/neal/cli.js +2 -2
- package/dist/neal/commands/check.js +2 -2
- package/dist/neal/commands/compat.js +100 -36
- package/dist/neal/commands/interactive-activity.js +2 -2
- package/dist/neal/commands/recovery-guidance.js +20 -8
- package/dist/neal/commands/runtime.js +4 -2
- package/dist/neal/config.js +16 -7
- package/dist/neal/context/inline-review-context.js +13 -28
- package/dist/neal/context/reviewer-context.js +19 -14
- package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
- package/dist/neal/eval/reviewer-eval.js +155 -0
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +49 -2
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +117 -80
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/transitions.js +7 -1
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/execute.js +7 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +7 -17
- package/dist/neal/prompts/specs.js +180 -32
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +18 -1
- package/dist/neal/providers/detection.js +3 -7
- package/dist/neal/providers/git-config-isolation.js +83 -0
- package/dist/neal/providers/openai-codex.js +25 -3
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1078 -430
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +1 -1
- package/dist/neal/providers/rate-card.js +6 -32
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/types.js +12 -0
- package/dist/neal/resume-decision.js +7 -6
- package/dist/neal/resume-planner.js +9 -0
- package/dist/neal/retrospective.js +3 -3
- package/dist/neal/review-debt.js +30 -0
- package/dist/neal/review-findings/provider.js +17 -29
- package/dist/neal/review.js +17 -1
- package/dist/neal/state-invariants.js +11 -3
- package/dist/neal/state-views.js +27 -5
- package/dist/neal/state.js +59 -15
- package/dist/neal/status.js +28 -1
- package/dist/neal/support.js +3 -3
- package/docs/README.md +22 -18
- package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
- package/docs/architecture.md +79 -83
- package/docs/automation.md +11 -11
- package/docs/compat.md +91 -61
- package/docs/compatible-models.md +145 -121
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +65 -34
- package/docs/plan-format.md +61 -33
- package/docs/prompt-evals.md +96 -0
- package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
- package/docs/providers.md +279 -349
- package/docs/release.md +63 -81
- package/docs/review-convergence.md +246 -0
- package/docs/state-machine.md +61 -59
- package/docs/storage.md +40 -31
- package/docs/troubleshooting.md +32 -29
- package/examples/compat/add-edit-verify/PLAN.md +2 -0
- package/examples/compat/add-edit-verify/broken.diff +2 -2
- package/examples/compat/add-edit-verify/good.diff +2 -2
- package/examples/compat/add-edit-verify/src/add.js +1 -1
- package/examples/compat/manifest.json +25 -29
- package/examples/compat/sum-grep-edit/PLAN.md +33 -0
- package/examples/compat/sum-grep-edit/broken.diff +12 -0
- package/examples/compat/sum-grep-edit/good.diff +12 -0
- package/examples/compat/sum-grep-edit/package.json +5 -0
- package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
- package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
- package/examples/issue-triage-js/README.md +0 -15
- package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
- package/examples/reviewer-eval/clean-doc/change.diff +9 -0
- package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
- package/examples/reviewer-eval/clean-extract/change.diff +13 -0
- package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
- package/examples/reviewer-eval/dropped-error/change.diff +15 -0
- package/examples/reviewer-eval/manifest.json +45 -0
- package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
- package/examples/reviewer-eval/off-by-one/change.diff +11 -0
- package/{config.yml → neal.yml} +1 -1
- package/package.json +15 -14
- package/dist/neal/providers/generic-agentic.js +0 -1256
- package/docs/comparison.md +0 -105
- package/docs/issue-pipeline.md +0 -124
- package/examples/compat/is-even-add-test/PLAN.md +0 -30
- package/examples/compat/is-even-add-test/broken.diff +0 -11
- package/examples/compat/is-even-add-test/good.diff +0 -11
- package/examples/compat/is-even-add-test/package.json +0 -5
- package/examples/compat/is-even-add-test/src/is-even.js +0 -3
- package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
- package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
- package/examples/compat/reverse-grep-edit/broken.diff +0 -12
- package/examples/compat/reverse-grep-edit/good.diff +0 -12
- package/examples/compat/reverse-grep-edit/package.json +0 -5
- package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
- package/examples/compat/reverse-grep-edit/test/strings.test.js +0 -15
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { runCoderPlanResponseRound, runPlanReviewerRound } from '../agents.js';
|
|
3
|
-
import {
|
|
3
|
+
import { readTextForInlineSection } from '../context/inline-review-context.js';
|
|
4
4
|
import { buildAndPersistReviewerContextPacket } from '../context/reviewer-context.js';
|
|
5
5
|
import { getProviderDefinition } from '../providers/registry.js';
|
|
6
6
|
import { getDerivedPlanView } from '../state-views.js';
|
|
7
|
+
import { findCanonicalId, getNextCanonicalIndex, getOpenBlockingCanonicalSet, getReopenedCanonical, hasRepeatedUnresolvedBlockingCanonicalsForWindow, isOpenBlockingFinding, isOpenNonBlockingFinding, } from './execute.js';
|
|
8
|
+
import { toPlanReviewDebt } from '../review-debt.js';
|
|
7
9
|
import { getAdjudicationSpec } from './specs.js';
|
|
8
10
|
export function isDerivedPlanReviewState(state) {
|
|
9
11
|
return getDerivedPlanView(state)?.reviewActive ?? false;
|
|
10
12
|
}
|
|
11
13
|
// The planner role runs through the coder adapter, so its session-resume capability comes from
|
|
12
14
|
// the provider's coder capabilities. Providers that declare `supportsSessionResume: false`
|
|
13
|
-
// (openai-compatible
|
|
15
|
+
// (openai-compatible) never persist a planner session handle — a null handle is
|
|
14
16
|
// their expected steady state, so top-level plan-refinement responses must start a fresh planner
|
|
15
17
|
// session for them instead of treating the missing handle as corrupted state. Resume-capable
|
|
16
18
|
// planner providers keep the hard invariant: a missing handle there is a genuine violation.
|
|
@@ -42,6 +44,28 @@ export function resolvePlanningAdjudicationContext(state) {
|
|
|
42
44
|
// Mirrors resolveExecuteReviewDisposition in adjudicator/execute.ts: one resolver derives every
|
|
43
45
|
// disposition-dependent value runPlanReviewPhase persists (plus the transition signal it asserts)
|
|
44
46
|
// from the same condition set, so the five outcomes can never drift apart.
|
|
47
|
+
// The plan-review acceptance transition, reused by clean acceptance and by the
|
|
48
|
+
// accept-with-recorded-reservations landing so both emit identical transition
|
|
49
|
+
// signals (`accept_plan` for top-level plan review, `accept_derived_plan` for a
|
|
50
|
+
// derived-plan review) — no adjudication-spec change is needed.
|
|
51
|
+
function planReviewAcceptanceDisposition(derivedPlanReview, currentDerivedPlanStatus) {
|
|
52
|
+
if (derivedPlanReview) {
|
|
53
|
+
return {
|
|
54
|
+
planningSignal: 'accept_derived_plan',
|
|
55
|
+
phase: 'awaiting_derived_plan_execution',
|
|
56
|
+
status: 'running',
|
|
57
|
+
derivedPlanStatus: 'accepted',
|
|
58
|
+
blockedFromPhase: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
planningSignal: 'accept_plan',
|
|
63
|
+
phase: 'done',
|
|
64
|
+
status: 'done',
|
|
65
|
+
derivedPlanStatus: currentDerivedPlanStatus,
|
|
66
|
+
blockedFromPhase: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
45
69
|
export function resolvePlanReviewDisposition(args) {
|
|
46
70
|
if (args.shouldBlockForConvergence) {
|
|
47
71
|
return {
|
|
@@ -52,7 +76,12 @@ export function resolvePlanReviewDisposition(args) {
|
|
|
52
76
|
blockedFromPhase: 'reviewer_plan',
|
|
53
77
|
};
|
|
54
78
|
}
|
|
55
|
-
|
|
79
|
+
// Round-forcing blocking findings keep their precedence over the
|
|
80
|
+
// accept-with-debt landing: any open plan-correctness / non-convertible /
|
|
81
|
+
// repeat blocker still forces a revision round (or blocks at the cap), even
|
|
82
|
+
// when convertible hardening findings are also present. Because roundForcing
|
|
83
|
+
// and landAcceptedWithDebt are mutually exclusive, no guard is needed here.
|
|
84
|
+
if (args.hasRoundForcingBlockingFindings) {
|
|
56
85
|
return {
|
|
57
86
|
planningSignal: args.reachedMaxRounds ? 'block_for_operator' : 'request_revision',
|
|
58
87
|
phase: args.reachedMaxRounds ? 'blocked' : 'coder_plan_response',
|
|
@@ -61,6 +90,9 @@ export function resolvePlanReviewDisposition(args) {
|
|
|
61
90
|
blockedFromPhase: args.reachedMaxRounds ? 'reviewer_plan' : null,
|
|
62
91
|
};
|
|
63
92
|
}
|
|
93
|
+
if (args.landAcceptedWithDebt) {
|
|
94
|
+
return planReviewAcceptanceDisposition(args.derivedPlanReview, args.currentDerivedPlanStatus);
|
|
95
|
+
}
|
|
64
96
|
if (args.hasOpenNonBlockingFindings) {
|
|
65
97
|
return {
|
|
66
98
|
planningSignal: 'optional_revision',
|
|
@@ -70,35 +102,202 @@ export function resolvePlanReviewDisposition(args) {
|
|
|
70
102
|
blockedFromPhase: null,
|
|
71
103
|
};
|
|
72
104
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
return planReviewAcceptanceDisposition(args.derivedPlanReview, args.currentDerivedPlanStatus);
|
|
106
|
+
}
|
|
107
|
+
// Pure, class- and novelty-aware partition of the current round's open blocking
|
|
108
|
+
// findings, shared by the arrival-time conversion and the disposition-time
|
|
109
|
+
// landing. The debt-convertibility predicate depends on whether we are at the
|
|
110
|
+
// round cap:
|
|
111
|
+
// Pre-cap (atCap === false): verification_hardening (an explicit match — an
|
|
112
|
+
// absent/undefined class never converts) AND first-occurrence of the canonical
|
|
113
|
+
// (its minimum round across all merged findings equals the current round) AND
|
|
114
|
+
// round >= threshold. Everything else is round-forcing: plan_correctness or any
|
|
115
|
+
// non-hardening/absent class, OR any repeat occurrence of an existing canonical
|
|
116
|
+
// (which still forces a revision round), OR below threshold.
|
|
117
|
+
// At the cap (atCap === true): there are no rounds left to force, so the
|
|
118
|
+
// predicate is class-only — every open verification_hardening finding converts
|
|
119
|
+
// to debt regardless of first-occurrence or threshold, and only a
|
|
120
|
+
// plan_correctness / non-hardening / absent-class finding stays round-forcing
|
|
121
|
+
// (which terminal-blocks the run). A re-litigated-but-not-reopened hardening
|
|
122
|
+
// point therefore converts at the cap rather than failing the run.
|
|
123
|
+
// The explicit-hardening gate is the system-wide fail-safe: an unclassified
|
|
124
|
+
// finding is always round-forcing (blocking). The 3-round reopen and stall
|
|
125
|
+
// terminal blocks are handled separately by shouldBlockForConvergence, which
|
|
126
|
+
// keeps precedence over this partition, so a re-opened or stalled hardening
|
|
127
|
+
// finding — even at the cap — keeps blocking rather than converting.
|
|
128
|
+
export function classifyPlanReviewConvergence(args) {
|
|
129
|
+
const minRoundByCanonical = new Map();
|
|
130
|
+
for (const finding of args.mergedFindings) {
|
|
131
|
+
const existing = minRoundByCanonical.get(finding.canonicalId);
|
|
132
|
+
if (existing === undefined || finding.round < existing) {
|
|
133
|
+
minRoundByCanonical.set(finding.canonicalId, finding.round);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const debtConvertible = [];
|
|
137
|
+
const roundForcing = [];
|
|
138
|
+
for (const finding of args.mergedFindings.filter(isOpenBlockingFinding)) {
|
|
139
|
+
const isHardening = finding.findingClass === 'verification_hardening';
|
|
140
|
+
const firstOccurrence = minRoundByCanonical.get(finding.canonicalId) === args.round;
|
|
141
|
+
const convertible = args.atCap
|
|
142
|
+
? isHardening
|
|
143
|
+
: isHardening && firstOccurrence && args.round >= args.debtRoundThreshold;
|
|
144
|
+
if (convertible) {
|
|
145
|
+
debtConvertible.push(finding);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
roundForcing.push(finding);
|
|
149
|
+
}
|
|
81
150
|
}
|
|
82
151
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
derivedPlanStatus: args.currentDerivedPlanStatus,
|
|
87
|
-
blockedFromPhase: null,
|
|
152
|
+
debtConvertible,
|
|
153
|
+
roundForcing,
|
|
154
|
+
landAcceptedWithDebt: roundForcing.length === 0 && debtConvertible.length > 0,
|
|
88
155
|
};
|
|
89
156
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
157
|
+
// A derived-plan review is split-plan recovery; frame its block reasons so an
|
|
158
|
+
// operator sees that the recovery (not the top-level plan) failed to converge.
|
|
159
|
+
// `derivedPlanReview` is exactly `isDerivedPlanReviewState(state)` (both read
|
|
160
|
+
// `getDerivedPlanView(state)?.reviewActive`), so callers pass the boolean they
|
|
161
|
+
// already resolved from the planning adjudication context.
|
|
162
|
+
export function getDerivedPlanBlockedReason(derivedPlanReview, reason) {
|
|
163
|
+
if (!derivedPlanReview) {
|
|
164
|
+
return reason;
|
|
98
165
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
166
|
+
return `split-plan recovery failed to converge: ${reason}`;
|
|
167
|
+
}
|
|
168
|
+
// The single pure gating/disposition policy for a plan-review round, shared by
|
|
169
|
+
// the runtime (synthesizePlanReviewRound, which assigns canonicals from the
|
|
170
|
+
// reviewer's raw inputs) and the replay harness (which supplies findings with
|
|
171
|
+
// trusted, recorded canonical IDs). Given the current round's already-assigned
|
|
172
|
+
// findings plus the prior findings on state.findings, it derives the merged set,
|
|
173
|
+
// open sets, convergence signals, disposition, blockReason, and round record. It
|
|
174
|
+
// performs no file, git, agent, or configuration IO; the shared stall helper is
|
|
175
|
+
// invoked through its window-taking pure variant.
|
|
176
|
+
export function synthesizePlanReviewRoundFromFindings(args) {
|
|
177
|
+
const { findings } = args;
|
|
178
|
+
const rawMergedFindings = [...args.state.findings, ...findings];
|
|
179
|
+
const reachedMaxRounds = args.round >= args.roundLimit;
|
|
180
|
+
const rawOpenBlockingCanonicalSet = getOpenBlockingCanonicalSet(rawMergedFindings);
|
|
181
|
+
// The convergence block (reopen / stall) is derived from the pre-conversion
|
|
182
|
+
// merged set and keeps precedence over the accept-with-debt landing.
|
|
183
|
+
const stalledBlockingCount = hasRepeatedUnresolvedBlockingCanonicalsForWindow({
|
|
184
|
+
rounds: args.state.rounds,
|
|
185
|
+
currentOpenBlockingCanonicals: rawOpenBlockingCanonicalSet,
|
|
186
|
+
reviewStuckWindow: args.reviewStuckWindow,
|
|
187
|
+
});
|
|
188
|
+
const reopenedCanonical = getReopenedCanonical(rawMergedFindings);
|
|
189
|
+
const shouldBlockForConvergence = Boolean(reopenedCanonical || stalledBlockingCount);
|
|
190
|
+
// Class- and novelty-aware partition of the open blocking findings. Pre-cap, a
|
|
191
|
+
// first-occurrence past-threshold hardening finding is debt-convertible; at the
|
|
192
|
+
// cap (reachedMaxRounds) every open hardening finding is convertible regardless
|
|
193
|
+
// of first-occurrence, so a re-litigated-but-not-reopened hardening point
|
|
194
|
+
// converts instead of failing the run. When every open blocking finding is
|
|
195
|
+
// convertible (and none is round-forcing), the round lands accepted-with-debt
|
|
196
|
+
// and those findings convert to deferred plan-review debt so they leave the
|
|
197
|
+
// open set.
|
|
198
|
+
const convergence = classifyPlanReviewConvergence({
|
|
199
|
+
mergedFindings: rawMergedFindings,
|
|
200
|
+
round: args.round,
|
|
201
|
+
debtRoundThreshold: args.debtRoundThreshold,
|
|
202
|
+
atCap: reachedMaxRounds,
|
|
203
|
+
});
|
|
204
|
+
// Drive the blocking gate off the merged open round-forcing set, not the
|
|
205
|
+
// current reviewer payload. An empty reviewer round over a prior still-open
|
|
206
|
+
// blocker must keep forcing a revision (or block at the cap), never accept.
|
|
207
|
+
const hasRoundForcingBlockingFindings = convergence.roundForcing.length > 0;
|
|
208
|
+
const disposition = resolvePlanReviewDisposition({
|
|
209
|
+
shouldBlockForConvergence,
|
|
210
|
+
hasRoundForcingBlockingFindings,
|
|
211
|
+
landAcceptedWithDebt: !shouldBlockForConvergence && convergence.landAcceptedWithDebt,
|
|
212
|
+
reachedMaxRounds,
|
|
213
|
+
hasOpenNonBlockingFindings: rawMergedFindings.some(isOpenNonBlockingFinding),
|
|
214
|
+
derivedPlanReview: args.derivedPlanReview,
|
|
215
|
+
currentDerivedPlanStatus: args.currentDerivedPlanStatus,
|
|
216
|
+
});
|
|
217
|
+
// Arrival-time debt conversion (Scope 4): pre-cap, a first-occurrence
|
|
218
|
+
// past-threshold hardening finding banks as deferred plan-review debt the moment
|
|
219
|
+
// it arrives, even when a co-occurring plan_correctness / repeat finding forces a
|
|
220
|
+
// revision this round — so novelty-bounded hardening asks never extend the round
|
|
221
|
+
// count. At the cap the convertible set is class-based (every open hardening
|
|
222
|
+
// finding), but it only banks when the round actually lands accepted-with-debt: a
|
|
223
|
+
// co-occurring plan_correctness finding terminal-blocks instead of converting.
|
|
224
|
+
// The convergence block (reopen/stall) keeps precedence: when it fires nothing
|
|
225
|
+
// converts, so a re-opened or stalled hardening finding keeps blocking. The
|
|
226
|
+
// open-blocking round record then reflects the post-flip set.
|
|
227
|
+
const shouldConvertDebt = !shouldBlockForConvergence &&
|
|
228
|
+
(reachedMaxRounds ? convergence.landAcceptedWithDebt : convergence.debtConvertible.length > 0);
|
|
229
|
+
const convertedIds = new Set(shouldConvertDebt ? convergence.debtConvertible.map((finding) => finding.id) : []);
|
|
230
|
+
const mergedFindings = convertedIds.size
|
|
231
|
+
? rawMergedFindings.map((finding) => convertedIds.has(finding.id) ? { ...finding, status: 'deferred' } : finding)
|
|
232
|
+
: rawMergedFindings;
|
|
233
|
+
const openBlockingCanonicalSet = convertedIds.size
|
|
234
|
+
? getOpenBlockingCanonicalSet(mergedFindings)
|
|
235
|
+
: rawOpenBlockingCanonicalSet;
|
|
236
|
+
const openBlockingCanonicalIds = [...openBlockingCanonicalSet].sort();
|
|
237
|
+
const openBlockingCanonicalCount = openBlockingCanonicalSet.size;
|
|
238
|
+
const blockReason = reopenedCanonical
|
|
239
|
+
? getDerivedPlanBlockedReason(args.derivedPlanReview, `review_stuck: blocking finding ${reopenedCanonical} reopened across multiple reviewer rounds`)
|
|
240
|
+
: stalledBlockingCount
|
|
241
|
+
? getDerivedPlanBlockedReason(args.derivedPlanReview, `review_stuck: blocking findings did not decrease across ${args.reviewStuckWindow} consecutive reviewer rounds`)
|
|
242
|
+
: reachedMaxRounds && hasRoundForcingBlockingFindings
|
|
243
|
+
? getDerivedPlanBlockedReason(args.derivedPlanReview, `reached max review rounds (${args.roundLimit}) with blocking findings still open`)
|
|
244
|
+
: null;
|
|
245
|
+
return {
|
|
246
|
+
findings,
|
|
247
|
+
mergedFindings,
|
|
248
|
+
disposition,
|
|
249
|
+
blockReason,
|
|
250
|
+
openBlockingCanonicalCount,
|
|
251
|
+
planReviewDebt: toPlanReviewDebt(mergedFindings),
|
|
252
|
+
roundRecord: {
|
|
253
|
+
round: args.round,
|
|
254
|
+
reviewerSessionHandle: args.reviewerSessionHandle,
|
|
255
|
+
reviewedPlanPath: args.reviewedPlanPath,
|
|
256
|
+
normalizationApplied: args.normalizationApplied,
|
|
257
|
+
normalizationOperations: args.normalizationOperations,
|
|
258
|
+
normalizationScopeLabelMappings: args.normalizationScopeLabelMappings,
|
|
259
|
+
commitRange: args.commitRange,
|
|
260
|
+
openBlockingCanonicalCount,
|
|
261
|
+
openBlockingCanonicalIds,
|
|
262
|
+
findings: findings.map((finding) => finding.id),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Runtime plan-review round core, mirroring synthesizeExecuteReviewerState in
|
|
267
|
+
// adjudicator/execute.ts: it assigns canonical IDs to the reviewer's raw finding
|
|
268
|
+
// inputs (via findCanonicalId), then routes them through the shared
|
|
269
|
+
// synthesizePlanReviewRoundFromFindings policy above, so runtime and replay can
|
|
270
|
+
// never drift apart. Config is resolved by the caller (reviewStuckWindow) so this
|
|
271
|
+
// stays IO-free.
|
|
272
|
+
export function synthesizePlanReviewRound(args) {
|
|
273
|
+
let nextCanonicalIndex = getNextCanonicalIndex(args.state.findings);
|
|
274
|
+
const findings = args.findingInputs.map((finding, index) => {
|
|
275
|
+
const canonicalId = findCanonicalId(args.state.findings, finding) ?? `C${nextCanonicalIndex++}`;
|
|
276
|
+
return {
|
|
277
|
+
...finding,
|
|
278
|
+
id: `R${args.round}-F${index + 1}`,
|
|
279
|
+
canonicalId,
|
|
280
|
+
status: 'open',
|
|
281
|
+
coderDisposition: null,
|
|
282
|
+
coderCommit: null,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
return synthesizePlanReviewRoundFromFindings({
|
|
286
|
+
state: args.state,
|
|
287
|
+
round: args.round,
|
|
288
|
+
roundLimit: args.roundLimit,
|
|
289
|
+
reviewStuckWindow: args.reviewStuckWindow,
|
|
290
|
+
debtRoundThreshold: args.debtRoundThreshold,
|
|
291
|
+
derivedPlanReview: args.derivedPlanReview,
|
|
292
|
+
currentDerivedPlanStatus: args.currentDerivedPlanStatus,
|
|
293
|
+
reviewerSessionHandle: args.reviewerSessionHandle,
|
|
294
|
+
reviewedPlanPath: args.reviewedPlanPath,
|
|
295
|
+
normalizationApplied: args.normalizationApplied,
|
|
296
|
+
normalizationOperations: args.normalizationOperations,
|
|
297
|
+
normalizationScopeLabelMappings: args.normalizationScopeLabelMappings,
|
|
298
|
+
commitRange: args.commitRange,
|
|
299
|
+
findings,
|
|
300
|
+
});
|
|
102
301
|
}
|
|
103
302
|
export async function runPlanningReviewerAdjudication(args) {
|
|
104
303
|
const context = resolvePlanningAdjudicationContext(args.state);
|
|
@@ -106,22 +305,16 @@ export async function runPlanningReviewerAdjudication(args) {
|
|
|
106
305
|
planPath: context.reviewTargetPath,
|
|
107
306
|
normalizedPlanPath: args.normalizedPlanPath,
|
|
108
307
|
});
|
|
109
|
-
// Plan and review-history contents are inlined only for reviewers without
|
|
110
|
-
// repository read access; read-capable reviewers inspect them directly.
|
|
111
|
-
const inlineContext = reviewerNeedsInlineContext(args.state.agentConfig.reviewer)
|
|
112
|
-
? await buildPlanReviewerInlineContext({
|
|
113
|
-
state: args.state,
|
|
114
|
-
context,
|
|
115
|
-
reviewedPlanPath: preparedReview.reviewedPlanPath,
|
|
116
|
-
reviewMarkdownPath: args.reviewMarkdownPath,
|
|
117
|
-
})
|
|
118
|
-
: null;
|
|
119
308
|
const reviewedPlanContent = await readTextForInlineSection(resolve(args.state.cwd, preparedReview.reviewedPlanPath));
|
|
120
309
|
const parentPlanContent = context.reviewMode === 'derived-plan' && context.parentPlanDoc
|
|
121
310
|
? await readTextForInlineSection(resolve(args.state.cwd, context.parentPlanDoc))
|
|
122
311
|
: null;
|
|
123
312
|
const reviewerResult = await (args.runReviewerRound ?? runPlanReviewerRound)({
|
|
124
313
|
reviewer: args.state.agentConfig.reviewer,
|
|
314
|
+
// Resume the reviewer's own session from the previous refinement round
|
|
315
|
+
// (null on round 1: fresh runs and split-plan recovery both clear it).
|
|
316
|
+
// Sessionless providers are filtered at the round layer.
|
|
317
|
+
resumeHandle: args.state.reviewerSessionHandle,
|
|
125
318
|
cwd: args.state.cwd,
|
|
126
319
|
planDoc: preparedReview.reviewedPlanPath,
|
|
127
320
|
round: args.round,
|
|
@@ -132,7 +325,6 @@ export async function runPlanningReviewerAdjudication(args) {
|
|
|
132
325
|
reviewerContext: await buildAndPersistReviewerContextPacket({ state: args.state }),
|
|
133
326
|
reviewedPlanContent,
|
|
134
327
|
parentPlanContent,
|
|
135
|
-
inlineContext,
|
|
136
328
|
unattended: args.state.unattended,
|
|
137
329
|
// Reinforce an author-declared `one_shot` only for the top-level authored plan; derived
|
|
138
330
|
// plans declare and own their own shape and are never clamped.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { assertNoReadPromptInstructionText, renderInlineReviewerContext, } from '../context/inline-review-context.js';
|
|
2
|
-
import { renderReviewerContextMarkdown } from '../context/reviewer-context.js';
|
|
3
2
|
import { AUTONOMY_BLOCKED as SHARED_AUTONOMY_BLOCKED, AUTONOMY_CHUNK_DONE as SHARED_AUTONOMY_CHUNK_DONE, AUTONOMY_DONE as SHARED_AUTONOMY_DONE, AUTONOMY_SCOPE_DONE as SHARED_AUTONOMY_SCOPE_DONE, AUTONOMY_SPLIT_PLAN as SHARED_AUTONOMY_SPLIT_PLAN, buildProgressSection, getCanonicalPlanContractLines, getProtocolMarkerArtifactProhibitionLines, getStandalonePlanPayloadSourceOfTruthLines, } from '../prompts/shared.js';
|
|
4
3
|
import { getUserGuidanceLines } from '../prompts/guidance.js';
|
|
5
4
|
import { assertPromptBuilder, resolvePrimaryVariant } from '../prompts/assert-builder.js';
|
|
@@ -11,18 +10,18 @@ export const AUTONOMY_CHUNK_DONE = SHARED_AUTONOMY_CHUNK_DONE;
|
|
|
11
10
|
export const AUTONOMY_DONE = SHARED_AUTONOMY_DONE;
|
|
12
11
|
export const AUTONOMY_SCOPE_DONE = SHARED_AUTONOMY_SCOPE_DONE;
|
|
13
12
|
export const AUTONOMY_SPLIT_PLAN = SHARED_AUTONOMY_SPLIT_PLAN;
|
|
14
|
-
// Single no-read-safe prompt variant for the read-only blocked-run
|
|
13
|
+
// Single no-read-safe prompt variant for the read-only blocked-run consultant.
|
|
15
14
|
// It generalizes across every triaged block class (a coder-blocked signal or a
|
|
16
15
|
// reviewer `review_stuck` deadlock): it takes a NON-null InlineReviewerContext
|
|
17
16
|
// (the round runner narrows before calling it) and judges entirely from whatever
|
|
18
17
|
// sections that context supplies, making no assumptions about which sections are
|
|
19
18
|
// present. The static Neal-authored instructions must never reference
|
|
20
19
|
// repository/file/shell access, so they pass through the shared no-read guard.
|
|
21
|
-
export function
|
|
22
|
-
const spec = assertPromptBuilder('
|
|
23
|
-
resolvePrimaryVariant(spec, '
|
|
20
|
+
export function buildConsultantPrompt(args) {
|
|
21
|
+
const spec = assertPromptBuilder('consultant', 'buildConsultantPrompt', 'src/neal/agents/prompts.ts');
|
|
22
|
+
resolvePrimaryVariant(spec, 'consultant');
|
|
24
23
|
const staticInstructionLines = [
|
|
25
|
-
'You are a read-only
|
|
24
|
+
'You are a read-only consultant for a blocked neal run. The run has stopped because a coder or reviewer turn reported a blocker it could not resolve on its own.',
|
|
26
25
|
'Judge entirely from the inlined context sections below. You make no changes, run nothing, and look at nothing outside this prompt.',
|
|
27
26
|
'Decide whether the block is an autonomously recoverable misunderstanding that can be resolved WITHIN the existing scope, or a genuine wall that must escalate to a human.',
|
|
28
27
|
'Return `recoverable:true` with `triageCategory:"misunderstanding"` and a concrete, in-scope `resolutionDirective` ONLY when ALL of these hold:',
|
|
@@ -37,7 +36,7 @@ export function buildBlockedAdjudicatorPrompt(args) {
|
|
|
37
36
|
'If the inlined context lists specific blocking findings, set `targetCanonicalIds` to the canonical ids your verdict addresses; otherwise leave it empty.',
|
|
38
37
|
'Always provide a non-empty `rationale` explaining the triage.',
|
|
39
38
|
];
|
|
40
|
-
assertNoReadPromptInstructionText(staticInstructionLines.join('\n'), '
|
|
39
|
+
assertNoReadPromptInstructionText(staticInstructionLines.join('\n'), 'buildConsultantPrompt');
|
|
41
40
|
return [
|
|
42
41
|
...staticInstructionLines,
|
|
43
42
|
'',
|
|
@@ -47,16 +46,6 @@ export function buildBlockedAdjudicatorPrompt(args) {
|
|
|
47
46
|
renderInlineReviewerContext(args.inlineContext),
|
|
48
47
|
].join('\n');
|
|
49
48
|
}
|
|
50
|
-
function getReviewerContextLines(reviewerContext, mode = 'tool-access') {
|
|
51
|
-
if (!reviewerContext) {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
return [
|
|
55
|
-
'',
|
|
56
|
-
'Bounded current-run continuity context:',
|
|
57
|
-
mode === 'inline' ? renderReviewerContextMarkdown(reviewerContext, { mode: 'inline' }) : reviewerContext.promptMarkdown,
|
|
58
|
-
];
|
|
59
|
-
}
|
|
60
49
|
export function buildBlockedRecoveryCoderPrompt(args) {
|
|
61
50
|
const allowReplacement = args.allowReplacement ?? true;
|
|
62
51
|
const actionLines = [
|
|
@@ -2,12 +2,12 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getInactivityTimeoutMs, } from '../config.js';
|
|
3
3
|
import { runWithAgentTurnLiveness } from '../providers/liveness.js';
|
|
4
4
|
import { normalizeExecutionShapeDeclaration } from '../plan-validation.js';
|
|
5
|
-
import { getCoderAdapter, getStructuredAdvisorAdapter } from '../providers/registry.js';
|
|
5
|
+
import { getCoderAdapter, getProviderDefinition, getStructuredAdvisorAdapter } from '../providers/registry.js';
|
|
6
6
|
import { createProviderTelemetrySink } from '../providers/telemetry.js';
|
|
7
7
|
import { isNealProviderError, NealProviderError } from '../providers/types.js';
|
|
8
|
-
import { getReviewerDoctrineAccessMode,
|
|
9
|
-
import { AUTONOMY_BLOCKED, AUTONOMY_CHUNK_DONE, AUTONOMY_DONE, AUTONOMY_SCOPE_DONE, AUTONOMY_SPLIT_PLAN, buildBlockedRecoveryCoderPrompt, buildCoderResponsePrompt, buildCoderPlanResponsePrompt, buildFinalCompletionReviewerPrompt, buildFinalCompletionSummaryPrompt, buildLegacyScopePrompt, buildLegacyPlanningPrompt, buildPlanReviewerPrompt, buildPlanningPrompt, buildReviewerPrompt,
|
|
10
|
-
import { buildCoderBlockedRecoveryDispositionSchema, buildCoderPlanSchema, buildCoderScopeSchema, buildCoderPlanResponseSchema, buildCoderResponseSchema,
|
|
8
|
+
import { getReviewerDoctrineAccessMode, } from '../context/inline-review-context.js';
|
|
9
|
+
import { AUTONOMY_BLOCKED, AUTONOMY_CHUNK_DONE, AUTONOMY_DONE, AUTONOMY_SCOPE_DONE, AUTONOMY_SPLIT_PLAN, buildBlockedRecoveryCoderPrompt, buildCoderResponsePrompt, buildCoderPlanResponsePrompt, buildFinalCompletionReviewerPrompt, buildFinalCompletionSummaryPrompt, buildLegacyScopePrompt, buildLegacyPlanningPrompt, buildPlanReviewerPrompt, buildPlanningPrompt, buildReviewerPrompt, buildConsultantPrompt, buildScopePrompt, } from './prompts.js';
|
|
10
|
+
import { buildCoderBlockedRecoveryDispositionSchema, buildCoderPlanSchema, buildCoderScopeSchema, buildCoderPlanResponseSchema, buildCoderResponseSchema, buildConsultantSchema, buildFinalCompletionSummarySchema, buildFinalCompletionReviewerSchema, buildPlanReviewerSchema, buildReviewerSchema, parseExecuteScopeProgressPayload, parseFinalCompletionReviewerPayload, parseFinalCompletionSummaryPayload, stripExecuteScopeProgressPayload, validateCoderBlockedRecoveryDispositionPayload, validateCoderPlanPayload, validateCoderPlanResponsePayload, validateCoderResponsePayload, validateCoderScopePayload, validatePlanReviewerPayload, validateConsultantVerdictPayload, validateReviewerPayload, } from './schemas.js';
|
|
11
11
|
import { runCoderStructuredPrompt, translateCoderProviderError } from './structured-coder.js';
|
|
12
12
|
export class ReviewerRoundError extends Error {
|
|
13
13
|
sessionHandle;
|
|
@@ -110,37 +110,20 @@ function createStructuredAdvisorProviderEventSink(args) {
|
|
|
110
110
|
cwd: args.cwd,
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// orchestrator reviewer failure path as any other reviewer round failure
|
|
125
|
-
// (persisted failed state, written artifacts, logged phase errors).
|
|
126
|
-
function assertReviewerRoundSupportsNoReadReviewer(args) {
|
|
127
|
-
if (args.inlineContextSupplied || !reviewerNeedsInlineContext(args.reviewer)) {
|
|
128
|
-
return;
|
|
113
|
+
// Resolves the session handle a reviewer round may resume: the previous
|
|
114
|
+
// round's handle when the provider's structured-advisor capability declares
|
|
115
|
+
// session resume, null otherwise (sessionless providers like
|
|
116
|
+
// openai-compatible must never receive one — openai-compatible treats a
|
|
117
|
+
// non-null resume handle as corrupted state).
|
|
118
|
+
// Reviewer continuity within a review engagement is the original design:
|
|
119
|
+
// a continuing reviewer converges like a human-shuttled review chat, while
|
|
120
|
+
// a cold-started reviewer re-audits from scratch every round.
|
|
121
|
+
export function resolveReviewerResumeHandle(reviewer, resumeHandle) {
|
|
122
|
+
if (!resumeHandle) {
|
|
123
|
+
return null;
|
|
129
124
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`Reviewer provider ${JSON.stringify(args.reviewer.provider)} has no repository read access,`,
|
|
133
|
-
`and the ${args.label} reviewer round did not receive Neal-inlined reviewer context,`,
|
|
134
|
-
'so its prompt would contain repository-access instructions the provider cannot follow.',
|
|
135
|
-
'Configure a read-capable reviewer provider (agent.reviewer.provider) for runs that reach',
|
|
136
|
-
`${args.label} rounds until this round supports inline reviewer context.`,
|
|
137
|
-
].join(' '),
|
|
138
|
-
provider: args.reviewer.provider,
|
|
139
|
-
role: 'structured-advisor',
|
|
140
|
-
sessionHandle: null,
|
|
141
|
-
kind: 'provider_failed',
|
|
142
|
-
retryable: false,
|
|
143
|
-
}));
|
|
125
|
+
const capabilities = getProviderDefinition(reviewer.provider).capabilities['structured-advisor'];
|
|
126
|
+
return capabilities.supported && capabilities.supportsSessionResume ? resumeHandle : null;
|
|
144
127
|
}
|
|
145
128
|
async function runReviewerStructuredRound(args) {
|
|
146
129
|
try {
|
|
@@ -150,8 +133,9 @@ async function runReviewerStructuredRound(args) {
|
|
|
150
133
|
role: 'structured-advisor',
|
|
151
134
|
label: args.label,
|
|
152
135
|
startupTimeoutMs: Math.min(getAgentTurnStartupTimeoutMs(args.cwd), getInactivityTimeoutMs(args.cwd)),
|
|
153
|
-
// Structured-advisor turns retry locally: they are non-writing by
|
|
154
|
-
// and
|
|
136
|
+
// Structured-advisor turns retry locally: they are non-writing by
|
|
137
|
+
// policy, and a liveness retry re-runs the round from the same resume
|
|
138
|
+
// handle (the prior round's session), never a partially-written one.
|
|
155
139
|
retryLimit: getAgentTurnRetryLimit(args.cwd),
|
|
156
140
|
logger: args.logger,
|
|
157
141
|
baseSink: createStructuredAdvisorProviderEventSink({
|
|
@@ -166,6 +150,7 @@ async function runReviewerStructuredRound(args) {
|
|
|
166
150
|
prompt: args.prompt,
|
|
167
151
|
schema: args.schema,
|
|
168
152
|
structuredJsonProtocol: args.structuredJsonProtocol,
|
|
153
|
+
resumeHandle: resolveReviewerResumeHandle(args.reviewer, args.resumeHandle),
|
|
169
154
|
// Liveness retry sits outside the adapter-owned apiRetryLimit; it
|
|
170
155
|
// does not replace provider API retry.
|
|
171
156
|
...getStructuredAdvisorRuntimeOptions(args.cwd),
|
|
@@ -183,14 +168,10 @@ async function runReviewerStructuredRound(args) {
|
|
|
183
168
|
}
|
|
184
169
|
}
|
|
185
170
|
export async function runReviewerRound(args) {
|
|
186
|
-
assertReviewerRoundSupportsNoReadReviewer({
|
|
187
|
-
reviewer: args.reviewer,
|
|
188
|
-
label: 'review',
|
|
189
|
-
inlineContextSupplied: Boolean(args.inlineContext),
|
|
190
|
-
});
|
|
191
171
|
const schema = buildReviewerSchema();
|
|
192
172
|
const { sessionHandle, structured } = await runReviewerStructuredRound({
|
|
193
173
|
reviewer: args.reviewer,
|
|
174
|
+
resumeHandle: args.resumeHandle,
|
|
194
175
|
label: 'review',
|
|
195
176
|
cwd: args.cwd,
|
|
196
177
|
// The doctrine access mode comes from the reviewer provider's declared
|
|
@@ -224,14 +205,10 @@ export async function runReviewerRound(args) {
|
|
|
224
205
|
};
|
|
225
206
|
}
|
|
226
207
|
export async function runPlanReviewerRound(args) {
|
|
227
|
-
assertReviewerRoundSupportsNoReadReviewer({
|
|
228
|
-
reviewer: args.reviewer,
|
|
229
|
-
label: 'plan-review',
|
|
230
|
-
inlineContextSupplied: Boolean(args.inlineContext),
|
|
231
|
-
});
|
|
232
208
|
const schema = buildPlanReviewerSchema();
|
|
233
209
|
const { sessionHandle, structured } = await runReviewerStructuredRound({
|
|
234
210
|
reviewer: args.reviewer,
|
|
211
|
+
resumeHandle: args.resumeHandle,
|
|
235
212
|
label: 'plan-review',
|
|
236
213
|
cwd: args.cwd,
|
|
237
214
|
// The doctrine access mode comes from the reviewer provider's declared
|
|
@@ -253,6 +230,9 @@ export async function runPlanReviewerRound(args) {
|
|
|
253
230
|
round: args.round,
|
|
254
231
|
source: 'reviewer',
|
|
255
232
|
severity: finding.severity,
|
|
233
|
+
// validatePlanReviewerPayload normalizes an absent class to plan_correctness,
|
|
234
|
+
// so a plan-review finding always carries a concrete class by this point.
|
|
235
|
+
findingClass: finding.findingClass,
|
|
256
236
|
files: finding.files,
|
|
257
237
|
claim: finding.claim,
|
|
258
238
|
requiredAction: finding.requiredAction,
|
|
@@ -260,25 +240,19 @@ export async function runPlanReviewerRound(args) {
|
|
|
260
240
|
})),
|
|
261
241
|
};
|
|
262
242
|
}
|
|
263
|
-
// Read-only, inline-only round for the review_stuck
|
|
243
|
+
// Read-only, inline-only round for the review_stuck consultant. It runs through the
|
|
264
244
|
// same runReviewerStructuredRound plumbing the support / final-completion
|
|
265
245
|
// reviewers use (read-only tools only, zero commits, zero file edits). Because
|
|
266
|
-
// the
|
|
246
|
+
// the consultant judges entirely from Neal-inlined in-memory artifacts (plan, open
|
|
267
247
|
// blocking findings, reviewer-round snapshots), it has a single no-read-safe
|
|
268
|
-
// prompt variant and therefore requires a non-null inlineContext. The
|
|
269
|
-
//
|
|
270
|
-
// contact
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
export async function runBlockedAdjudicatorRound(args) {
|
|
274
|
-
assertReviewerRoundSupportsNoReadReviewer({
|
|
275
|
-
reviewer: args.reviewer,
|
|
276
|
-
label: 'review-stuck-arbiter',
|
|
277
|
-
inlineContextSupplied: Boolean(args.inlineContext),
|
|
278
|
-
});
|
|
248
|
+
// prompt variant and therefore requires a non-null inlineContext. The explicit
|
|
249
|
+
// null check below rejects a caller that hands the round a null context before
|
|
250
|
+
// any adapter contact (the consultant always passes a non-null context in
|
|
251
|
+
// production).
|
|
252
|
+
export async function runConsultantRound(args) {
|
|
279
253
|
if (!args.inlineContext) {
|
|
280
254
|
throw new ReviewerRoundError(new NealProviderError({
|
|
281
|
-
message: '
|
|
255
|
+
message: 'consultant round requires Neal-inlined reviewer context',
|
|
282
256
|
provider: args.reviewer.provider,
|
|
283
257
|
role: 'structured-advisor',
|
|
284
258
|
sessionHandle: null,
|
|
@@ -286,26 +260,26 @@ export async function runBlockedAdjudicatorRound(args) {
|
|
|
286
260
|
retryable: false,
|
|
287
261
|
}));
|
|
288
262
|
}
|
|
289
|
-
const schema =
|
|
263
|
+
const schema = buildConsultantSchema();
|
|
290
264
|
const { sessionHandle, structured } = await runReviewerStructuredRound({
|
|
291
265
|
reviewer: args.reviewer,
|
|
292
|
-
label: '
|
|
266
|
+
label: 'consultant',
|
|
293
267
|
cwd: args.cwd,
|
|
294
|
-
prompt:
|
|
268
|
+
prompt: buildConsultantPrompt({
|
|
295
269
|
blockedReason: args.blockedReason,
|
|
296
270
|
inlineContext: args.inlineContext,
|
|
297
271
|
}),
|
|
298
272
|
schema,
|
|
299
273
|
structuredJsonProtocol: buildStructuredJsonProtocolSpec({
|
|
300
|
-
schemaLabel: '
|
|
274
|
+
schemaLabel: 'consultant_payload',
|
|
301
275
|
schema,
|
|
302
|
-
validator:
|
|
276
|
+
validator: validateConsultantVerdictPayload,
|
|
303
277
|
}),
|
|
304
278
|
logger: args.logger,
|
|
305
279
|
});
|
|
306
280
|
return {
|
|
307
281
|
sessionHandle,
|
|
308
|
-
verdict:
|
|
282
|
+
verdict: validateConsultantVerdictPayload(structured),
|
|
309
283
|
};
|
|
310
284
|
}
|
|
311
285
|
export async function runCoderFinalCompletionSummaryRound(args) {
|
|
@@ -317,8 +291,9 @@ export async function runCoderFinalCompletionSummaryRound(args) {
|
|
|
317
291
|
role: 'structured-advisor',
|
|
318
292
|
label: 'final-completion',
|
|
319
293
|
startupTimeoutMs: Math.min(getAgentTurnStartupTimeoutMs(args.cwd), getInactivityTimeoutMs(args.cwd)),
|
|
320
|
-
// Structured-advisor turns retry locally: they are non-writing by
|
|
321
|
-
// and
|
|
294
|
+
// Structured-advisor turns retry locally: they are non-writing by
|
|
295
|
+
// policy, and a liveness retry re-runs the round from the same resume
|
|
296
|
+
// handle (the prior round's session), never a partially-written one.
|
|
322
297
|
retryLimit: getAgentTurnRetryLimit(args.cwd),
|
|
323
298
|
logger: args.logger,
|
|
324
299
|
baseSink: createStructuredAdvisorProviderEventSink({
|
|
@@ -352,11 +327,6 @@ export async function runCoderFinalCompletionSummaryRound(args) {
|
|
|
352
327
|
}
|
|
353
328
|
}
|
|
354
329
|
export async function runReviewerFinalCompletionRound(args) {
|
|
355
|
-
assertReviewerRoundSupportsNoReadReviewer({
|
|
356
|
-
reviewer: args.reviewer,
|
|
357
|
-
label: 'final-completion',
|
|
358
|
-
inlineContextSupplied: Boolean(args.inlineContext),
|
|
359
|
-
});
|
|
360
330
|
const schema = buildFinalCompletionReviewerSchema();
|
|
361
331
|
const { sessionHandle, structured } = await runReviewerStructuredRound({
|
|
362
332
|
reviewer: args.reviewer,
|