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,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) {
@@ -22,7 +24,7 @@ async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Pro
22
24
  return _cachedExecuteTeamRun(...args);
23
25
  }
24
26
  import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
25
- import { appendEvent, readEvents } from "../../state/event-log.ts";
27
+ import { appendEvent, appendEventAsync, readEvents } from "../../state/event-log.ts";
26
28
  import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtime-resolver.ts";
27
29
  import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
28
30
  import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
@@ -67,7 +69,7 @@ function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: numbe
67
69
  const tail = tailFile(logPath);
68
70
  const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
69
71
  const failed = updateRunStatus(loaded.manifest, "failed", "Background runner exited within 3s; see background.log");
70
- appendEvent(failed.eventsPath, { type: "async.failed", runId: failed.runId, message, data: { pid, detail: liveness.detail } });
72
+ void appendEventAsync(failed.eventsPath, { type: "async.failed", runId: failed.runId, message, data: { pid, detail: liveness.detail } });
71
73
  }, 3000);
72
74
  timer.unref();
73
75
  }
@@ -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
- appendEvent(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } });
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,13 +185,13 @@ 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
- appendEvent(effectiveManifest.eventsPath, { type: "runtime.resolved", runId: effectiveManifest.runId, message: `Runtime overridden: child-process (async fallback from live-session)`, data: { runtimeResolution: effectiveRuntimeResolution } });
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") {
157
192
  const runningManifest = updateRunStatus(effectiveManifest, "running", "Checking worker runtime availability.");
158
193
  const blocked = updateRunStatus(runningManifest, "blocked", effectiveRuntime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
159
- appendEvent(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime: effectiveRuntime, runtimeResolution: effectiveRuntimeResolution, async: true, diagnostics: { requestedMode: effectiveRuntime.requestedMode, workersDisabled: executedConfig.executeWorkers === false, envCrew: process.env.PI_CREW_EXECUTE_WORKERS, envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS } } });
194
+ void appendEventAsync(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime: effectiveRuntime, runtimeResolution: effectiveRuntimeResolution, async: true, diagnostics: { requestedMode: effectiveRuntime.requestedMode, workersDisabled: executedConfig.executeWorkers === false, envCrew: process.env.PI_CREW_EXECUTE_WORKERS, envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS } } });
160
195
  unregisterActiveRun(blocked.runId);
161
196
  return result([
162
197
  `Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
@@ -169,7 +204,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
169
204
  const spawned = await spawnBackgroundTeamRun(effectiveManifest);
170
205
  const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
171
206
  atomicWriteJson(paths.manifestPath, asyncManifest);
172
- appendEvent(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
207
+ void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
173
208
  scheduleBackgroundEarlyExitGuard(ctx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
174
209
  // Wait for the async run to complete and return actual results.
175
210
  try {
@@ -280,7 +315,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
280
315
  if (runtime.safety === "blocked") {
281
316
  const runningManifest = updateRunStatus(executionManifest, "running", "Checking worker runtime availability.");
282
317
  const blocked = updateRunStatus(runningManifest, "blocked", runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
283
- appendEvent(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime, runtimeResolution, diagnostics: { requestedMode: runtime.requestedMode, workersDisabled: executedConfig.executeWorkers === false, envCrew: process.env.PI_CREW_EXECUTE_WORKERS, envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS } } });
318
+ void appendEventAsync(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime, runtimeResolution, diagnostics: { requestedMode: runtime.requestedMode, workersDisabled: executedConfig.executeWorkers === false, envCrew: process.env.PI_CREW_EXECUTE_WORKERS, envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS } } });
284
319
  unregisterActiveRun(blocked.runId);
285
320
  return result([
286
321
  `Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
@@ -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[] = [];
@@ -0,0 +1,69 @@
1
+ import type { AgentProgress } from "../runtime/progress-tracker.ts";
2
+
3
+ export type CrewEventType =
4
+ | "agent:progress"
5
+ | "agent:complete"
6
+ | "agent:error"
7
+ | "run:start"
8
+ | "run:complete";
9
+
10
+ export interface CrewEvent {
11
+ type: CrewEventType;
12
+ runId: string;
13
+ agentId?: string;
14
+ payload?: AgentProgress | string;
15
+ timestamp: number;
16
+ }
17
+
18
+ type CrewEventListener = (event: CrewEvent) => void;
19
+
20
+ class EventBus {
21
+ private listeners = new Map<CrewEventType, Set<CrewEventListener>>();
22
+ private static _instance?: EventBus;
23
+
24
+ static getInstance(): EventBus {
25
+ if (!EventBus._instance) {
26
+ EventBus._instance = new EventBus();
27
+ }
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;
38
+ }
39
+
40
+ emit(event: CrewEvent): void {
41
+ const listeners = this.listeners.get(event.type);
42
+ if (listeners) {
43
+ for (const listener of listeners) {
44
+ try {
45
+ listener(event);
46
+ } catch (e) {
47
+ console.error("[EventBus] Listener error:", e);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ on(type: CrewEventType, listener: CrewEventListener): () => void {
54
+ if (!this.listeners.has(type)) {
55
+ this.listeners.set(type, new Set());
56
+ }
57
+ this.listeners.get(type)!.add(listener);
58
+
59
+ return () => {
60
+ this.listeners.get(type)?.delete(listener);
61
+ };
62
+ }
63
+
64
+ off(type: CrewEventType, listener: CrewEventListener): void {
65
+ this.listeners.get(type)?.delete(listener);
66
+ }
67
+ }
68
+
69
+ export const crewEventBus = EventBus.getInstance();
@@ -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" })],