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
@@ -1,10 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { packageRoot, projectCrewRoot } from "../utils/paths.ts";
3
+ import { configPath as globalConfigPath } from "../config/config.ts";
4
+ import { DEFAULT_UI } from "../config/defaults.ts";
5
+ import { packageRoot, projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
4
6
 
5
7
  export interface ProjectInitOptions {
6
8
  copyBuiltins?: boolean;
7
9
  overwrite?: boolean;
10
+ configScope?: "global" | "project" | "none";
8
11
  }
9
12
 
10
13
  export interface ProjectInitResult {
@@ -13,6 +16,10 @@ export interface ProjectInitResult {
13
16
  skippedFiles: string[];
14
17
  gitignorePath: string;
15
18
  gitignoreUpdated: boolean;
19
+ configPath: string;
20
+ configScope: "global" | "project" | "none";
21
+ configCreated: boolean;
22
+ configSkipped: boolean;
16
23
  }
17
24
 
18
25
  function ensureDir(dir: string, createdDirs: string[]): void {
@@ -24,6 +31,44 @@ function ensureDir(dir: string, createdDirs: string[]): void {
24
31
  }
25
32
  }
26
33
 
34
+ const DEFAULT_PI_CREW_CONFIG = {
35
+ // Keep generated config non-invasive: do not set runtime/limits defaults here.
36
+ // Those are provided by pi-crew internals and should not make a normal workflow block.
37
+ autonomous: {
38
+ enabled: true,
39
+ injectPolicy: true,
40
+ preferAsyncForLongTasks: false,
41
+ allowWorktreeSuggestion: true,
42
+ },
43
+ agents: {
44
+ overrides: {
45
+ explorer: { model: false, thinking: "off" },
46
+ writer: { model: false, thinking: "off" },
47
+ planner: { model: false, thinking: "medium" },
48
+ analyst: { model: false, thinking: "off" },
49
+ critic: { model: false, thinking: "low" },
50
+ executor: { model: false, thinking: "medium" },
51
+ reviewer: { model: false, thinking: "off" },
52
+ "security-reviewer": { model: false, thinking: "medium" },
53
+ "test-engineer": { model: false, thinking: "low" },
54
+ verifier: { model: false, thinking: "off" },
55
+ },
56
+ },
57
+ ui: {
58
+ widgetPlacement: DEFAULT_UI.widgetPlacement,
59
+ widgetMaxLines: DEFAULT_UI.widgetMaxLines,
60
+ powerbar: DEFAULT_UI.powerbar,
61
+ dashboardPlacement: DEFAULT_UI.dashboardPlacement,
62
+ dashboardWidth: DEFAULT_UI.dashboardWidth,
63
+ dashboardLiveRefreshMs: DEFAULT_UI.dashboardLiveRefreshMs,
64
+ autoOpenDashboard: DEFAULT_UI.autoOpenDashboard,
65
+ autoOpenDashboardForForegroundRuns: DEFAULT_UI.autoOpenDashboardForForegroundRuns,
66
+ showModel: DEFAULT_UI.showModel,
67
+ showTokens: DEFAULT_UI.showTokens,
68
+ showTools: DEFAULT_UI.showTools,
69
+ },
70
+ };
71
+
27
72
  function copyBuiltinDir(kind: "agents" | "teams" | "workflows", targetDir: string, overwrite: boolean, copiedFiles: string[], skippedFiles: string[]): void {
28
73
  const sourceDir = path.join(packageRoot(), kind);
29
74
  if (!fs.existsSync(sourceDir)) return;
@@ -50,11 +95,26 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
50
95
  const agentsDir = path.join(crewRoot, "agents");
51
96
  const teamsDir = path.join(crewRoot, "teams");
52
97
  const workflowsDir = path.join(crewRoot, "workflows");
98
+ const configScope = options.configScope ?? "global";
99
+ const configPath = configScope === "project" ? path.join(projectPiRoot(cwd), "pi-crew.json") : configScope === "global" ? globalConfigPath() : "";
53
100
  ensureDir(agentsDir, createdDirs);
54
101
  ensureDir(teamsDir, createdDirs);
55
102
  ensureDir(workflowsDir, createdDirs);
56
103
  ensureDir(path.join(crewRoot, "imports"), createdDirs);
57
104
 
105
+ let configCreated = false;
106
+ let configSkipped = false;
107
+ if (configPath) {
108
+ if (configScope === "project") ensureDir(path.dirname(configPath), createdDirs);
109
+ else fs.mkdirSync(path.dirname(configPath), { recursive: true });
110
+ if (!fs.existsSync(configPath) || options.overwrite === true) {
111
+ fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_PI_CREW_CONFIG, null, 2)}\n`, "utf-8");
112
+ configCreated = true;
113
+ } else {
114
+ configSkipped = true;
115
+ }
116
+ }
117
+
58
118
  if (options.copyBuiltins) {
59
119
  copyBuiltinDir("agents", agentsDir, options.overwrite === true, copiedFiles, skippedFiles);
60
120
  copyBuiltinDir("teams", teamsDir, options.overwrite === true, copiedFiles, skippedFiles);
@@ -72,5 +132,5 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
72
132
  gitignoreUpdated = true;
73
133
  }
74
134
 
75
- return { createdDirs, copiedFiles, skippedFiles, gitignorePath, gitignoreUpdated };
135
+ return { createdDirs, copiedFiles, skippedFiles, gitignorePath, gitignoreUpdated, configPath, configScope, configCreated, configSkipped };
76
136
  }
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { loadConfig } from "../config/config.ts";
5
6
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
6
7
  import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
@@ -8,7 +9,7 @@ import { notifyActiveRuns } from "./session-summary.ts";
8
9
  import { LiveRunSidebar } from "../ui/live-run-sidebar.ts";
9
10
  import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
10
11
  import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
11
- import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
12
+ import { clearPiCrewPowerbar, disposePowerbarCoalescer, registerPiCrewPowerbarSegments, requestPowerbarUpdate, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
12
13
  import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
13
14
  import type { TeamRunManifest } from "../state/types.ts";
14
15
  import { terminateActiveChildPiProcesses } from "../subagents/spawn.ts";
@@ -36,7 +37,7 @@ import { createMetricFileSink, type MetricSink } from "../observability/metric-s
36
37
  import { OTLPExporter } from "../observability/exporters/otlp-exporter.ts";
37
38
  import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
38
39
  import { appendDeadletter } from "../runtime/deadletter.ts";
39
- import { detectInterruptedRuns } from "../runtime/crash-recovery.ts";
40
+ import { cancelOrphanedRuns, detectInterruptedRuns, purgeStaleActiveRunIndex } from "../runtime/crash-recovery.ts";
40
41
  import { DeliveryCoordinator } from "../runtime/delivery-coordinator.ts";
41
42
  import { OverflowRecoveryTracker } from "../runtime/overflow-recovery.ts";
42
43
  import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
@@ -112,7 +113,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
112
113
  if (currentCtx) {
113
114
  const uiConfig = loadConfig(currentCtx.cwd).config.ui;
114
115
  updateCrewWidget(currentCtx, widgetState, uiConfig, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd));
115
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, uiConfig, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
116
+ requestPowerbarUpdate(pi.events, currentCtx.cwd, uiConfig, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
116
117
  }
117
118
  });
118
119
  };
@@ -235,12 +236,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
235
236
  pi.events?.emit?.(event, payload);
236
237
  },
237
238
  );
238
- const foregroundControllers = new Set<AbortController>();
239
+ const foregroundControllers = new Map<string | symbol, AbortController>();
239
240
  let liveSidebarRunId: string | undefined;
240
241
  let renderScheduler: RenderScheduler | undefined;
241
242
  let preloadTimer: ReturnType<typeof setTimeout> | undefined;
242
243
  const stopSessionBoundSubagents = (): void => {
243
- for (const controller of foregroundControllers) controller.abort();
244
+ for (const controller of foregroundControllers.values()) controller.abort();
244
245
  foregroundControllers.clear();
245
246
  subagentManager.abortAll();
246
247
  terminateActiveChildPiProcesses();
@@ -253,18 +254,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
253
254
  const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
254
255
  const uiConfig = loadConfig(ctx.cwd).config.ui;
255
256
  const autoOpen = uiConfig?.autoOpenDashboard === true;
256
- const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns !== false;
257
- if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? "right") !== "right") return;
257
+ const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns ?? DEFAULT_UI.autoOpenDashboardForForegroundRuns;
258
+ if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) !== "right") return;
258
259
  if (liveSidebarRunId === runId) return;
259
260
  liveSidebarRunId = runId;
260
- const widgetPlacement = uiConfig?.widgetPlacement ?? "aboveEditor";
261
+ const widgetPlacement = uiConfig?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
261
262
  setExtensionWidget(ctx, "pi-crew", undefined, { placement: widgetPlacement });
262
263
  setExtensionWidget(ctx, "pi-crew-active", undefined, { placement: widgetPlacement });
263
264
  widgetState.lastVisibility = "hidden";
264
265
  widgetState.lastPlacement = widgetPlacement;
265
266
  widgetState.lastKey = "pi-crew-active";
266
267
  widgetState.model = undefined;
267
- const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
268
+ const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth));
268
269
  void showCustom<undefined>(ctx, (_tui, theme, _keybindings, done) => new LiveRunSidebar({ cwd: ctx.cwd, runId, done, theme, config: uiConfig, snapshotCache: getRunSnapshotCache(ctx.cwd) }), {
269
270
  overlay: true,
270
271
  overlayOptions: { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 }, visible: (termWidth: number) => termWidth >= 100 },
@@ -276,7 +277,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
276
277
  const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
277
278
  const ownerGeneration = captureSessionGeneration();
278
279
  const controller = new AbortController();
279
- foregroundControllers.add(controller);
280
+ const key = runId ?? Symbol();
281
+ foregroundControllers.set(key, controller);
280
282
  if (ctx.hasUI) {
281
283
  setWorkingIndicator(ctx, { frames: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], intervalMs: 80 });
282
284
  ctx.ui.setWorkingMessage(runId ? `pi-crew foreground run ${runId}...` : "pi-crew foreground run...");
@@ -294,9 +296,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
294
296
  }
295
297
  }
296
298
  if (isContextCurrent(ctx, ownerGeneration)) ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
299
+ else logInternalError("register.foreground-run-failure", error, `runId=${runId} context disposed`);
297
300
  })
298
301
  .finally(() => {
299
- foregroundControllers.delete(controller);
302
+ foregroundControllers.delete(key);
300
303
  const ownerCurrent = isContextCurrent(ctx, ownerGeneration);
301
304
  if (ownerCurrent && ctx.hasUI) {
302
305
  setWorkingIndicator(ctx);
@@ -333,7 +336,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
333
336
  if (ownerCurrent && currentCtx) {
334
337
  const config = loadConfig(currentCtx.cwd).config.ui;
335
338
  updateCrewWidget(currentCtx, widgetState, config, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd));
336
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
339
+ requestPowerbarUpdate(pi.events, currentCtx.cwd, config, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
337
340
  }
338
341
  });
339
342
  });
@@ -349,8 +352,20 @@ export function registerPiTeams(pi: ExtensionAPI): void {
349
352
  if (preloadTimer) { clearTimeout(preloadTimer); preloadTimer = undefined; }
350
353
  stopSessionBoundSubagents();
351
354
  stopAsyncRunNotifier(notifierState);
355
+
356
+ // P0: Purge all stale active-run-index entries on session cleanup.
357
+ // This handles: normal exit, SIGTERM, Ctrl+C — any case where cleanupRuntime fires.
358
+ // For SIGKILL / crash / SIGHUP (where cleanupRuntime does NOT fire),
359
+ // purgeStaleActiveRunIndex() runs at next session_start instead.
360
+ try {
361
+ purgeStaleActiveRunIndex();
362
+ } catch (error) {
363
+ logInternalError("register.cleanupRuntime.purgeStale", error);
364
+ }
365
+
352
366
  stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
353
367
  clearPiCrewPowerbar(pi.events, currentCtx);
368
+ disposePowerbarCoalescer();
354
369
  heartbeatWatcher?.dispose();
355
370
  metricSink?.dispose();
356
371
  eventMetricSub?.dispose();
@@ -393,12 +408,36 @@ export function registerPiTeams(pi: ExtensionAPI): void {
393
408
  if (widgetState.interval) clearInterval(widgetState.interval);
394
409
  widgetState.interval = undefined;
395
410
  notifyActiveRuns(ctx);
411
+
412
+ // Auto-cancel orphaned runs from dead sessions
413
+ const currentSessionId = (ctx as unknown as Record<string, unknown>).sessionId as string | undefined;
414
+ if (currentSessionId) {
415
+ try {
416
+ const { cancelled } = cancelOrphanedRuns(ctx.cwd, getManifestCache(ctx.cwd), currentSessionId);
417
+ if (cancelled.length > 0) {
418
+ notifyOperator({ id: `orphan_cleanup`, severity: "info", source: "crash-recovery", title: `Cleaned up ${cancelled.length} orphaned run(s)`, body: `Runs from previous sessions were auto-cancelled: ${cancelled.join(", ")}` });
419
+ }
420
+ } catch (error) {
421
+ logInternalError("register.sessionStart.orphanCleanup", error);
422
+ }
423
+ }
424
+
425
+ // Global purge of stale active-run-index entries (temp dirs, dead workers, etc.)
426
+ try {
427
+ const { purged } = purgeStaleActiveRunIndex();
428
+ if (purged.length > 0) {
429
+ notifyOperator({ id: `active_index_purge`, severity: "info", source: "crash-recovery", title: `Purged ${purged.length} stale active-run-index entr${purged.length === 1 ? "y" : "ies"}`, body: `Cleaned up global active run index` });
430
+ }
431
+ } catch (error) {
432
+ logInternalError("register.sessionStart.globalIndexPurge", error);
433
+ }
434
+
396
435
  const loadedConfig = loadConfig(ctx.cwd);
397
436
  autoRecoveryLast.clear();
398
437
  configureNotifications(ctx);
399
438
  configureObservability(ctx);
400
439
  configureDeliveryCoordinator();
401
- const sessionId = (ctx as unknown as Record<string, unknown>).sessionId;
440
+ const sessionId = ctx.sessionManager?.getSessionId?.() ?? (ctx as unknown as Record<string, unknown>).sessionId;
402
441
  if (typeof sessionId === "string" && sessionId) deliveryCoordinator?.activate(sessionId);
403
442
  tryRegisterSessionCleanup(pi, () => { terminateActiveChildPiProcesses(); cleanupRuntime(); });
404
443
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
@@ -460,7 +499,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
460
499
  const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
461
500
  const manifests = lastPreloadedManifests.length > 0 ? lastPreloadedManifests : activeCache.list(20);
462
501
  if (liveSidebarRunId) {
463
- const placement = config?.widgetPlacement ?? "aboveEditor";
502
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
464
503
  if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
465
504
  setExtensionWidget(currentCtx, "pi-crew", undefined, { placement });
466
505
  setExtensionWidget(currentCtx, "pi-crew-active", undefined, { placement });
@@ -473,7 +512,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
473
512
  } else {
474
513
  updateCrewWidget(currentCtx, widgetState, config, activeCache, snapshotCache, manifests);
475
514
  }
476
- updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, snapshotCache, currentCtx, widgetState.notificationCount ?? 0, manifests);
515
+ requestPowerbarUpdate(pi.events, currentCtx.cwd, config, activeCache, snapshotCache, currentCtx, widgetState.notificationCount ?? 0, manifests);
477
516
  // Health notifications: only warn about genuinely running runs
478
517
  const now = Date.now();
479
518
  for (const run of manifests) {
@@ -500,7 +539,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
500
539
  }
501
540
  };
502
541
 
503
- const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250;
542
+ const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? DEFAULT_UI.refreshMs;
504
543
  renderScheduler = new RenderScheduler(pi.events, renderTick, {
505
544
  fallbackMs,
506
545
  onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
@@ -530,8 +569,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
530
569
  // Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
531
570
  try {
532
571
  pi.on("resources_discover", () => {
533
- const skillDir = path.resolve(process.cwd(), "skills");
534
- const extSkillDir = path.resolve(__dirname, "..", "..", "skills");
572
+ const sessionCwd = currentCtx?.cwd ?? process.cwd();
573
+ const skillDir = path.resolve(sessionCwd, "skills");
574
+ const extSkillDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
535
575
  const paths: string[] = [];
536
576
  if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
537
577
  if (skillDir !== extSkillDir && fs.existsSync(skillDir)) paths.push(skillDir);
@@ -539,6 +579,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
539
579
  });
540
580
  } catch { /* older Pi without resources_discover */ }
541
581
 
582
+ const abortForegroundRun = (runId: string): boolean => {
583
+ const controller = foregroundControllers.get(runId);
584
+ if (!controller) return false;
585
+ controller.abort();
586
+ return true;
587
+ };
542
588
  registerCompactionGuard(pi, { foregroundControllers });
543
589
 
544
590
  // Phase 1.4: Permission gate for destructive team actions.
@@ -550,14 +596,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
550
596
  const action = typeof input.action === "string" ? input.action : undefined;
551
597
  const destructiveActions = new Set(["delete", "forget", "prune", "cleanup"]);
552
598
  if (!action || !destructiveActions.has(action)) return;
553
- if (input.confirm === true || input.force === true) return;
599
+ const forceBypassesReferenceChecks = action === "delete" && input.force === true;
600
+ if (input.confirm === true || forceBypassesReferenceChecks) return;
554
601
  return {
555
602
  block: true,
556
- reason: `Destructive action '${action}' requires confirm=true (or force=true to bypass reference checks).`,
603
+ reason: `Destructive action '${action}' requires confirm=true${action === "delete" ? " (or force=true to bypass reference checks)" : ""}.`,
557
604
  };
558
605
  });
559
606
 
560
- registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState, onJsonEvent: (taskId, runId, event) => {
607
+ registerTeamTool(pi, { foregroundControllers, startForegroundRun, abortForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState, onJsonEvent: (taskId, runId, event) => {
561
608
  const record = event as Record<string, unknown>;
562
609
  const eventType = typeof record.type === "string" ? record.type : undefined;
563
610
  if (eventType) overflowTracker?.feedEvent(taskId, runId, eventType);
@@ -565,7 +612,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
565
612
  registerSubagentTools(pi, subagentManager, { ownerSessionGeneration: captureSessionGeneration });
566
613
  time("register.tools");
567
614
 
568
- registerTeamCommands(pi, { startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, dismissNotifications: () => {
615
+ registerTeamCommands(pi, { startForegroundRun, abortForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, dismissNotifications: () => {
569
616
  widgetState.notificationCount = 0;
570
617
  if (currentCtx) {
571
618
  const uiConfig = loadConfig(currentCtx.cwd).config.ui;
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { loadConfig } from "../../config/config.ts";
3
3
  import { handleTeamTool } from "../team-tool.ts";
4
+ import { withSessionId } from "../team-tool/context.ts";
4
5
  import { piTeamsHelp } from "../help.ts";
5
6
  import { handleTeamManagerCommand } from "../team-manager-command.ts";
6
7
  import { loadRunManifestById } from "../../state/state-store.ts";
@@ -15,6 +16,7 @@ import { MailboxDetailOverlay, type MailboxAction } from "../../ui/overlays/mail
15
16
  import { MailboxComposeOverlay, type MailboxComposeResult } from "../../ui/overlays/mailbox-compose-overlay.ts";
16
17
  import { AgentPickerOverlay } from "../../ui/overlays/agent-picker-overlay.ts";
17
18
  import { dispatchDiagnosticExport, dispatchHealthRecovery, dispatchKillStaleWorkers, dispatchMailboxAck, dispatchMailboxAckAll, dispatchMailboxCompose, dispatchMailboxNudge } from "../../ui/run-action-dispatcher.ts";
19
+ import { DEFAULT_UI } from "../../config/defaults.ts";
18
20
  import { listRecentDiagnostic } from "../../runtime/diagnostic-export.ts";
19
21
  import { commandText, notifyCommandResult, parseRunArgs, parseScalar, pushUnset, setNestedConfig } from "./command-utils.ts";
20
22
  import { openTranscriptViewer, selectAgentTask } from "./viewers.ts";
@@ -25,6 +27,7 @@ import type { MetricRegistry } from "../../observability/metric-registry.ts";
25
27
 
26
28
  export interface RegisterTeamCommandsDeps {
27
29
  startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
30
+ abortForegroundRun: (runId: string) => boolean;
28
31
  openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
29
32
  getManifestCache: (cwd: string) => { list(max?: number): TeamRunManifest[] };
30
33
  getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
@@ -78,6 +81,10 @@ function depsNotify(ctx: ExtensionCommandContext, message: string, level: "info"
78
81
  ctx.ui.notify(message, level);
79
82
  }
80
83
 
84
+ function teamCommandContext(ctx: ExtensionCommandContext): ExtensionCommandContext & { sessionId?: string } {
85
+ return withSessionId(ctx);
86
+ }
87
+
81
88
  async function handleHealthDashboardAction(ctx: ExtensionCommandContext, selection: RunDashboardSelection): Promise<void> {
82
89
  const loaded = loadRunManifestById(ctx.cwd, selection.runId);
83
90
  if (!loaded) {
@@ -121,7 +128,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
121
128
  pi.registerCommand("teams", {
122
129
  description: "List pi-crew teams, workflows, and agents",
123
130
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
124
- const result = await handleTeamTool({ action: "list" }, ctx);
131
+ const result = await handleTeamTool({ action: "list" }, teamCommandContext(ctx));
125
132
  await notifyCommandResult(ctx, commandText(result));
126
133
  },
127
134
  });
@@ -129,7 +136,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
129
136
  pi.registerCommand("team-run", {
130
137
  description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
131
138
  handler: async (args: string, ctx: ExtensionCommandContext) => {
132
- const result = await handleTeamTool(parseRunArgs(args), { ...ctx, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx as ExtensionContext, runId) });
139
+ const result = await handleTeamTool(parseRunArgs(args), { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => deps.openLiveSidebar(ctx as ExtensionContext, runId) });
133
140
  await notifyCommandResult(ctx, commandText(result));
134
141
  },
135
142
  });
@@ -146,11 +153,26 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
146
153
  ] as const) {
147
154
  pi.registerCommand(name, { description, handler: async (args: string, ctx: ExtensionCommandContext) => {
148
155
  const runId = args.trim() || undefined;
149
- const result = await handleTeamTool({ action, runId }, ctx);
156
+ const result = await handleTeamTool({ action, runId }, teamCommandContext(ctx));
150
157
  await notifyCommandResult(ctx, commandText(result));
151
158
  } });
152
159
  }
153
160
 
161
+ pi.registerCommand("team-retry", {
162
+ description: "Retry failed/cancelled pi-crew tasks: <runId> [taskId]",
163
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
164
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
165
+ const runId = tokens.shift();
166
+ const taskId = tokens.shift();
167
+ if (!runId) {
168
+ await notifyCommandResult(ctx, "Usage: /team-retry <runId> [taskId]");
169
+ return;
170
+ }
171
+ const retryResult = await handleTeamTool({ action: "retry", runId, taskId }, teamCommandContext(ctx));
172
+ await notifyCommandResult(ctx, commandText(retryResult));
173
+ },
174
+ });
175
+
154
176
  pi.registerCommand("team-respond", {
155
177
  description: "Respond to a waiting pi-crew task: <runId> <taskId|--all> <message>",
156
178
  handler: async (args: string, ctx: ExtensionCommandContext) => {
@@ -159,7 +181,23 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
159
181
  const taskToken = tokens[0] === "--all" ? tokens.shift() : tokens.shift();
160
182
  const taskId = taskToken === "--all" ? undefined : taskToken;
161
183
  const message = tokens.join(" ") || undefined;
162
- const result = await handleTeamTool({ action: "respond", runId, taskId, message }, ctx);
184
+ const result = await handleTeamTool({ action: "respond", runId, taskId, message }, teamCommandContext(ctx));
185
+ await notifyCommandResult(ctx, commandText(result));
186
+ },
187
+ });
188
+
189
+ pi.registerCommand("team-follow-up", {
190
+ description: "Send a follow-up prompt to a pi-crew task: <runId> <taskId> <prompt>",
191
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
192
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
193
+ const runId = tokens.shift();
194
+ const taskId = tokens.shift();
195
+ const prompt = tokens.join(" ") || undefined;
196
+ if (!runId || !taskId || !prompt) {
197
+ await notifyCommandResult(ctx, "Usage: /team-follow-up <runId> <taskId> <prompt>. Use /team-respond for waiting-task replies.");
198
+ return;
199
+ }
200
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "follow-up-agent", taskId, prompt } }, teamCommandContext(ctx));
163
201
  await notifyCommandResult(ctx, commandText(result));
164
202
  },
165
203
  });
@@ -178,19 +216,19 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
178
216
  const [key, ...rest] = token.split("=");
179
217
  if (key) config[key] = parseScalar(rest.join("="));
180
218
  }
181
- const result = await handleTeamTool({ action: "api", runId, config }, ctx);
219
+ const result = await handleTeamTool({ action: "api", runId, config }, teamCommandContext(ctx));
182
220
  await notifyCommandResult(ctx, commandText(result));
183
221
  },
184
222
  });
185
223
 
186
224
  pi.registerCommand("team-metrics", { description: "Show pi-crew metrics snapshot: [filter]", handler: async (args: string, ctx: ExtensionCommandContext) => {
187
225
  const filter = args.trim() || undefined;
188
- const result = await handleTeamTool({ action: "api", config: { operation: "metrics-snapshot", filter } }, { ...ctx, metricRegistry: deps.getMetricRegistry?.() });
226
+ const result = await handleTeamTool({ action: "api", config: { operation: "metrics-snapshot", filter } }, { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.() });
189
227
  await notifyCommandResult(ctx, commandText(result));
190
228
  } });
191
229
 
192
230
  pi.registerCommand("team-imports", { description: "List imported pi-crew run bundles", handler: async (_args: string, ctx: ExtensionCommandContext) => {
193
- const result = await handleTeamTool({ action: "imports" }, ctx);
231
+ const result = await handleTeamTool({ action: "imports" }, teamCommandContext(ctx));
194
232
  await notifyCommandResult(ctx, commandText(result));
195
233
  } });
196
234
 
@@ -198,7 +236,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
198
236
  const tokens = args.trim().split(/\s+/).filter(Boolean);
199
237
  const pathArg = tokens.find((token) => !token.startsWith("--"));
200
238
  const scope = tokens.includes("--user") ? "user" : "project";
201
- const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, ctx);
239
+ const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, teamCommandContext(ctx));
202
240
  await notifyCommandResult(ctx, commandText(result));
203
241
  } });
204
242
 
@@ -206,21 +244,21 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
206
244
  const tokens = args.trim().split(/\s+/).filter(Boolean);
207
245
  const keepToken = tokens.find((token) => token.startsWith("--keep="));
208
246
  const keep = keepToken ? Number.parseInt(keepToken.slice("--keep=".length), 10) : undefined;
209
- const result = await handleTeamTool({ action: "prune", keep, confirm: tokens.includes("--confirm") }, ctx);
247
+ const result = await handleTeamTool({ action: "prune", keep, confirm: tokens.includes("--confirm") }, teamCommandContext(ctx));
210
248
  await notifyCommandResult(ctx, commandText(result));
211
249
  } });
212
250
 
213
251
  pi.registerCommand("team-forget", { description: "Forget a pi-crew run by deleting its state and artifacts", handler: async (args: string, ctx: ExtensionCommandContext) => {
214
252
  const tokens = args.trim().split(/\s+/).filter(Boolean);
215
253
  const runId = tokens.find((token) => !token.startsWith("--"));
216
- const result = await handleTeamTool({ action: "forget", runId, force: tokens.includes("--force"), confirm: tokens.includes("--confirm") }, ctx);
254
+ const result = await handleTeamTool({ action: "forget", runId, force: tokens.includes("--force"), confirm: tokens.includes("--confirm") }, teamCommandContext(ctx));
217
255
  await notifyCommandResult(ctx, commandText(result));
218
256
  } });
219
257
 
220
258
  pi.registerCommand("team-settings", {
221
259
  description: "View or update pi-crew settings: [list|get <key>|set <key> <value>|unset <key>|path|scope]",
222
260
  handler: async (args: string, ctx: ExtensionCommandContext) => {
223
- const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, ctx);
261
+ const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, teamCommandContext(ctx));
224
262
  await notifyCommandResult(ctx, commandText(result));
225
263
  },
226
264
  });
@@ -233,18 +271,18 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
233
271
  const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
234
272
  if (ctx.hasUI && loaded) {
235
273
  const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
236
- 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)";
274
+ const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, teamCommandContext(ctx))) : "(no result)";
237
275
  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" } });
238
276
  return;
239
277
  }
240
- const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
278
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, teamCommandContext(ctx));
241
279
  await notifyCommandResult(ctx, commandText(result));
242
280
  } });
243
281
 
244
282
  pi.registerCommand("team-transcript", { description: "Open a pi-crew transcript viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
245
283
  const [runId, taskId] = args.trim().split(/\s+/).filter(Boolean);
246
284
  if (await openTranscriptViewer(ctx, runId, taskId)) return;
247
- const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, ctx);
285
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, teamCommandContext(ctx));
248
286
  await notifyCommandResult(ctx, commandText(result));
249
287
  } });
250
288
 
@@ -252,8 +290,8 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
252
290
  for (;;) {
253
291
  const runs = deps.getManifestCache(ctx.cwd).list(50);
254
292
  const uiConfig = loadConfig(ctx.cwd).config.ui;
255
- const rightPanel = uiConfig?.dashboardPlacement !== "center";
256
- const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
293
+ const rightPanel = (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) === "right";
294
+ const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth)) : "90%";
257
295
  const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(ctx.cwd), runProvider: () => deps.getManifestCache(ctx.cwd).list(50), registry: deps.getMetricRegistry?.() }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
258
296
  if (!selection) return;
259
297
  if (selection.action === "reload") continue;
@@ -273,7 +311,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
273
311
  continue;
274
312
  }
275
313
  if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
276
- const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, ctx) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx) : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
314
+ const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, teamCommandContext(ctx)) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, teamCommandContext(ctx)) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, teamCommandContext(ctx)) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, teamCommandContext(ctx)) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, teamCommandContext(ctx)) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, teamCommandContext(ctx)) : await handleTeamTool({ action: selection.action, runId: selection.runId }, teamCommandContext(ctx));
277
315
  await notifyCommandResult(ctx, commandText(result));
278
316
  return;
279
317
  }
@@ -285,14 +323,15 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
285
323
  const uiConfig = loadConfig(ctx.cwd).config.ui;
286
324
  const styleArg = tokens.find((t) => t === "cat" || t === "armin");
287
325
  const effectArg = tokens.find((t) => ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"].includes(t));
288
- const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? "cat";
289
- const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? "random";
326
+ const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? DEFAULT_UI.mascotStyle;
327
+ const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? DEFAULT_UI.mascotEffect;
290
328
  await ctx.ui.custom<undefined>((tui, theme, _keybindings, done) => new AnimatedMascot(theme, () => done(undefined), { frameIntervalMs: style === "armin" ? 33 : 180, autoCloseMs: 7000, requestRender: () => requestRenderTarget(tui), style, effect }), { overlay: true, overlayOptions: { width: style === "armin" ? 48 : 62, maxHeight: "85%", anchor: "center" } });
291
329
  } });
292
330
 
293
- pi.registerCommand("team-init", { description: "Initialize project-local pi-crew directories and gitignore entries", handler: async (args: string, ctx: ExtensionCommandContext) => {
331
+ pi.registerCommand("team-init", { description: "Initialize pi-crew layout and global config. Use --project-config to write .pi/pi-crew.json.", handler: async (args: string, ctx: ExtensionCommandContext) => {
294
332
  const tokens = args.trim().split(/\s+/).filter(Boolean);
295
- const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite") } }, ctx);
333
+ const configScope = tokens.includes("--project-config") || tokens.includes("--project") ? "project" : tokens.includes("--no-config") ? "none" : "global";
334
+ const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite"), configScope } }, teamCommandContext(ctx));
296
335
  await notifyCommandResult(ctx, commandText(result));
297
336
  } });
298
337
 
@@ -300,14 +339,14 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
300
339
  const tokens = args.trim().split(/\s+/).filter(Boolean);
301
340
  const mode = tokens[0]?.toLowerCase();
302
341
  const config = mode === "on" ? { profile: "suggested", enabled: true, injectPolicy: true } : mode === "off" ? { profile: "manual", enabled: false } : mode === "manual" || mode === "suggested" || mode === "assisted" || mode === "aggressive" ? { profile: mode, enabled: mode !== "manual", injectPolicy: mode !== "manual" } : { preferAsyncForLongTasks: tokens.includes("--prefer-async") ? true : undefined, allowWorktreeSuggestion: tokens.includes("--no-worktree-suggest") ? false : undefined };
303
- const result = await handleTeamTool({ action: "autonomy", config }, ctx);
342
+ const result = await handleTeamTool({ action: "autonomy", config }, teamCommandContext(ctx));
304
343
  await notifyCommandResult(ctx, commandText(result));
305
344
  } });
306
345
 
307
346
  pi.registerCommand("team-config", { description: "Show or update pi-crew config. Use key=value [--project] to update.", handler: async (args: string, ctx: ExtensionCommandContext) => {
308
347
  const tokens = args.trim().split(/\s+/).filter(Boolean);
309
348
  if (tokens.length === 0) {
310
- const result = await handleTeamTool({ action: "config" }, ctx);
349
+ const result = await handleTeamTool({ action: "config" }, teamCommandContext(ctx));
311
350
  await notifyCommandResult(ctx, commandText(result));
312
351
  return;
313
352
  }
@@ -324,7 +363,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
324
363
  if (raw === "unset" || raw === "null") pushUnset(config, key);
325
364
  else setNestedConfig(config, key, parseScalar(raw));
326
365
  }
327
- const result = await handleTeamTool({ action: "config", config }, ctx);
366
+ const result = await handleTeamTool({ action: "config", config }, teamCommandContext(ctx));
328
367
  await notifyCommandResult(ctx, commandText(result));
329
368
  } });
330
369
 
@@ -332,7 +371,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
332
371
  ["team-validate", "validate", "Validate pi-crew agents, teams, and workflows"],
333
372
  ["team-doctor", "doctor", "Check pi-crew installation and discovery readiness"],
334
373
  ] as const) pi.registerCommand(name, { description, handler: async (_args: string, ctx: ExtensionCommandContext) => {
335
- const result = await handleTeamTool({ action }, ctx);
374
+ const result = await handleTeamTool({ action }, teamCommandContext(ctx));
336
375
  await notifyCommandResult(ctx, commandText(result));
337
376
  } });
338
377
 
@@ -3,7 +3,7 @@ import { listRecentRuns } from "../run-index.ts";
3
3
  import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
4
4
 
5
5
  export interface RegisterCompactionGuardOptions {
6
- foregroundControllers: Set<AbortController>;
6
+ foregroundControllers: Map<string | symbol, AbortController>;
7
7
  }
8
8
 
9
9
  const TRIGGER_RATIO = 0.75;
@@ -81,6 +81,13 @@ export function subagentToolResult(text: string, details: Record<string, unknown
81
81
  return { content: [{ type: "text" as const, text }], details, isError };
82
82
  }
83
83
 
84
+ function parseSkillParam(value: unknown): string | string[] | false | undefined {
85
+ if (value === false) return false;
86
+ if (typeof value === "string") return value;
87
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) return value;
88
+ return undefined;
89
+ }
90
+
84
91
  export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx: Pick<ExtensionContext, "cwd">): SubagentSpawnOptions {
85
92
  return {
86
93
  cwd: ctx.cwd,
@@ -89,6 +96,7 @@ export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx
89
96
  prompt: typeof params.prompt === "string" ? params.prompt : "",
90
97
  background: params.run_in_background === true,
91
98
  model: typeof params.model === "string" && params.model.trim() ? params.model.trim() : undefined,
99
+ skill: parseSkillParam(params.skill),
92
100
  maxTurns: typeof params.max_turns === "number" && Number.isFinite(params.max_turns) ? params.max_turns : undefined,
93
101
  };
94
102
  }