pi-crew 0.1.51 → 0.2.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 (239) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +176 -781
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +70 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/actions-reference.md +595 -0
  14. package/docs/commands-reference.md +347 -0
  15. package/docs/runtime-flow.md +148 -148
  16. package/index.ts +6 -6
  17. package/package.json +99 -99
  18. package/skills/async-worker-recovery/SKILL.md +42 -42
  19. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  20. package/skills/delegation-patterns/SKILL.md +54 -54
  21. package/skills/mailbox-interactive/SKILL.md +40 -40
  22. package/skills/model-routing-context/SKILL.md +39 -39
  23. package/skills/multi-perspective-review/SKILL.md +58 -58
  24. package/skills/observability-reliability/SKILL.md +41 -41
  25. package/skills/orchestration/SKILL.md +157 -157
  26. package/skills/ownership-session-security/SKILL.md +41 -41
  27. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  28. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  29. package/skills/resource-discovery-config/SKILL.md +41 -41
  30. package/skills/runtime-state-reader/SKILL.md +44 -44
  31. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  32. package/skills/state-mutation-locking/SKILL.md +42 -42
  33. package/skills/systematic-debugging/SKILL.md +67 -67
  34. package/skills/ui-render-performance/SKILL.md +39 -39
  35. package/skills/verification-before-done/SKILL.md +57 -57
  36. package/skills/worktree-isolation/SKILL.md +39 -39
  37. package/src/adapters/claude-adapter.ts +25 -0
  38. package/src/adapters/codex-adapter.ts +21 -0
  39. package/src/adapters/cursor-adapter.ts +17 -0
  40. package/src/adapters/export-util.ts +137 -0
  41. package/src/adapters/index.ts +15 -0
  42. package/src/adapters/registry.ts +18 -0
  43. package/src/adapters/types.ts +23 -0
  44. package/src/agents/agent-config.ts +2 -0
  45. package/src/agents/agent-search.ts +98 -98
  46. package/src/agents/discover-agents.ts +2 -1
  47. package/src/config/config.ts +13 -1
  48. package/src/config/drift-detector.ts +211 -0
  49. package/src/config/markers.ts +327 -0
  50. package/src/config/resilient-parser.ts +108 -0
  51. package/src/config/suggestions.ts +74 -0
  52. package/src/extension/cross-extension-rpc.ts +103 -94
  53. package/src/extension/project-init.ts +21 -1
  54. package/src/extension/register.ts +45 -14
  55. package/src/extension/registration/commands.ts +77 -8
  56. package/src/extension/registration/subagent-tools.ts +10 -1
  57. package/src/extension/registration/team-tool.ts +10 -1
  58. package/src/extension/registration/viewers.ts +48 -34
  59. package/src/extension/run-bundle-schema.ts +89 -89
  60. package/src/extension/run-import.ts +25 -1
  61. package/src/extension/run-index.ts +5 -1
  62. package/src/extension/run-maintenance.ts +142 -68
  63. package/src/extension/team-manager-command.ts +10 -1
  64. package/src/extension/team-tool/doctor.ts +28 -3
  65. package/src/extension/team-tool/handle-settings.ts +195 -188
  66. package/src/extension/team-tool/inspect.ts +41 -41
  67. package/src/extension/team-tool/intent-policy.ts +42 -42
  68. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  69. package/src/extension/team-tool/plan.ts +19 -19
  70. package/src/extension/team-tool/run.ts +12 -1
  71. package/src/extension/team-tool.ts +11 -1
  72. package/src/i18n.ts +184 -184
  73. package/src/observability/exporters/otlp-exporter.ts +92 -77
  74. package/src/prompt/prompt-runtime.ts +72 -72
  75. package/src/runtime/agent-memory.ts +72 -72
  76. package/src/runtime/agent-observability.ts +114 -114
  77. package/src/runtime/async-marker.ts +26 -26
  78. package/src/runtime/attention-events.ts +28 -28
  79. package/src/runtime/auto-resume.ts +100 -0
  80. package/src/runtime/background-runner.ts +11 -1
  81. package/src/runtime/cancellation-token.ts +89 -89
  82. package/src/runtime/cancellation.ts +61 -61
  83. package/src/runtime/capability-inventory.ts +116 -116
  84. package/src/runtime/child-pi.ts +7 -2
  85. package/src/runtime/compaction-summary.ts +271 -0
  86. package/src/runtime/completion-guard.ts +190 -190
  87. package/src/runtime/crash-recovery.ts +33 -0
  88. package/src/runtime/delta-conflict.ts +360 -0
  89. package/src/runtime/direct-run.ts +35 -35
  90. package/src/runtime/foreground-control.ts +82 -82
  91. package/src/runtime/green-contract.ts +46 -46
  92. package/src/runtime/group-join.ts +106 -106
  93. package/src/runtime/heartbeat-gradient.ts +28 -28
  94. package/src/runtime/heartbeat-watcher.ts +124 -124
  95. package/src/runtime/iteration-hooks.ts +262 -0
  96. package/src/runtime/live-agent-control.ts +88 -88
  97. package/src/runtime/live-control-realtime.ts +36 -36
  98. package/src/runtime/live-extension-bridge.ts +150 -150
  99. package/src/runtime/live-irc.ts +92 -92
  100. package/src/runtime/live-session-health.ts +100 -100
  101. package/src/runtime/loop-gates.ts +129 -0
  102. package/src/runtime/metric-parser.ts +40 -0
  103. package/src/runtime/notebook-helpers.ts +90 -90
  104. package/src/runtime/orphan-sentinel.ts +7 -7
  105. package/src/runtime/parallel-research.ts +44 -44
  106. package/src/runtime/phase-progress.ts +217 -0
  107. package/src/runtime/pi-args.ts +38 -11
  108. package/src/runtime/pi-json-output.ts +111 -111
  109. package/src/runtime/pi-spawn.ts +57 -7
  110. package/src/runtime/policy-engine.ts +79 -79
  111. package/src/runtime/post-checks.ts +122 -0
  112. package/src/runtime/progress-event-coalescer.ts +43 -43
  113. package/src/runtime/prose-compressor.ts +164 -164
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/result-extractor.ts +121 -121
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/sensitive-paths.ts +2 -2
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/stream-preview.ts +177 -177
  123. package/src/runtime/supervisor-contact.ts +59 -59
  124. package/src/runtime/task-display.ts +38 -38
  125. package/src/runtime/task-graph.ts +207 -0
  126. package/src/runtime/task-quality.ts +207 -0
  127. package/src/runtime/task-runner/capabilities.ts +78 -78
  128. package/src/runtime/task-runner/live-executor.ts +7 -1
  129. package/src/runtime/task-runner/progress.ts +119 -119
  130. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  131. package/src/runtime/task-runner/result-utils.ts +14 -14
  132. package/src/runtime/task-runner/run-projection.ts +103 -103
  133. package/src/runtime/task-runner/state-helpers.ts +22 -22
  134. package/src/runtime/team-runner.ts +117 -7
  135. package/src/runtime/worker-heartbeat.ts +21 -21
  136. package/src/runtime/worker-startup.ts +57 -57
  137. package/src/runtime/workflow-state.ts +187 -0
  138. package/src/runtime/workspace-tree.ts +298 -298
  139. package/src/schema/config-schema.ts +11 -0
  140. package/src/schema/validation-types.ts +148 -0
  141. package/src/skills/skill-templates.ts +374 -0
  142. package/src/state/active-run-registry.ts +35 -11
  143. package/src/state/atomic-write.ts +33 -26
  144. package/src/state/contracts.ts +1 -0
  145. package/src/state/event-reconstructor.ts +217 -0
  146. package/src/state/locks.ts +2 -13
  147. package/src/state/mailbox.ts +4 -3
  148. package/src/state/state-store.ts +32 -14
  149. package/src/state/task-claims.ts +44 -44
  150. package/src/state/types.ts +9 -0
  151. package/src/state/usage.ts +29 -29
  152. package/src/subagents/async-entry.ts +1 -1
  153. package/src/subagents/index.ts +3 -3
  154. package/src/subagents/live/control.ts +1 -1
  155. package/src/subagents/live/manager.ts +1 -1
  156. package/src/subagents/live/realtime.ts +1 -1
  157. package/src/subagents/live/session-runtime.ts +1 -1
  158. package/src/subagents/manager.ts +1 -1
  159. package/src/subagents/spawn.ts +1 -1
  160. package/src/teams/team-serializer.ts +38 -38
  161. package/src/types/diff.d.ts +18 -18
  162. package/src/ui/crew-footer.ts +101 -101
  163. package/src/ui/crew-select-list.ts +111 -111
  164. package/src/ui/crew-widget.ts +5 -2
  165. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  166. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  167. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  168. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  169. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  170. package/src/ui/dynamic-border.ts +25 -25
  171. package/src/ui/layout-primitives.ts +106 -106
  172. package/src/ui/loaders.ts +158 -158
  173. package/src/ui/render-coalescer.ts +51 -51
  174. package/src/ui/render-diff.ts +119 -119
  175. package/src/ui/render-scheduler.ts +143 -143
  176. package/src/ui/run-action-dispatcher.ts +10 -1
  177. package/src/ui/spinner.ts +17 -17
  178. package/src/ui/status-colors.ts +58 -58
  179. package/src/ui/syntax-highlight.ts +116 -116
  180. package/src/ui/transcript-entries.ts +258 -258
  181. package/src/utils/completion-dedupe.ts +63 -63
  182. package/src/utils/frontmatter.ts +68 -68
  183. package/src/utils/git.ts +262 -262
  184. package/src/utils/ids.ts +17 -17
  185. package/src/utils/incremental-reader.ts +104 -104
  186. package/src/utils/names.ts +27 -27
  187. package/src/utils/redaction.ts +44 -44
  188. package/src/utils/safe-paths.ts +47 -47
  189. package/src/utils/scan-cache.ts +136 -136
  190. package/src/utils/sleep.ts +40 -26
  191. package/src/utils/task-name-generator.ts +337 -337
  192. package/src/workflows/validate-workflow.ts +40 -40
  193. package/src/worktree/branch-freshness.ts +45 -45
  194. package/teams/default.team.md +12 -12
  195. package/teams/fast-fix.team.md +11 -11
  196. package/teams/implementation.team.md +18 -18
  197. package/teams/parallel-research.team.md +14 -14
  198. package/teams/research.team.md +11 -11
  199. package/teams/review.team.md +12 -12
  200. package/workflows/default.workflow.md +30 -29
  201. package/workflows/fast-fix.workflow.md +23 -22
  202. package/workflows/implementation.workflow.md +43 -43
  203. package/workflows/parallel-research.workflow.md +46 -46
  204. package/workflows/research.workflow.md +22 -22
  205. package/workflows/review.workflow.md +30 -30
  206. package/docs/refactor-tasks-phase3.md +0 -394
  207. package/docs/refactor-tasks-phase4.md +0 -564
  208. package/docs/refactor-tasks-phase5.md +0 -402
  209. package/docs/refactor-tasks-phase6.md +0 -662
  210. package/docs/refactor-tasks.md +0 -1484
  211. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  212. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  213. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  214. package/docs/research/AUDIT_PI_CREW.md +0 -457
  215. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  216. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  217. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  218. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  219. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  220. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  221. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  222. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  223. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  224. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  225. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  226. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  227. package/docs/research-extension-examples.md +0 -297
  228. package/docs/research-extension-system.md +0 -324
  229. package/docs/research-oh-my-pi-distillation.md +0 -369
  230. package/docs/research-optimization-plan.md +0 -548
  231. package/docs/research-phase10-distillation.md +0 -199
  232. package/docs/research-phase11-distillation.md +0 -201
  233. package/docs/research-phase8-operator-experience-plan.md +0 -819
  234. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  235. package/docs/research-pi-coding-agent.md +0 -357
  236. package/docs/research-source-pi-crew-reference.md +0 -174
  237. package/docs/research-ui-optimization-plan.md +0 -480
  238. package/docs/source-runtime-refactor-map.md +0 -107
  239. package/src/utils/atomic-write.ts +0 -33
@@ -1,104 +1,104 @@
1
- import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
- import type { MailboxMessage } from "../../state/mailbox.ts";
3
- import type { ArtifactDescriptor } from "../../state/types.ts";
4
-
5
- export interface RunProjectionSource {
6
- kind: "events" | "mailbox" | "artifacts" | "ui_metadata" | "runtime_metadata";
7
- bounded: boolean;
8
- reference?: string;
9
- }
10
-
11
- export interface RunProjectionResult {
12
- sources: RunProjectionSource[];
13
- summary: string;
14
- injectedAsContext: boolean;
15
- }
16
-
17
- /**
18
- * Transform run context before a worker starts.
19
- * Builds a bounded projection of durable history that will be available
20
- * to the worker as reference context, not as instructions.
21
- *
22
- * Rules:
23
- * - Durable history retains events, mailbox, artifacts, UI/runtime metadata.
24
- * - Worker prompt gets a bounded projection (truncated/summarized).
25
- * - UI/runtime events are not prompt text unless explicitly selected.
26
- */
27
- export function transformRunContextBeforeWorkerStart(input: {
28
- manifest: TeamRunManifest;
29
- tasks: TeamTaskState[];
30
- pendingMailbox: MailboxMessage[];
31
- artifacts: ArtifactDescriptor[];
32
- maxEvents?: number;
33
- maxMailboxMessages?: number;
34
- maxArtifactRefs?: number;
35
- }): RunProjectionResult {
36
- const maxEvents = input.maxEvents ?? 20;
37
- const maxMailbox = input.maxMailboxMessages ?? 10;
38
- const maxArtifacts = input.maxArtifactRefs ?? 15;
39
-
40
- const sources: RunProjectionSource[] = [];
41
- const lines: string[] = [];
42
-
43
- // Project a bounded slice of task history
44
- const completedTasks = input.tasks.filter((t) => t.status === "completed" || t.status === "failed");
45
- if (completedTasks.length > 0) {
46
- const tasks = completedTasks.slice(0, maxEvents);
47
- sources.push({ kind: "events", bounded: true, reference: `tasks:${tasks.length}/${completedTasks.length}` });
48
- lines.push(`Previous tasks (${tasks.length}/${completedTasks.length}):`);
49
- for (const task of tasks) {
50
- lines.push(`- ${task.id}: ${task.status}${task.error ? ` (${task.error})` : ""}`);
51
- }
52
- }
53
-
54
- // Project pending mailbox that is relevant to this worker
55
- if (input.pendingMailbox.length > 0) {
56
- const messages = input.pendingMailbox.slice(0, maxMailbox);
57
- sources.push({ kind: "mailbox", bounded: true, reference: `mailbox:${messages.length}/${input.pendingMailbox.length}` });
58
- lines.push(`Pending messages (${messages.length}/${input.pendingMailbox.length}):`);
59
- for (const msg of messages) {
60
- lines.push(`- ${msg.kind ?? "message"}: ${msg.body.slice(0, 100)}`);
61
- }
62
- }
63
-
64
- // Project artifact references (not content)
65
- if (input.artifacts.length > 0) {
66
- const artifacts = input.artifacts.slice(0, maxArtifacts);
67
- sources.push({ kind: "artifacts", bounded: true, reference: `artifacts:${artifacts.length}/${input.artifacts.length}` });
68
- lines.push(`Available artifacts (${artifacts.length}/${input.artifacts.length}):`);
69
- for (const art of artifacts) {
70
- lines.push(`- ${art.kind} (${art.producer})`);
71
- }
72
- }
73
-
74
- // Metadata markers — not injected as prompt instructions
75
- sources.push({ kind: "ui_metadata", bounded: false, reference: "excluded_from_prompt" });
76
- sources.push({ kind: "runtime_metadata", bounded: false, reference: "excluded_from_prompt" });
77
-
78
- return {
79
- sources,
80
- summary: lines.join("\n"),
81
- injectedAsContext: true,
82
- };
83
- }
84
-
85
- /**
86
- * Convert run history to a bounded worker prompt section.
87
- * Same logic as transformRunContextBeforeWorkerStart but returns
88
- * the prompt text directly for embedding in the worker prompt.
89
- */
90
- export function convertRunHistoryToWorkerPrompt(input: {
91
- manifest: TeamRunManifest;
92
- tasks: TeamTaskState[];
93
- pendingMailbox: MailboxMessage[];
94
- artifacts: ArtifactDescriptor[];
95
- }): string {
96
- const projection = transformRunContextBeforeWorkerStart(input);
97
- if (!projection.summary) return "";
98
- return [
99
- "## Run Context (bounded projection)",
100
- projection.summary,
101
- "",
102
- `Projection sources: ${projection.sources.map((s) => s.kind).join(", ")}`,
103
- ].join("\n");
1
+ import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
+ import type { MailboxMessage } from "../../state/mailbox.ts";
3
+ import type { ArtifactDescriptor } from "../../state/types.ts";
4
+
5
+ export interface RunProjectionSource {
6
+ kind: "events" | "mailbox" | "artifacts" | "ui_metadata" | "runtime_metadata";
7
+ bounded: boolean;
8
+ reference?: string;
9
+ }
10
+
11
+ export interface RunProjectionResult {
12
+ sources: RunProjectionSource[];
13
+ summary: string;
14
+ injectedAsContext: boolean;
15
+ }
16
+
17
+ /**
18
+ * Transform run context before a worker starts.
19
+ * Builds a bounded projection of durable history that will be available
20
+ * to the worker as reference context, not as instructions.
21
+ *
22
+ * Rules:
23
+ * - Durable history retains events, mailbox, artifacts, UI/runtime metadata.
24
+ * - Worker prompt gets a bounded projection (truncated/summarized).
25
+ * - UI/runtime events are not prompt text unless explicitly selected.
26
+ */
27
+ export function transformRunContextBeforeWorkerStart(input: {
28
+ manifest: TeamRunManifest;
29
+ tasks: TeamTaskState[];
30
+ pendingMailbox: MailboxMessage[];
31
+ artifacts: ArtifactDescriptor[];
32
+ maxEvents?: number;
33
+ maxMailboxMessages?: number;
34
+ maxArtifactRefs?: number;
35
+ }): RunProjectionResult {
36
+ const maxEvents = input.maxEvents ?? 20;
37
+ const maxMailbox = input.maxMailboxMessages ?? 10;
38
+ const maxArtifacts = input.maxArtifactRefs ?? 15;
39
+
40
+ const sources: RunProjectionSource[] = [];
41
+ const lines: string[] = [];
42
+
43
+ // Project a bounded slice of task history
44
+ const completedTasks = input.tasks.filter((t) => t.status === "completed" || t.status === "failed");
45
+ if (completedTasks.length > 0) {
46
+ const tasks = completedTasks.slice(0, maxEvents);
47
+ sources.push({ kind: "events", bounded: true, reference: `tasks:${tasks.length}/${completedTasks.length}` });
48
+ lines.push(`Previous tasks (${tasks.length}/${completedTasks.length}):`);
49
+ for (const task of tasks) {
50
+ lines.push(`- ${task.id}: ${task.status}${task.error ? ` (${task.error})` : ""}`);
51
+ }
52
+ }
53
+
54
+ // Project pending mailbox that is relevant to this worker
55
+ if (input.pendingMailbox.length > 0) {
56
+ const messages = input.pendingMailbox.slice(0, maxMailbox);
57
+ sources.push({ kind: "mailbox", bounded: true, reference: `mailbox:${messages.length}/${input.pendingMailbox.length}` });
58
+ lines.push(`Pending messages (${messages.length}/${input.pendingMailbox.length}):`);
59
+ for (const msg of messages) {
60
+ lines.push(`- ${msg.kind ?? "message"}: ${msg.body.slice(0, 100)}`);
61
+ }
62
+ }
63
+
64
+ // Project artifact references (not content)
65
+ if (input.artifacts.length > 0) {
66
+ const artifacts = input.artifacts.slice(0, maxArtifacts);
67
+ sources.push({ kind: "artifacts", bounded: true, reference: `artifacts:${artifacts.length}/${input.artifacts.length}` });
68
+ lines.push(`Available artifacts (${artifacts.length}/${input.artifacts.length}):`);
69
+ for (const art of artifacts) {
70
+ lines.push(`- ${art.kind} (${art.producer})`);
71
+ }
72
+ }
73
+
74
+ // Metadata markers — not injected as prompt instructions
75
+ sources.push({ kind: "ui_metadata", bounded: false, reference: "excluded_from_prompt" });
76
+ sources.push({ kind: "runtime_metadata", bounded: false, reference: "excluded_from_prompt" });
77
+
78
+ return {
79
+ sources,
80
+ summary: lines.join("\n"),
81
+ injectedAsContext: true,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Convert run history to a bounded worker prompt section.
87
+ * Same logic as transformRunContextBeforeWorkerStart but returns
88
+ * the prompt text directly for embedding in the worker prompt.
89
+ */
90
+ export function convertRunHistoryToWorkerPrompt(input: {
91
+ manifest: TeamRunManifest;
92
+ tasks: TeamTaskState[];
93
+ pendingMailbox: MailboxMessage[];
94
+ artifacts: ArtifactDescriptor[];
95
+ }): string {
96
+ const projection = transformRunContextBeforeWorkerStart(input);
97
+ if (!projection.summary) return "";
98
+ return [
99
+ "## Run Context (bounded projection)",
100
+ projection.summary,
101
+ "",
102
+ `Projection sources: ${projection.sources.map((s) => s.kind).join(", ")}`,
103
+ ].join("\n");
104
104
  }
@@ -1,22 +1,22 @@
1
- import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
- import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
3
- import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
4
-
5
- export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
6
- return tasks.map((task) => task.id === updated.id ? updated : task);
7
- }
8
-
9
- export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
10
- const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
11
- const merged = updateTask(latest, updated);
12
- saveRunTasks(manifest, merged);
13
- return merged;
14
- }
15
-
16
- export function checkpointTask(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, phase: TaskCheckpointState["phase"], childPid?: number): { task: TeamTaskState; tasks: TeamTaskState[] } {
17
- const checkpoint: TaskCheckpointState = { phase, updatedAt: new Date().toISOString(), ...(childPid ? { childPid } : task.checkpoint?.childPid ? { childPid: task.checkpoint.childPid } : {}) };
18
- const nextTask = { ...task, checkpoint };
19
- const nextTasks = persistSingleTaskUpdate(manifest, updateTask(tasks, nextTask), nextTask);
20
- upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
21
- return { task: nextTask, tasks: nextTasks };
22
- }
1
+ import type { TaskCheckpointState, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
+ import { loadRunManifestById, saveRunTasks } from "../../state/state-store.ts";
3
+ import { recordFromTask, upsertCrewAgent } from "../crew-agent-records.ts";
4
+
5
+ export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
6
+ return tasks.map((task) => task.id === updated.id ? updated : task);
7
+ }
8
+
9
+ export function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
10
+ const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
11
+ const merged = updateTask(latest, updated);
12
+ saveRunTasks(manifest, merged);
13
+ return merged;
14
+ }
15
+
16
+ export function checkpointTask(manifest: TeamRunManifest, tasks: TeamTaskState[], task: TeamTaskState, phase: TaskCheckpointState["phase"], childPid?: number): { task: TeamTaskState; tasks: TeamTaskState[] } {
17
+ const checkpoint: TaskCheckpointState = { phase, updatedAt: new Date().toISOString(), ...(childPid ? { childPid } : task.checkpoint?.childPid ? { childPid: task.checkpoint.childPid } : {}) };
18
+ const nextTask = { ...task, checkpoint };
19
+ const nextTasks = persistSingleTaskUpdate(manifest, updateTask(tasks, nextTask), nextTask);
20
+ upsertCrewAgent(manifest, recordFromTask(manifest, nextTask, "child-process"));
21
+ return { task: nextTask, tasks: nextTasks };
22
+ }
@@ -13,12 +13,14 @@ import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.
13
13
  import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
14
14
  import { buildRecoveryLedger } from "./recovery-recipes.ts";
15
15
  import { buildTaskGraphIndex, refreshTaskGraphQueues, taskGraphSnapshot } from "./task-graph-scheduler.ts";
16
+ import { buildExecutionPlan as buildDagExecutionPlan, getReadyTasks as getDagReadyTasks, type TaskNode } from "./task-graph.ts";
16
17
  import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
17
18
  import { aggregateTaskOutputs } from "./task-output-context.ts";
18
19
  import { saveCrewAgents } from "./crew-agent-records.ts";
19
20
  import { recordsForMaterializedTasks } from "./task-display.ts";
20
21
  import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
21
22
  import { runTeamTask } from "./task-runner.ts";
23
+ import { createWorkflowStateMachine, validatePhasePreconditions, transitionPhase, type PhaseState, type PhaseGuardContext } from "./workflow-state.ts";
22
24
  import { executeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "./retry-executor.ts";
23
25
  import { appendDeadletter } from "./deadletter.ts";
24
26
  import type { MetricRegistry } from "../observability/metric-registry.ts";
@@ -164,7 +166,13 @@ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]):
164
166
  return phases.length ? { phases } : undefined;
165
167
  }
166
168
 
167
- function closeUnbalancedJson(raw: string): string {
169
+ interface CloseUnbalancedJsonResult {
170
+ text: string;
171
+ status: "repaired" | "unstable";
172
+ warning?: string;
173
+ }
174
+
175
+ function closeUnbalancedJson(raw: string): CloseUnbalancedJsonResult {
168
176
  let result = raw.trim();
169
177
  const stack: string[] = [];
170
178
  let inString = false;
@@ -188,7 +196,11 @@ function closeUnbalancedJson(raw: string): string {
188
196
  else if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
189
197
  }
190
198
  while (stack.length) result += stack.pop();
191
- return result;
199
+ // If still in a string, the JSON string was truncated — values may be semantically different
200
+ if (inString) {
201
+ return { text: result, status: "unstable", warning: "JSON string was truncated — values may be incorrect" };
202
+ }
203
+ return { text: result, status: "repaired" };
192
204
  }
193
205
 
194
206
  function salvageCompletePhaseObjects(raw: string): unknown | undefined {
@@ -255,7 +267,8 @@ function adaptiveRoleAlias(role: string, allowed: Set<string>): string | undefin
255
267
  export function __test__repairAdaptivePlan(text: string, allowedRoles: string[]): { plan?: AdaptivePlan; repaired: boolean; reason?: string } {
256
268
  const raw = extractAdaptivePlanJson(text);
257
269
  if (!raw) return { repaired: false, reason: "missing-json" };
258
- const candidates = [raw, closeUnbalancedJson(raw)];
270
+ const closeResult = closeUnbalancedJson(raw);
271
+ const candidates = [raw, closeResult.text];
259
272
  let parsed: unknown;
260
273
  let salvageUsed = false;
261
274
  for (const candidate of candidates) {
@@ -276,7 +289,7 @@ export function __test__repairAdaptivePlan(text: string, allowedRoles: string[])
276
289
  const allowed = new Set(allowedRoles);
277
290
  const phases: AdaptivePlanPhase[] = [];
278
291
  let total = 0;
279
- let repaired = salvageUsed || raw !== closeUnbalancedJson(raw);
292
+ let repaired = salvageUsed || raw !== closeResult.text;
280
293
  for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
281
294
  if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
282
295
  const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
@@ -510,6 +523,24 @@ function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
510
523
  return tasks.some((task) => task.status === "queued" && task.adaptive && isMutatingTask(task));
511
524
  }
512
525
 
526
+ /**
527
+ * Check whether any task uses explicit `dependsOn` that would benefit from DAG-based
528
+ * execution planning. If so, build an execution plan and use `getDagReadyTasks`
529
+ * to augment the ready-set selection.
530
+ */
531
+ function dagReadyTaskIds(tasks: TeamTaskState[], completedIds: Set<string>): string[] | null {
532
+ const hasExplicitDeps = tasks.some((t) => t.dependsOn.length > 0);
533
+ if (!hasExplicitDeps) return null;
534
+ const nodes: TaskNode[] = tasks.map((t) => ({
535
+ id: t.id,
536
+ dependsOn: t.dependsOn,
537
+ phase: t.adaptive?.phase ?? t.stepId,
538
+ }));
539
+ const plan = buildDagExecutionPlan(nodes);
540
+ if (plan.hasCycle) return null; // fall back to existing scheduler
541
+ return getDagReadyTasks(plan, completedIds);
542
+ }
543
+
513
544
  export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
514
545
  let workflow = input.workflow;
515
546
  let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
@@ -584,6 +615,15 @@ async function executeTeamRunCore(
584
615
  const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
585
616
  saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
586
617
 
618
+ // Build a workflow phase state machine from workflow steps for precondition tracking.
619
+ const workflowPhases: PhaseState[] = workflow.steps.map((step): PhaseState => ({
620
+ name: step.id,
621
+ status: "pending",
622
+ inputs: step.reads === false ? [] : Array.isArray(step.reads) ? step.reads : [],
623
+ outputs: step.output === false ? [] : step.output ? [step.output] : [],
624
+ }));
625
+ let wfMachine = createWorkflowStateMachine(workflowPhases);
626
+
587
627
  while (tasks.some((task) => task.status === "queued")) {
588
628
  if (input.signal?.aborted) {
589
629
  const cancelReason = cancellationReasonFromSignal(input.signal);
@@ -614,13 +654,41 @@ async function executeTeamRunCore(
614
654
  }
615
655
 
616
656
  const snapshot = taskGraphSnapshot(tasks, queueIndex);
617
- const readyRoles = snapshot.ready.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
618
- const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, allowUnboundedConcurrency: input.limits?.allowUnboundedConcurrency, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
657
+
658
+ // DAG-based execution plan: when tasks have explicit dependsOn, use the
659
+ // topological wave planner to determine ready tasks. Fall back to the
660
+ // existing task-graph-scheduler when no explicit deps exist (backward compat).
661
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
662
+ const dagReady = dagReadyTaskIds(tasks, completedIds);
663
+ const effectiveReady = dagReady ?? snapshot.ready;
664
+
665
+ // Workflow phase precondition check (non-blocking: log warnings only).
666
+ if (wfMachine.currentPhaseIndex < wfMachine.phases.length) {
667
+ const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
668
+ const previousPhaseStatus = wfMachine.currentPhaseIndex > 0 ? (wfMachine.phases[wfMachine.currentPhaseIndex - 1]?.status ?? "pending") : "completed";
669
+ const wfContext: PhaseGuardContext = {
670
+ completedArtifacts,
671
+ previousPhaseStatus,
672
+ taskResults: tasks.filter((t) => t.status === "completed").map((t) => ({ taskId: t.id, status: t.status, outputPath: t.resultArtifact?.path })),
673
+ };
674
+ const preconditions = validatePhasePreconditions(wfMachine, wfContext);
675
+ if (!preconditions.ready) {
676
+ appendEvent(manifest.eventsPath, { type: "workflow.preconditions", runId: manifest.runId, message: `Workflow phase '${wfMachine.phases[wfMachine.currentPhaseIndex]?.name}' is missing inputs: ${preconditions.blocking.join(", ")}`, data: { phaseIndex: wfMachine.currentPhaseIndex, phaseName: wfMachine.phases[wfMachine.currentPhaseIndex]?.name, blocking: preconditions.blocking } });
677
+ } else {
678
+ // Advance the machine past completed phases.
679
+ while (wfMachine.currentPhaseIndex < wfMachine.phases.length && wfMachine.phases[wfMachine.currentPhaseIndex]?.status === "completed") {
680
+ wfMachine = { ...wfMachine, currentPhaseIndex: wfMachine.currentPhaseIndex + 1 };
681
+ }
682
+ }
683
+ }
684
+
685
+ const readyRoles = effectiveReady.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
686
+ const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, allowUnboundedConcurrency: input.limits?.allowUnboundedConcurrency, readyCount: effectiveReady.length, workspaceMode: manifest.workspaceMode, readyRoles });
619
687
  if (concurrency.reason.includes(";unbounded:")) {
620
688
  appendEvent(manifest.eventsPath, { type: "limits.unbounded", runId: manifest.runId, message: "Unbounded worker concurrency was explicitly enabled for this run.", data: { concurrencyReason: concurrency.reason, maxConcurrent: concurrency.maxConcurrent } });
621
689
  }
622
690
  const approvalPending = isPlanApprovalPending(manifest);
623
- const readyIds = approvalPending ? snapshot.ready : snapshot.ready.slice(0, concurrency.selectedCount);
691
+ const readyIds = approvalPending ? effectiveReady : effectiveReady.slice(0, concurrency.selectedCount);
624
692
  const candidateBatch = readyIds.map((id) => tasks.find((task) => task.id === id)).filter((task): task is TeamTaskState => Boolean(task));
625
693
  const readyBatch = approvalPending ? candidateBatch.filter((task) => !isMutatingTask(task)).slice(0, concurrency.selectedCount) : candidateBatch;
626
694
  if (readyBatch.length === 0) {
@@ -726,6 +794,48 @@ async function executeTeamRunCore(
726
794
  if (results.length === 0) break;
727
795
  manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
728
796
  tasks = __test__mergeTaskUpdates(tasks, results);
797
+
798
+ // Advance workflow phases whose tasks are all in terminal state
799
+ const terminalStatuses = new Set(["completed", "failed", "skipped", "cancelled"]);
800
+ const phaseTaskMap = new Map<string, string[]>();
801
+ for (const task of tasks) {
802
+ if (!task.stepId) continue;
803
+ const existing = phaseTaskMap.get(task.stepId) ?? [];
804
+ existing.push(task.id);
805
+ phaseTaskMap.set(task.stepId, existing);
806
+ }
807
+ for (let pi = wfMachine.currentPhaseIndex; pi < wfMachine.phases.length; pi++) {
808
+ const phase = wfMachine.phases[pi]!;
809
+ const phaseTaskIds = phaseTaskMap.get(phase.name) ?? [];
810
+ if (phaseTaskIds.length === 0) continue;
811
+ const allTerminal = phaseTaskIds.every((taskId) => {
812
+ const task = tasks.find((t) => t.id === taskId);
813
+ return task ? terminalStatuses.has(task.status) : false;
814
+ });
815
+ if (!allTerminal) break;
816
+ if (phase.status !== "completed" && phase.status !== "failed" && phase.status !== "skipped") {
817
+ const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
818
+ const previousPhaseStatus = pi > 0 ? (wfMachine.phases[pi - 1]?.status ?? "pending") : "completed";
819
+ const wfContext: PhaseGuardContext = {
820
+ completedArtifacts,
821
+ previousPhaseStatus,
822
+ taskResults: tasks.filter((t) => t.status === "completed").map((t) => ({ taskId: t.id, status: t.status, outputPath: t.resultArtifact?.path })),
823
+ };
824
+ // Determine phase transition status based on individual task outcomes
825
+ const phaseTasks = phaseTaskIds.map((taskId) => tasks.find((t) => t.id === taskId)).filter((t): t is NonNullable<typeof t> => t !== undefined);
826
+ const hasFailedOrCancelled = phaseTasks.some((t) => t.status === "failed" || t.status === "cancelled");
827
+ const phaseStatus = hasFailedOrCancelled ? "failed" : "completed";
828
+ const transition = transitionPhase(wfMachine, pi, phaseStatus, wfContext);
829
+ wfMachine = transition.machine;
830
+ if (transition.guardResult && !transition.guardResult.allowed) {
831
+ appendEvent(manifest.eventsPath, { type: "workflow.phase_guard_blocked", runId: manifest.runId, message: `Workflow phase '${phase.name}' guard blocked: ${transition.guardResult.reason ?? "unknown"}`, data: { phaseIndex: pi, phaseName: phase.name, reason: transition.guardResult.reason } });
832
+ break;
833
+ }
834
+ appendEvent(manifest.eventsPath, { type: phaseStatus === "failed" ? "workflow.phase_failed" : "workflow.phase_completed", runId: manifest.runId, message: `Workflow phase '${phase.name}' ${phaseStatus}.`, data: { phaseIndex: pi, phaseStatus } });
835
+ }
836
+ wfMachine = { ...wfMachine, currentPhaseIndex: pi + 1 };
837
+ }
838
+
729
839
  const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
730
840
  if (cancelledResult || input.signal?.aborted) {
731
841
  const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : undefined;
@@ -1,21 +1,21 @@
1
- export interface WorkerHeartbeatState {
2
- workerId: string;
3
- pid?: number;
4
- lastSeenAt: string;
5
- lastStdoutAt?: string;
6
- lastEventAt?: string;
7
- turnCount?: number;
8
- alive?: boolean;
9
- }
10
-
11
- export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
12
- return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
13
- }
14
-
15
- export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
16
- return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
17
- }
18
-
19
- export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
20
- return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
21
- }
1
+ export interface WorkerHeartbeatState {
2
+ workerId: string;
3
+ pid?: number;
4
+ lastSeenAt: string;
5
+ lastStdoutAt?: string;
6
+ lastEventAt?: string;
7
+ turnCount?: number;
8
+ alive?: boolean;
9
+ }
10
+
11
+ export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
12
+ return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
13
+ }
14
+
15
+ export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
16
+ return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
17
+ }
18
+
19
+ export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
20
+ return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
21
+ }
@@ -1,57 +1,57 @@
1
- export type WorkerLifecycleState = "spawning" | "trust_required" | "ready_for_prompt" | "running" | "finished" | "failed";
2
- export type StartupFailureClassification = "trust_required" | "prompt_misdelivery" | "prompt_acceptance_timeout" | "transport_dead" | "worker_crashed" | "unknown";
3
-
4
- export interface WorkerStartupEvidence {
5
- lastLifecycleState: WorkerLifecycleState;
6
- command: string;
7
- promptSentAt?: string;
8
- promptAccepted: boolean;
9
- trustPromptDetected: boolean;
10
- transportHealthy: boolean;
11
- childProcessAlive: boolean;
12
- elapsedMs: number;
13
- classification: StartupFailureClassification;
14
- stderrPreview?: string;
15
- }
16
-
17
- export function detectTrustPrompt(text: string): boolean {
18
- const lowered = text.toLowerCase();
19
- return lowered.includes("do you trust") || lowered.includes("trust this") || lowered.includes("untrusted") || lowered.includes("workspace trust") || lowered.includes("allow this folder");
20
- }
21
-
22
- export function classifyStartupFailure(evidence: Omit<WorkerStartupEvidence, "classification">): StartupFailureClassification {
23
- if (!evidence.transportHealthy) return "transport_dead";
24
- if (evidence.trustPromptDetected || evidence.lastLifecycleState === "trust_required") return "trust_required";
25
- if (evidence.promptSentAt && !evidence.promptAccepted && evidence.childProcessAlive) return "prompt_acceptance_timeout";
26
- if (evidence.promptSentAt && !evidence.promptAccepted && !evidence.childProcessAlive) return "worker_crashed";
27
- if (evidence.stderrPreview?.toLowerCase().includes("command not found") || evidence.stderrPreview?.toLowerCase().includes("not recognized")) return "prompt_misdelivery";
28
- if (!evidence.childProcessAlive && evidence.lastLifecycleState !== "finished") return "worker_crashed";
29
- return "unknown";
30
- }
31
-
32
- export function createStartupEvidence(input: {
33
- command: string;
34
- startedAt: Date;
35
- finishedAt?: Date;
36
- promptSentAt?: Date;
37
- promptAccepted?: boolean;
38
- stderr?: string;
39
- error?: string;
40
- exitCode?: number | null;
41
- }): WorkerStartupEvidence {
42
- const stderrPreview = (input.error || input.stderr || "").slice(0, 500) || undefined;
43
- const trustPromptDetected = detectTrustPrompt(stderrPreview ?? "");
44
- const childProcessAlive = input.exitCode === undefined || input.exitCode === null ? !input.finishedAt : false;
45
- const base: Omit<WorkerStartupEvidence, "classification"> = {
46
- lastLifecycleState: input.error || (input.exitCode !== undefined && input.exitCode !== null && input.exitCode !== 0) ? "failed" : input.finishedAt ? "finished" : "running",
47
- command: input.command,
48
- promptSentAt: input.promptSentAt?.toISOString(),
49
- promptAccepted: input.promptAccepted ?? !input.error,
50
- trustPromptDetected,
51
- transportHealthy: !input.error || !/enoent|spawn|transport/i.test(input.error),
52
- childProcessAlive,
53
- elapsedMs: Math.max(0, (input.finishedAt ?? new Date()).getTime() - input.startedAt.getTime()),
54
- stderrPreview,
55
- };
56
- return { ...base, classification: classifyStartupFailure(base) };
57
- }
1
+ export type WorkerLifecycleState = "spawning" | "trust_required" | "ready_for_prompt" | "running" | "finished" | "failed";
2
+ export type StartupFailureClassification = "trust_required" | "prompt_misdelivery" | "prompt_acceptance_timeout" | "transport_dead" | "worker_crashed" | "unknown";
3
+
4
+ export interface WorkerStartupEvidence {
5
+ lastLifecycleState: WorkerLifecycleState;
6
+ command: string;
7
+ promptSentAt?: string;
8
+ promptAccepted: boolean;
9
+ trustPromptDetected: boolean;
10
+ transportHealthy: boolean;
11
+ childProcessAlive: boolean;
12
+ elapsedMs: number;
13
+ classification: StartupFailureClassification;
14
+ stderrPreview?: string;
15
+ }
16
+
17
+ export function detectTrustPrompt(text: string): boolean {
18
+ const lowered = text.toLowerCase();
19
+ return lowered.includes("do you trust") || lowered.includes("trust this") || lowered.includes("untrusted") || lowered.includes("workspace trust") || lowered.includes("allow this folder");
20
+ }
21
+
22
+ export function classifyStartupFailure(evidence: Omit<WorkerStartupEvidence, "classification">): StartupFailureClassification {
23
+ if (!evidence.transportHealthy) return "transport_dead";
24
+ if (evidence.trustPromptDetected || evidence.lastLifecycleState === "trust_required") return "trust_required";
25
+ if (evidence.promptSentAt && !evidence.promptAccepted && evidence.childProcessAlive) return "prompt_acceptance_timeout";
26
+ if (evidence.promptSentAt && !evidence.promptAccepted && !evidence.childProcessAlive) return "worker_crashed";
27
+ if (evidence.stderrPreview?.toLowerCase().includes("command not found") || evidence.stderrPreview?.toLowerCase().includes("not recognized")) return "prompt_misdelivery";
28
+ if (!evidence.childProcessAlive && evidence.lastLifecycleState !== "finished") return "worker_crashed";
29
+ return "unknown";
30
+ }
31
+
32
+ export function createStartupEvidence(input: {
33
+ command: string;
34
+ startedAt: Date;
35
+ finishedAt?: Date;
36
+ promptSentAt?: Date;
37
+ promptAccepted?: boolean;
38
+ stderr?: string;
39
+ error?: string;
40
+ exitCode?: number | null;
41
+ }): WorkerStartupEvidence {
42
+ const stderrPreview = (input.error || input.stderr || "").slice(0, 500) || undefined;
43
+ const trustPromptDetected = detectTrustPrompt(stderrPreview ?? "");
44
+ const childProcessAlive = input.exitCode === undefined || input.exitCode === null ? !input.finishedAt : false;
45
+ const base: Omit<WorkerStartupEvidence, "classification"> = {
46
+ lastLifecycleState: input.error || (input.exitCode !== undefined && input.exitCode !== null && input.exitCode !== 0) ? "failed" : input.finishedAt ? "finished" : "running",
47
+ command: input.command,
48
+ promptSentAt: input.promptSentAt?.toISOString(),
49
+ promptAccepted: input.promptAccepted ?? !input.error,
50
+ trustPromptDetected,
51
+ transportHealthy: !input.error || !/enoent|spawn|transport/i.test(input.error),
52
+ childProcessAlive,
53
+ elapsedMs: Math.max(0, (input.finishedAt ?? new Date()).getTime() - input.startedAt.getTime()),
54
+ stderrPreview,
55
+ };
56
+ return { ...base, classification: classifyStartupFailure(base) };
57
+ }