@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,235 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { writeTextAtomic } from '../atomic-write.js';
4
+ import { cleanUntracked, getStagedDiff, getUnstagedDiff, getUntrackedFiles, getWorktreeStatus, resetHard, } from '../git.js';
5
+ import { validatePlanDocument } from '../plan-validation.js';
6
+ import { saveState } from '../state.js';
7
+ import { getDerivedPlanCountersView } from '../state-views.js';
8
+ import { filterWrapperOwnedWorktreeStatus, isWrapperOwnedPath, } from '../worktree-status.js';
9
+ import { flushDerivedPlanNotifications, notifyBlocked } from './notifications.js';
10
+ import { enterInteractiveBlockedRecovery, shouldNotifyInteractiveBlockedRecoveryEntry, } from './phases/recovery.js';
11
+ const MAX_SPLIT_PLANS_PER_SCOPE = 10;
12
+ function getSplitPlanArtifactPaths(state) {
13
+ return {
14
+ derivedPlanPath: join(state.runDir, `DERIVED_PLAN_SCOPE_${state.currentScopeNumber}.md`),
15
+ invalidDerivedPlanPath: join(state.runDir, `SCOPE_${state.currentScopeNumber}_INVALID_DERIVED_PLAN.md`),
16
+ discardedDiffPath: join(state.runDir, `SCOPE_${state.currentScopeNumber}_DISCARDED.diff`),
17
+ };
18
+ }
19
+ function validateSplitPlanPayload(derivedPlanMarkdown) {
20
+ const trimmedPayload = derivedPlanMarkdown.trim();
21
+ if (!trimmedPayload) {
22
+ return {
23
+ ok: false,
24
+ trimmedPayload,
25
+ errors: ['Replacement plan payload is empty.'],
26
+ };
27
+ }
28
+ const validation = validatePlanDocument(trimmedPayload);
29
+ if (!validation.ok) {
30
+ return {
31
+ ok: false,
32
+ trimmedPayload,
33
+ errors: validation.errors,
34
+ };
35
+ }
36
+ // Persist the normalized document so downstream derived-plan review sees the canonical Neal plan contract.
37
+ return {
38
+ ok: true,
39
+ persistedPlanBody: validation.normalization.applied ? validation.normalization.normalizedDocument.trim() : trimmedPayload,
40
+ };
41
+ }
42
+ function renderInvalidSplitPlanPayloadArtifact(payload, errors) {
43
+ return [
44
+ '# Invalid Derived Plan Payload',
45
+ '',
46
+ 'This artifact is diagnostic only. It is not a reviewable derived plan.',
47
+ '',
48
+ '## Validation Errors',
49
+ '',
50
+ ...errors.map((error) => `- ${error}`),
51
+ '',
52
+ '## Returned Payload',
53
+ '',
54
+ '```markdown',
55
+ payload,
56
+ '```',
57
+ '',
58
+ ].join('\n');
59
+ }
60
+ function formatInvalidSplitPlanReason(errors) {
61
+ return `split-plan recovery rejected: replacement plan payload is not a valid Neal-executable plan: ${errors.join('; ')}`;
62
+ }
63
+ function isRecoverableInvalidSplitPlanSourcePhase(sourcePhase) {
64
+ return sourcePhase === 'coder_scope' || sourcePhase === 'coder_response';
65
+ }
66
+ async function captureDiscardedScopeArtifact(cwd, artifactPath) {
67
+ const [status, stagedDiff, unstagedDiff, untrackedFiles] = await Promise.all([
68
+ getWorktreeStatus(cwd),
69
+ getStagedDiff(cwd),
70
+ getUnstagedDiff(cwd),
71
+ getUntrackedFiles(cwd),
72
+ ]);
73
+ const visibleUntrackedFiles = untrackedFiles.filter((file) => !isWrapperOwnedPath(file));
74
+ const lines = [
75
+ '# Discarded Scope WIP',
76
+ '',
77
+ '## Git Status',
78
+ '```text',
79
+ status || '(clean)',
80
+ '```',
81
+ '',
82
+ '## Staged Diff',
83
+ '```diff',
84
+ stagedDiff || '',
85
+ '```',
86
+ '',
87
+ '## Unstaged Diff',
88
+ '```diff',
89
+ unstagedDiff || '',
90
+ '```',
91
+ '',
92
+ '## Untracked Files',
93
+ visibleUntrackedFiles.length > 0 ? visibleUntrackedFiles.map((file) => `- ${file}`).join('\n') : '(none)',
94
+ ];
95
+ for (const file of visibleUntrackedFiles) {
96
+ lines.push('', `### ${file}`);
97
+ try {
98
+ const content = await readFile(join(cwd, file), 'utf8');
99
+ lines.push('```text', content, '```');
100
+ }
101
+ catch {
102
+ lines.push('(binary or unreadable file omitted from inline snapshot)');
103
+ }
104
+ }
105
+ await writeTextAtomic(artifactPath, `${lines.join('\n')}\n`);
106
+ }
107
+ async function discardScopeWorktree(state) {
108
+ if (!state.baseCommit) {
109
+ throw new Error('Cannot discard scope worktree without a baseCommit');
110
+ }
111
+ await resetHard(state.cwd, state.baseCommit);
112
+ await cleanUntracked(state.cwd, ['.neal', '.forge']);
113
+ const remainingStatus = filterWrapperOwnedWorktreeStatus(await getWorktreeStatus(state.cwd));
114
+ if (remainingStatus) {
115
+ throw new Error(`Failed to restore worktree to scope base ${state.baseCommit}:\n${remainingStatus}`);
116
+ }
117
+ }
118
+ export async function persistSplitPlanRecovery(state, statePath, args, deps) {
119
+ const derivedPlanCounters = getDerivedPlanCountersView(state);
120
+ if (derivedPlanCounters.splitPlanCountForCurrentScope >= MAX_SPLIT_PLANS_PER_SCOPE) {
121
+ const reason = `split-plan recovery rejected: scope ${state.currentScopeNumber} reached the split-plan limit (${MAX_SPLIT_PLANS_PER_SCOPE})`;
122
+ const blockedState = await saveState(statePath, {
123
+ ...state,
124
+ lastScopeMarker: 'AUTONOMY_SPLIT_PLAN',
125
+ phase: 'blocked',
126
+ status: 'blocked',
127
+ blockedFromPhase: args.sourcePhase,
128
+ interactiveBlockedRecovery: null,
129
+ });
130
+ await deps.writeExecutionArtifacts(blockedState);
131
+ const persistedState = await deps.persistBlockedScope(blockedState, statePath, reason);
132
+ return flushDerivedPlanNotifications(persistedState, statePath, args.logger, reason);
133
+ }
134
+ if (derivedPlanCounters.derivedPlanDepth >= 1) {
135
+ const reason = `split-plan recovery rejected: derived plan depth limit reached for scope ${state.currentScopeNumber}`;
136
+ const blockedState = await saveState(statePath, {
137
+ ...state,
138
+ lastScopeMarker: 'AUTONOMY_SPLIT_PLAN',
139
+ phase: 'blocked',
140
+ status: 'blocked',
141
+ blockedFromPhase: args.sourcePhase,
142
+ interactiveBlockedRecovery: null,
143
+ });
144
+ await deps.writeExecutionArtifacts(blockedState);
145
+ const persistedState = await deps.persistBlockedScope(blockedState, statePath, reason);
146
+ return flushDerivedPlanNotifications(persistedState, statePath, args.logger, reason);
147
+ }
148
+ const { derivedPlanPath, invalidDerivedPlanPath, discardedDiffPath } = getSplitPlanArtifactPaths(state);
149
+ const validation = validateSplitPlanPayload(args.derivedPlanMarkdown);
150
+ if (!validation.ok) {
151
+ const reason = formatInvalidSplitPlanReason(validation.errors);
152
+ await writeTextAtomic(invalidDerivedPlanPath, renderInvalidSplitPlanPayloadArtifact(validation.trimmedPayload, validation.errors));
153
+ await args.logger?.event('split_plan.invalid_payload', {
154
+ scopeNumber: state.currentScopeNumber,
155
+ sourcePhase: args.sourcePhase,
156
+ validationErrors: validation.errors,
157
+ invalidPayloadPath: invalidDerivedPlanPath,
158
+ resetSkipped: true,
159
+ createdCommits: args.createdCommits,
160
+ });
161
+ const recoveryCandidateState = {
162
+ ...state,
163
+ lastScopeMarker: 'AUTONOMY_SPLIT_PLAN',
164
+ blockedFromPhase: args.sourcePhase,
165
+ interactiveBlockedRecovery: null,
166
+ splitPlanBlockedNotified: false,
167
+ };
168
+ if (isRecoverableInvalidSplitPlanSourcePhase(args.sourcePhase)) {
169
+ await args.logger?.event('split_plan.invalid_payload_recovery_started', {
170
+ scopeNumber: state.currentScopeNumber,
171
+ sourcePhase: args.sourcePhase,
172
+ invalidPayloadPath: invalidDerivedPlanPath,
173
+ validationErrors: validation.errors,
174
+ createdCommits: args.createdCommits,
175
+ });
176
+ const persistedState = await enterInteractiveBlockedRecovery({
177
+ ...recoveryCandidateState,
178
+ status: 'blocked',
179
+ currentScopeProgressJustification: null,
180
+ currentScopeMeaningfulProgressVerdict: null,
181
+ }, statePath, reason, args.logger);
182
+ if (shouldNotifyInteractiveBlockedRecoveryEntry(persistedState)) {
183
+ await notifyBlocked(persistedState, reason, args.logger);
184
+ }
185
+ return persistedState;
186
+ }
187
+ const blockedState = await saveState(statePath, {
188
+ ...recoveryCandidateState,
189
+ phase: 'blocked',
190
+ status: 'blocked',
191
+ currentScopeProgressJustification: null,
192
+ currentScopeMeaningfulProgressVerdict: null,
193
+ });
194
+ await deps.writeExecutionArtifacts(blockedState);
195
+ const persistedState = await deps.persistBlockedScope(blockedState, statePath, reason);
196
+ return flushDerivedPlanNotifications(persistedState, statePath, args.logger, reason);
197
+ }
198
+ await captureDiscardedScopeArtifact(state.cwd, discardedDiffPath);
199
+ await writeTextAtomic(derivedPlanPath, `${validation.persistedPlanBody}\n`);
200
+ await discardScopeWorktree(state);
201
+ const nextState = await saveState(statePath, {
202
+ ...state,
203
+ lastScopeMarker: 'AUTONOMY_SPLIT_PLAN',
204
+ phase: 'reviewer_plan',
205
+ status: 'running',
206
+ blockedFromPhase: null,
207
+ currentScopeProgressJustification: null,
208
+ currentScopeMeaningfulProgressVerdict: null,
209
+ interactiveBlockedRecovery: null,
210
+ derivedPlanPath,
211
+ derivedFromScopeNumber: state.currentScopeNumber,
212
+ derivedPlanStatus: 'pending_review',
213
+ splitPlanStartedNotified: false,
214
+ derivedPlanAcceptedNotified: false,
215
+ splitPlanBlockedNotified: false,
216
+ derivedScopeIndex: null,
217
+ splitPlanCountForCurrentScope: state.splitPlanCountForCurrentScope + 1,
218
+ rounds: [],
219
+ recentBlocks: [],
220
+ reviewStuckArbiterCount: 0,
221
+ findings: [],
222
+ createdCommits: [],
223
+ coderRetryCount: 0,
224
+ reviewerSessionHandle: null,
225
+ });
226
+ await deps.writeExecutionArtifacts(nextState);
227
+ await args.logger?.event('split_plan.persisted', {
228
+ scopeNumber: state.currentScopeNumber,
229
+ sourcePhase: args.sourcePhase,
230
+ derivedPlanPath,
231
+ discardedDiffPath,
232
+ createdCommits: args.createdCommits,
233
+ });
234
+ return flushDerivedPlanNotifications(nextState, statePath, args.logger);
235
+ }
@@ -0,0 +1,309 @@
1
+ import { aggregateChangedFilesForAcceptedDerivedScopes, getAcceptedDerivedScopesForParentObjective, getAcceptedParentScopeForObjective, getCurrentScopeLabel, getParentScopeLabel, isExecutingDerivedPlan, shouldAdvanceTopLevelScopeNumber, shouldContinueTopLevelExecutionAfterAcceptedScope, } from '../scopes.js';
2
+ import { toResidualReviewDebt } from '../review-debt.js';
3
+ import { getDerivedPlanView, getFinalCompletionView } from '../state-views.js';
4
+ const ALREADY_SATISFIED_ACCEPTANCE_RATIONALE_PREFIX = 'Accepted top-level already-satisfied scope';
5
+ export function appendCompletedScope(state, result, details) {
6
+ const scopeLabel = details.scopeLabel ?? getCurrentScopeLabel(state);
7
+ const marker = details.marker ?? (state.lastScopeMarker ?? 'AUTONOMY_BLOCKED');
8
+ return [
9
+ ...state.completedScopes.filter((scope) => scope.number !== scopeLabel),
10
+ {
11
+ number: scopeLabel,
12
+ marker,
13
+ result,
14
+ baseCommit: state.baseCommit,
15
+ finalCommit: details.finalCommit,
16
+ summary: details.summary ?? null,
17
+ commitSubject: details.commitSubject,
18
+ changedFiles: [...(details.changedFiles ?? [])],
19
+ reviewRounds: state.rounds.length,
20
+ findings: state.findings.length,
21
+ residualReviewDebt: result === 'accepted' ? toResidualReviewDebt(state.findings) : [],
22
+ archivedReviewPath: details.archivedReviewPath,
23
+ blocker: details.blocker,
24
+ derivedFromParentScope: details.derivedFromParentScope ?? null,
25
+ replacedByDerivedPlanPath: details.replacedByDerivedPlanPath ?? null,
26
+ },
27
+ ];
28
+ }
29
+ export function shouldNotifyDerivedPlanAcceptance(previousState, nextState) {
30
+ const previousDerivedPlan = getDerivedPlanView(previousState);
31
+ const nextDerivedPlan = getDerivedPlanView(nextState);
32
+ return (previousDerivedPlan?.status !== 'accepted' &&
33
+ nextDerivedPlan?.acceptedAwaitingExecution === true &&
34
+ nextState.phase === 'awaiting_derived_plan_execution');
35
+ }
36
+ export function transitionPlanReviewWithoutOpenFindings(state, reviewMode) {
37
+ return {
38
+ ...state,
39
+ phase: reviewMode === 'derived-plan' ? 'awaiting_derived_plan_execution' : 'done',
40
+ status: reviewMode === 'plan' ? 'done' : 'running',
41
+ derivedPlanStatus: reviewMode === 'derived-plan' ? 'accepted' : state.derivedPlanStatus,
42
+ };
43
+ }
44
+ export function adoptAcceptedDerivedPlan(state) {
45
+ const derivedPlan = getDerivedPlanView(state);
46
+ if (derivedPlan?.status !== 'accepted' || derivedPlan.parentScopeNumber === null) {
47
+ return state;
48
+ }
49
+ if (state.phase !== 'awaiting_derived_plan_execution') {
50
+ throw new Error(`Cannot adopt derived plan from phase ${state.phase}`);
51
+ }
52
+ if (state.createdCommits.length > 0) {
53
+ throw new Error('Cannot adopt derived plan after derived execution has already created commits');
54
+ }
55
+ if (derivedPlan.scopeIndex !== null) {
56
+ throw new Error('Cannot adopt derived plan after derived scope execution has already started');
57
+ }
58
+ return {
59
+ ...state,
60
+ phase: 'coder_scope',
61
+ status: 'running',
62
+ derivedScopeIndex: derivedPlan.scopeIndex ?? 1,
63
+ coderSessionHandle: null,
64
+ coderSessionProtocol: null,
65
+ coderRetryCount: 0,
66
+ currentScopeProgressJustification: null,
67
+ currentScopeMeaningfulProgressVerdict: null,
68
+ finalCompletionSummary: null,
69
+ finalCompletionReviewVerdict: null,
70
+ finalCompletionResolvedAction: null,
71
+ finalCompletionContinueExecutionCapReached: false,
72
+ rounds: [],
73
+ recentBlocks: [],
74
+ reviewStuckArbiterCount: 0,
75
+ findings: [],
76
+ createdCommits: [],
77
+ blockedFromPhase: null,
78
+ };
79
+ }
80
+ export function computeNextScopeStateAfterExecuteFinalization({ state, finalCommit, completedScopes, archivedReviewPath, }) {
81
+ const derivedExecution = isExecutingDerivedPlan(state);
82
+ const derivedPlan = getDerivedPlanView(state);
83
+ const finalCompletion = getFinalCompletionView(state);
84
+ const derivedPlanCompleted = derivedExecution && state.lastScopeMarker === 'AUTONOMY_DONE';
85
+ const continueScopes = derivedExecution ? true : shouldContinueTopLevelExecutionAfterAcceptedScope(state);
86
+ const nextTopLevelScopeNumber = shouldAdvanceTopLevelScopeNumber(state)
87
+ ? state.currentScopeNumber + 1
88
+ : state.currentScopeNumber;
89
+ if (derivedExecution && derivedPlanCompleted) {
90
+ return {
91
+ ...state,
92
+ baseCommit: finalCommit,
93
+ finalCommit: null,
94
+ coderSessionHandle: null,
95
+ coderSessionProtocol: null,
96
+ currentScopeNumber: nextTopLevelScopeNumber,
97
+ lastScopeMarker: null,
98
+ currentScopeProgressJustification: null,
99
+ currentScopeMeaningfulProgressVerdict: null,
100
+ finalCompletionSummary: null,
101
+ finalCompletionReviewVerdict: null,
102
+ finalCompletionResolvedAction: null,
103
+ finalCompletionContinueExecutionCapReached: false,
104
+ derivedPlanPath: null,
105
+ derivedFromScopeNumber: null,
106
+ derivedPlanStatus: null,
107
+ derivedScopeIndex: null,
108
+ splitPlanStartedNotified: false,
109
+ derivedPlanAcceptedNotified: false,
110
+ splitPlanBlockedNotified: false,
111
+ splitPlanCountForCurrentScope: 0,
112
+ rounds: [],
113
+ recentBlocks: [],
114
+ reviewStuckArbiterCount: 0,
115
+ findings: [],
116
+ createdCommits: [],
117
+ completedScopes,
118
+ archivedReviewPath: null,
119
+ phase: 'coder_scope',
120
+ status: 'running',
121
+ };
122
+ }
123
+ if (derivedExecution) {
124
+ return {
125
+ ...state,
126
+ baseCommit: finalCommit,
127
+ finalCommit: null,
128
+ coderSessionHandle: null,
129
+ coderSessionProtocol: null,
130
+ lastScopeMarker: null,
131
+ currentScopeProgressJustification: null,
132
+ currentScopeMeaningfulProgressVerdict: null,
133
+ finalCompletionSummary: null,
134
+ finalCompletionReviewVerdict: null,
135
+ finalCompletionResolvedAction: null,
136
+ finalCompletionContinueExecutionCapReached: false,
137
+ derivedScopeIndex: (derivedPlan?.scopeIndex ?? 1) + 1,
138
+ splitPlanStartedNotified: false,
139
+ derivedPlanAcceptedNotified: false,
140
+ splitPlanBlockedNotified: false,
141
+ rounds: [],
142
+ recentBlocks: [],
143
+ reviewStuckArbiterCount: 0,
144
+ findings: [],
145
+ createdCommits: [],
146
+ completedScopes,
147
+ archivedReviewPath: null,
148
+ phase: 'coder_scope',
149
+ status: 'running',
150
+ };
151
+ }
152
+ if (continueScopes) {
153
+ return {
154
+ ...state,
155
+ baseCommit: finalCommit,
156
+ finalCommit: null,
157
+ coderSessionHandle: null,
158
+ coderSessionProtocol: null,
159
+ currentScopeNumber: nextTopLevelScopeNumber,
160
+ lastScopeMarker: null,
161
+ currentScopeProgressJustification: null,
162
+ currentScopeMeaningfulProgressVerdict: null,
163
+ finalCompletionSummary: null,
164
+ finalCompletionReviewVerdict: null,
165
+ finalCompletionResolvedAction: null,
166
+ finalCompletionContinueExecutionCapReached: false,
167
+ derivedPlanPath: null,
168
+ derivedFromScopeNumber: null,
169
+ derivedPlanStatus: null,
170
+ derivedScopeIndex: null,
171
+ splitPlanStartedNotified: false,
172
+ derivedPlanAcceptedNotified: false,
173
+ splitPlanBlockedNotified: false,
174
+ splitPlanCountForCurrentScope: 0,
175
+ rounds: [],
176
+ recentBlocks: [],
177
+ reviewStuckArbiterCount: 0,
178
+ findings: [],
179
+ createdCommits: [],
180
+ completedScopes,
181
+ archivedReviewPath: null,
182
+ phase: 'coder_scope',
183
+ status: 'running',
184
+ };
185
+ }
186
+ return {
187
+ ...state,
188
+ finalCommit,
189
+ archivedReviewPath,
190
+ completedScopes,
191
+ currentScopeProgressJustification: null,
192
+ currentScopeMeaningfulProgressVerdict: null,
193
+ finalCompletionSummary: finalCompletion?.summary ?? null,
194
+ finalCompletionReviewVerdict: finalCompletion?.reviewVerdict ?? null,
195
+ finalCompletionResolvedAction: finalCompletion?.resolvedAction ?? null,
196
+ phase: 'final_completion_review',
197
+ status: 'running',
198
+ };
199
+ }
200
+ export function appendParentCompletionFromAcceptedDerivedScopes(args) {
201
+ const derivedPlan = getDerivedPlanView(args.state);
202
+ const parentScopeLabel = getParentScopeLabel(args.state);
203
+ if (getAcceptedParentScopeForObjective(args.state, parentScopeLabel)) {
204
+ throw new Error(`Cannot append parent completion for objective ${parentScopeLabel}: accepted parent record already exists`);
205
+ }
206
+ const derivedScopes = getAcceptedDerivedScopesForParentObjective(args.state, parentScopeLabel);
207
+ const parentScopeChangedFiles = aggregateChangedFilesForAcceptedDerivedScopes(derivedScopes);
208
+ return appendCompletedScope(args.state, 'accepted', {
209
+ scopeLabel: parentScopeLabel,
210
+ finalCommit: args.finalCommit,
211
+ summary: `advance_parent accepted parent scope ${parentScopeLabel} via prior derived work; the current empty derived sub-scope was not accepted.`,
212
+ commitSubject: `Parent scope ${parentScopeLabel} complete via derived plan`,
213
+ changedFiles: parentScopeChangedFiles,
214
+ archivedReviewPath: args.archivedReviewPath,
215
+ blocker: null,
216
+ marker: 'AUTONOMY_SCOPE_DONE',
217
+ replacedByDerivedPlanPath: derivedPlan?.path ?? null,
218
+ });
219
+ }
220
+ export function computeNextScopeStateAfterParentAdvance(args) {
221
+ const nextTopLevelScopeNumber = shouldAdvanceTopLevelScopeNumber(args.state)
222
+ ? args.state.currentScopeNumber + 1
223
+ : args.state.currentScopeNumber;
224
+ return {
225
+ ...args.state,
226
+ baseCommit: args.finalCommit,
227
+ finalCommit: null,
228
+ archivedReviewPath: null,
229
+ coderSessionHandle: null,
230
+ coderSessionProtocol: null,
231
+ reviewerSessionHandle: null,
232
+ coderRetryCount: 0,
233
+ currentScopeNumber: nextTopLevelScopeNumber,
234
+ lastScopeMarker: null,
235
+ currentScopeProgressJustification: null,
236
+ currentScopeMeaningfulProgressVerdict: null,
237
+ manualGate: null,
238
+ finalCompletionSummary: null,
239
+ finalCompletionReviewVerdict: null,
240
+ finalCompletionResolvedAction: null,
241
+ finalCompletionContinueExecutionCapReached: false,
242
+ derivedPlanPath: null,
243
+ derivedFromScopeNumber: null,
244
+ derivedPlanStatus: null,
245
+ derivedScopeIndex: null,
246
+ splitPlanStartedNotified: false,
247
+ derivedPlanAcceptedNotified: false,
248
+ splitPlanBlockedNotified: false,
249
+ splitPlanCountForCurrentScope: 0,
250
+ rounds: [],
251
+ recentBlocks: [],
252
+ reviewStuckArbiterCount: 0,
253
+ findings: [],
254
+ createdCommits: [],
255
+ completedScopes: args.completedScopes,
256
+ blockedFromPhase: null,
257
+ interactiveBlockedRecovery: null,
258
+ phase: 'coder_scope',
259
+ status: 'running',
260
+ };
261
+ }
262
+ function getAcceptedScopeSummary(state) {
263
+ const meaningfulProgressVerdict = state.currentScopeMeaningfulProgressVerdict;
264
+ if (!isExecutingDerivedPlan(state) &&
265
+ meaningfulProgressVerdict?.action === 'accept' &&
266
+ meaningfulProgressVerdict.rationale.startsWith(ALREADY_SATISFIED_ACCEPTANCE_RATIONALE_PREFIX)) {
267
+ return meaningfulProgressVerdict.rationale;
268
+ }
269
+ return state.currentScopeProgressJustification?.milestoneTargeted ?? null;
270
+ }
271
+ export function appendDerivedSubScopeAndParentCompletion(args) {
272
+ const derivedExecution = isExecutingDerivedPlan(args.state);
273
+ const derivedPlan = getDerivedPlanView(args.state);
274
+ const currentScopeLabel = getCurrentScopeLabel(args.state);
275
+ const subScopeCompletedScopes = appendCompletedScope(args.state, 'accepted', {
276
+ scopeLabel: currentScopeLabel,
277
+ finalCommit: args.finalCommit,
278
+ summary: getAcceptedScopeSummary(args.state),
279
+ commitSubject: args.finalSubject,
280
+ changedFiles: args.changedFiles,
281
+ archivedReviewPath: args.archivedReviewPath,
282
+ blocker: null,
283
+ derivedFromParentScope: derivedExecution ? getParentScopeLabel(args.state) : null,
284
+ });
285
+ const derivedPlanCompleted = derivedExecution && args.state.lastScopeMarker === 'AUTONOMY_DONE';
286
+ const parentScopeChangedFiles = derivedPlanCompleted
287
+ ? [
288
+ ...new Set(subScopeCompletedScopes
289
+ .filter((scope) => scope.result === 'accepted' && scope.derivedFromParentScope === getParentScopeLabel(args.state))
290
+ .flatMap((scope) => scope.changedFiles)),
291
+ ]
292
+ : args.changedFiles;
293
+ return derivedPlanCompleted
294
+ ? appendCompletedScope({
295
+ ...args.state,
296
+ completedScopes: subScopeCompletedScopes,
297
+ }, 'accepted', {
298
+ scopeLabel: getParentScopeLabel(args.state),
299
+ finalCommit: args.finalCommit,
300
+ summary: null,
301
+ commitSubject: args.finalSubject,
302
+ changedFiles: parentScopeChangedFiles,
303
+ archivedReviewPath: args.archivedReviewPath,
304
+ blocker: null,
305
+ marker: 'AUTONOMY_SCOPE_DONE',
306
+ replacedByDerivedPlanPath: derivedPlan?.path ?? null,
307
+ })
308
+ : subScopeCompletedScopes;
309
+ }