@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,1260 @@
1
+ import { open, readdir, readFile, stat } from 'node:fs/promises';
2
+ import { basename, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { getPlanAndExecuteQueueSummaryPath, inspectQueueChildResumeEvidence, loadPlanAndExecuteQueueState, readQueueChildLink, resolveQueueStoredPath, } from './plan-queue.js';
4
+ import { buildBlockedGuidance, renderBlockedGuidanceSections, } from './blocked-guidance.js';
5
+ import { formatMaybePublicPhase, formatPublicPhase } from './phase-display.js';
6
+ import { decideResumeAction } from './resume-decision.js';
7
+ import { getChangedFilesForRange, getCommitRange } from './git.js';
8
+ import { inspectActiveRunLock } from './run-lock.js';
9
+ import { listRuns } from './run-registry.js';
10
+ import { formatPublicRunStatus, getRunDisplayStatus } from './run-status.js';
11
+ import { getRunStatePath, loadState } from './state.js';
12
+ import { getDerivedPlanView } from './state-views.js';
13
+ import { getNealBuildMetadata } from './version.js';
14
+ export { formatPublicPhase } from './phase-display.js';
15
+ const EVENT_TAIL_BYTES = 256 * 1024;
16
+ const STATUS_ERROR_MESSAGE_MAX_LENGTH = 1000;
17
+ export const OK_MEANINGFUL_EVENT_MS = 2 * 60 * 1000;
18
+ export const QUIET_WARNING_MS = 5 * 60 * 1000;
19
+ const PROVIDER_ERROR_KINDS = new Set([
20
+ 'timeout',
21
+ 'no_progress_timeout',
22
+ 'api_error',
23
+ 'structured_output_missing',
24
+ 'structured_output_invalid',
25
+ 'permission_denied',
26
+ 'session_unavailable',
27
+ 'provider_failed',
28
+ 'unknown',
29
+ ]);
30
+ const MEANINGFUL_EVENT_TYPES = new Set([
31
+ 'coder.thread_started',
32
+ 'coder.turn_started',
33
+ 'coder.item_started',
34
+ 'coder.item_updated',
35
+ 'coder.command_execution',
36
+ 'coder.file_change',
37
+ 'coder.turn_completed',
38
+ 'advisor.assistant_text',
39
+ 'advisor.task_started',
40
+ 'advisor.task_progress',
41
+ 'advisor.result',
42
+ 'provider.session_started',
43
+ 'provider.turn_started',
44
+ 'provider.turn_completed',
45
+ 'provider.tool_started',
46
+ 'provider.tool_progress',
47
+ 'provider.command_completed',
48
+ 'provider.file_changed',
49
+ 'provider.assistant_text',
50
+ 'provider.assistant_thinking',
51
+ 'provider.structured_output_received',
52
+ 'provider.provider_error',
53
+ 'provider.turn_liveness_timeout',
54
+ 'provider.turn_liveness_retry',
55
+ 'provider.turn_liveness_give_up',
56
+ 'review.meaningful_progress.advance_parent',
57
+ 'execute_finalization.advance_parent',
58
+ 'phase.start',
59
+ 'phase.complete',
60
+ 'phase.retry',
61
+ 'run.paused_after_scope',
62
+ ]);
63
+ const STATUS_INTERNAL_EVENT_TYPES = new Set(['narrative.updated']);
64
+ export async function buildStatusSnapshot(args) {
65
+ const statePath = isAbsolute(args.statePath) ? args.statePath : resolve(args.cwd, args.statePath);
66
+ const state = await loadState(statePath);
67
+ const eventsPath = join(state.runDir, 'events.ndjson');
68
+ const tail = await readEventTail(eventsPath);
69
+ const nowMs = (args.now ?? new Date()).getTime();
70
+ const eventSummary = summarizeEvents(tail.events);
71
+ const queue = await summarizeLinkedQueue(state.runDir, state.cwd);
72
+ const displayStatus = getRunDisplayStatus(state);
73
+ const latestRetrospectiveMarkdownPath = await findLatestRetrospectiveMarkdownPath(state.runDir);
74
+ const invalidDerivedPlanPayloadPath = await findCurrentInvalidSplitPlanPayloadArtifactPath(state);
75
+ const runId = basename(state.runDir);
76
+ const blockedGuidance = buildBlockedGuidance({
77
+ state,
78
+ runId,
79
+ waitingForOperatorGuidance: displayStatus.waitingForOperatorGuidance,
80
+ });
81
+ const blocker = summarizeBlocker(state, tail.events, invalidDerivedPlanPayloadPath, blockedGuidance);
82
+ const lock = await inspectActiveRunLock(state.cwd, runId);
83
+ const finalCompletionStaleness = classifyFinalCompletionStaleness({
84
+ state,
85
+ events: tail.events,
86
+ eventSummary,
87
+ nowMs,
88
+ lock,
89
+ });
90
+ const queueResumeEvidence = await inspectQueueChildResumeEvidence({ cwd: state.cwd, runDir: state.runDir });
91
+ const resumeDecision = decideResumeAction({
92
+ state,
93
+ selectedRunId: runId,
94
+ statePath,
95
+ lock,
96
+ queue: queueResumeEvidence,
97
+ retrospectivePath: latestRetrospectiveMarkdownPath,
98
+ });
99
+ const queueSummaryMarkdownPath = queue
100
+ ? getPlanAndExecuteQueueSummaryPath(state.cwd, queue.queueId)
101
+ : null;
102
+ const manualGate = summarizeManualGate(state, runId);
103
+ const health = classifyHealth(state, eventSummary, nowMs, finalCompletionStaleness);
104
+ const publicStatus = formatPublicStatusForDisplayStatus(displayStatus, health);
105
+ const publicPhase = formatPublicPhase(state.phase);
106
+ const nextAction = formatNextAction({
107
+ manualGate,
108
+ resumeDecision,
109
+ finalCompletionStaleness,
110
+ runId,
111
+ blockedGuidance,
112
+ });
113
+ const commits = summarizeCommits(state);
114
+ const squash = await summarizeSquashArtifact(state.runDir);
115
+ const providerError = summarizeProviderError(tail.events);
116
+ const build = await summarizeBuild(state);
117
+ const patch = await summarizePatch(state, displayStatus, squash);
118
+ return {
119
+ ok: true,
120
+ runId,
121
+ cwd: state.cwd,
122
+ statePath,
123
+ runDir: state.runDir,
124
+ planDoc: state.planDoc,
125
+ topLevelMode: state.topLevelMode,
126
+ executionShape: state.executionShape,
127
+ phase: state.phase,
128
+ publicPhase,
129
+ status: state.status,
130
+ effectiveStatus: displayStatus.effectiveStatus,
131
+ publicStatus,
132
+ nextAction,
133
+ waitingForOperatorGuidance: displayStatus.waitingForOperatorGuidance,
134
+ pendingOperatorGuidance: displayStatus.pendingOperatorGuidance,
135
+ blockedGuidance,
136
+ blocker,
137
+ manualGate,
138
+ resumeDecision,
139
+ currentScopeNumber: state.currentScopeNumber,
140
+ plannerSessionHandle: state.plannerSessionHandle,
141
+ coderSessionHandle: state.coderSessionHandle,
142
+ reviewerSessionHandle: state.reviewerSessionHandle,
143
+ coderRetryCount: state.coderRetryCount,
144
+ rounds: state.rounds.length,
145
+ createdCommits: state.createdCommits.length,
146
+ completedScopes: state.completedScopes.length,
147
+ lastEventAt: eventSummary.lastEvent?.ts ?? null,
148
+ lastMeaningfulEventAt: eventSummary.lastMeaningfulEvent?.ts ?? null,
149
+ lastCoderEventAt: eventSummary.lastCoderEvent?.ts ?? null,
150
+ lastReviewerEventAt: eventSummary.lastReviewerEvent?.ts ?? null,
151
+ lastMeaningfulEvent: eventSummary.lastMeaningfulEvent
152
+ ? {
153
+ type: eventSummary.lastMeaningfulEvent.type,
154
+ summary: summarizeEvent(eventSummary.lastMeaningfulEvent),
155
+ }
156
+ : null,
157
+ lock,
158
+ health,
159
+ findings: summarizeFindings(state),
160
+ commits,
161
+ squash,
162
+ providerError,
163
+ finalCompletionStaleness,
164
+ build,
165
+ patch,
166
+ derivedPlan: summarizeDerivedPlan(state),
167
+ ...(queue ? { queue } : {}),
168
+ events: {
169
+ path: tail.path,
170
+ exists: tail.exists,
171
+ parsedLines: tail.parsedLines,
172
+ malformedLines: tail.malformedLines,
173
+ truncated: tail.truncated,
174
+ },
175
+ artifacts: {
176
+ runStatePath: getRunStatePath(state.runDir),
177
+ eventsPath,
178
+ runNarrativeMarkdownPath: join(state.runDir, 'RUN_NARRATIVE.md'),
179
+ latestRetrospectiveMarkdownPath,
180
+ reviewMarkdownPath: state.reviewMarkdownPath,
181
+ progressJsonPath: state.progressJsonPath,
182
+ progressMarkdownPath: state.progressMarkdownPath,
183
+ recoveryMarkdownPath: state.recoveryMarkdownPath,
184
+ queueSummaryMarkdownPath,
185
+ archivedReviewPath: state.archivedReviewPath,
186
+ invalidDerivedPlanPayloadPath,
187
+ },
188
+ };
189
+ }
190
+ export async function buildStatusListSnapshot(args) {
191
+ const cwd = resolve(args.cwd);
192
+ const runs = await listRuns(cwd);
193
+ const snapshots = await Promise.all(runs.map((run) => buildStatusSnapshot({
194
+ cwd,
195
+ statePath: run.statePath,
196
+ now: args.now,
197
+ })));
198
+ return {
199
+ ok: true,
200
+ cwd,
201
+ runs: snapshots.map((snapshot, index) => {
202
+ const run = runs[index];
203
+ return {
204
+ runId: snapshot.runId,
205
+ runDir: snapshot.runDir,
206
+ statePath: snapshot.statePath,
207
+ planDoc: snapshot.planDoc,
208
+ topLevelMode: snapshot.topLevelMode,
209
+ executionShape: snapshot.executionShape,
210
+ status: snapshot.status,
211
+ effectiveStatus: snapshot.effectiveStatus,
212
+ publicStatus: snapshot.publicStatus,
213
+ phase: snapshot.phase,
214
+ publicPhase: snapshot.publicPhase,
215
+ currentScopeNumber: snapshot.currentScopeNumber,
216
+ waitingForOperatorGuidance: snapshot.waitingForOperatorGuidance,
217
+ pendingOperatorGuidance: snapshot.pendingOperatorGuidance,
218
+ manualGate: snapshot.manualGate,
219
+ commits: snapshot.commits,
220
+ squash: snapshot.squash,
221
+ providerError: snapshot.providerError,
222
+ finalCompletionStaleness: snapshot.finalCompletionStaleness,
223
+ build: snapshot.build,
224
+ patch: snapshot.patch,
225
+ lock: snapshot.lock,
226
+ createdAt: run.createdAt,
227
+ updatedAt: run.updatedAt,
228
+ nextAction: snapshot.nextAction,
229
+ artifacts: snapshot.artifacts,
230
+ };
231
+ }),
232
+ };
233
+ }
234
+ export function renderHumanStatusSnapshot(snapshot) {
235
+ const lines = [
236
+ '# Neal Status',
237
+ '',
238
+ `- Run: ${snapshot.runId}`,
239
+ `- Run directory: ${snapshot.runDir}`,
240
+ `- Plan: ${snapshot.planDoc}`,
241
+ `- Mode: ${snapshot.topLevelMode}`,
242
+ `- Status: ${snapshot.publicStatus}`,
243
+ `- Step: ${snapshot.publicPhase}`,
244
+ ];
245
+ if (shouldShowScope(snapshot)) {
246
+ lines.push(`- Current scope: ${snapshot.currentScopeNumber}`);
247
+ }
248
+ lines.push(`- Health: ${snapshot.health.classification} - ${snapshot.health.reason}`, `- Findings: ${snapshot.findings.openBlocking} open blocking, ${snapshot.findings.openNonBlocking} open non-blocking (${snapshot.findings.total} total)`, `- Operator guidance: waiting=${formatBoolean(snapshot.waitingForOperatorGuidance)}, pending=${formatBoolean(snapshot.pendingOperatorGuidance)}`);
249
+ if (snapshot.lastMeaningfulEvent) {
250
+ lines.push(`- Last progress: ${snapshot.lastMeaningfulEvent.type} - ${snapshot.lastMeaningfulEvent.summary}`);
251
+ }
252
+ else {
253
+ lines.push('- Last progress: none recorded');
254
+ }
255
+ if (snapshot.queue) {
256
+ lines.push(`- Queue: ${snapshot.queue.queueId} item ${snapshot.queue.itemIndex + 1}/${snapshot.queue.itemCount} ${snapshot.queue.stage}; queue ${snapshot.queue.queueStatus}, item ${snapshot.queue.itemStatus}`, `- Queue plan: ${snapshot.queue.requestedPlanPath}`);
257
+ }
258
+ if (snapshot.manualGate) {
259
+ lines.push('', '## Manual Gate', `- ID: ${snapshot.manualGate.id}`, `- Title: ${snapshot.manualGate.title}`, `- Reason: ${snapshot.manualGate.reason}`, `- Instructions: ${snapshot.manualGate.instructionsPath}`, `- Last checked: ${snapshot.manualGate.lastCheckedAt ?? 'never'}`, `- Resume command: ${snapshot.manualGate.resumeCommand}`);
260
+ if (snapshot.manualGate.lastFailure) {
261
+ lines.push(`- Last failure check: ${snapshot.manualGate.lastFailure.checkName}`, `- Last failure exit code: ${snapshot.manualGate.lastFailure.exitCode ?? 'none'}`, `- Last failure signal: ${snapshot.manualGate.lastFailure.signal ?? 'none'}`, `- Last failure stdout tail: ${snapshot.manualGate.lastFailure.stdoutTail || 'empty'}`, `- Last failure stderr tail: ${snapshot.manualGate.lastFailure.stderrTail || 'empty'}`);
262
+ }
263
+ else {
264
+ lines.push('- Last failure: none');
265
+ }
266
+ }
267
+ if (snapshot.derivedPlan) {
268
+ lines.push(`- Derived plan: ${snapshot.derivedPlan.path} (${snapshot.derivedPlan.status ?? 'unknown'})`);
269
+ }
270
+ if (snapshot.providerError) {
271
+ const provider = snapshot.providerError.provider ?? 'unclassified';
272
+ const kind = snapshot.providerError.kind ?? 'unclassified';
273
+ lines.push(`- Provider error: ${provider} ${kind} - ${snapshot.providerError.message}`);
274
+ }
275
+ lines.push(`- Active lock: ${formatLockSummary(snapshot.lock)}`, `- Squash: ${snapshot.squash.status}${snapshot.squash.replacementCommit ? ` replacement=${snapshot.squash.replacementCommit}` : ''}`, `- Patch policy: ${snapshot.patch.defaultSubmissionEligible ? 'eligible' : 'not eligible'} - ${snapshot.patch.reason}`);
276
+ if (snapshot.blocker.active && !snapshot.blockedGuidance) {
277
+ lines.push('', '## Blocker', `- Reason: ${snapshot.blocker.reason ?? 'unknown'}`, `- Source: ${snapshot.blocker.source ?? 'unknown'}`);
278
+ if (snapshot.blocker.artifactPaths.length > 0) {
279
+ lines.push('- Artifacts:');
280
+ for (const artifact of snapshot.blocker.artifactPaths) {
281
+ lines.push(` - ${artifact.label}: ${artifact.path}`);
282
+ }
283
+ }
284
+ }
285
+ lines.push('', '## Artifacts', `- Run state: ${snapshot.artifacts.runStatePath}`, `- Events: ${snapshot.artifacts.eventsPath}`, `- Run narrative: ${snapshot.artifacts.runNarrativeMarkdownPath}`, `- Latest retrospective: ${snapshot.artifacts.latestRetrospectiveMarkdownPath ?? 'none found'}`, `- Progress: ${snapshot.artifacts.progressMarkdownPath}`, `- Progress JSON: ${snapshot.artifacts.progressJsonPath}`, `- Review: ${snapshot.artifacts.reviewMarkdownPath}`, `- Recovery: ${snapshot.artifacts.recoveryMarkdownPath}`);
286
+ if (snapshot.artifacts.invalidDerivedPlanPayloadPath) {
287
+ lines.push(`- Invalid split-plan payload: ${snapshot.artifacts.invalidDerivedPlanPayloadPath}`);
288
+ }
289
+ if (snapshot.artifacts.queueSummaryMarkdownPath) {
290
+ lines.push(`- Queue summary: ${snapshot.artifacts.queueSummaryMarkdownPath}`);
291
+ }
292
+ if (snapshot.artifacts.archivedReviewPath) {
293
+ lines.push(`- Archived review: ${snapshot.artifacts.archivedReviewPath}`);
294
+ }
295
+ if (snapshot.blockedGuidance) {
296
+ lines.push('', ...renderBlockedGuidanceSections(snapshot.blockedGuidance));
297
+ }
298
+ lines.push('', '## Next Action', `- ${snapshot.nextAction}`);
299
+ return lines.join('\n');
300
+ }
301
+ export function renderHumanStatusListSnapshot(snapshot) {
302
+ if (snapshot.runs.length === 0) {
303
+ return 'No Neal runs found.\n';
304
+ }
305
+ const rows = [
306
+ ['RUN ID', 'MODE', 'STATUS', 'STEP', 'PLAN', 'UPDATED', 'NEXT ACTION'],
307
+ ...snapshot.runs.map((run) => [
308
+ run.runId,
309
+ run.topLevelMode,
310
+ run.publicStatus,
311
+ run.publicPhase,
312
+ toDisplayPath(snapshot.cwd, run.planDoc),
313
+ run.updatedAt,
314
+ run.nextAction,
315
+ ]),
316
+ ];
317
+ const widths = rows[0].map((_cell, index) => Math.max(...rows.map((row) => row[index].length)));
318
+ return rows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(' ').trimEnd()).join('\n') + '\n';
319
+ }
320
+ function formatPublicStatusForDisplayStatus(displayStatus, health) {
321
+ const publicStatus = formatPublicRunStatus({
322
+ effectiveStatus: displayStatus.effectiveStatus,
323
+ waitingForOperatorGuidance: displayStatus.waitingForOperatorGuidance,
324
+ pendingOperatorGuidance: displayStatus.pendingOperatorGuidance,
325
+ });
326
+ if (displayStatus.effectiveStatus === 'paused' || health.classification === 'paused') {
327
+ return 'paused';
328
+ }
329
+ if (health.classification === 'stale') {
330
+ return 'stale';
331
+ }
332
+ return publicStatus;
333
+ }
334
+ function shouldShowScope(snapshot) {
335
+ return snapshot.topLevelMode === 'execute' && snapshot.phase !== 'done';
336
+ }
337
+ function formatBoolean(value) {
338
+ return value ? 'yes' : 'no';
339
+ }
340
+ function formatLockSummary(lock) {
341
+ switch (lock.kind) {
342
+ case 'none':
343
+ return 'none';
344
+ case 'live_same_run':
345
+ return `live for this run${typeof lock.pid === 'number' ? ` pid=${lock.pid}` : ''}`;
346
+ case 'stale_same_run':
347
+ return `stale for this run${typeof lock.pid === 'number' ? ` pid=${lock.pid}` : ''}`;
348
+ case 'live_different_run':
349
+ return `live for ${lock.runId}${typeof lock.pid === 'number' ? ` pid=${lock.pid}` : ''}`;
350
+ case 'stale_different_run':
351
+ return `stale for ${lock.runId}${typeof lock.pid === 'number' ? ` pid=${lock.pid}` : ''}`;
352
+ case 'cross_host':
353
+ return `cross-host for ${lock.runId} on ${lock.hostname ?? 'unknown host'}`;
354
+ case 'unreadable':
355
+ return `unreadable - ${lock.reason}`;
356
+ }
357
+ }
358
+ function formatNextAction(snapshot) {
359
+ if (snapshot.manualGate) {
360
+ return `Complete manual gate ${snapshot.manualGate.id}, then resume this run: ${snapshot.manualGate.resumeCommand}`;
361
+ }
362
+ if (snapshot.finalCompletionStaleness.stale && snapshot.resumeDecision.kind === 'continue') {
363
+ return `Final completion appears stale after reviewer output. Inspect artifacts and recover explicitly after confirming the branch state: neal status --run ${snapshot.runId}`;
364
+ }
365
+ if (snapshot.resumeDecision.kind === 'needs_message' && snapshot.blockedGuidance) {
366
+ const firstOption = snapshot.blockedGuidance.options[0]?.command;
367
+ if (firstOption) {
368
+ return `Needs operator guidance: ${formatSentence(snapshot.blockedGuidance.summary)} Use first resume option: ${firstOption}`;
369
+ }
370
+ return `Needs operator guidance: ${formatSentence(snapshot.blockedGuidance.summary)} Inspect guidance: neal status --run ${snapshot.runId}`;
371
+ }
372
+ return formatNextActionForDecision(snapshot.resumeDecision);
373
+ }
374
+ export function formatStatusNextActionForState(state) {
375
+ const runId = basename(state.runDir);
376
+ const resumeDecision = decideResumeAction({
377
+ state,
378
+ selectedRunId: runId,
379
+ statePath: getRunStatePath(state.runDir),
380
+ });
381
+ return formatNextAction({
382
+ manualGate: summarizeManualGate(state, runId),
383
+ resumeDecision,
384
+ finalCompletionStaleness: {
385
+ stale: false,
386
+ reason: null,
387
+ reviewerOutputObserved: false,
388
+ finalCompletionPhaseStartedAt: null,
389
+ lastFinalCompletionReviewerOutputAt: null,
390
+ finalCompletionPhaseTerminalEventObserved: false,
391
+ },
392
+ runId,
393
+ blockedGuidance: buildBlockedGuidance({ state, runId }),
394
+ });
395
+ }
396
+ function formatNextActionForDecision(decision) {
397
+ switch (decision.kind) {
398
+ case 'continue':
399
+ return `Resume this run: ${decision.resumeCommand}`;
400
+ case 'needs_message':
401
+ return `Needs operator guidance: ${formatSentence(decision.blocker)} Record guidance and resume: ${decision.messageCommand}`;
402
+ case 'pending_message':
403
+ return `Operator guidance is recorded. Resume this run: ${decision.resumeCommand}`;
404
+ case 'already_running':
405
+ return `${decision.reason} Inspect status: ${decision.statusCommand}`;
406
+ case 'done':
407
+ return decision.retrospectivePath
408
+ ? `No resume needed. Review latest retrospective: ${decision.retrospectivePath}`
409
+ : `No resume needed. Inspect status: ${decision.statusCommand}`;
410
+ case 'cannot_resume':
411
+ return `${decision.reason} Inspect status and artifacts: ${decision.statusCommand}`;
412
+ }
413
+ }
414
+ function formatSentence(value) {
415
+ const trimmed = value.trim();
416
+ if (/[.!?]$/.test(trimmed)) {
417
+ return trimmed;
418
+ }
419
+ return `${trimmed}.`;
420
+ }
421
+ function toDisplayPath(cwd, path) {
422
+ if (!isAbsolute(path)) {
423
+ return path;
424
+ }
425
+ const relativePath = relative(cwd, path);
426
+ return relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath) ? relativePath : path;
427
+ }
428
+ function summarizeManualGate(state, runId) {
429
+ const gate = state.phase === 'manual_gate' ? state.manualGate : null;
430
+ if (!gate) {
431
+ return null;
432
+ }
433
+ return {
434
+ id: gate.id,
435
+ title: gate.title,
436
+ reason: gate.reason,
437
+ instructionsPath: gate.instructionsPath,
438
+ lastCheckedAt: gate.lastCheckedAt,
439
+ lastFailure: gate.lastFailure,
440
+ resumeCommand: `neal resume --run ${runId}`,
441
+ };
442
+ }
443
+ async function findLatestRetrospectiveMarkdownPath(runDir) {
444
+ const defaultPath = join(runDir, 'RETROSPECTIVE.md');
445
+ try {
446
+ const defaultStat = await stat(defaultPath);
447
+ if (defaultStat.isFile()) {
448
+ return defaultPath;
449
+ }
450
+ }
451
+ catch (error) {
452
+ if (!isNotFoundError(error)) {
453
+ throw error;
454
+ }
455
+ }
456
+ let entries;
457
+ try {
458
+ entries = await readdir(runDir, { withFileTypes: true });
459
+ }
460
+ catch (error) {
461
+ if (isNotFoundError(error)) {
462
+ return null;
463
+ }
464
+ throw error;
465
+ }
466
+ const latestName = entries
467
+ .filter((entry) => entry.isFile() && /^RETROSPECTIVE-.+\.md$/.test(entry.name))
468
+ .map((entry) => entry.name)
469
+ .sort()
470
+ .at(-1);
471
+ return latestName ? join(runDir, latestName) : null;
472
+ }
473
+ export function summarizeDerivedPlan(state) {
474
+ const derivedPlan = getDerivedPlanView(state);
475
+ if (!derivedPlan) {
476
+ return null;
477
+ }
478
+ return {
479
+ path: derivedPlan.path,
480
+ parentScopeNumber: derivedPlan.parentScopeNumber,
481
+ status: derivedPlan.status,
482
+ scopeIndex: derivedPlan.scopeIndex,
483
+ reviewActive: derivedPlan.reviewActive,
484
+ acceptedAwaitingExecution: derivedPlan.acceptedAwaitingExecution,
485
+ executing: derivedPlan.executing,
486
+ abandoned: derivedPlan.abandoned,
487
+ };
488
+ }
489
+ async function findCurrentInvalidSplitPlanPayloadArtifactPath(state) {
490
+ const artifactPath = join(state.runDir, `SCOPE_${state.currentScopeNumber}_INVALID_DERIVED_PLAN.md`);
491
+ const fileStat = await stat(artifactPath).catch((error) => {
492
+ if (error.code === 'ENOENT') {
493
+ return null;
494
+ }
495
+ throw error;
496
+ });
497
+ return fileStat?.isFile() ? artifactPath : null;
498
+ }
499
+ function summarizeBlocker(state, events, invalidDerivedPlanPayloadPath, guidance = null) {
500
+ const blockedLike = state.status === 'blocked' ||
501
+ state.status === 'failed' ||
502
+ state.phase === 'blocked' ||
503
+ state.phase === 'interactive_blocked_recovery';
504
+ const artifactPaths = invalidDerivedPlanPayloadPath
505
+ ? [{ label: 'Invalid split-plan payload', path: invalidDerivedPlanPayloadPath }]
506
+ : [];
507
+ if (!blockedLike) {
508
+ return {
509
+ active: false,
510
+ reason: null,
511
+ source: null,
512
+ artifactPaths,
513
+ };
514
+ }
515
+ const candidates = [
516
+ {
517
+ reason: guidance?.category === 'scope_accounting_guardrail' ? guidance.summary : null,
518
+ source: 'blocked guidance',
519
+ },
520
+ {
521
+ reason: state.interactiveBlockedRecovery?.blockedReason ?? null,
522
+ source: 'RUN_STATE.json interactive recovery',
523
+ },
524
+ {
525
+ reason: currentOrLatestBlockedCompletedScopeBlocker(state),
526
+ source: 'RUN_STATE.json completed scopes',
527
+ },
528
+ {
529
+ reason: latestInvalidSplitPlanEventBlocker(events, invalidDerivedPlanPayloadPath),
530
+ source: 'events.ndjson invalid split-plan payload',
531
+ },
532
+ ];
533
+ const candidate = candidates.find((item) => item.reason);
534
+ return {
535
+ active: Boolean(candidate?.reason),
536
+ reason: candidate?.reason ?? null,
537
+ source: candidate?.source ?? null,
538
+ artifactPaths,
539
+ };
540
+ }
541
+ function nonEmptyString(value) {
542
+ const trimmed = value?.trim();
543
+ return trimmed ? trimmed : null;
544
+ }
545
+ function currentOrLatestBlockedCompletedScopeBlocker(state) {
546
+ const blockedScopes = state.completedScopes.filter((scope) => scope.result === 'blocked' && scope.blocker);
547
+ const currentScope = blockedScopes.find((scope) => scope.number === String(state.currentScopeNumber));
548
+ return currentScope?.blocker ?? blockedScopes.at(-1)?.blocker ?? null;
549
+ }
550
+ function latestInvalidSplitPlanEventBlocker(events, invalidDerivedPlanPayloadPath) {
551
+ for (const event of [...events].reverse()) {
552
+ if (event.type !== 'split_plan.invalid_payload') {
553
+ continue;
554
+ }
555
+ const validationErrors = stringArrayValue(event.data.validationErrors);
556
+ const artifactPath = invalidDerivedPlanPayloadPath ?? stringValue(event.data.invalidPayloadPath);
557
+ const reason = validationErrors.length > 0
558
+ ? `Invalid split-plan payload: ${validationErrors.join('; ')}`
559
+ : 'Invalid split-plan payload was rejected by plan validation.';
560
+ return artifactPath ? `${reason} Invalid payload artifact: ${artifactPath}` : reason;
561
+ }
562
+ return null;
563
+ }
564
+ async function summarizeLinkedQueue(runDir, cwd) {
565
+ const link = await readQueueChildLink(runDir);
566
+ if (!link) {
567
+ return undefined;
568
+ }
569
+ const queueStatePath = resolveQueueStoredPath(cwd, link.queueStatePath);
570
+ const queueState = await loadPlanAndExecuteQueueState(queueStatePath, cwd);
571
+ if (queueState.queueId !== link.queueId) {
572
+ throw new Error(`Invalid queue child link: queue ${link.queueId} does not match state ${queueState.queueId}`);
573
+ }
574
+ const item = queueState.items[link.itemIndex];
575
+ if (!item) {
576
+ throw new Error(`Invalid queue child link: missing queue item ${link.itemIndex}`);
577
+ }
578
+ return {
579
+ queueId: queueState.queueId,
580
+ queueStatePath,
581
+ itemIndex: link.itemIndex,
582
+ itemCount: queueState.items.length,
583
+ stage: link.stage,
584
+ queueStatus: queueState.status,
585
+ itemStatus: item.status,
586
+ requestedPlanPath: item.requestedPlanPath,
587
+ };
588
+ }
589
+ async function readEventTail(path) {
590
+ let file;
591
+ try {
592
+ file = await open(path, 'r');
593
+ }
594
+ catch (error) {
595
+ if (isNotFoundError(error)) {
596
+ return {
597
+ path,
598
+ exists: false,
599
+ parsedLines: 0,
600
+ malformedLines: 0,
601
+ truncated: false,
602
+ events: [],
603
+ };
604
+ }
605
+ throw error;
606
+ }
607
+ try {
608
+ const stat = await file.stat();
609
+ const truncated = stat.size > EVENT_TAIL_BYTES;
610
+ const length = Math.min(stat.size, EVENT_TAIL_BYTES);
611
+ const start = stat.size - length;
612
+ const buffer = Buffer.alloc(length);
613
+ await file.read(buffer, 0, length, start);
614
+ let text = buffer.toString('utf8');
615
+ if (truncated) {
616
+ text = text.slice(Math.max(0, text.indexOf('\n') + 1));
617
+ }
618
+ return parseEventText(path, text, truncated);
619
+ }
620
+ finally {
621
+ await file.close();
622
+ }
623
+ }
624
+ function parseEventText(path, text, truncated) {
625
+ const lines = text.split('\n');
626
+ let malformedLines = 0;
627
+ let parsedLines = 0;
628
+ const events = [];
629
+ for (const [index, line] of lines.entries()) {
630
+ if (line.trim() === '') {
631
+ continue;
632
+ }
633
+ try {
634
+ const event = JSON.parse(line);
635
+ const parsed = parseRunEvent(event);
636
+ if (parsed) {
637
+ events.push(parsed);
638
+ parsedLines += 1;
639
+ }
640
+ else {
641
+ malformedLines += 1;
642
+ }
643
+ }
644
+ catch {
645
+ malformedLines += 1;
646
+ if (index === lines.length - 1) {
647
+ continue;
648
+ }
649
+ }
650
+ }
651
+ return {
652
+ path,
653
+ exists: true,
654
+ parsedLines,
655
+ malformedLines,
656
+ truncated,
657
+ events,
658
+ };
659
+ }
660
+ function parseRunEvent(event) {
661
+ if (typeof event.ts !== 'string' || typeof event.type !== 'string') {
662
+ return null;
663
+ }
664
+ const timeMs = Date.parse(event.ts);
665
+ if (!Number.isFinite(timeMs)) {
666
+ return null;
667
+ }
668
+ return {
669
+ ts: new Date(timeMs).toISOString(),
670
+ timeMs,
671
+ type: event.type,
672
+ data: event.data && typeof event.data === 'object' ? event.data : {},
673
+ };
674
+ }
675
+ function summarizeEvents(events) {
676
+ let lastEvent = null;
677
+ let lastMeaningfulEvent = null;
678
+ let lastCoderEvent = null;
679
+ let lastReviewerEvent = null;
680
+ let latestPhaseStart = null;
681
+ let latestTimeoutCleanupAfterPhaseStart = null;
682
+ for (const event of events) {
683
+ if (STATUS_INTERNAL_EVENT_TYPES.has(event.type)) {
684
+ continue;
685
+ }
686
+ if (!lastEvent || event.timeMs >= lastEvent.timeMs) {
687
+ lastEvent = event;
688
+ }
689
+ if (isMeaningfulEvent(event)) {
690
+ if (!lastMeaningfulEvent || event.timeMs >= lastMeaningfulEvent.timeMs) {
691
+ lastMeaningfulEvent = event;
692
+ }
693
+ }
694
+ if (isCoderEvent(event) && (!lastCoderEvent || event.timeMs >= lastCoderEvent.timeMs)) {
695
+ lastCoderEvent = event;
696
+ }
697
+ if (isReviewerEvent(event) && (!lastReviewerEvent || event.timeMs >= lastReviewerEvent.timeMs)) {
698
+ lastReviewerEvent = event;
699
+ }
700
+ if (event.type === 'phase.start' && (!latestPhaseStart || event.timeMs >= latestPhaseStart.timeMs)) {
701
+ latestPhaseStart = event;
702
+ }
703
+ }
704
+ if (latestPhaseStart) {
705
+ for (let index = events.length - 1; index >= 0; index -= 1) {
706
+ const event = events[index];
707
+ if (event.type === 'coder.timeout_cleanup' && event.timeMs >= latestPhaseStart.timeMs) {
708
+ latestTimeoutCleanupAfterPhaseStart = event;
709
+ break;
710
+ }
711
+ }
712
+ }
713
+ return {
714
+ lastEvent,
715
+ lastMeaningfulEvent,
716
+ lastCoderEvent,
717
+ lastReviewerEvent,
718
+ latestPhaseStart,
719
+ latestTimeoutCleanupAfterPhaseStart,
720
+ };
721
+ }
722
+ function isMeaningfulEvent(event) {
723
+ return MEANINGFUL_EVENT_TYPES.has(event.type) && event.type !== 'phase.heartbeat';
724
+ }
725
+ function isCoderEvent(event) {
726
+ return event.type.startsWith('coder.') || (event.type.startsWith('provider.') && event.data.role === 'coder');
727
+ }
728
+ function isReviewerEvent(event) {
729
+ return (event.type.startsWith('advisor.') ||
730
+ (event.type.startsWith('provider.') && event.data.role === 'structured-advisor'));
731
+ }
732
+ function classifyFinalCompletionStaleness(args) {
733
+ const evidence = summarizeFinalCompletionReviewEvidence(args.events);
734
+ const base = {
735
+ stale: false,
736
+ reason: null,
737
+ reviewerOutputObserved: evidence.reviewerOutputObserved,
738
+ finalCompletionPhaseStartedAt: evidence.finalCompletionPhaseStartedAt,
739
+ lastFinalCompletionReviewerOutputAt: evidence.lastFinalCompletionReviewerOutputAt,
740
+ finalCompletionPhaseTerminalEventObserved: evidence.finalCompletionPhaseTerminalEventObserved,
741
+ };
742
+ if (args.state.topLevelMode !== 'execute' ||
743
+ args.state.status !== 'running' ||
744
+ args.state.phase !== 'final_completion_review' ||
745
+ args.state.finalCommit === null ||
746
+ args.state.finalCompletionSummary === null ||
747
+ args.state.finalCompletionReviewVerdict !== null ||
748
+ args.state.finalCompletionResolvedAction !== null ||
749
+ !hasAcceptedTerminalCompletedScopeForCurrentPosition(args.state) ||
750
+ !evidence.reviewerOutputObserved ||
751
+ evidence.finalCompletionPhaseTerminalEventObserved ||
752
+ args.lock.kind === 'live_same_run') {
753
+ return base;
754
+ }
755
+ const quietMs = getQuietMs(args.eventSummary, args.nowMs);
756
+ if (quietMs === null || quietMs < QUIET_WARNING_MS) {
757
+ return base;
758
+ }
759
+ return {
760
+ ...base,
761
+ stale: true,
762
+ reason: 'final completion review has reviewer output but no terminal transition',
763
+ };
764
+ }
765
+ function summarizeFinalCompletionReviewEvidence(events) {
766
+ let latestPhaseStart = null;
767
+ for (const event of events) {
768
+ if (event.type === 'phase.start' &&
769
+ event.data.phase === 'final_completion_review' &&
770
+ (!latestPhaseStart || event.timeMs >= latestPhaseStart.timeMs)) {
771
+ latestPhaseStart = event;
772
+ }
773
+ }
774
+ let lastReviewerOutput = null;
775
+ let terminalEventObserved = false;
776
+ if (latestPhaseStart) {
777
+ for (const event of events) {
778
+ if (event.timeMs < latestPhaseStart.timeMs) {
779
+ continue;
780
+ }
781
+ if (isFinalCompletionReviewerOutputEvent(event)) {
782
+ if (!lastReviewerOutput || event.timeMs >= lastReviewerOutput.timeMs) {
783
+ lastReviewerOutput = event;
784
+ }
785
+ }
786
+ if (isFinalCompletionPhaseTerminalEvent(event)) {
787
+ terminalEventObserved = true;
788
+ }
789
+ }
790
+ }
791
+ return {
792
+ reviewerOutputObserved: lastReviewerOutput !== null,
793
+ finalCompletionPhaseStartedAt: latestPhaseStart?.ts ?? null,
794
+ lastFinalCompletionReviewerOutputAt: lastReviewerOutput?.ts ?? null,
795
+ finalCompletionPhaseTerminalEventObserved: terminalEventObserved,
796
+ };
797
+ }
798
+ function isFinalCompletionReviewerOutputEvent(event) {
799
+ const label = stringValue(event.data.label);
800
+ return event.type === 'provider.structured_output_received' && Boolean(label?.startsWith('final-completion'));
801
+ }
802
+ function isFinalCompletionPhaseTerminalEvent(event) {
803
+ return ((event.type === 'phase.complete' || event.type === 'phase.error') &&
804
+ event.data.phase === 'final_completion_review');
805
+ }
806
+ function hasAcceptedTerminalCompletedScopeForCurrentPosition(state) {
807
+ const currentScopeNumber = String(state.currentScopeNumber);
808
+ return state.completedScopes.some((scope) => scope.number === currentScopeNumber &&
809
+ scope.result === 'accepted' &&
810
+ scope.marker === 'AUTONOMY_DONE' &&
811
+ scope.finalCommit === state.finalCommit);
812
+ }
813
+ function classifyHealth(state, events, nowMs, finalCompletionStaleness) {
814
+ const phaseStartMs = getPhaseStartMs(state, events);
815
+ const phaseElapsedMs = phaseStartMs === null ? null : Math.max(0, nowMs - phaseStartMs);
816
+ const quietMs = getQuietMs(events, nowMs);
817
+ const heartbeatOnly = isHeartbeatOnly(events);
818
+ const makeHealth = (classification, reason) => ({
819
+ classification,
820
+ phaseElapsedMs,
821
+ quietMs,
822
+ heartbeatOnly,
823
+ reason,
824
+ });
825
+ if (isBlockedState(state)) {
826
+ return makeHealth('blocked', 'state is blocked or failed');
827
+ }
828
+ if (events.lastMeaningfulEvent?.type === 'run.paused_after_scope') {
829
+ return makeHealth('paused', 'run paused after current scope');
830
+ }
831
+ if (state.status === 'paused') {
832
+ return makeHealth('paused', 'run is paused');
833
+ }
834
+ if (events.lastMeaningfulEvent?.type === 'phase.retry' || events.latestTimeoutCleanupAfterPhaseStart) {
835
+ return makeHealth('timed_out', 'retry or timeout cleanup observed');
836
+ }
837
+ if (state.status === 'done' || state.phase === 'done') {
838
+ return makeHealth('ok', 'run is done');
839
+ }
840
+ if (finalCompletionStaleness.stale) {
841
+ return makeHealth('stale', finalCompletionStaleness.reason ?? 'final completion review appears stale');
842
+ }
843
+ const meaningfulAgeMs = events.lastMeaningfulEvent ? nowMs - events.lastMeaningfulEvent.timeMs : null;
844
+ if (meaningfulAgeMs !== null && meaningfulAgeMs <= OK_MEANINGFUL_EVENT_MS) {
845
+ return makeHealth('ok', 'recent meaningful event');
846
+ }
847
+ const eventAgeMs = events.lastEvent ? nowMs - events.lastEvent.timeMs : null;
848
+ if (heartbeatOnly && eventAgeMs !== null && eventAgeMs <= OK_MEANINGFUL_EVENT_MS) {
849
+ return makeHealth('heartbeat_only', 'recent heartbeat without phase progress');
850
+ }
851
+ if (state.status === 'running' && quietMs !== null && quietMs >= QUIET_WARNING_MS) {
852
+ return makeHealth('quiet', 'no recent meaningful event');
853
+ }
854
+ return makeHealth('unknown', 'insufficient event evidence');
855
+ }
856
+ function getQuietMs(events, nowMs) {
857
+ const quietBasisMs = events.lastMeaningfulEvent?.timeMs ?? events.lastEvent?.timeMs ?? null;
858
+ return quietBasisMs === null ? null : Math.max(0, nowMs - quietBasisMs);
859
+ }
860
+ function isBlockedState(state) {
861
+ const { waitingForOperatorGuidance } = getRunDisplayStatus(state);
862
+ return state.status === 'blocked' || state.status === 'failed' || state.phase === 'blocked' || waitingForOperatorGuidance;
863
+ }
864
+ function getPhaseStartMs(state, events) {
865
+ if (events.latestPhaseStart) {
866
+ return events.latestPhaseStart.timeMs;
867
+ }
868
+ const updatedAtMs = Date.parse(state.updatedAt);
869
+ if (Number.isFinite(updatedAtMs)) {
870
+ return updatedAtMs;
871
+ }
872
+ const createdAtMs = Date.parse(state.createdAt);
873
+ return Number.isFinite(createdAtMs) ? createdAtMs : null;
874
+ }
875
+ function isHeartbeatOnly(events) {
876
+ if (events.lastEvent?.type !== 'phase.heartbeat') {
877
+ return false;
878
+ }
879
+ const phaseStartMs = events.latestPhaseStart?.timeMs ?? null;
880
+ if (phaseStartMs === null) {
881
+ return events.lastMeaningfulEvent === null;
882
+ }
883
+ return !events.lastMeaningfulEvent || events.lastMeaningfulEvent.timeMs <= phaseStartMs;
884
+ }
885
+ function summarizeEvent(event) {
886
+ const candidates = [
887
+ event.data.message,
888
+ typeof event.data.phase === 'string' ? formatMaybePublicPhase(event.data.phase) : null,
889
+ typeof event.data.nextPhase === 'string' ? formatMaybePublicPhase(event.data.nextPhase) : null,
890
+ event.data.command,
891
+ event.data.label,
892
+ ];
893
+ const summary = candidates.find((candidate) => typeof candidate === 'string' && candidate.length > 0);
894
+ return summary ?? event.type;
895
+ }
896
+ function summarizeFindings(state) {
897
+ return {
898
+ total: state.findings.length,
899
+ openBlocking: state.findings.filter((finding) => finding.status === 'open' && finding.severity === 'blocking').length,
900
+ openNonBlocking: state.findings.filter((finding) => finding.status === 'open' && finding.severity === 'non_blocking').length,
901
+ fixed: state.findings.filter((finding) => finding.status === 'fixed').length,
902
+ rejected: state.findings.filter((finding) => finding.status === 'rejected').length,
903
+ deferred: state.findings.filter((finding) => finding.status === 'deferred').length,
904
+ };
905
+ }
906
+ function summarizeCommits(state) {
907
+ return {
908
+ initialBaseCommit: state.initialBaseCommit,
909
+ baseCommit: state.baseCommit,
910
+ finalCommit: state.finalCommit,
911
+ createdCommitCount: state.createdCommits.length,
912
+ acceptedScopeFinalCommits: state.completedScopes
913
+ .filter((scope) => scope.result === 'accepted' && typeof scope.finalCommit === 'string' && scope.finalCommit.length > 0)
914
+ .map((scope) => scope.finalCommit),
915
+ };
916
+ }
917
+ async function summarizeSquashArtifact(runDir) {
918
+ const artifactPath = join(runDir, 'SQUASH_RESULT.json');
919
+ let artifactText;
920
+ try {
921
+ artifactText = await readFile(artifactPath, 'utf8');
922
+ }
923
+ catch (error) {
924
+ if (isNotFoundError(error)) {
925
+ return {
926
+ artifactPath,
927
+ status: 'missing',
928
+ replacementCommit: null,
929
+ finalHeadCommit: null,
930
+ originalBaseCommit: null,
931
+ originalFinalCommit: null,
932
+ unavailableReason: 'SQUASH_RESULT.json is not present for this run.',
933
+ };
934
+ }
935
+ const reason = error instanceof Error && error.message.trim()
936
+ ? `SQUASH_RESULT.json could not be read: ${truncateStatusMessage(error.message)}`
937
+ : 'SQUASH_RESULT.json could not be read.';
938
+ return malformedSquashSummary(artifactPath, reason);
939
+ }
940
+ try {
941
+ const artifact = parseSquashArtifact(JSON.parse(artifactText));
942
+ if (!artifact) {
943
+ return malformedSquashSummary(artifactPath, 'SQUASH_RESULT.json does not match the expected squash artifact shape.');
944
+ }
945
+ return {
946
+ artifactPath,
947
+ status: artifact.status,
948
+ replacementCommit: artifact.replacementCommit,
949
+ finalHeadCommit: artifact.finalHeadCommit,
950
+ originalBaseCommit: artifact.originalBaseCommit,
951
+ originalFinalCommit: artifact.originalFinalCommit,
952
+ unavailableReason: null,
953
+ };
954
+ }
955
+ catch (error) {
956
+ const reason = error instanceof Error && error.message.trim()
957
+ ? `SQUASH_RESULT.json could not be parsed: ${truncateStatusMessage(error.message)}`
958
+ : 'SQUASH_RESULT.json could not be parsed.';
959
+ return malformedSquashSummary(artifactPath, reason);
960
+ }
961
+ }
962
+ function malformedSquashSummary(artifactPath, unavailableReason) {
963
+ return {
964
+ artifactPath,
965
+ status: 'malformed',
966
+ replacementCommit: null,
967
+ finalHeadCommit: null,
968
+ originalBaseCommit: null,
969
+ originalFinalCommit: null,
970
+ unavailableReason,
971
+ };
972
+ }
973
+ function parseSquashArtifact(value) {
974
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
975
+ return null;
976
+ }
977
+ const record = value;
978
+ if (record.version !== 1 || (record.status !== 'pending' && record.status !== 'complete')) {
979
+ return null;
980
+ }
981
+ const originalBaseCommit = stringValue(record.originalBaseCommit);
982
+ const originalFinalCommit = stringValue(record.originalFinalCommit);
983
+ if (!originalBaseCommit || !originalFinalCommit) {
984
+ return null;
985
+ }
986
+ const replacementCommit = nullableStringValue(record.replacementCommit);
987
+ const finalHeadCommit = nullableStringValue(record.finalHeadCommit);
988
+ if (record.status === 'complete' && (!replacementCommit || !finalHeadCommit)) {
989
+ return null;
990
+ }
991
+ if (record.status === 'pending' && (replacementCommit !== null || finalHeadCommit !== null)) {
992
+ return null;
993
+ }
994
+ return {
995
+ status: record.status,
996
+ replacementCommit,
997
+ finalHeadCommit,
998
+ originalBaseCommit,
999
+ originalFinalCommit,
1000
+ };
1001
+ }
1002
+ function summarizeProviderError(events) {
1003
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1004
+ const event = events[index];
1005
+ if (event.type !== 'provider.provider_error') {
1006
+ continue;
1007
+ }
1008
+ return {
1009
+ source: 'provider_event',
1010
+ timestamp: event.ts,
1011
+ provider: stringValue(event.data.provider),
1012
+ role: providerRoleValue(event.data.role),
1013
+ label: stringValue(event.data.label),
1014
+ sessionHandle: nullableStringValue(event.data.sessionHandle),
1015
+ kind: providerErrorKindValue(event.data.errorKind),
1016
+ message: truncateStatusMessage(stringValue(event.data.message) ?? 'Provider failed.'),
1017
+ retryable: typeof event.data.retryable === 'boolean' ? event.data.retryable : null,
1018
+ diagnostic: providerErrorDiagnosticValue(event.data.providerData),
1019
+ };
1020
+ }
1021
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1022
+ const event = events[index];
1023
+ if (event.type !== 'phase.error') {
1024
+ continue;
1025
+ }
1026
+ return {
1027
+ source: 'phase_error',
1028
+ timestamp: event.ts,
1029
+ provider: null,
1030
+ role: null,
1031
+ label: typeof event.data.phase === 'string' ? formatMaybePublicPhase(event.data.phase) : null,
1032
+ sessionHandle: nullableStringValue(event.data.sessionHandle),
1033
+ kind: null,
1034
+ message: truncateStatusMessage(stringValue(event.data.message) ?? 'Run phase failed.'),
1035
+ retryable: null,
1036
+ diagnostic: null,
1037
+ };
1038
+ }
1039
+ return null;
1040
+ }
1041
+ async function summarizeBuild(state) {
1042
+ const metaBuild = await readMetaBuild(join(state.runDir, 'meta.json'));
1043
+ if (metaBuild) {
1044
+ return {
1045
+ ...metaBuild,
1046
+ source: 'meta',
1047
+ agentConfig: state.agentConfig,
1048
+ };
1049
+ }
1050
+ return {
1051
+ ...await getNealBuildMetadata(),
1052
+ source: 'live_fallback',
1053
+ agentConfig: state.agentConfig,
1054
+ };
1055
+ }
1056
+ async function readMetaBuild(path) {
1057
+ try {
1058
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
1059
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1060
+ return null;
1061
+ }
1062
+ const build = parsed.build;
1063
+ if (!build || typeof build !== 'object' || Array.isArray(build)) {
1064
+ return null;
1065
+ }
1066
+ const record = build;
1067
+ const packageVersion = stringValue(record.packageVersion);
1068
+ const nodeVersion = stringValue(record.nodeVersion);
1069
+ const sourceGitSha = nullableStringValue(record.sourceGitSha);
1070
+ if (!packageVersion || !nodeVersion) {
1071
+ return null;
1072
+ }
1073
+ return {
1074
+ packageVersion,
1075
+ nodeVersion,
1076
+ sourceGitSha,
1077
+ };
1078
+ }
1079
+ catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+ async function summarizePatch(state, displayStatus, squash) {
1084
+ const candidate = selectPatchCandidate(state, squash);
1085
+ const range = candidate.baseCommit && candidate.headCommit ? `${candidate.baseCommit}..${candidate.headCommit}` : null;
1086
+ const baseSummary = {
1087
+ defaultSubmissionEligible: false,
1088
+ source: candidate.source,
1089
+ baseCommit: candidate.baseCommit,
1090
+ headCommit: candidate.headCommit,
1091
+ range,
1092
+ commitCount: null,
1093
+ changedFileCount: null,
1094
+ changedFiles: [],
1095
+ unavailableReason: null,
1096
+ };
1097
+ if (!candidate.baseCommit) {
1098
+ return {
1099
+ ...baseSummary,
1100
+ reason: 'No patch base commit is available.',
1101
+ unavailableReason: 'No patch base commit is available.',
1102
+ };
1103
+ }
1104
+ if (!candidate.headCommit) {
1105
+ return {
1106
+ ...baseSummary,
1107
+ reason: 'No patch head commit is available.',
1108
+ unavailableReason: 'No patch head commit is available.',
1109
+ };
1110
+ }
1111
+ let commits;
1112
+ let changedFiles;
1113
+ try {
1114
+ [commits, changedFiles] = await Promise.all([
1115
+ getCommitRange(state.cwd, candidate.baseCommit, candidate.headCommit),
1116
+ getChangedFilesForRange(state.cwd, candidate.baseCommit, candidate.headCommit),
1117
+ ]);
1118
+ }
1119
+ catch (error) {
1120
+ const unavailableReason = error instanceof Error && error.message.trim()
1121
+ ? truncateStatusMessage(error.message)
1122
+ : 'Git could not read the patch range.';
1123
+ return {
1124
+ ...baseSummary,
1125
+ reason: `Patch range is unavailable: ${unavailableReason}`,
1126
+ unavailableReason,
1127
+ };
1128
+ }
1129
+ const populatedSummary = {
1130
+ ...baseSummary,
1131
+ commitCount: commits.length,
1132
+ changedFileCount: changedFiles.length,
1133
+ changedFiles,
1134
+ };
1135
+ if (commits.length === 0 || changedFiles.length === 0) {
1136
+ return {
1137
+ ...populatedSummary,
1138
+ reason: 'Completed run produced an empty patch range.',
1139
+ };
1140
+ }
1141
+ if (squash.status === 'malformed') {
1142
+ return {
1143
+ ...populatedSummary,
1144
+ reason: 'SQUASH_RESULT.json is malformed; default submission requires an auditable patch head.',
1145
+ unavailableReason: squash.unavailableReason,
1146
+ };
1147
+ }
1148
+ if (squash.status === 'pending') {
1149
+ return {
1150
+ ...populatedSummary,
1151
+ reason: 'Squash artifact is pending; default submission requires finalized patch metadata.',
1152
+ };
1153
+ }
1154
+ if (!isCleanCompletedExecuteRun(state, displayStatus)) {
1155
+ return {
1156
+ ...populatedSummary,
1157
+ reason: `Run status is ${displayStatus.effectiveStatus}; default submission requires a completed execute run.`,
1158
+ };
1159
+ }
1160
+ return {
1161
+ ...populatedSummary,
1162
+ defaultSubmissionEligible: true,
1163
+ reason: 'Completed execute run has a non-empty patch range.',
1164
+ };
1165
+ }
1166
+ function selectPatchCandidate(state, squash) {
1167
+ if (squash.status === 'complete' && squash.originalBaseCommit && squash.replacementCommit) {
1168
+ return {
1169
+ source: 'squash_replacement',
1170
+ baseCommit: squash.originalBaseCommit,
1171
+ headCommit: squash.replacementCommit,
1172
+ };
1173
+ }
1174
+ const baseCommit = state.initialBaseCommit ?? state.baseCommit;
1175
+ if (baseCommit || state.finalCommit) {
1176
+ return {
1177
+ source: 'final_commit',
1178
+ baseCommit,
1179
+ headCommit: state.finalCommit,
1180
+ };
1181
+ }
1182
+ return {
1183
+ source: 'none',
1184
+ baseCommit: null,
1185
+ headCommit: null,
1186
+ };
1187
+ }
1188
+ function isCleanCompletedExecuteRun(state, displayStatus) {
1189
+ return (state.topLevelMode === 'execute' &&
1190
+ state.phase === 'done' &&
1191
+ state.status === 'done' &&
1192
+ displayStatus.effectiveStatus === 'done' &&
1193
+ !displayStatus.waitingForOperatorGuidance &&
1194
+ !displayStatus.pendingOperatorGuidance);
1195
+ }
1196
+ function providerRoleValue(value) {
1197
+ return value === 'coder' || value === 'structured-advisor' ? value : null;
1198
+ }
1199
+ function providerErrorKindValue(value) {
1200
+ return typeof value === 'string' && PROVIDER_ERROR_KINDS.has(value)
1201
+ ? value
1202
+ : null;
1203
+ }
1204
+ const STATUS_PROVIDER_DIAGNOSTIC_MAX_STRING_LENGTH = 2_000;
1205
+ const STATUS_PROVIDER_DIAGNOSTIC_MAX_DEPTH = 4;
1206
+ function providerErrorDiagnosticValue(providerData) {
1207
+ if (typeof providerData !== 'object' || providerData === null) {
1208
+ return null;
1209
+ }
1210
+ const diagnostic = providerData.diagnostic;
1211
+ const sanitized = sanitizeProviderDiagnostic(diagnostic);
1212
+ return typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)
1213
+ ? sanitized
1214
+ : null;
1215
+ }
1216
+ function sanitizeProviderDiagnostic(value, depth = 0) {
1217
+ if (depth > STATUS_PROVIDER_DIAGNOSTIC_MAX_DEPTH) {
1218
+ return '[truncated: max depth]';
1219
+ }
1220
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
1221
+ return value;
1222
+ }
1223
+ if (typeof value === 'string') {
1224
+ if (value.length <= STATUS_PROVIDER_DIAGNOSTIC_MAX_STRING_LENGTH) {
1225
+ return value;
1226
+ }
1227
+ return `${value.slice(0, STATUS_PROVIDER_DIAGNOSTIC_MAX_STRING_LENGTH)}...[truncated ${value.length - STATUS_PROVIDER_DIAGNOSTIC_MAX_STRING_LENGTH} chars]`;
1228
+ }
1229
+ if (Array.isArray(value)) {
1230
+ return value.slice(0, 20).map((item) => sanitizeProviderDiagnostic(item, depth + 1));
1231
+ }
1232
+ if (typeof value !== 'object') {
1233
+ return null;
1234
+ }
1235
+ const output = {};
1236
+ for (const [key, nestedValue] of Object.entries(value)) {
1237
+ output[key] = sanitizeProviderDiagnostic(nestedValue, depth + 1);
1238
+ }
1239
+ return output;
1240
+ }
1241
+ function nullableStringValue(value) {
1242
+ return value === null || value === undefined ? null : stringValue(value);
1243
+ }
1244
+ function truncateStatusMessage(value) {
1245
+ if (value.length <= STATUS_ERROR_MESSAGE_MAX_LENGTH) {
1246
+ return value;
1247
+ }
1248
+ return `${value.slice(0, STATUS_ERROR_MESSAGE_MAX_LENGTH - 3)}...`;
1249
+ }
1250
+ function stringValue(value) {
1251
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
1252
+ }
1253
+ function stringArrayValue(value) {
1254
+ return Array.isArray(value)
1255
+ ? value.filter((item) => typeof item === 'string' && item.trim().length > 0)
1256
+ : [];
1257
+ }
1258
+ function isNotFoundError(error) {
1259
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
1260
+ }