pi-crew 0.1.51 → 0.2.1

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 (240) 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/api.ts +441 -441
  65. package/src/extension/team-tool/doctor.ts +28 -3
  66. package/src/extension/team-tool/handle-settings.ts +195 -188
  67. package/src/extension/team-tool/inspect.ts +41 -41
  68. package/src/extension/team-tool/intent-policy.ts +42 -42
  69. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  70. package/src/extension/team-tool/plan.ts +19 -19
  71. package/src/extension/team-tool/run.ts +12 -1
  72. package/src/extension/team-tool.ts +332 -322
  73. package/src/i18n.ts +184 -184
  74. package/src/observability/exporters/otlp-exporter.ts +92 -77
  75. package/src/prompt/prompt-runtime.ts +72 -72
  76. package/src/runtime/agent-memory.ts +72 -72
  77. package/src/runtime/agent-observability.ts +114 -114
  78. package/src/runtime/async-marker.ts +26 -26
  79. package/src/runtime/attention-events.ts +28 -28
  80. package/src/runtime/auto-resume.ts +100 -0
  81. package/src/runtime/background-runner.ts +11 -1
  82. package/src/runtime/cancellation-token.ts +89 -89
  83. package/src/runtime/cancellation.ts +61 -61
  84. package/src/runtime/capability-inventory.ts +116 -116
  85. package/src/runtime/child-pi.ts +7 -2
  86. package/src/runtime/compaction-summary.ts +271 -0
  87. package/src/runtime/completion-guard.ts +190 -190
  88. package/src/runtime/crash-recovery.ts +33 -1
  89. package/src/runtime/delta-conflict.ts +360 -0
  90. package/src/runtime/direct-run.ts +35 -35
  91. package/src/runtime/foreground-control.ts +82 -82
  92. package/src/runtime/green-contract.ts +46 -46
  93. package/src/runtime/group-join.ts +106 -106
  94. package/src/runtime/heartbeat-gradient.ts +28 -28
  95. package/src/runtime/heartbeat-watcher.ts +124 -124
  96. package/src/runtime/iteration-hooks.ts +264 -0
  97. package/src/runtime/live-agent-control.ts +88 -88
  98. package/src/runtime/live-control-realtime.ts +36 -36
  99. package/src/runtime/live-extension-bridge.ts +150 -150
  100. package/src/runtime/live-irc.ts +92 -92
  101. package/src/runtime/live-session-health.ts +100 -100
  102. package/src/runtime/loop-gates.ts +129 -0
  103. package/src/runtime/metric-parser.ts +40 -0
  104. package/src/runtime/notebook-helpers.ts +90 -90
  105. package/src/runtime/orphan-sentinel.ts +7 -7
  106. package/src/runtime/parallel-research.ts +44 -44
  107. package/src/runtime/phase-progress.ts +217 -0
  108. package/src/runtime/pi-args.ts +38 -11
  109. package/src/runtime/pi-json-output.ts +111 -111
  110. package/src/runtime/pi-spawn.ts +57 -7
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/post-checks.ts +122 -0
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/prose-compressor.ts +164 -164
  115. package/src/runtime/recovery-recipes.ts +74 -74
  116. package/src/runtime/result-extractor.ts +121 -121
  117. package/src/runtime/role-permission.ts +39 -39
  118. package/src/runtime/sensitive-paths.ts +2 -2
  119. package/src/runtime/session-resources.ts +25 -25
  120. package/src/runtime/session-snapshot.ts +59 -59
  121. package/src/runtime/session-usage.ts +79 -79
  122. package/src/runtime/sidechain-output.ts +29 -29
  123. package/src/runtime/stream-preview.ts +177 -177
  124. package/src/runtime/supervisor-contact.ts +59 -59
  125. package/src/runtime/task-display.ts +38 -38
  126. package/src/runtime/task-graph.ts +207 -0
  127. package/src/runtime/task-quality.ts +207 -0
  128. package/src/runtime/task-runner/capabilities.ts +78 -78
  129. package/src/runtime/task-runner/live-executor.ts +7 -1
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  132. package/src/runtime/task-runner/result-utils.ts +14 -14
  133. package/src/runtime/task-runner/run-projection.ts +103 -103
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/team-runner.ts +117 -7
  136. package/src/runtime/worker-heartbeat.ts +21 -21
  137. package/src/runtime/worker-startup.ts +57 -57
  138. package/src/runtime/workflow-state.ts +187 -0
  139. package/src/runtime/workspace-tree.ts +298 -298
  140. package/src/schema/config-schema.ts +11 -0
  141. package/src/schema/validation-types.ts +148 -0
  142. package/src/skills/skill-templates.ts +374 -0
  143. package/src/state/active-run-registry.ts +35 -11
  144. package/src/state/atomic-write.ts +33 -26
  145. package/src/state/contracts.ts +1 -0
  146. package/src/state/event-reconstructor.ts +217 -0
  147. package/src/state/locks.ts +2 -13
  148. package/src/state/mailbox.ts +4 -3
  149. package/src/state/state-store.ts +16 -6
  150. package/src/state/task-claims.ts +44 -44
  151. package/src/state/types.ts +9 -0
  152. package/src/state/usage.ts +29 -29
  153. package/src/subagents/async-entry.ts +1 -1
  154. package/src/subagents/index.ts +3 -3
  155. package/src/subagents/live/control.ts +1 -1
  156. package/src/subagents/live/manager.ts +1 -1
  157. package/src/subagents/live/realtime.ts +1 -1
  158. package/src/subagents/live/session-runtime.ts +1 -1
  159. package/src/subagents/manager.ts +1 -1
  160. package/src/subagents/spawn.ts +1 -1
  161. package/src/teams/team-serializer.ts +38 -38
  162. package/src/types/diff.d.ts +18 -18
  163. package/src/ui/crew-footer.ts +101 -101
  164. package/src/ui/crew-select-list.ts +111 -111
  165. package/src/ui/crew-widget.ts +5 -2
  166. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  167. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  168. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  169. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  170. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  171. package/src/ui/dynamic-border.ts +25 -25
  172. package/src/ui/layout-primitives.ts +106 -106
  173. package/src/ui/loaders.ts +158 -158
  174. package/src/ui/render-coalescer.ts +51 -51
  175. package/src/ui/render-diff.ts +119 -119
  176. package/src/ui/render-scheduler.ts +143 -143
  177. package/src/ui/run-action-dispatcher.ts +10 -1
  178. package/src/ui/spinner.ts +17 -17
  179. package/src/ui/status-colors.ts +58 -58
  180. package/src/ui/syntax-highlight.ts +116 -116
  181. package/src/ui/transcript-entries.ts +258 -258
  182. package/src/utils/completion-dedupe.ts +63 -63
  183. package/src/utils/frontmatter.ts +68 -68
  184. package/src/utils/git.ts +262 -262
  185. package/src/utils/ids.ts +17 -17
  186. package/src/utils/incremental-reader.ts +104 -104
  187. package/src/utils/names.ts +27 -27
  188. package/src/utils/redaction.ts +44 -44
  189. package/src/utils/safe-paths.ts +47 -47
  190. package/src/utils/scan-cache.ts +136 -136
  191. package/src/utils/sleep.ts +40 -26
  192. package/src/utils/task-name-generator.ts +337 -337
  193. package/src/workflows/validate-workflow.ts +40 -40
  194. package/src/worktree/branch-freshness.ts +45 -45
  195. package/teams/default.team.md +12 -12
  196. package/teams/fast-fix.team.md +11 -11
  197. package/teams/implementation.team.md +18 -18
  198. package/teams/parallel-research.team.md +14 -14
  199. package/teams/research.team.md +11 -11
  200. package/teams/review.team.md +12 -12
  201. package/workflows/default.workflow.md +30 -29
  202. package/workflows/fast-fix.workflow.md +23 -22
  203. package/workflows/implementation.workflow.md +43 -43
  204. package/workflows/parallel-research.workflow.md +46 -46
  205. package/workflows/research.workflow.md +22 -22
  206. package/workflows/review.workflow.md +30 -30
  207. package/docs/refactor-tasks-phase3.md +0 -394
  208. package/docs/refactor-tasks-phase4.md +0 -564
  209. package/docs/refactor-tasks-phase5.md +0 -402
  210. package/docs/refactor-tasks-phase6.md +0 -662
  211. package/docs/refactor-tasks.md +0 -1484
  212. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  213. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  214. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  215. package/docs/research/AUDIT_PI_CREW.md +0 -457
  216. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  217. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  218. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  219. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  220. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  221. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  222. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  223. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  224. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  225. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  226. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  227. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  228. package/docs/research-extension-examples.md +0 -297
  229. package/docs/research-extension-system.md +0 -324
  230. package/docs/research-oh-my-pi-distillation.md +0 -369
  231. package/docs/research-optimization-plan.md +0 -548
  232. package/docs/research-phase10-distillation.md +0 -199
  233. package/docs/research-phase11-distillation.md +0 -201
  234. package/docs/research-phase8-operator-experience-plan.md +0 -819
  235. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  236. package/docs/research-pi-coding-agent.md +0 -357
  237. package/docs/research-source-pi-crew-reference.md +0 -174
  238. package/docs/research-ui-optimization-plan.md +0 -480
  239. package/docs/source-runtime-refactor-map.md +0 -107
  240. 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
+ }