@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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,220 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { planResumeActions } from './resume-planner.js';
4
+ import { getInteractiveRecoveryView } from './state-views.js';
5
+ export function decideResumeAction(args) {
6
+ const resumeCommand = formatResumeCommand(args.selectedRunId);
7
+ const messageCommand = formatMessageCommand(args.selectedRunId);
8
+ const statusCommand = formatStatusCommand(args.selectedRunId);
9
+ const lockDecision = decideLockAction(args.lock, args.selectedRunId, statusCommand);
10
+ if (lockDecision) {
11
+ return lockDecision;
12
+ }
13
+ const queueDecision = decideQueueAction(args.queue, statusCommand);
14
+ if (queueDecision) {
15
+ // A consumed queue child whose run itself completed is the ordinary
16
+ // "nothing left to resume" case, not a queue inconsistency: the queue
17
+ // recorded the outcome and moved on. Report it as done instead of
18
+ // surfacing queue-internal bookkeeping as a resume error.
19
+ if (args.queue?.kind === 'consumed_child' && isOrdinaryDone(planResumeActions(args.state))) {
20
+ return {
21
+ kind: 'done',
22
+ summary: args.retrospectivePath
23
+ ? `Run ${args.selectedRunId} is already complete. Latest retrospective: ${args.retrospectivePath}.`
24
+ : `Run ${args.selectedRunId} is already complete.`,
25
+ statusCommand,
26
+ retrospectivePath: args.retrospectivePath ?? null,
27
+ };
28
+ }
29
+ return queueDecision;
30
+ }
31
+ const interactiveRecovery = getInteractiveRecoveryView(args.state);
32
+ if (interactiveRecovery?.active) {
33
+ if (interactiveRecovery.waitingForOperatorGuidance) {
34
+ return {
35
+ kind: 'needs_message',
36
+ blocker: interactiveRecovery.blockedReason,
37
+ messageCommand,
38
+ statusCommand,
39
+ };
40
+ }
41
+ if (interactiveRecovery.pendingOperatorGuidance) {
42
+ return {
43
+ kind: 'pending_message',
44
+ reason: 'Operator guidance is recorded and ready for Neal to process.',
45
+ resumeCommand,
46
+ };
47
+ }
48
+ }
49
+ if (isWaitingForPlanReviewGuidance(args.state)) {
50
+ return {
51
+ kind: 'needs_message',
52
+ blocker: formatPlanReviewGuidanceBlocker(args.state),
53
+ messageCommand,
54
+ statusCommand,
55
+ };
56
+ }
57
+ if (hasPendingPlanReviewGuidance(args.state)) {
58
+ return {
59
+ kind: 'pending_message',
60
+ reason: 'Operator guidance is recorded and ready for Neal to process.',
61
+ resumeCommand,
62
+ };
63
+ }
64
+ const actions = planResumeActions(args.state);
65
+ if (onlyKeepsBlocked(actions)) {
66
+ return {
67
+ kind: 'cannot_resume',
68
+ reason: formatKeptBlockedReason(args.state),
69
+ statusCommand,
70
+ };
71
+ }
72
+ if (isOrdinaryDone(actions)) {
73
+ return {
74
+ kind: 'done',
75
+ summary: args.retrospectivePath
76
+ ? `Run ${args.selectedRunId} is already complete. Latest retrospective: ${args.retrospectivePath}.`
77
+ : `Run ${args.selectedRunId} is already complete.`,
78
+ statusCommand,
79
+ retrospectivePath: args.retrospectivePath ?? null,
80
+ };
81
+ }
82
+ return {
83
+ kind: 'continue',
84
+ reason: formatContinueReason(actions),
85
+ resumeCommand,
86
+ };
87
+ }
88
+ function isWaitingForPlanReviewGuidance(state) {
89
+ return (state.topLevelMode === 'plan' &&
90
+ state.status === 'blocked' &&
91
+ state.phase === 'blocked' &&
92
+ state.blockedFromPhase === 'reviewer_plan' &&
93
+ state.pendingPlanReviewGuidance === null);
94
+ }
95
+ function hasPendingPlanReviewGuidance(state) {
96
+ return state.topLevelMode === 'plan' && state.pendingPlanReviewGuidance !== null;
97
+ }
98
+ function formatPlanReviewGuidanceBlocker(state) {
99
+ const blockedFrom = state.blockedFromPhase ? ` from ${state.blockedFromPhase}` : '';
100
+ return `Run is blocked${blockedFrom} and is waiting for operator guidance via neal resume --message.`;
101
+ }
102
+ function decideLockAction(lock, selectedRunId, selectedStatusCommand) {
103
+ switch (lock?.kind) {
104
+ case undefined:
105
+ case 'none':
106
+ case 'stale_same_run':
107
+ case 'stale_different_run':
108
+ return null;
109
+ case 'live_same_run':
110
+ return {
111
+ kind: 'already_running',
112
+ reason: `Run ${selectedRunId} appears to already be running under the active writer lock.`,
113
+ statusCommand: selectedStatusCommand,
114
+ };
115
+ case 'live_different_run':
116
+ return {
117
+ kind: 'cannot_resume',
118
+ reason: `Another Neal writer run is active in this checkout: ${lock.runId}. Continue it with ${formatResumeCommand(lock.runId)} or inspect it with ${formatStatusCommand(lock.runId)}.`,
119
+ statusCommand: formatStatusCommand(lock.runId),
120
+ };
121
+ case 'cross_host':
122
+ return {
123
+ kind: 'cannot_resume',
124
+ reason: `The active writer lock belongs to host ${lock.hostname ?? 'unknown'} for run ${lock.runId}. Inspect that host before resuming a writer run here.`,
125
+ statusCommand: selectedStatusCommand,
126
+ };
127
+ case 'unreadable':
128
+ return {
129
+ kind: 'cannot_resume',
130
+ reason: `The active writer lock could not be read: ${lock.reason}. Inspect the lock before resuming a writer run.`,
131
+ statusCommand: selectedStatusCommand,
132
+ };
133
+ }
134
+ }
135
+ function decideQueueAction(queue, statusCommand) {
136
+ switch (queue?.kind) {
137
+ case undefined:
138
+ case 'not_linked':
139
+ case 'active_child':
140
+ return null;
141
+ case 'consumed_child':
142
+ case 'stale_child':
143
+ case 'mismatched_child':
144
+ case 'mismatched_stage':
145
+ case 'unreadable':
146
+ return {
147
+ kind: 'cannot_resume',
148
+ reason: queue.reason,
149
+ statusCommand,
150
+ };
151
+ }
152
+ }
153
+ function onlyKeepsBlocked(actions) {
154
+ const controlActions = actions.filter((action) => action.kind !== 'flush_derived_plan_notifications' &&
155
+ action.kind !== 'log_interactive_blocked_recovery');
156
+ return controlActions.length > 0 && controlActions.every((action) => action.kind === 'keep_blocked');
157
+ }
158
+ function isOrdinaryDone(actions) {
159
+ return actions.length === 1 && actions[0]?.kind === 'done_noop';
160
+ }
161
+ function formatKeptBlockedReason(state) {
162
+ const blockedFrom = state.blockedFromPhase ? ` from ${state.blockedFromPhase}` : '';
163
+ const diagnostics = formatTerminalBlockDiagnostics(state);
164
+ return diagnostics
165
+ ? `Run is blocked${blockedFrom} and cannot be mechanically resumed. ${diagnostics} Inspect status and artifacts before adding guidance.`
166
+ : `Run is blocked${blockedFrom} and cannot be mechanically resumed. Inspect status and artifacts before adding guidance.`;
167
+ }
168
+ function formatContinueReason(actions) {
169
+ const actionKinds = actions.map((action) => action.kind).filter((kind) => kind !== 'log_interactive_blocked_recovery');
170
+ if (actionKinds.length === 0) {
171
+ return 'Run is in a resumable state.';
172
+ }
173
+ return `Resume planner selected ${actionKinds.join(', ')}.`;
174
+ }
175
+ function formatResumeCommand(runId) {
176
+ return `neal resume --run ${runId}`;
177
+ }
178
+ function formatMessageCommand(runId) {
179
+ return `neal resume --run ${runId} --message "..."`;
180
+ }
181
+ function formatStatusCommand(runId) {
182
+ return `neal status --run ${runId}`;
183
+ }
184
+ function formatTerminalBlockDiagnostics(state) {
185
+ const invalidPayloadPath = findCurrentInvalidSplitPlanPayloadArtifactPath(state);
186
+ const parts = [
187
+ latestRecoveryBlocker(state) ?? currentOrLatestBlockedCompletedScopeBlocker(state),
188
+ invalidPayloadPath ? `Invalid payload artifact: ${invalidPayloadPath}` : null,
189
+ ].filter((part) => Boolean(part));
190
+ return parts.length > 0 ? parts.join(' ') : null;
191
+ }
192
+ function latestRecoveryBlocker(state) {
193
+ const active = state.interactiveBlockedRecovery;
194
+ if (active) {
195
+ const candidate = nonEmptyString(active.pendingDirective?.operatorGuidance) ?? nonEmptyString(active.blockedReason);
196
+ if (candidate) {
197
+ return candidate;
198
+ }
199
+ }
200
+ for (const record of [...state.interactiveBlockedRecoveryHistory].reverse()) {
201
+ const candidate = nonEmptyString(record.blockedReason);
202
+ if (candidate) {
203
+ return candidate;
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ function nonEmptyString(value) {
209
+ const trimmed = value?.trim();
210
+ return trimmed ? trimmed : null;
211
+ }
212
+ function currentOrLatestBlockedCompletedScopeBlocker(state) {
213
+ const blockedScopes = state.completedScopes.filter((scope) => scope.result === 'blocked' && scope.blocker);
214
+ const currentScope = blockedScopes.find((scope) => scope.number === String(state.currentScopeNumber));
215
+ return currentScope?.blocker ?? blockedScopes.at(-1)?.blocker ?? null;
216
+ }
217
+ function findCurrentInvalidSplitPlanPayloadArtifactPath(state) {
218
+ const artifactPath = join(state.runDir, `SCOPE_${state.currentScopeNumber}_INVALID_DERIVED_PLAN.md`);
219
+ return existsSync(artifactPath) ? artifactPath : null;
220
+ }
@@ -0,0 +1,265 @@
1
+ import { flushDerivedPlanNotifications } from './orchestrator/notifications.js';
2
+ import { recoverPendingReviewFromCleanCommittedScope } from './orchestrator/phases/coder.js';
3
+ import { isResumableBlockedPhase } from './orchestrator/phases/recovery.js';
4
+ import { persistBlockedScope } from './orchestrator/phases/shared.js';
5
+ import { classifyUnexecutedDerivedPlanResumeState, getDerivedPlanView, getInteractiveRecoveryView, needsDerivedPlanNotificationFlush, } from './state-views.js';
6
+ import { saveState } from './state.js';
7
+ function getResumableBlockedSessionRole(phase) {
8
+ return phase === 'coder_plan' || phase === 'coder_plan_response' || phase === 'coder_plan_optional_response'
9
+ ? 'planner'
10
+ : 'coder';
11
+ }
12
+ export function planResumeActions(state) {
13
+ const actions = [];
14
+ let plannedState = state;
15
+ const statusAction = planStatusResumeAction(plannedState);
16
+ if (statusAction) {
17
+ actions.push(statusAction);
18
+ plannedState = applyPlannedStatusAction(plannedState, statusAction);
19
+ }
20
+ const derivedPlanResumeDisposition = classifyUnexecutedDerivedPlanResumeState(plannedState);
21
+ const derivedPlanAction = toDerivedPlanResumeAction(derivedPlanResumeDisposition);
22
+ if (derivedPlanAction) {
23
+ actions.push(derivedPlanAction);
24
+ plannedState = applyPlannedDerivedPlanAction(plannedState, derivedPlanAction);
25
+ }
26
+ if (needsDerivedPlanNotificationFlush(plannedState)) {
27
+ actions.push({ kind: 'flush_derived_plan_notifications' });
28
+ }
29
+ if (mayRecoverPendingReviewFromCleanCommittedScope(plannedState)) {
30
+ actions.push({ kind: 'recover_pending_review_from_clean_committed_scope' });
31
+ }
32
+ const interactiveRecovery = getInteractiveRecoveryView(plannedState);
33
+ if (interactiveRecovery?.active) {
34
+ if (interactiveRecovery.pendingOperatorGuidance) {
35
+ actions.push({
36
+ kind: 'process_pending_operator_guidance',
37
+ pendingTurnCount: interactiveRecovery.pendingTurnCount,
38
+ });
39
+ }
40
+ else if (interactiveRecovery.waitingForOperatorGuidance) {
41
+ actions.push({
42
+ kind: 'wait_for_operator_guidance',
43
+ sourcePhase: interactiveRecovery.sourcePhase,
44
+ });
45
+ }
46
+ actions.push({
47
+ kind: 'log_interactive_blocked_recovery',
48
+ sourcePhase: interactiveRecovery.sourcePhase,
49
+ blockedReason: interactiveRecovery.blockedReason,
50
+ recordedTurns: interactiveRecovery.turns.length,
51
+ lastHandledTurn: interactiveRecovery.handledTurn,
52
+ });
53
+ }
54
+ if (actions.length === 0 && plannedState.status === 'done') {
55
+ actions.push({ kind: 'done_noop' });
56
+ }
57
+ return actions;
58
+ }
59
+ export async function applyResumeActions(state, statePath, logger, actions) {
60
+ let nextState = state;
61
+ for (const action of actions) {
62
+ switch (action.kind) {
63
+ case 'restore_resumable_blocked_phase':
64
+ nextState = await restoreResumableBlockedPhase(nextState, statePath, logger, action);
65
+ break;
66
+ case 'normalize_stopped_status':
67
+ nextState = await normalizeStoppedStatus(nextState, statePath, logger, action);
68
+ break;
69
+ case 'promote_accepted_derived_plan':
70
+ case 'promote_pending_derived_plan_review':
71
+ nextState = await promoteUnexecutedDerivedPlan(nextState, statePath, logger, action);
72
+ break;
73
+ case 'block_rejected_abandoned_derived_plan':
74
+ nextState = await blockRejectedUnexecutedDerivedPlan(nextState, statePath, logger, action);
75
+ break;
76
+ case 'flush_derived_plan_notifications':
77
+ nextState = await flushDerivedPlanNotifications(nextState, statePath, logger);
78
+ break;
79
+ case 'recover_pending_review_from_clean_committed_scope':
80
+ nextState =
81
+ (await recoverPendingReviewFromCleanCommittedScope(nextState, statePath, logger, 'run.recovered_pending_review_on_resume')) ?? nextState;
82
+ break;
83
+ case 'log_interactive_blocked_recovery':
84
+ await logger?.event('run.resumed_interactive_blocked_recovery', {
85
+ statePath,
86
+ sourcePhase: action.sourcePhase,
87
+ blockedReason: action.blockedReason,
88
+ recordedTurns: action.recordedTurns,
89
+ lastHandledTurn: action.lastHandledTurn,
90
+ });
91
+ break;
92
+ case 'keep_blocked':
93
+ case 'wait_for_operator_guidance':
94
+ case 'process_pending_operator_guidance':
95
+ case 'done_noop':
96
+ break;
97
+ }
98
+ }
99
+ return nextState;
100
+ }
101
+ function planStatusResumeAction(state) {
102
+ if (state.status === 'blocked' && isResumableBlockedPhase(state.blockedFromPhase)) {
103
+ const sessionRole = getResumableBlockedSessionRole(state.blockedFromPhase);
104
+ const sessionHandle = sessionRole === 'planner' ? state.plannerSessionHandle : state.coderSessionHandle;
105
+ if (sessionHandle) {
106
+ return {
107
+ kind: 'restore_resumable_blocked_phase',
108
+ phase: state.blockedFromPhase,
109
+ sessionRole,
110
+ sessionHandle,
111
+ };
112
+ }
113
+ }
114
+ if (state.status === 'blocked' && state.phase === 'blocked') {
115
+ return {
116
+ kind: 'keep_blocked',
117
+ blockedFromPhase: state.blockedFromPhase,
118
+ };
119
+ }
120
+ if (state.status !== 'done' && state.status !== 'running' && state.phase !== 'blocked') {
121
+ return {
122
+ kind: 'normalize_stopped_status',
123
+ previousStatus: state.status,
124
+ };
125
+ }
126
+ return null;
127
+ }
128
+ function applyPlannedStatusAction(state, action) {
129
+ switch (action.kind) {
130
+ case 'restore_resumable_blocked_phase':
131
+ return {
132
+ ...state,
133
+ phase: action.phase,
134
+ status: 'running',
135
+ };
136
+ case 'normalize_stopped_status':
137
+ return {
138
+ ...state,
139
+ status: 'running',
140
+ };
141
+ default:
142
+ return state;
143
+ }
144
+ }
145
+ function toDerivedPlanResumeAction(disposition) {
146
+ switch (disposition.kind) {
147
+ case 'accepted':
148
+ return {
149
+ kind: 'promote_accepted_derived_plan',
150
+ phase: disposition.phase,
151
+ };
152
+ case 'pending_review':
153
+ return {
154
+ kind: 'promote_pending_derived_plan_review',
155
+ phase: disposition.phase,
156
+ };
157
+ case 'rejected':
158
+ return {
159
+ kind: 'block_rejected_abandoned_derived_plan',
160
+ blockedFromPhase: disposition.blockedFromPhase,
161
+ blocker: disposition.blocker,
162
+ };
163
+ case 'none':
164
+ return null;
165
+ }
166
+ }
167
+ function applyPlannedDerivedPlanAction(state, action) {
168
+ switch (action.kind) {
169
+ case 'promote_accepted_derived_plan':
170
+ case 'promote_pending_derived_plan_review':
171
+ return {
172
+ ...state,
173
+ phase: action.phase,
174
+ status: 'running',
175
+ blockedFromPhase: null,
176
+ };
177
+ case 'block_rejected_abandoned_derived_plan':
178
+ return {
179
+ ...state,
180
+ phase: 'blocked',
181
+ status: 'blocked',
182
+ blockedFromPhase: action.blockedFromPhase,
183
+ };
184
+ default:
185
+ return state;
186
+ }
187
+ }
188
+ function mayRecoverPendingReviewFromCleanCommittedScope(state) {
189
+ return (state.topLevelMode === 'execute' &&
190
+ state.phase === 'coder_scope' &&
191
+ Boolean(state.baseCommit) &&
192
+ state.finalCommit === null &&
193
+ state.createdCommits.length === 0);
194
+ }
195
+ async function restoreResumableBlockedPhase(state, statePath, logger, action) {
196
+ const nextState = await saveState(statePath, {
197
+ ...state,
198
+ phase: action.phase,
199
+ status: 'running',
200
+ });
201
+ await logger?.event('run.resumed_from_blocked', {
202
+ statePath,
203
+ blockedFromPhase: nextState.blockedFromPhase,
204
+ sessionRole: action.sessionRole,
205
+ sessionHandle: action.sessionHandle,
206
+ });
207
+ return nextState;
208
+ }
209
+ async function normalizeStoppedStatus(state, statePath, logger, action) {
210
+ const nextState = await saveState(statePath, {
211
+ ...state,
212
+ status: 'running',
213
+ });
214
+ await logger?.event('run.status_normalized_on_resume', {
215
+ statePath,
216
+ phase: nextState.phase,
217
+ previousStatus: action.previousStatus,
218
+ normalizedStatus: nextState.status,
219
+ });
220
+ return nextState;
221
+ }
222
+ async function promoteUnexecutedDerivedPlan(state, statePath, logger, action) {
223
+ const previousPhase = state.phase;
224
+ const previousStatus = state.status;
225
+ const derivedPlan = getDerivedPlanView(state);
226
+ const nextState = await saveState(statePath, {
227
+ ...state,
228
+ phase: action.phase,
229
+ status: 'running',
230
+ blockedFromPhase: null,
231
+ });
232
+ await logger?.event(action.kind === 'promote_accepted_derived_plan'
233
+ ? 'run.promoted_accepted_derived_plan_on_resume'
234
+ : 'run.promoted_pending_derived_plan_on_resume', {
235
+ statePath,
236
+ previousPhase,
237
+ previousStatus,
238
+ promotedPhase: nextState.phase,
239
+ derivedPlanPath: derivedPlan?.path ?? null,
240
+ derivedFromScopeNumber: derivedPlan?.parentScopeNumber ?? null,
241
+ });
242
+ return nextState;
243
+ }
244
+ async function blockRejectedUnexecutedDerivedPlan(state, statePath, logger, action) {
245
+ const previousPhase = state.phase;
246
+ const previousStatus = state.status;
247
+ const derivedPlan = getDerivedPlanView(state);
248
+ let nextState = await saveState(statePath, {
249
+ ...state,
250
+ phase: 'blocked',
251
+ status: 'blocked',
252
+ blockedFromPhase: action.blockedFromPhase,
253
+ });
254
+ nextState = await persistBlockedScope(nextState, statePath, action.blocker);
255
+ await logger?.event('run.blocked_rejected_derived_plan_on_resume', {
256
+ statePath,
257
+ previousPhase,
258
+ previousStatus,
259
+ blockedFromPhase: nextState.blockedFromPhase,
260
+ blocker: action.blocker,
261
+ derivedPlanPath: derivedPlan?.path ?? null,
262
+ derivedFromScopeNumber: derivedPlan?.parentScopeNumber ?? null,
263
+ });
264
+ return nextState;
265
+ }