pi-crew 0.1.46 → 0.1.49

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 (253) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/next-upgrade-roadmap.md +117 -42
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  18. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  19. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  20. package/docs/research/AUDIT_PI_CREW.md +457 -0
  21. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  22. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  23. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  24. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  25. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  26. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  27. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  28. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  29. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  30. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  31. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  32. package/docs/research-awesome-agent-skills-distillation.md +100 -100
  33. package/docs/research-extension-examples.md +297 -297
  34. package/docs/research-extension-system.md +324 -324
  35. package/docs/research-oh-my-pi-distillation.md +56 -9
  36. package/docs/research-optimization-plan.md +548 -548
  37. package/docs/research-phase10-distillation.md +198 -198
  38. package/docs/research-phase11-distillation.md +201 -201
  39. package/docs/research-pi-coding-agent.md +357 -357
  40. package/docs/research-source-pi-crew-reference.md +174 -174
  41. package/docs/runtime-flow.md +148 -148
  42. package/docs/source-runtime-refactor-map.md +107 -107
  43. package/index.ts +6 -6
  44. package/package.json +99 -98
  45. package/schema.json +8 -0
  46. package/skills/async-worker-recovery/SKILL.md +42 -42
  47. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  48. package/skills/delegation-patterns/SKILL.md +54 -54
  49. package/skills/mailbox-interactive/SKILL.md +40 -40
  50. package/skills/model-routing-context/SKILL.md +39 -39
  51. package/skills/multi-perspective-review/SKILL.md +58 -58
  52. package/skills/observability-reliability/SKILL.md +41 -41
  53. package/skills/orchestration/SKILL.md +157 -0
  54. package/skills/ownership-session-security/SKILL.md +41 -41
  55. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  56. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  57. package/skills/resource-discovery-config/SKILL.md +41 -41
  58. package/skills/runtime-state-reader/SKILL.md +44 -44
  59. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  60. package/skills/state-mutation-locking/SKILL.md +42 -42
  61. package/skills/systematic-debugging/SKILL.md +67 -67
  62. package/skills/ui-render-performance/SKILL.md +39 -39
  63. package/skills/verification-before-done/SKILL.md +57 -57
  64. package/skills/worktree-isolation/SKILL.md +39 -39
  65. package/src/agents/agent-config.ts +6 -0
  66. package/src/agents/agent-search.ts +98 -0
  67. package/src/agents/agent-serializer.ts +4 -0
  68. package/src/agents/discover-agents.ts +17 -4
  69. package/src/config/config.ts +24 -0
  70. package/src/config/defaults.ts +11 -0
  71. package/src/extension/autonomous-policy.ts +26 -33
  72. package/src/extension/cross-extension-rpc.ts +82 -82
  73. package/src/extension/help.ts +1 -0
  74. package/src/extension/management.ts +5 -0
  75. package/src/extension/register.ts +58 -13
  76. package/src/extension/registration/commands.ts +33 -1
  77. package/src/extension/registration/compaction-guard.ts +125 -125
  78. package/src/extension/registration/team-tool.ts +6 -4
  79. package/src/extension/run-bundle-schema.ts +89 -89
  80. package/src/extension/run-index.ts +24 -18
  81. package/src/extension/run-maintenance.ts +68 -62
  82. package/src/extension/team-tool/api.ts +23 -2
  83. package/src/extension/team-tool/cancel.ts +86 -11
  84. package/src/extension/team-tool/context.ts +3 -0
  85. package/src/extension/team-tool/handle-settings.ts +188 -188
  86. package/src/extension/team-tool/inspect.ts +41 -41
  87. package/src/extension/team-tool/intent-policy.ts +42 -0
  88. package/src/extension/team-tool/lifecycle-actions.ts +47 -18
  89. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  90. package/src/extension/team-tool/plan.ts +19 -19
  91. package/src/extension/team-tool/respond.ts +10 -2
  92. package/src/extension/team-tool/run.ts +3 -2
  93. package/src/extension/team-tool/status.ts +1 -1
  94. package/src/extension/team-tool-types.ts +1 -0
  95. package/src/extension/team-tool.ts +13 -3
  96. package/src/hooks/registry.ts +61 -0
  97. package/src/hooks/types.ts +41 -0
  98. package/src/i18n.ts +184 -184
  99. package/src/observability/exporters/otlp-exporter.ts +77 -77
  100. package/src/prompt/prompt-runtime.ts +72 -72
  101. package/src/runtime/agent-control.ts +108 -2
  102. package/src/runtime/agent-memory.ts +72 -72
  103. package/src/runtime/agent-observability.ts +114 -114
  104. package/src/runtime/async-marker.ts +26 -26
  105. package/src/runtime/async-runner.ts +3 -1
  106. package/src/runtime/attention-events.ts +28 -28
  107. package/src/runtime/background-runner.ts +19 -0
  108. package/src/runtime/cancellation-token.ts +89 -0
  109. package/src/runtime/cancellation.ts +61 -51
  110. package/src/runtime/capability-inventory.ts +116 -0
  111. package/src/runtime/child-pi.ts +2 -1
  112. package/src/runtime/code-summary.ts +247 -0
  113. package/src/runtime/completion-guard.ts +190 -190
  114. package/src/runtime/crash-recovery.ts +181 -0
  115. package/src/runtime/crew-agent-records.ts +35 -7
  116. package/src/runtime/crew-agent-runtime.ts +1 -0
  117. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  118. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  119. package/src/runtime/delivery-coordinator.ts +3 -1
  120. package/src/runtime/direct-run.ts +35 -35
  121. package/src/runtime/effectiveness.ts +81 -76
  122. package/src/runtime/event-stream-bridge.ts +90 -0
  123. package/src/runtime/foreground-control.ts +82 -82
  124. package/src/runtime/green-contract.ts +46 -46
  125. package/src/runtime/group-join.ts +106 -106
  126. package/src/runtime/heartbeat-gradient.ts +28 -28
  127. package/src/runtime/heartbeat-watcher.ts +124 -124
  128. package/src/runtime/live-agent-control.ts +88 -88
  129. package/src/runtime/live-agent-manager.ts +78 -2
  130. package/src/runtime/live-control-realtime.ts +36 -36
  131. package/src/runtime/live-extension-bridge.ts +150 -0
  132. package/src/runtime/live-irc.ts +92 -0
  133. package/src/runtime/live-session-health.ts +100 -0
  134. package/src/runtime/live-session-runtime.ts +297 -7
  135. package/src/runtime/mcp-proxy.ts +113 -0
  136. package/src/runtime/notebook-helpers.ts +90 -0
  137. package/src/runtime/orphan-sentinel.ts +7 -0
  138. package/src/runtime/output-validator.ts +187 -0
  139. package/src/runtime/parallel-research.ts +44 -44
  140. package/src/runtime/parallel-utils.ts +57 -0
  141. package/src/runtime/parent-guard.ts +80 -0
  142. package/src/runtime/pi-json-output.ts +111 -111
  143. package/src/runtime/policy-engine.ts +79 -79
  144. package/src/runtime/progress-event-coalescer.ts +43 -43
  145. package/src/runtime/prose-compressor.ts +164 -0
  146. package/src/runtime/recovery-recipes.ts +74 -74
  147. package/src/runtime/result-extractor.ts +121 -0
  148. package/src/runtime/role-permission.ts +39 -39
  149. package/src/runtime/runtime-resolver.ts +1 -4
  150. package/src/runtime/semaphore.ts +131 -0
  151. package/src/runtime/sensitive-paths.ts +92 -0
  152. package/src/runtime/session-resources.ts +25 -25
  153. package/src/runtime/session-snapshot.ts +59 -59
  154. package/src/runtime/session-usage.ts +79 -79
  155. package/src/runtime/sidechain-output.ts +29 -29
  156. package/src/runtime/stream-preview.ts +177 -0
  157. package/src/runtime/subagent-manager.ts +3 -2
  158. package/src/runtime/subprocess-tool-registry.ts +67 -0
  159. package/src/runtime/supervisor-contact.ts +59 -59
  160. package/src/runtime/task-display.ts +38 -38
  161. package/src/runtime/task-output-context.ts +59 -9
  162. package/src/runtime/task-runner/capabilities.ts +78 -78
  163. package/src/runtime/task-runner/live-executor.ts +2 -0
  164. package/src/runtime/task-runner/progress.ts +119 -119
  165. package/src/runtime/task-runner/prompt-builder.ts +70 -8
  166. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  167. package/src/runtime/task-runner/result-utils.ts +14 -14
  168. package/src/runtime/task-runner/run-projection.ts +104 -0
  169. package/src/runtime/task-runner/state-helpers.ts +22 -22
  170. package/src/runtime/task-runner.ts +75 -4
  171. package/src/runtime/team-runner.ts +60 -8
  172. package/src/runtime/worker-heartbeat.ts +21 -21
  173. package/src/runtime/worker-startup.ts +57 -57
  174. package/src/runtime/workspace-tree.ts +298 -0
  175. package/src/runtime/yield-handler.ts +189 -0
  176. package/src/schema/config-schema.ts +6 -0
  177. package/src/schema/team-tool-schema.ts +11 -1
  178. package/src/skills/discover-skills.ts +67 -0
  179. package/src/state/active-run-registry.ts +4 -2
  180. package/src/state/artifact-store.ts +4 -1
  181. package/src/state/atomic-write.ts +50 -1
  182. package/src/state/blob-store.ts +117 -0
  183. package/src/state/contracts.ts +1 -0
  184. package/src/state/event-log-rotation.ts +158 -0
  185. package/src/state/event-log.ts +52 -2
  186. package/src/state/mailbox.ts +87 -7
  187. package/src/state/state-store.ts +24 -4
  188. package/src/state/task-claims.ts +44 -44
  189. package/src/state/types.ts +20 -0
  190. package/src/state/usage.ts +29 -29
  191. package/src/subagents/async-entry.ts +1 -1
  192. package/src/subagents/index.ts +3 -3
  193. package/src/subagents/live/control.ts +1 -1
  194. package/src/subagents/live/manager.ts +1 -1
  195. package/src/subagents/live/realtime.ts +1 -1
  196. package/src/subagents/live/session-runtime.ts +1 -1
  197. package/src/subagents/manager.ts +1 -1
  198. package/src/subagents/spawn.ts +1 -1
  199. package/src/teams/team-serializer.ts +38 -38
  200. package/src/types/diff.d.ts +18 -18
  201. package/src/ui/agent-management-overlay.ts +144 -0
  202. package/src/ui/crew-footer.ts +101 -101
  203. package/src/ui/crew-select-list.ts +111 -111
  204. package/src/ui/crew-widget.ts +11 -2
  205. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  206. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  207. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  208. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  209. package/src/ui/dynamic-border.ts +25 -25
  210. package/src/ui/layout-primitives.ts +106 -106
  211. package/src/ui/live-run-sidebar.ts +4 -0
  212. package/src/ui/loaders.ts +158 -158
  213. package/src/ui/powerbar-publisher.ts +77 -15
  214. package/src/ui/render-coalescer.ts +51 -0
  215. package/src/ui/render-diff.ts +119 -119
  216. package/src/ui/render-scheduler.ts +143 -143
  217. package/src/ui/run-dashboard.ts +4 -0
  218. package/src/ui/run-event-bus.ts +209 -0
  219. package/src/ui/run-snapshot-cache.ts +68 -16
  220. package/src/ui/snapshot-types.ts +8 -0
  221. package/src/ui/spinner.ts +17 -17
  222. package/src/ui/status-colors.ts +58 -58
  223. package/src/ui/syntax-highlight.ts +116 -116
  224. package/src/ui/transcript-entries.ts +258 -0
  225. package/src/utils/atomic-write.ts +33 -33
  226. package/src/utils/completion-dedupe.ts +63 -63
  227. package/src/utils/frontmatter.ts +68 -68
  228. package/src/utils/git.ts +262 -262
  229. package/src/utils/ids.ts +17 -12
  230. package/src/utils/incremental-reader.ts +104 -0
  231. package/src/utils/names.ts +27 -27
  232. package/src/utils/redaction.ts +44 -44
  233. package/src/utils/safe-paths.ts +47 -47
  234. package/src/utils/scan-cache.ts +137 -0
  235. package/src/utils/sleep.ts +32 -32
  236. package/src/utils/sse-parser.ts +134 -0
  237. package/src/utils/task-name-generator.ts +337 -0
  238. package/src/utils/visual.ts +33 -2
  239. package/src/workflows/validate-workflow.ts +40 -40
  240. package/src/worktree/branch-freshness.ts +45 -45
  241. package/src/worktree/cleanup.ts +2 -1
  242. package/teams/default.team.md +12 -12
  243. package/teams/fast-fix.team.md +11 -11
  244. package/teams/implementation.team.md +18 -18
  245. package/teams/parallel-research.team.md +14 -14
  246. package/teams/research.team.md +11 -11
  247. package/teams/review.team.md +12 -12
  248. package/workflows/default.workflow.md +29 -29
  249. package/workflows/fast-fix.workflow.md +22 -22
  250. package/workflows/implementation.workflow.md +38 -38
  251. package/workflows/parallel-research.workflow.md +46 -46
  252. package/workflows/research.workflow.md +22 -22
  253. package/workflows/review.workflow.md +30 -30
@@ -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,11 +13,13 @@ import { buildConfiguredModelRouting, formatModelAttemptNote, isRetryableModelFa
13
13
  import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
14
14
  import { runChildPi } from "./child-pi.ts";
15
15
  import { buildTaskPacket } from "./task-packet.ts";
16
+ import { executeHook, appendHookEvent } from "../hooks/registry.ts";
16
17
  import { createVerificationEvidence } from "./green-contract.ts";
17
18
  import { createStartupEvidence } from "./worker-startup.ts";
18
19
  import { permissionForRole } from "./role-permission.ts";
19
20
  import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
20
21
  import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
22
+ import { reserveControlChannel } from "./agent-control.ts";
21
23
  import { parseSessionUsage } from "./session-usage.ts";
22
24
  import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
23
25
  import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
@@ -28,10 +30,16 @@ import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, sh
28
30
  import { checkpointTask, persistSingleTaskUpdate, updateTask } from "./task-runner/state-helpers.ts";
29
31
  import { cleanResultText, isFinalChildEvent } from "./task-runner/result-utils.ts";
30
32
  import { evaluateCompletionMutationGuard } from "./completion-guard.ts";
31
- import { cancellationReasonFromSignal } from "./cancellation.ts";
33
+ import { cancellationReasonFromSignal, buildSyntheticTerminalEvidence } from "./cancellation.ts";
32
34
  import { appendTaskAttentionEvent } from "./attention-events.ts";
33
35
  import { parseSupervisorContactFromLine, recordSupervisorContact } from "./supervisor-contact.ts";
36
+ import { registerStreamBridge, bridgeEventFromJsonEvent } from "./event-stream-bridge.ts";
34
37
  import { renderSkillInstructions } from "./skill-instructions.ts";
38
+ import { DEFAULT_YIELD_CONFIG, extractYieldResult, hasYieldInOutput, isYieldEvent, registerYieldTool, type YieldResult } from "./yield-handler.ts";
39
+ import { validateWorkerOutput, type OutputValidationResult } from "./output-validator.ts";
40
+
41
+ // Register the submit_result tool handler so subprocess events can extract yield data.
42
+ registerYieldTool();
35
43
 
36
44
  export interface TaskRunnerInput {
37
45
  manifest: TeamRunManifest;
@@ -61,6 +69,10 @@ export interface TaskRunnerInput {
61
69
 
62
70
  export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
63
71
  let manifest = input.manifest;
72
+ // H4: registerStreamBridge inside try so dispose() in finally is safe
73
+ let streamBridge: ReturnType<typeof registerStreamBridge> | undefined;
74
+ try {
75
+ streamBridge = registerStreamBridge(manifest.runId);
64
76
  const workspace = prepareTaskWorkspace(manifest, input.task);
65
77
  const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
66
78
  const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
@@ -77,6 +89,8 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
77
89
  heartbeat: createWorkerHeartbeat(input.task.id),
78
90
  agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
79
91
  ...(dependencyContextText ? { dependencyContextText } : {}),
92
+ // Reserve control channel before spawn so cancel/steer can target this task immediately
93
+ controlReservation: reserveControlChannel(input.task.id, manifest.runId),
80
94
  } as TeamTaskState;
81
95
  let tasks = updateTask(input.tasks, task);
82
96
  const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
@@ -84,13 +98,17 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
84
98
  if (runtimeKind === "child-process") ({ task, tasks } = checkpointTask(manifest, tasks, task, "started"));
85
99
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
86
100
  appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, runtime: runtimeKind, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
101
+ // Emit immediate UI notification so widget shows agent as "running" within ~100ms
102
+ // instead of waiting for child process first JSON event (2-5s delay).
103
+ streamBridge?.handler({ runId: manifest.runId, taskId: task.id, eventType: "task.started", timestamp: Date.now() });
87
104
  const permissionMode = permissionForRole(task.role);
88
105
  const renderedSkills = input.skillBlock === undefined ? renderSkillInstructions({ cwd: task.cwd, role: task.role, agent: input.agent, teamRole: { skills: input.teamRoleSkills }, step: input.step, override: input.skillOverride }) : undefined;
89
106
  const skillBlock = input.skillBlock ?? renderedSkills?.block;
90
107
  const skillNames = input.skillNames ?? renderedSkills?.names;
91
108
  const skillPaths = input.skillPaths ?? renderedSkills?.paths;
92
109
 
93
- const prompt = renderTaskPrompt(manifest, input.step, task, input.agent, skillBlock);
110
+ const promptResult = await renderTaskPrompt(manifest, input.step, task, input.agent, skillBlock);
111
+ const prompt = promptResult.full;
94
112
  const promptArtifact = writeArtifact(manifest.artifactsRoot, {
95
113
  kind: "prompt",
96
114
  relativePath: `prompts/${task.id}.md`,
@@ -108,6 +126,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
108
126
  let finalStdout = "";
109
127
  let transcriptPath: string | undefined;
110
128
  let terminalEvidence: OperationTerminalEvidence[] = [];
129
+ const collectedJsonEvents: Record<string, unknown>[] = [];
111
130
 
112
131
  let startupEvidence = createStartupEvidence({ command: runtimeKind === "child-process" ? "pi" : runtimeKind === "live-session" ? "live-session" : "safe-scaffold", startedAt: new Date(task.startedAt ?? new Date().toISOString()), finishedAt: new Date(), promptSentAt: new Date(task.startedAt ?? new Date().toISOString()), promptAccepted: true, exitCode: 0 });
113
132
  const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
@@ -187,9 +206,15 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
187
206
  },
188
207
  onJsonEvent: (event) => {
189
208
  appendCrewAgentEvent(manifest, task.id, event);
209
+ if (event && typeof event === "object" && !Array.isArray(event)) collectedJsonEvents.push(event as Record<string, unknown>);
190
210
  persistHeartbeat();
191
211
  task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
192
212
  tasks = updateTask(tasks, task);
213
+ // Bridge event to UI event bus for near-instant updates
214
+ try {
215
+ const bridgeEvent = bridgeEventFromJsonEvent(manifest.runId, task.id, event);
216
+ if (bridgeEvent) streamBridge?.handler(bridgeEvent);
217
+ } catch { /* bridge errors should not affect task */ }
193
218
  // Feed overflow recovery tracker
194
219
  if (input.onJsonEvent) {
195
220
  try {
@@ -205,7 +230,11 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
205
230
  });
206
231
  const evidenceStatus = childResult.exitStatus?.cancelled ? "cancelled" : childResult.error || (childResult.exitCode && childResult.exitCode !== 0) ? "failed" : "completed";
207
232
  terminalEvidence = [...terminalEvidence, { operation: "worker", status: evidenceStatus, startedAt: attemptStartedAt.toISOString(), finishedAt: new Date().toISOString(), ...(input.signal?.aborted ? { reason: cancellationReasonFromSignal(input.signal) } : {}), ...(childResult.exitStatus ? { exitStatus: childResult.exitStatus } : {}) }];
208
- if (evidenceStatus === "cancelled") appendEvent(manifest.eventsPath, { type: "worker.cancelled", runId: manifest.runId, taskId: task.id, message: input.signal?.aborted ? cancellationReasonFromSignal(input.signal).message : "Worker cancelled.", data: { terminalEvidence: terminalEvidence.at(-1) } });
233
+ if (evidenceStatus === "cancelled") {
234
+ const cancelReason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : { code: "caller_cancelled" as const, message: "Worker cancelled." };
235
+ terminalEvidence.push(buildSyntheticTerminalEvidence("tool", cancelReason, attemptStartedAt.toISOString()));
236
+ appendEvent(manifest.eventsPath, { type: "worker.cancelled", runId: manifest.runId, taskId: task.id, message: cancelReason.message, data: { terminalEvidence: terminalEvidence.at(-1) } });
237
+ }
209
238
  startupEvidence = createStartupEvidence({ command: "pi", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: childResult.exitCode === 0 && !childResult.error, stderr: childResult.stderr, error: childResult.error, exitCode: childResult.exitCode });
210
239
  exitCode = childResult.exitCode;
211
240
  finalStdout = childResult.stdout;
@@ -287,6 +316,20 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
287
316
  });
288
317
  }
289
318
 
319
+ // --- Yield-based completion contract ---
320
+ let yieldResult: YieldResult | undefined;
321
+ const yieldEnabled = input.runtimeConfig?.yield?.enabled ?? DEFAULT_YIELD_CONFIG.enabled;
322
+ if (yieldEnabled && collectedJsonEvents.length > 0) {
323
+ if (hasYieldInOutput(collectedJsonEvents)) {
324
+ const yieldEvent = collectedJsonEvents.find((e) => isYieldEvent(e));
325
+ if (yieldEvent) {
326
+ yieldResult = extractYieldResult(yieldEvent);
327
+ }
328
+ } else if (!error) {
329
+ appendEvent(manifest.eventsPath, { type: "task.attention", runId: manifest.runId, taskId: task.id, message: "Worker completed without calling submit_result tool.", data: { activityState: "needs_attention", reason: "no_yield" } });
330
+ }
331
+ }
332
+
290
333
  const diffArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
291
334
  kind: "diff",
292
335
  relativePath: `diffs/${task.id}.diff`,
@@ -320,6 +363,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
320
363
  tasks = updateTask(tasks, task);
321
364
  }
322
365
 
366
+ // --- Output format validation (caveman Phase 4) ---
367
+ // Validate worker output against the role's output contract.
368
+ // On failure: emit attention event but don't fail the task.
369
+ let outputValidation: OutputValidationResult | undefined;
370
+ if (!error) {
371
+ const outputText = parsedOutput?.finalText ?? finalStdout;
372
+ if (outputText) {
373
+ outputValidation = validateWorkerOutput(task.role, outputText);
374
+ if (!outputValidation.valid) {
375
+ appendEvent(manifest.eventsPath, { type: "task.output_validation", runId: manifest.runId, taskId: task.id, data: { valid: false, formatMatch: outputValidation.formatMatch, structurePreserved: outputValidation.structurePreserved, issues: outputValidation.issues } });
376
+ task = { ...task, agentProgress: { ...(task.agentProgress ?? emptyCrewAgentProgress()), activityState: "needs_attention" } };
377
+ tasks = updateTask(tasks, task);
378
+ }
379
+ }
380
+ }
381
+
323
382
  task = {
324
383
  ...task,
325
384
  status: error ? "failed" : "completed",
@@ -378,10 +437,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
378
437
  content: `${JSON.stringify(buildWorkerPromptPipeline({ artifactsRoot: manifest.artifactsRoot, taskId: task.id, promptArtifact, inputsArtifact, skillArtifact, capabilityArtifact, coordinationArtifact, skillInstructionCount: skillNames?.length ?? 0, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false }), null, 2)}\n`,
379
438
  producer: task.id,
380
439
  });
381
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, ...(skillArtifact ? [skillArtifact] : []), packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, capabilityArtifact, promptPipelineArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
440
+ const outputValidationArtifact = outputValidation ? writeArtifact(manifest.artifactsRoot, {
441
+ kind: "metadata",
442
+ relativePath: `metadata/${task.id}.output-validation.json`,
443
+ content: `${JSON.stringify(outputValidation, null, 2)}\n`,
444
+ producer: task.id,
445
+ }) : undefined;
446
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, ...(skillArtifact ? [skillArtifact] : []), packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, capabilityArtifact, promptPipelineArtifact, ...(outputValidationArtifact ? [outputValidationArtifact] : []), ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
382
447
  saveRunManifest(manifest);
383
448
  tasks = persistSingleTaskUpdate(manifest, tasks, task);
384
449
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
450
+ // Execute task_result hook before emitting terminal event
451
+ const hookReport = await executeHook("task_result", { runId: manifest.runId, taskId: task.id, cwd: manifest.cwd });
452
+ appendHookEvent(manifest, hookReport);
385
453
  appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
386
454
  return { manifest, tasks };
455
+ } finally {
456
+ streamBridge?.dispose();
457
+ }
387
458
  }
@@ -3,6 +3,7 @@ import type { AgentConfig } from "../agents/agent-config.ts";
3
3
  import type { CrewLimitsConfig, CrewRuntimeConfig, CrewReliabilityConfig } from "../config/config.ts";
4
4
  import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
5
5
  import { writeArtifact } from "../state/artifact-store.ts";
6
+ import { executeHook, appendHookEvent } from "../hooks/registry.ts";
6
7
  import { appendEvent } from "../state/event-log.ts";
7
8
  import type { TeamConfig } from "../teams/team-config.ts";
8
9
  import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TaskAttemptState, TeamTaskState } from "../state/types.ts";
@@ -25,7 +26,7 @@ import { childCorrelation, withCorrelation } from "../observability/correlation.
25
26
  import { resolveBatchConcurrency } from "./concurrency.ts";
26
27
  import { mapConcurrent } from "./parallel-utils.ts";
27
28
  import { permissionForRole } from "./role-permission.ts";
28
- import { CrewCancellationError, cancellationReasonFromSignal } from "./cancellation.ts";
29
+ import { CrewCancellationError, buildSyntheticTerminalEvidence, cancellationReasonFromSignal } from "./cancellation.ts";
29
30
  import { effectivenessPolicyDecision, evaluateRunEffectiveness, formatRunEffectivenessLines } from "./effectiveness.ts";
30
31
 
31
32
  export interface ExecuteTeamRunInput {
@@ -506,6 +507,39 @@ function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
506
507
  export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
507
508
  let workflow = input.workflow;
508
509
  let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
510
+
511
+ try {
512
+ return await executeTeamRunCore(input, manifest, workflow);
513
+ } catch (error) {
514
+ // P1: Catch unhandled errors — ensure manifest is set to "failed" so it doesn't stay "running" forever.
515
+ const message = error instanceof Error ? error.message : String(error);
516
+ try {
517
+ manifest = updateRunStatus(manifest, "failed", `Unhandled error in team runner: ${message}`);
518
+ await saveRunManifestAsync(manifest);
519
+ } catch {
520
+ // Best-effort — state write may also fail
521
+ }
522
+ const tasks = refreshTaskGraphQueues(input.tasks).map((task) =>
523
+ task.status === "running" || task.status === "queued" || task.status === "waiting"
524
+ ? { ...task, status: "failed" as const, finishedAt: new Date().toISOString(), error: message }
525
+ : task,
526
+ );
527
+ return { manifest, tasks };
528
+ }
529
+ }
530
+
531
+ async function executeTeamRunCore(
532
+ input: ExecuteTeamRunInput,
533
+ manifest: TeamRunManifest,
534
+ workflow: WorkflowConfig,
535
+ ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
536
+ // Execute before_run_start hook (non-blocking by default)
537
+ const beforeRunReport = await executeHook("before_run_start", { runId: manifest.runId, cwd: manifest.cwd });
538
+ appendHookEvent(manifest, beforeRunReport);
539
+ if (beforeRunReport.outcome === "block") {
540
+ manifest = updateRunStatus(manifest, "blocked", beforeRunReport.reason ?? "before_run_start hook blocked the run.");
541
+ return { manifest, tasks: input.tasks };
542
+ }
509
543
  let tasks = refreshTaskGraphQueues(input.tasks);
510
544
  let queueIndex = buildTaskGraphIndex(tasks);
511
545
  const canInjectAdaptivePlan = workflow.name === "implementation";
@@ -552,7 +586,11 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
552
586
  tasks = tasks.map((task) => {
553
587
  if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") return task;
554
588
  cancelledTaskIds.push(task.id);
555
- return { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: message };
589
+ const base = { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: message };
590
+ if (task.status === "running") {
591
+ return { ...base, terminalEvidence: [...(task.terminalEvidence ?? []), buildSyntheticTerminalEvidence("worker", cancelReason, task.startedAt)] };
592
+ }
593
+ return base;
556
594
  });
557
595
  await saveRunTasksAsync(manifest, tasks);
558
596
  for (const taskId of cancelledTaskIds) appendEvent(manifest.eventsPath, { type: "task.cancelled", runId: manifest.runId, taskId, message, data: { reason: cancelReason.code } });
@@ -594,8 +632,18 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
594
632
  }
595
633
 
596
634
  appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, message: `Starting ready batch with ${readyBatch.length} task(s).`, data: { taskIds: readyBatch.map((task) => task.id), readyCount: snapshot.ready.length, blockedCount: snapshot.blocked.length, runningCount: snapshot.running.length, doneCount: snapshot.done.length, selectedCount: readyBatch.length, maxConcurrent: concurrency.maxConcurrent, defaultConcurrency: concurrency.defaultConcurrency, concurrencyReason: approvalPending ? `${concurrency.reason};plan-approval-read-only` : concurrency.reason } });
635
+ // Execute before_task_start hooks for the batch
636
+ for (const task of readyBatch) {
637
+ const taskReport = await executeHook("before_task_start", { runId: manifest.runId, taskId: task.id, cwd: manifest.cwd });
638
+ appendHookEvent(manifest, taskReport);
639
+ if (taskReport.outcome === "block") {
640
+ tasks = tasks.map((t) => t.id === task.id ? { ...t, status: "skipped" as const, error: taskReport.reason ?? "before_task_start hook blocked execution." } : t);
641
+ manifest = updateRunStatus(manifest, manifest.status, `Task '${task.id}' blocked by hook.`);
642
+ }
643
+ }
644
+ const batchTasks = readyBatch.filter((task) => tasks.find((t) => t.id === task.id && t.status !== "skipped"));
597
645
  const results = await mapConcurrent(
598
- readyBatch,
646
+ batchTasks,
599
647
  concurrency.selectedCount,
600
648
  async (task) => {
601
649
  const step = findStep(workflow, task);
@@ -604,6 +652,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
604
652
  const baseInput = { manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, teamRoleModel: teamRole?.model, teamRoleSkills: teamRole?.skills, skillOverride: input.skillOverride, limits: input.limits, onJsonEvent: input.onJsonEvent };
605
653
  if (input.reliability?.autoRetry !== true) return withCorrelation(childCorrelation(manifest.runId, task.id), () => runTeamTask(baseInput));
606
654
  let lastFailed: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
655
+ let lastAttemptId: string | undefined;
607
656
  const attemptsSoFar: TaskAttemptState[] = [...(task.attempts ?? [])];
608
657
  const policy = retryPolicyFromConfig(input.reliability);
609
658
  try {
@@ -634,10 +683,12 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
634
683
  signal: input.signal,
635
684
  attemptId: (attempt) => `${manifest.runId}:${task.id}:attempt-${attempt}`,
636
685
  onAttemptFailed: (attempt, error, delayMs, info) => {
637
- appendEvent(manifest.eventsPath, { type: "crew.task.retry_attempt", runId: manifest.runId, taskId: task.id, message: error.message, data: { attempt, attemptId: info.attemptId, delayMs } });
686
+ lastAttemptId = info.attemptId;
687
+ appendEvent(manifest.eventsPath, { type: "crew.task.retry_attempt", runId: manifest.runId, taskId: task.id, message: error.message, data: { attempt, attemptId: info.attemptId, delayMs }, metadata: { attemptId: info.attemptId } });
638
688
  input.metricRegistry?.histogram("crew.task.retry_delay_ms", "Retry backoff delay, milliseconds").observe({ runId: manifest.runId, taskId: task.id }, delayMs);
639
689
  },
640
690
  onRetryGivenUp: (attempts, error, info) => {
691
+ lastAttemptId = info.attemptId;
641
692
  appendDeadletter(manifest, { runId: manifest.runId, taskId: task.id, reason: "max-retries", attempts, attemptId: info.attemptId, lastError: error.message, timestamp: new Date().toISOString() });
642
693
  input.metricRegistry?.counter("crew.task.deadletter_total", "Deadletter triggers by reason").inc({ reason: "max-retries" });
643
694
  input.metricRegistry?.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]).observe({ runId: manifest.runId, team: input.team.name }, Math.max(0, attempts - 1));
@@ -650,7 +701,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
650
701
  const freshManifest = fresh?.manifest ?? manifest;
651
702
  const freshTasks = fresh?.tasks ?? tasks;
652
703
  const cancelledTasks = freshTasks.map((item) => item.id === task.id && (item.status === "queued" || item.status === "running") ? { ...item, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: `${reason.message} (${reason.code})` } : item);
653
- appendEvent(freshManifest.eventsPath, { type: "task.cancelled", runId: freshManifest.runId, taskId: task.id, message: reason.message, data: { reason, phase: "retry" } });
704
+ appendEvent(freshManifest.eventsPath, { type: "task.cancelled", runId: freshManifest.runId, taskId: task.id, message: reason.message, data: { reason, phase: "retry" }, metadata: lastAttemptId ? { attemptId: lastAttemptId } : undefined });
654
705
  return { manifest: updateRunStatus(freshManifest, "cancelled", reason.message), tasks: cancelledTasks };
655
706
  }
656
707
  if (lastFailed) return lastFailed;
@@ -663,6 +714,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
663
714
  }
664
715
  },
665
716
  );
717
+ if (results.length === 0) break;
666
718
  manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
667
719
  tasks = __test__mergeTaskUpdates(tasks, results);
668
720
  const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
@@ -701,14 +753,14 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
701
753
  }
702
754
  await saveRunTasksAsync(manifest, tasks);
703
755
  saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
704
- const completedBatch = readyBatch.map((task) => tasks.find((item) => item.id === task.id) ?? task);
756
+ const completedBatch = batchTasks.map((task) => tasks.find((item) => item.id === task.id) ?? task);
705
757
  const batchArtifact = writeArtifact(manifest.artifactsRoot, {
706
758
  kind: "summary",
707
- relativePath: `batches/${readyBatch.map((task) => task.id).join("+")}.md`,
759
+ relativePath: `batches/${batchTasks.map((task) => task.id).join("+")}.md`,
708
760
  producer: "team-runner",
709
761
  content: aggregateTaskOutputs(completedBatch, manifest),
710
762
  });
711
- const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: readyBatch, allTasks: tasks });
763
+ const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: batchTasks, allTasks: tasks });
712
764
  manifest = { ...manifest, artifacts: mergeArtifacts([...manifest.artifacts, batchArtifact, ...(groupDelivery?.artifact ? [groupDelivery.artifact] : [])]) };
713
765
  manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
714
766
  await saveRunManifestAsync(manifest);
@@ -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
+ }