@navels/neal 0.1.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/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { CoderRoundError, FinalCompletionReviewerVerdictError, ReviewerRoundError } from '../agents.js';
|
|
3
|
+
import { writeTextAtomic } from '../atomic-write.js';
|
|
4
|
+
import { runFinalCompletionReviewerAdjudication, runFinalCompletionSummaryAdjudication, } from '../adjudicator/final-completion.js';
|
|
5
|
+
import { assertAdjudicationTransitionSignal } from '../adjudicator/specs.js';
|
|
6
|
+
import { stripScopePrefixFromCommitMessage } from '../commit-message.js';
|
|
7
|
+
import { getFinalCompletionContinueExecutionMax } from '../config.js';
|
|
8
|
+
import { writeDetail } from '../diagnostic.js';
|
|
9
|
+
import { buildFinalCompletionPacket } from '../final-completion.js';
|
|
10
|
+
import { getFinalCompletionReviewArtifactPath, readFinalCompletionUnstructuredOutput, writeFinalCompletionReviewMarkdown, } from '../final-completion-review.js';
|
|
11
|
+
import { EXECUTE_FINALIZATION_PHASE } from '../execute-finalization.js';
|
|
12
|
+
import { getChangedFilesForRange, getCommitRange, getCommitMessage, getCommitSubjects, getHeadCommit, getStagedChangedFiles, getWorktreeStatus, assertNoIgnoredChangedFiles, cleanUntracked, resetHard, squashCommits as createScopeFinalizationCommit, stagePath, } from '../git.js';
|
|
13
|
+
import { notifyBlocked, notifyComplete, notifyScopeAccepted } from './notifications.js';
|
|
14
|
+
import { inspectPlanDocDisposition } from '../plan-doc.js';
|
|
15
|
+
import { appendDerivedSubScopeAndParentCompletion, appendParentCompletionFromAcceptedDerivedScopes, computeNextScopeStateAfterExecuteFinalization, computeNextScopeStateAfterParentAdvance, } from './transitions.js';
|
|
16
|
+
import { writePlanProgressArtifacts } from '../progress.js';
|
|
17
|
+
import { writeCheckpointRetrospective } from '../retrospective.js';
|
|
18
|
+
import { renderReviewMarkdown, writeReviewMarkdown } from '../review.js';
|
|
19
|
+
import { getCurrentScopeLabel, getParentScopeLabel, shouldAdvanceTopLevelScopeNumber } from '../scopes.js';
|
|
20
|
+
import { saveState } from '../state.js';
|
|
21
|
+
import { getFinalCompletionReviewerScratchDir, getScopeReviewerScratchDir } from '../storage-paths.js';
|
|
22
|
+
import { requireFinalCompletionView, resolveFinalCompletionReviewAction, } from '../state-views.js';
|
|
23
|
+
import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, formatDirtyWorktreeDiagnostic, } from '../worktree-status.js';
|
|
24
|
+
import { shouldNotifyFailure } from './failures.js';
|
|
25
|
+
import { persistUnattendedBlockUnresolvedFailure } from './phases/shared.js';
|
|
26
|
+
async function writeFailedFinalCompletionReviewArtifact(args) {
|
|
27
|
+
const unstructuredOutput = await readFinalCompletionUnstructuredOutput({
|
|
28
|
+
runDir: args.state.runDir,
|
|
29
|
+
source: args.source,
|
|
30
|
+
sessionHandle: args.sessionHandle,
|
|
31
|
+
});
|
|
32
|
+
await writeFinalCompletionReviewMarkdown(getFinalCompletionReviewArtifactPath(args.state.runDir), args.state, { unstructuredOutput });
|
|
33
|
+
}
|
|
34
|
+
async function persistFailedFinalCompletionReview(args) {
|
|
35
|
+
const failedState = await saveState(args.statePath, {
|
|
36
|
+
...args.state,
|
|
37
|
+
phase: 'final_completion_review',
|
|
38
|
+
reviewerSessionHandle: null,
|
|
39
|
+
status: 'failed',
|
|
40
|
+
});
|
|
41
|
+
await args.runtime.writeExecutionArtifacts(failedState);
|
|
42
|
+
await writeFailedFinalCompletionReviewArtifact({
|
|
43
|
+
state: failedState,
|
|
44
|
+
source: 'reviewer_verdict',
|
|
45
|
+
sessionHandle: args.error.sessionHandle,
|
|
46
|
+
});
|
|
47
|
+
await args.logger?.event('phase.error', {
|
|
48
|
+
phase: 'final_completion_review',
|
|
49
|
+
sessionHandle: args.error.sessionHandle,
|
|
50
|
+
subtype: args.error.subtype,
|
|
51
|
+
message: args.error.message,
|
|
52
|
+
});
|
|
53
|
+
return failedState;
|
|
54
|
+
}
|
|
55
|
+
function normalizeFinalCommitMessage(message) {
|
|
56
|
+
const normalizedNewlines = message.replace(/\r\n/g, '\n');
|
|
57
|
+
const convertedEscapes = normalizedNewlines.replace(/\\n(?=- )/g, '\n');
|
|
58
|
+
return convertedEscapes.replace(/\n+$/, '') + '\n';
|
|
59
|
+
}
|
|
60
|
+
function printFinalCompletionReviewResult(args, logger) {
|
|
61
|
+
const message = [
|
|
62
|
+
`[reviewer:final-completion] action: ${args.action}`,
|
|
63
|
+
`[reviewer:final-completion] summary: ${args.summary}`,
|
|
64
|
+
`[reviewer:final-completion] rationale: ${args.rationale}`,
|
|
65
|
+
].join('\n');
|
|
66
|
+
writeDetail(`${message}\n`, logger, {
|
|
67
|
+
phase: 'final_completion_review',
|
|
68
|
+
role: 'reviewer',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function getTerminalCompletedScope(state) {
|
|
72
|
+
const currentScopeLabel = getCurrentScopeLabel(state);
|
|
73
|
+
return state.completedScopes.find((scope) => scope.number === currentScopeLabel) ?? null;
|
|
74
|
+
}
|
|
75
|
+
function formatCommitList(commits) {
|
|
76
|
+
return commits.length > 0 ? commits.join(', ') : '(none)';
|
|
77
|
+
}
|
|
78
|
+
function haveSameCommitSequence(left, right) {
|
|
79
|
+
return left.length === right.length && left.every((commit, index) => commit === right[index]);
|
|
80
|
+
}
|
|
81
|
+
function unionChangedFiles(...groups) {
|
|
82
|
+
return [...new Set(groups.flat())];
|
|
83
|
+
}
|
|
84
|
+
function getFinalizationExpectedScratchDirs(state) {
|
|
85
|
+
const scratchDirs = [];
|
|
86
|
+
const lastReviewRound = state.rounds.at(-1)?.round ?? null;
|
|
87
|
+
if (lastReviewRound !== null) {
|
|
88
|
+
scratchDirs.push(getScopeReviewerScratchDir(state.runDir, getParentScopeLabel(state), lastReviewRound));
|
|
89
|
+
}
|
|
90
|
+
if (state.lastScopeMarker === 'AUTONOMY_DONE' || state.finalCompletionSummary) {
|
|
91
|
+
scratchDirs.push(getFinalCompletionReviewerScratchDir(state.runDir));
|
|
92
|
+
}
|
|
93
|
+
return scratchDirs;
|
|
94
|
+
}
|
|
95
|
+
function getFinalCompletionReviewBlockReason(args) {
|
|
96
|
+
if (args.effectiveAction === 'continue_execution') {
|
|
97
|
+
return `final_completion_review: reviewer reopened execution. ${args.rationale}`;
|
|
98
|
+
}
|
|
99
|
+
if (args.reviewerAction === 'continue_execution' && args.capReached) {
|
|
100
|
+
return ('final_completion_review: reviewer requested more execution, ' +
|
|
101
|
+
`but the continue_execution cap (${args.continueExecutionLimit}) is already exhausted ` +
|
|
102
|
+
`after ${args.continueExecutionCount} reopen cycle(s). ${args.rationale} ` +
|
|
103
|
+
'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.');
|
|
104
|
+
}
|
|
105
|
+
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
|
+
}
|
|
107
|
+
async function discardFinalCompletionReviewerWorktreeChanges(args) {
|
|
108
|
+
const statusOutput = filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(args.state.cwd, { untrackedFiles: 'all' }));
|
|
109
|
+
if (!statusOutput.trim()) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const headCommit = await getHeadCommit(args.state.cwd);
|
|
113
|
+
await args.logger?.event('final_completion_review.discarded_dirty_worktree', {
|
|
114
|
+
headCommit,
|
|
115
|
+
statusOutput,
|
|
116
|
+
message: 'Final completion review is read-only; discarding reviewer-created worktree changes before state transition.',
|
|
117
|
+
});
|
|
118
|
+
await resetHard(args.state.cwd, headCommit);
|
|
119
|
+
await cleanUntracked(args.state.cwd, ['.neal/', '.forge/', 'CURRENT_PLAN.md']);
|
|
120
|
+
const remainingStatusOutput = filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(args.state.cwd, { untrackedFiles: 'all' }));
|
|
121
|
+
if (remainingStatusOutput.trim()) {
|
|
122
|
+
throw new Error(`Final completion review left a dirty worktree Neal could not restore:\n${remainingStatusOutput}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function runParentAdvanceFinalization(args) {
|
|
126
|
+
const { state, statePath, logger, runtime, headCommit, statusOutput } = args;
|
|
127
|
+
if (!state.baseCommit) {
|
|
128
|
+
throw new Error('Cannot advance parent without a baseCommit');
|
|
129
|
+
}
|
|
130
|
+
if (statusOutput) {
|
|
131
|
+
throw new Error(`Cannot advance parent with a dirty worktree because resetting an empty derived range would be destructive:\n${statusOutput}`);
|
|
132
|
+
}
|
|
133
|
+
const parentScopeLabel = getParentScopeLabel(state);
|
|
134
|
+
const currentScopeLabel = getCurrentScopeLabel(state);
|
|
135
|
+
const currentRangeCommits = await getCommitRange(state.cwd, state.baseCommit, headCommit);
|
|
136
|
+
const changedFilesSinceBase = await getChangedFilesForRange(state.cwd, state.baseCommit, headCommit);
|
|
137
|
+
if (changedFilesSinceBase.length > 0) {
|
|
138
|
+
throw new Error([
|
|
139
|
+
`Cannot advance parent scope ${parentScopeLabel} through empty derived scope ${currentScopeLabel}: current range has changed files.`,
|
|
140
|
+
...changedFilesSinceBase.map((file) => `- ${file}`),
|
|
141
|
+
].join('\n'));
|
|
142
|
+
}
|
|
143
|
+
await assertNoIgnoredChangedFiles(state.cwd, changedFilesSinceBase, 'Execute parent advancement');
|
|
144
|
+
if (headCommit !== state.baseCommit) {
|
|
145
|
+
if (!haveSameCommitSequence(currentRangeCommits, state.createdCommits)) {
|
|
146
|
+
throw new Error([
|
|
147
|
+
`Cannot advance parent scope ${parentScopeLabel} by dropping empty derived scope ${currentScopeLabel}: current range commits do not exactly match Neal-created commits.`,
|
|
148
|
+
`Range commits: ${formatCommitList(currentRangeCommits)}`,
|
|
149
|
+
`Neal-created commits: ${formatCommitList(state.createdCommits)}`,
|
|
150
|
+
].join('\n'));
|
|
151
|
+
}
|
|
152
|
+
await resetHard(state.cwd, state.baseCommit);
|
|
153
|
+
}
|
|
154
|
+
const finalCommit = state.baseCommit;
|
|
155
|
+
const archivedReviewPath = join(state.runDir, `REVIEW-${finalCommit}.md`);
|
|
156
|
+
const completedScopes = appendParentCompletionFromAcceptedDerivedScopes({
|
|
157
|
+
state,
|
|
158
|
+
finalCommit,
|
|
159
|
+
archivedReviewPath,
|
|
160
|
+
});
|
|
161
|
+
const archivedReviewState = {
|
|
162
|
+
...state,
|
|
163
|
+
finalCommit,
|
|
164
|
+
archivedReviewPath,
|
|
165
|
+
completedScopes,
|
|
166
|
+
};
|
|
167
|
+
const retrospectiveState = archivedReviewState;
|
|
168
|
+
const nextState = await saveState(statePath, computeNextScopeStateAfterParentAdvance({
|
|
169
|
+
state,
|
|
170
|
+
finalCommit,
|
|
171
|
+
completedScopes,
|
|
172
|
+
}));
|
|
173
|
+
const parentScope = completedScopes.find((scope) => scope.number === parentScopeLabel && scope.derivedFromParentScope === null);
|
|
174
|
+
const aggregateChangedFileCount = parentScope?.changedFiles.length ?? 0;
|
|
175
|
+
const parentAdvanceMessage = `advance_parent completed parent scope ${parentScopeLabel} via derived plan; dropped empty derived scope ${currentScopeLabel} without accepting it.`;
|
|
176
|
+
await logger?.event('execute_finalization.advance_parent', {
|
|
177
|
+
message: parentAdvanceMessage,
|
|
178
|
+
parentScopeLabel,
|
|
179
|
+
currentScopeLabel,
|
|
180
|
+
priorDerivedCount: state.completedScopes.filter((scope) => scope.result === 'accepted' && scope.derivedFromParentScope === parentScopeLabel).length,
|
|
181
|
+
aggregateChangedFileCount,
|
|
182
|
+
droppedCommitCount: currentRangeCommits.length,
|
|
183
|
+
finalParentCommit: finalCommit,
|
|
184
|
+
});
|
|
185
|
+
await writeTextAtomic(archivedReviewPath, renderReviewMarkdown({ ...archivedReviewState, finalCompletionSummary: null, finalCompletionReviewVerdict: null }));
|
|
186
|
+
await writeCheckpointRetrospective(retrospectiveState, 'scope_accepted');
|
|
187
|
+
await runtime.writeExecutionArtifacts(nextState);
|
|
188
|
+
await logger?.event('phase.complete', {
|
|
189
|
+
phase: EXECUTE_FINALIZATION_PHASE,
|
|
190
|
+
finalCommit,
|
|
191
|
+
archivedReviewPath,
|
|
192
|
+
continueScopes: true,
|
|
193
|
+
meaningfulProgressAction: 'advance_parent',
|
|
194
|
+
message: parentAdvanceMessage,
|
|
195
|
+
});
|
|
196
|
+
await notifyScopeAccepted({
|
|
197
|
+
...state,
|
|
198
|
+
currentScopeNumber: state.derivedFromScopeNumber ?? state.currentScopeNumber,
|
|
199
|
+
derivedPlanPath: null,
|
|
200
|
+
derivedFromScopeNumber: null,
|
|
201
|
+
derivedPlanStatus: null,
|
|
202
|
+
derivedScopeIndex: null,
|
|
203
|
+
}, parentScope?.commitSubject ?? `Parent scope ${parentScopeLabel} complete via derived plan`, logger);
|
|
204
|
+
return nextState;
|
|
205
|
+
}
|
|
206
|
+
export async function runExecuteFinalizationPhase(state, statePath, logger, runtime) {
|
|
207
|
+
if (!state.baseCommit) {
|
|
208
|
+
throw new Error('Cannot finalize without a baseCommit');
|
|
209
|
+
}
|
|
210
|
+
await logger?.event('phase.start', { phase: EXECUTE_FINALIZATION_PHASE });
|
|
211
|
+
const headCommit = await getHeadCommit(state.cwd);
|
|
212
|
+
const statusOutput = filterAllowedDirtyPathStatus(state.cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(state.cwd)), state.allowedDirtyPaths);
|
|
213
|
+
if (statusOutput) {
|
|
214
|
+
const diagnostic = formatDirtyWorktreeDiagnostic({
|
|
215
|
+
statusOutput,
|
|
216
|
+
expectedScratchDirs: getFinalizationExpectedScratchDirs(state),
|
|
217
|
+
});
|
|
218
|
+
throw new Error([`Cannot finalize with a dirty worktree:\n${statusOutput}`, diagnostic].filter(Boolean).join('\n\n'));
|
|
219
|
+
}
|
|
220
|
+
if (state.currentScopeMeaningfulProgressVerdict?.action === 'advance_parent') {
|
|
221
|
+
return runParentAdvanceFinalization({
|
|
222
|
+
state,
|
|
223
|
+
statePath,
|
|
224
|
+
logger,
|
|
225
|
+
runtime,
|
|
226
|
+
headCommit,
|
|
227
|
+
statusOutput,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const commitSubjects = await getCommitSubjects(state.cwd, state.createdCommits);
|
|
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');
|
|
237
|
+
const planDocInspection = await inspectPlanDocDisposition(state.cwd, state.planDoc);
|
|
238
|
+
if (planDocInspection.eligibleForCommit && planDocInspection.repoRelativePath) {
|
|
239
|
+
await stagePath(state.cwd, planDocInspection.repoRelativePath);
|
|
240
|
+
}
|
|
241
|
+
const changedFilesSinceBase = await getChangedFilesForRange(state.cwd, state.baseCommit, headCommit);
|
|
242
|
+
const stagedChangedFiles = await getStagedChangedFiles(state.cwd);
|
|
243
|
+
const finalizationChangedFiles = unionChangedFiles(changedFilesSinceBase, stagedChangedFiles);
|
|
244
|
+
await assertNoIgnoredChangedFiles(state.cwd, finalizationChangedFiles, 'Execute finalization');
|
|
245
|
+
const onlyChangedFile = finalizationChangedFiles.length === 1 ? finalizationChangedFiles[0] : null;
|
|
246
|
+
const onlyChangedPlanDoc = onlyChangedFile !== null &&
|
|
247
|
+
planDocInspection.repoRelativePath !== null &&
|
|
248
|
+
onlyChangedFile === planDocInspection.repoRelativePath;
|
|
249
|
+
const finalCommitMessage = onlyChangedPlanDoc ? 'Record Neal plan document\n' : finalMessage;
|
|
250
|
+
const finalSubject = finalCommitMessage.split(/\r?\n/, 1)[0] || 'Finalize scope work';
|
|
251
|
+
const finalCommit = finalizationChangedFiles.length > 0
|
|
252
|
+
? await createScopeFinalizationCommit(state.cwd, state.baseCommit, finalCommitMessage)
|
|
253
|
+
: headCommit;
|
|
254
|
+
const archivedReviewPath = join(state.runDir, `REVIEW-${finalCommit}.md`);
|
|
255
|
+
const archivedReviewState = {
|
|
256
|
+
...state,
|
|
257
|
+
finalCommit,
|
|
258
|
+
archivedReviewPath,
|
|
259
|
+
};
|
|
260
|
+
const completedScopes = appendDerivedSubScopeAndParentCompletion({
|
|
261
|
+
state,
|
|
262
|
+
finalCommit,
|
|
263
|
+
finalSubject,
|
|
264
|
+
changedFiles: finalizationChangedFiles,
|
|
265
|
+
archivedReviewPath,
|
|
266
|
+
});
|
|
267
|
+
const retrospectiveState = {
|
|
268
|
+
...archivedReviewState,
|
|
269
|
+
completedScopes,
|
|
270
|
+
};
|
|
271
|
+
const provisionalNextState = computeNextScopeStateAfterExecuteFinalization({
|
|
272
|
+
state,
|
|
273
|
+
finalCommit,
|
|
274
|
+
completedScopes,
|
|
275
|
+
archivedReviewPath,
|
|
276
|
+
});
|
|
277
|
+
const continueScopes = provisionalNextState.phase === 'coder_scope' && provisionalNextState.status === 'running';
|
|
278
|
+
const initialFinalCompletion = requireFinalCompletionView(state, 'run execute finalization');
|
|
279
|
+
let finalCompletionSummary = initialFinalCompletion.summary;
|
|
280
|
+
if (!continueScopes && !finalCompletionSummary) {
|
|
281
|
+
await writeTextAtomic(archivedReviewPath, renderReviewMarkdown({ ...archivedReviewState, finalCompletionSummary: null, finalCompletionReviewVerdict: null }));
|
|
282
|
+
const packet = await buildFinalCompletionPacket({
|
|
283
|
+
state: retrospectiveState,
|
|
284
|
+
terminalScope: {
|
|
285
|
+
finalCommit,
|
|
286
|
+
commitSubject: finalSubject,
|
|
287
|
+
changedFiles: finalizationChangedFiles,
|
|
288
|
+
archivedReviewPath,
|
|
289
|
+
marker: state.lastScopeMarker,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
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
|
+
}
|
|
354
|
+
const nextState = await saveState(statePath, continueScopes
|
|
355
|
+
? {
|
|
356
|
+
...provisionalNextState,
|
|
357
|
+
finalCompletionSummary,
|
|
358
|
+
}
|
|
359
|
+
: {
|
|
360
|
+
...provisionalNextState,
|
|
361
|
+
blockedFromPhase: null,
|
|
362
|
+
finalCompletionSummary,
|
|
363
|
+
finalCompletionReviewVerdict: null,
|
|
364
|
+
finalCompletionResolvedAction: null,
|
|
365
|
+
finalCompletionContinueExecutionCapReached: false,
|
|
366
|
+
});
|
|
367
|
+
await writeTextAtomic(archivedReviewPath, renderReviewMarkdown({ ...archivedReviewState, finalCompletionSummary, finalCompletionReviewVerdict: null }));
|
|
368
|
+
await writeCheckpointRetrospective(retrospectiveState, 'scope_accepted');
|
|
369
|
+
if (continueScopes) {
|
|
370
|
+
await runtime.writeExecutionArtifacts(nextState);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
await writeReviewMarkdown(nextState.reviewMarkdownPath, { ...nextState, finalCommit, archivedReviewPath });
|
|
374
|
+
await writePlanProgressArtifacts(nextState);
|
|
375
|
+
await writeFinalCompletionReviewMarkdown(getFinalCompletionReviewArtifactPath(nextState.runDir), { ...nextState, finalCommit, archivedReviewPath });
|
|
376
|
+
}
|
|
377
|
+
await logger?.event('phase.complete', {
|
|
378
|
+
phase: EXECUTE_FINALIZATION_PHASE,
|
|
379
|
+
finalCommit,
|
|
380
|
+
archivedReviewPath,
|
|
381
|
+
continueScopes,
|
|
382
|
+
});
|
|
383
|
+
await notifyScopeAccepted(state, finalSubject, logger);
|
|
384
|
+
return nextState;
|
|
385
|
+
}
|
|
386
|
+
export async function runFinalCompletionReviewPhase(state, statePath, logger, runtime) {
|
|
387
|
+
const finalCompletion = requireFinalCompletionView(state, 'run final completion review');
|
|
388
|
+
if (!finalCompletion.summary) {
|
|
389
|
+
throw new Error('Cannot run final completion review without a final completion summary');
|
|
390
|
+
}
|
|
391
|
+
await logger?.event('phase.start', { phase: 'final_completion_review' });
|
|
392
|
+
const terminalScope = getTerminalCompletedScope(state);
|
|
393
|
+
const packet = await buildFinalCompletionPacket({
|
|
394
|
+
state,
|
|
395
|
+
terminalScope: terminalScope
|
|
396
|
+
? {
|
|
397
|
+
finalCommit: terminalScope.finalCommit,
|
|
398
|
+
commitSubject: terminalScope.commitSubject,
|
|
399
|
+
changedFiles: terminalScope.changedFiles,
|
|
400
|
+
archivedReviewPath: terminalScope.archivedReviewPath,
|
|
401
|
+
marker: terminalScope.marker,
|
|
402
|
+
}
|
|
403
|
+
: null,
|
|
404
|
+
});
|
|
405
|
+
let context;
|
|
406
|
+
let reviewerResult;
|
|
407
|
+
try {
|
|
408
|
+
({ context, reviewerResult } = await runFinalCompletionReviewerAdjudication({
|
|
409
|
+
state,
|
|
410
|
+
packet,
|
|
411
|
+
logger,
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (error instanceof ReviewerRoundError || error instanceof FinalCompletionReviewerVerdictError) {
|
|
416
|
+
const failedState = await persistFailedFinalCompletionReview({
|
|
417
|
+
state,
|
|
418
|
+
statePath,
|
|
419
|
+
error,
|
|
420
|
+
logger,
|
|
421
|
+
runtime,
|
|
422
|
+
});
|
|
423
|
+
if (error instanceof ReviewerRoundError && shouldNotifyFailure(error)) {
|
|
424
|
+
await notifyBlocked(failedState, error.message, logger);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
printFinalCompletionReviewResult(reviewerResult.verdict, logger);
|
|
430
|
+
await discardFinalCompletionReviewerWorktreeChanges({ state, logger });
|
|
431
|
+
const continueExecutionLimit = Math.max(0, getFinalCompletionContinueExecutionMax(state.cwd));
|
|
432
|
+
const actionResolution = resolveFinalCompletionReviewAction({
|
|
433
|
+
finalCompletion,
|
|
434
|
+
reviewerAction: reviewerResult.verdict.action,
|
|
435
|
+
continueExecutionLimit,
|
|
436
|
+
});
|
|
437
|
+
const baseState = {
|
|
438
|
+
...state,
|
|
439
|
+
reviewerSessionHandle: reviewerResult.sessionHandle,
|
|
440
|
+
finalCompletionReviewVerdict: reviewerResult.verdict,
|
|
441
|
+
finalCompletionResolvedAction: actionResolution.effectiveAction,
|
|
442
|
+
finalCompletionContinueExecutionCount: actionResolution.continueExecutionCount,
|
|
443
|
+
finalCompletionContinueExecutionCapReached: actionResolution.continueExecutionCapReached,
|
|
444
|
+
};
|
|
445
|
+
assertAdjudicationTransitionSignal(context.spec, actionResolution.effectiveAction, 'orchestrator:final_completion_review');
|
|
446
|
+
// Site B: the final-completion review gate. Under `unattended` a
|
|
447
|
+
// `block_for_operator` resolution (model-chosen or the forced
|
|
448
|
+
// continue-execution cap folded into `effectiveAction`) has no operator to
|
|
449
|
+
// answer, so run the shared terminal-fail action instead of saving
|
|
450
|
+
// `status:'blocked'`. The continue-execution cap already bounded the
|
|
451
|
+
// autonomous push, so there is no auto-resume. The aggregate diff is preserved
|
|
452
|
+
// unsubmitted by the shared action. Gate structurally on the persisted flag,
|
|
453
|
+
// never on guidance text.
|
|
454
|
+
const unattendedFinalCompletionBlock = actionResolution.effectiveAction === 'block_for_operator' && state.unattended;
|
|
455
|
+
const nextState = actionResolution.effectiveAction === 'accept_complete'
|
|
456
|
+
? await saveState(statePath, {
|
|
457
|
+
...baseState,
|
|
458
|
+
phase: 'done',
|
|
459
|
+
status: 'done',
|
|
460
|
+
blockedFromPhase: null,
|
|
461
|
+
})
|
|
462
|
+
: actionResolution.effectiveAction === 'continue_execution'
|
|
463
|
+
? await saveState(statePath, {
|
|
464
|
+
...baseState,
|
|
465
|
+
baseCommit: state.finalCommit,
|
|
466
|
+
finalCommit: null,
|
|
467
|
+
archivedReviewPath: null,
|
|
468
|
+
coderSessionHandle: null,
|
|
469
|
+
coderSessionProtocol: null,
|
|
470
|
+
coderRetryCount: 0,
|
|
471
|
+
currentScopeNumber: shouldAdvanceTopLevelScopeNumber(state)
|
|
472
|
+
? state.currentScopeNumber + 1
|
|
473
|
+
: state.currentScopeNumber,
|
|
474
|
+
lastScopeMarker: null,
|
|
475
|
+
currentScopeProgressJustification: null,
|
|
476
|
+
currentScopeMeaningfulProgressVerdict: null,
|
|
477
|
+
finalCompletionSummary: null,
|
|
478
|
+
rounds: [],
|
|
479
|
+
recentBlocks: [],
|
|
480
|
+
// The reopened follow-on scope is a fresh scope boundary: per-scope
|
|
481
|
+
// budgets reset here exactly as they do at every scope-advance
|
|
482
|
+
// transition, so an earlier scope's adjudication or split-plan
|
|
483
|
+
// consumption never exhausts the reopened scope's budget.
|
|
484
|
+
reviewStuckArbiterCount: 0,
|
|
485
|
+
splitPlanCountForCurrentScope: 0,
|
|
486
|
+
findings: [],
|
|
487
|
+
createdCommits: [],
|
|
488
|
+
blockedFromPhase: null,
|
|
489
|
+
phase: 'coder_scope',
|
|
490
|
+
status: 'running',
|
|
491
|
+
})
|
|
492
|
+
: unattendedFinalCompletionBlock
|
|
493
|
+
? await persistUnattendedBlockUnresolvedFailure({
|
|
494
|
+
...baseState,
|
|
495
|
+
phase: 'blocked',
|
|
496
|
+
blockedFromPhase: 'final_completion_review',
|
|
497
|
+
}, statePath, 'final_completion_review', logger)
|
|
498
|
+
: await saveState(statePath, {
|
|
499
|
+
...baseState,
|
|
500
|
+
phase: 'blocked',
|
|
501
|
+
status: 'blocked',
|
|
502
|
+
blockedFromPhase: 'final_completion_review',
|
|
503
|
+
});
|
|
504
|
+
await runtime.writeExecutionArtifacts(nextState);
|
|
505
|
+
await logger?.event('phase.complete', {
|
|
506
|
+
phase: 'final_completion_review',
|
|
507
|
+
action: reviewerResult.verdict.action,
|
|
508
|
+
resultingAction: actionResolution.effectiveAction,
|
|
509
|
+
continueExecutionCount: actionResolution.continueExecutionCount,
|
|
510
|
+
continueExecutionLimit: actionResolution.continueExecutionLimit,
|
|
511
|
+
continueExecutionCapReached: actionResolution.continueExecutionCapReached,
|
|
512
|
+
reviewerSessionHandle: reviewerResult.sessionHandle,
|
|
513
|
+
nextPhase: nextState.phase,
|
|
514
|
+
});
|
|
515
|
+
if (actionResolution.effectiveAction === 'accept_complete') {
|
|
516
|
+
const finalSubject = terminalScope?.commitSubject ?? 'Finalize scope work';
|
|
517
|
+
await notifyComplete(nextState, finalSubject, logger);
|
|
518
|
+
}
|
|
519
|
+
else if (actionResolution.effectiveAction === 'block_for_operator' && !state.unattended) {
|
|
520
|
+
const reason = getFinalCompletionReviewBlockReason({
|
|
521
|
+
reviewerAction: reviewerResult.verdict.action === 'accept_complete' ? 'block_for_operator' : reviewerResult.verdict.action,
|
|
522
|
+
effectiveAction: actionResolution.effectiveAction,
|
|
523
|
+
rationale: reviewerResult.verdict.rationale,
|
|
524
|
+
continueExecutionCount: actionResolution.continueExecutionCount,
|
|
525
|
+
continueExecutionLimit: actionResolution.continueExecutionLimit,
|
|
526
|
+
capReached: actionResolution.continueExecutionCapReached,
|
|
527
|
+
});
|
|
528
|
+
await notifyBlocked(nextState, reason, logger);
|
|
529
|
+
}
|
|
530
|
+
return nextState;
|
|
531
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CoderRoundError } from '../agents.js';
|
|
2
|
+
export function isCoderTimeoutError(error) {
|
|
3
|
+
return (error.kind === 'timeout' ||
|
|
4
|
+
error.kind === 'no_progress_timeout' ||
|
|
5
|
+
/\btimed out after\b/i.test(error.message));
|
|
6
|
+
}
|
|
7
|
+
export function isCoderResumeHistoryError(error) {
|
|
8
|
+
const text = error.message.toLowerCase();
|
|
9
|
+
return (text.includes('property_name_above_max_length') ||
|
|
10
|
+
text.includes('orphan function call output') ||
|
|
11
|
+
text.includes('failed to record rollout items'));
|
|
12
|
+
}
|
|
13
|
+
export function isCoderFreshSessionRetryableError(error) {
|
|
14
|
+
return isCoderTimeoutError(error) || isCoderResumeHistoryError(error);
|
|
15
|
+
}
|
|
16
|
+
export function isTransientApiFailureMessage(message, subtype) {
|
|
17
|
+
const text = `${subtype ?? ''} ${message}`.toLowerCase();
|
|
18
|
+
return (text.includes('api_error') ||
|
|
19
|
+
text.includes('api error') ||
|
|
20
|
+
text.includes('internal server error') ||
|
|
21
|
+
text.includes('overloaded') ||
|
|
22
|
+
text.includes('rate limit') ||
|
|
23
|
+
text.includes('temporar') ||
|
|
24
|
+
text.includes('try again'));
|
|
25
|
+
}
|
|
26
|
+
export function shouldNotifyFailure(error) {
|
|
27
|
+
if (error instanceof CoderRoundError) {
|
|
28
|
+
return isCoderTimeoutError(error) || isTransientApiFailureMessage(error.message);
|
|
29
|
+
}
|
|
30
|
+
return isTransientApiFailureMessage(error.message, error.subtype ?? undefined);
|
|
31
|
+
}
|