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
@@ -0,0 +1,209 @@
1
+ import type { TeamEvent } from "../state/event-log.ts";
2
+
3
+ export type RunEventType =
4
+ | "task_started"
5
+ | "task_completed"
6
+ | "task_failed"
7
+ | "task_cancelled"
8
+ | "worker_status"
9
+ | "mailbox_updated"
10
+ | "effectiveness_changed"
11
+ | "run_started"
12
+ | "run_completed"
13
+ | "run_blocked"
14
+ | "run_cancelled";
15
+
16
+ /** Typed channel names for category-based event subscription. */
17
+ export type EventChannel =
18
+ | "worker:progress"
19
+ | "worker:lifecycle"
20
+ | "worker:stream"
21
+ | "run:state"
22
+ | "ui:invalidate";
23
+
24
+ /** Sets used by classifyEventChannel for O(1) lookup. */
25
+ const WORKER_PROGRESS_TYPES = new Set([
26
+ "tool_execution_start", "tool_result", "agent_progress", "worker_status",
27
+ ]);
28
+ const WORKER_LIFECYCLE_TYPES = new Set([
29
+ "task.started", "task.completed", "task.failed",
30
+ "task_started", "task_completed", "task_failed", "task_cancelled",
31
+ "run.started", "run.completed", "run.cancelled", "run.failed",
32
+ "run_started", "run_completed", "run_cancelled", "run_blocked",
33
+ ]);
34
+ const WORKER_STREAM_TYPES = new Set([
35
+ "stdout_chunk", "stderr_chunk", "stream",
36
+ ]);
37
+ const RUN_STATE_TYPES = new Set([
38
+ "manifest.saved", "task.claimed", "task.unclaimed", "mailbox_updated",
39
+ ]);
40
+ const UI_INVALIDATE_TYPES = new Set([
41
+ "effectiveness_changed", "snapshot_stale",
42
+ ]);
43
+
44
+ /** Classify an event type string into a typed channel. */
45
+ export function classifyEventChannel(type: string): EventChannel {
46
+ if (WORKER_PROGRESS_TYPES.has(type)) return "worker:progress";
47
+ if (WORKER_LIFECYCLE_TYPES.has(type)) return "worker:lifecycle";
48
+ if (WORKER_STREAM_TYPES.has(type)) return "worker:stream";
49
+ if (RUN_STATE_TYPES.has(type)) return "run:state";
50
+ if (UI_INVALIDATE_TYPES.has(type)) return "ui:invalidate";
51
+ return "worker:progress"; // default fallback
52
+ }
53
+
54
+ export interface RunEventPayload {
55
+ type: RunEventType;
56
+ runId: string;
57
+ taskId?: string;
58
+ timestamp?: string;
59
+ data?: unknown;
60
+ channel?: EventChannel;
61
+ }
62
+
63
+ export type RunEventCallback = (event: RunEventPayload) => void;
64
+
65
+ class RunEventBus {
66
+ #listeners = new Map<string, Set<RunEventCallback>>();
67
+ #globalListeners = new Set<RunEventCallback>();
68
+ #channelListeners = new Map<EventChannel, Set<RunEventCallback>>();
69
+ #channelRunListeners = new Map<string, Map<EventChannel, Set<RunEventCallback>>>();
70
+
71
+ on(runId: string, callback: RunEventCallback): () => void {
72
+ const listeners = this.#listeners.get(runId) ?? new Set();
73
+ listeners.add(callback);
74
+ this.#listeners.set(runId, listeners);
75
+ return () => { listeners.delete(callback); if (listeners.size === 0) this.#listeners.delete(runId); };
76
+ }
77
+
78
+ onAny(callback: RunEventCallback): () => void {
79
+ this.#globalListeners.add(callback);
80
+ return () => { this.#globalListeners.delete(callback); };
81
+ }
82
+
83
+ off(runId: string, callback: RunEventCallback): void {
84
+ const listeners = this.#listeners.get(runId);
85
+ if (listeners) {
86
+ listeners.delete(callback);
87
+ if (listeners.size === 0) this.#listeners.delete(runId);
88
+ }
89
+ }
90
+
91
+ /** Subscribe to all events on a specific channel. */
92
+ onChannel(channel: EventChannel, callback: RunEventCallback): () => void {
93
+ const listeners = this.#channelListeners.get(channel) ?? new Set();
94
+ listeners.add(callback);
95
+ this.#channelListeners.set(channel, listeners);
96
+ return () => {
97
+ listeners.delete(callback);
98
+ if (listeners.size === 0) this.#channelListeners.delete(channel);
99
+ };
100
+ }
101
+
102
+ /** Subscribe to events on a specific channel for a given runId. */
103
+ onChannelForRun(channel: EventChannel, runId: string, callback: RunEventCallback): () => void {
104
+ const runKey = `${channel}::${runId}`;
105
+ const runMap = this.#channelRunListeners.get(runKey) ?? new Map();
106
+ const listeners = runMap.get(channel) ?? new Set();
107
+ listeners.add(callback);
108
+ runMap.set(channel, listeners);
109
+ this.#channelRunListeners.set(runKey, runMap);
110
+ return () => {
111
+ listeners.delete(callback);
112
+ if (listeners.size === 0) runMap.delete(channel);
113
+ if (runMap.size === 0) this.#channelRunListeners.delete(runKey);
114
+ };
115
+ }
116
+
117
+ emit(event: RunEventPayload): void {
118
+ // Auto-classify channel if not already set.
119
+ // M2: Use local variable for routing, but also set on event
120
+ // for subscriber API contract (listeners read event.channel).
121
+ if (!event.channel) {
122
+ (event as { channel?: EventChannel }).channel = classifyEventChannel(event.type);
123
+ }
124
+ const channel = event.channel!;
125
+
126
+ // Existing: runId-specific listeners
127
+ const listeners = this.#listeners.get(event.runId);
128
+ if (listeners) {
129
+ for (const cb of listeners) {
130
+ try { cb(event); } catch { /* subscriber errors are non-fatal */ }
131
+ }
132
+ }
133
+
134
+ // Existing: global listeners
135
+ for (const cb of this.#globalListeners) {
136
+ try { cb(event); } catch { /* subscriber errors are non-fatal */ }
137
+ }
138
+
139
+ // New: channel listeners
140
+ const channelListeners = this.#channelListeners.get(channel);
141
+ if (channelListeners) {
142
+ for (const cb of channelListeners) {
143
+ try { cb(event); } catch { /* subscriber errors are non-fatal */ }
144
+ }
145
+ }
146
+
147
+ // New: channel+runId listeners
148
+ const runKey = `${channel}::${event.runId}`;
149
+ const runMap = this.#channelRunListeners.get(runKey);
150
+ if (runMap) {
151
+ const runChannelListeners = runMap.get(channel);
152
+ if (runChannelListeners) {
153
+ for (const cb of runChannelListeners) {
154
+ try { cb(event); } catch { /* subscriber errors are non-fatal */ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ /** Dispose all subscriptions including channel-based ones. */
161
+ dispose(): void {
162
+ this.#listeners.clear();
163
+ this.#globalListeners.clear();
164
+ this.#channelListeners.clear();
165
+ this.#channelRunListeners.clear();
166
+ }
167
+
168
+ listenerCount(runId?: string): number {
169
+ if (runId) return this.#listeners.get(runId)?.size ?? 0;
170
+ let total = this.#globalListeners.size;
171
+ for (const set of this.#listeners.values()) total += set.size;
172
+ for (const set of this.#channelListeners.values()) total += set.size;
173
+ for (const runMap of this.#channelRunListeners.values()) {
174
+ for (const set of runMap.values()) total += set.size;
175
+ }
176
+ return total;
177
+ }
178
+ }
179
+
180
+ /** Global singleton run event bus for UI-first event delivery. */
181
+ export const runEventBus = new RunEventBus();
182
+
183
+ /** Derive a RunEventType from a TeamEvent. */
184
+ export function teamEventToRunEventType(event: TeamEvent): RunEventType | undefined {
185
+ const type = event.type;
186
+ if (type === "task.started") return "task_started";
187
+ if (type === "task.completed") return "task_completed";
188
+ if (type === "task.failed") return "task_failed";
189
+ if (type === "run.completed") return "run_completed";
190
+ if (type === "run.blocked") return "run_blocked";
191
+ if (type === "run.running") return "run_started";
192
+ if (type === "run.cancelled") return "run_cancelled";
193
+ if (type === "task.progress" || type === "mailbox.message_queued" || type === "mailbox.message_delivered") return "mailbox_updated";
194
+ if (type === "run.effectiveness" || type === "task.attention") return "effectiveness_changed";
195
+ return undefined;
196
+ }
197
+
198
+ /** Emit a run event from a TeamEvent. */
199
+ export function emitFromTeamEvent(event: TeamEvent): void {
200
+ const type = teamEventToRunEventType(event);
201
+ if (!type) return;
202
+ runEventBus.emit({
203
+ type,
204
+ runId: event.runId,
205
+ taskId: event.taskId,
206
+ timestamp: event.time,
207
+ data: event.data,
208
+ });
209
+ }
@@ -9,6 +9,7 @@ import type { MailboxMessageStatus } from "../state/mailbox.ts";
9
9
  import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
10
10
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
11
11
  import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
12
+ import { runEventBus } from "./run-event-bus.ts";
12
13
 
13
14
  export interface RunSnapshotCache extends RunSnapshotCacheBase {
14
15
  preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
@@ -378,6 +379,13 @@ interface MailboxCount {
378
379
  approximate: boolean;
379
380
  }
380
381
 
382
+ interface MailboxKindCount extends MailboxCount {
383
+ steer: number;
384
+ followUp: number;
385
+ response: number;
386
+ message: number;
387
+ }
388
+
381
389
  function tailApproximate(filePath: string): boolean {
382
390
  try {
383
391
  return fs.statSync(filePath).size > MAX_TAIL_BYTES;
@@ -394,34 +402,54 @@ async function tailApproximateAsync(filePath: string): Promise<boolean> {
394
402
  }
395
403
  }
396
404
 
397
- function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): MailboxCount {
405
+ function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): MailboxKindCount {
406
+ const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
398
407
  const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
399
408
  try {
400
409
  const parsed = JSON.parse(line) as unknown;
401
410
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
402
- const message = parsed as { id?: unknown; status?: unknown };
403
- if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
404
- return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
411
+ const msg = parsed as { id?: unknown; status?: unknown; kind?: unknown; data?: unknown };
412
+ if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
413
+ if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
414
+ const kind = typeof msg.kind === "string" ? msg.kind : typeof (msg.data as Record<string, unknown>)?.kind === "string" ? (msg.data as Record<string, unknown>).kind as string : undefined;
415
+ if (kind === "steer") kindCounts.steer++;
416
+ else if (kind === "follow-up") kindCounts.followUp++;
417
+ else if (kind === "response") kindCounts.response++;
418
+ else kindCounts.message++;
419
+ return 1;
420
+ }
421
+ return 0;
405
422
  } catch {
406
423
  return 0;
407
424
  }
408
425
  }) as number[];
409
- return { count: items.reduce((sum, val) => sum + val, 0), approximate: tailApproximate(filePath) };
426
+ const count = items.reduce((sum, val) => sum + val, 0);
427
+ return { count, approximate: tailApproximate(filePath), steer: kindCounts.steer, followUp: kindCounts.followUp, response: kindCounts.response, message: kindCounts.message };
410
428
  }
411
429
 
412
- async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<MailboxCount> {
430
+ async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<MailboxKindCount> {
431
+ const kindCounts = { steer: 0, followUp: 0, response: 0, message: 0 };
413
432
  const items = await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
414
433
  try {
415
434
  const parsed = JSON.parse(line) as unknown;
416
435
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
417
- const message = parsed as { id?: unknown; status?: unknown };
418
- if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
419
- return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
436
+ const msg = parsed as { id?: unknown; status?: unknown; kind?: unknown; data?: unknown };
437
+ if (typeof msg.id !== "string" || !isMailboxStatus(msg.status)) return 0;
438
+ if (msg.status !== "acknowledged" && delivery[msg.id] !== "acknowledged") {
439
+ const kind = typeof msg.kind === "string" ? msg.kind : typeof (msg.data as Record<string, unknown>)?.kind === "string" ? (msg.data as Record<string, unknown>).kind as string : undefined;
440
+ if (kind === "steer") kindCounts.steer++;
441
+ else if (kind === "follow-up") kindCounts.followUp++;
442
+ else if (kind === "response") kindCounts.response++;
443
+ else kindCounts.message++;
444
+ return 1;
445
+ }
446
+ return 0;
420
447
  } catch {
421
448
  return 0;
422
449
  }
423
450
  }) as number[];
424
- return { count: items.reduce((sum, val) => sum + val, 0), approximate: await tailApproximateAsync(filePath) };
451
+ const count = items.reduce((sum, val) => sum + val, 0);
452
+ return { count, approximate: await tailApproximateAsync(filePath), steer: kindCounts.steer, followUp: kindCounts.followUp, response: kindCounts.response, message: kindCounts.message };
425
453
  }
426
454
 
427
455
  function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
@@ -436,6 +464,17 @@ async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGrou
436
464
  return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
437
465
  }
438
466
 
467
+ function mergeKindCounts(a: MailboxKindCount, b: MailboxKindCount): MailboxKindCount {
468
+ return {
469
+ count: a.count + b.count,
470
+ approximate: a.approximate || b.approximate,
471
+ steer: a.steer + b.steer,
472
+ followUp: a.followUp + b.followUp,
473
+ response: a.response + b.response,
474
+ message: a.message + b.message,
475
+ };
476
+ }
477
+
439
478
  function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
440
479
  const root = path.join(manifest.stateRoot, "mailbox");
441
480
  const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
@@ -447,14 +486,17 @@ function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunU
447
486
  if (!entry.isDirectory()) continue;
448
487
  const taskInbox = readMailboxCounts(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
449
488
  const taskOutbox = readMailboxCounts(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
450
- inbox = { count: inbox.count + taskInbox.count, approximate: inbox.approximate || taskInbox.approximate };
451
- outbox = { count: outbox.count + taskOutbox.count, approximate: outbox.approximate || taskOutbox.approximate };
489
+ inbox = mergeKindCounts(inbox, taskInbox);
490
+ outbox = mergeKindCounts(outbox, taskOutbox);
452
491
  }
453
492
  } catch {
454
493
  // No task mailboxes yet.
455
494
  }
456
495
  const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
457
- return { inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate };
496
+ return {
497
+ inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate,
498
+ steerUnread: inbox.steer + outbox.steer, followUpUnread: inbox.followUp + outbox.followUp, responseUnread: inbox.response + outbox.response, messageUnread: inbox.message + outbox.message,
499
+ };
458
500
  }
459
501
 
460
502
  async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
@@ -468,14 +510,17 @@ async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentReco
468
510
  if (!entry.isDirectory()) continue;
469
511
  const taskInbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
470
512
  const taskOutbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
471
- inbox = { count: inbox.count + taskInbox.count, approximate: inbox.approximate || taskInbox.approximate };
472
- outbox = { count: outbox.count + taskOutbox.count, approximate: outbox.approximate || taskOutbox.approximate };
513
+ inbox = mergeKindCounts(inbox, taskInbox);
514
+ outbox = mergeKindCounts(outbox, taskOutbox);
473
515
  }
474
516
  } catch {
475
517
  // No task mailboxes yet.
476
518
  }
477
519
  const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
478
- return { inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate };
520
+ return {
521
+ inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate,
522
+ steerUnread: inbox.steer + outbox.steer, followUpUnread: inbox.followUp + outbox.followUp, responseUnread: inbox.response + outbox.response, messageUnread: inbox.message + outbox.message,
523
+ };
479
524
  }
480
525
 
481
526
  function cancellationReasonFromEvents(events: TeamEvent[]): string | undefined {
@@ -688,6 +733,12 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
688
733
  }
689
734
  }
690
735
 
736
+ const unsubscribe = runEventBus.onAny((event) => {
737
+ if (entries.has(event.runId)) {
738
+ entries.delete(event.runId);
739
+ }
740
+ });
741
+
691
742
  return {
692
743
  get(runId: string): RunUiSnapshot | undefined {
693
744
  const entry = entries.get(runId);
@@ -719,6 +770,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
719
770
  return new Map([...entries.entries()].map(([key, entry]) => [key, entry.snapshot]));
720
771
  },
721
772
  dispose(): void {
773
+ unsubscribe();
722
774
  entries.clear();
723
775
  },
724
776
  };
@@ -23,6 +23,14 @@ export interface RunUiMailbox {
23
23
  inboxUnread: number;
24
24
  outboxPending: number;
25
25
  needsAttention: number;
26
+ /** Urgent steering messages count. Default 0. */
27
+ steerUnread?: number;
28
+ /** Follow-up / continuation messages count. Default 0. */
29
+ followUpUnread?: number;
30
+ /** Response / reply messages count. Default 0. */
31
+ responseUnread?: number;
32
+ /** Generic messages count. Default 0. */
33
+ messageUnread?: number;
26
34
  /** True when counts come from bounded tail reads and older messages may be omitted. */
27
35
  approximate?: boolean;
28
36
  }
package/src/ui/spinner.ts CHANGED
@@ -1,17 +1,17 @@
1
- export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
2
- export const SUBAGENT_SPINNER_FRAME_MS = 160;
3
-
4
- export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
5
- return Math.floor(now / Math.max(1, frameMs));
6
- }
7
-
8
- function hashKey(key: string): number {
9
- let hash = 0;
10
- for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
11
- return hash;
12
- }
13
-
14
- export function spinnerFrame(key = "", now = Date.now()): string {
15
- const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
16
- return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
17
- }
1
+ export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
2
+ export const SUBAGENT_SPINNER_FRAME_MS = 160;
3
+
4
+ export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
5
+ return Math.floor(now / Math.max(1, frameMs));
6
+ }
7
+
8
+ function hashKey(key: string): number {
9
+ let hash = 0;
10
+ for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
11
+ return hash;
12
+ }
13
+
14
+ export function spinnerFrame(key = "", now = Date.now()): string {
15
+ const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
16
+ return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
17
+ }
@@ -1,58 +1,58 @@
1
- import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
2
-
3
- export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
4
-
5
- export function colorForStatus(status: RunStatus): CrewThemeColor {
6
- switch (status) {
7
- case "running":
8
- return "accent";
9
- case "waiting":
10
- return "muted";
11
- case "completed":
12
- return "success";
13
- case "failed":
14
- case "stale":
15
- return "error";
16
- case "cancelled":
17
- case "blocked":
18
- case "stopped":
19
- return "warning";
20
- case "queued":
21
- default:
22
- return "dim";
23
- }
24
- }
25
-
26
- export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
27
- const glyph = options?.runningGlyph ?? "▶";
28
- switch (status) {
29
- case "completed":
30
- return "✓";
31
- case "failed":
32
- case "stale":
33
- return "✗";
34
- case "cancelled":
35
- case "stopped":
36
- return "■";
37
- case "running":
38
- return glyph;
39
- case "waiting":
40
- return "⏳";
41
- case "queued":
42
- return "◦";
43
- case "blocked":
44
- return "⏸";
45
- default:
46
- return "·";
47
- }
48
- }
49
-
50
- export function colorForActivity(activityState: string | undefined): CrewThemeColor {
51
- if (activityState === "needs_attention") return "warning";
52
- if (activityState === "stale") return "error";
53
- return "dim";
54
- }
55
-
56
- export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
57
- return theme.fg(colorForStatus(status), text);
58
- }
1
+ import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
2
+
3
+ export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
4
+
5
+ export function colorForStatus(status: RunStatus): CrewThemeColor {
6
+ switch (status) {
7
+ case "running":
8
+ return "accent";
9
+ case "waiting":
10
+ return "muted";
11
+ case "completed":
12
+ return "success";
13
+ case "failed":
14
+ case "stale":
15
+ return "error";
16
+ case "cancelled":
17
+ case "blocked":
18
+ case "stopped":
19
+ return "warning";
20
+ case "queued":
21
+ default:
22
+ return "dim";
23
+ }
24
+ }
25
+
26
+ export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
27
+ const glyph = options?.runningGlyph ?? "▶";
28
+ switch (status) {
29
+ case "completed":
30
+ return "✓";
31
+ case "failed":
32
+ case "stale":
33
+ return "✗";
34
+ case "cancelled":
35
+ case "stopped":
36
+ return "■";
37
+ case "running":
38
+ return glyph;
39
+ case "waiting":
40
+ return "⏳";
41
+ case "queued":
42
+ return "◦";
43
+ case "blocked":
44
+ return "⏸";
45
+ default:
46
+ return "·";
47
+ }
48
+ }
49
+
50
+ export function colorForActivity(activityState: string | undefined): CrewThemeColor {
51
+ if (activityState === "needs_attention") return "warning";
52
+ if (activityState === "stale") return "error";
53
+ return "dim";
54
+ }
55
+
56
+ export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
57
+ return theme.fg(colorForStatus(status), text);
58
+ }