@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
|
@@ -11,8 +11,8 @@ import { getFinalCompletionReviewArtifactPath, readFinalCompletionUnstructuredOu
|
|
|
11
11
|
import { EXECUTE_FINALIZATION_PHASE } from '../execute-finalization.js';
|
|
12
12
|
import { getChangedFilesForRange, getCommitRange, getCommitMessage, getCommitSubjects, getHeadCommit, getStagedChangedFiles, getWorktreeStatus, assertNoIgnoredChangedFiles, cleanUntracked, resetHard, squashCommits as createScopeFinalizationCommit, stagePath, } from '../git.js';
|
|
13
13
|
import { notifyBlocked, notifyComplete, notifyScopeAccepted } from './notifications.js';
|
|
14
|
-
import { inspectPlanDocDisposition } from '../plan-doc.js';
|
|
15
|
-
import { appendDerivedSubScopeAndParentCompletion, appendParentCompletionFromAcceptedDerivedScopes, computeNextScopeStateAfterExecuteFinalization, computeNextScopeStateAfterParentAdvance, } from './transitions.js';
|
|
14
|
+
import { inspectPlanDocDisposition, withPlanDocPreserved } from '../plan-doc.js';
|
|
15
|
+
import { appendDerivedSubScopeAndParentCompletion, appendParentCompletionFromAcceptedDerivedScopes, computeNextScopeStateAfterExecuteFinalization, computeNextScopeStateAfterParentAdvance, createNextScopeEntryReset, } from './transitions.js';
|
|
16
16
|
import { writePlanProgressArtifacts } from '../progress.js';
|
|
17
17
|
import { writeCheckpointRetrospective } from '../retrospective.js';
|
|
18
18
|
import { renderReviewMarkdown, writeReviewMarkdown } from '../review.js';
|
|
@@ -21,7 +21,7 @@ import { saveState } from '../state.js';
|
|
|
21
21
|
import { getFinalCompletionReviewerScratchDir, getScopeReviewerScratchDir } from '../storage-paths.js';
|
|
22
22
|
import { requireFinalCompletionView, resolveFinalCompletionReviewAction, } from '../state-views.js';
|
|
23
23
|
import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, formatDirtyWorktreeDiagnostic, } from '../worktree-status.js';
|
|
24
|
-
import { shouldNotifyFailure } from './failures.js';
|
|
24
|
+
import { REVIEWER_CONTENT_REFUSED_BLOCK_REASON, shouldNotifyFailure } from './failures.js';
|
|
25
25
|
import { persistUnattendedBlockUnresolvedFailure } from './phases/shared.js';
|
|
26
26
|
async function writeFailedFinalCompletionReviewArtifact(args) {
|
|
27
27
|
const unstructuredOutput = await readFinalCompletionUnstructuredOutput({
|
|
@@ -52,6 +52,38 @@ async function persistFailedFinalCompletionReview(args) {
|
|
|
52
52
|
});
|
|
53
53
|
return failedState;
|
|
54
54
|
}
|
|
55
|
+
// Sibling of persistFailedFinalCompletionReview for the terminal blocked
|
|
56
|
+
// landing of a reviewer content-safety refusal (`content_refused`). Only a
|
|
57
|
+
// ReviewerRoundError can carry that kind; FinalCompletionReviewerVerdictError
|
|
58
|
+
// never does, so persistFailedFinalCompletionReview keeps serving the
|
|
59
|
+
// verdict-invalid path unchanged. Persists a terminal
|
|
60
|
+
// phase:'blocked'/status:'blocked' state so the run ends blocked (exit 2)
|
|
61
|
+
// rather than as a generic terminal failure, still writing the failed
|
|
62
|
+
// final-completion review artifact and execution artifacts.
|
|
63
|
+
async function persistBlockedFinalCompletionReview(args) {
|
|
64
|
+
const blockedState = await saveState(args.statePath, {
|
|
65
|
+
...args.state,
|
|
66
|
+
phase: 'blocked',
|
|
67
|
+
reviewerSessionHandle: null,
|
|
68
|
+
status: 'blocked',
|
|
69
|
+
blockedFromPhase: null,
|
|
70
|
+
blockerReason: REVIEWER_CONTENT_REFUSED_BLOCK_REASON,
|
|
71
|
+
});
|
|
72
|
+
await args.runtime.writeExecutionArtifacts(blockedState);
|
|
73
|
+
await writeFailedFinalCompletionReviewArtifact({
|
|
74
|
+
state: blockedState,
|
|
75
|
+
source: 'reviewer_verdict',
|
|
76
|
+
sessionHandle: args.error.sessionHandle,
|
|
77
|
+
});
|
|
78
|
+
await args.logger?.event('phase.error', {
|
|
79
|
+
phase: 'final_completion_review',
|
|
80
|
+
sessionHandle: args.error.sessionHandle,
|
|
81
|
+
subtype: args.error.subtype,
|
|
82
|
+
errorKind: args.error.kind,
|
|
83
|
+
message: args.error.message,
|
|
84
|
+
});
|
|
85
|
+
return blockedState;
|
|
86
|
+
}
|
|
55
87
|
function normalizeFinalCommitMessage(message) {
|
|
56
88
|
const normalizedNewlines = message.replace(/\r\n/g, '\n');
|
|
57
89
|
const convertedEscapes = normalizedNewlines.replace(/\\n(?=- )/g, '\n');
|
|
@@ -105,7 +137,12 @@ function getFinalCompletionReviewBlockReason(args) {
|
|
|
105
137
|
return `final_completion_review: reviewer blocked completion for operator guidance. ${args.rationale} One available next step is to inspect with \`neal status\` and provide guidance with \`neal resume --message "..."\` only if the run is waiting for operator guidance.`;
|
|
106
138
|
}
|
|
107
139
|
async function discardFinalCompletionReviewerWorktreeChanges(args) {
|
|
108
|
-
|
|
140
|
+
// The plan document is a wrapper-owned overlay, not reviewer dirt: it may be
|
|
141
|
+
// an uncommitted modification of a tracked file (a reset --hard would
|
|
142
|
+
// silently swap in the committed content — a different plan — mid-run) or an
|
|
143
|
+
// untracked seed. Exclude it from the trigger and the post-check, and
|
|
144
|
+
// preserve its bytes across the destructive cleanup.
|
|
145
|
+
const statusOutput = filterAllowedDirtyPathStatus(args.state.cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(args.state.cwd, { untrackedFiles: 'all' })), [args.state.planDoc]);
|
|
109
146
|
if (!statusOutput.trim()) {
|
|
110
147
|
return;
|
|
111
148
|
}
|
|
@@ -115,9 +152,11 @@ async function discardFinalCompletionReviewerWorktreeChanges(args) {
|
|
|
115
152
|
statusOutput,
|
|
116
153
|
message: 'Final completion review is read-only; discarding reviewer-created worktree changes before state transition.',
|
|
117
154
|
});
|
|
118
|
-
await
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
await withPlanDocPreserved(args.state.planDoc, async () => {
|
|
156
|
+
await resetHard(args.state.cwd, headCommit);
|
|
157
|
+
await cleanUntracked(args.state.cwd, ['.neal/', '.forge/', 'CURRENT_PLAN.md']);
|
|
158
|
+
});
|
|
159
|
+
const remainingStatusOutput = filterAllowedDirtyPathStatus(args.state.cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(args.state.cwd, { untrackedFiles: 'all' })), [args.state.planDoc]);
|
|
121
160
|
if (remainingStatusOutput.trim()) {
|
|
122
161
|
throw new Error(`Final completion review left a dirty worktree Neal could not restore:\n${remainingStatusOutput}`);
|
|
123
162
|
}
|
|
@@ -149,7 +188,8 @@ async function runParentAdvanceFinalization(args) {
|
|
|
149
188
|
`Neal-created commits: ${formatCommitList(state.createdCommits)}`,
|
|
150
189
|
].join('\n'));
|
|
151
190
|
}
|
|
152
|
-
|
|
191
|
+
const parentBaseCommit = state.baseCommit;
|
|
192
|
+
await withPlanDocPreserved(state.planDoc, () => resetHard(state.cwd, parentBaseCommit));
|
|
153
193
|
}
|
|
154
194
|
const finalCommit = state.baseCommit;
|
|
155
195
|
const archivedReviewPath = join(state.runDir, `REVIEW-${finalCommit}.md`);
|
|
@@ -203,11 +243,10 @@ async function runParentAdvanceFinalization(args) {
|
|
|
203
243
|
}, parentScope?.commitSubject ?? `Parent scope ${parentScopeLabel} complete via derived plan`, logger);
|
|
204
244
|
return nextState;
|
|
205
245
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await logger?.event('phase.start', { phase: EXECUTE_FINALIZATION_PHASE });
|
|
246
|
+
// Dirty-worktree admission for execute finalization: capture the head commit,
|
|
247
|
+
// filter the worktree status down to disallowed dirt, and refuse to finalize
|
|
248
|
+
// (with the expected-scratch-dir diagnostic) when any remains.
|
|
249
|
+
async function admitFinalizationWorktree(state) {
|
|
211
250
|
const headCommit = await getHeadCommit(state.cwd);
|
|
212
251
|
const statusOutput = filterAllowedDirtyPathStatus(state.cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(state.cwd)), state.allowedDirtyPaths);
|
|
213
252
|
if (statusOutput) {
|
|
@@ -217,6 +256,132 @@ export async function runExecuteFinalizationPhase(state, statePath, logger, runt
|
|
|
217
256
|
});
|
|
218
257
|
throw new Error([`Cannot finalize with a dirty worktree:\n${statusOutput}`, diagnostic].filter(Boolean).join('\n\n'));
|
|
219
258
|
}
|
|
259
|
+
return { headCommit, statusOutput };
|
|
260
|
+
}
|
|
261
|
+
// Raw material for final-commit message synthesis: the full message of the
|
|
262
|
+
// latest Neal-created commit, falling back to the last recorded subject (with
|
|
263
|
+
// any leading hash stripped) and then to the fixed default.
|
|
264
|
+
async function readRawFinalCommitMessage(cwd, createdCommits) {
|
|
265
|
+
const commitSubjects = await getCommitSubjects(cwd, createdCommits);
|
|
266
|
+
const latestCreatedCommit = createdCommits.at(-1) ?? null;
|
|
267
|
+
return latestCreatedCommit
|
|
268
|
+
? await getCommitMessage(cwd, latestCreatedCommit)
|
|
269
|
+
: commitSubjects.at(-1)?.replace(/^[a-f0-9]+\s+/, '') || 'Finalize scope work';
|
|
270
|
+
}
|
|
271
|
+
// Final-commit message synthesis: strip the scope prefix, normalize newlines
|
|
272
|
+
// (falling back to the fixed default when stripping leaves nothing), apply the
|
|
273
|
+
// plan-doc-only special case, and derive the subject line.
|
|
274
|
+
function synthesizeFinalCommitMessage(args) {
|
|
275
|
+
const strippedFinalMessage = stripScopePrefixFromCommitMessage(args.rawFinalMessage);
|
|
276
|
+
const finalMessage = normalizeFinalCommitMessage(strippedFinalMessage.trim() ? strippedFinalMessage : 'Finalize scope work');
|
|
277
|
+
const onlyChangedFile = args.finalizationChangedFiles.length === 1 ? args.finalizationChangedFiles[0] : null;
|
|
278
|
+
const onlyChangedPlanDoc = onlyChangedFile !== null &&
|
|
279
|
+
args.planDocRepoRelativePath !== null &&
|
|
280
|
+
onlyChangedFile === args.planDocRepoRelativePath;
|
|
281
|
+
const finalCommitMessage = onlyChangedPlanDoc ? 'Record Neal plan document\n' : finalMessage;
|
|
282
|
+
const finalSubject = finalCommitMessage.split(/\r?\n/, 1)[0] || 'Finalize scope work';
|
|
283
|
+
return { finalCommitMessage, finalSubject };
|
|
284
|
+
}
|
|
285
|
+
// The one shared failure-persistence path for both summary-adjudication catch
|
|
286
|
+
// branches. Ordering is pinned by test/execute-finalization-failure.test.ts:
|
|
287
|
+
// failed state save, execution artifacts, failed final-completion review
|
|
288
|
+
// artifact (coder-round branch only), checkpoint retrospective, phase.error,
|
|
289
|
+
// gated notification (coder-round branch only).
|
|
290
|
+
async function persistExecuteFinalizationFailure(args) {
|
|
291
|
+
const { state, coderRoundExtras } = args;
|
|
292
|
+
const failedState = await saveState(args.statePath, {
|
|
293
|
+
...state,
|
|
294
|
+
finalCommit: args.finalCommit,
|
|
295
|
+
archivedReviewPath: args.archivedReviewPath,
|
|
296
|
+
completedScopes: args.completedScopes,
|
|
297
|
+
...(coderRoundExtras
|
|
298
|
+
? {
|
|
299
|
+
coderSessionHandle: coderRoundExtras.sessionHandleOverride,
|
|
300
|
+
coderSessionProtocol: coderRoundExtras.sessionHandleOverride
|
|
301
|
+
? state.coderSessionProtocol ?? 'structured_json_v1'
|
|
302
|
+
: null,
|
|
303
|
+
}
|
|
304
|
+
: {}),
|
|
305
|
+
status: 'failed',
|
|
306
|
+
});
|
|
307
|
+
await args.runtime.writeExecutionArtifacts(failedState);
|
|
308
|
+
if (coderRoundExtras) {
|
|
309
|
+
await writeFailedFinalCompletionReviewArtifact({
|
|
310
|
+
state: failedState,
|
|
311
|
+
source: 'coder_summary',
|
|
312
|
+
sessionHandle: coderRoundExtras.sessionHandleOverride,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
await writeCheckpointRetrospective({
|
|
316
|
+
...failedState,
|
|
317
|
+
finalCompletionSummary: args.finalCompletionSummary,
|
|
318
|
+
}, 'failed');
|
|
319
|
+
await args.logger?.event('phase.error', {
|
|
320
|
+
phase: EXECUTE_FINALIZATION_PHASE,
|
|
321
|
+
sessionHandle: coderRoundExtras ? coderRoundExtras.sessionHandleOverride : state.coderSessionHandle,
|
|
322
|
+
message: args.errorMessage,
|
|
323
|
+
});
|
|
324
|
+
if (coderRoundExtras?.notify) {
|
|
325
|
+
await notifyBlocked(failedState, args.errorMessage, args.logger);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Summary adjudication for a terminal finalization: archive the review, build
|
|
329
|
+
// the final-completion packet, run the coder final-completion summary round,
|
|
330
|
+
// and route BOTH failure branches through the shared persistence helper
|
|
331
|
+
// before rethrowing.
|
|
332
|
+
async function adjudicateFinalCompletionSummary(args) {
|
|
333
|
+
const { state, logger } = args;
|
|
334
|
+
await writeTextAtomic(args.archivedReviewPath, renderReviewMarkdown({ ...args.archivedReviewState, finalCompletionSummary: null, finalCompletionReviewVerdict: null }));
|
|
335
|
+
const packet = await buildFinalCompletionPacket({
|
|
336
|
+
state: args.retrospectiveState,
|
|
337
|
+
terminalScope: {
|
|
338
|
+
finalCommit: args.finalCommit,
|
|
339
|
+
commitSubject: args.finalSubject,
|
|
340
|
+
changedFiles: args.finalizationChangedFiles,
|
|
341
|
+
archivedReviewPath: args.archivedReviewPath,
|
|
342
|
+
marker: state.lastScopeMarker,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
const { summary: finalCompletion } = await runFinalCompletionSummaryAdjudication({
|
|
347
|
+
state,
|
|
348
|
+
packet,
|
|
349
|
+
logger,
|
|
350
|
+
});
|
|
351
|
+
return finalCompletion.summary;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const coderRoundFailure = error instanceof CoderRoundError ? error : null;
|
|
355
|
+
await persistExecuteFinalizationFailure({
|
|
356
|
+
state,
|
|
357
|
+
statePath: args.statePath,
|
|
358
|
+
runtime: args.runtime,
|
|
359
|
+
logger,
|
|
360
|
+
finalCommit: args.finalCommit,
|
|
361
|
+
archivedReviewPath: args.archivedReviewPath,
|
|
362
|
+
completedScopes: args.completedScopes,
|
|
363
|
+
finalCompletionSummary: args.finalCompletionSummary,
|
|
364
|
+
errorMessage: coderRoundFailure
|
|
365
|
+
? coderRoundFailure.message
|
|
366
|
+
: error instanceof Error
|
|
367
|
+
? error.message
|
|
368
|
+
: String(error),
|
|
369
|
+
coderRoundExtras: coderRoundFailure
|
|
370
|
+
? {
|
|
371
|
+
sessionHandleOverride: coderRoundFailure.sessionHandle ?? state.coderSessionHandle,
|
|
372
|
+
notify: shouldNotifyFailure(coderRoundFailure),
|
|
373
|
+
}
|
|
374
|
+
: null,
|
|
375
|
+
});
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
export async function runExecuteFinalizationPhase(state, statePath, logger, runtime) {
|
|
380
|
+
if (!state.baseCommit) {
|
|
381
|
+
throw new Error('Cannot finalize without a baseCommit');
|
|
382
|
+
}
|
|
383
|
+
await logger?.event('phase.start', { phase: EXECUTE_FINALIZATION_PHASE });
|
|
384
|
+
const { headCommit, statusOutput } = await admitFinalizationWorktree(state);
|
|
220
385
|
if (state.currentScopeMeaningfulProgressVerdict?.action === 'advance_parent') {
|
|
221
386
|
return runParentAdvanceFinalization({
|
|
222
387
|
state,
|
|
@@ -227,13 +392,7 @@ export async function runExecuteFinalizationPhase(state, statePath, logger, runt
|
|
|
227
392
|
statusOutput,
|
|
228
393
|
});
|
|
229
394
|
}
|
|
230
|
-
const
|
|
231
|
-
const latestCreatedCommit = state.createdCommits.at(-1) ?? null;
|
|
232
|
-
const rawFinalMessage = latestCreatedCommit
|
|
233
|
-
? await getCommitMessage(state.cwd, latestCreatedCommit)
|
|
234
|
-
: commitSubjects.at(-1)?.replace(/^[a-f0-9]+\s+/, '') || 'Finalize scope work';
|
|
235
|
-
const strippedFinalMessage = stripScopePrefixFromCommitMessage(rawFinalMessage);
|
|
236
|
-
const finalMessage = normalizeFinalCommitMessage(strippedFinalMessage.trim() ? strippedFinalMessage : 'Finalize scope work');
|
|
395
|
+
const rawFinalMessage = await readRawFinalCommitMessage(state.cwd, state.createdCommits);
|
|
237
396
|
const planDocInspection = await inspectPlanDocDisposition(state.cwd, state.planDoc);
|
|
238
397
|
if (planDocInspection.eligibleForCommit && planDocInspection.repoRelativePath) {
|
|
239
398
|
await stagePath(state.cwd, planDocInspection.repoRelativePath);
|
|
@@ -242,12 +401,11 @@ export async function runExecuteFinalizationPhase(state, statePath, logger, runt
|
|
|
242
401
|
const stagedChangedFiles = await getStagedChangedFiles(state.cwd);
|
|
243
402
|
const finalizationChangedFiles = unionChangedFiles(changedFilesSinceBase, stagedChangedFiles);
|
|
244
403
|
await assertNoIgnoredChangedFiles(state.cwd, finalizationChangedFiles, 'Execute finalization');
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const finalSubject = finalCommitMessage.split(/\r?\n/, 1)[0] || 'Finalize scope work';
|
|
404
|
+
const { finalCommitMessage, finalSubject } = synthesizeFinalCommitMessage({
|
|
405
|
+
rawFinalMessage,
|
|
406
|
+
finalizationChangedFiles,
|
|
407
|
+
planDocRepoRelativePath: planDocInspection.repoRelativePath,
|
|
408
|
+
});
|
|
251
409
|
const finalCommit = finalizationChangedFiles.length > 0
|
|
252
410
|
? await createScopeFinalizationCommit(state.cwd, state.baseCommit, finalCommitMessage)
|
|
253
411
|
: headCommit;
|
|
@@ -278,78 +436,20 @@ export async function runExecuteFinalizationPhase(state, statePath, logger, runt
|
|
|
278
436
|
const initialFinalCompletion = requireFinalCompletionView(state, 'run execute finalization');
|
|
279
437
|
let finalCompletionSummary = initialFinalCompletion.summary;
|
|
280
438
|
if (!continueScopes && !finalCompletionSummary) {
|
|
281
|
-
await
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
439
|
+
finalCompletionSummary = await adjudicateFinalCompletionSummary({
|
|
440
|
+
state,
|
|
441
|
+
statePath,
|
|
442
|
+
logger,
|
|
443
|
+
runtime,
|
|
444
|
+
finalCommit,
|
|
445
|
+
archivedReviewPath,
|
|
446
|
+
archivedReviewState,
|
|
447
|
+
retrospectiveState,
|
|
448
|
+
completedScopes,
|
|
449
|
+
finalSubject,
|
|
450
|
+
finalizationChangedFiles,
|
|
451
|
+
finalCompletionSummary,
|
|
291
452
|
});
|
|
292
|
-
try {
|
|
293
|
-
const { summary: finalCompletion } = await runFinalCompletionSummaryAdjudication({
|
|
294
|
-
state,
|
|
295
|
-
packet,
|
|
296
|
-
logger,
|
|
297
|
-
});
|
|
298
|
-
finalCompletionSummary = finalCompletion.summary;
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
if (error instanceof CoderRoundError) {
|
|
302
|
-
const failedState = await saveState(statePath, {
|
|
303
|
-
...state,
|
|
304
|
-
finalCommit,
|
|
305
|
-
archivedReviewPath,
|
|
306
|
-
completedScopes,
|
|
307
|
-
coderSessionHandle: error.sessionHandle ?? state.coderSessionHandle,
|
|
308
|
-
coderSessionProtocol: (error.sessionHandle ?? state.coderSessionHandle)
|
|
309
|
-
? state.coderSessionProtocol ?? 'structured_json_v1'
|
|
310
|
-
: null,
|
|
311
|
-
status: 'failed',
|
|
312
|
-
});
|
|
313
|
-
await runtime.writeExecutionArtifacts(failedState);
|
|
314
|
-
await writeFailedFinalCompletionReviewArtifact({
|
|
315
|
-
state: failedState,
|
|
316
|
-
source: 'coder_summary',
|
|
317
|
-
sessionHandle: error.sessionHandle ?? state.coderSessionHandle,
|
|
318
|
-
});
|
|
319
|
-
await writeCheckpointRetrospective({
|
|
320
|
-
...failedState,
|
|
321
|
-
finalCompletionSummary,
|
|
322
|
-
}, 'failed');
|
|
323
|
-
await logger?.event('phase.error', {
|
|
324
|
-
phase: EXECUTE_FINALIZATION_PHASE,
|
|
325
|
-
sessionHandle: error.sessionHandle ?? state.coderSessionHandle,
|
|
326
|
-
message: error.message,
|
|
327
|
-
});
|
|
328
|
-
if (shouldNotifyFailure(error)) {
|
|
329
|
-
await notifyBlocked(failedState, error.message, logger);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
const failedState = await saveState(statePath, {
|
|
334
|
-
...state,
|
|
335
|
-
finalCommit,
|
|
336
|
-
archivedReviewPath,
|
|
337
|
-
completedScopes,
|
|
338
|
-
status: 'failed',
|
|
339
|
-
});
|
|
340
|
-
await runtime.writeExecutionArtifacts(failedState);
|
|
341
|
-
await writeCheckpointRetrospective({
|
|
342
|
-
...failedState,
|
|
343
|
-
finalCompletionSummary,
|
|
344
|
-
}, 'failed');
|
|
345
|
-
await logger?.event('phase.error', {
|
|
346
|
-
phase: EXECUTE_FINALIZATION_PHASE,
|
|
347
|
-
sessionHandle: state.coderSessionHandle,
|
|
348
|
-
message: error instanceof Error ? error.message : String(error),
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
353
453
|
}
|
|
354
454
|
const nextState = await saveState(statePath, continueScopes
|
|
355
455
|
? {
|
|
@@ -412,6 +512,21 @@ export async function runFinalCompletionReviewPhase(state, statePath, logger, ru
|
|
|
412
512
|
}));
|
|
413
513
|
}
|
|
414
514
|
catch (error) {
|
|
515
|
+
// A reviewer content-safety refusal ends the run blocked (exit 2) with a
|
|
516
|
+
// durable actionable reason, by RETURNING the terminal blocked state rather
|
|
517
|
+
// than throwing. Only a ReviewerRoundError carries `content_refused`; the
|
|
518
|
+
// verdict-invalid error never does, so it stays on the failed path below.
|
|
519
|
+
if (error instanceof ReviewerRoundError && error.kind === 'content_refused') {
|
|
520
|
+
const blockedState = await persistBlockedFinalCompletionReview({
|
|
521
|
+
state,
|
|
522
|
+
statePath,
|
|
523
|
+
error,
|
|
524
|
+
logger,
|
|
525
|
+
runtime,
|
|
526
|
+
});
|
|
527
|
+
await notifyBlocked(blockedState, REVIEWER_CONTENT_REFUSED_BLOCK_REASON, logger);
|
|
528
|
+
return blockedState;
|
|
529
|
+
}
|
|
415
530
|
if (error instanceof ReviewerRoundError || error instanceof FinalCompletionReviewerVerdictError) {
|
|
416
531
|
const failedState = await persistFailedFinalCompletionReview({
|
|
417
532
|
state,
|
|
@@ -462,32 +577,19 @@ export async function runFinalCompletionReviewPhase(state, statePath, logger, ru
|
|
|
462
577
|
: actionResolution.effectiveAction === 'continue_execution'
|
|
463
578
|
? await saveState(statePath, {
|
|
464
579
|
...baseState,
|
|
465
|
-
|
|
466
|
-
finalCommit: null,
|
|
467
|
-
archivedReviewPath: null,
|
|
468
|
-
coderSessionHandle: null,
|
|
469
|
-
coderSessionProtocol: null,
|
|
580
|
+
...createNextScopeEntryReset(state.finalCommit),
|
|
470
581
|
coderRetryCount: 0,
|
|
471
582
|
currentScopeNumber: shouldAdvanceTopLevelScopeNumber(state)
|
|
472
583
|
? state.currentScopeNumber + 1
|
|
473
584
|
: state.currentScopeNumber,
|
|
474
|
-
lastScopeMarker: null,
|
|
475
|
-
currentScopeProgressJustification: null,
|
|
476
|
-
currentScopeMeaningfulProgressVerdict: null,
|
|
477
|
-
finalCompletionSummary: null,
|
|
478
|
-
rounds: [],
|
|
479
|
-
recentBlocks: [],
|
|
480
585
|
// The reopened follow-on scope is a fresh scope boundary: per-scope
|
|
481
586
|
// budgets reset here exactly as they do at every scope-advance
|
|
482
587
|
// transition, so an earlier scope's adjudication or split-plan
|
|
483
588
|
// consumption never exhausts the reopened scope's budget.
|
|
484
|
-
|
|
589
|
+
// (`consultantAttemptCount` resets via the shared next-scope
|
|
590
|
+
// reset spread above.)
|
|
485
591
|
splitPlanCountForCurrentScope: 0,
|
|
486
|
-
findings: [],
|
|
487
|
-
createdCommits: [],
|
|
488
592
|
blockedFromPhase: null,
|
|
489
|
-
phase: 'coder_scope',
|
|
490
|
-
status: 'running',
|
|
491
593
|
})
|
|
492
594
|
: unattendedFinalCompletionBlock
|
|
493
595
|
? await persistUnattendedBlockUnresolvedFailure({
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { CoderRoundError } from '../agents.js';
|
|
2
|
+
// Shared, durable blocked reason for a reviewer content-safety refusal
|
|
3
|
+
// (`content_refused`). Defined once so the wording cannot drift across the
|
|
4
|
+
// three reviewer-phase catch sites that route it to a terminal blocked landing.
|
|
5
|
+
export const REVIEWER_CONTENT_REFUSED_BLOCK_REASON = 'The reviewer refused this content on content-safety grounds. The change ' +
|
|
6
|
+
'under review may read as security-adjacent. Switch the reviewer to a ' +
|
|
7
|
+
'different provider, rephrase the scope, or seek provider authorization, ' +
|
|
8
|
+
'then re-run.';
|
|
2
9
|
export function isCoderTimeoutError(error) {
|
|
3
10
|
return (error.kind === 'timeout' ||
|
|
4
11
|
error.kind === 'no_progress_timeout' ||
|
|
@@ -3,40 +3,40 @@ import { notify } from '../../notifier.js';
|
|
|
3
3
|
import { getCurrentScopeLabel, getExecutionPlanPath, getExecutionPlanScopeCount, getParentScopeLabel, renderScopeProgressSegments, } from '../scopes.js';
|
|
4
4
|
import { saveState } from '../state.js';
|
|
5
5
|
import { getDerivedPlanView } from '../state-views.js';
|
|
6
|
-
// Concise representation of the read-only
|
|
6
|
+
// Concise representation of the read-only consultant advice for the
|
|
7
7
|
// operator notification surface. Present only on an attended run whose active
|
|
8
|
-
// interactive-blocked-recovery record carries
|
|
8
|
+
// interactive-blocked-recovery record carries consultant advice (knob > 0,
|
|
9
9
|
// budget available, eligible source phase). Returns '' otherwise so notification
|
|
10
10
|
// behavior for recovery states without advice — and terminal blocked
|
|
11
11
|
// notifications, where the active record is already finalized to null — is
|
|
12
12
|
// unchanged.
|
|
13
|
-
function
|
|
14
|
-
const advice = state.interactiveBlockedRecovery?.
|
|
13
|
+
function consultantAdviceNotificationSuffix(state) {
|
|
14
|
+
const advice = state.interactiveBlockedRecovery?.consultantAdvice;
|
|
15
15
|
if (!advice) {
|
|
16
16
|
return '';
|
|
17
17
|
}
|
|
18
18
|
const directive = advice.resolutionDirective.trim() || 'n/a';
|
|
19
|
-
return ` |
|
|
19
|
+
return ` | consultant advice (read-only): triage ${advice.triageCategory}; suggested directive: ${directive}`;
|
|
20
20
|
}
|
|
21
21
|
async function notifyBlocked(state, reason, logger) {
|
|
22
22
|
const planName = basename(state.planDoc);
|
|
23
|
-
const adviceSuffix =
|
|
23
|
+
const adviceSuffix = consultantAdviceNotificationSuffix(state);
|
|
24
24
|
await logger?.event('notify.blocked', {
|
|
25
25
|
reason,
|
|
26
26
|
planName,
|
|
27
|
-
|
|
27
|
+
consultantAdvice: state.interactiveBlockedRecovery?.consultantAdvice ?? null,
|
|
28
28
|
});
|
|
29
29
|
await notify('blocked', `[neal] ${planName}: ${reason}${adviceSuffix}`, state.cwd);
|
|
30
30
|
}
|
|
31
31
|
async function notifyInteractiveBlockedRecovery(state, reason, logger) {
|
|
32
32
|
const planName = basename(state.planDoc);
|
|
33
33
|
const scopeLabel = getCurrentScopeLabel(state);
|
|
34
|
-
const adviceSuffix =
|
|
34
|
+
const adviceSuffix = consultantAdviceNotificationSuffix(state);
|
|
35
35
|
await logger?.event('notify.interactive_blocked_recovery', {
|
|
36
36
|
reason,
|
|
37
37
|
planName,
|
|
38
38
|
scopeNumber: scopeLabel,
|
|
39
|
-
|
|
39
|
+
consultantAdvice: state.interactiveBlockedRecovery?.consultantAdvice ?? null,
|
|
40
40
|
});
|
|
41
41
|
await notify('retry', `[neal] ${planName}: interactive blocked recovery for scope ${scopeLabel}: ${reason}${adviceSuffix}`, state.cwd);
|
|
42
42
|
}
|