pi-crew 0.1.45 → 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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -5
  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 +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/next-upgrade-roadmap.md +808 -0
  14. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  15. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  16. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  17. package/docs/research/AUDIT_PI_CREW.md +457 -0
  18. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  19. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  20. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  21. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  22. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  23. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  24. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  25. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  26. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  27. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  28. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  29. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  30. package/docs/research-oh-my-pi-distillation.md +369 -0
  31. package/docs/source-runtime-refactor-map.md +24 -0
  32. package/docs/usage.md +3 -3
  33. package/install.mjs +52 -8
  34. package/package.json +99 -98
  35. package/schema.json +10 -1
  36. package/skills/async-worker-recovery/SKILL.md +42 -0
  37. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  38. package/skills/delegation-patterns/SKILL.md +54 -0
  39. package/skills/mailbox-interactive/SKILL.md +40 -0
  40. package/skills/model-routing-context/SKILL.md +39 -0
  41. package/skills/multi-perspective-review/SKILL.md +58 -0
  42. package/skills/observability-reliability/SKILL.md +41 -0
  43. package/skills/orchestration/SKILL.md +157 -0
  44. package/skills/ownership-session-security/SKILL.md +41 -0
  45. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  46. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  47. package/skills/resource-discovery-config/SKILL.md +41 -0
  48. package/skills/runtime-state-reader/SKILL.md +44 -0
  49. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  50. package/skills/state-mutation-locking/SKILL.md +42 -0
  51. package/skills/systematic-debugging/SKILL.md +67 -0
  52. package/skills/ui-render-performance/SKILL.md +39 -0
  53. package/skills/verification-before-done/SKILL.md +57 -0
  54. package/skills/worktree-isolation/SKILL.md +39 -0
  55. package/src/agents/agent-config.ts +6 -0
  56. package/src/agents/agent-search.ts +98 -0
  57. package/src/agents/agent-serializer.ts +38 -34
  58. package/src/agents/discover-agents.ts +29 -15
  59. package/src/config/config.ts +72 -24
  60. package/src/config/defaults.ts +25 -0
  61. package/src/extension/autonomous-policy.ts +26 -33
  62. package/src/extension/help.ts +1 -0
  63. package/src/extension/management.ts +5 -0
  64. package/src/extension/project-init.ts +62 -2
  65. package/src/extension/register.ts +69 -22
  66. package/src/extension/registration/commands.ts +64 -25
  67. package/src/extension/registration/compaction-guard.ts +1 -1
  68. package/src/extension/registration/subagent-helpers.ts +8 -0
  69. package/src/extension/registration/subagent-tools.ts +149 -148
  70. package/src/extension/registration/team-tool.ts +14 -10
  71. package/src/extension/run-index.ts +35 -21
  72. package/src/extension/run-maintenance.ts +30 -5
  73. package/src/extension/team-tool/api.ts +47 -9
  74. package/src/extension/team-tool/cancel.ts +109 -5
  75. package/src/extension/team-tool/context.ts +8 -0
  76. package/src/extension/team-tool/intent-policy.ts +42 -0
  77. package/src/extension/team-tool/lifecycle-actions.ts +120 -79
  78. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  79. package/src/extension/team-tool/respond.ts +46 -18
  80. package/src/extension/team-tool/run.ts +55 -12
  81. package/src/extension/team-tool/status.ts +13 -2
  82. package/src/extension/team-tool-types.ts +3 -0
  83. package/src/extension/team-tool.ts +45 -14
  84. package/src/hooks/registry.ts +61 -0
  85. package/src/hooks/types.ts +41 -0
  86. package/src/observability/event-to-metric.ts +8 -1
  87. package/src/runtime/agent-control.ts +169 -63
  88. package/src/runtime/async-runner.ts +3 -1
  89. package/src/runtime/background-runner.ts +78 -53
  90. package/src/runtime/cancellation-token.ts +89 -0
  91. package/src/runtime/cancellation.ts +61 -0
  92. package/src/runtime/capability-inventory.ts +116 -0
  93. package/src/runtime/child-pi.ts +458 -444
  94. package/src/runtime/code-summary.ts +247 -0
  95. package/src/runtime/crash-recovery.ts +182 -0
  96. package/src/runtime/crew-agent-records.ts +70 -10
  97. package/src/runtime/crew-agent-runtime.ts +1 -0
  98. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  99. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  100. package/src/runtime/deadletter.ts +1 -0
  101. package/src/runtime/delivery-coordinator.ts +48 -25
  102. package/src/runtime/effectiveness.ts +81 -0
  103. package/src/runtime/event-stream-bridge.ts +90 -0
  104. package/src/runtime/live-agent-control.ts +2 -1
  105. package/src/runtime/live-agent-manager.ts +179 -85
  106. package/src/runtime/live-control-realtime.ts +1 -1
  107. package/src/runtime/live-extension-bridge.ts +150 -0
  108. package/src/runtime/live-irc.ts +92 -0
  109. package/src/runtime/live-session-health.ts +100 -0
  110. package/src/runtime/live-session-runtime.ts +599 -305
  111. package/src/runtime/manifest-cache.ts +17 -2
  112. package/src/runtime/mcp-proxy.ts +113 -0
  113. package/src/runtime/model-fallback.ts +6 -4
  114. package/src/runtime/notebook-helpers.ts +90 -0
  115. package/src/runtime/orphan-sentinel.ts +7 -0
  116. package/src/runtime/output-validator.ts +187 -0
  117. package/src/runtime/parallel-utils.ts +57 -0
  118. package/src/runtime/parent-guard.ts +80 -0
  119. package/src/runtime/pi-args.ts +18 -3
  120. package/src/runtime/process-status.ts +5 -1
  121. package/src/runtime/prose-compressor.ts +164 -0
  122. package/src/runtime/result-extractor.ts +121 -0
  123. package/src/runtime/retry-executor.ts +81 -64
  124. package/src/runtime/runtime-resolver.ts +23 -10
  125. package/src/runtime/semaphore.ts +131 -0
  126. package/src/runtime/sensitive-paths.ts +92 -0
  127. package/src/runtime/skill-instructions.ts +222 -0
  128. package/src/runtime/stale-reconciler.ts +4 -14
  129. package/src/runtime/stream-preview.ts +177 -0
  130. package/src/runtime/subagent-manager.ts +6 -2
  131. package/src/runtime/subprocess-tool-registry.ts +67 -0
  132. package/src/runtime/task-output-context.ts +177 -127
  133. package/src/runtime/task-runner/capabilities.ts +78 -0
  134. package/src/runtime/task-runner/live-executor.ts +107 -101
  135. package/src/runtime/task-runner/prompt-builder.ts +72 -8
  136. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  137. package/src/runtime/task-runner/run-projection.ts +104 -0
  138. package/src/runtime/task-runner.ts +115 -5
  139. package/src/runtime/team-runner.ts +134 -19
  140. package/src/runtime/workspace-tree.ts +298 -0
  141. package/src/runtime/yield-handler.ts +189 -0
  142. package/src/schema/config-schema.ts +7 -0
  143. package/src/schema/team-tool-schema.ts +14 -4
  144. package/src/skills/discover-skills.ts +67 -0
  145. package/src/state/active-run-registry.ts +167 -0
  146. package/src/state/artifact-store.ts +4 -1
  147. package/src/state/atomic-write.ts +50 -1
  148. package/src/state/blob-store.ts +117 -0
  149. package/src/state/contracts.ts +2 -1
  150. package/src/state/event-log-rotation.ts +158 -0
  151. package/src/state/event-log.ts +52 -2
  152. package/src/state/mailbox.ts +129 -9
  153. package/src/state/state-store.ts +32 -5
  154. package/src/state/types.ts +64 -2
  155. package/src/teams/team-config.ts +1 -0
  156. package/src/ui/agent-management-overlay.ts +144 -0
  157. package/src/ui/crew-widget.ts +15 -5
  158. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  159. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  160. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  161. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  162. package/src/ui/live-run-sidebar.ts +4 -0
  163. package/src/ui/powerbar-publisher.ts +77 -15
  164. package/src/ui/render-coalescer.ts +51 -0
  165. package/src/ui/run-dashboard.ts +4 -0
  166. package/src/ui/run-event-bus.ts +209 -0
  167. package/src/ui/run-snapshot-cache.ts +78 -18
  168. package/src/ui/snapshot-types.ts +10 -0
  169. package/src/ui/transcript-entries.ts +258 -0
  170. package/src/utils/ids.ts +5 -0
  171. package/src/utils/incremental-reader.ts +104 -0
  172. package/src/utils/paths.ts +4 -2
  173. package/src/utils/scan-cache.ts +137 -0
  174. package/src/utils/sse-parser.ts +134 -0
  175. package/src/utils/task-name-generator.ts +337 -0
  176. package/src/utils/visual.ts +33 -2
  177. package/src/workflows/workflow-config.ts +1 -0
  178. package/src/worktree/cleanup.ts +2 -1
@@ -0,0 +1,201 @@
1
+ /**
2
+ * G1: Custom tool — irc.
3
+ *
4
+ * Registers a real `irc` tool in the Pi SDK session so that
5
+ * live-session workers can send messages to other live agents.
6
+ *
7
+ * Operations:
8
+ * - `list`: List currently visible peer agents
9
+ * - `send`: Send a message to a specific agent or broadcast to all
10
+ *
11
+ * Adapted from oh-my-pi's `IrcTool` pattern. Uses the live-agent-manager
12
+ * for routing messages between in-process workers.
13
+ */
14
+
15
+ import { defineTool, type ToolDefinition } from "@mariozechner/pi-coding-agent";
16
+ import { Type, type Static } from "@sinclair/typebox";
17
+ import { listLiveAgents, sendIrcMessage, broadcastIrcMessage } from "../live-agent-manager.ts";
18
+ import type { IrcMessage } from "../live-irc.ts";
19
+
20
+ const IrcParams = Type.Object({
21
+ op: Type.Union(
22
+ [
23
+ Type.Literal("send", { description: "Send a message to one peer or to all peers." }),
24
+ Type.Literal("list", { description: "List currently visible peers." }),
25
+ ],
26
+ { description: "IRC operation." },
27
+ ),
28
+ to: Type.Optional(
29
+ Type.String({
30
+ description: 'Recipient agent ID or "all" to broadcast.',
31
+ }),
32
+ ),
33
+ message: Type.Optional(
34
+ Type.String({
35
+ description: "Message body to deliver.",
36
+ }),
37
+ ),
38
+ awaitReply: Type.Optional(
39
+ Type.Boolean({
40
+ description: "Wait for a reply (default: true for DM, false for broadcast). Not yet supported — messages are fire-and-forget.",
41
+ }),
42
+ ),
43
+ });
44
+
45
+ type IrcParams = Static<typeof IrcParams>;
46
+
47
+ interface IrcDetails {
48
+ op: "send" | "list";
49
+ from?: string;
50
+ to?: string;
51
+ delivered?: string[];
52
+ notFound?: string[];
53
+ peers?: Array<{ id: string; status: string }>;
54
+ error?: string;
55
+ }
56
+
57
+ /**
58
+ * Create an `irc` tool definition for a specific agent.
59
+ *
60
+ * @param selfId — This agent's ID (runId:taskId format)
61
+ */
62
+ export function createIrcTool(
63
+ selfId: string,
64
+ ): ToolDefinition<typeof IrcParams, IrcDetails> {
65
+ return defineTool({
66
+ name: "irc",
67
+ label: "IRC",
68
+ description:
69
+ "Send messages to other live agents in same team. " +
70
+ 'Use `op: "list"` to see peers, `op: "send"` with `to` (agent ID or "all") and `message` to communicate.',
71
+ parameters: IrcParams,
72
+ promptSnippet: "Send messages to other live agents via the irc tool",
73
+ promptGuidelines: [
74
+ "Use irc to coordinate with other agents when you need information or want to share findings.",
75
+ 'Use `op: "list"` first to discover available peers.',
76
+ ],
77
+ async execute(
78
+ _toolCallId: string,
79
+ params: IrcParams,
80
+ _signal: AbortSignal | undefined,
81
+ _onUpdate: unknown,
82
+ _ctx: unknown,
83
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: IrcDetails }> {
84
+ if (params.op === "list") {
85
+ return executeList(selfId);
86
+ }
87
+ if (params.op === "send") {
88
+ return executeSend(selfId, params);
89
+ }
90
+ return {
91
+ content: [{ type: "text", text: "Unknown irc op." }],
92
+ details: { op: params.op, from: selfId, error: "Unknown operation." },
93
+ };
94
+ },
95
+ });
96
+ }
97
+
98
+ function executeList(selfId: string): { content: Array<{ type: "text"; text: string }>; details: IrcDetails } {
99
+ const agents = listLiveAgents();
100
+ const peers = agents
101
+ .filter((a) => a.agentId !== selfId && (a.status === "running" || a.status === "queued"))
102
+ .map((a) => ({ id: a.agentId, status: a.status }));
103
+
104
+ const lines: string[] = [];
105
+ if (peers.length === 0) {
106
+ lines.push("No other live agents.");
107
+ } else {
108
+ lines.push(`${peers.length} peer(s):`);
109
+ for (const peer of peers) {
110
+ lines.push(`- ${peer.id} (${peer.status})`);
111
+ }
112
+ }
113
+
114
+ return {
115
+ content: [{ type: "text", text: lines.join("\n") }],
116
+ details: { op: "list", from: selfId, peers },
117
+ };
118
+ }
119
+
120
+ function executeSend(
121
+ selfId: string,
122
+ params: IrcParams,
123
+ ): { content: Array<{ type: "text"; text: string }>; details: IrcDetails } {
124
+ const to = params.to?.trim();
125
+ const message = params.message?.trim();
126
+
127
+ if (!to) {
128
+ return {
129
+ content: [{ type: "text", text: '`to` is required for op="send".' }],
130
+ details: { op: "send", from: selfId, error: "Missing 'to' field." },
131
+ };
132
+ }
133
+ if (!message) {
134
+ return {
135
+ content: [{ type: "text", text: '`message` is required for op="send".' }],
136
+ details: { op: "send", from: selfId, to, error: "Missing 'message' field." },
137
+ };
138
+ }
139
+ if (to === selfId) {
140
+ return {
141
+ content: [{ type: "text", text: "Cannot send a message to yourself." }],
142
+ details: { op: "send", from: selfId, to, error: "Self-message not allowed." },
143
+ };
144
+ }
145
+
146
+ const ircMessage: IrcMessage = {
147
+ from: selfId,
148
+ to,
149
+ content: message,
150
+ timestamp: new Date().toISOString(),
151
+ awaitReply: params.awaitReply,
152
+ };
153
+
154
+ const notFound: string[] = [];
155
+ const delivered: string[] = [];
156
+
157
+ try {
158
+ if (to === "all") {
159
+ const recipients = broadcastIrcMessage(selfId, ircMessage);
160
+ delivered.push(...recipients);
161
+ } else {
162
+ // DM to specific agent
163
+ const agents = listLiveAgents();
164
+ const target = agents.find((a) => a.agentId === to);
165
+ if (!target || (target.status !== "running" && target.status !== "queued")) {
166
+ notFound.push(to);
167
+ } else {
168
+ try {
169
+ sendIrcMessage(to, ircMessage);
170
+ delivered.push(to);
171
+ } catch {
172
+ notFound.push(to);
173
+ }
174
+ }
175
+ }
176
+ } catch {
177
+ // Agent deregistered during routing — treat as not found
178
+ notFound.push(to);
179
+ }
180
+
181
+ const lines: string[] = [];
182
+ if (delivered.length > 0) {
183
+ lines.push(`Delivered to ${delivered.length} peer(s): ${delivered.join(", ")}`);
184
+ } else {
185
+ lines.push("No recipients received the message.");
186
+ }
187
+ if (notFound.length > 0) {
188
+ lines.push(`Unknown / unavailable peers: ${notFound.join(", ")}`);
189
+ }
190
+
191
+ return {
192
+ content: [{ type: "text", text: lines.join("\n") }],
193
+ details: {
194
+ op: "send",
195
+ from: selfId,
196
+ to,
197
+ delivered: delivered.length > 0 ? delivered : undefined,
198
+ notFound: notFound.length > 0 ? notFound : undefined,
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * G1: Custom tool — submit_result.
3
+ *
4
+ * Registers a real `submit_result` tool in the Pi SDK session so that
5
+ * live-session workers can yield their result by calling a tool (instead of
6
+ * relying solely on prompt-based reminders).
7
+ *
8
+ * Adapted from oh-my-pi's `YieldTool` pattern. Uses Pi SDK's `defineTool()`
9
+ * and TypeBox schemas for validation.
10
+ */
11
+
12
+ import { defineTool, type ToolDefinition } from "@mariozechner/pi-coding-agent";
13
+ import { Type, type Static } from "@sinclair/typebox";
14
+ import type { YieldResult } from "../yield-handler.ts";
15
+
16
+ const SubmitResultParams = Type.Object({
17
+ summary: Type.String({ description: "Summary of completed work." }),
18
+ artifacts: Type.Optional(
19
+ Type.Record(Type.String(), Type.String(), {
20
+ description: "Key-value map of artifact labels to file paths or content.",
21
+ }),
22
+ ),
23
+ structuredData: Type.Optional(
24
+ Type.Record(Type.String(), Type.Unknown(), {
25
+ description: "Structured key-value data to pass back to the orchestrator.",
26
+ }),
27
+ ),
28
+ });
29
+
30
+ type SubmitResultParams = Static<typeof SubmitResultParams>;
31
+
32
+ interface SubmitResultDetails {
33
+ summary: string;
34
+ artifacts?: Record<string, string>;
35
+ structuredData?: Record<string, unknown>;
36
+ }
37
+
38
+ /**
39
+ * Create a `submit_result` tool definition that calls `onYield` when invoked.
40
+ *
41
+ * The tool is injected into the session via `createAgentSession({ customTools: [...] })`.
42
+ * When the model calls it, the result is captured via the `onYield` callback
43
+ * and the yield enforcement loop terminates.
44
+ */
45
+ export function createSubmitResultTool(
46
+ onYield: (result: YieldResult) => void,
47
+ ): ToolDefinition<typeof SubmitResultParams, SubmitResultDetails> {
48
+ return defineTool({
49
+ name: "submit_result",
50
+ label: "Submit Result",
51
+ description:
52
+ "Submit final task result. Call when task complete. " +
53
+ "Provide summary, optional artifacts (file paths/content), optional structured data.",
54
+ parameters: SubmitResultParams,
55
+ promptSnippet: "Submit your task result when done using submit_result",
56
+ promptGuidelines: [
57
+ "Always call submit_result when your task is complete, even if you were unable to finish.",
58
+ "Include a clear summary of what was accomplished.",
59
+ ],
60
+ async execute(
61
+ toolCallId: string,
62
+ params: SubmitResultParams,
63
+ _signal: AbortSignal | undefined,
64
+ _onUpdate: unknown,
65
+ _ctx: unknown,
66
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SubmitResultDetails }> {
67
+ const result: YieldResult = {
68
+ summary: params.summary,
69
+ toolCallId,
70
+ ...(params.artifacts ? { artifacts: params.artifacts } : {}),
71
+ ...(params.structuredData ? { structuredData: params.structuredData } : {}),
72
+ };
73
+ // Build response first so the model always gets confirmation
74
+ const response: { content: Array<{ type: "text"; text: string }>; details: SubmitResultDetails } = {
75
+ content: [{ type: "text", text: "Result submitted successfully. Thank you." }],
76
+ details: {
77
+ summary: params.summary,
78
+ artifacts: params.artifacts,
79
+ structuredData: params.structuredData,
80
+ },
81
+ };
82
+ try {
83
+ onYield(result);
84
+ } catch {
85
+ // Yield handler failure should not prevent tool response
86
+ }
87
+ return response;
88
+ },
89
+ });
90
+ }
@@ -12,6 +12,7 @@ export interface DeadletterEntry {
12
12
  reason: DeadletterReason;
13
13
  attempts: number;
14
14
  lastError?: string;
15
+ attemptId?: string;
15
16
  timestamp: string;
16
17
  }
17
18
 
@@ -25,6 +25,7 @@ export class DeliveryCoordinator {
25
25
  private active = false;
26
26
  private generation = 0;
27
27
  private pending: PendingDelivery[] = [];
28
+ private flushing = false;
28
29
  private readonly deps: DeliveryCoordinatorDeps;
29
30
  private ttlTimer: ReturnType<typeof setInterval> | undefined;
30
31
 
@@ -63,7 +64,7 @@ export class DeliveryCoordinator {
63
64
  logInternalError("delivery-coordinator.deliverResult", error, `runId=${runId}`);
64
65
  }
65
66
  }
66
- this.enqueue({ runId, payload: result, timestamp: Date.now(), type: "result" });
67
+ if (!this.flushing) this.enqueue({ runId, payload: result, timestamp: Date.now(), type: "result" });
67
68
  }
68
69
 
69
70
  deliverNotification(notification: NotificationDescriptor): void {
@@ -84,7 +85,7 @@ export class DeliveryCoordinator {
84
85
  }
85
86
  return;
86
87
  }
87
- this.enqueue({ runId: notification.runId ?? "", payload: notification, timestamp: Date.now(), type: "notification" });
88
+ if (!this.flushing) this.enqueue({ runId: notification.runId ?? "", payload: notification, timestamp: Date.now(), type: "notification" });
88
89
  }
89
90
 
90
91
  deliverSteer(runId: string, message: string): void {
@@ -96,36 +97,32 @@ export class DeliveryCoordinator {
96
97
  logInternalError("delivery-coordinator.deliverSteer", error, `runId=${runId}`);
97
98
  }
98
99
  }
99
- this.enqueue({ runId, payload: message, timestamp: Date.now(), type: "steer" });
100
+ if (!this.flushing) this.enqueue({ runId, payload: message, timestamp: Date.now(), type: "steer" });
100
101
  }
101
102
 
102
103
  flushQueuedResults(): void {
103
104
  if (!this.active || this.pending.length === 0) return;
105
+ // H7: Set flushing BEFORE splice to prevent re-entrancy
106
+ if (this.flushing) return;
107
+ this.flushing = true;
104
108
  const batch = this.pending.splice(0);
105
- for (const delivery of batch) {
106
- if (delivery.generation !== undefined && delivery.generation !== this.generation) {
107
- logInternalError("delivery-coordinator.flush.stale", undefined, `runId=${delivery.runId} type=${delivery.type}`);
108
- continue;
109
- }
110
- try {
111
- switch (delivery.type) {
112
- case "result":
113
- this.deliverResult(delivery.runId, delivery.payload);
114
- break;
115
- case "notification": {
116
- const notification = delivery.payload as NotificationDescriptor;
117
- this.deliverNotification(notification);
118
- break;
119
- }
120
- case "steer": {
121
- const message = typeof delivery.payload === "string" ? delivery.payload : String(delivery.payload);
122
- this.deliverSteer(delivery.runId, message);
123
- break;
124
- }
109
+ try {
110
+ const retryLater: PendingDelivery[] = [];
111
+ for (const delivery of batch) {
112
+ if (delivery.type === "steer" && delivery.generation !== undefined && delivery.generation !== this.generation) {
113
+ logInternalError("delivery-coordinator.flush.stale", undefined, `runId=${delivery.runId} type=${delivery.type}`);
114
+ continue;
115
+ }
116
+ try {
117
+ if (!this.deliverQueued(delivery)) retryLater.push({ ...delivery, generation: this.generation });
118
+ } catch (error) {
119
+ logInternalError("delivery-coordinator.flush", error, `runId=${delivery.runId} type=${delivery.type}`);
120
+ retryLater.push({ ...delivery, generation: this.generation });
125
121
  }
126
- } catch (error) {
127
- logInternalError("delivery-coordinator.flush", error, `runId=${delivery.runId} type=${delivery.type}`);
128
122
  }
123
+ this.pending.unshift(...retryLater);
124
+ } finally {
125
+ this.flushing = false;
129
126
  }
130
127
  }
131
128
 
@@ -138,6 +135,32 @@ export class DeliveryCoordinator {
138
135
  }
139
136
  }
140
137
 
138
+ private deliverQueued(delivery: PendingDelivery): boolean {
139
+ switch (delivery.type) {
140
+ case "result":
141
+ if (!this.deps.emit) return false;
142
+ this.deps.emit("pi-crew:run-result", delivery.payload);
143
+ return true;
144
+ case "notification": {
145
+ const notification = delivery.payload as NotificationDescriptor;
146
+ if (!this.deps.sendFollowUp) return false;
147
+ this.deps.sendFollowUp(notification.title, notification.body ?? "");
148
+ try {
149
+ this.deps.emit?.("pi-crew:notification", notification);
150
+ } catch {
151
+ // Secondary event delivery must not consume the user-facing notification.
152
+ }
153
+ return true;
154
+ }
155
+ case "steer": {
156
+ if (!this.deps.sendWakeUp) return false;
157
+ const message = typeof delivery.payload === "string" ? delivery.payload : String(delivery.payload);
158
+ this.deps.sendWakeUp(message);
159
+ return true;
160
+ }
161
+ }
162
+ }
163
+
141
164
  private enqueue(delivery: PendingDelivery): void {
142
165
  this.pending.push({ ...delivery, generation: this.generation });
143
166
  }
@@ -0,0 +1,81 @@
1
+ import { permissionForRole } from "./role-permission.ts";
2
+ import type { CrewRuntimeConfig } from "../config/config.ts";
3
+ import type { PolicyDecision, TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
+
5
+ export type EffectivenessGuardMode = "off" | "warn" | "block" | "fail";
6
+ export type WorkerExecutionState = "enabled" | "disabled/scaffold";
7
+ export type RunEffectivenessSeverity = "ok" | "warning" | "blocked" | "failed";
8
+
9
+ export interface RunEffectivenessSummary {
10
+ completed: number;
11
+ observable: number;
12
+ noObservedWorkTaskIds: string[];
13
+ needsAttentionTaskIds: string[];
14
+ workerExecution: WorkerExecutionState;
15
+ guardMode: EffectivenessGuardMode;
16
+ severity: RunEffectivenessSeverity;
17
+ }
18
+
19
+ export function taskHasObservableWorkerActivity(task: TeamTaskState): boolean {
20
+ return Boolean(
21
+ (task.agentProgress?.toolCount ?? 0) > 0
22
+ || task.usage
23
+ || task.transcriptArtifact
24
+ || task.modelAttempts?.some((attempt) => attempt.success)
25
+ || task.jsonEvents,
26
+ );
27
+ }
28
+
29
+ export function resolveEffectivenessGuardMode(runtimeConfig: CrewRuntimeConfig | undefined, manifest?: TeamRunManifest): EffectivenessGuardMode {
30
+ const configured = runtimeConfig?.effectivenessGuard;
31
+ if (configured === "off" || configured === "warn" || configured === "block" || configured === "fail") return configured;
32
+ if (manifest?.runtimeResolution?.safety === "explicit_dry_run") return "off";
33
+ return "warn";
34
+ }
35
+
36
+ export function evaluateRunEffectiveness(input: { manifest?: TeamRunManifest; tasks: TeamTaskState[]; executeWorkers: boolean; runtimeConfig?: CrewRuntimeConfig }): RunEffectivenessSummary {
37
+ const completedTasks = input.tasks.filter((task) => task.status === "completed");
38
+ const noObservedWorkTasks = completedTasks.filter((task) => !taskHasObservableWorkerActivity(task));
39
+ const needsAttentionTasks = input.tasks.filter((task) => task.agentProgress?.activityState === "needs_attention");
40
+ const workerExecution: WorkerExecutionState = input.executeWorkers ? "enabled" : "disabled/scaffold";
41
+ const guardMode = resolveEffectivenessGuardMode(input.runtimeConfig, input.manifest);
42
+ const observable = Math.max(0, completedTasks.length - noObservedWorkTasks.length - needsAttentionTasks.length);
43
+ let severity: RunEffectivenessSeverity = "ok";
44
+ if (input.executeWorkers && guardMode !== "off" && noObservedWorkTasks.length > 0) {
45
+ severity = guardMode === "fail" ? "failed" : guardMode === "block" ? "blocked" : "warning";
46
+ // P0.1: default warn escalates to blocked for mutating-role tasks without observed work
47
+ if (severity === "warning" && noObservedWorkTasks.some((task) => permissionForRole(task.role) !== "read_only")) {
48
+ severity = "blocked";
49
+ }
50
+ }
51
+ return {
52
+ completed: completedTasks.length,
53
+ observable,
54
+ noObservedWorkTaskIds: noObservedWorkTasks.map((task) => task.id),
55
+ needsAttentionTaskIds: needsAttentionTasks.map((task) => task.id),
56
+ workerExecution,
57
+ guardMode,
58
+ severity,
59
+ };
60
+ }
61
+
62
+ export function formatRunEffectivenessLines(summary: RunEffectivenessSummary): string[] {
63
+ return [
64
+ `Score: ${summary.observable}/${Math.max(1, summary.completed)} completed task(s) with observable worker activity`,
65
+ `Worker execution: ${summary.workerExecution}`,
66
+ `Guard: ${summary.guardMode} severity=${summary.severity}`,
67
+ `No observable worker activity: ${summary.noObservedWorkTaskIds.length ? summary.noObservedWorkTaskIds.join(", ") : "none"}`,
68
+ `Needs attention: ${summary.needsAttentionTaskIds.length ? summary.needsAttentionTaskIds.join(", ") : "none"}`,
69
+ ];
70
+ }
71
+
72
+ export function effectivenessPolicyDecision(summary: RunEffectivenessSummary): PolicyDecision | undefined {
73
+ if (summary.severity !== "warning" && summary.severity !== "blocked" && summary.severity !== "failed") return undefined;
74
+ const action = summary.severity === "failed" ? "fail" : summary.severity === "blocked" ? "block" : "notify";
75
+ return {
76
+ action,
77
+ reason: "ineffective_worker",
78
+ message: `Run effectiveness guard ${summary.guardMode}: no observable worker activity for ${summary.noObservedWorkTaskIds.join(", ")}.`,
79
+ createdAt: new Date().toISOString(),
80
+ };
81
+ }
@@ -0,0 +1,90 @@
1
+ import { runEventBus } from "../ui/run-event-bus.ts";
2
+ import { subprocessToolRegistry, type SubprocessToolEvent } from "./subprocess-tool-registry.ts";
3
+
4
+ export interface StreamBridgeEvent {
5
+ runId: string;
6
+ taskId: string;
7
+ eventType: string;
8
+ toolName?: string;
9
+ toolArgs?: string;
10
+ intent?: string;
11
+ tokens?: number;
12
+ timestamp: number;
13
+ extractedToolData?: Record<string, unknown>;
14
+ }
15
+
16
+ const activeBridges = new Map<string, (event: StreamBridgeEvent) => void>();
17
+
18
+ export function registerStreamBridge(runId: string): { handler: (event: StreamBridgeEvent) => void; dispose: () => void } {
19
+ const existing = activeBridges.get(runId);
20
+ if (existing) {
21
+ return { handler: existing, dispose: () => unregisterStreamBridge(runId) };
22
+ }
23
+
24
+ const handler = (event: StreamBridgeEvent) => {
25
+ runEventBus.emit({
26
+ type: "worker_status",
27
+ runId: event.runId,
28
+ taskId: event.taskId,
29
+ data: event,
30
+ });
31
+ };
32
+
33
+ activeBridges.set(runId, handler);
34
+ return { handler, dispose: () => unregisterStreamBridge(runId) };
35
+ }
36
+
37
+ export function unregisterStreamBridge(runId: string): void {
38
+ activeBridges.delete(runId);
39
+ }
40
+
41
+ export function bridgeEventFromJsonEvent(runId: string, taskId: string, event: unknown): StreamBridgeEvent | null {
42
+ if (!event || typeof event !== "object") return null;
43
+ const record = event as Record<string, unknown>;
44
+ const type = typeof record.type === "string" ? record.type : "";
45
+
46
+ const result: StreamBridgeEvent = {
47
+ runId,
48
+ taskId,
49
+ eventType: type,
50
+ timestamp: Date.now(),
51
+ };
52
+
53
+ if (typeof record.toolName === "string") result.toolName = record.toolName;
54
+ if (record.args && typeof record.args === "object") {
55
+ try {
56
+ result.toolArgs = JSON.stringify(record.args).slice(0, 200);
57
+ } catch {
58
+ /* skip */
59
+ }
60
+ }
61
+ if (typeof record.intent === "string") result.intent = record.intent;
62
+
63
+ // Extract tokens from usage/message_end events
64
+ const usage = record.usage ?? (record.message as Record<string, unknown> | undefined)?.usage;
65
+ if (usage && typeof usage === "object") {
66
+ const u = usage as Record<string, unknown>;
67
+ const input = typeof u.input === "number" ? u.input : 0;
68
+ const output = typeof u.output === "number" ? u.output : 0;
69
+ if (input || output) result.tokens = input + output;
70
+ }
71
+
72
+ // Attach extracted tool data via subprocess tool registry
73
+ if (result.toolName && subprocessToolRegistry.hasHandler(result.toolName)) {
74
+ const handler = subprocessToolRegistry.getHandler(result.toolName);
75
+ if (handler?.extractData) {
76
+ const extracted = handler.extractData({
77
+ toolName: result.toolName,
78
+ toolCallId: (record.toolCallId as string) ?? "",
79
+ args: record.args as Record<string, unknown> | undefined,
80
+ result: record.result as SubprocessToolEvent["result"],
81
+ isError: record.isError as boolean | undefined,
82
+ });
83
+ if (extracted !== undefined) {
84
+ result.extractedToolData = { [result.toolName]: extracted };
85
+ }
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import type { TeamRunManifest } from "../state/types.ts";
4
4
  import { agentStateFile, ensureAgentStateDir } from "./crew-agent-records.ts";
5
5
 
6
- export type LiveAgentControlOperation = "steer" | "stop" | "resume";
6
+ export type LiveAgentControlOperation = "steer" | "follow-up" | "stop" | "resume";
7
7
 
8
8
  export interface LiveAgentControlRequest {
9
9
  id: string;
@@ -75,6 +75,7 @@ export async function applyLiveAgentControlRequest(input: { request: LiveAgentCo
75
75
  if (request.agentId && request.agentId !== agentId && request.agentId !== taskId) return false;
76
76
  seenRequestIds?.add(request.id);
77
77
  if (request.operation === "steer") await session.steer?.(request.message ?? "Please report current status and wrap up if possible.");
78
+ else if (request.operation === "follow-up") await session.prompt?.(request.message ?? "Please continue with the follow-up request.", { source: "api", expandPromptTemplates: false });
78
79
  else if (request.operation === "resume") await session.prompt?.(request.message ?? "Please resume and report final status.", { source: "api", expandPromptTemplates: false });
79
80
  else if (request.operation === "stop") await session.abort?.();
80
81
  return true;