pi-crew 0.1.45 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -5
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/next-upgrade-roadmap.md +808 -0
  14. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  15. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  16. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  17. package/docs/research/AUDIT_PI_CREW.md +457 -0
  18. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  19. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  20. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  21. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  22. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  23. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  24. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  25. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  26. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  27. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  28. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  29. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  30. package/docs/research-oh-my-pi-distillation.md +369 -0
  31. package/docs/source-runtime-refactor-map.md +24 -0
  32. package/docs/usage.md +3 -3
  33. package/install.mjs +52 -8
  34. package/package.json +99 -98
  35. package/schema.json +10 -1
  36. package/skills/async-worker-recovery/SKILL.md +42 -0
  37. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  38. package/skills/delegation-patterns/SKILL.md +54 -0
  39. package/skills/mailbox-interactive/SKILL.md +40 -0
  40. package/skills/model-routing-context/SKILL.md +39 -0
  41. package/skills/multi-perspective-review/SKILL.md +58 -0
  42. package/skills/observability-reliability/SKILL.md +41 -0
  43. package/skills/orchestration/SKILL.md +157 -0
  44. package/skills/ownership-session-security/SKILL.md +41 -0
  45. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  46. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  47. package/skills/resource-discovery-config/SKILL.md +41 -0
  48. package/skills/runtime-state-reader/SKILL.md +44 -0
  49. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  50. package/skills/state-mutation-locking/SKILL.md +42 -0
  51. package/skills/systematic-debugging/SKILL.md +67 -0
  52. package/skills/ui-render-performance/SKILL.md +39 -0
  53. package/skills/verification-before-done/SKILL.md +57 -0
  54. package/skills/worktree-isolation/SKILL.md +39 -0
  55. package/src/agents/agent-config.ts +6 -0
  56. package/src/agents/agent-search.ts +98 -0
  57. package/src/agents/agent-serializer.ts +38 -34
  58. package/src/agents/discover-agents.ts +29 -15
  59. package/src/config/config.ts +72 -24
  60. package/src/config/defaults.ts +25 -0
  61. package/src/extension/autonomous-policy.ts +26 -33
  62. package/src/extension/help.ts +1 -0
  63. package/src/extension/management.ts +5 -0
  64. package/src/extension/project-init.ts +62 -2
  65. package/src/extension/register.ts +69 -22
  66. package/src/extension/registration/commands.ts +64 -25
  67. package/src/extension/registration/compaction-guard.ts +1 -1
  68. package/src/extension/registration/subagent-helpers.ts +8 -0
  69. package/src/extension/registration/subagent-tools.ts +149 -148
  70. package/src/extension/registration/team-tool.ts +14 -10
  71. package/src/extension/run-index.ts +35 -21
  72. package/src/extension/run-maintenance.ts +30 -5
  73. package/src/extension/team-tool/api.ts +47 -9
  74. package/src/extension/team-tool/cancel.ts +109 -5
  75. package/src/extension/team-tool/context.ts +8 -0
  76. package/src/extension/team-tool/intent-policy.ts +42 -0
  77. package/src/extension/team-tool/lifecycle-actions.ts +120 -79
  78. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  79. package/src/extension/team-tool/respond.ts +46 -18
  80. package/src/extension/team-tool/run.ts +55 -12
  81. package/src/extension/team-tool/status.ts +13 -2
  82. package/src/extension/team-tool-types.ts +3 -0
  83. package/src/extension/team-tool.ts +45 -14
  84. package/src/hooks/registry.ts +61 -0
  85. package/src/hooks/types.ts +41 -0
  86. package/src/observability/event-to-metric.ts +8 -1
  87. package/src/runtime/agent-control.ts +169 -63
  88. package/src/runtime/async-runner.ts +3 -1
  89. package/src/runtime/background-runner.ts +78 -53
  90. package/src/runtime/cancellation-token.ts +89 -0
  91. package/src/runtime/cancellation.ts +61 -0
  92. package/src/runtime/capability-inventory.ts +116 -0
  93. package/src/runtime/child-pi.ts +458 -444
  94. package/src/runtime/code-summary.ts +247 -0
  95. package/src/runtime/crash-recovery.ts +182 -0
  96. package/src/runtime/crew-agent-records.ts +70 -10
  97. package/src/runtime/crew-agent-runtime.ts +1 -0
  98. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  99. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  100. package/src/runtime/deadletter.ts +1 -0
  101. package/src/runtime/delivery-coordinator.ts +48 -25
  102. package/src/runtime/effectiveness.ts +81 -0
  103. package/src/runtime/event-stream-bridge.ts +90 -0
  104. package/src/runtime/live-agent-control.ts +2 -1
  105. package/src/runtime/live-agent-manager.ts +179 -85
  106. package/src/runtime/live-control-realtime.ts +1 -1
  107. package/src/runtime/live-extension-bridge.ts +150 -0
  108. package/src/runtime/live-irc.ts +92 -0
  109. package/src/runtime/live-session-health.ts +100 -0
  110. package/src/runtime/live-session-runtime.ts +599 -305
  111. package/src/runtime/manifest-cache.ts +17 -2
  112. package/src/runtime/mcp-proxy.ts +113 -0
  113. package/src/runtime/model-fallback.ts +6 -4
  114. package/src/runtime/notebook-helpers.ts +90 -0
  115. package/src/runtime/orphan-sentinel.ts +7 -0
  116. package/src/runtime/output-validator.ts +187 -0
  117. package/src/runtime/parallel-utils.ts +57 -0
  118. package/src/runtime/parent-guard.ts +80 -0
  119. package/src/runtime/pi-args.ts +18 -3
  120. package/src/runtime/process-status.ts +5 -1
  121. package/src/runtime/prose-compressor.ts +164 -0
  122. package/src/runtime/result-extractor.ts +121 -0
  123. package/src/runtime/retry-executor.ts +81 -64
  124. package/src/runtime/runtime-resolver.ts +23 -10
  125. package/src/runtime/semaphore.ts +131 -0
  126. package/src/runtime/sensitive-paths.ts +92 -0
  127. package/src/runtime/skill-instructions.ts +222 -0
  128. package/src/runtime/stale-reconciler.ts +4 -14
  129. package/src/runtime/stream-preview.ts +177 -0
  130. package/src/runtime/subagent-manager.ts +6 -2
  131. package/src/runtime/subprocess-tool-registry.ts +67 -0
  132. package/src/runtime/task-output-context.ts +177 -127
  133. package/src/runtime/task-runner/capabilities.ts +78 -0
  134. package/src/runtime/task-runner/live-executor.ts +107 -101
  135. package/src/runtime/task-runner/prompt-builder.ts +72 -8
  136. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  137. package/src/runtime/task-runner/run-projection.ts +104 -0
  138. package/src/runtime/task-runner.ts +115 -5
  139. package/src/runtime/team-runner.ts +134 -19
  140. package/src/runtime/workspace-tree.ts +298 -0
  141. package/src/runtime/yield-handler.ts +189 -0
  142. package/src/schema/config-schema.ts +7 -0
  143. package/src/schema/team-tool-schema.ts +14 -4
  144. package/src/skills/discover-skills.ts +67 -0
  145. package/src/state/active-run-registry.ts +167 -0
  146. package/src/state/artifact-store.ts +4 -1
  147. package/src/state/atomic-write.ts +50 -1
  148. package/src/state/blob-store.ts +117 -0
  149. package/src/state/contracts.ts +2 -1
  150. package/src/state/event-log-rotation.ts +158 -0
  151. package/src/state/event-log.ts +52 -2
  152. package/src/state/mailbox.ts +129 -9
  153. package/src/state/state-store.ts +32 -5
  154. package/src/state/types.ts +64 -2
  155. package/src/teams/team-config.ts +1 -0
  156. package/src/ui/agent-management-overlay.ts +144 -0
  157. package/src/ui/crew-widget.ts +15 -5
  158. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  159. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  160. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  161. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  162. package/src/ui/live-run-sidebar.ts +4 -0
  163. package/src/ui/powerbar-publisher.ts +77 -15
  164. package/src/ui/render-coalescer.ts +51 -0
  165. package/src/ui/run-dashboard.ts +4 -0
  166. package/src/ui/run-event-bus.ts +209 -0
  167. package/src/ui/run-snapshot-cache.ts +78 -18
  168. package/src/ui/snapshot-types.ts +10 -0
  169. package/src/ui/transcript-entries.ts +258 -0
  170. package/src/utils/ids.ts +5 -0
  171. package/src/utils/incremental-reader.ts +104 -0
  172. package/src/utils/paths.ts +4 -2
  173. package/src/utils/scan-cache.ts +137 -0
  174. package/src/utils/sse-parser.ts +134 -0
  175. package/src/utils/task-name-generator.ts +337 -0
  176. package/src/utils/visual.ts +33 -2
  177. package/src/workflows/workflow-config.ts +1 -0
  178. package/src/worktree/cleanup.ts +2 -1
@@ -1,148 +1,149 @@
1
- import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "typebox";
3
- import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
- import { handleTeamTool } from "../team-tool.ts";
5
- import { checkSubagentSpawnPermission, currentCrewRole } from "../../runtime/role-permission.ts";
6
- import { readPersistedSubagentRecord, savePersistedSubagentRecord, type SubagentManager, type SubagentSpawnOptions } from "../../subagents/manager.ts";
7
- import { loadConfig } from "../../config/config.ts";
8
- import { logInternalError } from "../../utils/internal-error.ts";
9
- import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
10
- import { t } from "../../i18n.ts";
11
-
12
- export interface SubagentToolRegistrationOptions {
13
- ownerSessionGeneration?: () => number;
14
- }
15
-
16
- export function registerSubagentTools(pi: ExtensionAPI, subagentManager: SubagentManager, options: SubagentToolRegistrationOptions = {}): void {
17
- const agentTool: ToolDefinition = {
18
- name: "Agent",
19
- label: "Agent",
20
- description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
21
- promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
22
- promptGuidelines: [
23
- "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
24
- "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
25
- "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
26
- ],
27
- parameters: Type.Object({
28
- prompt: Type.String({ description: "The task for the subagent to perform." }),
29
- description: Type.String({ description: "Short 3-5 word task description." }),
30
- subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
31
- model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
32
- max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
33
- run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
34
- }) as never,
35
- async execute(_id, params, signal, _onUpdate, ctx) {
36
- const currentRole = currentCrewRole();
37
- const permission = checkSubagentSpawnPermission(currentRole);
38
- if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
39
- const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
40
- spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
41
- if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
42
- const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
43
- const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
44
- if (spawnOptions.background || record.status === "queued") {
45
- // Phase 1.1a: Terminate turn for background queued — no LLM follow-up needed.
46
- // Phase 1.6: Record was terminated for telemetry.
47
- record.terminated = true;
48
- savePersistedSubagentRecord(ctx.cwd, record);
49
- return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
50
- }
51
- await record.promise;
52
- const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
53
- const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
54
- if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
55
- record.terminated = true;
56
- savePersistedSubagentRecord(ctx.cwd, record);
57
- return { ...foregroundResult, terminate: true };
58
- }
59
- return foregroundResult;
60
- },
61
- };
62
-
63
- const getSubagentResultTool: ToolDefinition = {
64
- name: "get_subagent_result",
65
- label: "Get Agent Result",
66
- description: "Check status and retrieve results from a pi-crew background subagent.",
67
- parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
68
- async execute(_id, params, signal, _onUpdate, ctx) {
69
- const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
70
- if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
71
- const inMemory = subagentManager.getRecord(p.agent_id);
72
- const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
73
- if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
74
- let current = refreshPersistedSubagentRecord(ctx, record);
75
- if (inMemory && current !== inMemory) Object.assign(inMemory, current);
76
- if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
77
- current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
78
- savePersistedSubagentRecord(ctx.cwd, current);
79
- }
80
- if (p.wait && (current.status === "running" || current.status === "queued")) {
81
- const waited = await subagentManager.waitForRecord(current.id);
82
- if (waited) current = waited;
83
- if (current.status === "blocked") {
84
- current.resultConsumed = false;
85
- if (inMemory) inMemory.resultConsumed = false;
86
- savePersistedSubagentRecord(ctx.cwd, current);
87
- } else {
88
- const waitStartMs = Date.now();
89
- const maxWaitMs = 300_000; // 5 minutes
90
- while (current.status === "running" || current.status === "queued") {
91
- if (signal?.aborted) {
92
- current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
93
- savePersistedSubagentRecord(ctx.cwd, current);
94
- break;
95
- }
96
- if (Date.now() - waitStartMs > maxWaitMs) {
97
- current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
98
- savePersistedSubagentRecord(ctx.cwd, current);
99
- break;
100
- }
101
- await new Promise((resolve) => setTimeout(resolve, 1000));
102
- current = refreshPersistedSubagentRecord(ctx, current);
103
- if (!current.runId) break;
104
- }
105
- }
106
- }
107
- const output = readSubagentRunResult(ctx, current);
108
- if (current.status !== "running" && current.status !== "queued" && current.status !== "blocked") {
109
- current.resultConsumed = true;
110
- if (inMemory) inMemory.resultConsumed = true;
111
- savePersistedSubagentRecord(ctx.cwd, current);
112
- }
113
- const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
114
- return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
115
- },
116
- };
117
-
118
- const steerSubagentTool: ToolDefinition = {
119
- name: "steer_subagent",
120
- label: "Steer Agent",
121
- description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
122
- parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
123
- async execute(_id, params, _signal, _onUpdate, ctx) {
124
- const p = params as { agent_id?: string; message?: string };
125
- const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
126
- if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
127
- return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
128
- },
129
- };
130
-
131
- const crewAgentTool: ToolDefinition = { ...agentTool, name: "crew_agent", label: "Crew Agent", description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.", promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool." };
132
- const crewAgentResultTool: ToolDefinition = { ...getSubagentResultTool, name: "crew_agent_result", label: "Get Crew Agent Result", description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name." };
133
- const crewAgentSteerTool: ToolDefinition = { ...steerSubagentTool, name: "crew_agent_steer", label: "Steer Crew Agent", description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name." };
134
- const toolConfig = loadConfig(process.cwd()).config.tools;
135
- const enableSteer = toolConfig?.enableSteer !== false;
136
- const enableClaudeStyleAliases = toolConfig?.enableClaudeStyleAliases !== false;
137
-
138
- for (const extraTool of enableSteer ? [crewAgentTool, crewAgentResultTool, crewAgentSteerTool] : [crewAgentTool, crewAgentResultTool]) pi.registerTool(extraTool);
139
- if (enableClaudeStyleAliases) {
140
- for (const extraTool of enableSteer ? [agentTool, getSubagentResultTool, steerSubagentTool] : [agentTool, getSubagentResultTool]) {
141
- try {
142
- pi.registerTool(extraTool);
143
- } catch (error) {
144
- logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
145
- }
146
- }
147
- }
148
- }
1
+ import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
+ import { handleTeamTool } from "../team-tool.ts";
5
+ import { checkSubagentSpawnPermission, currentCrewRole } from "../../runtime/role-permission.ts";
6
+ import { readPersistedSubagentRecord, savePersistedSubagentRecord, type SubagentManager, type SubagentSpawnOptions } from "../../subagents/manager.ts";
7
+ import { loadConfig } from "../../config/config.ts";
8
+ import { logInternalError } from "../../utils/internal-error.ts";
9
+ import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
10
+ import { t } from "../../i18n.ts";
11
+
12
+ export interface SubagentToolRegistrationOptions {
13
+ ownerSessionGeneration?: () => number;
14
+ }
15
+
16
+ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: SubagentManager, options: SubagentToolRegistrationOptions = {}): void {
17
+ const agentTool: ToolDefinition = {
18
+ name: "Agent",
19
+ label: "Agent",
20
+ description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
21
+ promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
22
+ promptGuidelines: [
23
+ "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
24
+ "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
25
+ "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
26
+ ],
27
+ parameters: Type.Object({
28
+ prompt: Type.String({ description: "The task for the subagent to perform." }),
29
+ description: Type.String({ description: "Short 3-5 word task description." }),
30
+ subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
31
+ model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
32
+ skill: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String()), Type.Boolean()], { description: "Skill name(s) to inject for this subagent, or false to disable selected/default skills." })),
33
+ max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
34
+ run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
35
+ }) as never,
36
+ async execute(_id, params, signal, _onUpdate, ctx) {
37
+ const currentRole = currentCrewRole();
38
+ const permission = checkSubagentSpawnPermission(currentRole);
39
+ if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
40
+ const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
41
+ spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
42
+ if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
43
+ const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, skill: currentOptions.skill, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
44
+ const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
45
+ if (spawnOptions.background || record.status === "queued") {
46
+ // Phase 1.1a: Terminate turn for background queued — no LLM follow-up needed.
47
+ // Phase 1.6: Record was terminated for telemetry.
48
+ record.terminated = true;
49
+ savePersistedSubagentRecord(ctx.cwd, record);
50
+ return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
51
+ }
52
+ await record.promise;
53
+ const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
54
+ const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
55
+ if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
56
+ record.terminated = true;
57
+ savePersistedSubagentRecord(ctx.cwd, record);
58
+ return { ...foregroundResult, terminate: true };
59
+ }
60
+ return foregroundResult;
61
+ },
62
+ };
63
+
64
+ const getSubagentResultTool: ToolDefinition = {
65
+ name: "get_subagent_result",
66
+ label: "Get Agent Result",
67
+ description: "Check status and retrieve results from a pi-crew background subagent.",
68
+ parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
69
+ async execute(_id, params, signal, _onUpdate, ctx) {
70
+ const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
71
+ if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
72
+ const inMemory = subagentManager.getRecord(p.agent_id);
73
+ const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
74
+ if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
75
+ let current = refreshPersistedSubagentRecord(ctx, record);
76
+ if (inMemory && current !== inMemory) Object.assign(inMemory, current);
77
+ if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
78
+ current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
79
+ savePersistedSubagentRecord(ctx.cwd, current);
80
+ }
81
+ if (p.wait && (current.status === "running" || current.status === "queued")) {
82
+ const waited = await subagentManager.waitForRecord(current.id);
83
+ if (waited) current = waited;
84
+ if (current.status === "blocked") {
85
+ current.resultConsumed = false;
86
+ if (inMemory) inMemory.resultConsumed = false;
87
+ savePersistedSubagentRecord(ctx.cwd, current);
88
+ } else {
89
+ const waitStartMs = Date.now();
90
+ const maxWaitMs = 300_000; // 5 minutes
91
+ while (current.status === "running" || current.status === "queued") {
92
+ if (signal?.aborted) {
93
+ current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
94
+ savePersistedSubagentRecord(ctx.cwd, current);
95
+ break;
96
+ }
97
+ if (Date.now() - waitStartMs > maxWaitMs) {
98
+ current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
99
+ savePersistedSubagentRecord(ctx.cwd, current);
100
+ break;
101
+ }
102
+ await new Promise((resolve) => setTimeout(resolve, 1000));
103
+ current = refreshPersistedSubagentRecord(ctx, current);
104
+ if (!current.runId) break;
105
+ }
106
+ }
107
+ }
108
+ const output = readSubagentRunResult(ctx, current);
109
+ if (current.status !== "running" && current.status !== "queued" && current.status !== "blocked") {
110
+ current.resultConsumed = true;
111
+ if (inMemory) inMemory.resultConsumed = true;
112
+ savePersistedSubagentRecord(ctx.cwd, current);
113
+ }
114
+ const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
115
+ return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
116
+ },
117
+ };
118
+
119
+ const steerSubagentTool: ToolDefinition = {
120
+ name: "steer_subagent",
121
+ label: "Steer Agent",
122
+ description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
123
+ parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
124
+ async execute(_id, params, _signal, _onUpdate, ctx) {
125
+ const p = params as { agent_id?: string; message?: string };
126
+ const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
127
+ if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
128
+ return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
129
+ },
130
+ };
131
+
132
+ const crewAgentTool: ToolDefinition = { ...agentTool, name: "crew_agent", label: "Crew Agent", description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.", promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool." };
133
+ const crewAgentResultTool: ToolDefinition = { ...getSubagentResultTool, name: "crew_agent_result", label: "Get Crew Agent Result", description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name." };
134
+ const crewAgentSteerTool: ToolDefinition = { ...steerSubagentTool, name: "crew_agent_steer", label: "Steer Crew Agent", description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name." };
135
+ const toolConfig = loadConfig(process.cwd()).config.tools;
136
+ const enableSteer = toolConfig?.enableSteer !== false;
137
+ const enableClaudeStyleAliases = toolConfig?.enableClaudeStyleAliases !== false;
138
+
139
+ for (const extraTool of enableSteer ? [crewAgentTool, crewAgentResultTool, crewAgentSteerTool] : [crewAgentTool, crewAgentResultTool]) pi.registerTool(extraTool);
140
+ if (enableClaudeStyleAliases) {
141
+ for (const extraTool of enableSteer ? [agentTool, getSubagentResultTool, steerSubagentTool] : [agentTool, getSubagentResultTool]) {
142
+ try {
143
+ pi.registerTool(extraTool);
144
+ } catch (error) {
145
+ logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
146
+ }
147
+ }
148
+ }
149
+ }
@@ -1,5 +1,4 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
2
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
4
3
  import { loadConfig } from "../../config/config.ts";
5
4
  import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
@@ -9,12 +8,15 @@ import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
9
8
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
10
9
  import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
11
10
  import type { MetricRegistry } from "../../observability/metric-registry.ts";
11
+ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
12
12
  import { handleTeamTool } from "../team-tool.ts";
13
+ import { withSessionId } from "../team-tool/context.ts";
13
14
  import { toolResult } from "../tool-result.ts";
14
15
 
15
16
  export interface RegisterTeamToolDeps {
16
- foregroundControllers: Set<AbortController>;
17
+ foregroundControllers: Map<string | symbol, AbortController>;
17
18
  startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
19
+ abortForegroundRun: (runId: string) => boolean;
18
20
  openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
19
21
  getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
20
22
  getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
@@ -23,15 +25,16 @@ export interface RegisterTeamToolDeps {
23
25
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
24
26
  }
25
27
 
26
- function resolveCwdOverride(baseCwd: string, override: string | undefined): { ok: true; cwd: string } | { ok: false; error: string } {
28
+ export function resolveCwdOverride(baseCwd: string, override: string | undefined): { ok: true; cwd: string } | { ok: false; error: string } {
27
29
  if (!override) return { ok: true, cwd: baseCwd };
28
- const resolved = path.resolve(baseCwd, override);
29
30
  try {
31
+ const resolved = resolveRealContainedPath(baseCwd, override);
30
32
  const stat = fs.statSync(resolved);
31
33
  if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
32
34
  return { ok: true, cwd: resolved };
33
- } catch {
34
- return { ok: false, error: `cwd override does not exist: ${resolved}` };
35
+ } catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return { ok: false, error: `Invalid cwd override: ${message}` };
35
38
  }
36
39
  }
37
40
 
@@ -44,20 +47,21 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
44
47
  parameters: TeamToolParams as never,
45
48
  async execute(_id, params, signal, _onUpdate, ctx) {
46
49
  const controller = new AbortController();
47
- deps.foregroundControllers.add(controller);
50
+ const toolKey = Symbol();
51
+ deps.foregroundControllers.set(toolKey, controller);
48
52
  const abort = (): void => controller.abort();
49
53
  signal?.addEventListener("abort", abort, { once: true });
50
54
  try {
51
55
  const resolved = params as TeamToolParamsValue;
52
56
  const cwdOverride = resolveCwdOverride(ctx.cwd, resolved.cwd);
53
57
  if (!cwdOverride.ok) return toolResult(cwdOverride.error, { action: resolved.action ?? "list", status: "error" }, true);
54
- const toolCtx = { ...ctx, cwd: cwdOverride.cwd };
58
+ const toolCtx = withSessionId({ ...ctx, cwd: cwdOverride.cwd });
55
59
  // Phase 1.5: Auto-set session name from team run context
56
60
  if (resolved.action === "run" && resolved.goal && !pi.getSessionName()) {
57
61
  const runLabel = resolved.team ?? resolved.agent ?? "direct";
58
62
  pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
59
63
  }
60
- const output = await handleTeamTool(resolved, { ...toolCtx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(toolCtx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(toolCtx, runId), onJsonEvent: deps.onJsonEvent });
64
+ const output = await handleTeamTool(resolved, { ...toolCtx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(toolCtx, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => deps.openLiveSidebar(toolCtx, runId), onJsonEvent: deps.onJsonEvent });
61
65
  if (resolved.action === "run" && !output.isError && typeof output.details?.runId === "string") {
62
66
  pi.appendEntry("crew:run-started", {
63
67
  runId: output.details.runId,
@@ -77,7 +81,7 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
77
81
  return output;
78
82
  } finally {
79
83
  signal?.removeEventListener("abort", abort);
80
- deps.foregroundControllers.delete(controller);
84
+ deps.foregroundControllers.delete(toolKey);
81
85
  }
82
86
  },
83
87
  };
@@ -3,9 +3,14 @@ import * as path from "node:path";
3
3
  import type { TeamRunManifest } from "../state/types.ts";
4
4
  import { DEFAULT_PATHS } from "../config/defaults.ts";
5
5
  import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
6
+ import { activeRunEntries } from "../state/active-run-registry.ts";
6
7
  import { isSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
8
+ import { sharedScanCache } from "../utils/scan-cache.ts";
9
+ import { CancellationToken, createCancellationToken } from "../runtime/cancellation-token.ts";
7
10
 
8
11
  function readManifest(filePath: string): TeamRunManifest | undefined {
12
+ const cached = sharedScanCache.readAndCache("manifests", filePath, filePath);
13
+ if (cached) return cached.raw as TeamRunManifest;
9
14
  try {
10
15
  return JSON.parse(fs.readFileSync(filePath, "utf-8")) as TeamRunManifest;
11
16
  } catch {
@@ -13,23 +18,25 @@ function readManifest(filePath: string): TeamRunManifest | undefined {
13
18
  }
14
19
  }
15
20
 
16
- function collectRuns(root: string, maxEntries?: number): TeamRunManifest[] {
21
+ function collectRuns(root: string, maxEntries?: number, signal?: AbortSignal): TeamRunManifest[] {
17
22
  const runsRoot = path.join(root, DEFAULT_PATHS.state.runsSubdir);
18
23
  if (!fs.existsSync(runsRoot)) return [];
24
+ if (signal?.aborted) return [];
25
+ const token = createCancellationToken({ signal });
19
26
  const entries = fs.readdirSync(runsRoot, { withFileTypes: true })
20
27
  .filter((entry) => entry.isDirectory() && isSafePathId(entry.name))
21
28
  .map((entry) => entry.name)
22
29
  .sort((a, b) => b.localeCompare(a));
23
30
  const selected = maxEntries !== undefined ? entries.slice(0, Math.max(0, maxEntries)) : entries;
24
- return selected
25
- .map((entry) => {
26
- try {
27
- return readManifest(path.join(resolveRealContainedPath(runsRoot, entry), DEFAULT_PATHS.state.manifestFile));
28
- } catch {
29
- return undefined;
30
- }
31
- })
32
- .filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
31
+ const results: TeamRunManifest[] = [];
32
+ for (let i = 0; i < selected.length; i++) {
33
+ if (i % 10 === 0) token.heartbeat(`collectRuns:${i}/${selected.length}`);
34
+ try {
35
+ const manifest = readManifest(path.join(resolveRealContainedPath(runsRoot, selected[i]), DEFAULT_PATHS.state.manifestFile));
36
+ if (manifest) results.push(manifest);
37
+ } catch { /* skip unreadable manifests */ }
38
+ }
39
+ return results;
33
40
  }
34
41
 
35
42
  function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[] {
@@ -40,20 +47,27 @@ function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[
40
47
  }
41
48
 
42
49
  function scopedRunRoots(cwd: string): string[] {
43
- const roots: string[] = [userCrewRoot()];
50
+ const roots = new Set<string>();
51
+ roots.add(userCrewRoot());
44
52
  const projectRoot = findRepoRoot(cwd);
45
- if (projectRoot) roots.unshift(projectCrewRoot(cwd));
46
- return roots;
53
+ if (projectRoot) roots.add(projectCrewRoot(cwd));
54
+ return [...roots];
55
+ }
56
+
57
+ function collectActiveRuns(): TeamRunManifest[] {
58
+ return activeRunEntries()
59
+ .map((entry) => readManifest(entry.manifestPath))
60
+ .filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
47
61
  }
48
62
 
49
- export function listRuns(cwd: string): TeamRunManifest[] {
63
+ export function listRuns(cwd: string, signal?: AbortSignal): TeamRunManifest[] {
50
64
  const roots = scopedRunRoots(cwd);
51
- return mergeRuns(roots.map((root) => collectRuns(root)));
65
+ return mergeRuns([...roots.map((root) => collectRuns(root, undefined, signal)), collectActiveRuns()]);
52
66
  }
53
67
 
54
- export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
68
+ export function listRecentRuns(cwd: string, max = 20, signal?: AbortSignal): TeamRunManifest[] {
55
69
  const roots = scopedRunRoots(cwd);
56
- return mergeRuns(roots.map((root) => collectRuns(root, max)), max);
70
+ return mergeRuns([...roots.map((root) => collectRuns(root, max, signal)), collectActiveRuns()], max);
57
71
  }
58
72
 
59
73
  /**
@@ -62,15 +76,15 @@ export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
62
76
  * - "user": only runs in the user crew root
63
77
  * - "all" (default): merge both scopes (current behavior)
64
78
  */
65
- export function listRunsByScope(cwd: string, scope: "project" | "user" | "all" = "all", max?: number): TeamRunManifest[] {
79
+ export function listRunsByScope(cwd: string, scope: "project" | "user" | "all" = "all", max?: number, signal?: AbortSignal): TeamRunManifest[] {
66
80
  const projectRoot = findRepoRoot(cwd);
67
81
  switch (scope) {
68
82
  case "project":
69
- return projectRoot ? collectRuns(projectCrewRoot(cwd), max) : [];
83
+ return projectRoot ? collectRuns(projectCrewRoot(cwd), max, signal) : [];
70
84
  case "user":
71
- return collectRuns(userCrewRoot(), max);
85
+ return collectRuns(userCrewRoot(), max, signal);
72
86
  case "all":
73
87
  default:
74
- return max !== undefined ? listRecentRuns(cwd, max) : listRuns(cwd);
88
+ return max !== undefined ? listRecentRuns(cwd, max, signal) : listRuns(cwd, signal);
75
89
  }
76
90
  }
@@ -1,13 +1,22 @@
1
1
  import * as fs from "node:fs";
2
+ import * as path from "node:path";
2
3
  import type { TeamRunManifest } from "../state/types.ts";
3
4
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
4
5
  import { projectCrewRoot } from "../utils/paths.ts";
5
6
  import { listRuns } from "./run-index.ts";
6
7
  import { logInternalError } from "../utils/internal-error.ts";
8
+ import { redactSecrets } from "../utils/redaction.ts";
9
+ import { createCancellationToken } from "../runtime/cancellation-token.ts";
7
10
 
8
11
  export interface PruneRunsResult {
9
12
  kept: string[];
10
13
  removed: string[];
14
+ auditPath?: string;
15
+ }
16
+
17
+ export interface PruneRunsOptions {
18
+ intent?: string;
19
+ signal?: AbortSignal;
11
20
  }
12
21
 
13
22
  function isFinished(run: TeamRunManifest): boolean {
@@ -25,12 +34,27 @@ function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
25
34
  }
26
35
  }
27
36
 
28
- export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
29
- const crewRoot = projectCrewRoot(cwd);
30
- const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
37
+ function appendPruneAudit(cwd: string, payload: Record<string, unknown>): string | undefined {
38
+ try {
39
+ const filePath = path.join(projectCrewRoot(cwd), "audit", "prune.jsonl");
40
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
+ fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ ...payload, auditedAt: new Date().toISOString() }))}\n`, "utf-8");
42
+ return filePath;
43
+ } catch (error) {
44
+ logInternalError("prune.audit-write", error, `cwd=${cwd}`);
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ export function pruneFinishedRuns(cwd: string, keep: number, options: PruneRunsOptions = {}): PruneRunsResult {
50
+ const token = createCancellationToken({ signal: options.signal });
51
+ const finished = listRuns(cwd, options.signal).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
31
52
  const kept = finished.slice(0, keep).map((run) => run.runId);
32
53
  const removed: string[] = [];
33
- for (const run of finished.slice(keep)) {
54
+ const toRemove = finished.slice(keep);
55
+ for (let i = 0; i < toRemove.length; i++) {
56
+ if (i % 5 === 0) token.heartbeat(`prune:${i}/${toRemove.length}`);
57
+ const run = toRemove[i];
34
58
  if (!isSafeToPrune(cwd, run)) {
35
59
  logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
36
60
  continue;
@@ -39,5 +63,6 @@ export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
39
63
  fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
40
64
  removed.push(run.runId);
41
65
  }
42
- return { kept, removed };
66
+ const auditPath = appendPruneAudit(cwd, { action: "prune", keep, intent: options.intent, kept, removed });
67
+ return { kept, removed, auditPath };
43
68
  }