taskplane 0.28.4 → 0.28.6

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 (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -215
  3. package/bin/gitignore-patterns.mjs +79 -79
  4. package/bin/rpc-wrapper.mjs +1086 -1086
  5. package/bin/taskplane.mjs +3254 -3254
  6. package/dashboard/public/app.js +2573 -2573
  7. package/dashboard/public/index.html +139 -139
  8. package/dashboard/public/style.css +1882 -1882
  9. package/dashboard/public/taskplane-word-color.svg +18 -18
  10. package/dashboard/public/taskplane-word-white.svg +18 -18
  11. package/dashboard/server.cjs +1666 -1666
  12. package/extensions/reviewer-extension.ts +119 -119
  13. package/extensions/task-orchestrator.ts +28 -28
  14. package/extensions/taskplane/abort.ts +502 -502
  15. package/extensions/taskplane/agent-bridge-extension.ts +838 -765
  16. package/extensions/taskplane/agent-host.ts +833 -745
  17. package/extensions/taskplane/cleanup.ts +747 -747
  18. package/extensions/taskplane/config-loader.ts +1328 -1322
  19. package/extensions/taskplane/config-schema.ts +692 -682
  20. package/extensions/taskplane/config.ts +73 -73
  21. package/extensions/taskplane/context-window.ts +66 -66
  22. package/extensions/taskplane/diagnostic-reports.ts +463 -463
  23. package/extensions/taskplane/diagnostics.ts +385 -385
  24. package/extensions/taskplane/engine-worker-entry.mjs +34 -34
  25. package/extensions/taskplane/engine-worker.ts +381 -381
  26. package/extensions/taskplane/engine.ts +4539 -4527
  27. package/extensions/taskplane/execution.ts +2733 -2708
  28. package/extensions/taskplane/extension.ts +30 -9
  29. package/extensions/taskplane/formatting.ts +773 -773
  30. package/extensions/taskplane/git.ts +90 -90
  31. package/extensions/taskplane/index.ts +28 -28
  32. package/extensions/taskplane/lane-runner.ts +1383 -1360
  33. package/extensions/taskplane/mailbox.ts +689 -689
  34. package/extensions/taskplane/merge.ts +3135 -3135
  35. package/extensions/taskplane/messages.ts +985 -985
  36. package/extensions/taskplane/migrations.ts +278 -278
  37. package/extensions/taskplane/naming.ts +117 -117
  38. package/extensions/taskplane/path-resolver.ts +237 -237
  39. package/extensions/taskplane/persistence.ts +2087 -2087
  40. package/extensions/taskplane/process-registry.ts +416 -416
  41. package/extensions/taskplane/quality-gate.ts +1033 -1033
  42. package/extensions/taskplane/resume.ts +2879 -2878
  43. package/extensions/taskplane/sessions.ts +57 -57
  44. package/extensions/taskplane/settings-loader.ts +136 -136
  45. package/extensions/taskplane/settings-tui.ts +1867 -1867
  46. package/extensions/taskplane/sidecar-telemetry.ts +252 -252
  47. package/extensions/taskplane/supervisor-primer.md +1694 -1694
  48. package/extensions/taskplane/supervisor.ts +4341 -4341
  49. package/extensions/taskplane/task-executor-core.ts +550 -550
  50. package/extensions/taskplane/tmux-compat.ts +37 -37
  51. package/extensions/taskplane/types.ts +4297 -4278
  52. package/extensions/taskplane/verification.ts +542 -542
  53. package/extensions/taskplane/waves.ts +1548 -1548
  54. package/extensions/taskplane/workspace.ts +705 -705
  55. package/extensions/taskplane/worktree.ts +2604 -2505
  56. package/package.json +57 -57
  57. package/skills/create-taskplane-task/SKILL.md +465 -465
  58. package/skills/create-taskplane-task/references/prompt-template.md +285 -285
  59. package/templates/agents/local/supervisor.md +33 -33
  60. package/templates/agents/local/task-merger.md +27 -27
  61. package/templates/agents/local/task-reviewer.md +30 -30
  62. package/templates/agents/local/task-worker.md +34 -34
  63. package/templates/agents/supervisor-routing.md +92 -92
  64. package/templates/agents/supervisor.md +168 -168
  65. package/templates/agents/task-merger.md +214 -214
  66. package/templates/agents/task-reviewer.md +192 -192
  67. package/templates/agents/task-worker.md +505 -429
  68. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -98
  69. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -73
  70. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -97
  71. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -73
@@ -1,385 +1,385 @@
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/taskplane/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
- * | `unknown` | Could not determine cause |
56
- */
57
- export type ExitClassification =
58
- | "completed"
59
- | "api_error"
60
- | "model_access_error"
61
- | "context_overflow"
62
- | "wall_clock_timeout"
63
- | "process_crash"
64
- | "session_vanished"
65
- | "stall_timeout"
66
- | "user_killed"
67
- | "unknown";
68
-
69
- /**
70
- * All classification values as a readonly array, for iteration and validation.
71
- */
72
- export const EXIT_CLASSIFICATIONS: readonly ExitClassification[] = [
73
- "completed",
74
- "api_error",
75
- "model_access_error",
76
- "context_overflow",
77
- "wall_clock_timeout",
78
- "process_crash",
79
- "session_vanished",
80
- "stall_timeout",
81
- "user_killed",
82
- "unknown",
83
- ] as const;
84
-
85
- // ── Retry Record ─────────────────────────────────────────────────────
86
-
87
- /**
88
- * A single API retry event from the RPC wrapper's exit summary.
89
- *
90
- * Captured from `auto_retry_start/end` RPC events.
91
- */
92
- export interface RetryRecord {
93
- /** Retry attempt number (1-indexed) */
94
- attempt: number;
95
- /** Error message that triggered the retry */
96
- error: string;
97
- /** Delay in milliseconds before retrying */
98
- delayMs: number;
99
- /** Whether the retry succeeded */
100
- succeeded: boolean;
101
- }
102
-
103
- // ── Exit Summary ─────────────────────────────────────────────────────
104
-
105
- /**
106
- * Exit summary written by rpc-wrapper.mjs on process exit.
107
- *
108
- * This is the wrapper's output artifact — a JSON file capturing
109
- * everything the wrapper observed during the session. The task-runner
110
- * reads this to build `TaskExitDiagnostic`.
111
- *
112
- * **Field optionality rationale:**
113
- * The wrapper initializes counters (toolCalls, compactions, durationSec,
114
- * retries) at startup, so they are always present even on crash — these
115
- * are required. Fields that depend on RPC event accumulation (tokens,
116
- * cost, lastToolCall, error) are nullable — they may be absent if the
117
- * process crashes before capturing any events. `exitCode` and
118
- * `exitSignal` are optional (`?`) because the wrapper may crash before
119
- * the Node exit handler fires, producing a partial JSON artifact that
120
- * `JSON.parse()` succeeds on but lacks these fields.
121
- *
122
- * Consumers MUST use `typeof` guards on optional/nullable fields before
123
- * branching (e.g., `typeof exitCode === "number"` rather than `!== null`).
124
- */
125
- export interface ExitSummary {
126
- /** Process exit code. Optional — may be absent if wrapper crashes before exit handler fires. Null if killed by signal. */
127
- exitCode?: number | null;
128
- /** Signal that killed the process (e.g., "SIGTERM"). Optional — may be absent on crash. Null if clean exit. */
129
- exitSignal?: string | null;
130
- /** Accumulated token counts across all turns (null if no message_end events received) */
131
- tokens: SessionTokenCounts | null;
132
- /** Total cost in USD (null if no cost data received) */
133
- cost: number | null;
134
- /** Total tool calls made (initialized to 0 at startup) */
135
- toolCalls: number;
136
- /** API retry events observed (initialized to [] at startup) */
137
- retries: RetryRecord[];
138
- /** Number of context compactions observed (initialized to 0 at startup) */
139
- compactions: number;
140
- /** Wall-clock duration of the session in seconds (always written, even on crash) */
141
- durationSec: number;
142
- /** Last tool call description (e.g., "bash: node --test tests/*.test.ts"), null if no tools were called */
143
- lastToolCall: string | null;
144
- /** Error message if the session ended with an error, null on clean exit */
145
- error: string | null;
146
- }
147
-
148
- // ── Classification Input ─────────────────────────────────────────────
149
-
150
- /**
151
- * Structured input to `classifyExit()`.
152
- *
153
- * Aggregates all signals needed for deterministic classification.
154
- * Sources:
155
- * - `exitSummary`: from rpc-wrapper.mjs exit summary JSON (null if file missing)
156
- * - `doneFileFound`: from .DONE file presence check (task-runner)
157
- * - `timerKilled`: true if task-runner's max_worker_minutes timer killed the session
158
- * - `contextKilled`: true if the task-runner explicitly killed the session due to context limit
159
- * - `stallDetected`: true if monitoring detected no STATUS.md progress
160
- * - `userKilled`: true if user manually killed the session (e.g., /orch-abort, forced process kill)
161
- * - `contextPct`: estimated context utilization % (0-100), null if unknown
162
- *
163
- * Design: single structured input object (not positional args) for
164
- * extensibility as new signals are added in future phases.
165
- */
166
- export interface ExitClassificationInput {
167
- /** Exit summary from rpc-wrapper.mjs. Null if the summary file was not found. */
168
- exitSummary: ExitSummary | null;
169
- /** Whether the .DONE file was found in the task folder */
170
- doneFileFound: boolean;
171
- /** Whether the task-runner's wall-clock timer killed the session */
172
- timerKilled: boolean;
173
- /** Whether the task-runner explicitly killed the session due to context limit (TP-026) */
174
- contextKilled?: boolean;
175
- /** Whether monitoring detected a stall (no STATUS.md progress) */
176
- stallDetected: boolean;
177
- /** Whether the user manually killed the session */
178
- userKilled: boolean;
179
- /** Estimated context utilization percentage (0-100), null if unknown */
180
- contextPct: number | null;
181
- }
182
-
183
- // ── Task Exit Diagnostic ─────────────────────────────────────────────
184
-
185
- /**
186
- * Structured diagnostic for a task session's exit.
187
- *
188
- * Sits alongside the legacy `exitReason: string` on `LaneTaskOutcome`
189
- * during the transition period (Phase 1). Promoted to canonical in
190
- * schema v3 (Phase 3).
191
- *
192
- * Produced by calling `classifyExit()` after the session ends, then
193
- * enriching with progress/context metadata from STATUS.md and git.
194
- */
195
- export interface TaskExitDiagnostic {
196
- /** Deterministic exit classification */
197
- classification: ExitClassification;
198
- /** Process exit code (null if killed by signal or summary missing) */
199
- exitCode: number | null;
200
- /** Human-readable error message (null if clean exit) */
201
- errorMessage: string | null;
202
- /** Token usage breakdown (null if no summary available) */
203
- tokensUsed: SessionTokenCounts | null;
204
- /** Estimated context utilization percentage (0-100, null if unknown) */
205
- contextPct: number | null;
206
- /** Number of commits on the task branch (partial progress indicator) */
207
- partialProgressCommits: number;
208
- /** Branch name with partial progress (null if no branch) */
209
- partialProgressBranch: string | null;
210
- /** Wall-clock duration of the session in seconds */
211
- durationSec: number;
212
- /** Last known step number from STATUS.md (null if unparsed) */
213
- lastKnownStep: number | null;
214
- /** Last known checkbox text from STATUS.md (null if unparsed) */
215
- lastKnownCheckbox: string | null;
216
- /** Repo identifier ("default" in repo mode, repo key in workspace mode) */
217
- repoId: string;
218
- }
219
-
220
- // ── Classification Logic ─────────────────────────────────────────────
221
-
222
- /**
223
- * Threshold for context utilization percentage to consider "high".
224
- * Used in the `context_overflow` classification path:
225
- * compactions > 0 AND contextPct >= this threshold → context_overflow.
226
- */
227
- export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
228
-
229
- /**
230
- * Patterns that indicate a model access error (as opposed to a generic API error).
231
- *
232
- * These patterns match error messages from API providers when:
233
- * - The model is not found or deprecated
234
- * - Authentication/authorization fails (HTTP 401/403)
235
- * - Rate limits are hit specifically for the model (HTTP 429)
236
- * - API key is expired or invalid
237
- *
238
- * The patterns are case-insensitive and tested against the error string.
239
- *
240
- * @since TP-055
241
- */
242
- export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
243
- /\b(?:401|403)\b/, // HTTP auth/forbidden status codes
244
- /\b429\b/, // HTTP rate limit
245
- /model[_ ]not[_ ]found/i, // Model not found
246
- /model[_ ](?:is[_ ])?unavailable/i, // Model unavailable
247
- /model[_ ](?:has[_ ]been[_ ])?deprecated/i, // Model deprecated
248
- /api[_ ]key[_ ](?:expired|invalid|revoked)/i, // API key issues
249
- /invalid[_ ]api[_ ]key/i, // Invalid API key (alternate phrasing)
250
- /authentication[_ ](?:failed|error|required)/i, // Auth failures
251
- /authorization[_ ](?:failed|error|denied)/i, // Authz failures
252
- /access[_ ]denied/i, // Generic access denied
253
- /permission[_ ]denied/i, // Permission denied
254
- /quota[_ ]exceeded/i, // Quota exceeded
255
- /rate[_ ]limit/i, // Rate limit (phrase)
256
- /insufficient[_ ]quota/i, // Insufficient quota
257
- ];
258
-
259
- /**
260
- * Test whether an error message indicates a model access error.
261
- *
262
- * Used by `classifyExit()` to distinguish model-specific failures from
263
- * generic API errors, enabling targeted fallback to the session model.
264
- *
265
- * @param errorMessage - Error message to test
266
- * @returns true if the error matches a model access pattern
267
- * @since TP-055
268
- */
269
- export function isModelAccessError(errorMessage: string): boolean {
270
- if (!errorMessage) return false;
271
- return MODEL_ACCESS_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage));
272
- }
273
-
274
- /**
275
- * Classify a task session's exit into a deterministic category.
276
- *
277
- * Uses a strict precedence order — the first matching condition wins.
278
- * This ensures deterministic results even when multiple signals are
279
- * present (e.g., a session that was both stalled AND crashed).
280
- *
281
- * **Classification precedence (highest → lowest):**
282
- *
283
- * | Priority | Condition | Result |
284
- * |----------|------------------------------------------------------|----------------------|
285
- * | 1 | `.DONE` file found | `completed` |
286
- * | 2a | Retries with model-access error pattern | `model_access_error` |
287
- * | 2b | Retries present with final retry failed | `api_error` |
288
- * | 2c | Error message has model-access pattern (no retries) | `model_access_error` |
289
- * | 3 | Compactions > 0 AND contextPct ≥ 90% | `context_overflow` |
290
- * | 3b | Task-runner explicitly context-killed | `context_overflow` |
291
- * | 4 | Timer killed the session | `wall_clock_timeout` |
292
- * | 5 | Non-zero exit code, no API error | `process_crash` |
293
- * | 6 | No exit summary file (session vanished) | `session_vanished` |
294
- * | 7 | Stall detected (no STATUS.md progress) | `stall_timeout` |
295
- * | 8 | User manually killed the session | `user_killed` |
296
- * | 9 | None of the above | `unknown` |
297
- *
298
- * **Tie-break rationale:**
299
- * - `.DONE` always wins because the task succeeded regardless of how messy
300
- * the session was (retries, compactions, etc.).
301
- * - `model_access_error` beats generic `api_error` because it's more specific
302
- * and enables targeted fallback (retry with session model).
303
- * - `api_error` beats `context_overflow` because API failures are more
304
- * actionable (auth fix, rate limit backoff).
305
- * - `wall_clock_timeout` beats `process_crash` because the timer kill
306
- * explains the non-zero exit code.
307
- * - `session_vanished` (no summary) is checked after exit-code-based
308
- * paths because those require the summary to exist.
309
- * - `stall_timeout` and `user_killed` are low-priority because they're
310
- * external signals that may co-occur with other conditions.
311
- *
312
- * @param input - Aggregated signals from the session exit
313
- * @returns The exit classification string
314
- */
315
- export function classifyExit(input: ExitClassificationInput): ExitClassification {
316
- const { exitSummary, doneFileFound, timerKilled, stallDetected, userKilled, contextPct } = input;
317
- const contextKilled = input.contextKilled ?? false;
318
-
319
- // 1. .DONE file found → completed (task succeeded, regardless of session state)
320
- if (doneFileFound) {
321
- return "completed";
322
- }
323
-
324
- // 2a. Retries present with model-access error pattern → model_access_error
325
- // 2b. Retries present with final retry failed → api_error
326
- if (exitSummary?.retries && exitSummary.retries.length > 0) {
327
- const lastRetry = exitSummary.retries[exitSummary.retries.length - 1];
328
- if (!lastRetry.succeeded) {
329
- // Check if the retry error indicates a model access issue
330
- if (isModelAccessError(lastRetry.error)) {
331
- return "model_access_error";
332
- }
333
- return "api_error";
334
- }
335
- }
336
-
337
- // 2c. Error message (no retries) indicates model access issue → model_access_error
338
- if (exitSummary?.error && isModelAccessError(exitSummary.error)) {
339
- return "model_access_error";
340
- }
341
-
342
- // 3. Compactions > 0 AND high context utilization → context_overflow
343
- if (exitSummary && exitSummary.compactions > 0) {
344
- const effectivePct = contextPct ?? 0;
345
- if (effectivePct >= CONTEXT_OVERFLOW_THRESHOLD_PCT) {
346
- return "context_overflow";
347
- }
348
- }
349
-
350
- // 3b. Task-runner explicitly killed session due to context limit → context_overflow
351
- // Catches cases where exit summary is missing (wrapper crashed) or compactions=0
352
- // but the task-runner's own context guard triggered the kill.
353
- if (contextKilled) {
354
- return "context_overflow";
355
- }
356
-
357
- // 4. Task-runner's wall-clock timer killed the session → wall_clock_timeout
358
- if (timerKilled) {
359
- return "wall_clock_timeout";
360
- }
361
-
362
- // 5. Non-zero exit code, no API error indicators → process_crash
363
- // Guard with typeof to handle partial summaries where exitCode may be undefined
364
- if (exitSummary && typeof exitSummary.exitCode === "number" && exitSummary.exitCode !== 0) {
365
- return "process_crash";
366
- }
367
-
368
- // 6. No exit summary file found → session_vanished
369
- if (exitSummary === null) {
370
- return "session_vanished";
371
- }
372
-
373
- // 7. Stall detected (no STATUS.md progress) → stall_timeout
374
- if (stallDetected) {
375
- return "stall_timeout";
376
- }
377
-
378
- // 8. User manually killed the session → user_killed
379
- if (userKilled) {
380
- return "user_killed";
381
- }
382
-
383
- // 9. None of the above → unknown
384
- return "unknown";
385
- }
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/taskplane/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
+ * | `unknown` | Could not determine cause |
56
+ */
57
+ export type ExitClassification =
58
+ | "completed"
59
+ | "api_error"
60
+ | "model_access_error"
61
+ | "context_overflow"
62
+ | "wall_clock_timeout"
63
+ | "process_crash"
64
+ | "session_vanished"
65
+ | "stall_timeout"
66
+ | "user_killed"
67
+ | "unknown";
68
+
69
+ /**
70
+ * All classification values as a readonly array, for iteration and validation.
71
+ */
72
+ export const EXIT_CLASSIFICATIONS: readonly ExitClassification[] = [
73
+ "completed",
74
+ "api_error",
75
+ "model_access_error",
76
+ "context_overflow",
77
+ "wall_clock_timeout",
78
+ "process_crash",
79
+ "session_vanished",
80
+ "stall_timeout",
81
+ "user_killed",
82
+ "unknown",
83
+ ] as const;
84
+
85
+ // ── Retry Record ─────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * A single API retry event from the RPC wrapper's exit summary.
89
+ *
90
+ * Captured from `auto_retry_start/end` RPC events.
91
+ */
92
+ export interface RetryRecord {
93
+ /** Retry attempt number (1-indexed) */
94
+ attempt: number;
95
+ /** Error message that triggered the retry */
96
+ error: string;
97
+ /** Delay in milliseconds before retrying */
98
+ delayMs: number;
99
+ /** Whether the retry succeeded */
100
+ succeeded: boolean;
101
+ }
102
+
103
+ // ── Exit Summary ─────────────────────────────────────────────────────
104
+
105
+ /**
106
+ * Exit summary written by rpc-wrapper.mjs on process exit.
107
+ *
108
+ * This is the wrapper's output artifact — a JSON file capturing
109
+ * everything the wrapper observed during the session. The task-runner
110
+ * reads this to build `TaskExitDiagnostic`.
111
+ *
112
+ * **Field optionality rationale:**
113
+ * The wrapper initializes counters (toolCalls, compactions, durationSec,
114
+ * retries) at startup, so they are always present even on crash — these
115
+ * are required. Fields that depend on RPC event accumulation (tokens,
116
+ * cost, lastToolCall, error) are nullable — they may be absent if the
117
+ * process crashes before capturing any events. `exitCode` and
118
+ * `exitSignal` are optional (`?`) because the wrapper may crash before
119
+ * the Node exit handler fires, producing a partial JSON artifact that
120
+ * `JSON.parse()` succeeds on but lacks these fields.
121
+ *
122
+ * Consumers MUST use `typeof` guards on optional/nullable fields before
123
+ * branching (e.g., `typeof exitCode === "number"` rather than `!== null`).
124
+ */
125
+ export interface ExitSummary {
126
+ /** Process exit code. Optional — may be absent if wrapper crashes before exit handler fires. Null if killed by signal. */
127
+ exitCode?: number | null;
128
+ /** Signal that killed the process (e.g., "SIGTERM"). Optional — may be absent on crash. Null if clean exit. */
129
+ exitSignal?: string | null;
130
+ /** Accumulated token counts across all turns (null if no message_end events received) */
131
+ tokens: SessionTokenCounts | null;
132
+ /** Total cost in USD (null if no cost data received) */
133
+ cost: number | null;
134
+ /** Total tool calls made (initialized to 0 at startup) */
135
+ toolCalls: number;
136
+ /** API retry events observed (initialized to [] at startup) */
137
+ retries: RetryRecord[];
138
+ /** Number of context compactions observed (initialized to 0 at startup) */
139
+ compactions: number;
140
+ /** Wall-clock duration of the session in seconds (always written, even on crash) */
141
+ durationSec: number;
142
+ /** Last tool call description (e.g., "bash: node --test tests/*.test.ts"), null if no tools were called */
143
+ lastToolCall: string | null;
144
+ /** Error message if the session ended with an error, null on clean exit */
145
+ error: string | null;
146
+ }
147
+
148
+ // ── Classification Input ─────────────────────────────────────────────
149
+
150
+ /**
151
+ * Structured input to `classifyExit()`.
152
+ *
153
+ * Aggregates all signals needed for deterministic classification.
154
+ * Sources:
155
+ * - `exitSummary`: from rpc-wrapper.mjs exit summary JSON (null if file missing)
156
+ * - `doneFileFound`: from .DONE file presence check (task-runner)
157
+ * - `timerKilled`: true if task-runner's max_worker_minutes timer killed the session
158
+ * - `contextKilled`: true if the task-runner explicitly killed the session due to context limit
159
+ * - `stallDetected`: true if monitoring detected no STATUS.md progress
160
+ * - `userKilled`: true if user manually killed the session (e.g., /orch-abort, forced process kill)
161
+ * - `contextPct`: estimated context utilization % (0-100), null if unknown
162
+ *
163
+ * Design: single structured input object (not positional args) for
164
+ * extensibility as new signals are added in future phases.
165
+ */
166
+ export interface ExitClassificationInput {
167
+ /** Exit summary from rpc-wrapper.mjs. Null if the summary file was not found. */
168
+ exitSummary: ExitSummary | null;
169
+ /** Whether the .DONE file was found in the task folder */
170
+ doneFileFound: boolean;
171
+ /** Whether the task-runner's wall-clock timer killed the session */
172
+ timerKilled: boolean;
173
+ /** Whether the task-runner explicitly killed the session due to context limit (TP-026) */
174
+ contextKilled?: boolean;
175
+ /** Whether monitoring detected a stall (no STATUS.md progress) */
176
+ stallDetected: boolean;
177
+ /** Whether the user manually killed the session */
178
+ userKilled: boolean;
179
+ /** Estimated context utilization percentage (0-100), null if unknown */
180
+ contextPct: number | null;
181
+ }
182
+
183
+ // ── Task Exit Diagnostic ─────────────────────────────────────────────
184
+
185
+ /**
186
+ * Structured diagnostic for a task session's exit.
187
+ *
188
+ * Sits alongside the legacy `exitReason: string` on `LaneTaskOutcome`
189
+ * during the transition period (Phase 1). Promoted to canonical in
190
+ * schema v3 (Phase 3).
191
+ *
192
+ * Produced by calling `classifyExit()` after the session ends, then
193
+ * enriching with progress/context metadata from STATUS.md and git.
194
+ */
195
+ export interface TaskExitDiagnostic {
196
+ /** Deterministic exit classification */
197
+ classification: ExitClassification;
198
+ /** Process exit code (null if killed by signal or summary missing) */
199
+ exitCode: number | null;
200
+ /** Human-readable error message (null if clean exit) */
201
+ errorMessage: string | null;
202
+ /** Token usage breakdown (null if no summary available) */
203
+ tokensUsed: SessionTokenCounts | null;
204
+ /** Estimated context utilization percentage (0-100, null if unknown) */
205
+ contextPct: number | null;
206
+ /** Number of commits on the task branch (partial progress indicator) */
207
+ partialProgressCommits: number;
208
+ /** Branch name with partial progress (null if no branch) */
209
+ partialProgressBranch: string | null;
210
+ /** Wall-clock duration of the session in seconds */
211
+ durationSec: number;
212
+ /** Last known step number from STATUS.md (null if unparsed) */
213
+ lastKnownStep: number | null;
214
+ /** Last known checkbox text from STATUS.md (null if unparsed) */
215
+ lastKnownCheckbox: string | null;
216
+ /** Repo identifier ("default" in repo mode, repo key in workspace mode) */
217
+ repoId: string;
218
+ }
219
+
220
+ // ── Classification Logic ─────────────────────────────────────────────
221
+
222
+ /**
223
+ * Threshold for context utilization percentage to consider "high".
224
+ * Used in the `context_overflow` classification path:
225
+ * compactions > 0 AND contextPct >= this threshold → context_overflow.
226
+ */
227
+ export const CONTEXT_OVERFLOW_THRESHOLD_PCT = 90;
228
+
229
+ /**
230
+ * Patterns that indicate a model access error (as opposed to a generic API error).
231
+ *
232
+ * These patterns match error messages from API providers when:
233
+ * - The model is not found or deprecated
234
+ * - Authentication/authorization fails (HTTP 401/403)
235
+ * - Rate limits are hit specifically for the model (HTTP 429)
236
+ * - API key is expired or invalid
237
+ *
238
+ * The patterns are case-insensitive and tested against the error string.
239
+ *
240
+ * @since TP-055
241
+ */
242
+ export const MODEL_ACCESS_ERROR_PATTERNS: readonly RegExp[] = [
243
+ /\b(?:401|403)\b/, // HTTP auth/forbidden status codes
244
+ /\b429\b/, // HTTP rate limit
245
+ /model[_ ]not[_ ]found/i, // Model not found
246
+ /model[_ ](?:is[_ ])?unavailable/i, // Model unavailable
247
+ /model[_ ](?:has[_ ]been[_ ])?deprecated/i, // Model deprecated
248
+ /api[_ ]key[_ ](?:expired|invalid|revoked)/i, // API key issues
249
+ /invalid[_ ]api[_ ]key/i, // Invalid API key (alternate phrasing)
250
+ /authentication[_ ](?:failed|error|required)/i, // Auth failures
251
+ /authorization[_ ](?:failed|error|denied)/i, // Authz failures
252
+ /access[_ ]denied/i, // Generic access denied
253
+ /permission[_ ]denied/i, // Permission denied
254
+ /quota[_ ]exceeded/i, // Quota exceeded
255
+ /rate[_ ]limit/i, // Rate limit (phrase)
256
+ /insufficient[_ ]quota/i, // Insufficient quota
257
+ ];
258
+
259
+ /**
260
+ * Test whether an error message indicates a model access error.
261
+ *
262
+ * Used by `classifyExit()` to distinguish model-specific failures from
263
+ * generic API errors, enabling targeted fallback to the session model.
264
+ *
265
+ * @param errorMessage - Error message to test
266
+ * @returns true if the error matches a model access pattern
267
+ * @since TP-055
268
+ */
269
+ export function isModelAccessError(errorMessage: string): boolean {
270
+ if (!errorMessage) return false;
271
+ return MODEL_ACCESS_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage));
272
+ }
273
+
274
+ /**
275
+ * Classify a task session's exit into a deterministic category.
276
+ *
277
+ * Uses a strict precedence order — the first matching condition wins.
278
+ * This ensures deterministic results even when multiple signals are
279
+ * present (e.g., a session that was both stalled AND crashed).
280
+ *
281
+ * **Classification precedence (highest → lowest):**
282
+ *
283
+ * | Priority | Condition | Result |
284
+ * |----------|------------------------------------------------------|----------------------|
285
+ * | 1 | `.DONE` file found | `completed` |
286
+ * | 2a | Retries with model-access error pattern | `model_access_error` |
287
+ * | 2b | Retries present with final retry failed | `api_error` |
288
+ * | 2c | Error message has model-access pattern (no retries) | `model_access_error` |
289
+ * | 3 | Compactions > 0 AND contextPct ≥ 90% | `context_overflow` |
290
+ * | 3b | Task-runner explicitly context-killed | `context_overflow` |
291
+ * | 4 | Timer killed the session | `wall_clock_timeout` |
292
+ * | 5 | Non-zero exit code, no API error | `process_crash` |
293
+ * | 6 | No exit summary file (session vanished) | `session_vanished` |
294
+ * | 7 | Stall detected (no STATUS.md progress) | `stall_timeout` |
295
+ * | 8 | User manually killed the session | `user_killed` |
296
+ * | 9 | None of the above | `unknown` |
297
+ *
298
+ * **Tie-break rationale:**
299
+ * - `.DONE` always wins because the task succeeded regardless of how messy
300
+ * the session was (retries, compactions, etc.).
301
+ * - `model_access_error` beats generic `api_error` because it's more specific
302
+ * and enables targeted fallback (retry with session model).
303
+ * - `api_error` beats `context_overflow` because API failures are more
304
+ * actionable (auth fix, rate limit backoff).
305
+ * - `wall_clock_timeout` beats `process_crash` because the timer kill
306
+ * explains the non-zero exit code.
307
+ * - `session_vanished` (no summary) is checked after exit-code-based
308
+ * paths because those require the summary to exist.
309
+ * - `stall_timeout` and `user_killed` are low-priority because they're
310
+ * external signals that may co-occur with other conditions.
311
+ *
312
+ * @param input - Aggregated signals from the session exit
313
+ * @returns The exit classification string
314
+ */
315
+ export function classifyExit(input: ExitClassificationInput): ExitClassification {
316
+ const { exitSummary, doneFileFound, timerKilled, stallDetected, userKilled, contextPct } = input;
317
+ const contextKilled = input.contextKilled ?? false;
318
+
319
+ // 1. .DONE file found → completed (task succeeded, regardless of session state)
320
+ if (doneFileFound) {
321
+ return "completed";
322
+ }
323
+
324
+ // 2a. Retries present with model-access error pattern → model_access_error
325
+ // 2b. Retries present with final retry failed → api_error
326
+ if (exitSummary?.retries && exitSummary.retries.length > 0) {
327
+ const lastRetry = exitSummary.retries[exitSummary.retries.length - 1];
328
+ if (!lastRetry.succeeded) {
329
+ // Check if the retry error indicates a model access issue
330
+ if (isModelAccessError(lastRetry.error)) {
331
+ return "model_access_error";
332
+ }
333
+ return "api_error";
334
+ }
335
+ }
336
+
337
+ // 2c. Error message (no retries) indicates model access issue → model_access_error
338
+ if (exitSummary?.error && isModelAccessError(exitSummary.error)) {
339
+ return "model_access_error";
340
+ }
341
+
342
+ // 3. Compactions > 0 AND high context utilization → context_overflow
343
+ if (exitSummary && exitSummary.compactions > 0) {
344
+ const effectivePct = contextPct ?? 0;
345
+ if (effectivePct >= CONTEXT_OVERFLOW_THRESHOLD_PCT) {
346
+ return "context_overflow";
347
+ }
348
+ }
349
+
350
+ // 3b. Task-runner explicitly killed session due to context limit → context_overflow
351
+ // Catches cases where exit summary is missing (wrapper crashed) or compactions=0
352
+ // but the task-runner's own context guard triggered the kill.
353
+ if (contextKilled) {
354
+ return "context_overflow";
355
+ }
356
+
357
+ // 4. Task-runner's wall-clock timer killed the session → wall_clock_timeout
358
+ if (timerKilled) {
359
+ return "wall_clock_timeout";
360
+ }
361
+
362
+ // 5. Non-zero exit code, no API error indicators → process_crash
363
+ // Guard with typeof to handle partial summaries where exitCode may be undefined
364
+ if (exitSummary && typeof exitSummary.exitCode === "number" && exitSummary.exitCode !== 0) {
365
+ return "process_crash";
366
+ }
367
+
368
+ // 6. No exit summary file found → session_vanished
369
+ if (exitSummary === null) {
370
+ return "session_vanished";
371
+ }
372
+
373
+ // 7. Stall detected (no STATUS.md progress) → stall_timeout
374
+ if (stallDetected) {
375
+ return "stall_timeout";
376
+ }
377
+
378
+ // 8. User manually killed the session → user_killed
379
+ if (userKilled) {
380
+ return "user_killed";
381
+ }
382
+
383
+ // 9. None of the above → unknown
384
+ return "unknown";
385
+ }