@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,1092 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, isAbsolute, join, normalize, relative, resolve } from 'node:path';
3
+ import { writeJsonAtomic, writeTextAtomic } from './atomic-write.js';
4
+ import { buildBlockedGuidance } from './blocked-guidance.js';
5
+ import { cleanUntrackedPaths as cleanGitUntrackedPaths, getWorktreeStatus as getGitWorktreeStatus, restoreWorktreePaths as restoreGitWorktreePaths, } from './git.js';
6
+ import { createRunId } from './logger.js';
7
+ import { loadOrInitialize } from './orchestrator.js';
8
+ import { writeExecutionArtifacts } from './orchestrator/artifacts.js';
9
+ import { assertAgentConfigSupportsResume, assertAgentConfigSupportsWriterRun } from './providers/registry.js';
10
+ import { writeCurrentRunPointer } from './run-registry.js';
11
+ import { formatPublicRunStatus, getRunDisplayStatus } from './run-status.js';
12
+ import { getRunStatePath, loadState, saveState } from './state.js';
13
+ import { QUEUE_LINK_FILE, QUEUE_STATE_FILE, QUEUE_SUMMARY_FILE, getCurrentPlanAndExecuteQueuePointerPath as getStorageCurrentPlanAndExecuteQueuePointerPath, getQueuesDir, } from './storage-paths.js';
14
+ import { executeRun, withPreparedWriterRun, } from './commands/runtime.js';
15
+ import { filterAllowedDirtyPathStatus, filterWrapperOwnedWorktreeStatus, parseWorktreeStatusLine, } from './worktree-status.js';
16
+ const QUEUE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
17
+ const QUEUE_STATUSES = new Set([
18
+ 'running',
19
+ 'paused',
20
+ 'blocked',
21
+ 'failed',
22
+ 'completed',
23
+ ]);
24
+ const QUEUE_ITEM_STATUSES = new Set([
25
+ 'pending',
26
+ 'planning',
27
+ 'planned',
28
+ 'executing',
29
+ 'blocked',
30
+ 'failed',
31
+ 'paused',
32
+ 'completed',
33
+ ]);
34
+ const QUEUE_CHILD_STAGES = new Set(['planning', 'execution']);
35
+ export function getPlanAndExecuteQueuesDir(cwd) {
36
+ return getQueuesDir(resolve(cwd));
37
+ }
38
+ export function getPlanAndExecuteQueueDir(cwd, queueId) {
39
+ assertValidQueueId(queueId);
40
+ return join(getPlanAndExecuteQueuesDir(cwd), queueId);
41
+ }
42
+ export function getPlanAndExecuteQueueStatePath(cwd, queueId) {
43
+ return join(getPlanAndExecuteQueueDir(cwd, queueId), QUEUE_STATE_FILE);
44
+ }
45
+ export function getPlanAndExecuteQueueSummaryPath(cwd, queueId) {
46
+ return join(getPlanAndExecuteQueueDir(cwd, queueId), QUEUE_SUMMARY_FILE);
47
+ }
48
+ export function getCurrentPlanAndExecuteQueuePointerPath(cwd) {
49
+ return getStorageCurrentPlanAndExecuteQueuePointerPath(resolve(cwd));
50
+ }
51
+ export function getQueueChildLinkPath(runDir) {
52
+ return join(runDir, QUEUE_LINK_FILE);
53
+ }
54
+ export function toQueueStoredPath(cwd, path) {
55
+ const absoluteCwd = resolve(cwd);
56
+ const absolutePath = isAbsolute(path) ? resolve(path) : resolve(absoluteCwd, path);
57
+ const relativePath = relative(absoluteCwd, absolutePath);
58
+ if (relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath)) {
59
+ return normalize(relativePath);
60
+ }
61
+ return absolutePath;
62
+ }
63
+ export function resolveQueueStoredPath(cwd, storedPath) {
64
+ if (typeof storedPath !== 'string' || storedPath.length === 0) {
65
+ throw new Error('Invalid queue path: expected a non-empty string');
66
+ }
67
+ if (isAbsolute(storedPath)) {
68
+ const absolutePath = resolve(storedPath);
69
+ if (storedPath !== absolutePath) {
70
+ throw new Error(`Invalid queue path: absolute paths must be normalized (${storedPath})`);
71
+ }
72
+ return absolutePath;
73
+ }
74
+ const normalizedPath = normalize(storedPath);
75
+ if (storedPath !== normalizedPath ||
76
+ normalizedPath === '.' ||
77
+ normalizedPath.startsWith('..') ||
78
+ isAbsolute(normalizedPath)) {
79
+ throw new Error(`Invalid queue path: relative paths must stay within cwd (${storedPath})`);
80
+ }
81
+ const absoluteCwd = resolve(cwd);
82
+ const absolutePath = resolve(absoluteCwd, normalizedPath);
83
+ if (!isPathInside(absoluteCwd, absolutePath)) {
84
+ throw new Error(`Invalid queue path: relative paths must stay within cwd (${storedPath})`);
85
+ }
86
+ return absolutePath;
87
+ }
88
+ export async function createPlanAndExecuteQueue(args) {
89
+ const cwd = resolve(args.cwd);
90
+ if (args.planDocs.length === 0) {
91
+ throw new Error('neal run requires at least one plan file path argument');
92
+ }
93
+ const now = new Date().toISOString();
94
+ const queueId = createRunId();
95
+ const items = [];
96
+ const requestedPlans = [];
97
+ const allowedDirtyPlanPaths = [];
98
+ for (const [index, planDoc] of args.planDocs.entries()) {
99
+ const resolvedPlanPath = resolveQueueInputPath(cwd, planDoc);
100
+ await requireExistingPlanFile(resolvedPlanPath);
101
+ const requestedPlanPath = toQueueStoredPath(cwd, planDoc);
102
+ const storedResolvedPlanPath = toQueueStoredPath(cwd, resolvedPlanPath);
103
+ requestedPlans.push(requestedPlanPath);
104
+ for (const allowedPath of getAllowedDirtyPlanPaths(cwd, resolvedPlanPath)) {
105
+ if (!allowedDirtyPlanPaths.includes(allowedPath)) {
106
+ allowedDirtyPlanPaths.push(allowedPath);
107
+ }
108
+ }
109
+ items.push({
110
+ index,
111
+ requestedPlanPath,
112
+ resolvedPlanPath: storedResolvedPlanPath,
113
+ status: 'pending',
114
+ planningRunId: null,
115
+ planningStatePath: null,
116
+ acceptedPlanPath: null,
117
+ executionRunId: null,
118
+ executionStatePath: null,
119
+ activeStage: null,
120
+ startedAt: null,
121
+ completedAt: null,
122
+ stopReason: null,
123
+ });
124
+ }
125
+ return savePlanAndExecuteQueueState({
126
+ version: 1,
127
+ queueId,
128
+ cwd,
129
+ status: 'running',
130
+ currentIndex: 0,
131
+ requestedPlans,
132
+ allowedDirtyPlanPaths,
133
+ items,
134
+ createdAt: now,
135
+ updatedAt: now,
136
+ completedAt: null,
137
+ stopReason: null,
138
+ });
139
+ }
140
+ export async function runPlanAndExecuteQueue(args) {
141
+ assertAgentConfigSupportsWriterRun(args.agentConfig, { context: 'plan-and-execute queue' });
142
+ const queue = await createPlanAndExecuteQueue({
143
+ cwd: args.cwd,
144
+ planDocs: args.planDocs,
145
+ });
146
+ return continuePlanAndExecuteQueue({
147
+ state: queue,
148
+ agentConfig: args.agentConfig,
149
+ squashOnCompletion: args.squashOnCompletion ?? true,
150
+ unattended: args.unattended ?? false,
151
+ deps: args.deps,
152
+ });
153
+ }
154
+ export async function continuePlanAndExecuteQueue(args) {
155
+ assertAgentConfigSupportsWriterRun(args.agentConfig, { context: 'plan-and-execute queue continuation' });
156
+ const deps = resolveQueueRunnerDeps(args.deps);
157
+ let state = args.state;
158
+ while (state.status === 'running' && state.currentIndex < state.items.length) {
159
+ const item = state.items[state.currentIndex];
160
+ if (!item) {
161
+ break;
162
+ }
163
+ if (item.status === 'pending') {
164
+ state = await runQueueChildStage(state, item.index, 'planning', args.agentConfig, args.squashOnCompletion ?? true, args.unattended ?? false, deps);
165
+ if (state.status !== 'running') {
166
+ return state;
167
+ }
168
+ }
169
+ const plannedItem = state.items[state.currentIndex];
170
+ if (!plannedItem) {
171
+ break;
172
+ }
173
+ if (plannedItem.status === 'planned') {
174
+ state = await runQueueChildStage(state, plannedItem.index, 'execution', args.agentConfig, args.squashOnCompletion ?? true, args.unattended ?? false, deps);
175
+ if (state.status !== 'running') {
176
+ return state;
177
+ }
178
+ }
179
+ const completedItem = state.items[state.currentIndex];
180
+ if (!completedItem || completedItem.status !== 'completed') {
181
+ return state;
182
+ }
183
+ state = await advanceQueuePastCompletedItem(state, completedItem.index);
184
+ }
185
+ if (state.status === 'running' && state.currentIndex >= state.items.length) {
186
+ const now = new Date().toISOString();
187
+ state = await savePlanAndExecuteQueueState({
188
+ ...state,
189
+ status: 'completed',
190
+ completedAt: now,
191
+ stopReason: null,
192
+ });
193
+ }
194
+ return state;
195
+ }
196
+ export async function continuePlanAndExecuteQueueFromChildRun(args) {
197
+ assertAgentConfigSupportsResume(args.agentConfig, args.childResult.finalState, {
198
+ context: 'plan-and-execute queue continuation from child run',
199
+ });
200
+ const link = await readQueueChildLink(args.childResult.finalState.runDir);
201
+ if (!link) {
202
+ return null;
203
+ }
204
+ const loadedState = await loadPlanAndExecuteQueueState(link.queueStatePath, args.childResult.finalState.cwd);
205
+ const state = validateLinkedQueueChild(loadedState, link, args.childResult.finalState.runDir);
206
+ if (state === null) {
207
+ return loadedState;
208
+ }
209
+ const deps = resolveQueueRunnerDeps(args.deps);
210
+ const classification = classifyChildResult(link.stage, args.childResult);
211
+ if (classification.kind === 'stopped') {
212
+ await persistQueueStoppedChildState(args.childResult, classification);
213
+ return stopQueueAtItem(state, link.itemIndex, classification.queueStatus, classification.itemStatus, classification.stopReason);
214
+ }
215
+ const advancedState = link.stage === 'planning'
216
+ ? await completePlanningStage(state, link.itemIndex, args.childResult.finalState, deps)
217
+ : await completeExecutionStage(state, link.itemIndex);
218
+ return continuePlanAndExecuteQueue({
219
+ state: advancedState,
220
+ agentConfig: args.agentConfig,
221
+ // Carry the squash preference forward across the cross-process
222
+ // queue-resume handoff the same way unattended is below: the resumed
223
+ // child's persisted run state wins when the caller supplies nothing.
224
+ squashOnCompletion: args.squashOnCompletion ?? args.childResult.finalState.autoSquashOnCompletion,
225
+ // Carry unattended forward across the cross-process queue-resume handoff.
226
+ // The resumed child's persisted finalState is the authoritative source (it
227
+ // was initialized with the value the original `neal run` resolved), so
228
+ // subsequent queue children do not silently revert to attended mode. An
229
+ // explicit caller-supplied value still wins.
230
+ unattended: args.unattended ?? args.childResult.finalState.unattended,
231
+ deps: args.deps,
232
+ });
233
+ }
234
+ export async function loadPlanAndExecuteQueueState(queueStatePath, cwd = process.cwd()) {
235
+ const absoluteQueueStatePath = resolveQueueStoredPath(cwd, queueStatePath);
236
+ const content = await readFile(absoluteQueueStatePath, 'utf8');
237
+ const parsed = JSON.parse(content);
238
+ const state = parsePlanAndExecuteQueueState(parsed);
239
+ const expectedStatePath = getPlanAndExecuteQueueStatePath(state.cwd, state.queueId);
240
+ if (!samePath(absoluteQueueStatePath, expectedStatePath)) {
241
+ throw new Error(`Invalid queue state: path ${absoluteQueueStatePath} does not match queue ${state.queueId} at ${expectedStatePath}`);
242
+ }
243
+ return state;
244
+ }
245
+ export async function savePlanAndExecuteQueueState(state) {
246
+ const nextState = parsePlanAndExecuteQueueState({
247
+ ...state,
248
+ requestedPlans: [...state.requestedPlans],
249
+ allowedDirtyPlanPaths: [...state.allowedDirtyPlanPaths],
250
+ items: state.items.map((item) => ({ ...item })),
251
+ updatedAt: new Date().toISOString(),
252
+ });
253
+ const statePath = getPlanAndExecuteQueueStatePath(nextState.cwd, nextState.queueId);
254
+ await writeJsonAtomic(statePath, nextState);
255
+ await writeCurrentPlanAndExecuteQueuePointer(nextState);
256
+ await writeQueueSummary(nextState);
257
+ return nextState;
258
+ }
259
+ export async function readCurrentPlanAndExecuteQueuePointer(cwd) {
260
+ const pointerPath = getCurrentPlanAndExecuteQueuePointerPath(cwd);
261
+ let content;
262
+ try {
263
+ content = await readFile(pointerPath, 'utf8');
264
+ }
265
+ catch (error) {
266
+ if (isNotFoundError(error)) {
267
+ return null;
268
+ }
269
+ throw error;
270
+ }
271
+ return parseCurrentPlanAndExecuteQueuePointer(JSON.parse(content), resolve(cwd));
272
+ }
273
+ export async function writeQueueChildLink(args) {
274
+ const link = parseQueueChildLink({
275
+ version: 1,
276
+ queueId: args.queueId,
277
+ queueStatePath: args.queueStatePath,
278
+ itemIndex: args.itemIndex,
279
+ stage: args.stage,
280
+ createdAt: args.createdAt ?? new Date().toISOString(),
281
+ });
282
+ const linkPath = getQueueChildLinkPath(args.runDir);
283
+ await writeJsonAtomic(linkPath, link);
284
+ return link;
285
+ }
286
+ export async function readQueueChildLink(runDir) {
287
+ const linkPath = getQueueChildLinkPath(runDir);
288
+ let content;
289
+ try {
290
+ content = await readFile(linkPath, 'utf8');
291
+ }
292
+ catch (error) {
293
+ if (isNotFoundError(error)) {
294
+ return null;
295
+ }
296
+ throw error;
297
+ }
298
+ return parseQueueChildLink(JSON.parse(content));
299
+ }
300
+ export async function inspectQueueChildResumeEvidence(args) {
301
+ let link;
302
+ try {
303
+ link = await readQueueChildLink(args.runDir);
304
+ }
305
+ catch (error) {
306
+ return {
307
+ kind: 'unreadable',
308
+ reason: `Queue child link could not be read: ${formatQueueEvidenceError(error)}`,
309
+ };
310
+ }
311
+ if (!link) {
312
+ return { kind: 'not_linked' };
313
+ }
314
+ let state;
315
+ try {
316
+ state = await loadPlanAndExecuteQueueState(link.queueStatePath, args.cwd);
317
+ }
318
+ catch (error) {
319
+ return {
320
+ kind: 'unreadable',
321
+ reason: `Queue state for linked child could not be read: ${formatQueueEvidenceError(error)}`,
322
+ queueId: link.queueId,
323
+ itemIndex: link.itemIndex,
324
+ stage: link.stage,
325
+ };
326
+ }
327
+ const classification = classifyLinkedQueueChild(state, link, args.runDir);
328
+ if (classification.kind === 'active') {
329
+ return {
330
+ kind: 'active_child',
331
+ queueId: state.queueId,
332
+ itemIndex: link.itemIndex,
333
+ stage: link.stage,
334
+ queueStatus: state.status,
335
+ itemStatus: classification.item.status,
336
+ };
337
+ }
338
+ return {
339
+ kind: classification.kind,
340
+ reason: classification.reason,
341
+ queueId: link.queueId,
342
+ itemIndex: link.itemIndex,
343
+ stage: link.stage,
344
+ };
345
+ }
346
+ function resolveQueueRunnerDeps(deps) {
347
+ return {
348
+ cleanUntrackedPaths: deps?.cleanUntrackedPaths ?? cleanGitUntrackedPaths,
349
+ getWorktreeStatus: deps?.getWorktreeStatus ?? getGitWorktreeStatus,
350
+ restoreWorktreePaths: deps?.restoreWorktreePaths ?? restoreGitWorktreePaths,
351
+ runFreshChild: deps?.runFreshChild ?? runFreshPlanAndExecuteChild,
352
+ };
353
+ }
354
+ async function runQueueChildStage(state, itemIndex, stage, agentConfig, squashOnCompletion, unattended, deps) {
355
+ try {
356
+ await assertQueueWorktreeReady(state, deps);
357
+ }
358
+ catch (error) {
359
+ const message = error instanceof Error ? error.message : String(error);
360
+ await stopQueueAtItem(state, itemIndex, 'failed', 'failed', message);
361
+ throw error;
362
+ }
363
+ const item = getQueueItemOrThrow(state, itemIndex);
364
+ const planDoc = getQueueStagePlanDoc(state, item, stage);
365
+ let activeState = state;
366
+ let initialized = false;
367
+ let result;
368
+ try {
369
+ result = await deps.runFreshChild({
370
+ queueState: state,
371
+ item,
372
+ stage,
373
+ planDoc,
374
+ agentConfig,
375
+ squashOnCompletion,
376
+ unattended,
377
+ }, async (child) => {
378
+ initialized = true;
379
+ activeState = await recordQueueChildInitialized(activeState, itemIndex, stage, child);
380
+ });
381
+ if (!initialized) {
382
+ throw new Error(`Queue ${stage} child did not report initialization`);
383
+ }
384
+ }
385
+ catch (error) {
386
+ const message = error instanceof Error ? error.message : String(error);
387
+ await stopQueueAtItem(activeState, itemIndex, 'failed', 'failed', `${stage} child failed before completion: ${message}`);
388
+ throw error;
389
+ }
390
+ const classification = classifyChildResult(stage, result);
391
+ if (classification.kind === 'stopped') {
392
+ await persistQueueStoppedChildState(result, classification);
393
+ return stopQueueAtItem(activeState, itemIndex, classification.queueStatus, classification.itemStatus, classification.stopReason);
394
+ }
395
+ if (stage === 'planning') {
396
+ return completePlanningStage(activeState, itemIndex, result.finalState, deps);
397
+ }
398
+ return completeExecutionStage(activeState, itemIndex);
399
+ }
400
+ function validateLinkedQueueChild(state, link, childRunDir) {
401
+ const classification = classifyLinkedQueueChild(state, link, childRunDir);
402
+ if (classification.kind === 'consumed_child') {
403
+ return null;
404
+ }
405
+ if (classification.kind !== 'active') {
406
+ throw new Error(classification.reason);
407
+ }
408
+ const item = classification.item;
409
+ return {
410
+ ...state,
411
+ status: 'running',
412
+ stopReason: null,
413
+ items: replaceQueueItem(state.items, link.itemIndex, {
414
+ ...item,
415
+ status: link.stage === 'planning' ? 'planning' : 'executing',
416
+ stopReason: null,
417
+ }),
418
+ };
419
+ }
420
+ function classifyLinkedQueueChild(state, link, childRunDir) {
421
+ if (state.queueId !== link.queueId) {
422
+ return {
423
+ kind: 'mismatched_child',
424
+ reason: `Invalid queue child link: queue ${link.queueId} does not match state ${state.queueId}`,
425
+ };
426
+ }
427
+ const item = state.items[link.itemIndex];
428
+ if (!item) {
429
+ return {
430
+ kind: 'stale_child',
431
+ reason: `Invalid queue child link: missing queue item ${link.itemIndex}`,
432
+ };
433
+ }
434
+ const childRunId = basename(childRunDir);
435
+ const expectedRunId = getQueueItemRunIdForStage(item, link.stage);
436
+ if (expectedRunId !== childRunId) {
437
+ return {
438
+ kind: 'mismatched_child',
439
+ reason: `Invalid queue child link: ${link.stage} run ${childRunId} does not match queue item ${link.itemIndex}`,
440
+ };
441
+ }
442
+ if (isLinkedChildAlreadyConsumed(state, item, link.stage)) {
443
+ return {
444
+ kind: 'consumed_child',
445
+ reason: `Queue ${link.stage} child ${childRunId} for item ${link.itemIndex + 1} has already been consumed: the queue recorded this run's outcome and moved on, so the run can no longer be resumed through its queue.`,
446
+ };
447
+ }
448
+ if (state.currentIndex !== link.itemIndex) {
449
+ return {
450
+ kind: 'stale_child',
451
+ reason: `Invalid queue child link: queue currentIndex ${state.currentIndex} does not match item ${link.itemIndex}`,
452
+ };
453
+ }
454
+ if (item.activeStage !== link.stage) {
455
+ return {
456
+ kind: 'mismatched_stage',
457
+ reason: `Invalid queue child link: queue item ${link.itemIndex} active stage ${String(item.activeStage)} does not match ${link.stage}`,
458
+ };
459
+ }
460
+ return { kind: 'active', item };
461
+ }
462
+ function getQueueItemRunIdForStage(item, stage) {
463
+ return stage === 'planning' ? item.planningRunId : item.executionRunId;
464
+ }
465
+ function isLinkedChildAlreadyConsumed(state, item, stage) {
466
+ if (item.index < state.currentIndex) {
467
+ return true;
468
+ }
469
+ if (stage === 'planning') {
470
+ return (item.status === 'planned' ||
471
+ item.status === 'executing' ||
472
+ item.status === 'completed' ||
473
+ item.acceptedPlanPath !== null ||
474
+ item.executionRunId !== null);
475
+ }
476
+ return item.status === 'completed';
477
+ }
478
+ async function runFreshPlanAndExecuteChild(args, onInitialized) {
479
+ const cwd = args.queueState.cwd;
480
+ const topLevelMode = getTopLevelModeForStage(args.stage);
481
+ assertAgentConfigSupportsWriterRun(args.agentConfig, { context: `${args.stage} queue child` });
482
+ return withPreparedWriterRun({
483
+ cwd,
484
+ topLevelMode,
485
+ getLockPlanDoc: () => args.planDoc,
486
+ }, async (prepared, markInitialized) => {
487
+ const planDoc = resolveQueueInputPath(cwd, args.planDoc);
488
+ await requireExistingPlanFile(planDoc);
489
+ const loaded = await loadOrInitialize(planDoc, cwd, args.agentConfig, undefined, topLevelMode, {
490
+ allowedDirtyPaths: args.stage === 'execution' ? [planDoc] : [],
491
+ runDir: prepared.runDir,
492
+ unattended: args.unattended,
493
+ // Seed the queue's resolved squash preference onto the child run
494
+ // state (unstaged: planning children persist it too so a resumed
495
+ // planning child carries it back into queue continuation). Once
496
+ // initialized, the run state is the single source of truth for
497
+ // this run — `neal resume` reads it back from there.
498
+ autoSquashOnCompletion: args.squashOnCompletion,
499
+ });
500
+ markInitialized();
501
+ assertAgentConfigSupportsWriterRun(loaded.state.agentConfig, { context: `${args.stage} queue child` });
502
+ await onInitialized({
503
+ runId: basename(loaded.state.runDir),
504
+ runDir: loaded.state.runDir,
505
+ runStatePath: getRunStatePath(loaded.state.runDir),
506
+ statePath: loaded.statePath,
507
+ planDoc: loaded.state.planDoc,
508
+ });
509
+ return executeRun(loaded.state, loaded.statePath, loaded.logger, {
510
+ autoSquashOnCompletion: args.stage === 'execution' && args.squashOnCompletion,
511
+ unattended: args.unattended,
512
+ });
513
+ });
514
+ }
515
+ function getTopLevelModeForStage(stage) {
516
+ return stage === 'planning' ? 'plan' : 'execute';
517
+ }
518
+ function getQueueStagePlanDoc(state, item, stage) {
519
+ if (stage === 'planning') {
520
+ return resolveQueueStoredPath(state.cwd, item.resolvedPlanPath);
521
+ }
522
+ if (!item.acceptedPlanPath) {
523
+ throw new Error(`Cannot start queue item ${item.index} execution before planning has accepted a plan`);
524
+ }
525
+ return resolveQueueStoredPath(state.cwd, item.acceptedPlanPath);
526
+ }
527
+ function buildInitializedQueueItem(cwd, item, stage, child) {
528
+ const childStatePath = toQueueStoredPath(cwd, child.runStatePath);
529
+ const baseItem = {
530
+ ...item,
531
+ activeStage: stage,
532
+ startedAt: item.startedAt ?? new Date().toISOString(),
533
+ stopReason: null,
534
+ };
535
+ if (stage === 'planning') {
536
+ return {
537
+ ...baseItem,
538
+ status: 'planning',
539
+ planningRunId: child.runId,
540
+ planningStatePath: childStatePath,
541
+ };
542
+ }
543
+ return {
544
+ ...baseItem,
545
+ status: 'executing',
546
+ executionRunId: child.runId,
547
+ executionStatePath: childStatePath,
548
+ };
549
+ }
550
+ async function recordQueueChildInitialized(state, itemIndex, stage, child) {
551
+ const item = getQueueItemOrThrow(state, itemIndex);
552
+ const nextItem = buildInitializedQueueItem(state.cwd, item, stage, child);
553
+ const nextState = await savePlanAndExecuteQueueState({
554
+ ...state,
555
+ status: 'running',
556
+ currentIndex: itemIndex,
557
+ items: replaceQueueItem(state.items, itemIndex, nextItem),
558
+ stopReason: null,
559
+ });
560
+ await writeQueueChildLink({
561
+ runDir: child.runDir,
562
+ queueId: nextState.queueId,
563
+ queueStatePath: toQueueStoredPath(nextState.cwd, getPlanAndExecuteQueueStatePath(nextState.cwd, nextState.queueId)),
564
+ itemIndex,
565
+ stage,
566
+ });
567
+ await writeCurrentRunPointer(await loadState(child.runStatePath));
568
+ return nextState;
569
+ }
570
+ async function completePlanningStage(state, itemIndex, finalState, deps) {
571
+ const item = getQueueItemOrThrow(state, itemIndex);
572
+ const acceptedPlanPath = toQueueStoredPath(state.cwd, finalState.planDoc);
573
+ const allowedDirtyPlanPaths = withAllowedDirtyPlanPaths(state.allowedDirtyPlanPaths, getAllowedDirtyPlanPaths(state.cwd, finalState.planDoc));
574
+ await restoreQueuePlanningSideEffects({ ...state, allowedDirtyPlanPaths }, deps);
575
+ return savePlanAndExecuteQueueState({
576
+ ...state,
577
+ status: 'running',
578
+ allowedDirtyPlanPaths,
579
+ stopReason: null,
580
+ items: replaceQueueItem(state.items, itemIndex, {
581
+ ...item,
582
+ status: 'planned',
583
+ acceptedPlanPath,
584
+ activeStage: null,
585
+ stopReason: null,
586
+ }),
587
+ });
588
+ }
589
+ function getAllowedDirtyPlanPaths(cwd, planPath) {
590
+ const storedPath = toQueueStoredPath(cwd, planPath);
591
+ if (!isAbsolute(storedPath)) {
592
+ return [storedPath];
593
+ }
594
+ const localPlanSurrogate = basename(storedPath);
595
+ return localPlanSurrogate ? [storedPath, localPlanSurrogate] : [storedPath];
596
+ }
597
+ function withAllowedDirtyPlanPaths(paths, planPaths) {
598
+ const nextPaths = [...paths];
599
+ for (const planPath of planPaths) {
600
+ if (!nextPaths.includes(planPath)) {
601
+ nextPaths.push(planPath);
602
+ }
603
+ }
604
+ return nextPaths;
605
+ }
606
+ async function completeExecutionStage(state, itemIndex) {
607
+ const item = getQueueItemOrThrow(state, itemIndex);
608
+ return savePlanAndExecuteQueueState({
609
+ ...state,
610
+ status: 'running',
611
+ stopReason: null,
612
+ items: replaceQueueItem(state.items, itemIndex, {
613
+ ...item,
614
+ status: 'completed',
615
+ activeStage: null,
616
+ completedAt: new Date().toISOString(),
617
+ stopReason: null,
618
+ }),
619
+ });
620
+ }
621
+ async function advanceQueuePastCompletedItem(state, itemIndex) {
622
+ const nextIndex = itemIndex + 1;
623
+ return savePlanAndExecuteQueueState({
624
+ ...state,
625
+ currentIndex: nextIndex,
626
+ });
627
+ }
628
+ async function stopQueueAtItem(state, itemIndex, queueStatus, itemStatus, stopReason) {
629
+ const item = getQueueItemOrThrow(state, itemIndex);
630
+ return savePlanAndExecuteQueueState({
631
+ ...state,
632
+ status: queueStatus,
633
+ currentIndex: itemIndex,
634
+ stopReason,
635
+ items: replaceQueueItem(state.items, itemIndex, {
636
+ ...item,
637
+ status: itemStatus,
638
+ stopReason,
639
+ }),
640
+ });
641
+ }
642
+ function classifyChildResult(stage, result) {
643
+ if (result.finalState.status === 'done') {
644
+ return { kind: 'succeeded' };
645
+ }
646
+ if (result.waitingForOperatorGuidance) {
647
+ return classifyStoppedChild(stage, 'blocked', 'blocked', formatOperatorGuidanceChildStop(result));
648
+ }
649
+ if (isManualGateWait(result)) {
650
+ return classifyStoppedChild(stage, 'paused', 'paused', `is waiting for manual gate ${result.finalState.manualGate.id}`);
651
+ }
652
+ if (result.finalState.status === 'blocked') {
653
+ return classifyStoppedChild(stage, 'blocked', 'blocked', 'blocked');
654
+ }
655
+ if (result.finalState.status === 'failed') {
656
+ return classifyStoppedChild(stage, 'failed', 'failed', 'failed');
657
+ }
658
+ if (result.stopRequestedAfterScope) {
659
+ return classifyStoppedChild(stage, 'paused', 'paused', 'paused after stop request');
660
+ }
661
+ if (result.finalState.status === 'paused') {
662
+ return classifyStoppedChild(stage, 'paused', 'paused', 'paused');
663
+ }
664
+ return classifyStoppedChild(stage, 'paused', 'paused', `returned still-running state: status ${result.finalState.status}, phase ${result.finalState.phase}`);
665
+ }
666
+ function formatOperatorGuidanceChildStop(result) {
667
+ const state = result.finalState;
668
+ const runId = basename(state.runDir);
669
+ const childStatePath = toQueueStoredPath(state.cwd, getRunStatePath(state.runDir));
670
+ const displayStatus = getRunDisplayStatus(state);
671
+ const publicStatus = displayStatus.waitingForOperatorGuidance
672
+ ? formatPublicRunStatus(displayStatus)
673
+ : 'waiting_for_guidance';
674
+ const guidance = buildBlockedGuidance({
675
+ state,
676
+ runId,
677
+ waitingForOperatorGuidance: true,
678
+ });
679
+ const summary = guidance?.summary ?? 'Neal is waiting for operator guidance before this child can continue.';
680
+ const nextAction = guidance?.options[0]?.command ?? `neal status --run ${runId}`;
681
+ return (`${runId} (${childStatePath}) is waiting for operator guidance (public status: ${publicStatus}). ` +
682
+ `${summary} Next: ${nextAction}`);
683
+ }
684
+ function classifyStoppedChild(stage, queueStatus, itemStatus, reason) {
685
+ return {
686
+ kind: 'stopped',
687
+ queueStatus,
688
+ itemStatus,
689
+ stopReason: `${stage} child ${reason}`,
690
+ };
691
+ }
692
+ async function persistQueueStoppedChildState(result, classification) {
693
+ if (classification.itemStatus !== 'paused' || result.finalState.status !== 'running') {
694
+ return;
695
+ }
696
+ if (isManualGateWait(result)) {
697
+ return;
698
+ }
699
+ const pausedState = await saveState(getRunStatePath(result.finalState.runDir), {
700
+ ...result.finalState,
701
+ status: 'paused',
702
+ });
703
+ await writeExecutionArtifacts(pausedState);
704
+ result.finalState = pausedState;
705
+ }
706
+ function isManualGateWait(result) {
707
+ return (result.waitingForManualGate === true ||
708
+ (result.finalState.phase === 'manual_gate' && result.finalState.manualGate !== null));
709
+ }
710
+ async function assertQueueWorktreeReady(state, deps) {
711
+ const statusOutput = filterWrapperOwnedWorktreeStatus(await deps.getWorktreeStatus(state.cwd));
712
+ const disallowedStatusOutput = filterAllowedDirtyPlanStatus(state, statusOutput);
713
+ if (disallowedStatusOutput) {
714
+ throw new Error([
715
+ 'Cannot continue neal run with a dirty worktree:',
716
+ disallowedStatusOutput,
717
+ '',
718
+ 'Only queued plan files may be dirty between queue stages. Commit, stash, or discard unrelated changes before continuing the queue.',
719
+ ].join('\n'));
720
+ }
721
+ }
722
+ async function restoreQueuePlanningSideEffects(state, deps) {
723
+ const statusOutput = filterWrapperOwnedWorktreeStatus(await deps.getWorktreeStatus(state.cwd));
724
+ const disallowedStatusOutput = filterAllowedDirtyPlanStatus(state, statusOutput);
725
+ if (!disallowedStatusOutput) {
726
+ return;
727
+ }
728
+ const { trackedPaths, untrackedPaths } = splitQueueSideEffectPaths(disallowedStatusOutput);
729
+ await deps.restoreWorktreePaths(state.cwd, trackedPaths);
730
+ await deps.cleanUntrackedPaths(state.cwd, untrackedPaths);
731
+ const remainingStatusOutput = filterAllowedDirtyPlanStatus(state, filterWrapperOwnedWorktreeStatus(await deps.getWorktreeStatus(state.cwd)));
732
+ if (remainingStatusOutput) {
733
+ throw new Error([
734
+ 'Cannot continue neal run after cleaning plan-stage side effects:',
735
+ remainingStatusOutput,
736
+ '',
737
+ 'Only queued plan files may be dirty between queue stages. Commit, stash, or discard unrelated changes before continuing the queue.',
738
+ ].join('\n'));
739
+ }
740
+ }
741
+ function splitQueueSideEffectPaths(statusOutput) {
742
+ const trackedPaths = new Set();
743
+ const untrackedPaths = new Set();
744
+ for (const line of statusOutput.split('\n').map((entry) => entry.trimEnd()).filter(Boolean)) {
745
+ const entry = parseWorktreeStatusLine(line);
746
+ if (!entry) {
747
+ continue;
748
+ }
749
+ const target = line.startsWith('??') ? untrackedPaths : trackedPaths;
750
+ for (const path of entry.paths) {
751
+ target.add(path);
752
+ }
753
+ }
754
+ return {
755
+ trackedPaths: [...trackedPaths],
756
+ untrackedPaths: [...untrackedPaths],
757
+ };
758
+ }
759
+ function filterAllowedDirtyPlanStatus(state, statusOutput) {
760
+ return filterAllowedDirtyPathStatus(state.cwd, statusOutput, state.allowedDirtyPlanPaths);
761
+ }
762
+ function getQueueItemOrThrow(state, itemIndex) {
763
+ const item = state.items[itemIndex];
764
+ if (!item) {
765
+ throw new Error(`Invalid queue state: missing item ${itemIndex}`);
766
+ }
767
+ return item;
768
+ }
769
+ function replaceQueueItem(items, itemIndex, item) {
770
+ return items.map((existing, index) => (index === itemIndex ? item : existing));
771
+ }
772
+ async function writeCurrentPlanAndExecuteQueuePointer(state) {
773
+ const pointerPath = getCurrentPlanAndExecuteQueuePointerPath(state.cwd);
774
+ const pointer = parseCurrentPlanAndExecuteQueuePointer({
775
+ version: 1,
776
+ queueId: state.queueId,
777
+ queueStatePath: toQueueStoredPath(state.cwd, getPlanAndExecuteQueueStatePath(state.cwd, state.queueId)),
778
+ status: state.status,
779
+ currentIndex: state.currentIndex,
780
+ activeStage: getActiveQueueStage(state),
781
+ activeRunId: getActiveQueueRunId(state),
782
+ updatedAt: state.updatedAt,
783
+ }, state.cwd);
784
+ await writeJsonAtomic(pointerPath, pointer);
785
+ }
786
+ async function writeQueueSummary(state) {
787
+ const summaryPath = getPlanAndExecuteQueueSummaryPath(state.cwd, state.queueId);
788
+ await writeTextAtomic(summaryPath, formatQueueSummary(state));
789
+ }
790
+ function formatQueueSummary(state) {
791
+ const lines = [
792
+ '# Neal Plan-And-Execute Queue Summary',
793
+ '',
794
+ `- Queue: ${state.queueId}`,
795
+ `- Status: ${state.status}`,
796
+ `- Cwd: ${state.cwd}`,
797
+ `- Current item: ${formatCurrentQueueItem(state)}`,
798
+ `- Created: ${state.createdAt}`,
799
+ `- Updated: ${state.updatedAt}`,
800
+ `- Completed: ${state.completedAt ?? 'not completed'}`,
801
+ `- Terminal outcome: ${formatQueueTerminalOutcome(state)}`,
802
+ `- Stop reason: ${state.stopReason ?? 'none'}`,
803
+ '',
804
+ '## Items',
805
+ '',
806
+ ];
807
+ for (const item of state.items) {
808
+ lines.push(`### Item ${item.index + 1}: ${item.requestedPlanPath}`, '', `- Status: ${item.status}`, `- Active stage: ${item.activeStage ?? 'none'}`, `- Planning run: ${formatRunReference(item.planningRunId, item.planningStatePath)}`, `- Accepted plan: ${item.acceptedPlanPath ?? 'none'}`, `- Execution run: ${formatRunReference(item.executionRunId, item.executionStatePath)}`, `- Started: ${item.startedAt ?? 'not started'}`, `- Completed: ${item.completedAt ?? 'not completed'}`, `- Terminal outcome: ${formatItemTerminalOutcome(item)}`, `- Stop reason: ${item.stopReason ?? 'none'}`, '');
809
+ }
810
+ return lines.join('\n');
811
+ }
812
+ function formatCurrentQueueItem(state) {
813
+ if (state.currentIndex >= state.items.length) {
814
+ return `${state.items.length} of ${state.items.length}`;
815
+ }
816
+ return `${state.currentIndex + 1} of ${state.items.length}`;
817
+ }
818
+ function formatQueueTerminalOutcome(state) {
819
+ return state.status === 'running' ? 'not terminal' : state.status;
820
+ }
821
+ function formatItemTerminalOutcome(item) {
822
+ switch (item.status) {
823
+ case 'completed':
824
+ case 'blocked':
825
+ case 'failed':
826
+ case 'paused':
827
+ return item.status;
828
+ default:
829
+ return 'not terminal';
830
+ }
831
+ }
832
+ function formatRunReference(runId, statePath) {
833
+ if (!runId) {
834
+ return 'none';
835
+ }
836
+ return statePath ? `${runId} (${statePath})` : runId;
837
+ }
838
+ async function requireExistingPlanFile(planPath) {
839
+ let planStat;
840
+ try {
841
+ planStat = await stat(planPath);
842
+ }
843
+ catch (error) {
844
+ if (isNotFoundError(error)) {
845
+ throw new Error(`Plan file does not exist: ${planPath}`);
846
+ }
847
+ throw error;
848
+ }
849
+ if (!planStat.isFile()) {
850
+ throw new Error(`Plan path is not a file: ${planPath}`);
851
+ }
852
+ }
853
+ function parsePlanAndExecuteQueueState(value) {
854
+ if (!value || typeof value !== 'object') {
855
+ throw new Error('Invalid queue state: expected object');
856
+ }
857
+ const state = value;
858
+ if (state.version !== 1) {
859
+ throw new Error(`Invalid queue state version: ${String(state.version)}`);
860
+ }
861
+ const queueId = requireQueueId(state.queueId, 'Invalid queue state: malformed queueId');
862
+ const cwd = requireAbsolutePath(state.cwd, 'Invalid queue state: cwd must be an absolute path');
863
+ const status = requireQueueStatus(state.status, 'Invalid queue state: malformed queue status');
864
+ const currentIndex = requireNonNegativeInteger(state.currentIndex, 'Invalid queue state: malformed currentIndex');
865
+ const requestedPlans = requireStringArray(state.requestedPlans, 'Invalid queue state: malformed requestedPlans');
866
+ if (requestedPlans.length === 0) {
867
+ throw new Error('Invalid queue state: requestedPlans must not be empty');
868
+ }
869
+ const allowedDirtyPlanPaths = requireStringArray(state.allowedDirtyPlanPaths, 'Invalid queue state: malformed allowedDirtyPlanPaths');
870
+ validateStoredPaths(cwd, requestedPlans, 'requestedPlans');
871
+ validateStoredPaths(cwd, allowedDirtyPlanPaths, 'allowedDirtyPlanPaths');
872
+ if (!Array.isArray(state.items) || state.items.length !== requestedPlans.length) {
873
+ throw new Error('Invalid queue state: items must match requestedPlans');
874
+ }
875
+ const items = state.items.map((item, index) => parsePlanAndExecuteQueueItem(item, index, cwd));
876
+ validateQueuePathInvariants(requestedPlans, allowedDirtyPlanPaths, items);
877
+ if (currentIndex > items.length) {
878
+ throw new Error('Invalid queue state: currentIndex is out of range');
879
+ }
880
+ return {
881
+ version: 1,
882
+ queueId,
883
+ cwd,
884
+ status,
885
+ currentIndex,
886
+ requestedPlans,
887
+ allowedDirtyPlanPaths,
888
+ items,
889
+ createdAt: requireString(state.createdAt, 'Invalid queue state: malformed createdAt'),
890
+ updatedAt: requireString(state.updatedAt, 'Invalid queue state: malformed updatedAt'),
891
+ completedAt: requireNullableString(state.completedAt, 'Invalid queue state: malformed completedAt'),
892
+ stopReason: requireNullableString(state.stopReason, 'Invalid queue state: malformed stopReason'),
893
+ };
894
+ }
895
+ function parsePlanAndExecuteQueueItem(value, expectedIndex, cwd) {
896
+ if (!value || typeof value !== 'object') {
897
+ throw new Error('Invalid queue state: malformed queue item');
898
+ }
899
+ const item = value;
900
+ const index = requireNonNegativeInteger(item.index, 'Invalid queue state: malformed queue item index');
901
+ if (index !== expectedIndex) {
902
+ throw new Error('Invalid queue state: non-contiguous item indices');
903
+ }
904
+ const requestedPlanPath = requireString(item.requestedPlanPath, 'Invalid queue state: malformed requestedPlanPath');
905
+ const resolvedPlanPath = requireString(item.resolvedPlanPath, 'Invalid queue state: malformed resolvedPlanPath');
906
+ const acceptedPlanPath = requireNullableString(item.acceptedPlanPath, 'Invalid queue state: malformed acceptedPlanPath');
907
+ const planningStatePath = requireNullableString(item.planningStatePath, 'Invalid queue state: malformed planningStatePath');
908
+ const executionStatePath = requireNullableString(item.executionStatePath, 'Invalid queue state: malformed executionStatePath');
909
+ validateStoredPaths(cwd, [requestedPlanPath, resolvedPlanPath], 'queue item paths');
910
+ validateStoredPaths(cwd, nullableStringsToArray([acceptedPlanPath, planningStatePath, executionStatePath]), 'queue item paths');
911
+ return {
912
+ index,
913
+ requestedPlanPath,
914
+ resolvedPlanPath,
915
+ status: requireQueueItemStatus(item.status, 'Invalid queue state: malformed queue item status'),
916
+ planningRunId: requireNullableString(item.planningRunId, 'Invalid queue state: malformed planningRunId'),
917
+ planningStatePath,
918
+ acceptedPlanPath,
919
+ executionRunId: requireNullableString(item.executionRunId, 'Invalid queue state: malformed executionRunId'),
920
+ executionStatePath,
921
+ activeStage: requireNullableQueueChildStage(item.activeStage, 'Invalid queue state: malformed activeStage'),
922
+ startedAt: requireNullableString(item.startedAt, 'Invalid queue state: malformed startedAt'),
923
+ completedAt: requireNullableString(item.completedAt, 'Invalid queue state: malformed item completedAt'),
924
+ stopReason: requireNullableString(item.stopReason, 'Invalid queue state: malformed item stopReason'),
925
+ };
926
+ }
927
+ function parseCurrentPlanAndExecuteQueuePointer(value, cwd) {
928
+ if (!value || typeof value !== 'object') {
929
+ throw new Error('Invalid .neal/current-queue.json: expected object');
930
+ }
931
+ const pointer = value;
932
+ const queueStatePath = requireString(pointer.queueStatePath, 'Invalid .neal/current-queue.json: malformed queueStatePath');
933
+ resolveQueueStoredPath(cwd, queueStatePath);
934
+ return {
935
+ version: pointer.version === 1 ? 1 : fail('Invalid .neal/current-queue.json: malformed queue pointer version'),
936
+ queueId: requireQueueId(pointer.queueId, 'Invalid .neal/current-queue.json: malformed queueId'),
937
+ queueStatePath,
938
+ status: requireQueueStatus(pointer.status, 'Invalid .neal/current-queue.json: malformed queue status'),
939
+ currentIndex: requireNonNegativeInteger(pointer.currentIndex, 'Invalid .neal/current-queue.json: malformed currentIndex'),
940
+ activeStage: requireNullableQueueChildStage(pointer.activeStage, 'Invalid .neal/current-queue.json: malformed activeStage'),
941
+ activeRunId: requireNullableString(pointer.activeRunId, 'Invalid .neal/current-queue.json: malformed activeRunId'),
942
+ updatedAt: requireString(pointer.updatedAt, 'Invalid .neal/current-queue.json: malformed updatedAt'),
943
+ };
944
+ }
945
+ function parseQueueChildLink(value) {
946
+ if (!value || typeof value !== 'object') {
947
+ throw new Error('Invalid queue child link: expected object');
948
+ }
949
+ const link = value;
950
+ return {
951
+ version: link.version === 1 ? 1 : fail('Invalid queue child link: malformed version'),
952
+ queueId: requireQueueId(link.queueId, 'Invalid queue child link: malformed queueId'),
953
+ queueStatePath: requireString(link.queueStatePath, 'Invalid queue child link: malformed queueStatePath'),
954
+ itemIndex: requireNonNegativeInteger(link.itemIndex, 'Invalid queue child link: malformed itemIndex'),
955
+ stage: requireQueueChildStage(link.stage, 'Invalid queue child link: malformed stage'),
956
+ createdAt: requireString(link.createdAt, 'Invalid queue child link: malformed createdAt'),
957
+ };
958
+ }
959
+ function resolveQueueInputPath(cwd, path) {
960
+ return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
961
+ }
962
+ function getActiveQueueStage(state) {
963
+ return state.items[state.currentIndex]?.activeStage ?? null;
964
+ }
965
+ function getActiveQueueRunId(state) {
966
+ const item = state.items[state.currentIndex];
967
+ if (!item) {
968
+ return null;
969
+ }
970
+ switch (item.activeStage) {
971
+ case 'planning':
972
+ return item.planningRunId;
973
+ case 'execution':
974
+ return item.executionRunId;
975
+ default:
976
+ return null;
977
+ }
978
+ }
979
+ function validateStoredPaths(cwd, paths, label) {
980
+ for (const path of paths) {
981
+ try {
982
+ resolveQueueStoredPath(cwd, path);
983
+ }
984
+ catch (error) {
985
+ const message = error instanceof Error ? error.message : String(error);
986
+ throw new Error(`Invalid queue state: malformed ${label}: ${message}`);
987
+ }
988
+ }
989
+ }
990
+ function validateQueuePathInvariants(requestedPlans, allowedDirtyPlanPaths, items) {
991
+ for (const [index, item] of items.entries()) {
992
+ if (item.requestedPlanPath !== requestedPlans[index]) {
993
+ throw new Error('Invalid queue state: item requestedPlanPath must match requestedPlans');
994
+ }
995
+ }
996
+ for (const requestedPlan of requestedPlans) {
997
+ if (!allowedDirtyPlanPaths.includes(requestedPlan)) {
998
+ throw new Error('Invalid queue state: allowedDirtyPlanPaths must include all requested plans');
999
+ }
1000
+ }
1001
+ for (const item of items) {
1002
+ if (item.acceptedPlanPath && !allowedDirtyPlanPaths.includes(item.acceptedPlanPath)) {
1003
+ throw new Error('Invalid queue state: allowedDirtyPlanPaths must include accepted plan paths');
1004
+ }
1005
+ }
1006
+ }
1007
+ function nullableStringsToArray(values) {
1008
+ return values.filter((value) => value !== null);
1009
+ }
1010
+ function requireQueueId(value, message) {
1011
+ if (typeof value !== 'string' || !isValidQueueId(value)) {
1012
+ throw new Error(message);
1013
+ }
1014
+ return value;
1015
+ }
1016
+ function assertValidQueueId(queueId) {
1017
+ requireQueueId(queueId, `Invalid Neal queue id: ${queueId}`);
1018
+ }
1019
+ function isValidQueueId(queueId) {
1020
+ return QUEUE_ID_PATTERN.test(queueId) && queueId !== '.' && queueId !== '..' && !queueId.includes('..');
1021
+ }
1022
+ function requireAbsolutePath(value, message) {
1023
+ const stringValue = requireString(value, message);
1024
+ if (!isAbsolute(stringValue) || resolve(stringValue) !== stringValue) {
1025
+ throw new Error(message);
1026
+ }
1027
+ return stringValue;
1028
+ }
1029
+ function requireString(value, message) {
1030
+ if (typeof value !== 'string') {
1031
+ throw new Error(message);
1032
+ }
1033
+ return value;
1034
+ }
1035
+ function requireNullableString(value, message) {
1036
+ if (value === null) {
1037
+ return null;
1038
+ }
1039
+ return requireString(value, message);
1040
+ }
1041
+ function requireStringArray(value, message) {
1042
+ if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
1043
+ throw new Error(message);
1044
+ }
1045
+ return [...value];
1046
+ }
1047
+ function requireNonNegativeInteger(value, message) {
1048
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
1049
+ throw new Error(message);
1050
+ }
1051
+ return value;
1052
+ }
1053
+ function requireQueueStatus(value, message) {
1054
+ if (typeof value !== 'string' || !QUEUE_STATUSES.has(value)) {
1055
+ throw new Error(message);
1056
+ }
1057
+ return value;
1058
+ }
1059
+ function requireQueueItemStatus(value, message) {
1060
+ if (typeof value !== 'string' || !QUEUE_ITEM_STATUSES.has(value)) {
1061
+ throw new Error(message);
1062
+ }
1063
+ return value;
1064
+ }
1065
+ function requireQueueChildStage(value, message) {
1066
+ if (typeof value !== 'string' || !QUEUE_CHILD_STAGES.has(value)) {
1067
+ throw new Error(message);
1068
+ }
1069
+ return value;
1070
+ }
1071
+ function requireNullableQueueChildStage(value, message) {
1072
+ if (value === null) {
1073
+ return null;
1074
+ }
1075
+ return requireQueueChildStage(value, message);
1076
+ }
1077
+ function isPathInside(cwd, path) {
1078
+ const relativePath = relative(cwd, path);
1079
+ return relativePath === '' || (relativePath !== '' && !relativePath.startsWith('..') && !isAbsolute(relativePath));
1080
+ }
1081
+ function samePath(left, right) {
1082
+ return resolve(left) === resolve(right);
1083
+ }
1084
+ function formatQueueEvidenceError(error) {
1085
+ return error instanceof Error ? error.message : String(error);
1086
+ }
1087
+ function isNotFoundError(error) {
1088
+ return Boolean(error && typeof error === 'object' && error.code === 'ENOENT');
1089
+ }
1090
+ function fail(message) {
1091
+ throw new Error(message);
1092
+ }