pi-crew 0.5.2 → 0.5.6

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 (137) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +17 -1
  3. package/docs/architecture.md +2 -0
  4. package/docs/bugs/cross-session-notification-leakage.md +82 -0
  5. package/docs/coding-agent-optimization.md +268 -0
  6. package/docs/deep-review-report.md +384 -0
  7. package/docs/distillation/cybersecurity-patterns.md +294 -0
  8. package/docs/migration-v0.4-v0.5.md +208 -0
  9. package/docs/optimization-plan.md +642 -0
  10. package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
  11. package/docs/pi-mono-opportunities.md +969 -0
  12. package/docs/pi-mono-review.md +291 -0
  13. package/docs/skills/REFERENCE.md +144 -0
  14. package/package.json +12 -9
  15. package/skills/artifact-analysis-loop/SKILL.md +302 -0
  16. package/skills/async-worker-recovery/SKILL.md +19 -1
  17. package/skills/child-pi-spawning/SKILL.md +19 -6
  18. package/skills/context-artifact-hygiene/SKILL.md +19 -2
  19. package/skills/delegation-patterns/SKILL.md +68 -3
  20. package/skills/detection-pipeline-design/SKILL.md +285 -0
  21. package/skills/event-log-tracing/SKILL.md +20 -6
  22. package/skills/git-master/SKILL.md +20 -6
  23. package/skills/hunting-investigation-loop/SKILL.md +401 -0
  24. package/skills/incident-playbook-construction/SKILL.md +383 -0
  25. package/skills/live-agent-lifecycle/SKILL.md +20 -6
  26. package/skills/mailbox-interactive/SKILL.md +19 -6
  27. package/skills/model-routing-context/SKILL.md +19 -1
  28. package/skills/multi-perspective-review/SKILL.md +19 -4
  29. package/skills/observability-reliability/SKILL.md +19 -2
  30. package/skills/orchestration/SKILL.md +20 -2
  31. package/skills/ownership-session-security/SKILL.md +20 -2
  32. package/skills/pi-extension-lifecycle/SKILL.md +20 -2
  33. package/skills/post-mortem/SKILL.md +7 -2
  34. package/skills/read-only-explorer/SKILL.md +20 -6
  35. package/skills/requirements-to-task-packet/SKILL.md +23 -3
  36. package/skills/resource-discovery-config/SKILL.md +20 -2
  37. package/skills/runtime-state-reader/SKILL.md +20 -2
  38. package/skills/safe-bash/SKILL.md +21 -6
  39. package/skills/scrutinize/SKILL.md +20 -2
  40. package/skills/secure-agent-orchestration-review/SKILL.md +29 -2
  41. package/skills/security-review/SKILL.md +560 -0
  42. package/skills/state-mutation-locking/SKILL.md +22 -2
  43. package/skills/systematic-debugging/SKILL.md +8 -6
  44. package/skills/threat-hypothesis-framework/SKILL.md +175 -0
  45. package/skills/ui-render-performance/SKILL.md +20 -2
  46. package/skills/verification-before-done/SKILL.md +17 -2
  47. package/skills/widget-rendering/SKILL.md +21 -6
  48. package/skills/workspace-isolation/SKILL.md +20 -6
  49. package/skills/worktree-isolation/SKILL.md +20 -6
  50. package/src/agents/agent-config.ts +40 -1
  51. package/src/benchmark/benchmark-runner.ts +45 -0
  52. package/src/benchmark/feedback-loop.ts +5 -0
  53. package/src/config/config.ts +32 -5
  54. package/src/config/role-tools.ts +82 -0
  55. package/src/config/suggestions.ts +8 -0
  56. package/src/config/types.ts +4 -0
  57. package/src/extension/async-notifier.ts +10 -1
  58. package/src/extension/crew-cleanup.ts +114 -0
  59. package/src/extension/cross-extension-rpc.ts +1 -1
  60. package/src/extension/notification-router.ts +18 -0
  61. package/src/extension/register.ts +27 -19
  62. package/src/extension/registration/subagent-tools.ts +1 -1
  63. package/src/extension/team-tool/anchor.ts +201 -0
  64. package/src/extension/team-tool/api.ts +2 -1
  65. package/src/extension/team-tool/auto-summarize.ts +154 -0
  66. package/src/extension/team-tool/run.ts +42 -7
  67. package/src/extension/team-tool.ts +44 -2
  68. package/src/hooks/registry.ts +1 -3
  69. package/src/observability/event-bus.ts +69 -0
  70. package/src/observability/event-to-metric.ts +0 -2
  71. package/src/runtime/anchor-manager.ts +473 -0
  72. package/src/runtime/async-runner.ts +8 -4
  73. package/src/runtime/auto-summarize.ts +350 -0
  74. package/src/runtime/background-runner.ts +10 -3
  75. package/src/runtime/budget-tracker.ts +354 -0
  76. package/src/runtime/chain-runner.ts +507 -0
  77. package/src/runtime/child-pi.ts +123 -35
  78. package/src/runtime/crash-recovery.ts +5 -4
  79. package/src/runtime/crew-agent-runtime.ts +1 -0
  80. package/src/runtime/custom-tools/irc-tool.ts +13 -0
  81. package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
  82. package/src/runtime/delivery-coordinator.ts +10 -3
  83. package/src/runtime/dynamic-script-runner.ts +482 -0
  84. package/src/runtime/foreground-control.ts +87 -17
  85. package/src/runtime/handoff-manager.ts +589 -0
  86. package/src/runtime/hidden-handoff.ts +424 -0
  87. package/src/runtime/live-agent-manager.ts +20 -4
  88. package/src/runtime/live-session-runtime.ts +39 -4
  89. package/src/runtime/manifest-cache.ts +2 -1
  90. package/src/runtime/model-resolver.ts +16 -4
  91. package/src/runtime/phase-tracker.ts +373 -0
  92. package/src/runtime/pi-args.ts +11 -1
  93. package/src/runtime/pi-json-output.ts +31 -0
  94. package/src/runtime/pipeline-runner.ts +514 -0
  95. package/src/runtime/progress-tracker.ts +124 -0
  96. package/src/runtime/retry-runner.ts +354 -0
  97. package/src/runtime/sandbox.ts +252 -0
  98. package/src/runtime/scheduler.ts +7 -2
  99. package/src/runtime/skill-effectiveness.ts +473 -0
  100. package/src/runtime/skill-instructions.ts +37 -3
  101. package/src/runtime/subagent-manager.ts +1 -1
  102. package/src/runtime/task-graph.ts +11 -1
  103. package/src/runtime/task-runner.ts +92 -18
  104. package/src/runtime/team-runner.ts +13 -12
  105. package/src/runtime/tool-progress.ts +10 -3
  106. package/src/runtime/verification-gates.ts +367 -0
  107. package/src/schema/team-tool-schema.ts +37 -0
  108. package/src/skills/discover-skills.ts +5 -0
  109. package/src/state/active-run-registry.ts +9 -2
  110. package/src/state/contracts.ts +9 -0
  111. package/src/state/crew-init.ts +3 -3
  112. package/src/state/decision-ledger.ts +98 -55
  113. package/src/state/event-log-rotation.ts +2 -2
  114. package/src/state/event-log.ts +144 -10
  115. package/src/state/hook-instinct-bridge.ts +5 -5
  116. package/src/state/mailbox.ts +10 -0
  117. package/src/state/run-cache.ts +18 -8
  118. package/src/state/state-store.ts +3 -1
  119. package/src/state/types.ts +4 -0
  120. package/src/tools/safe-bash-extension.ts +1 -0
  121. package/src/tools/safe-bash.ts +152 -20
  122. package/src/types/new-api-types.ts +34 -0
  123. package/src/ui/agent-management-overlay.ts +5 -1
  124. package/src/ui/crew-widget.ts +29 -15
  125. package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
  126. package/src/ui/powerbar-publisher.ts +101 -7
  127. package/src/ui/tool-render.ts +15 -15
  128. package/src/ui/transcript-cache.ts +13 -0
  129. package/src/utils/bm25-search.ts +16 -8
  130. package/src/utils/env-filter.ts +8 -5
  131. package/src/utils/redaction.ts +169 -15
  132. package/src/utils/session-utils.ts +52 -0
  133. package/src/utils/sse-parser.ts +10 -1
  134. package/src/worktree/cleanup.ts +6 -1
  135. package/src/worktree/worktree-manager.ts +32 -13
  136. package/workflows/chain.workflow.md +252 -0
  137. package/workflows/pipeline.workflow.md +27 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Role-based tool configurations for pi-crew agents.
3
+ * Uses the excludeTools option from pi v0.77.0.
4
+ */
5
+
6
+ export interface RoleToolConfig {
7
+ /** Explicit list of tools to use (if undefined, use all default tools) */
8
+ tools?: string[];
9
+ /** Tools to exclude from the default set */
10
+ excludeTools?: string[];
11
+ }
12
+
13
+ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
14
+ // Explorer - Read-only, no write or execute
15
+ explorer: {
16
+ tools: ["read", "grep", "find", "ls", "glob"],
17
+ excludeTools: ["edit", "write", "bash", "web"],
18
+ },
19
+
20
+ // Analyst - Read and analyze, limited execution
21
+ analyst: {
22
+ excludeTools: ["edit", "write", "ask_question"],
23
+ },
24
+
25
+ // Planner - Planning and documentation
26
+ planner: {
27
+ excludeTools: ["ask_question"],
28
+ },
29
+
30
+ // Executor - Full access (default)
31
+ executor: {
32
+ // No restrictions - full tool access
33
+ },
34
+
35
+ // Reviewer - Read and review, no write
36
+ reviewer: {
37
+ tools: ["read", "grep", "find", "ls", "glob", "bash"],
38
+ excludeTools: ["edit", "write"],
39
+ },
40
+
41
+ // Writer - Documentation focused
42
+ writer: {
43
+ tools: ["read", "edit", "write", "ls"],
44
+ excludeTools: ["bash", "web", "ask_question"],
45
+ },
46
+
47
+ // Security Reviewer - Strict restrictions
48
+ security_reviewer: {
49
+ tools: ["read", "grep", "find"],
50
+ excludeTools: ["edit", "write", "bash", "web", "ask_question"],
51
+ },
52
+
53
+ // Test Engineer - Can write tests
54
+ test_engineer: {
55
+ tools: ["read", "edit", "write", "bash", "ls"],
56
+ excludeTools: ["web"],
57
+ },
58
+ };
59
+
60
+ /**
61
+ * Get tool configuration for a specific role.
62
+ */
63
+ export function getToolConfig(role: string): RoleToolConfig {
64
+ return ROLE_TOOL_CONFIGS[role] ?? {};
65
+ }
66
+
67
+ /**
68
+ * Check if a role has any tool restrictions.
69
+ */
70
+ export function hasToolRestrictions(role: string): boolean {
71
+ const config = getToolConfig(role);
72
+ return (config.tools !== undefined) || (config.excludeTools !== undefined);
73
+ }
74
+
75
+ /**
76
+ * Get all restricted roles.
77
+ */
78
+ export function getRestrictedRoles(): string[] {
79
+ return Object.entries(ROLE_TOOL_CONFIGS)
80
+ .filter(([, config]) => (config.tools !== undefined) || (config.excludeTools !== undefined))
81
+ .map(([role]) => role);
82
+ }
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * Fuzzy config key suggestions — Levenshtein-based typo correction for pi-crew config keys.
3
+ *
4
+ * SECURITY NOTE: The levenshtein() function processes user-supplied input and compares
5
+ * against a known list. While the timing variance across edit distances is minimal
6
+ * and the input is typically config key names (not secrets), there is a theoretical
7
+ * timing attack risk if an attacker could measure response times for different inputs.
8
+ * Risk level: LOW — mitigated by the small alphabet (config key names only) and
9
+ * the fixed-size DP array used in this implementation.
3
10
  */
4
11
 
5
12
  /**
6
13
  * Classic Levenshtein edit distance between two strings.
14
+ * See security note above regarding timing attack considerations.
7
15
  */
8
16
  export function levenshtein(a: string, b: string): number {
9
17
  const la = a.length;
@@ -19,6 +19,8 @@ export interface PiTeamsAutonomousConfig {
19
19
  preferAsyncForLongTasks?: boolean;
20
20
  allowWorktreeSuggestion?: boolean;
21
21
  magicKeywords?: Record<string, string[]>;
22
+ /** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
23
+ excludeContextBash?: boolean;
22
24
  }
23
25
 
24
26
  export interface CrewLimitsConfig {
@@ -66,6 +68,8 @@ export interface CrewRuntimeConfig {
66
68
  /** Default runtime for roles not in isolatedRoles. Default: "live-session" (uses live-session). */
67
69
  defaultRuntime?: "live-session" | "child-process";
68
70
  };
71
+ /** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
72
+ excludeContextBash?: boolean;
69
73
  }
70
74
 
71
75
  export interface CrewControlConfig {
@@ -12,6 +12,7 @@ export interface AsyncNotifierState {
12
12
  interval?: ReturnType<typeof setInterval>;
13
13
  generation?: number;
14
14
  lastStoppedAtMs?: number;
15
+ lastListRunsMs?: number;
15
16
  }
16
17
 
17
18
  export interface AsyncNotifierOptions {
@@ -80,6 +81,8 @@ export function markDeadAsyncRunIfNeeded(run: TeamRunManifest, now = Date.now(),
80
81
  });
81
82
  }
82
83
 
84
+ const LIST_RUNS_DEBOUNCE_MS = 30_000;
85
+
83
86
  export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifierState, intervalMs = 5000, options: AsyncNotifierOptions = {}): void {
84
87
  if (state.interval) clearInterval(state.interval);
85
88
  const generation = options.generation ?? ((state.generation ?? 0) + 1);
@@ -93,10 +96,16 @@ export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifie
93
96
  const updatedAtMs = timeMs(run.updatedAt) ?? 0;
94
97
  if (isFinished(run.status) && updatedAtMs < staleBeforeMs) state.seenFinishedRunIds.add(run.runId);
95
98
  }
99
+ let cachedRuns: TeamRunManifest[] | undefined;
96
100
  state.interval = setInterval(() => {
97
101
  try {
98
102
  if (options.isCurrent && !options.isCurrent(generation)) return;
99
- for (const run of listRuns(ctx.cwd).slice(0, 20)) {
103
+ const nowMs = Date.now();
104
+ if (cachedRuns === undefined || nowMs - (state.lastListRunsMs ?? 0) > LIST_RUNS_DEBOUNCE_MS) {
105
+ cachedRuns = listRuns(ctx.cwd).slice(0, 20);
106
+ state.lastListRunsMs = nowMs;
107
+ }
108
+ for (const run of cachedRuns) {
100
109
  const current = markDeadAsyncRunIfNeeded(run) ?? run;
101
110
  if (!isFinished(current.status) || state.seenFinishedRunIds.has(current.runId)) continue;
102
111
  state.seenFinishedRunIds.add(current.runId);
@@ -0,0 +1,114 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ // NOTE: globalProgressTracker import kept for documentation but not directly used
3
+ // since we don't have agent IDs to untrack. Actual progress clearing should be
4
+ // handled by the progress tracker itself on shutdown.
5
+ // import { globalProgressTracker } from "../runtime/progress-tracker.ts";
6
+
7
+ /**
8
+ * Registers cleanup handlers for graceful shutdown.
9
+ * Handles session_shutdown and SIGTERM/SIGHUP signals.
10
+ */
11
+
12
+ interface ChildProcessInfo {
13
+ pid: number;
14
+ runId: string;
15
+ agentId: string;
16
+ startedAt: number;
17
+ }
18
+
19
+ class ChildProcessRegistry {
20
+ private processes = new Map<number, ChildProcessInfo>();
21
+
22
+ register(pid: number, runId: string, agentId: string): void {
23
+ this.processes.set(pid, { pid, runId, agentId, startedAt: Date.now() });
24
+ }
25
+
26
+ unregister(pid: number): void {
27
+ this.processes.delete(pid);
28
+ }
29
+
30
+ getAllPids(): number[] {
31
+ return Array.from(this.processes.keys());
32
+ }
33
+
34
+ getInfo(pid: number): ChildProcessInfo | undefined {
35
+ return this.processes.get(pid);
36
+ }
37
+
38
+ clear(): void {
39
+ this.processes.clear();
40
+ }
41
+ }
42
+
43
+ export const childProcessRegistry = new ChildProcessRegistry();
44
+
45
+ export function registerCleanupHandler(pi: ExtensionAPI): void {
46
+ // Handle session_shutdown event
47
+ pi.on("session_shutdown", async () => {
48
+ console.log("[pi-crew] Session shutdown - cleaning up resources");
49
+
50
+ try {
51
+ // Kill all child-pi processes
52
+ await cleanupChildProcesses();
53
+
54
+ // Cleanup temp directories
55
+ await cleanupTempDirectories();
56
+
57
+ console.log("[pi-crew] Cleanup complete");
58
+ } catch (error) {
59
+ console.error("[pi-crew] Cleanup error:", error);
60
+ }
61
+ });
62
+
63
+ // Handle SIGTERM/SIGHUP signals
64
+ const handleSignal = async (signal: string): Promise<void> => {
65
+ console.log(`[pi-crew] Received ${signal} - starting cleanup`);
66
+ await cleanupChildProcesses();
67
+ };
68
+
69
+ process.on("SIGTERM", () => { void handleSignal("SIGTERM"); });
70
+ process.on("SIGHUP", () => { void handleSignal("SIGHUP"); });
71
+ }
72
+
73
+ async function cleanupChildProcesses(): Promise<void> {
74
+ const pids = childProcessRegistry.getAllPids();
75
+
76
+ for (const pid of pids) {
77
+ try {
78
+ process.kill(pid, "SIGTERM");
79
+ console.log(`[pi-crew] Sent SIGTERM to child process ${pid}`);
80
+ } catch (error: unknown) {
81
+ // Process may already be dead or not exist
82
+ const err = error as NodeJS.ErrnoException;
83
+ if (err.code !== "ESRCH" && err.code !== "ENOENT") {
84
+ console.error(`[pi-crew] Error killing process ${pid}:`, err.message);
85
+ }
86
+ }
87
+ childProcessRegistry.unregister(pid);
88
+ }
89
+
90
+ // Clear progress tracker
91
+ // Note: Can't call untrack on all because we don't track agent IDs here
92
+ // The progress tracker should clear itself on shutdown via session_dispose
93
+ }
94
+
95
+ async function cleanupTempDirectories(): Promise<void> {
96
+ // NOTE: getTempDir is not available in paths.ts.
97
+ // For now, just log that cleanup is pending.
98
+ // Actual temp directory cleanup should be implemented by the run-graph
99
+ // or the specific code that creates temporary workspaces.
100
+ try {
101
+ console.log(`[pi-crew] Temp directory cleanup deferred to run-graph`);
102
+ } catch (error) {
103
+ console.error("[pi-crew] Temp cleanup error:", error);
104
+ }
105
+ }
106
+
107
+ // Export for child-pi.ts to register processes
108
+ export function registerChildProcess(pid: number, runId: string, agentId: string): void {
109
+ childProcessRegistry.register(pid, runId, agentId);
110
+ }
111
+
112
+ export function unregisterChildProcess(pid: number): void {
113
+ childProcessRegistry.unregister(pid);
114
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
+ import { resolveContainedPath } from "../utils/safe-paths.ts";
3
4
  // Lazy-loaded to avoid pulling team-tool.ts (and its entire runtime chain) into module load.
4
5
  import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
5
6
  let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
@@ -69,7 +70,6 @@ function isAllowedRpcRunParams(params: TeamToolParamsValue): { ok: boolean; erro
69
70
  // SECURITY: Validate cwd is within the project directory if provided.
70
71
  if (params.cwd && typeof params.cwd === "string") {
71
72
  try {
72
- const { resolveContainedPath } = require("../utils/safe-paths.ts");
73
73
  resolveContainedPath(params.cwd, ".");
74
74
  } catch {
75
75
  return { ok: false, error: "RPC run config.cwd must be within the project directory" };
@@ -55,12 +55,29 @@ export class NotificationRouter {
55
55
  private readonly seen = new Map<string, number>();
56
56
  private batch: NotificationDescriptor[] = [];
57
57
  private timer: ReturnType<typeof setTimeout> | undefined;
58
+ private seenCleanupCounter = 0;
59
+ private static readonly SEEN_MAP_MAX_SIZE = 10000;
58
60
 
59
61
  constructor(opts: NotificationRouterOptions = {}, deliver: (notification: NotificationDescriptor) => void) {
60
62
  this.opts = opts;
61
63
  this.deliver = deliver;
62
64
  }
63
65
 
66
+ /**
67
+ * Evict oldest entries from seen Map if it exceeds MAX_SIZE.
68
+ * This prevents unbounded memory growth from notifications without TTL.
69
+ */
70
+ private evictSeenIfNeeded(): void {
71
+ if (this.seen.size > NotificationRouter.SEEN_MAP_MAX_SIZE) {
72
+ // Sort by timestamp (oldest first) and keep only half
73
+ const entries = [...this.seen.entries()].sort((a, b) => a[1] - b[1]);
74
+ const keepCount = Math.floor(NotificationRouter.SEEN_MAP_MAX_SIZE / 2);
75
+ for (const [key] of entries.slice(0, entries.length - keepCount)) {
76
+ this.seen.delete(key);
77
+ }
78
+ }
79
+ }
80
+
64
81
  enqueue(notification: NotificationDescriptor): boolean {
65
82
  const now = this.opts.now?.() ?? Date.now();
66
83
  const withTime = { ...notification, timestamp: notification.timestamp ?? now };
@@ -77,6 +94,7 @@ export class NotificationRouter {
77
94
  const previous = this.seen.get(key);
78
95
  if (previous !== undefined && now - previous < dedupWindow) return false;
79
96
  this.seen.set(key, now);
97
+ this.evictSeenIfNeeded();
80
98
  const batchWindow = this.opts.batchWindowMs ?? 0;
81
99
  if (batchWindow <= 0) {
82
100
  this.deliver(withTime);
@@ -17,6 +17,7 @@ import {
17
17
  stopAsyncRunNotifier,
18
18
  } from "./async-notifier.ts";
19
19
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
20
+ import { registerCleanupHandler } from "./crew-cleanup.ts";
20
21
  import { notifyActiveRuns } from "./session-summary.ts";
21
22
 
22
23
  let _cachedLiveRunSidebar: typeof LiveRunSidebarType | undefined;
@@ -703,7 +704,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
703
704
  widgetState,
704
705
  loadConfig(currentCtx.cwd).config.ui,
705
706
  );
706
- clearPiCrewPowerbar(pi.events, currentCtx);
707
+ clearPiCrewPowerbar(pi.events);
707
708
  };
708
709
  const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
709
710
  const uiConfig = loadConfig(ctx.cwd).config.ui;
@@ -859,7 +860,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
859
860
  .then(({ stopWatchdog }) => {
860
861
  stopWatchdog(runId);
861
862
  })
862
- .catch(() => {});
863
+ .catch((error) => logInternalError("register.foreground-watchdog", error, `runId=${runId}`));
863
864
  }
864
865
  const ownerCurrent = isContextCurrent(ctx, ownerGeneration);
865
866
  if (ctx.hasUI) {
@@ -942,9 +943,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
942
943
  function getPiEvents():
943
944
  | Parameters<typeof registerPiCrewRpc>[0]
944
945
  | undefined {
945
- if (pi && typeof pi === "object" && "events" in pi)
946
- return (pi as unknown as Record<string, unknown>)
947
- .events as Parameters<typeof registerPiCrewRpc>[0];
946
+ if (pi && typeof pi === "object" && "events" in pi) {
947
+ // pi.events may not be typed in the original pi type, so cast through unknown
948
+ const events = (pi as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events;
949
+ return events;
950
+ }
948
951
  return undefined;
949
952
  }
950
953
  rpcHandle = registerPiCrewRpc(getPiEvents(), () => currentCtx);
@@ -1084,7 +1087,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1084
1087
  widgetState,
1085
1088
  currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined,
1086
1089
  );
1087
- clearPiCrewPowerbar(pi.events, currentCtx);
1090
+ clearPiCrewPowerbar(pi.events);
1088
1091
  disposePowerbarCoalescer();
1089
1092
  heartbeatWatcher?.dispose();
1090
1093
  if (autoRepairTimer) {
@@ -1496,8 +1499,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1496
1499
  manifests,
1497
1500
  );
1498
1501
  // Health notifications: only warn about genuinely running runs
1502
+ // Filter to only current session's runs to prevent cross-session notification leakage
1503
+ const currentSessionGen = sessionGeneration;
1504
+ const currentSessionId = currentCtx ? (currentCtx as { sessionId?: string }).sessionId : undefined;
1505
+ const sessionManifests = manifests.filter(
1506
+ (run) =>
1507
+ !run.ownerSessionId ||
1508
+ run.ownerSessionId === currentSessionId ||
1509
+ (run as unknown as Record<string, unknown>).ownerSessionGeneration === currentSessionGen,
1510
+ );
1499
1511
  const now = Date.now();
1500
- for (const run of manifests) {
1512
+ for (const run of sessionManifests) {
1501
1513
  if (run.status !== "running") continue;
1502
1514
  try {
1503
1515
  const snapshot = snapshotCache.get(run.runId);
@@ -1749,19 +1761,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1749
1761
  // AGENTS.md requires confirm=true for management deletes.
1750
1762
  pi.on("tool_call", async (event, ctx) => {
1751
1763
  if (event.toolName !== "team") return;
1752
- const input = (event as { input?: Record<string, unknown> }).input;
1753
- if (!input) return;
1754
- const action =
1755
- typeof input.action === "string" ? input.action : undefined;
1756
- const destructiveActions = new Set([
1757
- "delete",
1758
- "forget",
1759
- "prune",
1760
- "cleanup",
1761
- ]);
1764
+ const rawInput = event.input;
1765
+ if (!rawInput || typeof rawInput !== "object") return;
1766
+ const input = rawInput as { action?: unknown; confirm?: unknown; force?: unknown };
1767
+ const action = typeof input.action === "string" ? input.action : undefined;
1768
+ const destructiveActions = new Set(["delete", "forget", "prune", "cleanup"]);
1762
1769
  if (!action || !destructiveActions.has(action)) return;
1763
- const forceBypassesReferenceChecks =
1764
- action === "delete" && input.force === true;
1770
+ const forceBypassesReferenceChecks = action === "delete" && input.force === true;
1765
1771
  if (input.confirm === true || forceBypassesReferenceChecks) return;
1766
1772
  return {
1767
1773
  block: true,
@@ -1792,6 +1798,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1792
1798
  });
1793
1799
  time("register.tools");
1794
1800
 
1801
+ registerCleanupHandler(pi);
1802
+
1795
1803
  registerTeamCommands(pi, {
1796
1804
  startForegroundRun,
1797
1805
  abortForegroundRun,
@@ -56,7 +56,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
56
56
  async execute(_id, params, signal, onUpdate, ctx) {
57
57
  // Diagnostic: detect pre-aborted signal before spawn
58
58
  if (signal?.aborted) {
59
- logInternalError("subagent-tools.pre-aborted-signal", undefined, `params=${JSON.stringify(params).slice(0, 200)}`);
59
+ logInternalError("subagent-tools.pre-aborted-signal", undefined, `aborted=true paramsKeys=${Object.keys(params as object).join(",")}`);
60
60
  return subagentToolResult("Agent tool signal was already aborted before execution started. This usually means Pi cancelled the tool call before it ran.", { action: "agent", status: "error" }, true);
61
61
  }
62
62
  const currentRole = currentCrewRole();
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Anchor commands for team tool.
3
+ * Provides set/clear/status commands for anchor points.
4
+ */
5
+
6
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
7
+ import type { PiTeamsToolResult } from "../tool-result.ts";
8
+ import { result, type TeamContext } from "./context.ts";
9
+ import {
10
+ AnchorManager,
11
+ createAnchorManager,
12
+ AnchorNotFoundError,
13
+ NoHandoffsError,
14
+ } from "../../runtime/anchor-manager.ts";
15
+ import type { HandoffSummary } from "../../runtime/handoff-manager.ts";
16
+
17
+ // Global anchor manager instance for CLI usage
18
+ let globalAnchorManager: AnchorManager | null = null;
19
+
20
+ function getAnchorManager(): AnchorManager {
21
+ if (!globalAnchorManager) {
22
+ globalAnchorManager = createAnchorManager();
23
+ }
24
+ return globalAnchorManager;
25
+ }
26
+
27
+ /**
28
+ * Get the session ID from context or generate a default.
29
+ */
30
+ function getSessionId(ctx: TeamContext): string {
31
+ return ctx.sessionId ?? "default";
32
+ }
33
+
34
+ export function handleAnchorSet(
35
+ params: TeamToolParamsValue,
36
+ ctx: TeamContext,
37
+ ): PiTeamsToolResult {
38
+ const manager = getAnchorManager();
39
+ const sessionId = getSessionId(ctx);
40
+ const cfg = params.config ?? {};
41
+
42
+ // Parse context from config
43
+ const context: Record<string, unknown> = {};
44
+ if (cfg.context && typeof cfg.context === "object") {
45
+ Object.assign(context, cfg.context as Record<string, unknown>);
46
+ }
47
+ if (cfg.key) {
48
+ // Single key shorthand
49
+ context.key = cfg.key;
50
+ }
51
+
52
+ const anchorId = manager.setAnchor(sessionId, context);
53
+
54
+ return result(
55
+ [
56
+ `Anchor set successfully.`,
57
+ `Anchor ID: ${anchorId}`,
58
+ `Session: ${sessionId}`,
59
+ context && Object.keys(context).length > 0
60
+ ? `Context: ${JSON.stringify(context)}`
61
+ : "",
62
+ ].filter(Boolean).join("\n"),
63
+ { action: "anchor", status: "ok" },
64
+ );
65
+ }
66
+
67
+ export function handleAnchorClear(
68
+ params: TeamToolParamsValue,
69
+ ctx: TeamContext,
70
+ ): PiTeamsToolResult {
71
+ const manager = getAnchorManager();
72
+ const sessionId = getSessionId(ctx);
73
+ const cfg = params.config ?? {};
74
+
75
+ let anchorId: string | undefined;
76
+ if (cfg.anchorId) {
77
+ anchorId = cfg.anchorId as string;
78
+ }
79
+
80
+ let accumulated: HandoffSummary;
81
+ try {
82
+ if (anchorId) {
83
+ accumulated = manager.clearAnchor(anchorId);
84
+ } else {
85
+ const anchorResult = manager.clearAnchorBySession(sessionId);
86
+ if (!anchorResult) {
87
+ return result(
88
+ "No anchor found for this session.",
89
+ { action: "anchor", status: "error" },
90
+ true,
91
+ );
92
+ }
93
+ accumulated = anchorResult;
94
+ }
95
+ } catch (error) {
96
+ if (error instanceof AnchorNotFoundError) {
97
+ return result(
98
+ `Anchor not found: ${error.anchorId}`,
99
+ { action: "anchor", status: "error" },
100
+ true,
101
+ );
102
+ }
103
+ if (error instanceof NoHandoffsError) {
104
+ return result(
105
+ "No handoffs have been accumulated to this anchor.",
106
+ { action: "anchor", status: "error" },
107
+ true,
108
+ );
109
+ }
110
+ throw error;
111
+ }
112
+
113
+ return result(
114
+ [
115
+ `Anchor cleared successfully.`,
116
+ `Accumulated summary:`,
117
+ ``,
118
+ `Task: ${accumulated.task}`,
119
+ `Outcome: ${accumulated.outcome}`,
120
+ ``,
121
+ `Metrics:`,
122
+ ` Tokens: ${accumulated.metrics.tokensUsed}`,
123
+ ` Duration: ${Math.round(accumulated.metrics.duration / 1000)}s`,
124
+ ` Iterations: ${accumulated.metrics.iterations}`,
125
+ ` Tools: ${accumulated.metrics.toolsUsed.join(", ") || "(none)"}`,
126
+ ``,
127
+ `Files created: ${accumulated.filesCreated.join(", ") || "(none)"}`,
128
+ `Files modified: ${accumulated.filesModified.join(", ") || "(none)"}`,
129
+ `Files deleted: ${accumulated.filesDeleted.join(", ") || "(none)"}`,
130
+ accumulated.decisions.length > 0
131
+ ? `\nDecisions:\n${accumulated.decisions.map((d: { rationale: string; outcome: string }) => ` - ${d.rationale}: ${d.outcome}`).join("\n")}`
132
+ : "",
133
+ accumulated.blockers.length > 0
134
+ ? `\nBlockers: ${accumulated.blockers.join("; ")}`
135
+ : "",
136
+ accumulated.nextSteps.length > 0
137
+ ? `\nNext steps: ${accumulated.nextSteps.join("; ")}`
138
+ : "",
139
+ ].filter(Boolean).join("\n"),
140
+ { action: "anchor", status: "ok" },
141
+ );
142
+ }
143
+
144
+ export function handleAnchorStatus(
145
+ params: TeamToolParamsValue,
146
+ ctx: TeamContext,
147
+ ): PiTeamsToolResult {
148
+ const manager = getAnchorManager();
149
+ const sessionId = getSessionId(ctx);
150
+ const cfg = params.config ?? {};
151
+
152
+ let anchorId: string | undefined;
153
+ if (cfg.anchorId) {
154
+ anchorId = cfg.anchorId as string;
155
+ }
156
+
157
+ let status;
158
+ if (anchorId) {
159
+ status = manager.getAnchorStatus(anchorId);
160
+ } else {
161
+ status = manager.getAnchorStatusBySession(sessionId);
162
+ }
163
+
164
+ if (!status) {
165
+ return result(
166
+ anchorId
167
+ ? `No anchor found with ID: ${anchorId}`
168
+ : `No anchor set for session: ${sessionId}`,
169
+ { action: "anchor", status: "ok" },
170
+ );
171
+ }
172
+
173
+ return result(
174
+ [
175
+ `Anchor Status`,
176
+ `─────────────`,
177
+ `Anchor ID: ${status.anchorId}`,
178
+ `Session ID: ${status.sessionId}`,
179
+ `Created: ${new Date(status.createdAt).toISOString()}`,
180
+ `Handoffs: ${status.handoffCount}`,
181
+ `Total tokens: ${status.totalTokens}`,
182
+ `Total duration: ${Math.round(status.totalDuration / 1000)}s`,
183
+ status.context && Object.keys(status.context).length > 0
184
+ ? `\nContext: ${JSON.stringify(status.context, null, 2)}`
185
+ : "",
186
+ ].filter(Boolean).join("\n"),
187
+ { action: "anchor", status: "ok" },
188
+ );
189
+ }
190
+
191
+ export function handleAnchorAccumulate(
192
+ params: TeamToolParamsValue,
193
+ ctx: TeamContext,
194
+ ): PiTeamsToolResult {
195
+ // This would be used to manually accumulate a handoff to the current anchor
196
+ // In practice, this is called internally by HandoffManager when anchor is set
197
+ return result(
198
+ "Use handleAnchorSet to set an anchor, then run tasks normally. Handoffs will be accumulated automatically.",
199
+ { action: "anchor", status: "ok" },
200
+ );
201
+ }
@@ -18,6 +18,7 @@ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "..
18
18
  import { followUpLiveAgent, getLiveAgent, listActiveLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
19
19
  import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
20
20
  import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
21
+ import { logInternalError } from "../../utils/internal-error.ts";
21
22
  import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
22
23
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
23
24
  import type { PiTeamsToolResult } from "../tool-result.ts";
@@ -125,7 +126,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
125
126
  saveRunTasks(manifest, tasks);
126
127
  appendEvent(manifest.eventsPath, { type: "plan.cancelled", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan was cancelled.", metadata: { provenance: "api" } });
127
128
  manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
128
- void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch(() => {});
129
+ void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch((error) => logInternalError("team-tool.cancel-plan.terminate", error, `runId=${manifest.runId}`));
129
130
  return result(JSON.stringify({ planApproval: manifest.planApproval, cancelledTasks: tasks.filter((task) => task.status === "cancelled").map((task) => task.id) }, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
130
131
  });
131
132
  } catch (error) {