pi-crew 0.10.1 → 0.10.3

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 (127) hide show
  1. package/CHANGELOG.md +347 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/executor.md +1 -1
  5. package/agents/test-engineer.md +1 -1
  6. package/dist/index.mjs +2555 -932
  7. package/package.json +2 -1
  8. package/schema.json +503 -94
  9. package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +6 -2
  10. package/skills/real-test-pi-crew/SKILL.md +278 -79
  11. package/src/config/config-merge.ts +11 -1
  12. package/src/config/config-validation.ts +47 -2
  13. package/src/config/config.ts +28 -6
  14. package/src/config/defaults.ts +43 -11
  15. package/src/config/env-vars.ts +27 -2
  16. package/src/config/role-tools.ts +4 -2
  17. package/src/config/types.ts +55 -1
  18. package/src/extension/crew-cleanup.ts +13 -0
  19. package/src/extension/crew-vibes/footer.ts +19 -0
  20. package/src/extension/crew-vibes/index.ts +11 -1
  21. package/src/extension/register.ts +8 -0
  22. package/src/extension/registration/command-registration.ts +1 -0
  23. package/src/extension/registration/commands/run.ts +15 -1
  24. package/src/extension/registration/commands/shared.ts +8 -0
  25. package/src/extension/registration/foreground-run-controller.ts +10 -2
  26. package/src/extension/registration/lifecycle-handlers.ts +92 -11
  27. package/src/extension/registration/runtime-cleanup.ts +23 -5
  28. package/src/extension/registration/team-tool.ts +58 -6
  29. package/src/extension/registration/ui.ts +5 -4
  30. package/src/extension/team-tool/doctor.ts +364 -7
  31. package/src/extension/team-tool/handle-settings.ts +19 -0
  32. package/src/extension/team-tool/inspect.ts +10 -2
  33. package/src/extension/team-tool/run-deadline.ts +20 -3
  34. package/src/extension/team-tool/run.ts +26 -2
  35. package/src/extension/team-tool/status.ts +7 -0
  36. package/src/extension/team-tool.ts +35 -2
  37. package/src/hooks/registry.ts +59 -56
  38. package/src/prompt/inbox-poll.ts +90 -0
  39. package/src/prompt/message-tool.ts +166 -0
  40. package/src/prompt/prompt-runtime.ts +201 -18
  41. package/src/prompt/surface-worker.ts +720 -0
  42. package/src/prompt/worker-events-channel.ts +49 -3
  43. package/src/runtime/async-runner.ts +29 -1
  44. package/src/runtime/background-runner.ts +13 -7
  45. package/src/runtime/broker/broker-issuer.ts +27 -2
  46. package/src/runtime/broker/crew-broker-tokens.ts +56 -4
  47. package/src/runtime/broker/crew-broker.ts +261 -41
  48. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  49. package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
  50. package/src/runtime/child-pi/child-pi-streams.ts +30 -2
  51. package/src/runtime/child-pi/child-pi.ts +353 -5
  52. package/src/runtime/crew-agent-records.ts +13 -1
  53. package/src/runtime/detached-run-results.ts +90 -0
  54. package/src/runtime/dispatch-batch.ts +12 -1
  55. package/src/runtime/event-log-tail-source.ts +374 -0
  56. package/src/runtime/finalize-run.ts +4 -0
  57. package/src/runtime/goal-workflow/adaptive-plan.ts +30 -3
  58. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +5 -1
  59. package/src/runtime/live-session/live-agent-manager.ts +34 -1
  60. package/src/runtime/live-session/live-control-realtime.ts +10 -0
  61. package/src/runtime/live-session/live-session-runtime.ts +47 -27
  62. package/src/runtime/manifest-cache.ts +128 -17
  63. package/src/runtime/merge-gate.ts +25 -9
  64. package/src/runtime/model/model-fallback.ts +7 -3
  65. package/src/runtime/model/pi-args.ts +54 -65
  66. package/src/runtime/output/sidechain-output.ts +61 -6
  67. package/src/runtime/process/proc-stat.ts +46 -0
  68. package/src/runtime/process/zombie-scanner.ts +32 -19
  69. package/src/runtime/process-status.ts +16 -1
  70. package/src/runtime/recovery/crash-recovery.ts +16 -0
  71. package/src/runtime/run-tracker.ts +77 -10
  72. package/src/runtime/spawn-policy.ts +27 -41
  73. package/src/runtime/surface/degrade.ts +776 -0
  74. package/src/runtime/surface/herdr-provider.ts +546 -0
  75. package/src/runtime/surface/launch-script.ts +172 -0
  76. package/src/runtime/surface/resolve-surface.ts +274 -0
  77. package/src/runtime/surface/surface-provider.ts +129 -0
  78. package/src/runtime/surface/surface-spawn.ts +475 -0
  79. package/src/runtime/surface/tmux-provider.ts +400 -0
  80. package/src/runtime/task-runner/child-executor.ts +47 -0
  81. package/src/runtime/task-runner/post-execution.ts +57 -2
  82. package/src/runtime/task-runner/prompt-builder.ts +1 -0
  83. package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
  84. package/src/runtime/task-runner/state-helpers.ts +54 -30
  85. package/src/runtime/task-runner.ts +4 -2
  86. package/src/runtime/team-runner.ts +104 -2
  87. package/src/schema/config-schema.ts +25 -1
  88. package/src/state/atomic-write.ts +219 -40
  89. package/src/state/coordination/locks.ts +7 -5
  90. package/src/state/coordination/mailbox.ts +56 -10
  91. package/src/state/event-log/cursor.ts +413 -23
  92. package/src/state/event-log/event-log.ts +120 -113
  93. package/src/state/event-log/sequence-cache.ts +21 -3
  94. package/src/state/stores/plan-store.ts +1 -1
  95. package/src/state/stores/state-store.ts +171 -9
  96. package/src/state/types.ts +53 -0
  97. package/src/ui/dock-footer.ts +49 -0
  98. package/src/ui/inline-panel/agent-pane.ts +378 -0
  99. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  100. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  101. package/src/ui/inline-panel/crew-editor.ts +192 -0
  102. package/src/ui/inline-panel/index.ts +290 -0
  103. package/src/ui/inline-panel/panel-rows.ts +37 -0
  104. package/src/ui/inline-panel/panel-selection.ts +157 -0
  105. package/src/ui/inline-panel/panel-store.ts +111 -0
  106. package/src/ui/inline-panel/view-session-store.ts +36 -0
  107. package/src/ui/pi-ui-compat.ts +9 -0
  108. package/src/ui/render-diff.ts +16 -8
  109. package/src/ui/run-dashboard.ts +87 -42
  110. package/src/ui/run-event-bus.ts +10 -1
  111. package/src/ui/run-snapshot-cache.ts +83 -35
  112. package/src/ui/transcript-cache.ts +101 -13
  113. package/src/ui/transcript-viewer.ts +92 -24
  114. package/src/ui/widget/index.ts +203 -25
  115. package/src/ui/widget/task-list.ts +198 -0
  116. package/src/ui/widget/widget-formatters.ts +240 -4
  117. package/src/ui/widget/widget-renderer.ts +234 -37
  118. package/src/ui/widget/widget-types.ts +11 -0
  119. package/src/utils/child-process-shield.ts +106 -0
  120. package/src/utils/redaction.ts +7 -0
  121. package/src/utils/safe-abort.ts +45 -0
  122. package/src/utils/visual.ts +43 -0
  123. package/src/workflows/discover-workflows.ts +1 -0
  124. package/src/workflows/workflow-config.ts +7 -0
  125. package/src/worktree/worktree-manager.ts +65 -4
  126. package/workflows/default.workflow.md +36 -26
  127. package/workflows/strict-fast-fix.workflow.md +26 -0
@@ -21,9 +21,10 @@ import { loadConfig } from "../../config/config.ts";
21
21
  import { DEFAULT_UI } from "../../config/defaults.ts";
22
22
  import { getCrewEnv } from "../../config/env-vars.ts";
23
23
  import { pruneFinishedRuns, pruneUserLevelRuns } from "../../extension/run-maintenance.ts";
24
- import { type BrokerSpawnCredentials, setActiveBrokerIssuer } from "../../runtime/broker/broker-issuer.ts";
24
+ import { type BrokerSpawnCredentials, setActiveBrokerIssuer, setActiveBrokerRevoker } from "../../runtime/broker/broker-issuer.ts";
25
25
  import { CrewBroker } from "../../runtime/broker/crew-broker.ts";
26
26
  import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
27
+ import { forgetDetachedRun, hasDetachedRuns, peekFinishedDetachedRunResults } from "../../runtime/detached-run-results.ts";
27
28
  import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
28
29
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
29
30
  import { configuredModelInfosFromPiConfig } from "../../runtime/model/model-fallback.ts";
@@ -39,7 +40,9 @@ import { applyCrewSettingsTiersToConfig, loadCrewSettingsTiers } from "../../run
39
40
  import { loadRunManifestById } from "../../state/stores/state-store.ts";
40
41
  import type { TeamRunManifest } from "../../state/types.ts";
41
42
  import { summarizeHeartbeats } from "../../ui/heartbeat-aggregator.ts";
42
- import { requestRender, setExtensionWidget } from "../../ui/pi-ui-compat.ts";
43
+ import { installInlinePanel } from "../../ui/inline-panel/index.ts";
44
+ import { clearSessionSwitchInFlight, markSessionSwitchInFlight } from "../../ui/inline-panel/view-session-store.ts";
45
+ import { requestRender, setExtensionWidget, toPiWidgetPlacement } from "../../ui/pi-ui-compat.ts";
43
46
  import {
44
47
  registerPiCrewPowerbarSegments,
45
48
  requestPowerbarUpdate,
@@ -75,6 +78,36 @@ export function installSessionLifecycleHandlers(pi: ExtensionAPI, ctx: Registrat
75
78
  installModelTrackingHandlers(pi);
76
79
  }
77
80
 
81
+ /**
82
+ * Hand over the outcome of runs detached by an agent-view switch.
83
+ *
84
+ * Delivered as a DISPLAYED session entry (not a follow-up queue item): an idle
85
+ * session never flushes a follow-up, so the queued variant left the result
86
+ * invisible. Each result is dropped from the registry only after its send
87
+ * succeeded, and only while the owning session is current — a worker's view
88
+ * session must never receive the parent run's report.
89
+ */
90
+ function deliverDetachedRunResults(pi: ExtensionAPI, extensionCtx: ExtensionContext): void {
91
+ if (!hasDetachedRuns()) return;
92
+ try {
93
+ // Agent views are in-document panes now, never sessions: the current
94
+ // session is always the main one, so detached results always deliver.
95
+ const inViewSession = false;
96
+ for (const { runId, text } of peekFinishedDetachedRunResults({ inViewSession })) {
97
+ pi.sendMessage({ customType: "pi-crew-run-result", content: text, display: true });
98
+ forgetDetachedRun(runId);
99
+ try {
100
+ extensionCtx.ui.notify(text.split("\n")[0] ?? `pi-crew run finished: ${runId}`, "info");
101
+ } catch {
102
+ /* toast is secondary to the session entry */
103
+ }
104
+ }
105
+ } catch (error) {
106
+ // Keep the entry: the next tick retries rather than losing the outcome.
107
+ logInternalError("register.detachedRunResults", error);
108
+ }
109
+ }
110
+
78
111
  /**
79
112
  * model_select / thinking_level_select:
80
113
  * Track what the MAIN session is *actually* running so subagents that
@@ -125,6 +158,12 @@ function installSessionShutdownHandler(pi: ExtensionAPI, ctx: RegistrationContex
125
158
  */
126
159
  function installSessionBeforeSwitchHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
127
160
  pi.on("session_before_switch", () => {
161
+ // The switch tears the current turn down right after this handler
162
+ // (teardownCurrent → session.abort()). That turn-abort must NOT cancel
163
+ // a foreground team run that is still forming (see run-deadline.ts):
164
+ // foreground runs survive session switches (P0). Cleared on the next
165
+ // session_start.
166
+ markSessionSwitchInFlight();
128
167
  ctx.sessionGeneration++;
129
168
  const pendingCount = ctx.lifecycleState.deliveryCoordinator?.getPendingCount() ?? 0;
130
169
  try {
@@ -167,6 +206,9 @@ function installSessionBeforeSwitchHandler(pi: ExtensionAPI, ctx: RegistrationCo
167
206
  */
168
207
  function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext): void {
169
208
  pi.on("session_start", (_event, extensionCtx) => {
209
+ // Any session start means a pending switch landed — the turn-abort
210
+ // suppression window for that switch is over.
211
+ clearSessionSwitchInFlight();
170
212
  runArtifactCleanup(extensionCtx.cwd);
171
213
 
172
214
  // Restore brief mode state from session entries (best-effort).
@@ -273,6 +315,13 @@ function installSessionStartHandler(pi: ExtensionAPI, ctx: RegistrationContext):
273
315
  });
274
316
  const cache = ctx.getManifestCache(extensionCtx.cwd);
275
317
  updateCrewWidget(extensionCtx, ctx.widgetState, loadedConfig.config.ui, cache, ctx.getRunSnapshotCache(extensionCtx.cwd));
318
+ // Inline agent panel: keyboard-navigable rows under the prompt + per-agent
319
+ // transcript pane. Installed after the widget so the row projection sees
320
+ // the same caches the widget paint uses.
321
+ installInlinePanel(pi, extensionCtx, loadedConfig.config.ui);
322
+ // Returning from an agent view lands here: report any detached run that
323
+ // finished while the view was open, without waiting for a render tick.
324
+ deliverDetachedRunResults(pi, extensionCtx);
276
325
  updatePiCrewPowerbar(
277
326
  pi.events,
278
327
  extensionCtx.cwd,
@@ -632,8 +681,13 @@ function setupRenderLoop(
632
681
  // file just changed on disk, so force a fresh snapshot while keeping
633
682
  // the entry populated — deleting it left a window where the widget's
634
683
  // `get()` returned undefined and dropped the run to "(loading…)".
684
+ // PERF (2026-08-24): route through the coalesced ASYNC refresh —
685
+ // fs.watch can fire many times per second and the sync rebuild
686
+ // blocked the UI event loop. The entry stays populated until the
687
+ // async rebuild re-sets it in place; the render schedule below
688
+ // repaints while it lands.
635
689
  try {
636
- ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).refresh(runId);
690
+ ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).scheduleRefresh(runId);
637
691
  } catch (error) {
638
692
  logInternalError("register.runWatcher.refresh", error, runId);
639
693
  }
@@ -681,13 +735,14 @@ function setupRenderLoop(
681
735
 
682
736
  const renderTick = (): void => {
683
737
  if (!ctx.currentCtx) return;
738
+ deliverDetachedRunResults(pi, ctx.currentCtx);
684
739
  const config = lastPreloadedConfig?.config.ui;
685
740
  const activeCache = lastFrameManifestCache ?? ctx.getManifestCache(ctx.currentCtx.cwd);
686
741
  const snapshotCache = lastFrameSnapshotCache ?? ctx.getRunSnapshotCache(ctx.currentCtx.cwd);
687
742
  const manifests = lastPreloadedManifests;
688
743
  if (!lastPreloadedConfig) backgroundPreload();
689
744
  if (ctx.uiState.liveSidebarRunId || ctx.uiState.dashboardOpen) {
690
- const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
745
+ const placement = toPiWidgetPlacement(config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement);
691
746
  if (ctx.widgetState.lastVisibility !== "hidden" || ctx.widgetState.lastPlacement !== placement) {
692
747
  setExtensionWidget(ctx.currentCtx, "pi-crew", undefined, { placement });
693
748
  setExtensionWidget(ctx.currentCtx, "pi-crew-active", undefined, { placement });
@@ -880,8 +935,11 @@ function setupRenderLoop(
880
935
  // FLICKER FIX: rebuild-in-place instead of deleting the entry (see
881
936
  // onRunChange above). A hard delete left `get()` returning undefined for
882
937
  // a frame, dropping the run to "(loading…)" and causing visible flicker.
938
+ // PERF (2026-08-24): coalesced ASYNC refresh — fs.watch can fire many
939
+ // times per second and the sync rebuild blocked the UI event loop; the
940
+ // render schedule below repaints while the rebuild lands.
883
941
  try {
884
- ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).refresh(runId);
942
+ ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).scheduleRefresh(runId);
885
943
  } catch (error) {
886
944
  logInternalError("register.crewRunWatcher.refresh", error, runId);
887
945
  }
@@ -1027,10 +1085,11 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
1027
1085
  // config.broker.waitMethodsEnabled a dead knob and the ADR-0
1028
1086
  // "then true" flip a silent no-op. Fail-closed when unset.
1029
1087
  waitMethodsEnabled: cfg?.waitMethodsEnabled ?? false,
1030
- // T3/R5 (ADR-5 §10): governed-nesting capability gate — fail-closed
1031
- // default; production threads config.nesting (sensitive: user config
1032
- // only). Nested-slot sizing + admission-time model catalog (ADR-5 §7 —
1033
- // the production wiring MUST supply it) + workspace gate mirror.
1088
+ // T3/R5 (ADR-5 §10): governed-nesting capability gate — default-on
1089
+ // since D8 (loadConfig layers DEFAULT_NESTING.enabled=true; sensitive
1090
+ // flag, so only USER config may flip it). Nested-slot sizing +
1091
+ // admission-time model catalog (ADR-5 §7 — the production wiring
1092
+ // MUST supply it) + workspace gate mirror.
1034
1093
  nestingEnabled: nestingCfg?.nesting?.enabled ?? false,
1035
1094
  ...(nestingCfg?.nesting?.maxSlots !== undefined ? { nestingMaxSlots: nestingCfg.nesting.maxSlots } : {}),
1036
1095
  ...(nestingCfg?.nesting?.maxDepth !== undefined ? { nestingMaxDepth: nestingCfg.nesting.maxDepth } : {}),
@@ -1070,8 +1129,8 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
1070
1129
  if (!effectiveEnabled()) return undefined;
1071
1130
  // ADR-5 §4 (governed nesting): tokens are minted ONLY for children that
1072
1131
  // may themselves delegate — childDepth < resolved maxDepth. At the default
1073
- // maxDepth=2 a delegate-spawned depth-2 grandchild gets NO credentials
1074
- // (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at depth 2; identity
1132
+ // maxDepth=4 a delegate-spawned depth-4 grandchild gets NO credentials
1133
+ // (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at the cap depth; identity
1075
1134
  // routing via PI_CREW_BROKER_RUN_ID/TASK_ID is threaded unconditionally
1076
1135
  // elsewhere). Undefined childDepth = legacy worker spawn (depth 1).
1077
1136
  if (childDepth !== undefined && childDepth >= resolveCrewMaxDepth(undefined)) return undefined;
@@ -1086,6 +1145,27 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
1086
1145
  }
1087
1146
  };
1088
1147
 
1148
+ // MuxSurface A1 (spec §7 D3 step 2): publish the revoker alongside the
1149
+ // issuer — team-runner's degrade controller calls it when a pane is lost.
1150
+ // Best-effort and self-gated: no broker bound (never started / session
1151
+ // switched) means nothing to revoke; re-issue after respawn mints fresh (T10).
1152
+ const revokeForTask = (taskId: string): void => {
1153
+ if (!taskId || typeof taskId !== "string") return;
1154
+ if (!isRootSession(process.env)) return;
1155
+ if (!broker || brokerSessionId !== cachedSessionId) return;
1156
+ try {
1157
+ broker.revokeTaskToken(taskId);
1158
+ } catch (error) {
1159
+ logInternalError(
1160
+ "broker.revoke-for-task",
1161
+ error instanceof Error ? error : new Error(String(error)),
1162
+ `taskId=${taskId}`,
1163
+ "warn",
1164
+ );
1165
+ }
1166
+ };
1167
+ setActiveBrokerRevoker(revokeForTask);
1168
+
1089
1169
  // Publish this issuer as the process-local active issuer so runChildPi can
1090
1170
  // default `brokerIssuer` without the registration context being threaded
1091
1171
  // through every runner call site. The issuer self-gates (root + flag), so
@@ -1096,6 +1176,7 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
1096
1176
  issueForChild,
1097
1177
  stop: async () => {
1098
1178
  setActiveBrokerIssuer(undefined);
1179
+ setActiveBrokerRevoker(undefined);
1099
1180
  if (broker) {
1100
1181
  try {
1101
1182
  await broker.stop();
@@ -20,12 +20,12 @@
20
20
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
21
  import { loadConfig } from "../../config/config.ts";
22
22
  import { clearHooksScoped } from "../../hooks/registry.ts";
23
- import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
24
23
  import { stopAllWatchdogs } from "../../runtime/foreground-watchdog.ts";
25
24
  import { clearPiCrewPowerbar, disposePowerbarCoalescer } from "../../ui/powerbar-publisher.ts";
26
25
  import { stopCrewWidget } from "../../ui/widget/index.ts";
27
26
  import { logInternalError } from "../../utils/internal-error.ts";
28
27
  import { clearProjectRootCache } from "../../utils/paths.ts";
28
+ import { safeAbortAll } from "../../utils/safe-abort.ts";
29
29
  import { extractSessionId } from "../../utils/session-utils.ts";
30
30
  import { stopAsyncRunNotifier } from "../async-notifier.ts";
31
31
  import { uninstallCrewGlobalRegistry } from "../team-tool.ts";
@@ -135,7 +135,9 @@ function buildCleanupRuntime(ctx: RegistrationContext): () => void {
135
135
  ctx.stopSessionBoundSubagents();
136
136
  // P0 fix: also abort foreground team runs on session shutdown (not on session switch).
137
137
  // This is the only place where foreground team run controllers should be aborted.
138
- for (const controller of ctx.foregroundTeamRunControllers.values()) controller.abort();
138
+ // safe-abort: shutdown races completed runs whose spawned children already
139
+ // exited — abort() would throw AbortError from Node's child_process listener.
140
+ safeAbortAll(ctx.foregroundTeamRunControllers.values(), "cleanup-runtime.foreground-team");
139
141
  ctx.foregroundTeamRunControllers.clear();
140
142
  // RC-01: stop the foreground-run watchdog timers on full shutdown — they were
141
143
  // never cleared, so an active (non-terminal) foreground run left its watchdog
@@ -219,10 +221,26 @@ function buildStopSessionBoundSubagents(ctx: RegistrationContext): () => void {
219
221
  // Only abort subagent controllers — NOT foreground team runs.
220
222
  // Foreground team runs are bound to the session lifecycle; they will be aborted
221
223
  // by cleanupRuntime during session_shutdown.
222
- for (const controller of ctx.foregroundControllers.values()) controller.abort();
224
+ // safe-abort: these signals may be linked to spawned children (pipelines pass
225
+ // them into spawn options.signal); aborting after a child exited makes Node's
226
+ // child_process listener throw AbortError synchronously — inside a session
227
+ // event handler that would surface as uncaughtException and kill pi.
228
+ safeAbortAll(ctx.foregroundControllers.values(), "stop-session-bound.foreground");
223
229
  ctx.foregroundControllers.clear();
224
- ctx.subagentManager.abortAll("Session switching — foreground subagents cancelled.");
225
- terminateActiveChildPiProcesses();
230
+ try {
231
+ ctx.subagentManager.abortAll("Session switching — foreground subagents cancelled.");
232
+ } catch (error) {
233
+ logInternalError("stop-session-bound.abortAll", error);
234
+ }
235
+ // Deliberately NOT terminateActiveChildPiProcesses() here: the active
236
+ // child registry holds EVERY child pi spawned in this process —
237
+ // including foreground team-run workers. Killing them on a session
238
+ // switch (resume/new/fork) is exactly the regression "jumping into any
239
+ // session stops the run" (exit 143). Session-bound subagents are
240
+ // cancelled above via their own controllers; foreground runs must
241
+ // survive switches (P0) and are aborted only by cleanupRuntime on real
242
+ // shutdown; the process-exit hook (tryRegisterSessionCleanup) drains
243
+ // the registry when pi actually exits.
226
244
  ctx.disposeRenderSchedulerSubscriptions();
227
245
  ctx.renderScheduler?.dispose();
228
246
  ctx.renderScheduler = undefined;
@@ -13,6 +13,7 @@ import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
13
13
  import { statusIcon, type ToolRenderContext, teamToolRenderer } from "../../ui/tool-renderers/index.ts";
14
14
  import type { CrewWidgetState } from "../../ui/widget/index.ts";
15
15
  import { updateCrewWidget } from "../../ui/widget/index.ts";
16
+ import { safeAbort } from "../../utils/safe-abort.ts";
16
17
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
17
18
  // Team tool handler — lazy-loaded because team-tool.ts imports many modules
18
19
  import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
@@ -146,6 +147,52 @@ export function detectUnrecognizedParams(schema: TObject, params: unknown): stri
146
147
  ].join("\n");
147
148
  }
148
149
 
150
+ // PERF (2026-08-24): cheap read actions (status/summary/events/get/list) re-rendered
151
+ // the widget + powerbar after every call; the event bus and render tick already
152
+ // reflect run-state changes. Refresh only on actions that mutate run state
153
+ // outside the watched files. Names mirror the five domain dispatcher tables in
154
+ // src/schema/team-tool-schema.ts (RUN/STATUS/CONTROL/MANAGE/AUTOMATE). Actions
155
+ // whose effect depends on sub-params (api: approve-plan/agent-control/mailbox;
156
+ // plans: approve/reject; anchor/auto-summarize/auto_boomerang: set/clear/toggle;
157
+ // config/autonomy: show vs write) are included — a redundant refresh on their
158
+ // read sub-actions is cheap, a missed refresh after a write leaves stale UI.
159
+ const MUTATING_ACTIONS = new Set([
160
+ // run domain
161
+ "run",
162
+ "parallel",
163
+ "orchestrate",
164
+ "resume",
165
+ "retry",
166
+ "steer",
167
+ "goal",
168
+ "plans",
169
+ // control domain
170
+ "cancel",
171
+ "invalidate",
172
+ "respond",
173
+ "cleanup",
174
+ "prune",
175
+ "forget",
176
+ // manage domain
177
+ "create",
178
+ "update",
179
+ "delete",
180
+ "init",
181
+ "config",
182
+ "autonomy",
183
+ "settings",
184
+ "workflow-create",
185
+ "workflow-save",
186
+ "workflow-delete",
187
+ "import",
188
+ // automate domain
189
+ "schedule",
190
+ "anchor",
191
+ "auto-summarize",
192
+ "auto_boomerang",
193
+ "api",
194
+ ]);
195
+
149
196
  export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
150
197
  const tool: ToolDefinition = {
151
198
  name: "team",
@@ -174,7 +221,10 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
174
221
  const controller = new AbortController();
175
222
  const toolKey = Symbol();
176
223
  deps.foregroundControllers.set(toolKey, controller);
177
- const abort = (): void => controller.abort();
224
+ // safe-abort: pi's tool-call abort can race a completed run whose
225
+ // spawned children already exited — Node's child_process signal
226
+ // listener throws AbortError out of abort() and kills the host.
227
+ const abort = (): void => safeAbort(controller, "team-tool.caller-abort");
178
228
  signal?.addEventListener("abort", abort, { once: true });
179
229
  const stopProgress = startTeamToolProgressBinder(onUpdate as OnUpdate | undefined);
180
230
  try {
@@ -225,11 +275,13 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
225
275
  timestamp: Date.now(),
226
276
  });
227
277
  }
228
- const config = loadConfig(toolCtx.cwd).config.ui;
229
- const cache = deps.getManifestCache(toolCtx.cwd);
230
- const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
231
- updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
232
- updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
278
+ if (MUTATING_ACTIONS.has(resolved.action ?? "list")) {
279
+ const config = loadConfig(toolCtx.cwd).config.ui;
280
+ const cache = deps.getManifestCache(toolCtx.cwd);
281
+ const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
282
+ updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
283
+ updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
284
+ }
233
285
  return output;
234
286
  } finally {
235
287
  signal?.removeEventListener("abort", abort);
@@ -15,7 +15,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
15
15
  import { loadConfig } from "../../config/config.ts";
16
16
  import { DEFAULT_UI } from "../../config/defaults.ts";
17
17
  import type { LiveRunSidebar as LiveRunSidebarType } from "../../ui/live-run-sidebar.ts";
18
- import { requestRender, setExtensionWidget, showCustom } from "../../ui/pi-ui-compat.ts";
18
+ import { requestRender, setExtensionWidget, showCustom, toPiWidgetPlacement } from "../../ui/pi-ui-compat.ts";
19
19
  import {
20
20
  clearPiCrewPowerbar,
21
21
  registerPiCrewPowerbarSegments,
@@ -76,10 +76,11 @@ export async function installLiveSidebar(ctx: ExtensionContext, runId: string, s
76
76
  state.liveSidebarRunId = runId;
77
77
  state.dashboardOpened = true;
78
78
  const widgetPlacement = uiConfig?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
79
- setExtensionWidget(ctx, "pi-crew", undefined, { placement: widgetPlacement });
80
- setExtensionWidget(ctx, "pi-crew-active", undefined, { placement: widgetPlacement });
79
+ const placement = toPiWidgetPlacement(widgetPlacement);
80
+ setExtensionWidget(ctx, "pi-crew", undefined, { placement });
81
+ setExtensionWidget(ctx, "pi-crew-active", undefined, { placement });
81
82
  deps.widgetState.lastVisibility = "hidden";
82
- deps.widgetState.lastPlacement = widgetPlacement;
83
+ deps.widgetState.lastPlacement = placement;
83
84
  deps.widgetState.lastKey = "pi-crew-active";
84
85
  deps.widgetState.model = undefined;
85
86
  const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth));