pi-crew 0.5.5 → 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 (72) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +17 -1
  3. package/docs/architecture.md +2 -0
  4. package/docs/migration-v0.4-v0.5.md +19 -2
  5. package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
  6. package/package.json +7 -5
  7. package/src/benchmark/benchmark-runner.ts +45 -0
  8. package/src/benchmark/feedback-loop.ts +5 -0
  9. package/src/config/config.ts +10 -0
  10. package/src/config/suggestions.ts +8 -0
  11. package/src/extension/async-notifier.ts +10 -1
  12. package/src/extension/cross-extension-rpc.ts +1 -1
  13. package/src/extension/notification-router.ts +18 -0
  14. package/src/extension/register.ts +13 -17
  15. package/src/extension/registration/subagent-tools.ts +1 -1
  16. package/src/extension/team-tool/anchor.ts +201 -0
  17. package/src/extension/team-tool/api.ts +2 -1
  18. package/src/extension/team-tool/auto-summarize.ts +154 -0
  19. package/src/extension/team-tool/run.ts +37 -2
  20. package/src/extension/team-tool.ts +44 -2
  21. package/src/hooks/registry.ts +1 -3
  22. package/src/observability/event-bus.ts +13 -4
  23. package/src/observability/event-to-metric.ts +0 -2
  24. package/src/runtime/anchor-manager.ts +473 -0
  25. package/src/runtime/async-runner.ts +8 -4
  26. package/src/runtime/auto-summarize.ts +350 -0
  27. package/src/runtime/background-runner.ts +2 -1
  28. package/src/runtime/budget-tracker.ts +354 -0
  29. package/src/runtime/chain-runner.ts +507 -0
  30. package/src/runtime/child-pi.ts +1 -1
  31. package/src/runtime/crash-recovery.ts +5 -4
  32. package/src/runtime/custom-tools/irc-tool.ts +13 -0
  33. package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
  34. package/src/runtime/delivery-coordinator.ts +10 -3
  35. package/src/runtime/dynamic-script-runner.ts +482 -0
  36. package/src/runtime/handoff-manager.ts +589 -0
  37. package/src/runtime/hidden-handoff.ts +424 -0
  38. package/src/runtime/live-agent-manager.ts +20 -4
  39. package/src/runtime/live-session-runtime.ts +39 -4
  40. package/src/runtime/manifest-cache.ts +2 -1
  41. package/src/runtime/model-resolver.ts +16 -4
  42. package/src/runtime/phase-tracker.ts +373 -0
  43. package/src/runtime/pipeline-runner.ts +514 -0
  44. package/src/runtime/retry-runner.ts +354 -0
  45. package/src/runtime/sandbox.ts +252 -0
  46. package/src/runtime/scheduler.ts +7 -2
  47. package/src/runtime/subagent-manager.ts +1 -1
  48. package/src/runtime/task-graph.ts +11 -1
  49. package/src/runtime/task-runner.ts +1 -1
  50. package/src/runtime/team-runner.ts +4 -3
  51. package/src/schema/team-tool-schema.ts +30 -0
  52. package/src/skills/discover-skills.ts +5 -0
  53. package/src/state/active-run-registry.ts +9 -2
  54. package/src/state/contracts.ts +9 -0
  55. package/src/state/crew-init.ts +3 -3
  56. package/src/state/decision-ledger.ts +26 -32
  57. package/src/state/event-log-rotation.ts +2 -2
  58. package/src/state/event-log.ts +9 -1
  59. package/src/state/mailbox.ts +10 -0
  60. package/src/state/run-cache.ts +18 -8
  61. package/src/tools/safe-bash-extension.ts +1 -0
  62. package/src/tools/safe-bash.ts +152 -20
  63. package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
  64. package/src/ui/powerbar-publisher.ts +1 -0
  65. package/src/ui/transcript-cache.ts +13 -0
  66. package/src/utils/bm25-search.ts +16 -8
  67. package/src/utils/env-filter.ts +8 -5
  68. package/src/utils/redaction.ts +169 -15
  69. package/src/utils/sse-parser.ts +10 -1
  70. package/src/worktree/cleanup.ts +6 -1
  71. package/workflows/chain.workflow.md +252 -0
  72. package/workflows/pipeline.workflow.md +27 -0
@@ -860,7 +860,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
860
860
  .then(({ stopWatchdog }) => {
861
861
  stopWatchdog(runId);
862
862
  })
863
- .catch(() => {});
863
+ .catch((error) => logInternalError("register.foreground-watchdog", error, `runId=${runId}`));
864
864
  }
865
865
  const ownerCurrent = isContextCurrent(ctx, ownerGeneration);
866
866
  if (ctx.hasUI) {
@@ -943,9 +943,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
943
943
  function getPiEvents():
944
944
  | Parameters<typeof registerPiCrewRpc>[0]
945
945
  | undefined {
946
- if (pi && typeof pi === "object" && "events" in pi)
947
- return (pi as unknown as Record<string, unknown>)
948
- .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
+ }
949
951
  return undefined;
950
952
  }
951
953
  rpcHandle = registerPiCrewRpc(getPiEvents(), () => currentCtx);
@@ -1499,7 +1501,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1499
1501
  // Health notifications: only warn about genuinely running runs
1500
1502
  // Filter to only current session's runs to prevent cross-session notification leakage
1501
1503
  const currentSessionGen = sessionGeneration;
1502
- const currentSessionId = currentCtx ? (currentCtx as unknown as Record<string, unknown>).sessionId as string | undefined : undefined;
1504
+ const currentSessionId = currentCtx ? (currentCtx as { sessionId?: string }).sessionId : undefined;
1503
1505
  const sessionManifests = manifests.filter(
1504
1506
  (run) =>
1505
1507
  !run.ownerSessionId ||
@@ -1759,19 +1761,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1759
1761
  // AGENTS.md requires confirm=true for management deletes.
1760
1762
  pi.on("tool_call", async (event, ctx) => {
1761
1763
  if (event.toolName !== "team") return;
1762
- const input = (event as { input?: Record<string, unknown> }).input;
1763
- if (!input) return;
1764
- const action =
1765
- typeof input.action === "string" ? input.action : undefined;
1766
- const destructiveActions = new Set([
1767
- "delete",
1768
- "forget",
1769
- "prune",
1770
- "cleanup",
1771
- ]);
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"]);
1772
1769
  if (!action || !destructiveActions.has(action)) return;
1773
- const forceBypassesReferenceChecks =
1774
- action === "delete" && input.force === true;
1770
+ const forceBypassesReferenceChecks = action === "delete" && input.force === true;
1775
1771
  if (input.confirm === true || forceBypassesReferenceChecks) return;
1776
1772
  return {
1777
1773
  block: true,
@@ -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) {
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Auto-summarize commands for team tool.
3
+ * Provides on/off/status commands for auto-summarization.
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
+ AutoSummarizeService,
11
+ createAutoSummarizeService,
12
+ DEFAULT_AUTO_SUMMARIZE_CONFIG,
13
+ } from "../../runtime/auto-summarize.ts";
14
+
15
+ // Global auto-summarize service instance for CLI usage
16
+ let globalAutoSummarize: AutoSummarizeService | null = null;
17
+
18
+ function getAutoSummarize(): AutoSummarizeService {
19
+ if (!globalAutoSummarize) {
20
+ globalAutoSummarize = createAutoSummarizeService();
21
+ }
22
+ return globalAutoSummarize;
23
+ }
24
+
25
+ export function handleAutoSummarizeOn(
26
+ params: TeamToolParamsValue,
27
+ ctx: TeamContext,
28
+ ): PiTeamsToolResult {
29
+ const service = getAutoSummarize();
30
+ const cfg = params.config ?? {};
31
+
32
+ // Apply config updates if provided
33
+ if (cfg.threshold !== undefined) {
34
+ const threshold = typeof cfg.threshold === "number" ? cfg.threshold : parseInt(String(cfg.threshold), 10);
35
+ if (!isNaN(threshold) && threshold >= 0) {
36
+ service.setThreshold(threshold);
37
+ }
38
+ }
39
+
40
+ if (cfg.minTools !== undefined) {
41
+ const minTools = typeof cfg.minTools === "number" ? cfg.minTools : parseInt(String(cfg.minTools), 10);
42
+ if (!isNaN(minTools) && minTools >= 0) {
43
+ service.setMinToolsUsed(minTools);
44
+ }
45
+ }
46
+
47
+ const previousState = service.isEnabled();
48
+ service.enable();
49
+ const config = service.getConfig();
50
+
51
+ return result(
52
+ [
53
+ `Auto-summarize enabled.`,
54
+ ``,
55
+ `Configuration:`,
56
+ ` Token threshold: ${config.threshold}`,
57
+ ` Min tools: ${config.minToolsUsed}`,
58
+ ` Collapse context: ${config.collapseContext}`,
59
+ ].join("\n"),
60
+ { action: "auto-summarize", status: "ok" },
61
+ );
62
+ }
63
+
64
+ export function handleAutoSummarizeOff(
65
+ params: TeamToolParamsValue,
66
+ ctx: TeamContext,
67
+ ): PiTeamsToolResult {
68
+ const service = getAutoSummarize();
69
+
70
+ service.disable();
71
+
72
+ return result(
73
+ "Auto-summarize disabled.",
74
+ { action: "auto-summarize", status: "ok" },
75
+ );
76
+ }
77
+
78
+ export function handleAutoSummarizeStatus(
79
+ params: TeamToolParamsValue,
80
+ ctx: TeamContext,
81
+ ): PiTeamsToolResult {
82
+ const service = getAutoSummarize();
83
+ const config = service.getConfig();
84
+ const isEnabled = service.isEnabled();
85
+
86
+ return result(
87
+ [
88
+ `Auto-summarize Status`,
89
+ `──────────────────`,
90
+ `Enabled: ${isEnabled ? "Yes" : "No"}`,
91
+ ``,
92
+ `Configuration:`,
93
+ ` Token threshold: ${config.threshold} (default: ${DEFAULT_AUTO_SUMMARIZE_CONFIG.threshold})`,
94
+ ` Min tools used: ${config.minToolsUsed} (default: ${DEFAULT_AUTO_SUMMARIZE_CONFIG.minToolsUsed})`,
95
+ ` Collapse context: ${config.collapseContext ? "Yes" : "No"} (default: ${DEFAULT_AUTO_SUMMARIZE_CONFIG.collapseContext ? "Yes" : "No"})`,
96
+ ``,
97
+ `Triggers:`,
98
+ ` - Token count >= ${config.threshold}`,
99
+ ` - Tool count >= ${config.minToolsUsed}`,
100
+ ` - High token-to-tool ratio (>1000 tokens/tool with 3+ tools)`,
101
+ ].join("\n"),
102
+ { action: "auto-summarize", status: "ok" },
103
+ );
104
+ }
105
+
106
+ export function handleAutoSummarizeConfig(
107
+ params: TeamToolParamsValue,
108
+ ctx: TeamContext,
109
+ ): PiTeamsToolResult {
110
+ const service = getAutoSummarize();
111
+ const cfg = params.config ?? {};
112
+
113
+ // Parse config options
114
+ const updates: { threshold?: number; minTools?: number; collapseContext?: boolean } = {};
115
+
116
+ if (cfg.threshold !== undefined) {
117
+ const threshold = typeof cfg.threshold === "number" ? cfg.threshold : parseInt(String(cfg.threshold), 10);
118
+ if (!isNaN(threshold) && threshold >= 0) {
119
+ updates.threshold = threshold;
120
+ }
121
+ }
122
+
123
+ if (cfg.minTools !== undefined) {
124
+ const minTools = typeof cfg.minTools === "number" ? cfg.minTools : parseInt(String(cfg.minTools), 10);
125
+ if (!isNaN(minTools) && minTools >= 0) {
126
+ updates.minTools = minTools;
127
+ }
128
+ }
129
+
130
+ if (cfg.collapseContext !== undefined) {
131
+ updates.collapseContext = Boolean(cfg.collapseContext);
132
+ }
133
+
134
+ if (Object.keys(updates).length > 0) {
135
+ service.updateConfig(updates);
136
+ }
137
+
138
+ const config = service.getConfig();
139
+
140
+ return result(
141
+ [
142
+ `Auto-summarize configuration updated.`,
143
+ ``,
144
+ `Current settings:`,
145
+ ` Token threshold: ${config.threshold}`,
146
+ ` Min tools used: ${config.minToolsUsed}`,
147
+ ` Collapse context: ${config.collapseContext ? "Yes" : "No"}`,
148
+ ` Enabled: ${config.enabled ? "Yes" : "No"}`,
149
+ ].join("\n"),
150
+ { action: "auto-summarize", status: "ok" },
151
+ );
152
+ }
153
+ // Re-export for team-tool.ts
154
+ export { createAutoSummarizeService } from "../../runtime/auto-summarize.ts";
@@ -8,10 +8,12 @@ import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-r
8
8
  import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
9
9
  import { atomicWriteJson } from "../../state/atomic-write.ts";
10
10
  import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
11
+ import { PipelineRunner, type PipelineWorkflow, type PipelineStage } from "../../runtime/pipeline-runner.ts";
11
12
  // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
12
13
  import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
13
14
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
14
15
  const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
16
+ import { logInternalError } from "../../utils/internal-error.ts";
15
17
  let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
16
18
  async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
17
19
  if (!_cachedExecuteTeamRun) {
@@ -110,6 +112,39 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
110
112
  if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
111
113
  const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
112
114
 
115
+ // Check if this is a pipeline workflow - special handling for multi-stage execution
116
+ const isPipelineWorkflow = workflowName === "pipeline" && !directAgent;
117
+ if (isPipelineWorkflow) {
118
+ // For pipeline workflows, use PipelineRunner for execution
119
+ const pipelineRunner = new PipelineRunner();
120
+ const pipelineWorkflow: PipelineWorkflow = {
121
+ name: workflow.name,
122
+ description: workflow.description,
123
+ goal,
124
+ stages: workflow.steps.map((step) => ({
125
+ name: step.id,
126
+ team: step.role,
127
+ inputs: step.task,
128
+ usePreviousResults: step.dependsOn && step.dependsOn.length > 0,
129
+ })),
130
+ stopOnError: true,
131
+ defaultMaxConcurrency: workflow.maxConcurrency ?? 5,
132
+ };
133
+
134
+ // For now, show pipeline workflow info - full integration would require
135
+ // connecting PipelineRunner to the actual team execution system
136
+ const stageInfo = pipelineWorkflow.stages.map((s) => `- ${s.name} (${s.team})`).join("\n");
137
+ return result([
138
+ `Pipeline workflow: ${workflow.name}`,
139
+ `Goal: ${goal}`,
140
+ `Stages (${pipelineWorkflow.stages.length}):`,
141
+ stageInfo,
142
+ "",
143
+ "Pipeline execution is available via the PipelineRunner API.",
144
+ "Full CLI integration requires connecting to the team execution system.",
145
+ ].join("\n"), { action: "run", status: "ok" }, false);
146
+ }
147
+
113
148
  const validationErrors = validateWorkflowForTeam(workflow, team);
114
149
  if (validationErrors.length > 0) {
115
150
  return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...validationErrors.map((error) => `- ${error}`)].join("\n"), { action: "run", status: "error" }, true);
@@ -140,7 +175,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
140
175
  const runtimeResolution = runtimeResolutionState(runtime);
141
176
  const executionManifest = { ...updatedManifest, runtimeResolution, runConfig: executedConfig, updatedAt: new Date().toISOString() };
142
177
  atomicWriteJson(paths.manifestPath, executionManifest);
143
- appendEventAsync(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } }).catch(() => {});
178
+ appendEventAsync(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } }).catch((error) => logInternalError("team-tool.run.resolved", error, `runId=${executionManifest.runId}`));
144
179
  const runAsync = params.async ?? executedConfig.asyncByDefault ?? false;
145
180
  let effectiveRuntime = runtime;
146
181
  if (runAsync && runtime.kind === "live-session") {
@@ -150,7 +185,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
150
185
  const effectiveManifest = effectiveRuntime !== runtime ? { ...executionManifest, runtimeResolution: effectiveRuntimeResolution, updatedAt: new Date().toISOString() } : executionManifest;
151
186
  if (effectiveRuntime !== runtime) {
152
187
  atomicWriteJson(paths.manifestPath, effectiveManifest);
153
- appendEventAsync(effectiveManifest.eventsPath, { type: "runtime.resolved", runId: effectiveManifest.runId, message: `Runtime overridden: child-process (async fallback from live-session)`, data: { runtimeResolution: effectiveRuntimeResolution } }).catch(() => {});
188
+ appendEventAsync(effectiveManifest.eventsPath, { type: "runtime.resolved", runId: effectiveManifest.runId, message: `Runtime overridden: child-process (async fallback from live-session)`, data: { runtimeResolution: effectiveRuntimeResolution } }).catch((error) => logInternalError("team-tool.run.override", error, `runId=${effectiveManifest.runId}`));
154
189
  }
155
190
  if (runAsync) {
156
191
  if (effectiveRuntime.safety === "blocked") {
@@ -170,6 +170,8 @@ import { handlePlan } from "./team-tool/plan.ts";
170
170
  import { handleOrchestrate } from "./team-tool/orchestrate.ts";
171
171
  import { handleRespond } from "./team-tool/respond.ts";
172
172
  import { handleStatus } from "./team-tool/status.ts";
173
+ import { handleAnchorSet, handleAnchorClear, handleAnchorStatus, handleAnchorAccumulate } from "./team-tool/anchor.ts";
174
+ import { handleAutoSummarizeOn, handleAutoSummarizeOff, handleAutoSummarizeStatus, handleAutoSummarizeConfig, createAutoSummarizeService } from "./team-tool/auto-summarize.ts";
173
175
 
174
176
  export { handleApi } from "./team-tool/api.ts";
175
177
  export { handleRetry } from "./team-tool/cancel.ts";
@@ -715,7 +717,12 @@ export function handleSteer(
715
717
  true,
716
718
  );
717
719
  if (!task.pendingSteers) task.pendingSteers = [];
718
- task.pendingSteers.push(message);
720
+ // HIGH-04: Cap pendingSteers array to prevent unbounded memory growth
721
+ const MAX_PENDING_STEERS = 100;
722
+ if (task.pendingSteers.length >= MAX_PENDING_STEERS) {
723
+ task.pendingSteers = task.pendingSteers.slice(-(MAX_PENDING_STEERS - 1));
724
+ }
725
+ task.pendingSteers.push(message);
719
726
  saveRunTasks(loaded.manifest, loaded.tasks);
720
727
  appendEvent(loaded.manifest.eventsPath, {
721
728
  type: "task.steer_queued",
@@ -871,7 +878,7 @@ export async function handleTeamTool(
871
878
  ctx: TeamContext,
872
879
  ): Promise<PiTeamsToolResult> {
873
880
  const action = params.action ?? "list";
874
- switch (action) {
881
+ switch (action as string) {
875
882
  case "list":
876
883
  return handleList(params, ctx);
877
884
  case "get":
@@ -1157,6 +1164,41 @@ export async function handleTeamTool(
1157
1164
  return handleSchedule(params, ctx);
1158
1165
  case "scheduled":
1159
1166
  return handleListScheduled(params, ctx);
1167
+ case "anchor": {
1168
+ const subAction = typeof params.config?.subAction === "string" ? params.config.subAction : "status";
1169
+ switch (subAction) {
1170
+ case "set":
1171
+ return handleAnchorSet(params, ctx);
1172
+ case "clear":
1173
+ return handleAnchorClear(params, ctx);
1174
+ case "accumulate":
1175
+ return handleAnchorAccumulate(params, ctx);
1176
+ default:
1177
+ return handleAnchorStatus(params, ctx);
1178
+ }
1179
+ }
1180
+ case "auto-summarize":
1181
+ case "auto_boomerang": {
1182
+ const subAction = typeof params.config?.subAction === "string" ? params.config.subAction : ((params.action as string) === "auto_boomerang" ? "toggle" : "status");
1183
+ switch (subAction) {
1184
+ case "on":
1185
+ return handleAutoSummarizeOn(params, ctx);
1186
+ case "off":
1187
+ return handleAutoSummarizeOff(params, ctx);
1188
+ case "config":
1189
+ return handleAutoSummarizeConfig(params, ctx);
1190
+ case "toggle": {
1191
+ const service = createAutoSummarizeService();
1192
+ service.toggle();
1193
+ return result(
1194
+ `Auto-summarize ${service.isEnabled() ? "enabled" : "disabled"}.`,
1195
+ { action: "auto-summarize", status: "ok" },
1196
+ );
1197
+ }
1198
+ default:
1199
+ return handleAutoSummarizeStatus(params, ctx);
1200
+ }
1201
+ }
1160
1202
  case "onboard": {
1161
1203
  const team = params.team ?? "default";
1162
1204
  const onboarding = buildTeamOnboarding(team, ctx.cwd);
@@ -30,9 +30,7 @@ export async function executeHook(name: HookName, ctx: HookContext): Promise<Hoo
30
30
  // SECURITY: If ctx contains a workspaceId, filter hooks to only those scoped to
31
31
  // this workspace. This prevents globally-registered hooks from operating on runs
32
32
  // they weren't designed for.
33
- const scopedHooks = ctx.workspaceId
34
- ? hooks.filter((h) => !h.workspaceId || h.workspaceId === ctx.workspaceId)
35
- : hooks;
33
+ const scopedHooks = hooks.filter((h) => !h.workspaceId || h.workspaceId === ctx.workspaceId);
36
34
  if (scopedHooks.length === 0) return { hookName: name, outcome: "allow", durationMs: 0 };
37
35
  const start = Date.now();
38
36
  const diagnostics: string[] = [];
@@ -19,13 +19,22 @@ type CrewEventListener = (event: CrewEvent) => void;
19
19
 
20
20
  class EventBus {
21
21
  private listeners = new Map<CrewEventType, Set<CrewEventListener>>();
22
- private static instance?: EventBus;
22
+ private static _instance?: EventBus;
23
23
 
24
24
  static getInstance(): EventBus {
25
- if (!EventBus.instance) {
26
- EventBus.instance = new EventBus();
25
+ if (!EventBus._instance) {
26
+ EventBus._instance = new EventBus();
27
27
  }
28
- return EventBus.instance;
28
+ return EventBus._instance;
29
+ }
30
+
31
+ /**
32
+ * Dispose of the EventBus instance and clear all listeners.
33
+ * Resets the singleton so a new instance can be created.
34
+ */
35
+ dispose(): void {
36
+ this.listeners.clear();
37
+ EventBus._instance = undefined;
29
38
  }
30
39
 
31
40
  emit(event: CrewEvent): void {
@@ -32,7 +32,6 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
32
32
  const retryAttemptCount = registry.counter("crew.task.retry_attempt_total", "Retry attempts by run and task");
33
33
  const deadletterCount = registry.counter("crew.task.deadletter_total", "Deadletter triggers by reason");
34
34
  const overflowCount = registry.counter("crew.task.overflow_phase_total", "Overflow recovery phase transitions");
35
- const waitingCount = registry.counter("crew.task.waiting_total", "Tasks entering waiting state");
36
35
  const supervisorContactCount = registry.counter("crew.task.supervisor_contact_total", "Supervisor contact requests by reason");
37
36
  registry.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds");
38
37
  const runDuration = registry.histogram("crew.run.duration_ms", "Run end-to-end duration, milliseconds", [1000, 5000, 15000, 30000, 60000, 300000, 600000, 1800000]);
@@ -50,7 +49,6 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
50
49
  ["crew.task.retry_attempt", (data) => { const item = recordValue(data); taskCount.inc({ status: "retry" }); retryAttemptCount.inc({ runId: stringValue(item.runId, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
51
50
  ["crew.task.deadletter", (data) => { const item = recordValue(data); deadletterCount.inc({ reason: stringValue(item.reason, "unknown") }); }],
52
51
  ["crew.task.overflow", (data) => { const item = recordValue(data); overflowCount.inc({ phase: stringValue(item.phase, "unknown"), previous_phase: stringValue(item.previousPhase, "none") }); }],
53
- ["task.waiting", (data) => { const item = recordValue(data); waitingCount.inc({ taskId: stringValue(item.taskId, "unknown"), runId: stringValue(item.runId, "unknown") }); }],
54
52
  ["supervisor.contact", (data) => { const item = recordValue(data); supervisorContactCount.inc({ reason: stringValue(item.reason, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
55
53
  ["crew.subagent.completed", (data) => { const item = recordValue(data); subagentCount.inc({ status: stringValue(item.status, "completed") }); }],
56
54
  ["crew.subagent.failed", () => subagentCount.inc({ status: "failed" })],