@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,175 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { notify } from '../../notifier.js';
|
|
3
|
+
import { getCurrentScopeLabel, getExecutionPlanPath, getExecutionPlanScopeCount, getParentScopeLabel, renderScopeProgressSegments, } from '../scopes.js';
|
|
4
|
+
import { saveState } from '../state.js';
|
|
5
|
+
import { getDerivedPlanView } from '../state-views.js';
|
|
6
|
+
// Concise representation of the read-only blocked-adjudicator advice for the
|
|
7
|
+
// operator notification surface. Present only on an attended run whose active
|
|
8
|
+
// interactive-blocked-recovery record carries adjudicator advice (knob > 0,
|
|
9
|
+
// budget available, eligible source phase). Returns '' otherwise so notification
|
|
10
|
+
// behavior for recovery states without advice — and terminal blocked
|
|
11
|
+
// notifications, where the active record is already finalized to null — is
|
|
12
|
+
// unchanged.
|
|
13
|
+
function adjudicatorAdviceNotificationSuffix(state) {
|
|
14
|
+
const advice = state.interactiveBlockedRecovery?.adjudicatorAdvice;
|
|
15
|
+
if (!advice) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
const directive = advice.resolutionDirective.trim() || 'n/a';
|
|
19
|
+
return ` | adjudicator advice (read-only): triage ${advice.triageCategory}; suggested directive: ${directive}`;
|
|
20
|
+
}
|
|
21
|
+
async function notifyBlocked(state, reason, logger) {
|
|
22
|
+
const planName = basename(state.planDoc);
|
|
23
|
+
const adviceSuffix = adjudicatorAdviceNotificationSuffix(state);
|
|
24
|
+
await logger?.event('notify.blocked', {
|
|
25
|
+
reason,
|
|
26
|
+
planName,
|
|
27
|
+
adjudicatorAdvice: state.interactiveBlockedRecovery?.adjudicatorAdvice ?? null,
|
|
28
|
+
});
|
|
29
|
+
await notify('blocked', `[neal] ${planName}: ${reason}${adviceSuffix}`, state.cwd);
|
|
30
|
+
}
|
|
31
|
+
async function notifyInteractiveBlockedRecovery(state, reason, logger) {
|
|
32
|
+
const planName = basename(state.planDoc);
|
|
33
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
34
|
+
const adviceSuffix = adjudicatorAdviceNotificationSuffix(state);
|
|
35
|
+
await logger?.event('notify.interactive_blocked_recovery', {
|
|
36
|
+
reason,
|
|
37
|
+
planName,
|
|
38
|
+
scopeNumber: scopeLabel,
|
|
39
|
+
adjudicatorAdvice: state.interactiveBlockedRecovery?.adjudicatorAdvice ?? null,
|
|
40
|
+
});
|
|
41
|
+
await notify('retry', `[neal] ${planName}: interactive blocked recovery for scope ${scopeLabel}: ${reason}${adviceSuffix}`, state.cwd);
|
|
42
|
+
}
|
|
43
|
+
async function notifyComplete(state, message, logger) {
|
|
44
|
+
const planName = basename(state.planDoc);
|
|
45
|
+
const completionLabel = state.topLevelMode === 'plan' ? 'plan complete' : 'implementation complete';
|
|
46
|
+
await logger?.event('notify.complete', { message, planName, completionLabel });
|
|
47
|
+
await notify('complete', `[neal] ${planName}: ${completionLabel}: ${message}`, state.cwd);
|
|
48
|
+
}
|
|
49
|
+
async function notifyScopeAccepted(state, message, logger) {
|
|
50
|
+
const planName = basename(state.planDoc);
|
|
51
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
52
|
+
const totalScopeCount = await getExecutionPlanScopeCount(getExecutionPlanPath(state));
|
|
53
|
+
const { scopeSegment, derivedSegment } = renderScopeProgressSegments(state, totalScopeCount);
|
|
54
|
+
const progressSegment = derivedSegment && totalScopeCount.kind !== 'unavailable'
|
|
55
|
+
? `${scopeSegment} complete (${derivedSegment})`
|
|
56
|
+
: `${scopeSegment} complete`;
|
|
57
|
+
await logger?.event('notify.scope_complete', {
|
|
58
|
+
message,
|
|
59
|
+
planName,
|
|
60
|
+
scopeNumber: scopeLabel,
|
|
61
|
+
totalScopeCount,
|
|
62
|
+
});
|
|
63
|
+
await notify('complete', `[neal] ${planName}: ${progressSegment}: ${message}`, state.cwd);
|
|
64
|
+
}
|
|
65
|
+
async function notifyRetry(state, message, logger) {
|
|
66
|
+
const planName = basename(state.planDoc);
|
|
67
|
+
await logger?.event('notify.retry', {
|
|
68
|
+
message,
|
|
69
|
+
planName,
|
|
70
|
+
scopeNumber: getCurrentScopeLabel(state),
|
|
71
|
+
phase: state.phase,
|
|
72
|
+
});
|
|
73
|
+
await notify('retry', `[neal] ${planName}: ${message}`, state.cwd);
|
|
74
|
+
}
|
|
75
|
+
async function notifyManualGate(state, logger) {
|
|
76
|
+
const planName = basename(state.planDoc);
|
|
77
|
+
const gate = state.manualGate;
|
|
78
|
+
if (!gate) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await logger?.event('notify.manual_gate', {
|
|
82
|
+
planName,
|
|
83
|
+
scopeNumber: getCurrentScopeLabel(state),
|
|
84
|
+
gateId: gate.id,
|
|
85
|
+
title: gate.title,
|
|
86
|
+
instructionsPath: gate.instructionsPath,
|
|
87
|
+
});
|
|
88
|
+
await notify('retry', `[neal] ${planName}: waiting for manual gate ${gate.id}: ${gate.title}; resume with neal resume --run ${basename(state.runDir)}`, state.cwd);
|
|
89
|
+
}
|
|
90
|
+
async function notifySplitPlanStarted(state, logger) {
|
|
91
|
+
const planName = basename(state.planDoc);
|
|
92
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
93
|
+
const derivedPlan = getDerivedPlanView(state);
|
|
94
|
+
await logger?.event('notify.split_plan_started', {
|
|
95
|
+
planName,
|
|
96
|
+
scopeNumber: scopeLabel,
|
|
97
|
+
derivedPlanPath: derivedPlan?.path ?? null,
|
|
98
|
+
});
|
|
99
|
+
await notify('retry', `[neal] ${planName}: scope ${scopeLabel} split into derived plan; reviewing`, state.cwd);
|
|
100
|
+
}
|
|
101
|
+
async function notifyDerivedPlanAccepted(state, logger) {
|
|
102
|
+
const planName = basename(state.planDoc);
|
|
103
|
+
const scopeLabel = getParentScopeLabel(state);
|
|
104
|
+
const derivedPlan = getDerivedPlanView(state);
|
|
105
|
+
await logger?.event('notify.derived_plan_accepted', {
|
|
106
|
+
planName,
|
|
107
|
+
scopeNumber: scopeLabel,
|
|
108
|
+
derivedPlanPath: derivedPlan?.path ?? null,
|
|
109
|
+
});
|
|
110
|
+
await notify('complete', `[neal] ${planName}: derived plan accepted for scope ${scopeLabel}`, state.cwd);
|
|
111
|
+
}
|
|
112
|
+
async function notifyDerivedPlanFailed(state, reason, logger) {
|
|
113
|
+
const planName = basename(state.planDoc);
|
|
114
|
+
const scopeLabel = getParentScopeLabel(state);
|
|
115
|
+
const derivedPlan = getDerivedPlanView(state);
|
|
116
|
+
await logger?.event('notify.derived_plan_failed', {
|
|
117
|
+
planName,
|
|
118
|
+
scopeNumber: scopeLabel,
|
|
119
|
+
derivedPlanPath: derivedPlan?.path ?? null,
|
|
120
|
+
reason,
|
|
121
|
+
});
|
|
122
|
+
await notify('blocked', `[neal] ${planName}: blocked: derived plan review did not converge`, state.cwd);
|
|
123
|
+
}
|
|
124
|
+
async function notifySplitPlanRejected(state, reason, logger) {
|
|
125
|
+
const planName = basename(state.planDoc);
|
|
126
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
127
|
+
await logger?.event('notify.split_plan_rejected', {
|
|
128
|
+
planName,
|
|
129
|
+
scopeNumber: scopeLabel,
|
|
130
|
+
reason,
|
|
131
|
+
});
|
|
132
|
+
await notify('blocked', `[neal] ${planName}: blocked: split-plan recovery rejected for scope ${scopeLabel}`, state.cwd);
|
|
133
|
+
}
|
|
134
|
+
function getCurrentScopeBlockedReason(state) {
|
|
135
|
+
const currentScope = state.completedScopes.find((scope) => scope.number === getCurrentScopeLabel(state));
|
|
136
|
+
return currentScope?.blocker ?? null;
|
|
137
|
+
}
|
|
138
|
+
export async function flushDerivedPlanNotifications(state, statePath, logger, explicitBlockReason) {
|
|
139
|
+
let nextState = state;
|
|
140
|
+
if (nextState.topLevelMode !== 'execute') {
|
|
141
|
+
return nextState;
|
|
142
|
+
}
|
|
143
|
+
let derivedPlan = getDerivedPlanView(nextState);
|
|
144
|
+
if (derivedPlan?.notifications.needsSplitPlanStartedNotification) {
|
|
145
|
+
await notifySplitPlanStarted(nextState, logger);
|
|
146
|
+
nextState = await saveState(statePath, {
|
|
147
|
+
...nextState,
|
|
148
|
+
splitPlanStartedNotified: true,
|
|
149
|
+
});
|
|
150
|
+
derivedPlan = getDerivedPlanView(nextState);
|
|
151
|
+
}
|
|
152
|
+
if (derivedPlan?.notifications.needsDerivedPlanAcceptedNotification) {
|
|
153
|
+
await notifyDerivedPlanAccepted(nextState, logger);
|
|
154
|
+
nextState = await saveState(statePath, {
|
|
155
|
+
...nextState,
|
|
156
|
+
derivedPlanAcceptedNotified: true,
|
|
157
|
+
});
|
|
158
|
+
derivedPlan = getDerivedPlanView(nextState);
|
|
159
|
+
}
|
|
160
|
+
const blockReason = explicitBlockReason ?? getCurrentScopeBlockedReason(nextState);
|
|
161
|
+
if (nextState.status === 'blocked' && nextState.lastScopeMarker === 'AUTONOMY_SPLIT_PLAN' && !nextState.splitPlanBlockedNotified) {
|
|
162
|
+
if (derivedPlan?.abandoned) {
|
|
163
|
+
await notifyDerivedPlanFailed(nextState, blockReason ?? 'split-plan recovery failed');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await notifySplitPlanRejected(nextState, blockReason ?? 'split-plan recovery rejected');
|
|
167
|
+
}
|
|
168
|
+
nextState = await saveState(statePath, {
|
|
169
|
+
...nextState,
|
|
170
|
+
splitPlanBlockedNotified: true,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return nextState;
|
|
174
|
+
}
|
|
175
|
+
export { notifyBlocked, notifyComplete, notifyDerivedPlanAccepted, notifyDerivedPlanFailed, notifyInteractiveBlockedRecovery, notifyManualGate, notifyRetry, notifyScopeAccepted, notifySplitPlanRejected, notifySplitPlanStarted, };
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { CoderRoundError, runCoderScopeRound } from '../../agents.js';
|
|
2
|
+
import { getExecuteResponseOpenFindings, resolveExecuteAdjudicationContext, runExecuteResponseAdjudication, synthesizeExecuteResponseState, } from '../../adjudicator/execute.js';
|
|
3
|
+
import { assertAdjudicationTransitionSignal } from '../../adjudicator/specs.js';
|
|
4
|
+
import { commitStagedChanges, dropIgnoredPathsFromCurrentCommitRange, getChangedFilesForRange, getCommitRange, getCommitSubjects, getHeadCommit, getStagedChangedFiles, getWorktreeStatus, assertNoIgnoredChangedFiles, stagePath, } from '../../git.js';
|
|
5
|
+
import { EXECUTE_FINALIZATION_PHASE } from '../../execute-finalization.js';
|
|
6
|
+
import { getCurrentScopeLabel, getExecutionPlanPath } from '../../scopes.js';
|
|
7
|
+
import { saveState } from '../../state.js';
|
|
8
|
+
import { getDerivedPlanView } from '../../state-views.js';
|
|
9
|
+
import { writeExecutionArtifacts, writeManualGateArtifact } from '../artifacts.js';
|
|
10
|
+
import { isCoderTimeoutError, shouldNotifyFailure } from '../failures.js';
|
|
11
|
+
import { notifyBlocked, notifyInteractiveBlockedRecovery, notifyManualGate } from '../notifications.js';
|
|
12
|
+
import { persistSplitPlanRecovery } from '../split-plan.js';
|
|
13
|
+
import { enterInteractiveBlockedRecovery, shouldNotifyInteractiveBlockedRecoveryEntry } from './recovery.js';
|
|
14
|
+
import { bestEffortCleanupTimedOutCoder, persistBlockedScope, persistCoderFailureState, scheduleCoderFreshSessionRetry, shouldRetryCoderWithFreshSession, } from './shared.js';
|
|
15
|
+
import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, parseWorktreeStatusLine, } from '../../worktree-status.js';
|
|
16
|
+
export async function recoverPendingReviewFromCleanCommittedScope(state, statePath, logger, eventType) {
|
|
17
|
+
if (state.topLevelMode !== 'execute' ||
|
|
18
|
+
state.phase !== 'coder_scope' ||
|
|
19
|
+
!state.baseCommit ||
|
|
20
|
+
state.finalCommit !== null ||
|
|
21
|
+
state.createdCommits.length > 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
let headCommit = await getHeadCommit(state.cwd);
|
|
25
|
+
const worktreeStatus = await getWorktreeStatus(state.cwd);
|
|
26
|
+
let createdCommits = await getCommitRange(state.cwd, state.baseCommit, headCommit);
|
|
27
|
+
if (headCommit === state.baseCommit || createdCommits.length === 0 || worktreeStatus.trim() !== '') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
headCommit = await dropIgnoredCoderCommitPaths({
|
|
31
|
+
state,
|
|
32
|
+
beforeHead: state.baseCommit,
|
|
33
|
+
afterHead: headCommit,
|
|
34
|
+
phase: 'coder_scope',
|
|
35
|
+
logger,
|
|
36
|
+
});
|
|
37
|
+
createdCommits = await getCommitRange(state.cwd, state.baseCommit, headCommit);
|
|
38
|
+
if (headCommit === state.baseCommit || createdCommits.length === 0) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
await assertNoIgnoredChangedFiles(state.cwd, await getChangedFilesForRange(state.cwd, state.baseCommit, headCommit), 'Recovered coder scope');
|
|
42
|
+
const progressJustification = state.currentScopeProgressJustification ??
|
|
43
|
+
(await buildRecoveredPendingReviewProgressJustification(state, createdCommits));
|
|
44
|
+
const recoveredState = await saveState(statePath, {
|
|
45
|
+
...state,
|
|
46
|
+
createdCommits,
|
|
47
|
+
currentScopeProgressJustification: progressJustification,
|
|
48
|
+
phase: 'reviewer_scope',
|
|
49
|
+
status: 'running',
|
|
50
|
+
coderRetryCount: 0,
|
|
51
|
+
});
|
|
52
|
+
await writeExecutionArtifacts(recoveredState);
|
|
53
|
+
await logger?.event(eventType, {
|
|
54
|
+
statePath,
|
|
55
|
+
previousPhase: 'coder_scope',
|
|
56
|
+
recoveredPhase: recoveredState.phase,
|
|
57
|
+
baseCommit: recoveredState.baseCommit,
|
|
58
|
+
headCommit,
|
|
59
|
+
createdCommits,
|
|
60
|
+
progressJustification,
|
|
61
|
+
});
|
|
62
|
+
return recoveredState;
|
|
63
|
+
}
|
|
64
|
+
function getScopeCompletionProblem(marker) {
|
|
65
|
+
if (marker === 'AUTONOMY_BLOCKED' || marker === 'AUTONOMY_SPLIT_PLAN') {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return marker === 'AUTONOMY_SCOPE_DONE' || marker === 'AUTONOMY_CHUNK_DONE' || marker === 'AUTONOMY_DONE'
|
|
69
|
+
? null
|
|
70
|
+
: 'Execution must end with AUTONOMY_SCOPE_DONE, AUTONOMY_DONE, AUTONOMY_SPLIT_PLAN, or AUTONOMY_BLOCKED.';
|
|
71
|
+
}
|
|
72
|
+
function isSplitPlanMarker(marker) {
|
|
73
|
+
return marker === 'AUTONOMY_SPLIT_PLAN';
|
|
74
|
+
}
|
|
75
|
+
function stripTrailingMarker(text, marker) {
|
|
76
|
+
const lines = text.split(/\r?\n/);
|
|
77
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
78
|
+
if (lines[index].trim() === marker) {
|
|
79
|
+
lines.splice(index, 1);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n').trim();
|
|
84
|
+
}
|
|
85
|
+
export function getExecuteResponseRetryPhase(mode) {
|
|
86
|
+
return mode === 'optional' ? 'coder_optional_response' : 'coder_response';
|
|
87
|
+
}
|
|
88
|
+
export function getExecuteResponsePhaseWithoutOpenFindings() {
|
|
89
|
+
return {
|
|
90
|
+
phase: EXECUTE_FINALIZATION_PHASE,
|
|
91
|
+
status: 'running',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function recoverPendingReviewFromCleanCommittedResponse(state, statePath, beforeHead, phase, logger) {
|
|
95
|
+
let afterHead = await getHeadCommit(state.cwd);
|
|
96
|
+
if (afterHead === beforeHead) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
afterHead = await dropIgnoredCoderCommitPaths({
|
|
100
|
+
state,
|
|
101
|
+
beforeHead,
|
|
102
|
+
afterHead,
|
|
103
|
+
phase,
|
|
104
|
+
logger,
|
|
105
|
+
});
|
|
106
|
+
const createdCommits = await getCommitRange(state.cwd, beforeHead, afterHead);
|
|
107
|
+
if (createdCommits.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const worktreeStatus = filterAllowedDirtyPathStatus(state.cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(state.cwd)), state.allowedDirtyPaths);
|
|
111
|
+
if (worktreeStatus.trim() !== '') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const changedFiles = await getChangedFilesForRange(state.cwd, beforeHead, afterHead);
|
|
115
|
+
await assertNoIgnoredChangedFiles(state.cwd, changedFiles, 'Recovered coder review response');
|
|
116
|
+
const recoveredState = await saveState(statePath, {
|
|
117
|
+
...state,
|
|
118
|
+
phase: 'reviewer_scope',
|
|
119
|
+
status: 'running',
|
|
120
|
+
blockedFromPhase: null,
|
|
121
|
+
createdCommits: [...state.createdCommits, ...createdCommits],
|
|
122
|
+
coderRetryCount: 0,
|
|
123
|
+
});
|
|
124
|
+
await writeExecutionArtifacts(recoveredState);
|
|
125
|
+
await logger?.event('run.recovered_pending_review_after_coder_response_failure', {
|
|
126
|
+
statePath,
|
|
127
|
+
previousPhase: phase,
|
|
128
|
+
recoveredPhase: recoveredState.phase,
|
|
129
|
+
beforeHead,
|
|
130
|
+
afterHead,
|
|
131
|
+
createdCommits,
|
|
132
|
+
changedFiles,
|
|
133
|
+
});
|
|
134
|
+
return recoveredState;
|
|
135
|
+
}
|
|
136
|
+
export async function runCoderScopePhase(state, statePath, logger) {
|
|
137
|
+
if (!state.baseCommit) {
|
|
138
|
+
throw new Error('Cannot run coder scope phase without baseCommit');
|
|
139
|
+
}
|
|
140
|
+
await logger?.event('phase.start', { phase: 'coder_scope' });
|
|
141
|
+
const beforeHead = await getHeadCommit(state.cwd);
|
|
142
|
+
let workingState = state;
|
|
143
|
+
let codex;
|
|
144
|
+
const activeCoderSessionProtocol = state.coderSessionHandle
|
|
145
|
+
? state.coderSessionProtocol
|
|
146
|
+
: 'structured_json_v1';
|
|
147
|
+
try {
|
|
148
|
+
codex = await runCoderScopeRound({
|
|
149
|
+
coder: state.agentConfig.coder,
|
|
150
|
+
cwd: state.cwd,
|
|
151
|
+
planDoc: getExecutionPlanPath(state),
|
|
152
|
+
progressMarkdownPath: state.progressMarkdownPath,
|
|
153
|
+
sessionHandle: state.coderSessionHandle,
|
|
154
|
+
coderSessionProtocol: state.coderSessionProtocol,
|
|
155
|
+
unattended: state.unattended,
|
|
156
|
+
onSessionStarted: async (sessionHandle) => {
|
|
157
|
+
state.coderSessionHandle = sessionHandle;
|
|
158
|
+
state.coderSessionProtocol = activeCoderSessionProtocol;
|
|
159
|
+
workingState = await saveState(statePath, {
|
|
160
|
+
...workingState,
|
|
161
|
+
coderSessionHandle: sessionHandle,
|
|
162
|
+
coderSessionProtocol: activeCoderSessionProtocol,
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
logger,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof CoderRoundError) {
|
|
170
|
+
if (shouldRetryCoderWithFreshSession(workingState, 'coder_scope', error)) {
|
|
171
|
+
return scheduleCoderFreshSessionRetry(workingState, statePath, 'coder_scope', error, logger);
|
|
172
|
+
}
|
|
173
|
+
if (isCoderTimeoutError(error)) {
|
|
174
|
+
await bestEffortCleanupTimedOutCoder(error.sessionHandle ?? workingState.coderSessionHandle, logger);
|
|
175
|
+
}
|
|
176
|
+
const recoveredState = await recoverPendingReviewFromCleanCommittedScope({
|
|
177
|
+
...workingState,
|
|
178
|
+
coderSessionHandle: error.sessionHandle ?? workingState.coderSessionHandle,
|
|
179
|
+
coderSessionProtocol: (error.sessionHandle ?? workingState.coderSessionHandle)
|
|
180
|
+
? workingState.coderSessionProtocol ?? activeCoderSessionProtocol
|
|
181
|
+
: null,
|
|
182
|
+
}, statePath, logger, 'run.recovered_pending_review_after_coder_failure');
|
|
183
|
+
if (recoveredState) {
|
|
184
|
+
return recoveredState;
|
|
185
|
+
}
|
|
186
|
+
const failedState = await persistCoderFailureState(workingState, statePath, 'coder_scope', error, logger);
|
|
187
|
+
if (shouldNotifyFailure(error)) {
|
|
188
|
+
await notifyBlocked(failedState, error.message, logger);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
let afterHead = await getHeadCommit(state.cwd);
|
|
194
|
+
afterHead = await dropIgnoredCoderCommitPaths({
|
|
195
|
+
state,
|
|
196
|
+
beforeHead,
|
|
197
|
+
afterHead,
|
|
198
|
+
phase: 'coder_scope',
|
|
199
|
+
logger,
|
|
200
|
+
});
|
|
201
|
+
let createdCommits = await getCommitRange(state.cwd, beforeHead, afterHead);
|
|
202
|
+
if (codex.manualGate) {
|
|
203
|
+
const now = new Date().toISOString();
|
|
204
|
+
const instructionsPath = await writeManualGateArtifact({
|
|
205
|
+
state: workingState,
|
|
206
|
+
...codex.manualGate,
|
|
207
|
+
});
|
|
208
|
+
const manualGateState = await saveState(statePath, {
|
|
209
|
+
...workingState,
|
|
210
|
+
coderSessionHandle: codex.sessionHandle,
|
|
211
|
+
coderSessionProtocol: codex.sessionHandle ? activeCoderSessionProtocol : null,
|
|
212
|
+
lastScopeMarker: null,
|
|
213
|
+
currentScopeProgressJustification: codex.progressJustification,
|
|
214
|
+
phase: 'manual_gate',
|
|
215
|
+
status: 'running',
|
|
216
|
+
blockedFromPhase: null,
|
|
217
|
+
manualGate: {
|
|
218
|
+
id: codex.manualGate.id,
|
|
219
|
+
title: codex.manualGate.title,
|
|
220
|
+
reason: codex.manualGate.reason,
|
|
221
|
+
instructionsPath,
|
|
222
|
+
resumeChecks: codex.manualGate.resumeChecks,
|
|
223
|
+
resumePhase: 'coder_scope',
|
|
224
|
+
createdAt: now,
|
|
225
|
+
updatedAt: now,
|
|
226
|
+
lastCheckedAt: null,
|
|
227
|
+
lastFailure: null,
|
|
228
|
+
},
|
|
229
|
+
createdCommits: [...workingState.createdCommits, ...createdCommits],
|
|
230
|
+
coderRetryCount: 0,
|
|
231
|
+
});
|
|
232
|
+
await writeExecutionArtifacts(manualGateState);
|
|
233
|
+
const openedGate = manualGateState.manualGate;
|
|
234
|
+
if (!openedGate) {
|
|
235
|
+
throw new Error('Manual gate state was not persisted after manual_gate coder action.');
|
|
236
|
+
}
|
|
237
|
+
await logger?.event('manual_gate.opened', {
|
|
238
|
+
phase: 'coder_scope',
|
|
239
|
+
gateId: openedGate.id,
|
|
240
|
+
title: openedGate.title,
|
|
241
|
+
instructionsPath,
|
|
242
|
+
resumeChecks: openedGate.resumeChecks.map((check) => check.name),
|
|
243
|
+
sessionHandle: codex.sessionHandle,
|
|
244
|
+
createdCommits,
|
|
245
|
+
nextPhase: manualGateState.phase,
|
|
246
|
+
});
|
|
247
|
+
await notifyManualGate(manualGateState, logger);
|
|
248
|
+
return manualGateState;
|
|
249
|
+
}
|
|
250
|
+
const completionProblem = getScopeCompletionProblem(codex.marker);
|
|
251
|
+
const splitPlan = isSplitPlanMarker(codex.marker);
|
|
252
|
+
const canCommitDirtyCoderWorktree = codex.marker !== 'AUTONOMY_BLOCKED' && !splitPlan && !completionProblem;
|
|
253
|
+
if (canCommitDirtyCoderWorktree) {
|
|
254
|
+
const autoCommit = await commitDirtyCoderWorktreeChanges(workingState, logger, 'coder_scope');
|
|
255
|
+
if (autoCommit) {
|
|
256
|
+
afterHead = autoCommit;
|
|
257
|
+
createdCommits = await getCommitRange(state.cwd, beforeHead, afterHead);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const changedFilesInCoderRound = createdCommits.length > 0 ? await getChangedFilesForRange(state.cwd, beforeHead, afterHead) : [];
|
|
261
|
+
await assertNoIgnoredChangedFiles(state.cwd, changedFilesInCoderRound, 'Coder scope');
|
|
262
|
+
const changedFilesSinceBase = await getChangedFilesForRange(state.cwd, state.baseCommit, afterHead);
|
|
263
|
+
const verificationOnlyCompletion = codex.marker === 'AUTONOMY_DONE' &&
|
|
264
|
+
createdCommits.length === 0 &&
|
|
265
|
+
changedFilesSinceBase.length === 0;
|
|
266
|
+
const nextState = await saveState(statePath, {
|
|
267
|
+
...workingState,
|
|
268
|
+
coderSessionHandle: codex.sessionHandle,
|
|
269
|
+
coderSessionProtocol: codex.sessionHandle ? activeCoderSessionProtocol : null,
|
|
270
|
+
lastScopeMarker: codex.marker,
|
|
271
|
+
currentScopeProgressJustification: codex.progressJustification,
|
|
272
|
+
phase: codex.marker === 'AUTONOMY_BLOCKED' || splitPlan || completionProblem
|
|
273
|
+
? 'blocked'
|
|
274
|
+
: verificationOnlyCompletion
|
|
275
|
+
? EXECUTE_FINALIZATION_PHASE
|
|
276
|
+
: 'reviewer_scope',
|
|
277
|
+
status: codex.marker === 'AUTONOMY_BLOCKED' || splitPlan || completionProblem ? 'blocked' : 'running',
|
|
278
|
+
blockedFromPhase: codex.marker === 'AUTONOMY_BLOCKED' || completionProblem ? 'coder_scope' : null,
|
|
279
|
+
createdCommits: [...workingState.createdCommits, ...createdCommits],
|
|
280
|
+
coderRetryCount: 0,
|
|
281
|
+
});
|
|
282
|
+
await writeExecutionArtifacts(nextState);
|
|
283
|
+
await logger?.event('phase.complete', {
|
|
284
|
+
phase: 'coder_scope',
|
|
285
|
+
marker: codex.marker,
|
|
286
|
+
sessionHandle: codex.sessionHandle,
|
|
287
|
+
createdCommits,
|
|
288
|
+
verificationOnlyCompletion,
|
|
289
|
+
nextPhase: nextState.phase,
|
|
290
|
+
});
|
|
291
|
+
if (splitPlan) {
|
|
292
|
+
return persistSplitPlanRecovery(nextState, statePath, {
|
|
293
|
+
sourcePhase: 'coder_scope',
|
|
294
|
+
derivedPlanMarkdown: stripTrailingMarker(codex.responseWithoutProgressPayload, 'AUTONOMY_SPLIT_PLAN'),
|
|
295
|
+
createdCommits,
|
|
296
|
+
logger,
|
|
297
|
+
}, {
|
|
298
|
+
persistBlockedScope,
|
|
299
|
+
writeExecutionArtifacts,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (nextState.status === 'blocked') {
|
|
303
|
+
const reason = completionProblem ?? 'The coder reported a blocker during scope execution';
|
|
304
|
+
const persistedState = await enterInteractiveBlockedRecovery(nextState, statePath, reason, logger);
|
|
305
|
+
if (shouldNotifyInteractiveBlockedRecoveryEntry(persistedState)) {
|
|
306
|
+
await notifyInteractiveBlockedRecovery(persistedState, reason, logger);
|
|
307
|
+
}
|
|
308
|
+
return persistedState;
|
|
309
|
+
}
|
|
310
|
+
return nextState;
|
|
311
|
+
}
|
|
312
|
+
export async function runExecuteResponsePhase(state, statePath, phase, logger) {
|
|
313
|
+
const mode = phase === 'coder_optional_response' ? 'optional' : 'required';
|
|
314
|
+
await logger?.event('phase.start', { phase });
|
|
315
|
+
const { spec } = resolveExecuteAdjudicationContext(state);
|
|
316
|
+
const openFindings = getExecuteResponseOpenFindings(state, mode === 'optional' ? 'optional' : undefined);
|
|
317
|
+
if (openFindings.length === 0) {
|
|
318
|
+
assertAdjudicationTransitionSignal(spec, 'accept_scope', `orchestrator:${phase}:settled`);
|
|
319
|
+
const settled = getExecuteResponsePhaseWithoutOpenFindings();
|
|
320
|
+
const nextState = await saveState(statePath, {
|
|
321
|
+
...state,
|
|
322
|
+
phase: settled.phase,
|
|
323
|
+
status: settled.status,
|
|
324
|
+
});
|
|
325
|
+
await writeExecutionArtifacts(nextState);
|
|
326
|
+
await logger?.event('phase.complete', {
|
|
327
|
+
phase,
|
|
328
|
+
openFindings: 0,
|
|
329
|
+
nextPhase: nextState.phase,
|
|
330
|
+
});
|
|
331
|
+
return nextState;
|
|
332
|
+
}
|
|
333
|
+
const beforeHead = await getHeadCommit(state.cwd);
|
|
334
|
+
let codex;
|
|
335
|
+
try {
|
|
336
|
+
({ response: codex } = await runExecuteResponseAdjudication({
|
|
337
|
+
state,
|
|
338
|
+
mode: mode === 'optional' ? 'optional' : undefined,
|
|
339
|
+
logger,
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof CoderRoundError) {
|
|
344
|
+
const retryPhase = getExecuteResponseRetryPhase(mode);
|
|
345
|
+
if (shouldRetryCoderWithFreshSession(state, retryPhase, error)) {
|
|
346
|
+
return scheduleCoderFreshSessionRetry(state, statePath, retryPhase, error, logger);
|
|
347
|
+
}
|
|
348
|
+
if (isCoderTimeoutError(error)) {
|
|
349
|
+
await bestEffortCleanupTimedOutCoder(error.sessionHandle ?? state.coderSessionHandle, logger);
|
|
350
|
+
}
|
|
351
|
+
const recoveredState = await recoverPendingReviewFromCleanCommittedResponse(state, statePath, beforeHead, retryPhase, logger);
|
|
352
|
+
if (recoveredState) {
|
|
353
|
+
return recoveredState;
|
|
354
|
+
}
|
|
355
|
+
const failedState = await persistCoderFailureState(state, statePath, retryPhase, error, logger);
|
|
356
|
+
if (shouldNotifyFailure(error)) {
|
|
357
|
+
await notifyBlocked(failedState, error.message, logger);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
let afterHead = await getHeadCommit(state.cwd);
|
|
363
|
+
afterHead = await dropIgnoredCoderCommitPaths({
|
|
364
|
+
state,
|
|
365
|
+
beforeHead,
|
|
366
|
+
afterHead,
|
|
367
|
+
phase,
|
|
368
|
+
logger,
|
|
369
|
+
});
|
|
370
|
+
let createdCommits = await getCommitRange(state.cwd, beforeHead, afterHead);
|
|
371
|
+
if (codex.payload.outcome === 'responded') {
|
|
372
|
+
const autoCommit = await commitDirtyCoderWorktreeChanges(state, logger, phase);
|
|
373
|
+
if (autoCommit) {
|
|
374
|
+
afterHead = autoCommit;
|
|
375
|
+
afterHead = await dropIgnoredCoderCommitPaths({
|
|
376
|
+
state,
|
|
377
|
+
beforeHead,
|
|
378
|
+
afterHead,
|
|
379
|
+
phase,
|
|
380
|
+
logger,
|
|
381
|
+
});
|
|
382
|
+
createdCommits = await getCommitRange(state.cwd, beforeHead, afterHead);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const changedFilesInResponse = createdCommits.length > 0 ? await getChangedFilesForRange(state.cwd, beforeHead, afterHead) : [];
|
|
386
|
+
await assertNoIgnoredChangedFiles(state.cwd, changedFilesInResponse, 'Coder review response');
|
|
387
|
+
const responseState = synthesizeExecuteResponseState({
|
|
388
|
+
state,
|
|
389
|
+
mode: mode === 'optional' ? 'optional' : undefined,
|
|
390
|
+
response: codex,
|
|
391
|
+
createdCommits,
|
|
392
|
+
});
|
|
393
|
+
const nextState = await saveState(statePath, {
|
|
394
|
+
...state,
|
|
395
|
+
coderSessionHandle: codex.sessionHandle,
|
|
396
|
+
coderSessionProtocol: codex.sessionHandle ? state.coderSessionProtocol : null,
|
|
397
|
+
findings: responseState.findings,
|
|
398
|
+
createdCommits: [...state.createdCommits, ...createdCommits],
|
|
399
|
+
phase: responseState.nextPhase,
|
|
400
|
+
status: responseState.nextStatus,
|
|
401
|
+
blockedFromPhase: responseState.blockedFromPhase,
|
|
402
|
+
coderRetryCount: 0,
|
|
403
|
+
});
|
|
404
|
+
await writeExecutionArtifacts(nextState);
|
|
405
|
+
await logger?.event('phase.complete', {
|
|
406
|
+
phase,
|
|
407
|
+
outcome: codex.payload.outcome,
|
|
408
|
+
respondedFindings: codex.payload.responses.length,
|
|
409
|
+
createdCommits,
|
|
410
|
+
nextPhase: nextState.phase,
|
|
411
|
+
});
|
|
412
|
+
if (codex.payload.outcome === 'split_plan') {
|
|
413
|
+
return persistSplitPlanRecovery(nextState, statePath, {
|
|
414
|
+
sourcePhase: phase,
|
|
415
|
+
derivedPlanMarkdown: codex.payload.derivedPlan?.trim() ?? '',
|
|
416
|
+
createdCommits,
|
|
417
|
+
logger,
|
|
418
|
+
}, {
|
|
419
|
+
persistBlockedScope,
|
|
420
|
+
writeExecutionArtifacts,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (nextState.status === 'blocked') {
|
|
424
|
+
const blocker = codex.payload.blocker?.trim() ||
|
|
425
|
+
codex.payload.summary.trim() ||
|
|
426
|
+
(mode === 'optional'
|
|
427
|
+
? 'The coder reported a blocker while considering non-blocking review findings'
|
|
428
|
+
: 'The coder reported a blocker during review response');
|
|
429
|
+
const persistedState = await enterInteractiveBlockedRecovery(nextState, statePath, blocker, logger);
|
|
430
|
+
if (shouldNotifyInteractiveBlockedRecoveryEntry(persistedState)) {
|
|
431
|
+
await notifyBlocked(persistedState, blocker, logger);
|
|
432
|
+
}
|
|
433
|
+
return persistedState;
|
|
434
|
+
}
|
|
435
|
+
return nextState;
|
|
436
|
+
}
|
|
437
|
+
async function buildRecoveredPendingReviewProgressJustification(state, createdCommits) {
|
|
438
|
+
const commitSubjects = await getCommitSubjects(state.cwd, createdCommits);
|
|
439
|
+
const changedFiles = state.baseCommit
|
|
440
|
+
? await getChangedFilesForRange(state.cwd, state.baseCommit, createdCommits.at(-1) ?? state.baseCommit)
|
|
441
|
+
: [];
|
|
442
|
+
const derivedPlan = getDerivedPlanView(state);
|
|
443
|
+
const scopeLabel = derivedPlan?.executing
|
|
444
|
+
? `derived scope ${derivedPlan.scopeIndex} of parent scope ${derivedPlan.parentScopeNumber ?? state.currentScopeNumber}`
|
|
445
|
+
: `scope ${state.currentScopeNumber}`;
|
|
446
|
+
return {
|
|
447
|
+
milestoneTargeted: `Recovered completed coder work for ${scopeLabel}`,
|
|
448
|
+
newEvidence: [
|
|
449
|
+
`Resume detected ${createdCommits.length} committed change(s) after the saved base commit while the worktree was clean.`,
|
|
450
|
+
commitSubjects.length > 0 ? `Commits: ${commitSubjects.join('; ')}` : null,
|
|
451
|
+
changedFiles.length > 0 ? `Changed files: ${changedFiles.join(', ')}` : null,
|
|
452
|
+
]
|
|
453
|
+
.filter((line) => Boolean(line))
|
|
454
|
+
.join(' '),
|
|
455
|
+
whyNotRedundant: 'The previous coder turn failed after creating commits, so this recovered review checkpoint covers committed work that has not yet been adjudicated.',
|
|
456
|
+
nextStepUnlocked: 'Reviewer can adjudicate the recovered committed scope instead of Neal crashing or rerunning coder work that already produced commits.',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function commitDirtyCoderWorktreeChanges(state, logger, phase) {
|
|
460
|
+
const statusOutput = filterAllowedDirtyPathStatus(state.cwd, filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(state.cwd)), state.allowedDirtyPaths);
|
|
461
|
+
if (!statusOutput) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const paths = [
|
|
465
|
+
...new Set(statusOutput
|
|
466
|
+
.split('\n')
|
|
467
|
+
.map(parseWorktreeStatusLine)
|
|
468
|
+
.filter((entry) => entry !== null)
|
|
469
|
+
.flatMap((entry) => entry.paths)),
|
|
470
|
+
];
|
|
471
|
+
for (const path of paths) {
|
|
472
|
+
await stagePath(state.cwd, path);
|
|
473
|
+
}
|
|
474
|
+
const stagedChangedFiles = await getStagedChangedFiles(state.cwd);
|
|
475
|
+
if (stagedChangedFiles.length === 0) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const context = phase === 'coder_scope' ? 'Coder scope' : 'Coder review response';
|
|
479
|
+
await assertNoIgnoredChangedFiles(state.cwd, stagedChangedFiles, context);
|
|
480
|
+
const finalCommit = await commitStagedChanges(state.cwd, `${getAutoCommitSubject(state, phase)}\n`);
|
|
481
|
+
await logger?.event(`${phase}.autocommit_dirty_worktree`, {
|
|
482
|
+
message: phase === 'coder_scope'
|
|
483
|
+
? 'Created a Neal commit for dirty coder edits after the coder reported scope completion without committing all changes.'
|
|
484
|
+
: 'Created a Neal commit for dirty coder edits after the coder reported review findings were handled without committing all changes.',
|
|
485
|
+
finalCommit,
|
|
486
|
+
changedFiles: stagedChangedFiles,
|
|
487
|
+
});
|
|
488
|
+
return finalCommit;
|
|
489
|
+
}
|
|
490
|
+
function getAutoCommitSubject(state, phase) {
|
|
491
|
+
switch (phase) {
|
|
492
|
+
case 'coder_scope':
|
|
493
|
+
return `Complete scope ${getCurrentScopeLabel(state)}`;
|
|
494
|
+
case 'coder_optional_response':
|
|
495
|
+
return `Address optional review findings for scope ${getCurrentScopeLabel(state)}`;
|
|
496
|
+
case 'coder_response':
|
|
497
|
+
return `Address review findings for scope ${getCurrentScopeLabel(state)}`;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function dropIgnoredCoderCommitPaths(args) {
|
|
501
|
+
if (args.beforeHead === args.afterHead) {
|
|
502
|
+
return args.afterHead;
|
|
503
|
+
}
|
|
504
|
+
const result = await dropIgnoredPathsFromCurrentCommitRange(args.state.cwd, args.beforeHead, args.afterHead);
|
|
505
|
+
if (result.ignoredFiles.length > 0) {
|
|
506
|
+
await args.logger?.event('coder.ignored_paths_dropped', {
|
|
507
|
+
phase: args.phase,
|
|
508
|
+
baseCommit: args.beforeHead,
|
|
509
|
+
originalHeadCommit: args.afterHead,
|
|
510
|
+
headCommit: result.headCommit,
|
|
511
|
+
createdReplacementCommit: result.createdReplacementCommit,
|
|
512
|
+
ignoredFiles: result.ignoredFiles,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return result.headCommit;
|
|
516
|
+
}
|