@navels/neal 0.5.1 → 0.6.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 +53 -0
- package/dist/neal/adjudicator/execute.js +32 -6
- package/dist/neal/agents/prompts.js +19 -5
- package/dist/neal/agents/rounds.js +33 -4
- package/dist/neal/agents/schemas.js +34 -1
- package/dist/neal/commands/check.js +8 -1
- package/dist/neal/commands/resume-run.js +7 -1
- package/dist/neal/config.js +24 -1
- package/dist/neal/context/inline-review-context.js +116 -0
- package/dist/neal/final-completion.js +4 -7
- package/dist/neal/orchestrator/phases/planning.js +53 -8
- package/dist/neal/orchestrator/phases/recovery.js +99 -8
- package/dist/neal/plan-scope-revision.js +129 -0
- package/dist/neal/prompts/execute.js +28 -9
- package/dist/neal/prompts/guidance.js +8 -1
- package/dist/neal/prompts/planning.js +9 -3
- package/dist/neal/prompts/review-doctrine.js +39 -1
- package/dist/neal/prompts/specialized.js +87 -22
- package/dist/neal/prompts/specs.js +33 -5
- package/dist/neal/providers/input-budget.js +80 -0
- package/dist/neal/providers/openai-codex.js +69 -1
- package/dist/neal/review-findings/run.js +17 -3
- package/dist/neal/scopes.js +4 -4
- package/dist/neal/state-invariants.js +8 -2
- package/dist/neal/state.js +15 -0
- package/dist/neal/status.js +52 -1
- package/dist/neal/support.js +6 -2
- package/dist/neal/verification-events.js +49 -0
- package/docs/plan-format.md +10 -0
- package/docs/prompt-specs.md +80 -0
- package/docs/providers.md +19 -0
- package/docs/state-machine.md +6 -0
- package/neal.yml +10 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -453,6 +453,54 @@ agent:
|
|
|
453
453
|
effort: xhigh
|
|
454
454
|
```
|
|
455
455
|
|
|
456
|
+
### Review level
|
|
457
|
+
|
|
458
|
+
`neal.review_level` sets how strict the scope reviewer and the final-completion
|
|
459
|
+
reviewer are about what rises to a blocking finding. It takes one of three
|
|
460
|
+
values and defaults to `moderate`:
|
|
461
|
+
|
|
462
|
+
- `strict`: assume adversarial trust boundaries. Block on any failure reachable
|
|
463
|
+
under the worst case, including hardening gaps and missing defenses against
|
|
464
|
+
local or adversarial actors.
|
|
465
|
+
- `moderate`: ordinary trust boundaries. Internal run artifacts aren't security
|
|
466
|
+
boundaries. Block on correctness bugs and failures reachable under normal use;
|
|
467
|
+
don't require defenses against an actor who could already subvert the system.
|
|
468
|
+
- `lenient`: correctness and real, reachable bugs only. Minimal robustness,
|
|
469
|
+
style, or hardening demands.
|
|
470
|
+
|
|
471
|
+
```yaml
|
|
472
|
+
neal:
|
|
473
|
+
review_level: moderate
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Set it in the repo's `neal.yml` or in `~/.neal/config.yml`; the repo value wins.
|
|
477
|
+
A blank or null value means unset and falls back to `moderate`. Any other
|
|
478
|
+
nonblank value (a typo, say) is rejected before any agent work: `neal check`
|
|
479
|
+
fails, every fresh writer command fails at config load, and a `neal resume`
|
|
480
|
+
that has selected a run and is about to resume writer work (plain, manual
|
|
481
|
+
gate, or `--message`) fails before it takes the writer lock, rewrites run
|
|
482
|
+
state, or starts an agent turn. Resume outcomes that never execute writer work
|
|
483
|
+
(already done, already running, waiting for operator guidance) are decided
|
|
484
|
+
first and don't validate the level.
|
|
485
|
+
|
|
486
|
+
Under every level a blocking finding has to describe a failure that's actually
|
|
487
|
+
reachable under the assumed trust boundaries, and the reviewer still treats the
|
|
488
|
+
change as hostile input and tries to falsify it before crediting it. A level
|
|
489
|
+
narrows what counts as blocking; it never means trust the coder or skip
|
|
490
|
+
inspection. The plan reviewer, the consultant, and `neal review` don't use the
|
|
491
|
+
level.
|
|
492
|
+
|
|
493
|
+
`~/.neal/guidance/reviewer.md` refines the level rather than replacing it. It
|
|
494
|
+
can widen or narrow the assumed trust boundaries ("we do defend the run
|
|
495
|
+
directory against local processes" makes a local-process attack on the run
|
|
496
|
+
directory blockable even at `moderate`) and it can demote or promote finding
|
|
497
|
+
categories ("ignore performance, correctness only" makes a performance
|
|
498
|
+
regression non-blocking at any level). It can't turn off the reachability
|
|
499
|
+
filter, the adversarial stance, or blocking on reachable correctness failures,
|
|
500
|
+
including correctness regressions. Guidance that conflicts with that floor is
|
|
501
|
+
ignored on that point. See [Custom guidance](#custom-guidance) for where the
|
|
502
|
+
file lives.
|
|
503
|
+
|
|
456
504
|
### Custom guidance
|
|
457
505
|
|
|
458
506
|
neal supports additive guidance files for local preferences alongside the built-in protocol prompts:
|
|
@@ -463,6 +511,11 @@ neal supports additive guidance files for local preferences alongside the built-
|
|
|
463
511
|
|
|
464
512
|
Set `NEAL_GUIDANCE_DIR=/path/to/guidance` to load those same `coder.md`, `reviewer.md`, and `planner.md` files from another directory. neal records applied guidance roles, selected paths, and byte counts in run artifacts. Guidance contents stay out of terminal output.
|
|
465
513
|
|
|
514
|
+
For the two code reviewers, `reviewer.md` layers on top of `neal.review_level`
|
|
515
|
+
(see [Review level](#review-level)): it can adjust trust boundaries and finding
|
|
516
|
+
categories, but it can't switch off the reachability filter or blocking on
|
|
517
|
+
reachable correctness failures.
|
|
518
|
+
|
|
466
519
|
## Artifacts and storage
|
|
467
520
|
|
|
468
521
|
Writer run artifacts live under `.neal/runs/<run-id>/`, including the original-plan backup at `.neal/runs/<run-id>/PLAN_ORIGINAL.md` and reviewer scratch space under `.neal/runs/<run-id>/scratch/`. Queue artifacts live under `.neal/queues/<queue-id>/`. Review findings artifacts live under `.neal/reviews/<review-id>/`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
2
|
import { runCoderResponseRound, runReviewerRound } from '../agents.js';
|
|
3
|
-
import { readOnlyReviewerNeedsInlinedDiff } from '../context/inline-review-context.js';
|
|
3
|
+
import { OPEN_FINDINGS_PROMPT_ITEM_LIMIT, readOnlyReviewerNeedsInlinedDiff, } from '../context/inline-review-context.js';
|
|
4
4
|
import { buildAndPersistReviewerContextPacket } from '../context/reviewer-context.js';
|
|
5
5
|
import { getReviewStuckWindow } from '../config.js';
|
|
6
6
|
import { getDiffForRangePaths } from '../git.js';
|
|
@@ -229,9 +229,19 @@ export function buildVerificationHint(state) {
|
|
|
229
229
|
'- Rerun full test suites only if your new changes materially invalidate that reviewed baseline or the plan explicitly requires new end-of-scope full-suite verification.',
|
|
230
230
|
].join('\n');
|
|
231
231
|
}
|
|
232
|
+
// The bounded finding set for one coder response round. Both the response
|
|
233
|
+
// prompt and validateExecuteResponseCoverage consume this same selection, so
|
|
234
|
+
// the coder is asked to disposition exactly the findings it can see. At most
|
|
235
|
+
// OPEN_FINDINGS_PROMPT_ITEM_LIMIT findings are presented per round; findings
|
|
236
|
+
// beyond the limit stay open and synthesizeExecuteResponseState keeps the
|
|
237
|
+
// response phase active, presenting the next batch immediately, until every
|
|
238
|
+
// open finding of the round's kind has been dispositioned.
|
|
232
239
|
export function getExecuteResponseOpenFindings(state, mode = 'blocking') {
|
|
233
240
|
const selector = mode === 'optional' ? isOpenNonBlockingFinding : isOpenBlockingFinding;
|
|
234
|
-
return state.findings
|
|
241
|
+
return state.findings
|
|
242
|
+
.filter(selector)
|
|
243
|
+
.slice(0, OPEN_FINDINGS_PROMPT_ITEM_LIMIT)
|
|
244
|
+
.map((finding) => ({
|
|
235
245
|
id: finding.id,
|
|
236
246
|
source: finding.source,
|
|
237
247
|
claim: finding.claim,
|
|
@@ -266,7 +276,7 @@ function validateExecuteResponseCoverage(args) {
|
|
|
266
276
|
throw new Error(`Coder ${args.mode} response returned duplicate finding dispositions: ${[...duplicateIds].join(', ')}`);
|
|
267
277
|
}
|
|
268
278
|
if (unknownIds.size > 0) {
|
|
269
|
-
throw new Error(`Coder ${args.mode} response returned dispositions for
|
|
279
|
+
throw new Error(`Coder ${args.mode} response returned dispositions for findings outside the presented open set: ${[...unknownIds].join(', ')}`);
|
|
270
280
|
}
|
|
271
281
|
if (args.response.payload.outcome !== 'responded') {
|
|
272
282
|
return;
|
|
@@ -526,7 +536,12 @@ export function synthesizeExecuteReviewerState(args) {
|
|
|
526
536
|
];
|
|
527
537
|
mergedFindings = [...args.state.findings, ...findings];
|
|
528
538
|
}
|
|
529
|
-
|
|
539
|
+
// Deliberately derived from the merged set as well as the current reviewer
|
|
540
|
+
// payload: a prior round's still-open blocker (for example one beyond the
|
|
541
|
+
// batched response round's presentation limit) must keep forcing a revision
|
|
542
|
+
// even when the current reviewer round is empty, so an empty round can never
|
|
543
|
+
// accept over an open blocking finding.
|
|
544
|
+
const hasBlockingFindings = findings.some((finding) => finding.severity === 'blocking') || mergedFindings.some(isOpenBlockingFinding);
|
|
530
545
|
const hasOpenNonBlockingFindings = mergedFindings.some(isOpenNonBlockingFinding);
|
|
531
546
|
const openBlockingCanonicalSet = getOpenBlockingCanonicalSet(mergedFindings);
|
|
532
547
|
const openBlockingCanonicalIds = [...openBlockingCanonicalSet].sort();
|
|
@@ -633,11 +648,22 @@ export function synthesizeExecuteResponseState(args) {
|
|
|
633
648
|
};
|
|
634
649
|
});
|
|
635
650
|
const outcome = args.response.payload.outcome;
|
|
651
|
+
// Response rounds batch: when open findings of this round's kind remain
|
|
652
|
+
// after the dispositions are applied (they were beyond the per-round
|
|
653
|
+
// presentation limit), stay in the same response phase so the next batch is
|
|
654
|
+
// presented immediately instead of spending a reviewer round per batch.
|
|
655
|
+
// Coverage validation guarantees the presented batch was fully
|
|
656
|
+
// dispositioned, so the backlog strictly shrinks and the loop terminates.
|
|
657
|
+
const hasNextResponseBatch = findings.some(mode === 'optional' ? isOpenNonBlockingFinding : isOpenBlockingFinding);
|
|
636
658
|
const nextPhase = outcome === 'blocked' || outcome === 'split_plan'
|
|
637
659
|
? 'blocked'
|
|
638
660
|
: mode === 'optional'
|
|
639
|
-
?
|
|
640
|
-
|
|
661
|
+
? hasNextResponseBatch
|
|
662
|
+
? 'coder_optional_response'
|
|
663
|
+
: EXECUTE_FINALIZATION_PHASE
|
|
664
|
+
: hasNextResponseBatch
|
|
665
|
+
? 'coder_response'
|
|
666
|
+
: 'reviewer_scope';
|
|
641
667
|
return {
|
|
642
668
|
findings,
|
|
643
669
|
nextPhase,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { assertNoReadPromptInstructionText, renderInlineReviewerContext, } from '../context/inline-review-context.js';
|
|
1
|
+
import { assertNoReadPromptInstructionText, boundFreeTextValues, renderInlineReviewerContext, truncateInlineSectionBody, } from '../context/inline-review-context.js';
|
|
2
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';
|
|
3
|
-
import { getUserGuidanceLines } from '../prompts/guidance.js';
|
|
3
|
+
import { getUserGuidanceLines, USER_GUIDANCE_MAX_CHARS } from '../prompts/guidance.js';
|
|
4
4
|
import { assertPromptBuilder, resolvePrimaryVariant } from '../prompts/assert-builder.js';
|
|
5
5
|
export { buildCoderResponsePrompt, buildLegacyScopePrompt, buildReviewerPrompt, buildScopePrompt } from '../prompts/execute.js';
|
|
6
6
|
export { buildCoderPlanResponsePrompt, buildLegacyPlanningPrompt, buildPlanReviewerPrompt, buildPlanningPrompt, } from '../prompts/planning.js';
|
|
@@ -41,13 +41,16 @@ export function buildConsultantPrompt(args) {
|
|
|
41
41
|
...staticInstructionLines,
|
|
42
42
|
'',
|
|
43
43
|
'Blocked reason:',
|
|
44
|
-
|
|
44
|
+
// Agent-authored free text shares the fixed agent free-text cap at render
|
|
45
|
+
// time; the persisted blocked reason keeps its full text.
|
|
46
|
+
boundFreeTextValues([args.blockedReason])[0],
|
|
45
47
|
'',
|
|
46
48
|
renderInlineReviewerContext(args.inlineContext),
|
|
47
49
|
].join('\n');
|
|
48
50
|
}
|
|
49
51
|
export function buildBlockedRecoveryCoderPrompt(args) {
|
|
50
52
|
const allowReplacement = args.allowReplacement ?? true;
|
|
53
|
+
const laterScopeRevision = args.terminalOnly ? null : args.laterScopeRevision ?? null;
|
|
51
54
|
const actionLines = [
|
|
52
55
|
'- `resume_current_scope`',
|
|
53
56
|
...(allowReplacement ? ['- `replace_current_scope`'] : []),
|
|
@@ -85,6 +88,14 @@ export function buildBlockedRecoveryCoderPrompt(args) {
|
|
|
85
88
|
allowReplacement
|
|
86
89
|
? 'Always include a `replacementPlan` string. Use an empty string unless action=`replace_current_scope`.'
|
|
87
90
|
: 'Always include an empty `replacementPlan` string.',
|
|
91
|
+
...(laterScopeRevision
|
|
92
|
+
? [
|
|
93
|
+
'Always include an integer `laterScopeNumber` and a `laterScopeBody` string. Use `0` and an empty string unless the operator guidance directs a change to a later top-level scope.',
|
|
94
|
+
`The operator guidance may direct a change to one later scope of the top-level plan at ${laterScopeRevision.topLevelPlanDoc}. The current top-level scope is ${laterScopeRevision.currentScopeNumber}; eligible target scopes are ${laterScopeRevision.currentScopeNumber + 1} through ${laterScopeRevision.scopeCount}.`,
|
|
95
|
+
'To revise a later scope, set `laterScopeNumber` to the target scope number and `laterScopeBody` to the complete replacement text of that one `### Scope N:` entry. The body must start with the line `### Scope N:` for the same N (the title after the colon may change), must contain no other `### ` or `## ` heading, and must keep the `- Goal:`, `- Verification:`, and `- Success Condition:` bullets.',
|
|
96
|
+
'A later-scope revision may accompany action=`resume_current_scope` or action=`stay_blocked` only. Set both fields or neither. Do not revise the current scope, an earlier scope, or a derived plan this way, and do not edit the plan file yourself: Neal validates the revised plan and writes it. Put the reasoning for the revision in `rationale`.',
|
|
97
|
+
]
|
|
98
|
+
: ['Always include `laterScopeNumber` as `0` and `laterScopeBody` as an empty string.']),
|
|
88
99
|
...(allowReplacement
|
|
89
100
|
? [
|
|
90
101
|
'When action=`replace_current_scope`, `replacementPlan` must use the same Neal-executable contract as a top-level plan.',
|
|
@@ -102,9 +113,12 @@ export function buildBlockedRecoveryCoderPrompt(args) {
|
|
|
102
113
|
'Do not treat operator guidance as authorization to skip verification, waive policy, or reinterpret the target beyond the current scope.',
|
|
103
114
|
'',
|
|
104
115
|
'Blocked recovery context:',
|
|
105
|
-
|
|
116
|
+
// The blocked reason is agent-authored free text and the guidance is
|
|
117
|
+
// operator-authored; each gets its class's render-time cap while the
|
|
118
|
+
// persisted values keep their full text.
|
|
119
|
+
`- Blocked reason: ${boundFreeTextValues([args.blockedReason])[0]}`,
|
|
106
120
|
`- Recovery turns used: ${args.turnsTaken} of ${args.maxTurns}`,
|
|
107
|
-
`- Latest operator guidance: ${args.operatorGuidance}`,
|
|
121
|
+
`- Latest operator guidance: ${truncateInlineSectionBody(args.operatorGuidance, USER_GUIDANCE_MAX_CHARS)}`,
|
|
108
122
|
'',
|
|
109
123
|
'Current progress state:',
|
|
110
124
|
buildProgressSection(args.progressText),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getInactivityTimeoutMs, } from '../config.js';
|
|
3
|
+
import { getReviewLevel } from '../config.js';
|
|
3
4
|
import { runWithAgentTurnLiveness } from '../providers/liveness.js';
|
|
4
5
|
import { normalizeExecutionShapeDeclaration } from '../plan-validation.js';
|
|
5
6
|
import { getCoderAdapter, getProviderDefinition, getStructuredAdvisorAdapter } from '../providers/registry.js';
|
|
@@ -176,7 +177,13 @@ export async function runReviewerRound(args) {
|
|
|
176
177
|
cwd: args.cwd,
|
|
177
178
|
// The doctrine access mode comes from the reviewer provider's declared
|
|
178
179
|
// structured-advisor tool access, not from inline-context presence alone.
|
|
179
|
-
|
|
180
|
+
// The review level comes from current config (`neal.review_level`), not
|
|
181
|
+
// persisted run state; the builder itself never reads config.
|
|
182
|
+
prompt: buildReviewerPrompt({
|
|
183
|
+
...args,
|
|
184
|
+
accessMode: getReviewerDoctrineAccessMode(args.reviewer),
|
|
185
|
+
reviewLevel: getReviewLevel(args.cwd),
|
|
186
|
+
}),
|
|
180
187
|
schema,
|
|
181
188
|
structuredJsonProtocol: buildStructuredJsonProtocolSpec({
|
|
182
189
|
schemaLabel: 'reviewer_payload',
|
|
@@ -334,7 +341,13 @@ export async function runReviewerFinalCompletionRound(args) {
|
|
|
334
341
|
cwd: args.cwd,
|
|
335
342
|
// The doctrine access mode comes from the reviewer provider's declared
|
|
336
343
|
// structured-advisor tool access, not from inline-context presence alone.
|
|
337
|
-
|
|
344
|
+
// The review level comes from current config (`neal.review_level`), not
|
|
345
|
+
// persisted run state; the builder itself never reads config.
|
|
346
|
+
prompt: buildFinalCompletionReviewerPrompt({
|
|
347
|
+
...args,
|
|
348
|
+
accessMode: getReviewerDoctrineAccessMode(args.reviewer),
|
|
349
|
+
reviewLevel: getReviewLevel(args.cwd),
|
|
350
|
+
}),
|
|
338
351
|
schema,
|
|
339
352
|
structuredJsonProtocol: buildStructuredJsonProtocolSpec({
|
|
340
353
|
schemaLabel: 'final_completion_reviewer_payload',
|
|
@@ -629,6 +642,15 @@ export async function runCoderResponseRound(args) {
|
|
|
629
642
|
export async function runBlockedRecoveryCoderRound(args) {
|
|
630
643
|
const progressText = await safeReadText(args.progressMarkdownPath);
|
|
631
644
|
const schema = buildCoderBlockedRecoveryDispositionSchema();
|
|
645
|
+
const laterScopeRevision = args.terminalOnly ? null : args.laterScopeRevision ?? null;
|
|
646
|
+
const laterScopeContext = laterScopeRevision
|
|
647
|
+
? {
|
|
648
|
+
allowLaterScopeRevision: true,
|
|
649
|
+
currentScopeNumber: laterScopeRevision.currentScopeNumber,
|
|
650
|
+
planDocument: laterScopeRevision.planDocument,
|
|
651
|
+
}
|
|
652
|
+
: null;
|
|
653
|
+
const validator = (rawPayload) => validateCoderBlockedRecoveryDispositionPayload(rawPayload, laterScopeContext);
|
|
632
654
|
const { sessionHandle, structured } = await runCoderStructuredPrompt({
|
|
633
655
|
coder: args.coder,
|
|
634
656
|
cwd: args.cwd,
|
|
@@ -642,20 +664,27 @@ export async function runBlockedRecoveryCoderRound(args) {
|
|
|
642
664
|
turnsTaken: args.turnsTaken,
|
|
643
665
|
terminalOnly: args.terminalOnly,
|
|
644
666
|
allowReplacement: args.allowReplacement,
|
|
667
|
+
laterScopeRevision: laterScopeRevision
|
|
668
|
+
? {
|
|
669
|
+
topLevelPlanDoc: laterScopeRevision.topLevelPlanDoc,
|
|
670
|
+
currentScopeNumber: laterScopeRevision.currentScopeNumber,
|
|
671
|
+
scopeCount: laterScopeRevision.scopeCount,
|
|
672
|
+
}
|
|
673
|
+
: null,
|
|
645
674
|
}),
|
|
646
675
|
schema,
|
|
647
676
|
label: 'Coder blocked-recovery round',
|
|
648
677
|
structuredJsonProtocol: buildStructuredJsonProtocolSpec({
|
|
649
678
|
schemaLabel: 'coder_blocked_recovery_disposition_payload',
|
|
650
679
|
schema,
|
|
651
|
-
validator
|
|
680
|
+
validator,
|
|
652
681
|
}),
|
|
653
682
|
resumeHandle: args.sessionHandle,
|
|
654
683
|
logger: args.logger,
|
|
655
684
|
});
|
|
656
685
|
return {
|
|
657
686
|
sessionHandle,
|
|
658
|
-
payload:
|
|
687
|
+
payload: validator(structured),
|
|
659
688
|
};
|
|
660
689
|
}
|
|
661
690
|
export async function runCoderPlanResponseRound(args) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { reviseLaterScope } from '../plan-scope-revision.js';
|
|
2
3
|
import { normalizeExecutionShapeDeclaration, validatePlanDocument } from '../plan-validation.js';
|
|
3
4
|
import { repairReviewerSquashMessageDraft, validateReviewerSquashMessageDraft } from '../squash-message.js';
|
|
4
5
|
export const EXECUTE_SCOPE_PROGRESS_PAYLOAD_START = 'NEAL_PROGRESS_JUSTIFICATION_JSON_START';
|
|
@@ -101,6 +102,8 @@ const coderBlockedRecoveryDispositionPayloadSchema = z.object({
|
|
|
101
102
|
rationale: z.string(),
|
|
102
103
|
blocker: z.string(),
|
|
103
104
|
replacementPlan: z.string(),
|
|
105
|
+
laterScopeNumber: z.number(),
|
|
106
|
+
laterScopeBody: z.string(),
|
|
104
107
|
});
|
|
105
108
|
const coderPlanResponsePayloadSchema = z.object({
|
|
106
109
|
outcome: z.enum(CODER_PLAN_RESPONSE_OUTCOMES),
|
|
@@ -823,7 +826,33 @@ function validateManualGateResumeChecks(value) {
|
|
|
823
826
|
};
|
|
824
827
|
});
|
|
825
828
|
}
|
|
826
|
-
export function
|
|
829
|
+
export function getCoderBlockedRecoveryLaterScopeErrors(payload, context) {
|
|
830
|
+
const hasNumber = payload.laterScopeNumber !== 0;
|
|
831
|
+
const hasBody = payload.laterScopeBody.trim().length > 0;
|
|
832
|
+
if (!hasNumber && !hasBody) {
|
|
833
|
+
return [];
|
|
834
|
+
}
|
|
835
|
+
if (!Number.isInteger(payload.laterScopeNumber) || payload.laterScopeNumber < 0) {
|
|
836
|
+
return [`laterScopeNumber must be a non-negative integer, received ${String(payload.laterScopeNumber)}.`];
|
|
837
|
+
}
|
|
838
|
+
if (hasNumber !== hasBody) {
|
|
839
|
+
return ['laterScopeNumber and laterScopeBody must be set together: both for a later-scope revision, or 0 and an empty string.'];
|
|
840
|
+
}
|
|
841
|
+
if (context === null || !context.allowLaterScopeRevision) {
|
|
842
|
+
return ['A later-scope revision is not available for this round; return laterScopeNumber=0 and an empty laterScopeBody.'];
|
|
843
|
+
}
|
|
844
|
+
if (payload.action !== 'resume_current_scope' && payload.action !== 'stay_blocked') {
|
|
845
|
+
return [`A later-scope revision may accompany only action=resume_current_scope or action=stay_blocked, not action=${payload.action}.`];
|
|
846
|
+
}
|
|
847
|
+
const result = reviseLaterScope({
|
|
848
|
+
planDocument: context.planDocument,
|
|
849
|
+
currentScopeNumber: context.currentScopeNumber,
|
|
850
|
+
targetScopeNumber: payload.laterScopeNumber,
|
|
851
|
+
replacementBody: payload.laterScopeBody,
|
|
852
|
+
});
|
|
853
|
+
return result.ok ? [] : result.errors;
|
|
854
|
+
}
|
|
855
|
+
export function validateCoderBlockedRecoveryDispositionPayload(rawPayload, laterScopeContext = null) {
|
|
827
856
|
const payload = parsePayload(coderBlockedRecoveryDispositionPayloadSchema, rawPayload, 'Coder blocked-recovery payload');
|
|
828
857
|
const blocker = payload.blocker.trim();
|
|
829
858
|
const replacementPlan = payload.replacementPlan.trim();
|
|
@@ -836,6 +865,10 @@ export function validateCoderBlockedRecoveryDispositionPayload(rawPayload) {
|
|
|
836
865
|
if ((payload.action === 'stay_blocked' || payload.action === 'terminal_block') && !blocker) {
|
|
837
866
|
throw new Error(`Coder blocked-recovery round returned action=${payload.action} without a blocker payload.`);
|
|
838
867
|
}
|
|
868
|
+
const laterScopeErrors = getCoderBlockedRecoveryLaterScopeErrors(payload, laterScopeContext);
|
|
869
|
+
if (laterScopeErrors.length > 0) {
|
|
870
|
+
throw new Error(`Coder blocked-recovery round returned an invalid later-scope revision: ${laterScopeErrors.join(' ')}`);
|
|
871
|
+
}
|
|
839
872
|
return payload;
|
|
840
873
|
}
|
|
841
874
|
export function parseFinalCompletionSummaryPayload(rawPayload) {
|
|
@@ -2,8 +2,9 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
import { verifyNotification } from '../../notifier.js';
|
|
4
4
|
import { parseCheckArgs } from '../cli.js';
|
|
5
|
-
import { assertWriterProvidersConfigured, getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getFinalCompletionContinueExecutionMax, getInactivityTimeoutMs, getInteractiveBlockedRecoveryMaxTurns, getMaxReviewRounds, getNotifyBin, getPhaseHeartbeatMs, getReviewStuckWindow, } from '../config.js';
|
|
5
|
+
import { assertWriterProvidersConfigured, getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getFinalCompletionContinueExecutionMax, getInactivityTimeoutMs, getInteractiveBlockedRecoveryMaxTurns, getMaxReviewRounds, getNotifyBin, getPhaseHeartbeatMs, getReviewLevel, getReviewStuckWindow, } from '../config.js';
|
|
6
6
|
import { getNealDirGitIgnoreStatus } from '../git.js';
|
|
7
|
+
import { collectGuidanceDiagnostics, USER_GUIDANCE_MAX_CHARS } from '../prompts/guidance.js';
|
|
7
8
|
import { runWithAgentTurnLiveness } from '../providers/liveness.js';
|
|
8
9
|
import { getCoderAdapter, getProviderDefinition, getStructuredAdvisorAdapter, } from '../providers/registry.js';
|
|
9
10
|
import { sanitizeSensitiveText } from '../sensitive-text.js';
|
|
@@ -64,6 +65,7 @@ function validateConfig(cwd) {
|
|
|
64
65
|
getInteractiveBlockedRecoveryMaxTurns(cwd);
|
|
65
66
|
getFinalCompletionContinueExecutionMax(cwd);
|
|
66
67
|
getNotifyBin(cwd);
|
|
68
|
+
getReviewLevel(cwd);
|
|
67
69
|
return agentConfig;
|
|
68
70
|
}
|
|
69
71
|
async function promptForProviderVerification(stdin, stdout) {
|
|
@@ -343,6 +345,11 @@ export async function runNealCheckCli(options = {}) {
|
|
|
343
345
|
writeLine(stdout, ` ${describeRole('coder', agentConfig.coder)}`);
|
|
344
346
|
writeLine(stdout, ` ${describeRole('reviewer', agentConfig.reviewer)}`);
|
|
345
347
|
writeLine(stdout, ` ${describeNotificationScript(notifyBin)}`);
|
|
348
|
+
for (const entry of collectGuidanceDiagnostics()) {
|
|
349
|
+
if (entry.chars > USER_GUIDANCE_MAX_CHARS) {
|
|
350
|
+
writeLine(stdout, ` warning: ${entry.role} guidance at ${entry.path} is ${entry.chars} characters; prompts inline the first ${USER_GUIDANCE_MAX_CHARS} and truncate the rest. Trim the file.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
346
353
|
writeLine(stdout, '');
|
|
347
354
|
// Native adapters drive their providers directly; any other writer provider is
|
|
348
355
|
// an openai-compatible model that should be qualified end-to-end with `neal compat`.
|
|
@@ -2,7 +2,7 @@ import process from 'node:process';
|
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { parseResumeArgs } from '../cli.js';
|
|
5
|
-
import { assertWriterProvidersConfigured } from '../config.js';
|
|
5
|
+
import { assertWriterProvidersConfigured, getReviewLevel } from '../config.js';
|
|
6
6
|
import { writeDiagnostic } from '../diagnostic.js';
|
|
7
7
|
import { assertGitRepositoryWithCommit } from '../git.js';
|
|
8
8
|
import { loadOrInitialize } from '../orchestrator.js';
|
|
@@ -261,6 +261,12 @@ function emitAlreadyRunningOutcome(selection) {
|
|
|
261
261
|
return { kind: 'already_running' };
|
|
262
262
|
}
|
|
263
263
|
async function withResumeWriterLock(selection, evidence, action) {
|
|
264
|
+
// Every path that resumes writer work (plain, manual gate, --message) goes
|
|
265
|
+
// through this seam. The review level is read from current config, not
|
|
266
|
+
// persisted run state, so it is validated here before the lock is taken or
|
|
267
|
+
// any run state is rewritten. No-op and rejection outcomes (done, already
|
|
268
|
+
// running, waiting for guidance) are decided earlier and never reach it.
|
|
269
|
+
getReviewLevel(selection.state.cwd);
|
|
264
270
|
let lock;
|
|
265
271
|
try {
|
|
266
272
|
lock = await acquireResumeWriterLock(selection, evidence);
|
package/dist/neal/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
import { assertAgentConfigSupportsWriterRun, parseProviderId, } from './providers/registry.js';
|
|
6
|
+
const REVIEW_LEVELS = ['strict', 'moderate', 'lenient'];
|
|
6
7
|
const OPENAI_COMPATIBLE_DEFAULT_API_KEY_ENV = 'OPENAI_COMPATIBLE_API_KEY';
|
|
7
8
|
const WRITER_PROVIDER_CONFIG_KEYS = {
|
|
8
9
|
coder: 'agent.coder.provider',
|
|
@@ -19,9 +20,10 @@ const DEFAULT_CONFIG = {
|
|
|
19
20
|
agent_turn_startup_timeout_ms: 300_000,
|
|
20
21
|
agent_turn_retry_limit: 1,
|
|
21
22
|
interactive_blocked_recovery_max_turns: 3,
|
|
22
|
-
final_completion_continue_execution_max:
|
|
23
|
+
final_completion_continue_execution_max: 3,
|
|
23
24
|
consultant_max_attempts: 1,
|
|
24
25
|
notify_bin: null,
|
|
26
|
+
review_level: 'moderate',
|
|
25
27
|
},
|
|
26
28
|
agent: {
|
|
27
29
|
planner: {
|
|
@@ -101,6 +103,22 @@ function parseNumberValue(value) {
|
|
|
101
103
|
function parseStringValue(value) {
|
|
102
104
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
103
105
|
}
|
|
106
|
+
function isReviewLevel(value) {
|
|
107
|
+
return REVIEW_LEVELS.includes(value);
|
|
108
|
+
}
|
|
109
|
+
function parseReviewLevelValue(value, fieldPath) {
|
|
110
|
+
if (value === undefined || value === null) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === 'string' && !value.trim()) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const level = typeof value === 'string' ? value.trim() : value;
|
|
117
|
+
if (typeof level !== 'string' || !isReviewLevel(level)) {
|
|
118
|
+
throw new Error(`Invalid review level for ${fieldPath}: ${JSON.stringify(level)}. Valid values: ${REVIEW_LEVELS.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
return level;
|
|
121
|
+
}
|
|
104
122
|
function parseConfigProviderValue(value, fieldPath) {
|
|
105
123
|
if (value === undefined || value === null) {
|
|
106
124
|
return undefined;
|
|
@@ -255,8 +273,13 @@ export function assertWriterProvidersConfigured(cwd = process.cwd(), options = {
|
|
|
255
273
|
throw new WriterProvidersNotConfiguredError(options.guidance ?? 'writer-run', missingProviderKeys);
|
|
256
274
|
}
|
|
257
275
|
assertAgentConfigSupportsWriterRun(agentConfig, { context: options.context });
|
|
276
|
+
getReviewLevel(cwd);
|
|
258
277
|
return agentConfig;
|
|
259
278
|
}
|
|
279
|
+
export function getReviewLevel(cwd = process.cwd()) {
|
|
280
|
+
const config = loadConfigFile(cwd);
|
|
281
|
+
return parseReviewLevelValue(config.neal?.review_level, 'neal.review_level') ?? DEFAULT_CONFIG.neal.review_level;
|
|
282
|
+
}
|
|
260
283
|
export function getInactivityTimeoutMs(cwd = process.cwd()) {
|
|
261
284
|
const config = loadConfigFile(cwd);
|
|
262
285
|
return (parseNumberValue(config.neal?.inactivity_timeout_ms) ??
|
|
@@ -4,6 +4,122 @@ import { getProviderDefinition, isRegisteredProviderId } from '../providers/regi
|
|
|
4
4
|
// truncation posture of truncateForPrompt in src/neal/agents/structured-json.ts:
|
|
5
5
|
// truncate with an explicit marker instead of silently dropping content.
|
|
6
6
|
export const INLINE_SECTION_MAX_CHARS = 200_000;
|
|
7
|
+
// Fixed aggregate character budget for the free-text values of one
|
|
8
|
+
// agent-authored payload embedded in another agent's prompt (review-finding
|
|
9
|
+
// text, progress justifications, completion summaries). The budget covers the
|
|
10
|
+
// total rendered free text including truncation markers, so the free-text
|
|
11
|
+
// contribution of a section never exceeds this constant. Control data (ids,
|
|
12
|
+
// severities, statuses, file paths) never rides this budget: callers render it
|
|
13
|
+
// separately and exactly via boundOpenFindingsForPrompt-style wrappers.
|
|
14
|
+
export const AGENT_FREE_TEXT_SECTION_MAX_CHARS = 20_000;
|
|
15
|
+
// Upper bound on one rendered truncation marker:
|
|
16
|
+
// '\n[truncated N character(s)]' with N at most 16 digits.
|
|
17
|
+
const TRUNCATION_MARKER_MAX_CHARS = 42;
|
|
18
|
+
// Maximum open findings presented to the coder per response round. Response
|
|
19
|
+
// selection (getExecuteResponseOpenFindings and the plan-review response
|
|
20
|
+
// phase) and prompt rendering share this bound, so the coder always sees
|
|
21
|
+
// exactly the finding set it must disposition; findings beyond the limit stay
|
|
22
|
+
// open and are presented in later response rounds until every one is handled.
|
|
23
|
+
// The limit also keeps boundFreeTextValues' cardinality precondition
|
|
24
|
+
// satisfied: 3 free-text values per finding times this limit stays far under
|
|
25
|
+
// AGENT_FREE_TEXT_SECTION_MAX_CHARS / TRUNCATION_MARKER_MAX_CHARS.
|
|
26
|
+
export const OPEN_FINDINGS_PROMPT_ITEM_LIMIT = 50;
|
|
27
|
+
function renderBoundedFreeTextValue(text, cap) {
|
|
28
|
+
if (text.length <= cap) {
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
if (cap <= 0) {
|
|
32
|
+
return `[truncated ${text.length} character(s)]`;
|
|
33
|
+
}
|
|
34
|
+
return `${text.slice(0, cap)}\n[truncated ${text.length - cap} character(s)]`;
|
|
35
|
+
}
|
|
36
|
+
// Largest equal per-value kept-character cap such that the values fit the
|
|
37
|
+
// kept-character budget (water-filling): short values keep their full text and
|
|
38
|
+
// long values share the remaining budget equally. Returns MAX_SAFE_INTEGER
|
|
39
|
+
// when everything already fits.
|
|
40
|
+
function computeFairValueCap(lengths, budget) {
|
|
41
|
+
const sorted = [...lengths].sort((left, right) => left - right);
|
|
42
|
+
let remaining = budget;
|
|
43
|
+
for (let index = 0; index < sorted.length; index += 1) {
|
|
44
|
+
const share = Math.floor(remaining / (sorted.length - index));
|
|
45
|
+
if (sorted[index] > share) {
|
|
46
|
+
return Math.max(share, 0);
|
|
47
|
+
}
|
|
48
|
+
remaining -= sorted[index];
|
|
49
|
+
}
|
|
50
|
+
return Number.MAX_SAFE_INTEGER;
|
|
51
|
+
}
|
|
52
|
+
// Bounds a fixed list of agent-authored free-text values to a fixed aggregate
|
|
53
|
+
// budget, truncation markers included: marker allowance is reserved up front,
|
|
54
|
+
// the kept characters are water-filled over the rest, so the total rendered
|
|
55
|
+
// length is always <= budget. Positions are preserved one-to-one with the
|
|
56
|
+
// input. Callers keep control data (ids, files, severities) out of this list
|
|
57
|
+
// and enforce the finite cardinality this validates. Never mutates the input.
|
|
58
|
+
export function boundFreeTextValues(texts, budget = AGENT_FREE_TEXT_SECTION_MAX_CHARS) {
|
|
59
|
+
if (texts.length * TRUNCATION_MARKER_MAX_CHARS > budget) {
|
|
60
|
+
throw new Error(`boundFreeTextValues received ${texts.length} free-text values; the aggregate budget of ${budget} characters supports at most ${Math.floor(budget / TRUNCATION_MARKER_MAX_CHARS)}. Bound the payload's item cardinality before rendering free text.`);
|
|
61
|
+
}
|
|
62
|
+
const keptBudget = budget - texts.length * TRUNCATION_MARKER_MAX_CHARS;
|
|
63
|
+
const cap = computeFairValueCap(texts.map((text) => text.length), keptBudget);
|
|
64
|
+
return texts.map((text) => renderBoundedFreeTextValue(text, cap));
|
|
65
|
+
}
|
|
66
|
+
// Render-only view of an open-findings list for prompt embedding. Every
|
|
67
|
+
// finding renders: id, source, and severity are copied exactly, each files
|
|
68
|
+
// path renders whole with the list length bounded via boundChangedFileList,
|
|
69
|
+
// and only the claim, requiredAction, and roundSummary free text shares the
|
|
70
|
+
// fixed aggregate budget. Callers must present at most
|
|
71
|
+
// OPEN_FINDINGS_PROMPT_ITEM_LIMIT findings — the same bounded set their
|
|
72
|
+
// response processing validates against — so this never drops a finding the
|
|
73
|
+
// coder is required to disposition; it throws on a larger list instead of
|
|
74
|
+
// silently diverging from the response contract. Never mutates the input.
|
|
75
|
+
export function boundOpenFindingsForPrompt(findings) {
|
|
76
|
+
if (findings.length > OPEN_FINDINGS_PROMPT_ITEM_LIMIT) {
|
|
77
|
+
throw new Error(`boundOpenFindingsForPrompt received ${findings.length} findings; response rounds present at most ${OPEN_FINDINGS_PROMPT_ITEM_LIMIT}. Bound the selection where the response set is chosen.`);
|
|
78
|
+
}
|
|
79
|
+
const boundedTexts = boundFreeTextValues(findings.flatMap((finding) => [finding.claim, finding.requiredAction, finding.roundSummary]));
|
|
80
|
+
return findings.map((finding, index) => ({
|
|
81
|
+
...finding,
|
|
82
|
+
claim: boundedTexts[index * 3],
|
|
83
|
+
requiredAction: boundedTexts[index * 3 + 1],
|
|
84
|
+
roundSummary: boundedTexts[index * 3 + 2],
|
|
85
|
+
files: boundChangedFileList(finding.files),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
// Maximum commit subjects rendered per commit-subject list that reaches a
|
|
89
|
+
// prompt (the aggregate completion range and the scope review's commits-in-scope
|
|
90
|
+
// list). Matches CHANGED_FILE_LIST_LIMIT so both run-scaling list bounds read
|
|
91
|
+
// the same way.
|
|
92
|
+
export const COMMIT_SUBJECT_LIST_LIMIT = 20;
|
|
93
|
+
// Character cap for one rendered git summary block that grows with run length
|
|
94
|
+
// (diff-stat output, one line per file). Applied via truncateInlineSectionBody,
|
|
95
|
+
// so an over-cap block carries an explicit truncation marker.
|
|
96
|
+
export const GIT_SUMMARY_SECTION_MAX_CHARS = 20_000;
|
|
97
|
+
// Bounds a commit-subject list for prompt rendering: the first
|
|
98
|
+
// COMMIT_SUBJECT_LIST_LIMIT subjects render with their text sharing the fixed
|
|
99
|
+
// aggregate free-text budget (subjects are largely agent-authored), and the
|
|
100
|
+
// rest collapse to an explicit "(+N more)" entry. The underlying arrays keep
|
|
101
|
+
// every subject for non-prompt consumers. Never mutates the input.
|
|
102
|
+
export function boundCommitSubjectList(subjects) {
|
|
103
|
+
const kept = boundFreeTextValues(subjects.slice(0, COMMIT_SUBJECT_LIST_LIMIT));
|
|
104
|
+
if (subjects.length <= COMMIT_SUBJECT_LIST_LIMIT) {
|
|
105
|
+
return kept;
|
|
106
|
+
}
|
|
107
|
+
return [...kept, `(+${subjects.length - COMMIT_SUBJECT_LIST_LIMIT} more)`];
|
|
108
|
+
}
|
|
109
|
+
// Maximum file paths rendered per changed-file list that reaches a prompt.
|
|
110
|
+
// Matches the reviewer continuity packet's per-scope bound
|
|
111
|
+
// (COMPLETED_SCOPE_CHANGED_FILE_LIMIT in src/neal/context/reviewer-context.ts).
|
|
112
|
+
export const CHANGED_FILE_LIST_LIMIT = 20;
|
|
113
|
+
// Bounds a changed-file list for prompt rendering: the first
|
|
114
|
+
// CHANGED_FILE_LIST_LIMIT entries render and the rest collapse to an explicit
|
|
115
|
+
// "(+N more)" marker entry. Callers join or JSON-embed the result; the
|
|
116
|
+
// underlying arrays in state and packets keep every path.
|
|
117
|
+
export function boundChangedFileList(files, limit = CHANGED_FILE_LIST_LIMIT) {
|
|
118
|
+
if (files.length <= limit) {
|
|
119
|
+
return [...files];
|
|
120
|
+
}
|
|
121
|
+
return [...files.slice(0, limit), `(+${files.length - limit} more)`];
|
|
122
|
+
}
|
|
7
123
|
// Canonical forbidden-phrase list for no-read prompts (today: the blocked-run
|
|
8
124
|
// consultant, which always judges from Neal-inlined in-memory context). A
|
|
9
125
|
// no-read prompt must contain no instruction that requires repository, file,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getFinalCompletionContinueExecutionMax } from './config.js';
|
|
4
|
+
import { boundChangedFileList } from './context/inline-review-context.js';
|
|
4
5
|
import { getChangedFilesForRange, getCommitRange, getCommitSubjects, getDiffStatForRange, } from './git.js';
|
|
5
6
|
import { toResidualReviewDebt } from './review-debt.js';
|
|
6
7
|
import { buildScopeAccountingSummary, getCurrentScopeLabel } from './scopes.js';
|
|
7
8
|
import { getDerivedPlanView, getFinalCompletionView } from './state-views.js';
|
|
8
|
-
import {
|
|
9
|
+
import { buildVerificationTally, extractVerificationCommandResults, } from './verification-events.js';
|
|
9
10
|
function getEventsPath(runDir) {
|
|
10
11
|
return join(runDir, 'events.ndjson');
|
|
11
12
|
}
|
|
@@ -27,7 +28,7 @@ function renderCompletedScopeSummary(scopes) {
|
|
|
27
28
|
return scopes
|
|
28
29
|
.map((scope) => {
|
|
29
30
|
const changedFiles = scope.changedFiles.length > 0
|
|
30
|
-
? `${scope.changedFiles.length} file(s): ${scope.changedFiles.join(', ')}`
|
|
31
|
+
? `${scope.changedFiles.length} file(s): ${boundChangedFileList(scope.changedFiles).join(', ')}`
|
|
31
32
|
: 'no changed files';
|
|
32
33
|
const commit = scope.finalCommit ?? 'pending';
|
|
33
34
|
const parent = scope.derivedFromParentScope ? ` | parent ${scope.derivedFromParentScope}` : '';
|
|
@@ -191,8 +192,6 @@ export async function buildFinalCompletionPacket(args) {
|
|
|
191
192
|
const finalCommit = terminalScope?.finalCommit ?? args.state.finalCommit;
|
|
192
193
|
const effectiveScopes = mergeCompletedScopesWithTerminalScope(args.state, terminalScope);
|
|
193
194
|
const allVerificationCommandResults = await loadVerificationCommandResults(args.state.runDir);
|
|
194
|
-
const verificationCommandResults = latestCommandResultPerCommand(allVerificationCommandResults);
|
|
195
|
-
const verificationCommands = verificationCommandResults.map((result) => result.command);
|
|
196
195
|
const scopeAccounting = buildScopeAccountingSummary(effectiveScopes);
|
|
197
196
|
const terminalChangedFiles = [...(terminalScope?.changedFiles ?? [])];
|
|
198
197
|
const planChangedFiles = uniqueFiles(effectiveScopes
|
|
@@ -219,9 +218,7 @@ export async function buildFinalCompletionPacket(args) {
|
|
|
219
218
|
planChangedFilesSummary: summarizeChangedFiles(planChangedFiles),
|
|
220
219
|
residualReviewDebt,
|
|
221
220
|
residualReviewDebtSummary: summarizeResidualReviewDebt(effectiveScopes),
|
|
222
|
-
|
|
223
|
-
verificationCommandResults,
|
|
224
|
-
verificationSummary: summarizeVerificationCommandResults(verificationCommandResults),
|
|
221
|
+
verificationTally: buildVerificationTally(allVerificationCommandResults),
|
|
225
222
|
lastNonEmptyImplementationScope: findLastNonEmptyImplementationScope(effectiveScopes, terminalScope, args.state),
|
|
226
223
|
continueExecutionCount: finalCompletion?.continueExecutionCount ?? 0,
|
|
227
224
|
continueExecutionMax: Math.max(0, getFinalCompletionContinueExecutionMax(args.state.cwd)),
|