@pi-agents/orchid 0.1.0-beta.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 (163) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/agents/AGENTS-MANIFEST.md +42 -0
  5. package/agents/brain.md +42 -0
  6. package/agents/context-builder.md +46 -0
  7. package/agents/delegate.md +12 -0
  8. package/agents/dev-1.md +42 -0
  9. package/agents/oracle.md +73 -0
  10. package/agents/planner.md +55 -0
  11. package/agents/researcher.md +52 -0
  12. package/agents/reviewer.md +79 -0
  13. package/agents/scout.md +50 -0
  14. package/agents/tester.md +45 -0
  15. package/agents/worker.md +55 -0
  16. package/extensions/ralph.ts +1 -0
  17. package/extensions/reviewer-extension.ts +125 -0
  18. package/extensions/task-orchestrator.ts +28 -0
  19. package/package.json +63 -0
  20. package/prompts/gather-context-and-clarify.md +13 -0
  21. package/prompts/parallel-cleanup.md +59 -0
  22. package/prompts/parallel-context-build.md +53 -0
  23. package/prompts/parallel-handoff-plan.md +59 -0
  24. package/prompts/parallel-research.md +50 -0
  25. package/prompts/parallel-review.md +54 -0
  26. package/prompts/review-loop.md +41 -0
  27. package/skills/orchid/SKILL.md +214 -0
  28. package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
  29. package/skills/orchid/orchid-converge/SKILL.md +124 -0
  30. package/skills/orchid/orchid-decompose/SKILL.md +201 -0
  31. package/skills/orchid/orchid-doctor/SKILL.md +162 -0
  32. package/skills/orchid/orchid-investigate/SKILL.md +102 -0
  33. package/skills/orchid/orchid-launch/SKILL.md +147 -0
  34. package/skills/ralph/SKILL.md +73 -0
  35. package/skills/subagents/pi-subagents/SKILL.md +813 -0
  36. package/src/index.ts +7 -0
  37. package/src/orchestrator/abort.ts +534 -0
  38. package/src/orchestrator/agent-bridge-extension.ts +1020 -0
  39. package/src/orchestrator/agent-host.ts +954 -0
  40. package/src/orchestrator/cleanup.ts +776 -0
  41. package/src/orchestrator/config-loader.ts +1412 -0
  42. package/src/orchestrator/config-schema.ts +690 -0
  43. package/src/orchestrator/config.ts +81 -0
  44. package/src/orchestrator/context-window.ts +66 -0
  45. package/src/orchestrator/diagnostic-reports.ts +475 -0
  46. package/src/orchestrator/diagnostics.ts +394 -0
  47. package/src/orchestrator/discovery.ts +1833 -0
  48. package/src/orchestrator/engine-worker.ts +415 -0
  49. package/src/orchestrator/engine.ts +5940 -0
  50. package/src/orchestrator/execution.ts +3104 -0
  51. package/src/orchestrator/extension.ts +5934 -0
  52. package/src/orchestrator/formatting.ts +785 -0
  53. package/src/orchestrator/git.ts +88 -0
  54. package/src/orchestrator/index.ts +28 -0
  55. package/src/orchestrator/lane-runner.ts +1787 -0
  56. package/src/orchestrator/mailbox.ts +780 -0
  57. package/src/orchestrator/merge.ts +3414 -0
  58. package/src/orchestrator/messages.ts +1062 -0
  59. package/src/orchestrator/migrations.ts +278 -0
  60. package/src/orchestrator/naming.ts +117 -0
  61. package/src/orchestrator/path-resolver.ts +275 -0
  62. package/src/orchestrator/persistence.ts +2625 -0
  63. package/src/orchestrator/process-registry.ts +452 -0
  64. package/src/orchestrator/quality-gate.ts +1085 -0
  65. package/src/orchestrator/resume.ts +3488 -0
  66. package/src/orchestrator/sessions.ts +57 -0
  67. package/src/orchestrator/settings-loader.ts +136 -0
  68. package/src/orchestrator/settings-tui.ts +2208 -0
  69. package/src/orchestrator/sidecar-telemetry.ts +267 -0
  70. package/src/orchestrator/supervisor.ts +4548 -0
  71. package/src/orchestrator/task-executor-core.ts +675 -0
  72. package/src/orchestrator/tmux-compat.ts +37 -0
  73. package/src/orchestrator/tool-allowlist-constants.ts +37 -0
  74. package/src/orchestrator/types.ts +4465 -0
  75. package/src/orchestrator/verification.ts +547 -0
  76. package/src/orchestrator/waves.ts +1564 -0
  77. package/src/orchestrator/workspace.ts +707 -0
  78. package/src/orchestrator/worktree.ts +2725 -0
  79. package/src/ralph/index.ts +825 -0
  80. package/src/subagents/agents/agent-management.ts +648 -0
  81. package/src/subagents/agents/agent-scope.ts +6 -0
  82. package/src/subagents/agents/agent-selection.ts +23 -0
  83. package/src/subagents/agents/agent-serializer.ts +86 -0
  84. package/src/subagents/agents/agents.ts +832 -0
  85. package/src/subagents/agents/chain-serializer.ts +137 -0
  86. package/src/subagents/agents/frontmatter.ts +29 -0
  87. package/src/subagents/agents/identity.ts +30 -0
  88. package/src/subagents/agents/skills.ts +632 -0
  89. package/src/subagents/extension/config.ts +16 -0
  90. package/src/subagents/extension/control-notices.ts +92 -0
  91. package/src/subagents/extension/doctor.ts +199 -0
  92. package/src/subagents/extension/fanout-child.ts +170 -0
  93. package/src/subagents/extension/index.ts +573 -0
  94. package/src/subagents/extension/schemas.ts +168 -0
  95. package/src/subagents/intercom/intercom-bridge.ts +379 -0
  96. package/src/subagents/intercom/result-intercom.ts +377 -0
  97. package/src/subagents/runs/background/async-execution.ts +712 -0
  98. package/src/subagents/runs/background/async-job-tracker.ts +310 -0
  99. package/src/subagents/runs/background/async-resume.ts +345 -0
  100. package/src/subagents/runs/background/async-status.ts +325 -0
  101. package/src/subagents/runs/background/completion-dedupe.ts +63 -0
  102. package/src/subagents/runs/background/notify.ts +108 -0
  103. package/src/subagents/runs/background/parallel-groups.ts +45 -0
  104. package/src/subagents/runs/background/result-watcher.ts +307 -0
  105. package/src/subagents/runs/background/run-id-resolver.ts +83 -0
  106. package/src/subagents/runs/background/run-status.ts +269 -0
  107. package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
  108. package/src/subagents/runs/background/subagent-runner.ts +1808 -0
  109. package/src/subagents/runs/background/top-level-async.ts +13 -0
  110. package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
  111. package/src/subagents/runs/foreground/chain-execution.ts +938 -0
  112. package/src/subagents/runs/foreground/execution.ts +918 -0
  113. package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
  114. package/src/subagents/runs/shared/completion-guard.ts +147 -0
  115. package/src/subagents/runs/shared/long-running-guard.ts +175 -0
  116. package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
  117. package/src/subagents/runs/shared/model-fallback.ts +103 -0
  118. package/src/subagents/runs/shared/nested-events.ts +819 -0
  119. package/src/subagents/runs/shared/nested-path.ts +52 -0
  120. package/src/subagents/runs/shared/nested-render.ts +115 -0
  121. package/src/subagents/runs/shared/parallel-utils.ts +109 -0
  122. package/src/subagents/runs/shared/pi-args.ts +220 -0
  123. package/src/subagents/runs/shared/pi-spawn.ts +115 -0
  124. package/src/subagents/runs/shared/run-history.ts +60 -0
  125. package/src/subagents/runs/shared/single-output.ts +164 -0
  126. package/src/subagents/runs/shared/subagent-control.ts +226 -0
  127. package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
  128. package/src/subagents/runs/shared/worktree.ts +577 -0
  129. package/src/subagents/shared/artifacts.ts +98 -0
  130. package/src/subagents/shared/atomic-json.ts +16 -0
  131. package/src/subagents/shared/file-coalescer.ts +40 -0
  132. package/src/subagents/shared/fork-context.ts +76 -0
  133. package/src/subagents/shared/formatters.ts +133 -0
  134. package/src/subagents/shared/jsonl-writer.ts +81 -0
  135. package/src/subagents/shared/model-info.ts +78 -0
  136. package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
  137. package/src/subagents/shared/session-identity.ts +10 -0
  138. package/src/subagents/shared/session-tokens.ts +44 -0
  139. package/src/subagents/shared/settings.ts +397 -0
  140. package/src/subagents/shared/status-format.ts +49 -0
  141. package/src/subagents/shared/types.ts +822 -0
  142. package/src/subagents/shared/utils.ts +450 -0
  143. package/src/subagents/slash/prompt-template-bridge.ts +397 -0
  144. package/src/subagents/slash/slash-bridge.ts +174 -0
  145. package/src/subagents/slash/slash-commands.ts +528 -0
  146. package/src/subagents/slash/slash-live-state.ts +292 -0
  147. package/src/subagents/tui/render-helpers.ts +80 -0
  148. package/src/subagents/tui/render.ts +1358 -0
  149. package/templates/agents/local/supervisor.md +33 -0
  150. package/templates/agents/local/task-merger.md +27 -0
  151. package/templates/agents/local/task-reviewer.md +30 -0
  152. package/templates/agents/local/task-worker.md +34 -0
  153. package/templates/agents/supervisor-routing.md +92 -0
  154. package/templates/agents/supervisor.md +229 -0
  155. package/templates/agents/task-merger.md +214 -0
  156. package/templates/agents/task-reviewer.md +260 -0
  157. package/templates/agents/task-worker-segment.md +44 -0
  158. package/templates/agents/task-worker.md +557 -0
  159. package/templates/tasks/CONTEXT.md +30 -0
  160. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
  161. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
  162. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
  163. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Exit classification types and logic for task diagnostics.
3
+ *
4
+ * Defines the structured `TaskExitDiagnostic` type that replaces
5
+ * free-text `exitReason` for deterministic retry decisions,
6
+ * cost tracking, and dashboard telemetry.
7
+ *
8
+ * @module orch/diagnostics
9
+ * @see docs/specifications/orchid/resilience-and-diagnostics-roadmap.md §1b
10
+ */
11
+
12
+ // ── Token Counts (Diagnostics) ───────────────────────────────────────
13
+
14
+ /**
15
+ * Token usage breakdown for a single session.
16
+ *
17
+ * Matches the RPC exit-summary `tokens` shape: four count fields only.
18
+ * Cost is tracked separately as a top-level field on `ExitSummary` and
19
+ * `TaskExitDiagnostic`, not embedded in the token counts.
20
+ *
21
+ * This is distinct from `TokenCounts` in `types.ts` (which bundles
22
+ * `costUsd` for batch-history aggregation). Downstream consumers that
23
+ * need to convert can merge `{ ...sessionTokens, costUsd: cost }`.
24
+ */
25
+ export interface SessionTokenCounts {
26
+ /** Input tokens consumed */
27
+ input: number;
28
+ /** Output tokens generated */
29
+ output: number;
30
+ /** Tokens served from cache (read) */
31
+ cacheRead: number;
32
+ /** Tokens written to cache */
33
+ cacheWrite: number;
34
+ }
35
+
36
+ // ── Exit Classification ──────────────────────────────────────────────
37
+
38
+ /**
39
+ * All possible exit classifications for a task session.
40
+ *
41
+ * Each value maps to a specific failure mode that downstream consumers
42
+ * (retry logic, dashboard, cost reports) can branch on deterministically.
43
+ *
44
+ * | Classification | Meaning |
45
+ * |----------------------|------------------------------------------------------|
46
+ * | `completed` | `.DONE` file found — task finished successfully |
47
+ * | `api_error` | API returned error (auth, rate limit, overload) |
48
+ * | `model_access_error` | Model unavailable (401/403/429, model not found) |
49
+ * | `context_overflow` | Hit context window limit (compactions + high ctx %) |
50
+ * | `wall_clock_timeout` | Killed by task-runner's max_worker_minutes timer |
51
+ * | `process_crash` | Non-zero exit code with no API error indicators |
52
+ * | `session_vanished` | Session disappeared without exit summary |
53
+ * | `stall_timeout` | No STATUS.md progress for stall_timeout minutes |
54
+ * | `user_killed` | User manually killed the session (e.g., forced process kill) |
55
+ * | `spawn_failure` | Worker process never spawned (e.g., Pi CLI not findable, worktree provisioning) |
56
+ * | `unknown` | Could not determine cause |
57
+ *
58
+ * Note: `spawn_failure` (TP-190, #561) is set BEFORE any agent process exists —
59
+ * it is produced synchronously when `spawnAgent()` throws (resolvePiCliPath
60
+ * miss, file-system error, etc.). It is intentionally NOT in
61
+ * `TIER0_RETRYABLE_CLASSIFICATIONS` because spawn-stage failures are never
62
+ * transient; retrying without operator intervention only burns budget.
63
+ */
64
+ export type ExitClassification =
65
+ | "completed"
66
+ | "api_error"
67
+ | "model_access_error"
68
+ | "context_overflow"
69
+ | "wall_clock_timeout"
70
+ | "process_crash"
71
+ | "session_vanished"
72
+ | "stall_timeout"
73
+ | "user_killed"
74
+ | "spawn_failure"
75
+ | "unknown";
76
+
77
+ /**
78
+ * All classification values as a readonly array, for iteration and validation.
79
+ */
80
+ export const EXIT_CLASSIFICATIONS: readonly ExitClassification[] = [
81
+ "completed",
82
+ "api_error",
83
+ "model_access_error",
84
+ "context_overflow",
85
+ "wall_clock_timeout",
86
+ "process_crash",
87
+ "session_vanished",
88
+ "stall_timeout",
89
+ "user_killed",
90
+ "spawn_failure",
91
+ "unknown",
92
+ ] as const;
93
+
94
+ // ── Retry Record ─────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * A single API retry event from the RPC wrapper's exit summary.
98
+ *
99
+ * Captured from `auto_retry_start/end` RPC events.
100
+ */
101
+ export interface RetryRecord {
102
+ /** Retry attempt number (1-indexed) */
103
+ attempt: number;
104
+ /** Error message that triggered the retry */
105
+ error: string;
106
+ /** Delay in milliseconds before retrying */
107
+ delayMs: number;
108
+ /** Whether the retry succeeded */
109
+ succeeded: boolean;
110
+ }
111
+
112
+ // ── Exit Summary ─────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Exit summary written by rpc-wrapper.mjs on process exit.
116
+ *
117
+ * This is the wrapper's output artifact — a JSON file capturing
118
+ * everything the wrapper observed during the session. The task-runner
119
+ * reads this to build `TaskExitDiagnostic`.
120
+ *
121
+ * **Field optionality rationale:**
122
+ * The wrapper initializes counters (toolCalls, compactions, durationSec,
123
+ * retries) at startup, so they are always present even on crash — these
124
+ * are required. Fields that depend on RPC event accumulation (tokens,
125
+ * cost, lastToolCall, error) are nullable — they may be absent if the
126
+ * process crashes before capturing any events. `exitCode` and
127
+ * `exitSignal` are optional (`?`) because the wrapper may crash before
128
+ * the Node exit handler fires, producing a partial JSON artifact that
129
+ * `JSON.parse()` succeeds on but lacks these fields.
130
+ *
131
+ * Consumers MUST use `typeof` guards on optional/nullable fields before
132
+ * branching (e.g., `typeof exitCode === "number"` rather than `!== null`).
133
+ */
134
+ export interface ExitSummary {
135
+ /** Process exit code. Optional — may be absent if wrapper crashes before exit handler fires. Null if killed by signal. */
136
+ exitCode?: number | null;
137
+ /** Signal that killed the process (e.g., "SIGTERM"). Optional — may be absent on crash. Null if clean exit. */
138
+ exitSignal?: string | null;
139
+ /** Accumulated token counts across all turns (null if no message_end events received) */
140
+ tokens: SessionTokenCounts | null;
141
+ /** Total cost in USD (null if no cost data received) */
142
+ cost: number | null;
143
+ /** Total tool calls made (initialized to 0 at startup) */
144
+ toolCalls: number;
145
+ /** API retry events observed (initialized to [] at startup) */
146
+ retries: RetryRecord[];
147
+ /** Number of context compactions observed (initialized to 0 at startup) */
148
+ compactions: number;
149
+ /** Wall-clock duration of the session in seconds (always written, even on crash) */
150
+ durationSec: number;
151
+ /** Last tool call description (e.g., "bash: node --test tests/*.test.ts"), null if no tools were called */
152
+ lastToolCall: string | null;
153
+ /** Error message if the session ended with an error, null on clean exit */
154
+ error: string | null;
155
+ }
156
+
157
+ // ── Classification Input ─────────────────────────────────────────────
158
+
159
+ /**
160
+ * Structured input to `classifyExit()`.
161
+ *
162
+ * Aggregates all signals needed for deterministic classification.
163
+ * Sources:
164
+ * - `exitSummary`: from rpc-wrapper.mjs exit summary JSON (null if file missing)
165
+ * - `doneFileFound`: from .DONE file presence check (task-runner)
166
+ * - `timerKilled`: true if task-runner's max_worker_minutes timer killed the session
167
+ * - `contextKilled`: true if the task-runner explicitly killed the session due to context limit
168
+ * - `stallDetected`: true if monitoring detected no STATUS.md progress
169
+ * - `userKilled`: true if user manually killed the session (e.g., /orch-abort, forced process kill)
170
+ * - `contextPct`: estimated context utilization % (0-100), null if unknown
171
+ *
172
+ * Design: single structured input object (not positional args) for
173
+ * extensibility as new signals are added in future phases.
174
+ */
175
+ export interface ExitClassificationInput {
176
+ /** Exit summary from rpc-wrapper.mjs. Null if the summary file was not found. */
177
+ exitSummary: ExitSummary | null;
178
+ /** Whether the .DONE file was found in the task folder */
179
+ doneFileFound: boolean;
180
+ /** Whether the task-runner's wall-clock timer killed the session */
181
+ timerKilled: boolean;
182
+ /** Whether the task-runner explicitly killed the session due to context limit (TP-026) */
183
+ contextKilled?: boolean;
184
+ /** Whether monitoring detected a stall (no STATUS.md progress) */
185
+ stallDetected: boolean;
186
+ /** Whether the user manually killed the session */
187
+ userKilled: boolean;
188
+ /** Estimated context utilization percentage (0-100), null if unknown */
189
+ contextPct: number | null;
190
+ }
191
+
192
+ // ── Task Exit Diagnostic ─────────────────────────────────────────────
193
+
194
+ /**
195
+ * Structured diagnostic for a task session's exit.
196
+ *
197
+ * Sits alongside the legacy `exitReason: string` on `LaneTaskOutcome`
198
+ * during the transition period (Phase 1). Promoted to canonical in
199
+ * schema v3 (Phase 3).
200
+ *
201
+ * Produced by calling `classifyExit()` after the session ends, then
202
+ * enriching with progress/context metadata from STATUS.md and git.
203
+ */
204
+ export interface TaskExitDiagnostic {
205
+ /** Deterministic exit classification */
206
+ classification: ExitClassification;
207
+ /** Process exit code (null if killed by signal or summary missing) */
208
+ exitCode: number | null;
209
+ /** Human-readable error message (null if clean exit) */
210
+ errorMessage: string | null;
211
+ /** Token usage breakdown (null if no summary available) */
212
+ tokensUsed: SessionTokenCounts | null;
213
+ /** Estimated context utilization percentage (0-100, null if unknown) */
214
+ contextPct: number | null;
215
+ /** Number of commits on the task branch (partial progress indicator) */
216
+ partialProgressCommits: number;
217
+ /** Branch name with partial progress (null if no branch) */
218
+ partialProgressBranch: string | null;
219
+ /** Wall-clock duration of the session in seconds */
220
+ durationSec: number;
221
+ /** Last known step number from STATUS.md (null if unparsed) */
222
+ lastKnownStep: number | null;
223
+ /** Last known checkbox text from STATUS.md (null if unparsed) */
224
+ lastKnownCheckbox: string | null;
225
+ /** Repo identifier ("default" in repo mode, repo key in workspace mode) */
226
+ repoId: string;
227
+ }
228
+
229
+ // ── Classification Logic ─────────────────────────────────────────────
230
+
231
+ /**
232
+ * Threshold for context utilization percentage to consider "high".
233
+ * Used in the `context_overflow` classification path:
234
+ * compactions > 0 AND contextPct >= this threshold → context_overflow.
235
+ */
236
+ export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
237
+
238
+ /**
239
+ * Patterns that indicate a model access error (as opposed to a generic API error).
240
+ *
241
+ * These patterns match error messages from API providers when:
242
+ * - The model is not found or deprecated
243
+ * - Authentication/authorization fails (HTTP 401/403)
244
+ * - Rate limits are hit specifically for the model (HTTP 429)
245
+ * - API key is expired or invalid
246
+ *
247
+ * The patterns are case-insensitive and tested against the error string.
248
+ *
249
+ * @since TP-055
250
+ */
251
+ export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
252
+ /\b(?:401|403)\b/, // HTTP auth/forbidden status codes
253
+ /\b429\b/, // HTTP rate limit
254
+ /model[_ ]not[_ ]found/i, // Model not found
255
+ /model[_ ](?:is[_ ])?unavailable/i, // Model unavailable
256
+ /model[_ ](?:has[_ ]been[_ ])?deprecated/i, // Model deprecated
257
+ /api[_ ]key[_ ](?:expired|invalid|revoked)/i, // API key issues
258
+ /invalid[_ ]api[_ ]key/i, // Invalid API key (alternate phrasing)
259
+ /authentication[_ ](?:failed|error|required)/i, // Auth failures
260
+ /authorization[_ ](?:failed|error|denied)/i, // Authz failures
261
+ /access[_ ]denied/i, // Generic access denied
262
+ /permission[_ ]denied/i, // Permission denied
263
+ /quota[_ ]exceeded/i, // Quota exceeded
264
+ /rate[_ ]limit/i, // Rate limit (phrase)
265
+ /insufficient[_ ]quota/i, // Insufficient quota
266
+ ];
267
+
268
+ /**
269
+ * Test whether an error message indicates a model access error.
270
+ *
271
+ * Used by `classifyExit()` to distinguish model-specific failures from
272
+ * generic API errors, enabling targeted fallback to the session model.
273
+ *
274
+ * @param errorMessage - Error message to test
275
+ * @returns true if the error matches a model access pattern
276
+ * @since TP-055
277
+ */
278
+ export function isModelAccessError(errorMessage: string): boolean {
279
+ if (!errorMessage) return false;
280
+ return MODEL_ACCESS_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
281
+ }
282
+
283
+ /**
284
+ * Classify a task session's exit into a deterministic category.
285
+ *
286
+ * Uses a strict precedence order — the first matching condition wins.
287
+ * This ensures deterministic results even when multiple signals are
288
+ * present (e.g., a session that was both stalled AND crashed).
289
+ *
290
+ * **Classification precedence (highest → lowest):**
291
+ *
292
+ * | Priority | Condition | Result |
293
+ * |----------|------------------------------------------------------|----------------------|
294
+ * | 1 | `.DONE` file found | `completed` |
295
+ * | 2a | Retries with model-access error pattern | `model_access_error` |
296
+ * | 2b | Retries present with final retry failed | `api_error` |
297
+ * | 2c | Error message has model-access pattern (no retries) | `model_access_error` |
298
+ * | 3 | Compactions > 0 AND contextPct ≥ 90% | `context_overflow` |
299
+ * | 3b | Task-runner explicitly context-killed | `context_overflow` |
300
+ * | 4 | Timer killed the session | `wall_clock_timeout` |
301
+ * | 5 | Non-zero exit code, no API error | `process_crash` |
302
+ * | 6 | No exit summary file (session vanished) | `session_vanished` |
303
+ * | 7 | Stall detected (no STATUS.md progress) | `stall_timeout` |
304
+ * | 8 | User manually killed the session | `user_killed` |
305
+ * | 9 | None of the above | `unknown` |
306
+ *
307
+ * **Tie-break rationale:**
308
+ * - `.DONE` always wins because the task succeeded regardless of how messy
309
+ * the session was (retries, compactions, etc.).
310
+ * - `model_access_error` beats generic `api_error` because it's more specific
311
+ * and enables targeted fallback (retry with session model).
312
+ * - `api_error` beats `context_overflow` because API failures are more
313
+ * actionable (auth fix, rate limit backoff).
314
+ * - `wall_clock_timeout` beats `process_crash` because the timer kill
315
+ * explains the non-zero exit code.
316
+ * - `session_vanished` (no summary) is checked after exit-code-based
317
+ * paths because those require the summary to exist.
318
+ * - `stall_timeout` and `user_killed` are low-priority because they're
319
+ * external signals that may co-occur with other conditions.
320
+ *
321
+ * @param input - Aggregated signals from the session exit
322
+ * @returns The exit classification string
323
+ */
324
+ export function classifyExit(input: ExitClassificationInput): ExitClassification {
325
+ const { exitSummary, doneFileFound, timerKilled, stallDetected, userKilled, contextPct } = input;
326
+ const contextKilled = input.contextKilled ?? false;
327
+
328
+ // 1. .DONE file found → completed (task succeeded, regardless of session state)
329
+ if (doneFileFound) {
330
+ return "completed";
331
+ }
332
+
333
+ // 2a. Retries present with model-access error pattern → model_access_error
334
+ // 2b. Retries present with final retry failed → api_error
335
+ if (exitSummary?.retries && exitSummary.retries.length > 0) {
336
+ const lastRetry = exitSummary.retries[exitSummary.retries.length - 1];
337
+ if (!lastRetry.succeeded) {
338
+ // Check if the retry error indicates a model access issue
339
+ if (isModelAccessError(lastRetry.error)) {
340
+ return "model_access_error";
341
+ }
342
+ return "api_error";
343
+ }
344
+ }
345
+
346
+ // 2c. Error message (no retries) indicates model access issue → model_access_error
347
+ if (exitSummary?.error && isModelAccessError(exitSummary.error)) {
348
+ return "model_access_error";
349
+ }
350
+
351
+ // 3. Compactions > 0 AND high context utilization → context_overflow
352
+ if (exitSummary && exitSummary.compactions > 0) {
353
+ const effectivePct = contextPct ?? 0;
354
+ if (effectivePct >= CONTEXT_OVERFLOW_THRESHOLD_PCT) {
355
+ return "context_overflow";
356
+ }
357
+ }
358
+
359
+ // 3b. Task-runner explicitly killed session due to context limit → context_overflow
360
+ // Catches cases where exit summary is missing (wrapper crashed) or compactions=0
361
+ // but the task-runner's own context guard triggered the kill.
362
+ if (contextKilled) {
363
+ return "context_overflow";
364
+ }
365
+
366
+ // 4. Task-runner's wall-clock timer killed the session → wall_clock_timeout
367
+ if (timerKilled) {
368
+ return "wall_clock_timeout";
369
+ }
370
+
371
+ // 5. Non-zero exit code, no API error indicators → process_crash
372
+ // Guard with typeof to handle partial summaries where exitCode may be undefined
373
+ if (exitSummary && typeof exitSummary.exitCode === "number" && exitSummary.exitCode !== 0) {
374
+ return "process_crash";
375
+ }
376
+
377
+ // 6. No exit summary file found → session_vanished
378
+ if (exitSummary === null) {
379
+ return "session_vanished";
380
+ }
381
+
382
+ // 7. Stall detected (no STATUS.md progress) → stall_timeout
383
+ if (stallDetected) {
384
+ return "stall_timeout";
385
+ }
386
+
387
+ // 8. User manually killed the session → user_killed
388
+ if (userKilled) {
389
+ return "user_killed";
390
+ }
391
+
392
+ // 9. None of the above → unknown
393
+ return "unknown";
394
+ }