pi-crew 0.1.41 → 0.1.44

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 (191) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +51 -0
  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/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-extension-examples.md +297 -297
  18. package/docs/research-extension-system.md +324 -324
  19. package/docs/research-optimization-plan.md +548 -548
  20. package/docs/research-phase10-distillation.md +199 -0
  21. package/docs/research-phase11-distillation.md +201 -0
  22. package/docs/research-pi-coding-agent.md +357 -357
  23. package/docs/research-source-pi-crew-reference.md +174 -174
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/src/agents/agent-serializer.ts +34 -34
  29. package/src/agents/discover-agents.ts +5 -4
  30. package/src/config/config.ts +28 -4
  31. package/src/extension/cross-extension-rpc.ts +82 -82
  32. package/src/extension/management.ts +37 -8
  33. package/src/extension/notification-router.ts +2 -2
  34. package/src/extension/register.ts +130 -8
  35. package/src/extension/registration/commands.ts +11 -9
  36. package/src/extension/registration/compaction-guard.ts +125 -125
  37. package/src/extension/registration/subagent-tools.ts +28 -19
  38. package/src/extension/registration/team-tool.ts +2 -1
  39. package/src/extension/result-watcher.ts +4 -4
  40. package/src/extension/run-bundle-schema.ts +8 -4
  41. package/src/extension/run-import.ts +4 -0
  42. package/src/extension/run-index.ts +23 -1
  43. package/src/extension/run-maintenance.ts +43 -24
  44. package/src/extension/team-tool/api.ts +2 -2
  45. package/src/extension/team-tool/cancel.ts +76 -4
  46. package/src/extension/team-tool/context.ts +1 -0
  47. package/src/extension/team-tool/doctor.ts +8 -1
  48. package/src/extension/team-tool/handle-settings.ts +188 -0
  49. package/src/extension/team-tool/inspect.ts +41 -41
  50. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  51. package/src/extension/team-tool/plan.ts +19 -19
  52. package/src/extension/team-tool/respond.ts +67 -0
  53. package/src/extension/team-tool/run.ts +6 -4
  54. package/src/extension/team-tool/status.ts +99 -93
  55. package/src/extension/team-tool-types.ts +4 -0
  56. package/src/extension/team-tool.ts +5 -1
  57. package/src/i18n.ts +184 -0
  58. package/src/observability/correlation.ts +2 -2
  59. package/src/observability/event-to-metric.ts +10 -3
  60. package/src/observability/exporters/adapter.ts +7 -1
  61. package/src/observability/exporters/otlp-exporter.ts +14 -2
  62. package/src/observability/exporters/prometheus-exporter.ts +9 -2
  63. package/src/observability/metric-registry.ts +18 -3
  64. package/src/observability/metric-retention.ts +11 -3
  65. package/src/observability/metric-sink.ts +9 -4
  66. package/src/observability/metrics-primitives.ts +4 -3
  67. package/src/prompt/prompt-runtime.ts +72 -68
  68. package/src/runtime/agent-control.ts +63 -63
  69. package/src/runtime/agent-memory.ts +72 -72
  70. package/src/runtime/agent-observability.ts +114 -114
  71. package/src/runtime/async-marker.ts +26 -26
  72. package/src/runtime/attention-events.ts +28 -23
  73. package/src/runtime/background-runner.ts +53 -53
  74. package/src/runtime/child-pi.ts +4 -4
  75. package/src/runtime/completion-guard.ts +95 -4
  76. package/src/runtime/concurrency.ts +1 -1
  77. package/src/runtime/crash-recovery.ts +32 -1
  78. package/src/runtime/crew-agent-runtime.ts +59 -58
  79. package/src/runtime/deadletter.ts +14 -4
  80. package/src/runtime/delivery-coordinator.ts +143 -0
  81. package/src/runtime/direct-run.ts +35 -35
  82. package/src/runtime/foreground-control.ts +82 -82
  83. package/src/runtime/green-contract.ts +46 -46
  84. package/src/runtime/group-join.ts +106 -106
  85. package/src/runtime/heartbeat-gradient.ts +28 -28
  86. package/src/runtime/heartbeat-watcher.ts +48 -4
  87. package/src/runtime/live-agent-control.ts +87 -87
  88. package/src/runtime/live-agent-manager.ts +85 -85
  89. package/src/runtime/live-control-realtime.ts +36 -36
  90. package/src/runtime/live-session-runtime.ts +305 -305
  91. package/src/runtime/manifest-cache.ts +2 -2
  92. package/src/runtime/model-fallback.ts +272 -261
  93. package/src/runtime/overflow-recovery.ts +157 -0
  94. package/src/runtime/parallel-research.ts +44 -44
  95. package/src/runtime/parallel-utils.ts +1 -1
  96. package/src/runtime/pi-json-output.ts +111 -111
  97. package/src/runtime/policy-engine.ts +79 -78
  98. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  99. package/src/runtime/process-status.ts +56 -56
  100. package/src/runtime/progress-event-coalescer.ts +43 -43
  101. package/src/runtime/recovery-recipes.ts +74 -74
  102. package/src/runtime/retry-executor.ts +5 -0
  103. package/src/runtime/role-permission.ts +39 -39
  104. package/src/runtime/runtime-resolver.ts +1 -1
  105. package/src/runtime/session-resources.ts +25 -0
  106. package/src/runtime/session-snapshot.ts +59 -0
  107. package/src/runtime/session-usage.ts +79 -79
  108. package/src/runtime/sidechain-output.ts +29 -29
  109. package/src/runtime/stale-reconciler.ts +179 -0
  110. package/src/runtime/subagent-manager.ts +3 -3
  111. package/src/runtime/supervisor-contact.ts +59 -0
  112. package/src/runtime/task-display.ts +38 -38
  113. package/src/runtime/task-output-context.ts +127 -127
  114. package/src/runtime/task-runner/live-executor.ts +101 -101
  115. package/src/runtime/task-runner/progress.ts +119 -111
  116. package/src/runtime/task-runner/result-utils.ts +14 -14
  117. package/src/runtime/task-runner/state-helpers.ts +22 -22
  118. package/src/runtime/task-runner.ts +14 -0
  119. package/src/runtime/team-runner.ts +9 -10
  120. package/src/runtime/worker-heartbeat.ts +21 -21
  121. package/src/runtime/worker-startup.ts +57 -57
  122. package/src/schema/config-schema.ts +2 -1
  123. package/src/schema/team-tool-schema.ts +115 -109
  124. package/src/state/artifact-store.ts +4 -2
  125. package/src/state/atomic-write.ts +12 -4
  126. package/src/state/contracts.ts +109 -105
  127. package/src/state/event-log.ts +3 -4
  128. package/src/state/jsonl-writer.ts +4 -1
  129. package/src/state/locks.ts +9 -1
  130. package/src/state/task-claims.ts +44 -42
  131. package/src/state/usage.ts +29 -29
  132. package/src/subagents/async-entry.ts +1 -1
  133. package/src/subagents/index.ts +3 -3
  134. package/src/subagents/live/control.ts +1 -1
  135. package/src/subagents/live/manager.ts +1 -1
  136. package/src/subagents/live/realtime.ts +1 -1
  137. package/src/subagents/live/session-runtime.ts +1 -1
  138. package/src/subagents/manager.ts +1 -1
  139. package/src/subagents/spawn.ts +1 -1
  140. package/src/teams/discover-teams.ts +2 -2
  141. package/src/teams/team-serializer.ts +38 -38
  142. package/src/types/diff.d.ts +18 -18
  143. package/src/ui/crew-footer.ts +101 -101
  144. package/src/ui/crew-select-list.ts +111 -111
  145. package/src/ui/crew-widget.ts +5 -4
  146. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  147. package/src/ui/dynamic-border.ts +25 -25
  148. package/src/ui/layout-primitives.ts +106 -106
  149. package/src/ui/live-run-sidebar.ts +1 -1
  150. package/src/ui/loaders.ts +158 -158
  151. package/src/ui/mascot.ts +3 -2
  152. package/src/ui/powerbar-publisher.ts +7 -6
  153. package/src/ui/render-diff.ts +119 -119
  154. package/src/ui/render-scheduler.ts +54 -14
  155. package/src/ui/run-dashboard.ts +39 -11
  156. package/src/ui/run-snapshot-cache.ts +336 -36
  157. package/src/ui/spinner.ts +17 -17
  158. package/src/ui/status-colors.ts +58 -54
  159. package/src/ui/syntax-highlight.ts +116 -116
  160. package/src/ui/theme-adapter.ts +1 -1
  161. package/src/ui/transcript-viewer.ts +7 -2
  162. package/src/utils/atomic-write.ts +33 -0
  163. package/src/utils/completion-dedupe.ts +63 -63
  164. package/src/utils/file-coalescer.ts +5 -3
  165. package/src/utils/frontmatter.ts +68 -36
  166. package/src/utils/git.ts +262 -262
  167. package/src/utils/ids.ts +12 -12
  168. package/src/utils/internal-error.ts +1 -1
  169. package/src/utils/names.ts +27 -26
  170. package/src/utils/paths.ts +1 -1
  171. package/src/utils/redaction.ts +44 -41
  172. package/src/utils/safe-paths.ts +47 -34
  173. package/src/utils/sleep.ts +2 -2
  174. package/src/utils/timings.ts +2 -0
  175. package/src/utils/visual.ts +9 -1
  176. package/src/workflows/discover-workflows.ts +4 -1
  177. package/src/workflows/validate-workflow.ts +40 -40
  178. package/src/worktree/branch-freshness.ts +45 -45
  179. package/src/worktree/worktree-manager.ts +6 -1
  180. package/teams/default.team.md +12 -12
  181. package/teams/fast-fix.team.md +11 -11
  182. package/teams/implementation.team.md +18 -18
  183. package/teams/parallel-research.team.md +14 -14
  184. package/teams/research.team.md +11 -11
  185. package/teams/review.team.md +12 -12
  186. package/workflows/default.workflow.md +29 -29
  187. package/workflows/fast-fix.workflow.md +22 -22
  188. package/workflows/implementation.workflow.md +38 -38
  189. package/workflows/parallel-research.workflow.md +46 -46
  190. package/workflows/research.workflow.md +22 -22
  191. package/workflows/review.workflow.md +30 -30
@@ -80,6 +80,7 @@ export interface AgentOverrideConfig {
80
80
  fallbackModels?: string[] | false;
81
81
  thinking?: string | false;
82
82
  tools?: string[] | false;
83
+ skills?: string[] | false;
83
84
  }
84
85
 
85
86
  export interface CrewAgentsConfig {
@@ -189,6 +190,14 @@ export function projectConfigPath(cwd: string): string {
189
190
  return path.join(projectCrewRoot(cwd), "config.json");
190
191
  }
191
192
 
193
+ /**
194
+ * Alternative project config path: `.pi/pi-crew.json` in the project root.
195
+ * This is a convenience path alongside the standard `config.json` in crewRoot.
196
+ */
197
+ export function projectPiCrewJsonPath(cwd: string): string {
198
+ return path.join(cwd, ".pi", "pi-crew.json");
199
+ }
200
+
192
201
  function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
193
202
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
194
203
  }
@@ -331,7 +340,7 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
331
340
  ...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
332
341
  overrides: {
333
342
  ...(base.agents?.overrides ?? {}),
334
- ...(override.agents?.overrides ?? {}),
343
+ ...withoutUndefined((override.agents?.overrides ?? {}) as Record<string, unknown>) as Record<string, AgentOverrideConfig>,
335
344
  },
336
345
  };
337
346
  }
@@ -421,7 +430,7 @@ function parseStringList(value: unknown): string[] | undefined {
421
430
 
422
431
  function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
423
432
  if (value === false) return false;
424
- if (typeof value === "string") return parseStringList(value.split(","));
433
+ if (typeof value === "string") return value.trim() === "" ? [] : parseStringList(value.split(","));
425
434
  return parseStringList(value);
426
435
  }
427
436
 
@@ -536,6 +545,7 @@ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
536
545
  fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
537
546
  thinking: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.thinking),
538
547
  tools: parseStringArrayOrFalse(obj.tools),
548
+ skills: parseStringArrayOrFalse(obj.skills),
539
549
  };
540
550
  return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
541
551
  }
@@ -649,9 +659,14 @@ function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefin
649
659
  function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
650
660
  const obj = asRecord(value);
651
661
  if (!obj) return undefined;
652
- const headers: Record<string, string> = {};
662
+ const headers: Record<string, string> = Object.create(null);
653
663
  const rawHeaders = asRecord(obj.headers);
654
- if (rawHeaders) for (const [key, entry] of Object.entries(rawHeaders)) if (typeof entry === "string") headers[key] = entry;
664
+ if (rawHeaders) for (const [key, entry] of Object.entries(rawHeaders)) {
665
+ if (typeof entry !== "string") continue;
666
+ // Prevent prototype pollution via __proto__ / constructor / prototype keys.
667
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
668
+ headers[key] = entry;
669
+ }
655
670
  const otlp: CrewOtlpConfig = {
656
671
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
657
672
  endpoint: parseWithSchema(Type.String({ minLength: 1 }), obj.endpoint),
@@ -726,6 +741,15 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
726
741
  const projectSafeConfig = sanitizeProjectConfig(projectPath, config, projectConfig.config);
727
742
  warnings.push(...projectConfig.warnings.map((warning) => `${projectPath}: ${warning}`), ...projectSafeConfig.warnings);
728
743
  config = mergeConfig(config, projectSafeConfig.config);
744
+ // Also load .pi/pi-crew.json from project root if it exists
745
+ const piCrewJsonPath = projectPiCrewJsonPath(cwd);
746
+ if (fs.existsSync(piCrewJsonPath)) {
747
+ const piCrewJsonConfig = parseConfigWithWarnings(readConfigRecord(piCrewJsonPath));
748
+ const piCrewJsonSafeConfig = sanitizeProjectConfig(piCrewJsonPath, config, piCrewJsonConfig.config);
749
+ warnings.push(...piCrewJsonConfig.warnings.map((warning) => `${piCrewJsonPath}: ${warning}`), ...piCrewJsonSafeConfig.warnings);
750
+ config = mergeConfig(config, piCrewJsonSafeConfig.config);
751
+ paths.push(piCrewJsonPath);
752
+ }
729
753
  }
730
754
  return { path: filePath, paths, config, warnings: warnings.length > 0 ? warnings : undefined };
731
755
  } catch (error) {
@@ -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
+ }
@@ -41,7 +41,11 @@ function extensionFor(resource: "agent" | "team" | "workflow"): string {
41
41
  }
42
42
 
43
43
  function backupFile(filePath: string): string {
44
- const backupPath = `${filePath}.bak-${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}`;
44
+ // Include milliseconds and a short random suffix to prevent collision
45
+ // when multiple backups happen within the same second.
46
+ const ts = new Date().toISOString().replace(/[-:.TZ]/g, "");
47
+ const random = Math.random().toString(36).slice(2, 6);
48
+ const backupPath = `${filePath}.bak-${ts.slice(0, 17)}-${random}`;
45
49
  fs.copyFileSync(filePath, backupPath);
46
50
  return backupPath;
47
51
  }
@@ -83,7 +87,7 @@ function parseRoles(value: unknown): { roles?: TeamRole[]; error?: string } {
83
87
  agent: sanitizeName(agent.value!),
84
88
  description: typeof obj.description === "string" ? obj.description.trim() : undefined,
85
89
  model: typeof obj.model === "string" ? obj.model.trim() : undefined,
86
- maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) ? obj.maxConcurrency : undefined,
90
+ maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) && obj.maxConcurrency > 0 ? obj.maxConcurrency : undefined,
87
91
  });
88
92
  }
89
93
  return { roles };
@@ -131,6 +135,8 @@ function findResource(ctx: ManagementContext, resource: "agent" | "team" | "work
131
135
  return allWorkflows(discoverWorkflows(ctx.cwd)).filter(sourceMatches);
132
136
  }
133
137
 
138
+ // Note: only checks agent→team references and defaultWorkflow. Does not detect
139
+ // workflow-step→agent/team references or team name in workflow metadata.
134
140
  function findReferences(ctx: ManagementContext, resource: "agent" | "team" | "workflow", name: string): string[] {
135
141
  const refs: string[] = [];
136
142
  if (resource === "agent") {
@@ -231,7 +237,7 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
231
237
  roles: parsedRoles.roles!,
232
238
  defaultWorkflow: typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined,
233
239
  workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : "single",
234
- maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : undefined,
240
+ maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) && cfg.maxConcurrency > 0 ? cfg.maxConcurrency : undefined,
235
241
  routing: parseRouting(cfg),
236
242
  });
237
243
  } else {
@@ -248,7 +254,11 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
248
254
  }
249
255
 
250
256
  if (params.dryRun) return result(`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`);
251
- fs.writeFileSync(filePath, content, "utf-8");
257
+ try {
258
+ fs.writeFileSync(filePath, content, "utf-8");
259
+ } catch (writeError) {
260
+ return result(`Failed to create ${params.resource}: ${writeError instanceof Error ? writeError.message : String(writeError)}`, "error", true);
261
+ }
252
262
  return result(`Created ${params.resource} '${name}' at ${filePath}.`);
253
263
  }
254
264
 
@@ -301,7 +311,7 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
301
311
  roles,
302
312
  defaultWorkflow: hasOwn(cfg, "defaultWorkflow") ? (typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined) : team.defaultWorkflow,
303
313
  workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : cfg.workspaceMode === "single" ? "single" : team.workspaceMode,
304
- maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : team.maxConcurrency,
314
+ maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) && cfg.maxConcurrency > 0 ? cfg.maxConcurrency : team.maxConcurrency,
305
315
  routing: parseRouting(cfg, team.routing),
306
316
  });
307
317
  } else {
@@ -327,8 +337,23 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
327
337
  return result([`[dry-run] Would update ${params.resource} at ${current.filePath}:`, "", content, ...(referenceUpdates.length ? ["", "Would update references in:", ...referenceUpdates.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
328
338
  }
329
339
  const backupPath = backupFile(current.filePath);
330
- if (nextPath !== current.filePath) fs.renameSync(current.filePath, nextPath);
331
- fs.writeFileSync(nextPath, content, "utf-8");
340
+ try {
341
+ if (nextPath !== current.filePath) {
342
+ try {
343
+ fs.renameSync(current.filePath, nextPath);
344
+ } catch (renameError) {
345
+ if ((renameError as NodeJS.ErrnoException).code === "EXDEV") {
346
+ fs.copyFileSync(current.filePath, nextPath);
347
+ fs.unlinkSync(current.filePath);
348
+ } else {
349
+ throw renameError;
350
+ }
351
+ }
352
+ }
353
+ fs.writeFileSync(nextPath, content, "utf-8");
354
+ } catch (updateError) {
355
+ return result(`Failed to update ${params.resource}: ${updateError instanceof Error ? updateError.message : String(updateError)}`, "error", true);
356
+ }
332
357
  const updatedRefs = params.updateReferences ? updateReferencesForRename(ctx, params.resource!, current.name, nextName, source, false) : [];
333
358
  return result([`Updated ${params.resource} at ${nextPath}. Backup: ${backupPath}.`, ...(updatedRefs.length ? ["Updated references:", ...updatedRefs.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
334
359
  }
@@ -343,6 +368,10 @@ export function handleDelete(params: TeamToolParamsValue, ctx: ManagementContext
343
368
  }
344
369
  if (params.dryRun) return result(`[dry-run] Would delete ${params.resource} at ${resolved.resource!.filePath}.${refs.length ? `\nReferences:\n${refs.map((ref) => `- ${ref}`).join("\n")}` : ""}`);
345
370
  const backupPath = backupFile(resolved.resource!.filePath);
346
- fs.unlinkSync(resolved.resource!.filePath);
371
+ try {
372
+ fs.unlinkSync(resolved.resource!.filePath);
373
+ } catch (deleteError) {
374
+ return result(`Failed to delete ${params.resource}: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, "error", true);
375
+ }
347
376
  return result(`Deleted ${params.resource} at ${resolved.resource!.filePath}. Backup: ${backupPath}.`);
348
377
  }
@@ -66,8 +66,8 @@ export class NotificationRouter {
66
66
  const withTime = { ...notification, timestamp: notification.timestamp ?? now };
67
67
  try {
68
68
  this.opts.sink?.(withTime);
69
- } catch {
70
- // Notification delivery must never crash the extension.
69
+ } catch (sinkError) {
70
+ process.stderr.write(`[pi-crew] notification-sink: ${sinkError instanceof Error ? sinkError.message : String(sinkError)}\n`);
71
71
  }
72
72
  const filter = this.opts.severityFilter ?? DEFAULT_SEVERITY_FILTER;
73
73
  if (!filter.includes(withTime.severity)) return false;
@@ -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,10 +37,15 @@ 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 { initI18n } from "../i18n.ts";
37
44
 
38
45
  export { __test__subagentSpawnParams };
39
46
 
40
47
  export function registerPiTeams(pi: ExtensionAPI): void {
48
+ const disposeI18n = initI18n(pi);
41
49
  resetTimings();
42
50
  time("register:start");
43
51
  const globalStore = globalThis as Record<string, unknown>;
@@ -81,6 +89,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
81
89
  let metricSink: MetricSink | undefined;
82
90
  let heartbeatWatcher: HeartbeatWatcher | undefined;
83
91
  let otlpExporter: OTLPExporter | undefined;
92
+ let deliveryCoordinator: DeliveryCoordinator | undefined;
93
+ let overflowTracker: OverflowRecoveryTracker | undefined;
84
94
  const configureNotifications = (ctx: ExtensionContext): void => {
85
95
  notificationRouter?.dispose();
86
96
  notificationSink?.dispose();
@@ -146,6 +156,28 @@ export function registerPiTeams(pi: ExtensionAPI): void {
146
156
  }
147
157
  };
148
158
  const autoRecoveryLast = new Map<string, number>();
159
+ const configureDeliveryCoordinator = (): void => {
160
+ deliveryCoordinator?.dispose();
161
+ deliveryCoordinator = undefined;
162
+ overflowTracker?.dispose();
163
+ overflowTracker = undefined;
164
+ deliveryCoordinator = new DeliveryCoordinator({
165
+ emit: (event, data) => { pi.events?.emit?.(event, data); },
166
+ sendFollowUp: (title, body) => { sendFollowUp(pi, [title, body].filter((line): line is string => Boolean(line)).join("\n")); },
167
+ sendWakeUp: (message) => { sendAgentWakeUp(pi, message); },
168
+ });
169
+ overflowTracker = new OverflowRecoveryTracker({
170
+ onPhaseChange: (state, previousPhase) => {
171
+ if (metricRegistry) {
172
+ metricRegistry.counter("crew.task.overflow_recovery_total", "Overflow recovery phase transitions").inc({ phase: state.phase, previous_phase: previousPhase });
173
+ }
174
+ pi.events?.emit?.("crew.task.overflow", { runId: state.runId, taskId: state.taskId, phase: state.phase, previousPhase });
175
+ },
176
+ onTimeout: (state) => {
177
+ 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.` });
178
+ },
179
+ });
180
+ };
149
181
  const notifyOperator = (notification: NotificationDescriptor): void => {
150
182
  try {
151
183
  notificationRouter?.enqueue(notification);
@@ -205,6 +237,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
205
237
  const foregroundControllers = new Set<AbortController>();
206
238
  let liveSidebarRunId: string | undefined;
207
239
  let renderScheduler: RenderScheduler | undefined;
240
+ let preloadTimer: ReturnType<typeof setTimeout> | undefined;
208
241
  const stopSessionBoundSubagents = (): void => {
209
242
  for (const controller of foregroundControllers) controller.abort();
210
243
  foregroundControllers.clear();
@@ -312,6 +345,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
312
345
  const cleanupRuntime = (): void => {
313
346
  if (cleanedUp) return;
314
347
  cleanedUp = true;
348
+ if (preloadTimer) { clearTimeout(preloadTimer); preloadTimer = undefined; }
315
349
  stopSessionBoundSubagents();
316
350
  stopAsyncRunNotifier(notifierState);
317
351
  stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
@@ -326,6 +360,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
326
360
  eventMetricSub = undefined;
327
361
  otlpExporter = undefined;
328
362
  metricRegistry = undefined;
363
+ deliveryCoordinator?.dispose();
364
+ overflowTracker?.dispose();
365
+ deliveryCoordinator = undefined;
366
+ overflowTracker = undefined;
329
367
  manifestCache.dispose();
330
368
  runSnapshotCache.dispose?.();
331
369
  renderScheduler?.dispose();
@@ -337,6 +375,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
337
375
  notificationSink = undefined;
338
376
  rpcHandle?.unsubscribe();
339
377
  rpcHandle = undefined;
378
+ disposeI18n();
340
379
  sessionGeneration += 1;
341
380
  currentCtx = undefined;
342
381
  if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
@@ -357,16 +396,68 @@ export function registerPiTeams(pi: ExtensionAPI): void {
357
396
  autoRecoveryLast.clear();
358
397
  configureNotifications(ctx);
359
398
  configureObservability(ctx);
399
+ configureDeliveryCoordinator();
400
+ const sessionId = (ctx as unknown as Record<string, unknown>).sessionId;
401
+ if (typeof sessionId === "string" && sessionId) deliveryCoordinator?.activate(sessionId);
402
+ tryRegisterSessionCleanup(pi, () => { terminateActiveChildPiProcesses(); cleanupRuntime(); });
360
403
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
361
404
  startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, { generation: ownerGeneration, isCurrent: (generation) => generation === sessionGeneration && currentCtx === ctx && !cleanedUp });
362
405
  const cache = getManifestCache(ctx.cwd);
363
406
  updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd));
364
407
  updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd), ctx, widgetState.notificationCount ?? 0);
365
408
  renderScheduler?.dispose();
409
+ // Phase 12: Async preloading — renderTick reads only a pre-computed frame
410
+ // from memory (zero fs I/O). Background preload refreshes the frame async.
411
+ let preloading = false;
412
+
413
+ let lastPreloadedConfig: ReturnType<typeof loadConfig> | undefined;
414
+ let lastPreloadedManifests: TeamRunManifest[] = [];
415
+ let lastFrameManifestCache: ReturnType<typeof createManifestCache> | undefined;
416
+ let lastFrameSnapshotCache: ReturnType<typeof createRunSnapshotCache> | undefined;
417
+
418
+ const buildFrame = async (): Promise<boolean> => {
419
+ if (!currentCtx) return false;
420
+ lastPreloadedConfig = loadConfig(currentCtx.cwd);
421
+ lastFrameManifestCache = getManifestCache(currentCtx.cwd);
422
+ lastFrameSnapshotCache = getRunSnapshotCache(currentCtx.cwd);
423
+ const manifests = lastFrameManifestCache.list(20);
424
+ lastPreloadedManifests = manifests;
425
+ const runIds = manifests.map((r) => r.runId);
426
+ await lastFrameSnapshotCache.preloadAllStale(runIds);
427
+ return true;
428
+ };
429
+
430
+ const backgroundPreload = (): void => {
431
+ if (!currentCtx || preloading) return;
432
+ preloading = true;
433
+ buildFrame()
434
+ .then((ok) => {
435
+ preloading = false;
436
+ if (ok) renderScheduler?.schedule();
437
+ })
438
+ .catch((error: unknown) => {
439
+ preloading = false;
440
+ logInternalError("register.backgroundPreload", error);
441
+ });
442
+ };
443
+
444
+ const startPreloadLoop = (intervalMs: number): void => {
445
+ if (preloadTimer) clearTimeout(preloadTimer);
446
+ const tick = (): void => {
447
+ backgroundPreload();
448
+ preloadTimer = setTimeout(tick, intervalMs);
449
+ preloadTimer.unref();
450
+ };
451
+ preloadTimer = setTimeout(tick, intervalMs);
452
+ preloadTimer.unref();
453
+ };
454
+
366
455
  const renderTick = (): void => {
367
456
  if (!currentCtx) return;
368
- const config = loadConfig(currentCtx.cwd).config.ui;
369
- const activeCache = getManifestCache(currentCtx.cwd);
457
+ const config = lastPreloadedConfig?.config.ui;
458
+ const activeCache = lastFrameManifestCache ?? getManifestCache(currentCtx.cwd);
459
+ const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
460
+ const manifests = lastPreloadedManifests.length > 0 ? lastPreloadedManifests : activeCache.list(20);
370
461
  if (liveSidebarRunId) {
371
462
  const placement = config?.widgetPlacement ?? "aboveEditor";
372
463
  if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
@@ -379,13 +470,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
379
470
  }
380
471
  requestRender(currentCtx);
381
472
  } else {
382
- updateCrewWidget(currentCtx, widgetState, config, activeCache, getRunSnapshotCache(currentCtx.cwd));
473
+ updateCrewWidget(currentCtx, widgetState, config, activeCache, snapshotCache, manifests);
383
474
  }
384
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
475
+ updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, snapshotCache, currentCtx, widgetState.notificationCount ?? 0, manifests);
476
+ // Health notifications: only warn about genuinely running runs
385
477
  const now = Date.now();
386
- for (const run of activeCache.list(20)) {
478
+ for (const run of manifests) {
479
+ if (run.status !== "running") continue;
387
480
  try {
388
- const snapshot = getRunSnapshotCache(currentCtx.cwd).refreshIfStale(run.runId);
481
+ const snapshot = snapshotCache.get(run.runId);
482
+ if (!snapshot) continue;
483
+ // Skip if snapshot shows run already completed/failed (stale cache)
484
+ if (snapshot.manifest.status !== "running") continue;
389
485
  const summary = summarizeHeartbeats(snapshot, { now });
390
486
  const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
391
487
  if (count <= 0) return;
@@ -402,18 +498,40 @@ export function registerPiTeams(pi: ExtensionAPI): void {
402
498
  }
403
499
  }
404
500
  };
501
+
502
+ const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250;
405
503
  renderScheduler = new RenderScheduler(pi.events, renderTick, {
406
- fallbackMs: loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250,
504
+ fallbackMs,
407
505
  onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
408
506
  });
507
+ // Start async preload loop — refreshes snapshot cache in background
508
+ startPreloadLoop(fallbackMs);
409
509
  });
410
510
  pi.on("session_before_switch", () => {
411
511
  sessionGeneration++;
512
+ // Phase 11b: Capture state before session switch
513
+ const pendingCount = deliveryCoordinator?.getPendingCount() ?? 0;
514
+ if (pendingCount > 0) {
515
+ logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
516
+ }
517
+ deliveryCoordinator?.deactivate();
412
518
  stopAsyncRunNotifier(notifierState);
413
519
  stopSessionBoundSubagents();
414
520
  });
415
521
  pi.on("session_shutdown", () => cleanupRuntime());
416
522
 
523
+ // Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
524
+ try {
525
+ pi.on("resources_discover", () => {
526
+ const skillDir = path.resolve(process.cwd(), "skills");
527
+ const extSkillDir = path.resolve(__dirname, "..", "..", "skills");
528
+ const paths: string[] = [];
529
+ if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
530
+ if (skillDir !== extSkillDir && fs.existsSync(skillDir)) paths.push(skillDir);
531
+ return paths.length > 0 ? { skillPaths: paths } : {};
532
+ });
533
+ } catch { /* older Pi without resources_discover */ }
534
+
417
535
  registerCompactionGuard(pi, { foregroundControllers });
418
536
 
419
537
  // Phase 1.4: Permission gate for destructive team actions.
@@ -432,7 +550,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
432
550
  };
433
551
  });
434
552
 
435
- registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState });
553
+ registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState, onJsonEvent: (taskId, runId, event) => {
554
+ const record = event as Record<string, unknown>;
555
+ const eventType = typeof record.type === "string" ? record.type : undefined;
556
+ if (eventType) overflowTracker?.feedEvent(taskId, runId, eventType);
557
+ } });
436
558
  registerSubagentTools(pi, subagentManager, { ownerSessionGeneration: captureSessionGeneration });
437
559
  time("register.tools");
438
560
 
@@ -74,6 +74,7 @@ async function handleMailboxDashboardAction(ctx: ExtensionCommandContext, runId:
74
74
  }
75
75
 
76
76
  function depsNotify(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void {
77
+ if (!ctx.hasUI) return;
77
78
  ctx.ui.notify(message, level);
78
79
  }
79
80
 
@@ -200,14 +201,15 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
200
201
  await notifyCommandResult(ctx, commandText(result));
201
202
  } });
202
203
 
203
- pi.registerCommand("team-cleanup", { description: "Clean up pi-crew worktrees for a run", handler: async (args: string, ctx: ExtensionCommandContext) => {
204
- const tokens = args.trim().split(/\s+/).filter(Boolean);
205
- const runId = tokens.find((token) => !token.startsWith("--"));
206
- const result = await handleTeamTool({ action: "cleanup", runId, force: tokens.includes("--force") }, ctx);
207
- await notifyCommandResult(ctx, commandText(result));
208
- } });
204
+ pi.registerCommand("team-settings", {
205
+ description: "View or update pi-crew settings: [list|get <key>|set <key> <value>|unset <key>|path|scope]",
206
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
207
+ const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, ctx);
208
+ await notifyCommandResult(ctx, commandText(result));
209
+ },
210
+ });
209
211
 
210
- pi.registerCommand("team-manager", { description: "Open a simple pi-crew interactive manager", handler: handleTeamManagerCommand });
212
+ pi.registerCommand("team-cleanup", { description: "Open a simple pi-crew interactive manager", handler: handleTeamManagerCommand });
211
213
 
212
214
  pi.registerCommand("team-result", { description: "Open a pi-crew agent result viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
213
215
  const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
@@ -215,8 +217,8 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
215
217
  const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
216
218
  if (ctx.hasUI && loaded) {
217
219
  const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
218
- const text = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected!.runId, config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
219
- await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected!.runId}:${agent?.taskId ?? "unknown"}`, text.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
220
+ const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
221
+ await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected?.runId ?? ""}:${agent?.taskId ?? "unknown"}`, resultText.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
220
222
  return;
221
223
  }
222
224
  const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);