@navels/neal 0.1.0 → 0.3.0
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 +264 -24
- package/dist/neal/agents/prompts.js +7 -15
- package/dist/neal/agents/rounds.js +51 -73
- package/dist/neal/agents/schemas.js +615 -502
- package/dist/neal/agents/structured-json.js +36 -0
- 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 +40 -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/git.js +9 -3
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +215 -113
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +124 -112
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/split-plan.js +12 -11
- package/dist/neal/orchestrator/transitions.js +36 -71
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-doc.js +24 -1
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/assert-builder.js +8 -1
- package/dist/neal/prompts/execute.js +11 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +28 -22
- package/dist/neal/prompts/specs.js +223 -17
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +309 -248
- 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 +102 -204
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1079 -403
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +124 -0
- package/dist/neal/providers/rate-card.js +2275 -0
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/telemetry.js +4 -0
- 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 +36 -7
- 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/run-metrics.js +74 -9
- 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 -110
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +74 -32
- 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 +330 -283
- package/docs/release.md +43 -40
- 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 +20 -15
- package/dist/neal/providers/generic-agentic.js +0 -1238
- package/docs/comparison.md +0 -105
- 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,10 +1,11 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, parse } from 'node:path';
|
|
3
3
|
import { CoderRoundError, ReviewerRoundError, runCoderPlanRound } from '../../agents.js';
|
|
4
|
-
import {
|
|
5
|
-
import { isDerivedPlanReviewState, plannerProviderStartsFreshSessions, resolvePlanningAdjudicationContext, runPlanningResponseAdjudication, runPlanningReviewerAdjudication, } from '../../adjudicator/planning.js';
|
|
4
|
+
import { isOpenBlockingFinding, isOpenNonBlockingFinding, mapDecisionToStatus, } from '../../adjudicator/execute.js';
|
|
5
|
+
import { getDerivedPlanBlockedReason, isDerivedPlanReviewState, plannerProviderStartsFreshSessions, resolvePlanningAdjudicationContext, runPlanningResponseAdjudication, runPlanningReviewerAdjudication, synthesizePlanReviewRound, } from '../../adjudicator/planning.js';
|
|
6
6
|
import { assertAdjudicationTransitionSignal } from '../../adjudicator/specs.js';
|
|
7
|
-
import { getReviewStuckWindow } from '../../config.js';
|
|
7
|
+
import { getPlanReviewDebtRoundThreshold, getReviewStuckWindow } from '../../config.js';
|
|
8
|
+
import { toPlanReviewDebt } from '../../review-debt.js';
|
|
8
9
|
import { writeDiagnostic } from '../../diagnostic.js';
|
|
9
10
|
import { getWorktreeStatus } from '../../git.js';
|
|
10
11
|
import { formatPlanRefinementRoundLine, isPlanRefinementState } from '../../plan-refinement.js';
|
|
@@ -13,7 +14,7 @@ import { getCurrentScopeLabel, getExecutionPlanPath } from '../../scopes.js';
|
|
|
13
14
|
import { saveState } from '../../state.js';
|
|
14
15
|
import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, } from '../../worktree-status.js';
|
|
15
16
|
import { writeExecutionArtifacts } from '../artifacts.js';
|
|
16
|
-
import { isCoderTimeoutError, shouldNotifyFailure } from '../failures.js';
|
|
17
|
+
import { isCoderTimeoutError, REVIEWER_CONTENT_REFUSED_BLOCK_REASON, shouldNotifyFailure } from '../failures.js';
|
|
17
18
|
import { flushDerivedPlanNotifications, notifyBlocked, notifyComplete, } from '../notifications.js';
|
|
18
19
|
import { adoptAcceptedDerivedPlan, shouldNotifyDerivedPlanAcceptance, transitionPlanReviewWithoutOpenFindings, } from '../transitions.js';
|
|
19
20
|
import { enterInteractiveBlockedRecovery, shouldNotifyInteractiveBlockedRecoveryEntry } from './recovery.js';
|
|
@@ -58,6 +59,9 @@ export async function synthesizePlanReviewFindings(args) {
|
|
|
58
59
|
round: args.round,
|
|
59
60
|
source: 'plan_structure',
|
|
60
61
|
severity: 'blocking',
|
|
62
|
+
// Structural defects are always plan-correctness (fail-safe blocking);
|
|
63
|
+
// they must never be treated as convertible verification-hardening debt.
|
|
64
|
+
findingClass: 'plan_correctness',
|
|
61
65
|
files: [preparedReview.originalPlanPath],
|
|
62
66
|
claim: `Plan document structure is invalid: ${error}`,
|
|
63
67
|
requiredAction: 'Revise the plan document so it satisfies the required execution-shape and execution-queue contract.',
|
|
@@ -72,12 +76,6 @@ function getPlanningCompletionProblem(marker) {
|
|
|
72
76
|
}
|
|
73
77
|
return marker === 'AUTONOMY_DONE' ? null : 'Planning mode must end with AUTONOMY_DONE or AUTONOMY_BLOCKED.';
|
|
74
78
|
}
|
|
75
|
-
function getDerivedPlanBlockedReason(state, reason) {
|
|
76
|
-
if (!isDerivedPlanReviewState(state)) {
|
|
77
|
-
return reason;
|
|
78
|
-
}
|
|
79
|
-
return `split-plan recovery failed to converge: ${reason}`;
|
|
80
|
-
}
|
|
81
79
|
async function getPlanPhaseDirtyWorktreeBlocker(state, phase) {
|
|
82
80
|
const allowedDirtyPaths = [...new Set([...state.allowedDirtyPaths, state.planDoc])];
|
|
83
81
|
let statusOutput;
|
|
@@ -122,18 +120,34 @@ async function finalizePlanReviewResponseWithoutOpenFindings(state, statePath, p
|
|
|
122
120
|
}
|
|
123
121
|
return nextState;
|
|
124
122
|
}
|
|
125
|
-
export async function finalizeBlockedPlanReviewResponse(state, statePath, derivedPlanReview, blocker, logger) {
|
|
123
|
+
export async function finalizeBlockedPlanReviewResponse(state, statePath, derivedPlanReview, blocker, blockCause, logger) {
|
|
126
124
|
const { spec } = resolvePlanningAdjudicationContext(state);
|
|
127
125
|
assertAdjudicationTransitionSignal(spec, 'block_for_operator', 'orchestrator:planning:block_for_operator');
|
|
128
126
|
if (state.topLevelMode !== 'execute') {
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
// Top-level plan stage (`neal run`/`neal plan`).
|
|
128
|
+
if (blockCause === 'coder_authored') {
|
|
129
|
+
// Recoverable landing for a coder-authored *response* block: persist the
|
|
130
|
+
// durable blocker reason and leave `status:'blocked'` (writer exit 2) for
|
|
131
|
+
// an operator to answer via `neal resume --message`. Attended and
|
|
132
|
+
// unattended land identically now — both blocked-with-reason — because
|
|
133
|
+
// neither could previously answer this block (attended left it blocked but
|
|
134
|
+
// reason-less; unattended terminal-failed).
|
|
135
|
+
const blockedState = await saveState(statePath, {
|
|
136
|
+
...state,
|
|
137
|
+
blockerReason: blocker,
|
|
138
|
+
});
|
|
139
|
+
if (!derivedPlanReview) {
|
|
140
|
+
await notifyBlocked(blockedState, blocker, logger);
|
|
141
|
+
}
|
|
142
|
+
return flushDerivedPlanNotifications(blockedState, statePath, logger, blocker);
|
|
143
|
+
}
|
|
144
|
+
// Site C: a `dirty_worktree` safety block or a `reviewer_convergence`
|
|
145
|
+
// cap/stall block. Attended runs leave `status:'blocked'`; unattended runs
|
|
146
|
+
// have no operator to answer, so run the shared terminal-fail action. The
|
|
147
|
+
// dirty-worktree case must not be silently converted to the recoverable
|
|
148
|
+
// landing (the planner dirtied non-plan files with no operator to clean it),
|
|
149
|
+
// and the convergence cap already bounded the autonomous push. Any plan
|
|
150
|
+
// artifact is preserved unsubmitted, exactly as today's failed runs leave it.
|
|
137
151
|
if (state.unattended) {
|
|
138
152
|
return persistUnattendedBlockUnresolvedFailure(state, statePath, 'reviewer_plan', logger);
|
|
139
153
|
}
|
|
@@ -213,6 +227,9 @@ export async function runCoderPlanPhase(state, statePath, logger) {
|
|
|
213
227
|
phase: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem || dirtyWorktreeBlocker ? 'blocked' : 'reviewer_plan',
|
|
214
228
|
status: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem || dirtyWorktreeBlocker ? 'blocked' : 'running',
|
|
215
229
|
blockedFromPhase: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem || dirtyWorktreeBlocker ? 'coder_plan' : null,
|
|
230
|
+
// The initial coder_plan authoring block never carries the recoverable
|
|
231
|
+
// coder-response blocker reason; keep it cleared whether we advance or block.
|
|
232
|
+
blockerReason: null,
|
|
216
233
|
});
|
|
217
234
|
await writeExecutionArtifacts(nextState);
|
|
218
235
|
await logger?.event('phase.complete', {
|
|
@@ -261,6 +278,31 @@ export async function runPlanReviewPhase(state, statePath, logger) {
|
|
|
261
278
|
}
|
|
262
279
|
catch (error) {
|
|
263
280
|
if (error instanceof ReviewerRoundError) {
|
|
281
|
+
// A content-safety refusal is a distinct, terminal, non-coder-recoverable
|
|
282
|
+
// condition: end the run blocked (exit 2) with a durable actionable reason
|
|
283
|
+
// by RETURNING the terminal blocked state, never throwing (a thrown error
|
|
284
|
+
// over a persisted blocked state escapes executeRun).
|
|
285
|
+
if (error.kind === 'content_refused') {
|
|
286
|
+
const blockedState = await saveState(statePath, {
|
|
287
|
+
...state,
|
|
288
|
+
reviewerSessionHandle: null,
|
|
289
|
+
phase: 'blocked',
|
|
290
|
+
status: 'blocked',
|
|
291
|
+
blockedFromPhase: null,
|
|
292
|
+
blockerReason: REVIEWER_CONTENT_REFUSED_BLOCK_REASON,
|
|
293
|
+
});
|
|
294
|
+
await writeExecutionArtifacts(blockedState);
|
|
295
|
+
await logger?.event('phase.error', {
|
|
296
|
+
phase: 'reviewer_plan',
|
|
297
|
+
round,
|
|
298
|
+
sessionHandle: error.sessionHandle,
|
|
299
|
+
subtype: error.subtype,
|
|
300
|
+
errorKind: error.kind,
|
|
301
|
+
message: error.message,
|
|
302
|
+
});
|
|
303
|
+
await notifyBlocked(blockedState, REVIEWER_CONTENT_REFUSED_BLOCK_REASON, logger);
|
|
304
|
+
return blockedState;
|
|
305
|
+
}
|
|
264
306
|
const failedState = await saveState(statePath, {
|
|
265
307
|
...state,
|
|
266
308
|
reviewerSessionHandle: null,
|
|
@@ -290,100 +332,40 @@ export async function runPlanReviewPhase(state, statePath, logger) {
|
|
|
290
332
|
// as a soft planner-prompt bias than a hard shape clamp.
|
|
291
333
|
const normalizedFindingInputs = synthesizedReview.findings;
|
|
292
334
|
printReviewResult('plan-review', claude.summary, normalizedFindingInputs, logger);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const stalledBlockingCount = hasRepeatedUnresolvedBlockingCanonicals({
|
|
313
|
-
rounds: state.rounds,
|
|
314
|
-
currentOpenBlockingCanonicals: openBlockingCanonicalSet,
|
|
315
|
-
cwd: state.cwd,
|
|
335
|
+
const { findings, mergedFindings, disposition, blockReason, roundRecord, planReviewDebt } = synthesizePlanReviewRound({
|
|
336
|
+
state,
|
|
337
|
+
round,
|
|
338
|
+
roundLimit,
|
|
339
|
+
reviewStuckWindow: getReviewStuckWindow(state.cwd),
|
|
340
|
+
debtRoundThreshold: getPlanReviewDebtRoundThreshold(state.cwd),
|
|
341
|
+
derivedPlanReview,
|
|
342
|
+
currentDerivedPlanStatus: state.derivedPlanStatus,
|
|
343
|
+
executionShape: synthesizedReview.executionShape,
|
|
344
|
+
findingInputs: normalizedFindingInputs,
|
|
345
|
+
reviewerSessionHandle: claude.sessionHandle,
|
|
346
|
+
reviewedPlanPath: synthesizedReview.reviewedPlanPath,
|
|
347
|
+
normalizationApplied: preparedReview.validation.normalization.applied,
|
|
348
|
+
normalizationOperations: preparedReview.validation.normalization.operations,
|
|
349
|
+
normalizationScopeLabelMappings: preparedReview.validation.normalization.scopeLabelMappings,
|
|
350
|
+
commitRange: {
|
|
351
|
+
base: state.baseCommit ?? '',
|
|
352
|
+
head: state.finalCommit ?? state.baseCommit ?? '',
|
|
353
|
+
},
|
|
316
354
|
});
|
|
317
|
-
|
|
318
|
-
const shouldBlockForConvergence = Boolean(reopenedCanonical || stalledBlockingCount);
|
|
319
|
-
const blockReason = reopenedCanonical
|
|
320
|
-
? getDerivedPlanBlockedReason(state, `review_stuck: blocking finding ${reopenedCanonical} reopened across multiple reviewer rounds`)
|
|
321
|
-
: stalledBlockingCount
|
|
322
|
-
? getDerivedPlanBlockedReason(state, `review_stuck: blocking findings did not decrease across ${getReviewStuckWindow(state.cwd)} consecutive reviewer rounds`)
|
|
323
|
-
: reachedMaxRounds && hasBlockingFindings
|
|
324
|
-
? getDerivedPlanBlockedReason(state, `reached max review rounds (${roundLimit}) with blocking findings still open`)
|
|
325
|
-
: null;
|
|
326
|
-
const planningSignal = shouldBlockForConvergence || (hasBlockingFindings && reachedMaxRounds)
|
|
327
|
-
? 'block_for_operator'
|
|
328
|
-
: hasBlockingFindings
|
|
329
|
-
? 'request_revision'
|
|
330
|
-
: hasOpenNonBlockingFindings
|
|
331
|
-
? 'optional_revision'
|
|
332
|
-
: derivedPlanReview
|
|
333
|
-
? 'accept_derived_plan'
|
|
334
|
-
: 'accept_plan';
|
|
335
|
-
assertAdjudicationTransitionSignal(context.spec, planningSignal, 'orchestrator:reviewer_plan');
|
|
355
|
+
assertAdjudicationTransitionSignal(context.spec, disposition.planningSignal, 'orchestrator:reviewer_plan');
|
|
336
356
|
const nextState = await saveState(statePath, {
|
|
337
357
|
...state,
|
|
338
358
|
reviewerSessionHandle: claude.sessionHandle,
|
|
339
359
|
executionShape: synthesizedReview.executionShape,
|
|
340
|
-
phase:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
? reachedMaxRounds
|
|
344
|
-
? 'blocked'
|
|
345
|
-
: 'coder_plan_response'
|
|
346
|
-
: hasOpenNonBlockingFindings
|
|
347
|
-
? 'coder_plan_optional_response'
|
|
348
|
-
: derivedPlanReview
|
|
349
|
-
? 'awaiting_derived_plan_execution'
|
|
350
|
-
: 'done',
|
|
351
|
-
status: shouldBlockForConvergence
|
|
352
|
-
? 'blocked'
|
|
353
|
-
: hasBlockingFindings
|
|
354
|
-
? reachedMaxRounds
|
|
355
|
-
? 'blocked'
|
|
356
|
-
: 'running'
|
|
357
|
-
: hasOpenNonBlockingFindings
|
|
358
|
-
? 'running'
|
|
359
|
-
: derivedPlanReview
|
|
360
|
-
? 'running'
|
|
361
|
-
: 'done',
|
|
362
|
-
rounds: [
|
|
363
|
-
...state.rounds,
|
|
364
|
-
{
|
|
365
|
-
round,
|
|
366
|
-
reviewerSessionHandle: claude.sessionHandle,
|
|
367
|
-
reviewedPlanPath: synthesizedReview.reviewedPlanPath,
|
|
368
|
-
normalizationApplied: preparedReview.validation.normalization.applied,
|
|
369
|
-
normalizationOperations: preparedReview.validation.normalization.operations,
|
|
370
|
-
normalizationScopeLabelMappings: preparedReview.validation.normalization.scopeLabelMappings,
|
|
371
|
-
commitRange: {
|
|
372
|
-
base: state.baseCommit ?? '',
|
|
373
|
-
head: state.finalCommit ?? state.baseCommit ?? '',
|
|
374
|
-
},
|
|
375
|
-
openBlockingCanonicalCount,
|
|
376
|
-
openBlockingCanonicalIds,
|
|
377
|
-
findings: findings.map((finding) => finding.id),
|
|
378
|
-
},
|
|
379
|
-
],
|
|
360
|
+
phase: disposition.phase,
|
|
361
|
+
status: disposition.status,
|
|
362
|
+
rounds: [...state.rounds, roundRecord],
|
|
380
363
|
findings: mergedFindings,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
: null,
|
|
364
|
+
// Recompute the current-negotiation plan-review debt projection from the
|
|
365
|
+
// post-conversion merged findings. Never touches inheritedPlanReviewDebt.
|
|
366
|
+
planReviewDebt,
|
|
367
|
+
derivedPlanStatus: disposition.derivedPlanStatus,
|
|
368
|
+
blockedFromPhase: disposition.blockedFromPhase,
|
|
387
369
|
});
|
|
388
370
|
await writeExecutionArtifacts(nextState);
|
|
389
371
|
await logger?.event('phase.complete', {
|
|
@@ -395,7 +377,8 @@ export async function runPlanReviewPhase(state, statePath, logger) {
|
|
|
395
377
|
nextPhase: nextState.phase,
|
|
396
378
|
});
|
|
397
379
|
if (nextState.status === 'blocked' && blockReason) {
|
|
398
|
-
|
|
380
|
+
// A reviewer convergence/cap terminal block, never a coder-authored one.
|
|
381
|
+
return finalizeBlockedPlanReviewResponse(nextState, statePath, derivedPlanReview, blockReason, 'reviewer_convergence', logger);
|
|
399
382
|
}
|
|
400
383
|
if (shouldNotifyDerivedPlanAcceptance(state, nextState)) {
|
|
401
384
|
return flushDerivedPlanNotifications(nextState, statePath, logger);
|
|
@@ -412,7 +395,7 @@ export async function runPlanningResponsePhase(state, statePath, phase, logger)
|
|
|
412
395
|
// authored by a persisted planner session, so a missing handle stays a hard error for
|
|
413
396
|
// resume-capable planner providers. Derived-plan revisions are authored by the coder and create
|
|
414
397
|
// no planner session, and no-resume planner providers (`supportsSessionResume: false`, e.g.
|
|
415
|
-
//
|
|
398
|
+
// openai-compatible) never persist a planner handle at all — they start a
|
|
416
399
|
// fresh planner session instead.
|
|
417
400
|
if (!derivedPlanReview &&
|
|
418
401
|
!state.plannerSessionHandle &&
|
|
@@ -429,7 +412,12 @@ export async function runPlanningResponsePhase(state, statePath, phase, logger)
|
|
|
429
412
|
writeDiagnostic(`${formatPlanRefinementRoundLine({ round: state.rounds.length + 1, maxRounds: state.maxRounds })}\n`, logger);
|
|
430
413
|
}
|
|
431
414
|
const openFindings = state.findings.filter(mode === 'optional' ? isOpenNonBlockingFinding : isOpenBlockingFinding);
|
|
432
|
-
|
|
415
|
+
// Recorded operator guidance must reach the planner. If a prior blocked response
|
|
416
|
+
// closed every finding, the guidance would otherwise be silently discarded here
|
|
417
|
+
// (finalizePlanReviewResponseWithoutOpenFindings clears pendingPlanReviewGuidance
|
|
418
|
+
// and accepts the plan) without ever delivering the operator's answer. Force a
|
|
419
|
+
// response adjudication whenever guidance is pending so it is delivered first.
|
|
420
|
+
if (openFindings.length === 0 && state.pendingPlanReviewGuidance === null) {
|
|
433
421
|
return finalizePlanReviewResponseWithoutOpenFindings(state, statePath, phase, logger);
|
|
434
422
|
}
|
|
435
423
|
let codex;
|
|
@@ -468,7 +456,22 @@ export async function runPlanningResponsePhase(state, statePath, phase, logger)
|
|
|
468
456
|
const responseById = new Map(codex.payload.responses.map((response) => [response.id, response]));
|
|
469
457
|
assertAdjudicationTransitionSignal(spec, codex.payload.outcome === 'blocked' ? 'block_for_operator' : mode === 'optional' ? 'optional_revision' : 'request_revision', `orchestrator:${phase}`);
|
|
470
458
|
const dirtyWorktreeBlocker = await getPlanPhaseDirtyWorktreeBlocker(state, phase);
|
|
459
|
+
// Response-eligibility contract: only findings this invocation actually
|
|
460
|
+
// presented to the coder (the mode-matching `openFindings` set — open blocking
|
|
461
|
+
// for a required response, open non-blocking for an optional one) may be
|
|
462
|
+
// dispositioned. The response `id` schema is an unconstrained `z.string()`, so a
|
|
463
|
+
// planner can name any finding; a response whose `id` is outside `openFindings`
|
|
464
|
+
// — an already-`deferred` banked-debt finding, a prior-round `fixed`/`rejected`
|
|
465
|
+
// finding, a wrong-mode finding (a blocking id in an optional response or the
|
|
466
|
+
// reverse), or an unknown id — is a no-op that never mutates status or
|
|
467
|
+
// coderDisposition. This keeps `toPlanReviewDebt`'s reservation intact (a banked
|
|
468
|
+
// hardening finding cannot be silently un-banked by an out-of-band disposition)
|
|
469
|
+
// and matches the replay harness's open-blocking eligibility guard.
|
|
470
|
+
const openFindingIds = new Set(openFindings.map((finding) => finding.id));
|
|
471
471
|
const findings = state.findings.map((finding) => {
|
|
472
|
+
if (!openFindingIds.has(finding.id)) {
|
|
473
|
+
return finding;
|
|
474
|
+
}
|
|
472
475
|
const response = responseById.get(finding.id);
|
|
473
476
|
if (!response) {
|
|
474
477
|
return finding;
|
|
@@ -485,6 +488,10 @@ export async function runPlanningResponsePhase(state, statePath, phase, logger)
|
|
|
485
488
|
plannerSessionHandle: codex.sessionHandle,
|
|
486
489
|
plannerSessionProtocol: codex.sessionHandle ? activePlannerSessionProtocol : null,
|
|
487
490
|
findings,
|
|
491
|
+
// Reproject the current-negotiation plan-review debt after applying coder
|
|
492
|
+
// dispositions (a re-defer keeps debt; a fix/reject/reopen drops it). Never
|
|
493
|
+
// touches inheritedPlanReviewDebt.
|
|
494
|
+
planReviewDebt: toPlanReviewDebt(findings),
|
|
488
495
|
phase: dirtyWorktreeBlocker || codex.payload.outcome === 'blocked'
|
|
489
496
|
? 'blocked'
|
|
490
497
|
: mode === 'optional'
|
|
@@ -502,6 +509,9 @@ export async function runPlanningResponsePhase(state, statePath, phase, logger)
|
|
|
502
509
|
: state.derivedPlanStatus,
|
|
503
510
|
blockedFromPhase: dirtyWorktreeBlocker || codex.payload.outcome === 'blocked' ? phase : null,
|
|
504
511
|
pendingPlanReviewGuidance: null,
|
|
512
|
+
// Clear any stale reason as we advance to running/done; the recoverable
|
|
513
|
+
// coder-authored blocked landing re-persists it in finalizeBlockedPlanReviewResponse.
|
|
514
|
+
blockerReason: null,
|
|
505
515
|
});
|
|
506
516
|
await writeExecutionArtifacts(nextState);
|
|
507
517
|
await logger?.event('phase.complete', {
|
|
@@ -511,13 +521,15 @@ export async function runPlanningResponsePhase(state, statePath, phase, logger)
|
|
|
511
521
|
nextPhase: nextState.phase,
|
|
512
522
|
});
|
|
513
523
|
if (nextState.status === 'blocked') {
|
|
514
|
-
const blocker = getDerivedPlanBlockedReason(
|
|
524
|
+
const blocker = getDerivedPlanBlockedReason(derivedPlanReview, dirtyWorktreeBlocker ||
|
|
515
525
|
codex.payload.blocker?.trim() ||
|
|
516
526
|
codex.payload.summary.trim() ||
|
|
517
527
|
(mode === 'optional'
|
|
518
528
|
? 'The coder reported a blocker while considering non-blocking plan findings'
|
|
519
529
|
: 'The coder reported a blocker during plan response'));
|
|
520
|
-
|
|
530
|
+
// The coder authored the block unless the safety net tripped on a dirtied
|
|
531
|
+
// non-plan worktree; the cause drives the recoverable-vs-terminal landing.
|
|
532
|
+
return finalizeBlockedPlanReviewResponse(nextState, statePath, derivedPlanReview, blocker, dirtyWorktreeBlocker ? 'dirty_worktree' : 'coder_authored', logger);
|
|
521
533
|
}
|
|
522
534
|
if (mode === 'optional' && shouldNotifyDerivedPlanAcceptance(state, nextState)) {
|
|
523
535
|
return flushDerivedPlanNotifications(nextState, statePath, logger);
|