pi-crew 0.1.43 → 0.1.45

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 (158) hide show
  1. package/CHANGELOG.md +27 -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/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +199 -0
  20. package/docs/research-phase11-distillation.md +201 -0
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/agents/discover-agents.ts +1 -0
  29. package/src/config/config.ts +19 -0
  30. package/src/extension/cross-extension-rpc.ts +82 -82
  31. package/src/extension/register.ts +134 -8
  32. package/src/extension/registration/commands.ts +18 -2
  33. package/src/extension/registration/compaction-guard.ts +125 -125
  34. package/src/extension/registration/subagent-tools.ts +148 -148
  35. package/src/extension/registration/team-tool.ts +27 -8
  36. package/src/extension/run-bundle-schema.ts +89 -89
  37. package/src/extension/run-index.ts +19 -0
  38. package/src/extension/run-maintenance.ts +43 -43
  39. package/src/extension/team-tool/api.ts +1 -1
  40. package/src/extension/team-tool/cancel.ts +79 -4
  41. package/src/extension/team-tool/context.ts +2 -0
  42. package/src/extension/team-tool/handle-settings.ts +188 -188
  43. package/src/extension/team-tool/inspect.ts +41 -41
  44. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  45. package/src/extension/team-tool/plan.ts +19 -19
  46. package/src/extension/team-tool/respond.ts +84 -0
  47. package/src/extension/team-tool/run.ts +3 -2
  48. package/src/extension/team-tool/status.ts +7 -1
  49. package/src/extension/team-tool-types.ts +4 -0
  50. package/src/extension/team-tool.ts +2 -0
  51. package/src/i18n.ts +184 -184
  52. package/src/observability/event-to-metric.ts +6 -0
  53. package/src/observability/exporters/otlp-exporter.ts +77 -77
  54. package/src/prompt/prompt-runtime.ts +72 -72
  55. package/src/runtime/agent-control.ts +63 -63
  56. package/src/runtime/agent-memory.ts +72 -72
  57. package/src/runtime/agent-observability.ts +114 -114
  58. package/src/runtime/async-marker.ts +26 -26
  59. package/src/runtime/attention-events.ts +28 -28
  60. package/src/runtime/background-runner.ts +53 -53
  61. package/src/runtime/child-pi.ts +444 -444
  62. package/src/runtime/completion-guard.ts +87 -0
  63. package/src/runtime/crash-recovery.ts +30 -0
  64. package/src/runtime/crew-agent-records.ts +8 -0
  65. package/src/runtime/crew-agent-runtime.ts +2 -1
  66. package/src/runtime/delivery-coordinator.ts +154 -0
  67. package/src/runtime/direct-run.ts +35 -35
  68. package/src/runtime/foreground-control.ts +82 -82
  69. package/src/runtime/green-contract.ts +46 -46
  70. package/src/runtime/group-join.ts +106 -106
  71. package/src/runtime/heartbeat-gradient.ts +28 -28
  72. package/src/runtime/heartbeat-watcher.ts +124 -124
  73. package/src/runtime/live-agent-control.ts +87 -87
  74. package/src/runtime/live-agent-manager.ts +85 -85
  75. package/src/runtime/live-control-realtime.ts +36 -36
  76. package/src/runtime/live-session-runtime.ts +305 -305
  77. package/src/runtime/model-fallback.ts +5 -2
  78. package/src/runtime/overflow-recovery.ts +176 -0
  79. package/src/runtime/parallel-research.ts +44 -44
  80. package/src/runtime/pi-json-output.ts +111 -111
  81. package/src/runtime/policy-engine.ts +79 -79
  82. package/src/runtime/process-status.ts +1 -1
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/retry-executor.ts +64 -64
  86. package/src/runtime/role-permission.ts +39 -39
  87. package/src/runtime/session-resources.ts +25 -0
  88. package/src/runtime/session-snapshot.ts +59 -0
  89. package/src/runtime/session-usage.ts +79 -79
  90. package/src/runtime/sidechain-output.ts +29 -29
  91. package/src/runtime/stale-reconciler.ts +199 -0
  92. package/src/runtime/supervisor-contact.ts +59 -0
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -127
  95. package/src/runtime/task-runner/live-executor.ts +101 -101
  96. package/src/runtime/task-runner/progress.ts +119 -119
  97. package/src/runtime/task-runner/result-utils.ts +14 -14
  98. package/src/runtime/task-runner/state-helpers.ts +22 -22
  99. package/src/runtime/task-runner.ts +14 -0
  100. package/src/runtime/team-runner.ts +19 -8
  101. package/src/runtime/worker-heartbeat.ts +21 -21
  102. package/src/runtime/worker-startup.ts +57 -57
  103. package/src/schema/config-schema.ts +1 -0
  104. package/src/schema/team-tool-schema.ts +6 -1
  105. package/src/state/contracts.ts +6 -2
  106. package/src/state/state-store.ts +43 -0
  107. package/src/state/task-claims.ts +44 -44
  108. package/src/state/types.ts +2 -0
  109. package/src/state/usage.ts +29 -29
  110. package/src/subagents/async-entry.ts +1 -1
  111. package/src/subagents/index.ts +3 -3
  112. package/src/subagents/live/control.ts +1 -1
  113. package/src/subagents/live/manager.ts +1 -1
  114. package/src/subagents/live/realtime.ts +1 -1
  115. package/src/subagents/live/session-runtime.ts +1 -1
  116. package/src/subagents/manager.ts +1 -1
  117. package/src/subagents/spawn.ts +1 -1
  118. package/src/teams/team-serializer.ts +38 -38
  119. package/src/types/diff.d.ts +18 -18
  120. package/src/ui/crew-footer.ts +101 -101
  121. package/src/ui/crew-select-list.ts +111 -111
  122. package/src/ui/crew-widget.ts +10 -5
  123. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  124. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  125. package/src/ui/dynamic-border.ts +25 -25
  126. package/src/ui/layout-primitives.ts +106 -106
  127. package/src/ui/loaders.ts +158 -158
  128. package/src/ui/powerbar-publisher.ts +4 -4
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/render-scheduler.ts +143 -143
  131. package/src/ui/run-snapshot-cache.ts +310 -17
  132. package/src/ui/snapshot-types.ts +5 -0
  133. package/src/ui/spinner.ts +17 -17
  134. package/src/ui/status-colors.ts +58 -54
  135. package/src/ui/syntax-highlight.ts +116 -116
  136. package/src/utils/atomic-write.ts +33 -0
  137. package/src/utils/completion-dedupe.ts +63 -63
  138. package/src/utils/frontmatter.ts +68 -68
  139. package/src/utils/git.ts +262 -262
  140. package/src/utils/ids.ts +12 -12
  141. package/src/utils/names.ts +27 -27
  142. package/src/utils/redaction.ts +44 -44
  143. package/src/utils/safe-paths.ts +47 -47
  144. package/src/utils/sleep.ts +32 -32
  145. package/src/workflows/validate-workflow.ts +40 -40
  146. package/src/worktree/branch-freshness.ts +45 -45
  147. package/teams/default.team.md +12 -12
  148. package/teams/fast-fix.team.md +11 -11
  149. package/teams/implementation.team.md +18 -18
  150. package/teams/parallel-research.team.md +14 -14
  151. package/teams/research.team.md +11 -11
  152. package/teams/review.team.md +12 -12
  153. package/workflows/default.workflow.md +29 -29
  154. package/workflows/fast-fix.workflow.md +22 -22
  155. package/workflows/implementation.workflow.md +38 -38
  156. package/workflows/parallel-research.workflow.md +46 -46
  157. package/workflows/research.workflow.md +22 -22
  158. package/workflows/review.workflow.md +30 -30
@@ -1,82 +1,82 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
- import { handleTeamTool } from "./team-tool.ts";
4
- import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
5
-
6
- export interface EventBusLike {
7
- on(event: string, handler: (data: unknown) => void): (() => void) | void;
8
- emit(event: string, data: unknown): void;
9
- }
10
-
11
- export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
12
- export const PI_CREW_RPC_VERSION = 1;
13
-
14
- export interface PiCrewRpcHandle {
15
- unsubscribe(): void;
16
- }
17
-
18
- function requestId(raw: unknown): string | undefined {
19
- return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
20
- }
21
-
22
- function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
23
- if (!id) return;
24
- events.emit(`${channel}:reply:${id}`, payload);
25
- }
26
-
27
- function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
28
- return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
29
- }
30
-
31
- function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
32
- const unsub = events.on(channel, handler);
33
- return typeof unsub === "function" ? unsub : () => {};
34
- }
35
-
36
- export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
37
- if (!events) return undefined;
38
- const unsubs = [
39
- on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
40
- on(events, "pi-crew:rpc:run", async (raw) => {
41
- const id = requestId(raw);
42
- try {
43
- const ctx = getCtx();
44
- if (!ctx) throw new Error("No active pi-crew session context.");
45
- const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
46
- const result = await handleTeamTool(params, ctx);
47
- reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
48
- } catch (error) {
49
- reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
50
- }
51
- }),
52
- on(events, "pi-crew:rpc:status", async (raw) => {
53
- const id = requestId(raw);
54
- try {
55
- const ctx = getCtx();
56
- if (!ctx) throw new Error("No active pi-crew session context.");
57
- const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
58
- const result = await handleTeamTool({ action: "status", runId }, ctx);
59
- reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
60
- } catch (error) {
61
- reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
62
- }
63
- }),
64
- on(events, "pi-crew:live-control", (raw) => {
65
- const request = parseLiveControlRealtimeMessage(raw);
66
- if (request) publishLiveControlRealtime(request);
67
- }),
68
- on(events, "pi-crew:rpc:live-control", async (raw) => {
69
- const id = requestId(raw);
70
- try {
71
- const ctx = getCtx();
72
- if (!ctx) throw new Error("No active pi-crew session context.");
73
- const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
74
- const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
75
- reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
76
- } catch (error) {
77
- reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
78
- }
79
- }),
80
- ];
81
- return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
82
- }
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
+ import { handleTeamTool } from "./team-tool.ts";
4
+ import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
5
+
6
+ export interface EventBusLike {
7
+ on(event: string, handler: (data: unknown) => void): (() => void) | void;
8
+ emit(event: string, data: unknown): void;
9
+ }
10
+
11
+ export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
12
+ export const PI_CREW_RPC_VERSION = 1;
13
+
14
+ export interface PiCrewRpcHandle {
15
+ unsubscribe(): void;
16
+ }
17
+
18
+ function requestId(raw: unknown): string | undefined {
19
+ return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
20
+ }
21
+
22
+ function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
23
+ if (!id) return;
24
+ events.emit(`${channel}:reply:${id}`, payload);
25
+ }
26
+
27
+ function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
28
+ return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
29
+ }
30
+
31
+ function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
32
+ const unsub = events.on(channel, handler);
33
+ return typeof unsub === "function" ? unsub : () => {};
34
+ }
35
+
36
+ export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
37
+ if (!events) return undefined;
38
+ const unsubs = [
39
+ on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
40
+ on(events, "pi-crew:rpc:run", async (raw) => {
41
+ const id = requestId(raw);
42
+ try {
43
+ const ctx = getCtx();
44
+ if (!ctx) throw new Error("No active pi-crew session context.");
45
+ const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
46
+ const result = await handleTeamTool(params, ctx);
47
+ reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
48
+ } catch (error) {
49
+ reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
50
+ }
51
+ }),
52
+ on(events, "pi-crew:rpc:status", async (raw) => {
53
+ const id = requestId(raw);
54
+ try {
55
+ const ctx = getCtx();
56
+ if (!ctx) throw new Error("No active pi-crew session context.");
57
+ const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
58
+ const result = await handleTeamTool({ action: "status", runId }, ctx);
59
+ reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
60
+ } catch (error) {
61
+ reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
62
+ }
63
+ }),
64
+ on(events, "pi-crew:live-control", (raw) => {
65
+ const request = parseLiveControlRealtimeMessage(raw);
66
+ if (request) publishLiveControlRealtime(request);
67
+ }),
68
+ on(events, "pi-crew:rpc:live-control", async (raw) => {
69
+ const id = requestId(raw);
70
+ try {
71
+ const ctx = getCtx();
72
+ if (!ctx) throw new Error("No active pi-crew session context.");
73
+ const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
74
+ const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
75
+ reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
76
+ } catch (error) {
77
+ reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
78
+ }
79
+ }),
80
+ ];
81
+ return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
82
+ }
@@ -1,4 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
2
4
  import { loadConfig } from "../config/config.ts";
3
5
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
4
6
  import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
@@ -8,6 +10,7 @@ import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.t
8
10
  import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
9
11
  import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
10
12
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
13
+ import type { TeamRunManifest } from "../state/types.ts";
11
14
  import { terminateActiveChildPiProcesses } from "../subagents/spawn.ts";
12
15
  import { SubagentManager } from "../subagents/manager.ts";
13
16
  import { __test__subagentSpawnParams, sendAgentWakeUp, sendFollowUp } from "./registration/subagent-helpers.ts";
@@ -34,6 +37,10 @@ import { OTLPExporter } from "../observability/exporters/otlp-exporter.ts";
34
37
  import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
35
38
  import { appendDeadletter } from "../runtime/deadletter.ts";
36
39
  import { detectInterruptedRuns } from "../runtime/crash-recovery.ts";
40
+ import { DeliveryCoordinator } from "../runtime/delivery-coordinator.ts";
41
+ import { OverflowRecoveryTracker } from "../runtime/overflow-recovery.ts";
42
+ import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
43
+ import { createSessionSnapshot } from "../runtime/session-snapshot.ts";
37
44
  import { initI18n } from "../i18n.ts";
38
45
 
39
46
  export { __test__subagentSpawnParams };
@@ -83,6 +90,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
83
90
  let metricSink: MetricSink | undefined;
84
91
  let heartbeatWatcher: HeartbeatWatcher | undefined;
85
92
  let otlpExporter: OTLPExporter | undefined;
93
+ let deliveryCoordinator: DeliveryCoordinator | undefined;
94
+ let overflowTracker: OverflowRecoveryTracker | undefined;
86
95
  const configureNotifications = (ctx: ExtensionContext): void => {
87
96
  notificationRouter?.dispose();
88
97
  notificationSink?.dispose();
@@ -148,6 +157,28 @@ export function registerPiTeams(pi: ExtensionAPI): void {
148
157
  }
149
158
  };
150
159
  const autoRecoveryLast = new Map<string, number>();
160
+ const configureDeliveryCoordinator = (): void => {
161
+ deliveryCoordinator?.dispose();
162
+ deliveryCoordinator = undefined;
163
+ overflowTracker?.dispose();
164
+ overflowTracker = undefined;
165
+ deliveryCoordinator = new DeliveryCoordinator({
166
+ emit: (event, data) => { pi.events?.emit?.(event, data); },
167
+ sendFollowUp: (title, body) => { sendFollowUp(pi, [title, body].filter((line): line is string => Boolean(line)).join("\n")); },
168
+ sendWakeUp: (message) => { sendAgentWakeUp(pi, message); },
169
+ });
170
+ overflowTracker = new OverflowRecoveryTracker({
171
+ onPhaseChange: (state, previousPhase) => {
172
+ if (metricRegistry) {
173
+ metricRegistry.counter("crew.task.overflow_recovery_total", "Overflow recovery phase transitions").inc({ phase: state.phase, previous_phase: previousPhase });
174
+ }
175
+ pi.events?.emit?.("crew.task.overflow", { runId: state.runId, taskId: state.taskId, phase: state.phase, previousPhase });
176
+ },
177
+ onTimeout: (state) => {
178
+ notifyOperator({ id: `overflow_timeout_${state.taskId}`, severity: "warning", source: "overflow-recovery", runId: state.runId, title: `Task ${state.taskId} overflow recovery timed out`, body: `Phase: ${state.phase}, compaction_count: ${state.compactionCount}, retry_count: ${state.retryCount}. The task may be stuck.` });
179
+ },
180
+ });
181
+ };
151
182
  const notifyOperator = (notification: NotificationDescriptor): void => {
152
183
  try {
153
184
  notificationRouter?.enqueue(notification);
@@ -207,6 +238,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
207
238
  const foregroundControllers = new Set<AbortController>();
208
239
  let liveSidebarRunId: string | undefined;
209
240
  let renderScheduler: RenderScheduler | undefined;
241
+ let preloadTimer: ReturnType<typeof setTimeout> | undefined;
210
242
  const stopSessionBoundSubagents = (): void => {
211
243
  for (const controller of foregroundControllers) controller.abort();
212
244
  foregroundControllers.clear();
@@ -314,6 +346,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
314
346
  const cleanupRuntime = (): void => {
315
347
  if (cleanedUp) return;
316
348
  cleanedUp = true;
349
+ if (preloadTimer) { clearTimeout(preloadTimer); preloadTimer = undefined; }
317
350
  stopSessionBoundSubagents();
318
351
  stopAsyncRunNotifier(notifierState);
319
352
  stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
@@ -328,6 +361,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
328
361
  eventMetricSub = undefined;
329
362
  otlpExporter = undefined;
330
363
  metricRegistry = undefined;
364
+ deliveryCoordinator?.dispose();
365
+ overflowTracker?.dispose();
366
+ deliveryCoordinator = undefined;
367
+ overflowTracker = undefined;
331
368
  manifestCache.dispose();
332
369
  runSnapshotCache.dispose?.();
333
370
  renderScheduler?.dispose();
@@ -360,16 +397,68 @@ export function registerPiTeams(pi: ExtensionAPI): void {
360
397
  autoRecoveryLast.clear();
361
398
  configureNotifications(ctx);
362
399
  configureObservability(ctx);
400
+ configureDeliveryCoordinator();
401
+ const sessionId = (ctx as unknown as Record<string, unknown>).sessionId;
402
+ if (typeof sessionId === "string" && sessionId) deliveryCoordinator?.activate(sessionId);
403
+ tryRegisterSessionCleanup(pi, () => { terminateActiveChildPiProcesses(); cleanupRuntime(); });
363
404
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
364
405
  startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, { generation: ownerGeneration, isCurrent: (generation) => generation === sessionGeneration && currentCtx === ctx && !cleanedUp });
365
406
  const cache = getManifestCache(ctx.cwd);
366
407
  updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd));
367
408
  updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd), ctx, widgetState.notificationCount ?? 0);
368
409
  renderScheduler?.dispose();
410
+ // Phase 12: Async preloading — renderTick reads only a pre-computed frame
411
+ // from memory (zero fs I/O). Background preload refreshes the frame async.
412
+ let preloading = false;
413
+
414
+ let lastPreloadedConfig: ReturnType<typeof loadConfig> | undefined;
415
+ let lastPreloadedManifests: TeamRunManifest[] = [];
416
+ let lastFrameManifestCache: ReturnType<typeof createManifestCache> | undefined;
417
+ let lastFrameSnapshotCache: ReturnType<typeof createRunSnapshotCache> | undefined;
418
+
419
+ const buildFrame = async (): Promise<boolean> => {
420
+ if (!currentCtx) return false;
421
+ lastPreloadedConfig = loadConfig(currentCtx.cwd);
422
+ lastFrameManifestCache = getManifestCache(currentCtx.cwd);
423
+ lastFrameSnapshotCache = getRunSnapshotCache(currentCtx.cwd);
424
+ const manifests = lastFrameManifestCache.list(20);
425
+ lastPreloadedManifests = manifests;
426
+ const runIds = manifests.map((r) => r.runId);
427
+ await lastFrameSnapshotCache.preloadAllStale(runIds);
428
+ return true;
429
+ };
430
+
431
+ const backgroundPreload = (): void => {
432
+ if (!currentCtx || preloading) return;
433
+ preloading = true;
434
+ buildFrame()
435
+ .then((ok) => {
436
+ preloading = false;
437
+ if (ok) renderScheduler?.schedule();
438
+ })
439
+ .catch((error: unknown) => {
440
+ preloading = false;
441
+ logInternalError("register.backgroundPreload", error);
442
+ });
443
+ };
444
+
445
+ const startPreloadLoop = (intervalMs: number): void => {
446
+ if (preloadTimer) clearTimeout(preloadTimer);
447
+ const tick = (): void => {
448
+ backgroundPreload();
449
+ preloadTimer = setTimeout(tick, intervalMs);
450
+ preloadTimer.unref();
451
+ };
452
+ preloadTimer = setTimeout(tick, intervalMs);
453
+ preloadTimer.unref();
454
+ };
455
+
369
456
  const renderTick = (): void => {
370
457
  if (!currentCtx) return;
371
- const config = loadConfig(currentCtx.cwd).config.ui;
372
- const activeCache = getManifestCache(currentCtx.cwd);
458
+ const config = lastPreloadedConfig?.config.ui;
459
+ const activeCache = lastFrameManifestCache ?? getManifestCache(currentCtx.cwd);
460
+ const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
461
+ const manifests = lastPreloadedManifests.length > 0 ? lastPreloadedManifests : activeCache.list(20);
373
462
  if (liveSidebarRunId) {
374
463
  const placement = config?.widgetPlacement ?? "aboveEditor";
375
464
  if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
@@ -382,13 +471,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
382
471
  }
383
472
  requestRender(currentCtx);
384
473
  } else {
385
- updateCrewWidget(currentCtx, widgetState, config, activeCache, getRunSnapshotCache(currentCtx.cwd));
474
+ updateCrewWidget(currentCtx, widgetState, config, activeCache, snapshotCache, manifests);
386
475
  }
387
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
476
+ updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, snapshotCache, currentCtx, widgetState.notificationCount ?? 0, manifests);
477
+ // Health notifications: only warn about genuinely running runs
388
478
  const now = Date.now();
389
- for (const run of activeCache.list(20)) {
479
+ for (const run of manifests) {
480
+ if (run.status !== "running") continue;
390
481
  try {
391
- const snapshot = getRunSnapshotCache(currentCtx.cwd).refreshIfStale(run.runId);
482
+ const snapshot = snapshotCache.get(run.runId);
483
+ if (!snapshot) continue;
484
+ // Skip if snapshot shows run already completed/failed (stale cache)
485
+ if (snapshot.manifest.status !== "running") continue;
392
486
  const summary = summarizeHeartbeats(snapshot, { now });
393
487
  const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
394
488
  if (count <= 0) return;
@@ -405,18 +499,46 @@ export function registerPiTeams(pi: ExtensionAPI): void {
405
499
  }
406
500
  }
407
501
  };
502
+
503
+ const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250;
408
504
  renderScheduler = new RenderScheduler(pi.events, renderTick, {
409
- fallbackMs: loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250,
505
+ fallbackMs,
410
506
  onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
411
507
  });
508
+ // Start async preload loop — refreshes snapshot cache in background
509
+ startPreloadLoop(fallbackMs);
412
510
  });
413
511
  pi.on("session_before_switch", () => {
414
512
  sessionGeneration++;
513
+ const pendingCount = deliveryCoordinator?.getPendingCount() ?? 0;
514
+ try {
515
+ const activeRuns = currentCtx ? getManifestCache(currentCtx.cwd).list(50).filter((run) => run.status === "running" || run.status === "queued" || run.status === "blocked") : [];
516
+ const snapshot = createSessionSnapshot(activeRuns, pendingCount, sessionGeneration);
517
+ if (pendingCount > 0 || snapshot.activeRunIds.length > 0) logInternalError("register.session-before-switch", undefined, JSON.stringify(snapshot));
518
+ } catch (error) {
519
+ logInternalError("register.session-before-switch.snapshot", error);
520
+ }
521
+ if (pendingCount > 0) {
522
+ logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
523
+ }
524
+ deliveryCoordinator?.deactivate();
415
525
  stopAsyncRunNotifier(notifierState);
416
526
  stopSessionBoundSubagents();
417
527
  });
418
528
  pi.on("session_shutdown", () => cleanupRuntime());
419
529
 
530
+ // Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
531
+ try {
532
+ pi.on("resources_discover", () => {
533
+ const skillDir = path.resolve(process.cwd(), "skills");
534
+ const extSkillDir = path.resolve(__dirname, "..", "..", "skills");
535
+ const paths: string[] = [];
536
+ if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
537
+ if (skillDir !== extSkillDir && fs.existsSync(skillDir)) paths.push(skillDir);
538
+ return paths.length > 0 ? { skillPaths: paths } : {};
539
+ });
540
+ } catch { /* older Pi without resources_discover */ }
541
+
420
542
  registerCompactionGuard(pi, { foregroundControllers });
421
543
 
422
544
  // Phase 1.4: Permission gate for destructive team actions.
@@ -435,7 +557,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
435
557
  };
436
558
  });
437
559
 
438
- registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState });
560
+ registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState, onJsonEvent: (taskId, runId, event) => {
561
+ const record = event as Record<string, unknown>;
562
+ const eventType = typeof record.type === "string" ? record.type : undefined;
563
+ if (eventType) overflowTracker?.feedEvent(taskId, runId, eventType);
564
+ } });
439
565
  registerSubagentTools(pi, subagentManager, { ownerSessionGeneration: captureSessionGeneration });
440
566
  time("register.tools");
441
567
 
@@ -151,12 +151,28 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
151
151
  } });
152
152
  }
153
153
 
154
+ pi.registerCommand("team-respond", {
155
+ description: "Respond to a waiting pi-crew task: <runId> <taskId|--all> <message>",
156
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
157
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
158
+ const runId = tokens.shift();
159
+ const taskToken = tokens[0] === "--all" ? tokens.shift() : tokens.shift();
160
+ const taskId = taskToken === "--all" ? undefined : taskToken;
161
+ const message = tokens.join(" ") || undefined;
162
+ const result = await handleTeamTool({ action: "respond", runId, taskId, message }, ctx);
163
+ await notifyCommandResult(ctx, commandText(result));
164
+ },
165
+ });
166
+
154
167
  pi.registerCommand("team-api", {
155
168
  description: "Run safe pi-crew API interop operations: <runId> <operation> [key=value]",
156
169
  handler: async (args: string, ctx: ExtensionCommandContext) => {
157
170
  const tokens = args.trim().split(/\s+/).filter(Boolean);
158
- const runId = tokens.find((token) => !token.includes("=") && !token.startsWith("--"));
159
- const operation = tokens.find((token) => token !== runId && !token.includes("=") && !token.startsWith("--")) ?? "read-manifest";
171
+ const positional = tokens.filter((token) => !token.includes("=") && !token.startsWith("--"));
172
+ const runIdLessOperations = new Set(["metrics-snapshot"]);
173
+ const first = positional[0];
174
+ const runId = first && runIdLessOperations.has(first) ? undefined : first;
175
+ const operation = runId ? (positional[1] ?? "read-manifest") : (first ?? "read-manifest");
160
176
  const config: Record<string, unknown> = { operation };
161
177
  for (const token of tokens.filter((item) => item.includes("="))) {
162
178
  const [key, ...rest] = token.split("=");