pi-crew 0.1.28 → 0.1.30

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 (59) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/NOTICE.md +1 -0
  3. package/docs/architecture.md +164 -92
  4. package/docs/refactor-tasks-phase6.md +662 -0
  5. package/docs/runtime-flow.md +148 -0
  6. package/package.json +1 -1
  7. package/schema.json +1 -0
  8. package/skills/git-master/SKILL.md +19 -0
  9. package/skills/read-only-explorer/SKILL.md +21 -0
  10. package/skills/safe-bash/SKILL.md +16 -0
  11. package/skills/task-packet/SKILL.md +23 -0
  12. package/skills/verify-evidence/SKILL.md +22 -0
  13. package/src/config/config.ts +2 -0
  14. package/src/config/defaults.ts +1 -0
  15. package/src/extension/async-notifier.ts +33 -4
  16. package/src/extension/register.ts +15 -522
  17. package/src/extension/registration/artifact-cleanup.ts +14 -0
  18. package/src/extension/registration/commands.ts +208 -0
  19. package/src/extension/registration/subagent-helpers.ts +1 -1
  20. package/src/extension/registration/subagent-tools.ts +110 -0
  21. package/src/extension/registration/team-tool.ts +44 -0
  22. package/src/extension/team-tool/api.ts +4 -4
  23. package/src/extension/team-tool/cancel.ts +31 -0
  24. package/src/extension/team-tool/inspect.ts +41 -0
  25. package/src/extension/team-tool/lifecycle-actions.ts +79 -0
  26. package/src/extension/team-tool/plan.ts +19 -0
  27. package/src/extension/team-tool/run.ts +41 -3
  28. package/src/extension/team-tool/status.ts +73 -0
  29. package/src/extension/team-tool.ts +57 -224
  30. package/src/runtime/async-marker.ts +26 -0
  31. package/src/runtime/async-runner.ts +44 -9
  32. package/src/runtime/background-runner.ts +2 -0
  33. package/src/runtime/child-pi.ts +5 -1
  34. package/src/runtime/concurrency.ts +9 -3
  35. package/src/runtime/crew-agent-records.ts +1 -0
  36. package/src/runtime/crew-agent-runtime.ts +2 -1
  37. package/src/runtime/model-fallback.ts +21 -4
  38. package/src/runtime/pi-args.ts +2 -0
  39. package/src/runtime/process-status.ts +1 -0
  40. package/src/runtime/role-permission.ts +11 -0
  41. package/src/runtime/task-runner/live-executor.ts +98 -0
  42. package/src/runtime/task-runner/progress.ts +111 -0
  43. package/src/runtime/task-runner/prompt-builder.ts +72 -0
  44. package/src/runtime/task-runner/result-utils.ts +14 -0
  45. package/src/runtime/task-runner/state-helpers.ts +22 -0
  46. package/src/runtime/task-runner.ts +38 -283
  47. package/src/runtime/team-runner.ts +116 -7
  48. package/src/schema/config-schema.ts +1 -0
  49. package/src/state/mailbox.ts +28 -0
  50. package/src/state/types.ts +16 -0
  51. package/src/subagents/async-entry.ts +1 -0
  52. package/src/subagents/index.ts +3 -0
  53. package/src/subagents/live/control.ts +1 -0
  54. package/src/subagents/live/manager.ts +1 -0
  55. package/src/subagents/live/realtime.ts +1 -0
  56. package/src/subagents/live/session-runtime.ts +1 -0
  57. package/src/subagents/manager.ts +1 -0
  58. package/src/subagents/spawn.ts +1 -0
  59. package/src/ui/live-run-sidebar.ts +1 -1
@@ -0,0 +1,208 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { loadConfig } from "../../config/config.ts";
3
+ import { handleTeamTool } from "../team-tool.ts";
4
+ import { piTeamsHelp } from "../help.ts";
5
+ import { handleTeamManagerCommand } from "../team-manager-command.ts";
6
+ import { loadRunManifestById } from "../../state/state-store.ts";
7
+ import type { TeamRunManifest } from "../../state/types.ts";
8
+ import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
9
+ import { AnimatedMascot } from "../../ui/mascot.ts";
10
+ import { RunDashboard, type RunDashboardSelection } from "../../ui/run-dashboard.ts";
11
+ import { DurableTextViewer } from "../../ui/transcript-viewer.ts";
12
+ import { commandText, notifyCommandResult, parseRunArgs, parseScalar, pushUnset, setNestedConfig } from "./command-utils.ts";
13
+ import { openTranscriptViewer, selectAgentTask } from "./viewers.ts";
14
+ import { printTimings, time } from "../../utils/timings.ts";
15
+
16
+ export interface RegisterTeamCommandsDeps {
17
+ startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
18
+ openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
19
+ getManifestCache: (cwd: string) => { list(max?: number): TeamRunManifest[] };
20
+ }
21
+
22
+ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
23
+ pi.registerCommand("teams", {
24
+ description: "List pi-crew teams, workflows, and agents",
25
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
26
+ const result = await handleTeamTool({ action: "list" }, ctx);
27
+ await notifyCommandResult(ctx, commandText(result));
28
+ },
29
+ });
30
+
31
+ pi.registerCommand("team-run", {
32
+ description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
33
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
34
+ const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx as ExtensionContext, runId) });
35
+ await notifyCommandResult(ctx, commandText(result));
36
+ },
37
+ });
38
+
39
+ for (const [name, action, description] of [
40
+ ["team-status", "status", "Show pi-crew run status"],
41
+ ["team-resume", "resume", "Resume a pi-crew run by re-queueing failed/cancelled/skipped/running tasks"],
42
+ ["team-summary", "summary", "Show pi-crew run summary"],
43
+ ["team-events", "events", "Show full pi-crew event log for a run"],
44
+ ["team-artifacts", "artifacts", "List pi-crew artifacts for a run"],
45
+ ["team-worktrees", "worktrees", "List pi-crew worktrees for a run"],
46
+ ["team-export", "export", "Export a pi-crew run bundle to artifacts/export"],
47
+ ["team-cancel", "cancel", "Cancel a pi-crew run"],
48
+ ] as const) {
49
+ pi.registerCommand(name, { description, handler: async (args: string, ctx: ExtensionCommandContext) => {
50
+ const runId = args.trim() || undefined;
51
+ const result = await handleTeamTool({ action, runId }, ctx);
52
+ await notifyCommandResult(ctx, commandText(result));
53
+ } });
54
+ }
55
+
56
+ pi.registerCommand("team-api", {
57
+ description: "Run safe pi-crew API interop operations: <runId> <operation> [key=value]",
58
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
59
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
60
+ const runId = tokens.find((token) => !token.includes("=") && !token.startsWith("--"));
61
+ const operation = tokens.find((token) => token !== runId && !token.includes("=") && !token.startsWith("--")) ?? "read-manifest";
62
+ const config: Record<string, unknown> = { operation };
63
+ for (const token of tokens.filter((item) => item.includes("="))) {
64
+ const [key, ...rest] = token.split("=");
65
+ if (key) config[key] = parseScalar(rest.join("="));
66
+ }
67
+ const result = await handleTeamTool({ action: "api", runId, config }, ctx);
68
+ await notifyCommandResult(ctx, commandText(result));
69
+ },
70
+ });
71
+
72
+ pi.registerCommand("team-imports", { description: "List imported pi-crew run bundles", handler: async (_args: string, ctx: ExtensionCommandContext) => {
73
+ const result = await handleTeamTool({ action: "imports" }, ctx);
74
+ await notifyCommandResult(ctx, commandText(result));
75
+ } });
76
+
77
+ pi.registerCommand("team-import", { description: "Import a pi-crew run-export.json bundle into local imports", handler: async (args: string, ctx: ExtensionCommandContext) => {
78
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
79
+ const pathArg = tokens.find((token) => !token.startsWith("--"));
80
+ const scope = tokens.includes("--user") ? "user" : "project";
81
+ const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, ctx);
82
+ await notifyCommandResult(ctx, commandText(result));
83
+ } });
84
+
85
+ pi.registerCommand("team-prune", { description: "Prune old finished pi-crew runs, keeping the newest N", handler: async (args: string, ctx: ExtensionCommandContext) => {
86
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
87
+ const keepToken = tokens.find((token) => token.startsWith("--keep="));
88
+ const keep = keepToken ? Number.parseInt(keepToken.slice("--keep=".length), 10) : undefined;
89
+ const result = await handleTeamTool({ action: "prune", keep, confirm: tokens.includes("--confirm") }, ctx);
90
+ await notifyCommandResult(ctx, commandText(result));
91
+ } });
92
+
93
+ pi.registerCommand("team-forget", { description: "Forget a pi-crew run by deleting its state and artifacts", handler: async (args: string, ctx: ExtensionCommandContext) => {
94
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
95
+ const runId = tokens.find((token) => !token.startsWith("--"));
96
+ const result = await handleTeamTool({ action: "forget", runId, force: tokens.includes("--force"), confirm: tokens.includes("--confirm") }, ctx);
97
+ await notifyCommandResult(ctx, commandText(result));
98
+ } });
99
+
100
+ pi.registerCommand("team-cleanup", { description: "Clean up pi-crew worktrees for a run", handler: async (args: string, ctx: ExtensionCommandContext) => {
101
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
102
+ const runId = tokens.find((token) => !token.startsWith("--"));
103
+ const result = await handleTeamTool({ action: "cleanup", runId, force: tokens.includes("--force") }, ctx);
104
+ await notifyCommandResult(ctx, commandText(result));
105
+ } });
106
+
107
+ pi.registerCommand("team-manager", { description: "Open a simple pi-crew interactive manager", handler: handleTeamManagerCommand });
108
+
109
+ pi.registerCommand("team-result", { description: "Open a pi-crew agent result viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
110
+ const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
111
+ const selected = await selectAgentTask(ctx, runId, rawTaskId);
112
+ const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
113
+ if (ctx.hasUI && loaded) {
114
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
115
+ const text = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected!.runId, config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
116
+ await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected!.runId}:${agent?.taskId ?? "unknown"}`, text.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
117
+ return;
118
+ }
119
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
120
+ await notifyCommandResult(ctx, commandText(result));
121
+ } });
122
+
123
+ pi.registerCommand("team-transcript", { description: "Open a pi-crew transcript viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
124
+ const [runId, taskId] = args.trim().split(/\s+/).filter(Boolean);
125
+ if (await openTranscriptViewer(ctx, runId, taskId)) return;
126
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, ctx);
127
+ await notifyCommandResult(ctx, commandText(result));
128
+ } });
129
+
130
+ pi.registerCommand("team-dashboard", { description: "Open a pi-crew run dashboard overlay", handler: async (_args: string, ctx: ExtensionCommandContext) => {
131
+ for (;;) {
132
+ const runs = deps.getManifestCache(ctx.cwd).list(50);
133
+ const uiConfig = loadConfig(ctx.cwd).config.ui;
134
+ const rightPanel = uiConfig?.dashboardPlacement !== "center";
135
+ const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
136
+ const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
137
+ if (!selection) return;
138
+ if (selection.action === "reload") continue;
139
+ if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
140
+ const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx) : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
141
+ await notifyCommandResult(ctx, commandText(result));
142
+ return;
143
+ }
144
+ } });
145
+
146
+ pi.registerCommand("team-mascot", { description: "Show an animated mascot splash", handler: async (args: string, ctx: ExtensionCommandContext) => {
147
+ if (!ctx.hasUI) return;
148
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
149
+ const uiConfig = loadConfig(ctx.cwd).config.ui;
150
+ const styleArg = tokens.find((t) => t === "cat" || t === "armin");
151
+ const effectArg = tokens.find((t) => ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"].includes(t));
152
+ const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? "cat";
153
+ const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? "random";
154
+ await ctx.ui.custom<undefined>((tui, theme, _keybindings, done) => new AnimatedMascot(theme, () => done(undefined), { frameIntervalMs: style === "armin" ? 33 : 180, autoCloseMs: 7000, requestRender: () => (tui as { requestRender?: () => void }).requestRender?.(), style, effect }), { overlay: true, overlayOptions: { width: style === "armin" ? 48 : 62, maxHeight: "85%", anchor: "center" } });
155
+ } });
156
+
157
+ pi.registerCommand("team-init", { description: "Initialize project-local pi-crew directories and gitignore entries", handler: async (args: string, ctx: ExtensionCommandContext) => {
158
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
159
+ const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite") } }, ctx);
160
+ await notifyCommandResult(ctx, commandText(result));
161
+ } });
162
+
163
+ pi.registerCommand("team-autonomy", { description: "Show or toggle pi-crew autonomous delegation policy: status|on|off", handler: async (args: string, ctx: ExtensionCommandContext) => {
164
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
165
+ const mode = tokens[0]?.toLowerCase();
166
+ const config = mode === "on" ? { profile: "suggested", enabled: true, injectPolicy: true } : mode === "off" ? { profile: "manual", enabled: false } : mode === "manual" || mode === "suggested" || mode === "assisted" || mode === "aggressive" ? { profile: mode, enabled: mode !== "manual", injectPolicy: mode !== "manual" } : { preferAsyncForLongTasks: tokens.includes("--prefer-async") ? true : undefined, allowWorktreeSuggestion: tokens.includes("--no-worktree-suggest") ? false : undefined };
167
+ const result = await handleTeamTool({ action: "autonomy", config }, ctx);
168
+ await notifyCommandResult(ctx, commandText(result));
169
+ } });
170
+
171
+ pi.registerCommand("team-config", { description: "Show or update pi-crew config. Use key=value [--project] to update.", handler: async (args: string, ctx: ExtensionCommandContext) => {
172
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
173
+ if (tokens.length === 0) {
174
+ const result = await handleTeamTool({ action: "config" }, ctx);
175
+ await notifyCommandResult(ctx, commandText(result));
176
+ return;
177
+ }
178
+ const config: Record<string, unknown> = { scope: tokens.includes("--project") ? "project" : "user" };
179
+ for (const token of tokens) {
180
+ if (token.startsWith("--unset=")) {
181
+ pushUnset(config, token.slice("--unset=".length));
182
+ continue;
183
+ }
184
+ if (!token.includes("=")) continue;
185
+ const [key, ...rest] = token.split("=");
186
+ if (!key) continue;
187
+ const raw = rest.join("=");
188
+ if (raw === "unset" || raw === "null") pushUnset(config, key);
189
+ else setNestedConfig(config, key, parseScalar(raw));
190
+ }
191
+ const result = await handleTeamTool({ action: "config", config }, ctx);
192
+ await notifyCommandResult(ctx, commandText(result));
193
+ } });
194
+
195
+ for (const [name, action, description] of [
196
+ ["team-validate", "validate", "Validate pi-crew agents, teams, and workflows"],
197
+ ["team-doctor", "doctor", "Check pi-crew installation and discovery readiness"],
198
+ ] as const) pi.registerCommand(name, { description, handler: async (_args: string, ctx: ExtensionCommandContext) => {
199
+ const result = await handleTeamTool({ action }, ctx);
200
+ await notifyCommandResult(ctx, commandText(result));
201
+ } });
202
+
203
+ pi.registerCommand("team-help", { description: "Show pi-crew command help", handler: async (_args: string, ctx: ExtensionCommandContext) => {
204
+ await notifyCommandResult(ctx, piTeamsHelp());
205
+ } });
206
+ time("register.commands");
207
+ printTimings();
208
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
3
  import { loadRunManifestById } from "../../state/state-store.ts";
4
- import { savePersistedSubagentRecord, type SubagentRecord, type SubagentSpawnOptions } from "../../runtime/subagent-manager.ts";
4
+ import { savePersistedSubagentRecord, type SubagentRecord, type SubagentSpawnOptions } from "../../subagents/manager.ts";
5
5
 
6
6
  export function sendFollowUp(pi: ExtensionAPI, content: string): void {
7
7
  const sender = (pi as unknown as { sendMessage?: (message: unknown, options?: unknown) => void }).sendMessage;
@@ -0,0 +1,110 @@
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 { logInternalError } from "../../utils/internal-error.ts";
8
+ import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
9
+
10
+ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: SubagentManager): void {
11
+ const agentTool: ToolDefinition = {
12
+ name: "Agent",
13
+ label: "Agent",
14
+ 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.",
15
+ 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.",
16
+ promptGuidelines: [
17
+ "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
18
+ "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
19
+ "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
20
+ ],
21
+ parameters: Type.Object({
22
+ prompt: Type.String({ description: "The task for the subagent to perform." }),
23
+ description: Type.String({ description: "Short 3-5 word task description." }),
24
+ subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
25
+ model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
26
+ max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
27
+ run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
28
+ }) as never,
29
+ async execute(_id, params, signal, _onUpdate, ctx) {
30
+ const currentRole = currentCrewRole();
31
+ const permission = checkSubagentSpawnPermission(currentRole);
32
+ if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
33
+ const options = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
34
+ if (!options.prompt.trim()) return subagentToolResult("Agent requires prompt.", {}, true);
35
+ const runner = async (spawnOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: spawnOptions.type, goal: spawnOptions.prompt, model: spawnOptions.model, async: spawnOptions.background, config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined } as TeamToolParamsValue, spawnOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
36
+ const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
37
+ if (options.background || record.status === "queued") return subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status });
38
+ await record.promise;
39
+ const output = readSubagentRunResult(ctx, record) ?? record.result ?? "No output.";
40
+ return subagentToolResult([`Agent ${record.id} ${record.status}.`, "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
41
+ },
42
+ };
43
+
44
+ const getSubagentResultTool: ToolDefinition = {
45
+ name: "get_subagent_result",
46
+ label: "Get Agent Result",
47
+ description: "Check status and retrieve results from a pi-crew background subagent.",
48
+ 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,
49
+ async execute(_id, params, signal, _onUpdate, ctx) {
50
+ const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
51
+ if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
52
+ const inMemory = subagentManager.getRecord(p.agent_id);
53
+ const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
54
+ if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
55
+ let current = refreshPersistedSubagentRecord(ctx, record);
56
+ if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
57
+ current = { ...current, status: "error", error: "Subagent was interrupted before its durable run id was recorded; it cannot be recovered after restart.", completedAt: current.completedAt ?? Date.now() };
58
+ savePersistedSubagentRecord(ctx.cwd, current);
59
+ }
60
+ if (p.wait && (current.status === "running" || current.status === "queued")) {
61
+ current.resultConsumed = true;
62
+ savePersistedSubagentRecord(ctx.cwd, current);
63
+ const waited = await subagentManager.waitForRecord(current.id);
64
+ if (waited) current = waited;
65
+ else while (current.status === "running" || current.status === "queued") {
66
+ if (signal?.aborted) {
67
+ current = { ...current, status: "error", error: "Waiting for subagent result was aborted.", completedAt: Date.now() };
68
+ savePersistedSubagentRecord(ctx.cwd, current);
69
+ break;
70
+ }
71
+ await new Promise((resolve) => setTimeout(resolve, 1000));
72
+ current = refreshPersistedSubagentRecord(ctx, current);
73
+ if (!current.runId) break;
74
+ }
75
+ }
76
+ const output = readSubagentRunResult(ctx, current);
77
+ if (current.status !== "running" && current.status !== "queued") {
78
+ current.resultConsumed = true;
79
+ savePersistedSubagentRecord(ctx.cwd, current);
80
+ }
81
+ const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
82
+ return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
83
+ },
84
+ };
85
+
86
+ const steerSubagentTool: ToolDefinition = {
87
+ name: "steer_subagent",
88
+ label: "Steer Agent",
89
+ 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.",
90
+ parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
91
+ async execute(_id, params, _signal, _onUpdate, ctx) {
92
+ const p = params as { agent_id?: string; message?: string };
93
+ const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
94
+ if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
95
+ return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
96
+ },
97
+ };
98
+
99
+ 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." };
100
+ 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." };
101
+ 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." };
102
+ for (const extraTool of [crewAgentTool, crewAgentResultTool, crewAgentSteerTool]) pi.registerTool(extraTool);
103
+ for (const extraTool of [agentTool, getSubagentResultTool, steerSubagentTool]) {
104
+ try {
105
+ pi.registerTool(extraTool);
106
+ } catch (error) {
107
+ logInternalError("register.duplicate-tool", error, `tool=${extraTool.name}`);
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,44 @@
1
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ import { loadConfig } from "../../config/config.ts";
3
+ import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
+ import type { CrewWidgetState } from "../../ui/crew-widget.ts";
5
+ import { updateCrewWidget } from "../../ui/crew-widget.ts";
6
+ import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
7
+ import type { createManifestCache } from "../../runtime/manifest-cache.ts";
8
+ import { handleTeamTool } from "../team-tool.ts";
9
+
10
+ export interface RegisterTeamToolDeps {
11
+ foregroundControllers: Set<AbortController>;
12
+ startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
13
+ openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
14
+ getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
15
+ widgetState: CrewWidgetState;
16
+ }
17
+
18
+ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
19
+ const tool: ToolDefinition = {
20
+ name: "team",
21
+ label: "Team",
22
+ description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
23
+ promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
24
+ parameters: TeamToolParams as never,
25
+ async execute(_id, params, signal, _onUpdate, ctx) {
26
+ const controller = new AbortController();
27
+ deps.foregroundControllers.add(controller);
28
+ const abort = (): void => controller.abort();
29
+ signal?.addEventListener("abort", abort, { once: true });
30
+ try {
31
+ const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx, runId) });
32
+ const config = loadConfig(ctx.cwd).config.ui;
33
+ const cache = deps.getManifestCache(ctx.cwd);
34
+ updateCrewWidget(ctx, deps.widgetState, config, cache);
35
+ updatePiCrewPowerbar(pi.events, ctx.cwd, config, cache);
36
+ return output;
37
+ } finally {
38
+ signal?.removeEventListener("abort", abort);
39
+ deps.foregroundControllers.delete(controller);
40
+ }
41
+ },
42
+ };
43
+ pi.registerTool(tool);
44
+ }
@@ -8,14 +8,14 @@ import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../
8
8
  import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, validateMailbox, type MailboxDirection } from "../../state/mailbox.ts";
9
9
  import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log.ts";
10
10
  import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
11
- import { probeLiveSessionRuntime } from "../../runtime/live-session-runtime.ts";
11
+ import { probeLiveSessionRuntime } from "../../subagents/live/session-runtime.ts";
12
12
  import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
13
13
  import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
14
14
  import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
15
15
  import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
16
- import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../runtime/live-agent-manager.ts";
17
- import { appendLiveAgentControlRequest } from "../../runtime/live-agent-control.ts";
18
- import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../runtime/live-control-realtime.ts";
16
+ import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
17
+ import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
18
+ import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
19
19
  import type { PiTeamsToolResult } from "../tool-result.ts";
20
20
  import { configRecord, result, type TeamContext } from "./context.ts";
21
21
 
@@ -0,0 +1,31 @@
1
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
2
+ import { withRunLockSync } from "../../state/locks.ts";
3
+ import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
4
+ import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
5
+ import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
6
+ import { logInternalError } from "../../utils/internal-error.ts";
7
+ import type { PiTeamsToolResult } from "../tool-result.ts";
8
+ import { result, type TeamContext } from "./context.ts";
9
+
10
+ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
11
+ if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
12
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
13
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
14
+ return withRunLockSync(loaded.manifest, () => {
15
+ if (loaded.manifest.status === "completed" && !params.force) return result(`Run ${loaded.manifest.runId} is already completed; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
16
+ const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
17
+ saveRunTasks(loaded.manifest, tasks);
18
+ try {
19
+ saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
20
+ } catch (error) {
21
+ logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
22
+ }
23
+ try {
24
+ writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
25
+ } catch (error) {
26
+ logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
27
+ }
28
+ const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
29
+ return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
30
+ });
31
+ }
@@ -0,0 +1,41 @@
1
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
2
+ import { readEvents } from "../../state/event-log.ts";
3
+ import { loadRunManifestById } from "../../state/state-store.ts";
4
+ import { aggregateUsage, formatUsage } from "../../state/usage.ts";
5
+ import type { PiTeamsToolResult } from "../tool-result.ts";
6
+ import { result, type TeamContext } from "./context.ts";
7
+
8
+ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
+ if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
10
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
11
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
12
+ const events = readEvents(loaded.manifest.eventsPath);
13
+ const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
14
+ return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
15
+ }
16
+
17
+ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
18
+ if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
19
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
20
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
21
+ const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
22
+ return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
23
+ }
24
+
25
+ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
26
+ if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
27
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
28
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
29
+ const usage = aggregateUsage(loaded.tasks);
30
+ const lines = [
31
+ `Summary for ${loaded.manifest.runId}`,
32
+ `Status: ${loaded.manifest.status}`,
33
+ `Team: ${loaded.manifest.team}`,
34
+ `Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
35
+ `Goal: ${loaded.manifest.goal}`,
36
+ `Usage: ${formatUsage(usage)}`,
37
+ "Tasks:",
38
+ ...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
39
+ ];
40
+ return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
41
+ }
@@ -0,0 +1,79 @@
1
+ import * as fs from "node:fs";
2
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
3
+ import { appendEvent } from "../../state/event-log.ts";
4
+ import { loadRunManifestById } from "../../state/state-store.ts";
5
+ import { cleanupRunWorktrees } from "../../worktree/cleanup.ts";
6
+ import { listImportedRuns } from "../import-index.ts";
7
+ import { exportRunBundle } from "../run-export.ts";
8
+ import { importRunBundle } from "../run-import.ts";
9
+ import { pruneFinishedRuns } from "../run-maintenance.ts";
10
+ import type { PiTeamsToolResult } from "../tool-result.ts";
11
+ import { configRecord, result, type TeamContext } from "./context.ts";
12
+
13
+ export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
14
+ if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
15
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
16
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
17
+ const withWorktrees = loaded.tasks.filter((task) => task.worktree);
18
+ const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
19
+ return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
20
+ }
21
+
22
+ export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
23
+ const imports = listImportedRuns(ctx.cwd);
24
+ const lines = ["Imported pi-crew runs:", ...(imports.length ? imports.map((entry) => `- ${entry.runId} (${entry.scope})${entry.status ? ` [${entry.status}]` : ""} ${entry.team ?? "unknown"}/${entry.workflow ?? "none"}: ${entry.goal ?? ""}\n Bundle: ${entry.bundlePath}\n Summary: ${entry.summaryPath}`) : ["- (none)"])];
25
+ return result(lines.join("\n"), { action: "imports", status: "ok" });
26
+ }
27
+
28
+ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
29
+ const cfg = configRecord(params.config);
30
+ const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
31
+ if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
32
+ const scope = cfg.scope === "user" ? "user" : "project";
33
+ try {
34
+ const imported = importRunBundle(ctx.cwd, bundlePath, scope);
35
+ return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
39
+ }
40
+ }
41
+
42
+ export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
43
+ if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
44
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
45
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
46
+ const exported = exportRunBundle(loaded.manifest, loaded.tasks);
47
+ appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
48
+ return result([`Exported run ${loaded.manifest.runId}.`, `JSON: ${exported.jsonPath}`, `Markdown: ${exported.markdownPath}`].join("\n"), { action: "export", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
49
+ }
50
+
51
+ export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
52
+ const keep = params.keep ?? 20;
53
+ if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
54
+ if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
55
+ const pruned = pruneFinishedRuns(ctx.cwd, keep);
56
+ return result([`Pruned finished pi-crew runs.`, `Kept: ${pruned.kept.length}`, `Removed: ${pruned.removed.length}`, ...(pruned.removed.length ? ["Removed runs:", ...pruned.removed.map((runId) => `- ${runId}`)] : [])].join("\n"), { action: "prune", status: "ok" });
57
+ }
58
+
59
+ export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
60
+ if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
61
+ if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
62
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
63
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
64
+ const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
65
+ if (cleanup.preserved.length > 0 && !params.force) return result([`Run '${params.runId}' has preserved worktrees. Use force: true to forget anyway.`, ...cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`)].join("\n"), { action: "forget", status: "error", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, true);
66
+ fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
67
+ fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
68
+ return result([`Forgot run ${loaded.manifest.runId}.`, `Removed state: ${loaded.manifest.stateRoot}`, `Removed artifacts: ${loaded.manifest.artifactsRoot}`, ...(cleanup.removed.length ? ["Removed worktrees:", ...cleanup.removed.map((item) => `- ${item}`)] : [])].join("\n"), { action: "forget", status: "ok", runId: loaded.manifest.runId });
69
+ }
70
+
71
+ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
72
+ if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
73
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
74
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
75
+ const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
76
+ appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths } });
77
+ const lines = [`Worktree cleanup for ${loaded.manifest.runId}:`, "Removed:", ...(cleanup.removed.length ? cleanup.removed.map((item) => `- ${item}`) : ["- (none)"]), "Preserved:", ...(cleanup.preserved.length ? cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`) : ["- (none)"]), "Artifacts:", ...(cleanup.artifactPaths.length ? cleanup.artifactPaths.map((item) => `- ${item}`) : ["- (none)"])];
78
+ return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
79
+ }
@@ -0,0 +1,19 @@
1
+ import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
2
+ import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
3
+ import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
4
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
+ import type { PiTeamsToolResult } from "../tool-result.ts";
6
+ import { result, type TeamContext } from "./context.ts";
7
+
8
+ export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
9
+ const teamName = params.team ?? "default";
10
+ const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === teamName);
11
+ if (!team) return result(`Team '${teamName}' not found.`, { action: "plan", status: "error" }, true);
12
+ const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
13
+ const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === workflowName);
14
+ if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "plan", status: "error" }, true);
15
+ const errors = validateWorkflowForTeam(workflow, team);
16
+ if (errors.length > 0) return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...errors.map((error) => `- ${error}`)].join("\n"), { action: "plan", status: "error" }, true);
17
+ const lines = [`Team plan: ${team.name}`, `Workflow: ${workflow.name}`, `Goal: ${params.goal ?? params.task ?? "(not provided)"}`, "", "Steps:", ...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`)];
18
+ return result(lines.join("\n"), { action: "plan", status: "ok" });
19
+ }