pi-crew 0.1.0

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 (95) hide show
  1. package/AGENTS.md +32 -0
  2. package/CHANGELOG.md +6 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +15 -0
  5. package/README.md +703 -0
  6. package/agents/analyst.md +11 -0
  7. package/agents/critic.md +11 -0
  8. package/agents/executor.md +11 -0
  9. package/agents/explorer.md +11 -0
  10. package/agents/planner.md +11 -0
  11. package/agents/reviewer.md +11 -0
  12. package/agents/security-reviewer.md +11 -0
  13. package/agents/test-engineer.md +11 -0
  14. package/agents/verifier.md +11 -0
  15. package/agents/writer.md +11 -0
  16. package/docs/architecture.md +92 -0
  17. package/docs/live-mailbox-runtime.md +36 -0
  18. package/docs/publishing.md +65 -0
  19. package/docs/resource-formats.md +131 -0
  20. package/docs/usage.md +203 -0
  21. package/index.ts +6 -0
  22. package/install.mjs +19 -0
  23. package/package.json +79 -0
  24. package/schema.json +45 -0
  25. package/skills/.gitkeep +0 -0
  26. package/src/agents/agent-config.ts +27 -0
  27. package/src/agents/agent-serializer.ts +34 -0
  28. package/src/agents/discover-agents.ts +73 -0
  29. package/src/config/config.ts +193 -0
  30. package/src/extension/async-notifier.ts +36 -0
  31. package/src/extension/autonomous-policy.ts +122 -0
  32. package/src/extension/help.ts +43 -0
  33. package/src/extension/import-index.ts +52 -0
  34. package/src/extension/management.ts +335 -0
  35. package/src/extension/project-init.ts +74 -0
  36. package/src/extension/register.ts +349 -0
  37. package/src/extension/run-bundle-schema.ts +85 -0
  38. package/src/extension/run-export.ts +59 -0
  39. package/src/extension/run-import.ts +46 -0
  40. package/src/extension/run-index.ts +28 -0
  41. package/src/extension/run-maintenance.ts +24 -0
  42. package/src/extension/session-summary.ts +8 -0
  43. package/src/extension/team-manager-command.ts +86 -0
  44. package/src/extension/team-recommendation.ts +174 -0
  45. package/src/extension/team-tool.ts +783 -0
  46. package/src/extension/tool-result.ts +16 -0
  47. package/src/extension/validate-resources.ts +77 -0
  48. package/src/prompt/prompt-runtime.ts +58 -0
  49. package/src/runtime/async-runner.ts +26 -0
  50. package/src/runtime/background-runner.ts +43 -0
  51. package/src/runtime/child-pi.ts +75 -0
  52. package/src/runtime/model-fallback.ts +101 -0
  53. package/src/runtime/pi-args.ts +81 -0
  54. package/src/runtime/pi-json-output.ts +110 -0
  55. package/src/runtime/pi-spawn.ts +96 -0
  56. package/src/runtime/process-status.ts +25 -0
  57. package/src/runtime/task-runner.ts +164 -0
  58. package/src/runtime/team-runner.ts +135 -0
  59. package/src/runtime/worker-heartbeat.ts +21 -0
  60. package/src/schema/team-tool-schema.ts +100 -0
  61. package/src/state/artifact-store.ts +36 -0
  62. package/src/state/atomic-write.ts +18 -0
  63. package/src/state/contracts.ts +88 -0
  64. package/src/state/event-log.ts +27 -0
  65. package/src/state/locks.ts +40 -0
  66. package/src/state/mailbox.ts +188 -0
  67. package/src/state/state-store.ts +119 -0
  68. package/src/state/task-claims.ts +42 -0
  69. package/src/state/types.ts +88 -0
  70. package/src/state/usage.ts +29 -0
  71. package/src/teams/discover-teams.ts +84 -0
  72. package/src/teams/team-config.ts +22 -0
  73. package/src/teams/team-serializer.ts +36 -0
  74. package/src/ui/run-dashboard.ts +138 -0
  75. package/src/utils/frontmatter.ts +36 -0
  76. package/src/utils/ids.ts +12 -0
  77. package/src/utils/names.ts +26 -0
  78. package/src/utils/paths.ts +15 -0
  79. package/src/workflows/discover-workflows.ts +101 -0
  80. package/src/workflows/validate-workflow.ts +40 -0
  81. package/src/workflows/workflow-config.ts +24 -0
  82. package/src/workflows/workflow-serializer.ts +31 -0
  83. package/src/worktree/cleanup.ts +69 -0
  84. package/src/worktree/worktree-manager.ts +60 -0
  85. package/teams/default.team.md +12 -0
  86. package/teams/fast-fix.team.md +11 -0
  87. package/teams/implementation.team.md +15 -0
  88. package/teams/research.team.md +11 -0
  89. package/teams/review.team.md +12 -0
  90. package/tsconfig.json +19 -0
  91. package/workflows/default.workflow.md +29 -0
  92. package/workflows/fast-fix.workflow.md +22 -0
  93. package/workflows/implementation.workflow.md +47 -0
  94. package/workflows/research.workflow.md +22 -0
  95. package/workflows/review.workflow.md +30 -0
@@ -0,0 +1,783 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
5
+ import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
6
+ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
7
+ import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
8
+ import { effectiveAutonomousConfig, loadConfig, updateAutonomousConfig, updateConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../config/config.ts";
9
+ import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
10
+ import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
11
+ import { writeArtifact } from "../state/artifact-store.ts";
12
+ import { createRunManifest, loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
13
+ import { withRunLock, withRunLockSync } from "../state/locks.ts";
14
+ import { canTransitionTaskStatus, isTeamTaskStatus } from "../state/contracts.ts";
15
+ import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../state/task-claims.ts";
16
+ import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, validateMailbox, type MailboxDirection } from "../state/mailbox.ts";
17
+ import { aggregateUsage, formatUsage } from "../state/usage.ts";
18
+ import { atomicWriteJson } from "../state/atomic-write.ts";
19
+ import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
20
+ import { getPiSpawnCommand } from "../runtime/pi-spawn.ts";
21
+ import { executeTeamRun } from "../runtime/team-runner.ts";
22
+ import { spawnBackgroundTeamRun } from "../runtime/async-runner.ts";
23
+ import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
24
+ import { appendEvent, readEvents } from "../state/event-log.ts";
25
+ import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
26
+ import { piTeamsHelp } from "./help.ts";
27
+ import { initializeProject } from "./project-init.ts";
28
+ import { handleCreate, handleDelete, handleUpdate } from "./management.ts";
29
+ import { pruneFinishedRuns } from "./run-maintenance.ts";
30
+ import { exportRunBundle } from "./run-export.ts";
31
+ import { importRunBundle } from "./run-import.ts";
32
+ import { listImportedRuns } from "./import-index.ts";
33
+ import { listRuns } from "./run-index.ts";
34
+ import { formatValidationReport, validateResources } from "./validate-resources.ts";
35
+ import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
36
+ import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
37
+ import { touchWorkerHeartbeat } from "../runtime/worker-heartbeat.ts";
38
+
39
+ export interface TeamToolDetails {
40
+ action: string;
41
+ status: "ok" | "error" | "planned";
42
+ runId?: string;
43
+ artifactsRoot?: string;
44
+ }
45
+
46
+ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model">>;
47
+
48
+ function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
49
+ return toolResult(text, details, isError);
50
+ }
51
+
52
+ function formatScoped(name: string, source: string, description: string): string {
53
+ return `- ${name} (${source}): ${description}`;
54
+ }
55
+
56
+ export function handleList(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
57
+ const resource = params.resource;
58
+ const blocks: string[] = [];
59
+ if (!resource || resource === "team") {
60
+ const teams = allTeams(discoverTeams(ctx.cwd));
61
+ blocks.push("Teams:", ...(teams.length ? teams.map((team) => formatScoped(team.name, team.source, team.description)) : ["- (none)"]));
62
+ }
63
+ if (!resource || resource === "workflow") {
64
+ const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
65
+ blocks.push("", "Workflows:", ...(workflows.length ? workflows.map((workflow) => formatScoped(workflow.name, workflow.source, workflow.description)) : ["- (none)"]));
66
+ }
67
+ if (!resource || resource === "agent") {
68
+ const agents = allAgents(discoverAgents(ctx.cwd));
69
+ blocks.push("", "Agents:", ...(agents.length ? agents.map((agent) => formatScoped(agent.name, agent.source, agent.description)) : ["- (none)"]));
70
+ }
71
+ if (!resource) {
72
+ const runs = listRuns(ctx.cwd).slice(0, 10);
73
+ blocks.push("", "Recent runs:", ...(runs.length ? runs.map((run) => `- ${run.runId} [${run.status}] ${run.team}/${run.workflow ?? "none"}: ${run.goal}`) : ["- (none)"]));
74
+ }
75
+ return result(blocks.join("\n"), { action: "list", status: "ok" });
76
+ }
77
+
78
+ export function handleGet(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
79
+ if (params.team) {
80
+ const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === params.team);
81
+ if (!team) return result(`Team '${params.team}' not found.`, { action: "get", status: "error" }, true);
82
+ const lines = [
83
+ `Team: ${team.name} (${team.source})`,
84
+ `Path: ${team.filePath}`,
85
+ `Description: ${team.description}`,
86
+ `Default workflow: ${team.defaultWorkflow ?? "(none)"}`,
87
+ `Workspace mode: ${team.workspaceMode ?? "single"}`,
88
+ "Roles:",
89
+ ...(team.roles.length ? team.roles.map((role) => `- ${role.name} -> ${role.agent}${role.description ? `: ${role.description}` : ""}`) : ["- (none)"]),
90
+ ];
91
+ return result(lines.join("\n"), { action: "get", status: "ok" });
92
+ }
93
+ if (params.workflow) {
94
+ const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === params.workflow);
95
+ if (!workflow) return result(`Workflow '${params.workflow}' not found.`, { action: "get", status: "error" }, true);
96
+ const lines = [
97
+ `Workflow: ${workflow.name} (${workflow.source})`,
98
+ `Path: ${workflow.filePath}`,
99
+ `Description: ${workflow.description}`,
100
+ "Steps:",
101
+ ...(workflow.steps.length ? workflow.steps.map((step) => `- ${step.id} [${step.role}] dependsOn=${step.dependsOn?.join(",") ?? "none"}`) : ["- (none)"]),
102
+ ];
103
+ return result(lines.join("\n"), { action: "get", status: "ok" });
104
+ }
105
+ if (params.agent) {
106
+ const agent = allAgents(discoverAgents(ctx.cwd)).find((item) => item.name === params.agent);
107
+ if (!agent) return result(`Agent '${params.agent}' not found.`, { action: "get", status: "error" }, true);
108
+ const lines = [
109
+ `Agent: ${agent.name} (${agent.source})`,
110
+ `Path: ${agent.filePath}`,
111
+ `Description: ${agent.description}`,
112
+ agent.model ? `Model: ${agent.model}` : undefined,
113
+ agent.skills?.length ? `Skills: ${agent.skills.join(", ")}` : undefined,
114
+ "",
115
+ agent.systemPrompt || "(empty system prompt)",
116
+ ].filter((line): line is string => line !== undefined);
117
+ return result(lines.join("\n"), { action: "get", status: "ok" });
118
+ }
119
+ return result("Specify team, workflow, or agent for get.", { action: "get", status: "error" }, true);
120
+ }
121
+
122
+ function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
123
+ const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
124
+ return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "available";
125
+ }
126
+
127
+ function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
128
+ const output = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
129
+ if (!output.error && output.status === 0) return { ok: true, detail: firstOutputLine(output.stdout, output.stderr) };
130
+ return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
131
+ }
132
+
133
+ function piCommandExists(): { ok: boolean; detail: string } {
134
+ const spec = getPiSpawnCommand(["--version"]);
135
+ const output = spawnSync(spec.command, spec.args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
136
+ if (!output.error && output.status === 0) {
137
+ const executable = spec.command === "pi" ? "pi" : `${spec.command} ${spec.args[0] ?? ""}`.trim();
138
+ return { ok: true, detail: `${firstOutputLine(output.stdout, output.stderr)} (${executable})` };
139
+ }
140
+ return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
141
+ }
142
+
143
+ function checkWritableDir(dir: string): { ok: boolean; detail: string } {
144
+ try {
145
+ fs.mkdirSync(dir, { recursive: true });
146
+ const probe = path.join(dir, `.pi-crew-write-${process.pid}-${Date.now()}`);
147
+ fs.writeFileSync(probe, "ok", "utf-8");
148
+ fs.unlinkSync(probe);
149
+ return { ok: true, detail: dir };
150
+ } catch (error) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ return { ok: false, detail: `${dir}: ${message}` };
153
+ }
154
+ }
155
+
156
+ export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
157
+ const discoveredAgents = allAgents(discoverAgents(ctx.cwd));
158
+ const agentCount = discoveredAgents.length;
159
+ const teamCount = allTeams(discoverTeams(ctx.cwd)).length;
160
+ const workflowCount = allWorkflows(discoverWorkflows(ctx.cwd)).length;
161
+ const git = commandExists("git", ["--version"]);
162
+ const pi = piCommandExists();
163
+ const loadedConfig = loadConfig(ctx.cwd);
164
+ const userWritable = checkWritableDir(path.join(userPiRoot(), "extensions", "pi-crew"));
165
+ const projectWritable = checkWritableDir(path.join(projectPiRoot(ctx.cwd), "teams"));
166
+ const validation = validateResources(ctx.cwd);
167
+ const validationErrors = validation.issues.filter((issue) => issue.level === "error").length;
168
+ const validationWarnings = validation.issues.filter((issue) => issue.level === "warning").length;
169
+ let smokeChildPi: { ok: boolean; detail: string } | undefined;
170
+ const doctorCfg = configRecord(params.config);
171
+ if (doctorCfg.smokeChildPi === true) {
172
+ try {
173
+ const spec = getPiSpawnCommand(["--mode", "json", "-p", "Reply with exactly PI-TEAMS-SMOKE-OK"]);
174
+ const output = execFileSync(spec.command, spec.args, { cwd: ctx.cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15_000 }).trim();
175
+ smokeChildPi = { ok: output.includes("PI-TEAMS-SMOKE-OK"), detail: output.split("\n").slice(-1)[0] ?? "completed" };
176
+ } catch (error) {
177
+ const message = error instanceof Error ? error.message : String(error);
178
+ smokeChildPi = { ok: false, detail: message };
179
+ }
180
+ }
181
+ const checks = [
182
+ { label: "cwd", ok: fs.existsSync(ctx.cwd), detail: ctx.cwd },
183
+ { label: "platform", ok: true, detail: `${process.platform}/${process.arch} node=${process.version}` },
184
+ { label: "pi command", ok: pi.ok, detail: pi.detail },
185
+ { label: "git command", ok: git.ok, detail: git.detail },
186
+ { label: "user state writable", ok: userWritable.ok, detail: userWritable.detail },
187
+ { label: "project state writable", ok: projectWritable.ok, detail: projectWritable.detail },
188
+ { label: "config", ok: !loadedConfig.error, detail: loadedConfig.error ? `${loadedConfig.path}: ${loadedConfig.error}` : loadedConfig.path },
189
+ { label: "current model", ok: true, detail: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "not available in this context" },
190
+ { label: "resource model hints", ok: true, detail: `${discoveredAgents.filter((agent) => agent.model || agent.fallbackModels?.length).length} agents declare model/fallback preferences` },
191
+ { label: "agents", ok: agentCount > 0, detail: `${agentCount} discovered` },
192
+ { label: "teams", ok: teamCount > 0, detail: `${teamCount} discovered` },
193
+ { label: "workflows", ok: workflowCount > 0, detail: `${workflowCount} discovered` },
194
+ { label: "resource validation", ok: validationErrors === 0, detail: `${validationErrors} errors, ${validationWarnings} warnings` },
195
+ ...(smokeChildPi ? [{ label: "child Pi smoke", ok: smokeChildPi.ok, detail: smokeChildPi.detail }] : []),
196
+ ];
197
+ const text = ["pi-crew doctor:", ...checks.map((check) => `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`)].join("\n");
198
+ return result(text, { action: "doctor", status: checks.every((check) => check.ok) ? "ok" : "error" }, checks.some((check) => !check.ok));
199
+ }
200
+
201
+ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
202
+ const goal = params.goal ?? params.task;
203
+ if (!goal) return result("Run requires goal or task.", { action: "run", status: "error" }, true);
204
+
205
+ const teams = allTeams(discoverTeams(ctx.cwd));
206
+ const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
207
+ const agents = allAgents(discoverAgents(ctx.cwd));
208
+ const teamName = params.team ?? "default";
209
+ const team = teams.find((item) => item.name === teamName);
210
+ if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
211
+ const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
212
+ const workflow = workflows.find((item) => item.name === workflowName);
213
+ if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
214
+
215
+ const validationErrors = validateWorkflowForTeam(workflow, team);
216
+ if (validationErrors.length > 0) {
217
+ return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...validationErrors.map((error) => `- ${error}`)].join("\n"), { action: "run", status: "error" }, true);
218
+ }
219
+
220
+ const { manifest, tasks, paths } = createRunManifest({
221
+ cwd: ctx.cwd,
222
+ team,
223
+ workflow,
224
+ goal,
225
+ workspaceMode: params.workspaceMode,
226
+ });
227
+ const goalArtifact = writeArtifact(paths.artifactsRoot, {
228
+ kind: "prompt",
229
+ relativePath: "goal.md",
230
+ content: `${goal}\n`,
231
+ producer: "team-tool",
232
+ });
233
+ const updatedManifest = { ...manifest, artifacts: [goalArtifact], summary: "Run manifest created; worker execution is not implemented yet." };
234
+ atomicWriteJson(paths.manifestPath, updatedManifest);
235
+
236
+ const loadedConfig = loadConfig(ctx.cwd);
237
+ const runAsync = params.async ?? loadedConfig.config.asyncByDefault ?? false;
238
+ if (runAsync) {
239
+ const spawned = spawnBackgroundTeamRun(updatedManifest);
240
+ const asyncManifest = { ...updatedManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
241
+ atomicWriteJson(paths.manifestPath, asyncManifest);
242
+ appendEvent(updatedManifest.eventsPath, { type: "async.spawned", runId: updatedManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
243
+ const text = [
244
+ `Started async pi-crew run ${updatedManifest.runId}.`,
245
+ `Team: ${team.name}`,
246
+ `Workflow: ${workflow.name}`,
247
+ `Status: ${updatedManifest.status}`,
248
+ `Tasks: ${tasks.length}`,
249
+ `State: ${updatedManifest.stateRoot}`,
250
+ `Artifacts: ${updatedManifest.artifactsRoot}`,
251
+ `Background log: ${spawned.logPath}`,
252
+ "",
253
+ `Check status with: team status runId=${updatedManifest.runId}`,
254
+ ].join("\n");
255
+ return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
256
+ }
257
+
258
+ const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
259
+ const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers });
260
+ const text = [
261
+ `Created pi-crew run ${executed.manifest.runId}.`,
262
+ `Team: ${team.name}`,
263
+ `Workflow: ${workflow.name}`,
264
+ `Status: ${executed.manifest.status}`,
265
+ `Tasks: ${executed.tasks.length}`,
266
+ `State: ${executed.manifest.stateRoot}`,
267
+ `Artifacts: ${executed.manifest.artifactsRoot}`,
268
+ "",
269
+ executeWorkers
270
+ ? "Child Pi worker execution was enabled with PI_TEAMS_EXECUTE_WORKERS=1."
271
+ : "Safe scaffold mode: child Pi workers were not launched. Set PI_TEAMS_EXECUTE_WORKERS=1 to enable real worker execution.",
272
+ ].join("\n");
273
+ return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
274
+ }
275
+
276
+ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
277
+ if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
278
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
279
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
280
+ let { manifest, tasks } = loaded;
281
+ let asyncLivenessLine: string | undefined;
282
+ if (manifest.async) {
283
+ const asyncState = manifest.async;
284
+ const liveness = checkProcessLiveness(asyncState.pid);
285
+ asyncLivenessLine = `Async: pid=${asyncState.pid ?? "unknown"} alive=${liveness.alive ? "true" : "false"} detail=${liveness.detail} log=${asyncState.logPath} spawnedAt=${asyncState.spawnedAt}`;
286
+ if (!liveness.alive && isActiveRunStatus(manifest.status)) {
287
+ manifest = updateRunStatus(manifest, "failed", `Async process stale: ${liveness.detail}`);
288
+ appendEvent(manifest.eventsPath, { type: "async.stale", runId: manifest.runId, message: liveness.detail, data: { pid: asyncState.pid } });
289
+ }
290
+ }
291
+ const counts = new Map<string, number>();
292
+ for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
293
+ const events = readEvents(manifest.eventsPath).slice(-8);
294
+ const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
295
+ const totalUsage = aggregateUsage(tasks);
296
+ const lines = [
297
+ `Run: ${manifest.runId}`,
298
+ `Team: ${manifest.team}`,
299
+ `Workflow: ${manifest.workflow ?? "(none)"}`,
300
+ `Status: ${manifest.status}`,
301
+ `Workspace mode: ${manifest.workspaceMode}`,
302
+ `Goal: ${manifest.goal}`,
303
+ `Created: ${manifest.createdAt}`,
304
+ `Updated: ${manifest.updatedAt}`,
305
+ `State: ${manifest.stateRoot}`,
306
+ `Artifacts: ${manifest.artifactsRoot}`,
307
+ ...(asyncLivenessLine ? [asyncLivenessLine] : []),
308
+ "Tasks:",
309
+ ...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
310
+ `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
311
+ `Total usage: ${formatUsage(totalUsage)}`,
312
+ "",
313
+ "Recent artifacts:",
314
+ ...(artifactLines.length ? artifactLines : ["- (none)"]),
315
+ "",
316
+ "Recent events:",
317
+ ...(events.length ? events.map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`) : ["- (none)"]),
318
+ ];
319
+ return result(lines.join("\n"), { action: "status", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
320
+ }
321
+
322
+ export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
323
+ const teamName = params.team ?? "default";
324
+ const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === teamName);
325
+ if (!team) return result(`Team '${teamName}' not found.`, { action: "plan", status: "error" }, true);
326
+ const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
327
+ const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === workflowName);
328
+ if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "plan", status: "error" }, true);
329
+ const errors = validateWorkflowForTeam(workflow, team);
330
+ 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);
331
+ const lines = [
332
+ `Team plan: ${team.name}`,
333
+ `Workflow: ${workflow.name}`,
334
+ `Goal: ${params.goal ?? params.task ?? "(not provided)"}`,
335
+ "",
336
+ "Steps:",
337
+ ...workflow.steps.map((step, index) => `${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`),
338
+ ];
339
+ return result(lines.join("\n"), { action: "plan", status: "ok" });
340
+ }
341
+
342
+ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
343
+ if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
344
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
345
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
346
+ return withRunLockSync(loaded.manifest, () => {
347
+ if (loaded.manifest.status === "completed" && !params.force) {
348
+ 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 });
349
+ }
350
+ 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);
351
+ saveRunTasks(loaded.manifest, tasks);
352
+ const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
353
+ return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
354
+ });
355
+ }
356
+
357
+ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
358
+ if (!params.runId) return result("Resume requires runId.", { action: "resume", status: "error" }, true);
359
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
360
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "resume", status: "error" }, true);
361
+ if (!loaded.manifest.workflow) return result(`Run '${params.runId}' has no workflow to resume.`, { action: "resume", status: "error" }, true);
362
+ const team = allTeams(discoverTeams(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.team);
363
+ if (!team) return result(`Team '${loaded.manifest.team}' not found.`, { action: "resume", status: "error" }, true);
364
+ const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
365
+ if (!workflow) return result(`Workflow '${loaded.manifest.workflow}' not found.`, { action: "resume", status: "error" }, true);
366
+ return await withRunLock(loaded.manifest, async () => {
367
+ const resetTasks = loaded.tasks.map((task) => task.status === "failed" || task.status === "cancelled" || task.status === "skipped" || task.status === "running" ? { ...task, status: "queued" as const, error: undefined, startedAt: undefined, finishedAt: undefined, claim: undefined } : task);
368
+ saveRunTasks(loaded.manifest, resetTasks);
369
+ appendEvent(loaded.manifest.eventsPath, { type: "run.resume_requested", runId: loaded.manifest.runId });
370
+ const loadedConfig = loadConfig(ctx.cwd);
371
+ const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
372
+ const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers });
373
+ return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
374
+ });
375
+ }
376
+
377
+ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
378
+ if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
379
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
380
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
381
+ const events = readEvents(loaded.manifest.eventsPath);
382
+ 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)"])];
383
+ return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
384
+ }
385
+
386
+ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
387
+ if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
388
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
389
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
390
+ 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)"])];
391
+ return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
392
+ }
393
+
394
+ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
395
+ if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
396
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
397
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
398
+ const usage = aggregateUsage(loaded.tasks);
399
+ const lines = [
400
+ `Summary for ${loaded.manifest.runId}`,
401
+ `Status: ${loaded.manifest.status}`,
402
+ `Team: ${loaded.manifest.team}`,
403
+ `Workflow: ${loaded.manifest.workflow ?? "(none)"}`,
404
+ `Goal: ${loaded.manifest.goal}`,
405
+ `Usage: ${formatUsage(usage)}`,
406
+ "Tasks:",
407
+ ...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
408
+ ];
409
+ return result(lines.join("\n"), { action: "summary", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
410
+ }
411
+
412
+ export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
413
+ if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
414
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
415
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
416
+ const withWorktrees = loaded.tasks.filter((task) => task.worktree);
417
+ const lines = [
418
+ `Worktrees for ${loaded.manifest.runId}:`,
419
+ ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"]),
420
+ ];
421
+ return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
422
+ }
423
+
424
+ function configRecord(config: unknown): Record<string, unknown> {
425
+ if (!config || typeof config !== "object" || Array.isArray(config)) return {};
426
+ return config as Record<string, unknown>;
427
+ }
428
+
429
+ function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
430
+ const cfg = configRecord(config);
431
+ const profile = cfg.profile === "manual" || cfg.profile === "suggested" || cfg.profile === "assisted" || cfg.profile === "aggressive" ? cfg.profile : undefined;
432
+ const magicKeywords = cfg.magicKeywords && typeof cfg.magicKeywords === "object" && !Array.isArray(cfg.magicKeywords)
433
+ ? Object.fromEntries(Object.entries(cfg.magicKeywords as Record<string, unknown>).filter((entry): entry is [string, string[]] => Array.isArray(entry[1]) && entry[1].every((item) => typeof item === "string")))
434
+ : undefined;
435
+ return {
436
+ profile,
437
+ enabled: typeof cfg.enabled === "boolean" ? cfg.enabled : undefined,
438
+ injectPolicy: typeof cfg.injectPolicy === "boolean" ? cfg.injectPolicy : undefined,
439
+ preferAsyncForLongTasks: typeof cfg.preferAsyncForLongTasks === "boolean" ? cfg.preferAsyncForLongTasks : undefined,
440
+ allowWorktreeSuggestion: typeof cfg.allowWorktreeSuggestion === "boolean" ? cfg.allowWorktreeSuggestion : undefined,
441
+ magicKeywords,
442
+ };
443
+ }
444
+
445
+ function configPatchFromConfig(config: unknown): PiTeamsConfig {
446
+ const cfg = configRecord(config);
447
+ return {
448
+ asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
449
+ executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
450
+ notifierIntervalMs: typeof cfg.notifierIntervalMs === "number" && Number.isFinite(cfg.notifierIntervalMs) ? cfg.notifierIntervalMs : undefined,
451
+ requireCleanWorktreeLeader: typeof cfg.requireCleanWorktreeLeader === "boolean" ? cfg.requireCleanWorktreeLeader : undefined,
452
+ autonomous: typeof cfg.autonomous === "object" && cfg.autonomous !== null && !Array.isArray(cfg.autonomous) ? autonomousPatchFromConfig(cfg.autonomous) : undefined,
453
+ };
454
+ }
455
+
456
+ function formatAutonomyStatus(config: PiTeamsAutonomousConfig | undefined, pathValue: string, updated: boolean): string {
457
+ const effective = effectiveAutonomousConfig(config);
458
+ return [
459
+ updated ? "Updated pi-crew autonomous mode." : "pi-crew autonomous mode:",
460
+ `Path: ${pathValue}`,
461
+ `Profile: ${effective.profile}`,
462
+ `Enabled: ${effective.enabled}`,
463
+ `Inject policy: ${effective.injectPolicy}`,
464
+ `Prefer async for long tasks: ${effective.preferAsyncForLongTasks}`,
465
+ `Allow worktree suggestion: ${effective.allowWorktreeSuggestion}`,
466
+ ].join("\n");
467
+ }
468
+
469
+ export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
470
+ const imports = listImportedRuns(ctx.cwd);
471
+ const lines = [
472
+ "Imported pi-crew runs:",
473
+ ...(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)"]),
474
+ ];
475
+ return result(lines.join("\n"), { action: "imports", status: "ok" });
476
+ }
477
+
478
+ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
479
+ const cfg = configRecord(params.config);
480
+ const bundlePath = typeof cfg.path === "string" ? cfg.path : typeof cfg.bundlePath === "string" ? cfg.bundlePath : undefined;
481
+ if (!bundlePath) return result("Import requires config.path pointing at run-export.json.", { action: "import", status: "error" }, true);
482
+ const scope = cfg.scope === "user" ? "user" : "project";
483
+ try {
484
+ const imported = importRunBundle(ctx.cwd, bundlePath, scope);
485
+ return result([`Imported run bundle ${imported.runId}.`, `Bundle: ${imported.bundlePath}`, `Summary: ${imported.summaryPath}`].join("\n"), { action: "import", status: "ok" });
486
+ } catch (error) {
487
+ const message = error instanceof Error ? error.message : String(error);
488
+ return result(`Import failed: ${message}`, { action: "import", status: "error" }, true);
489
+ }
490
+ }
491
+
492
+ export function handleExport(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
493
+ if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
494
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
495
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
496
+ const exported = exportRunBundle(loaded.manifest, loaded.tasks);
497
+ appendEvent(loaded.manifest.eventsPath, { type: "run.exported", runId: loaded.manifest.runId, data: exported });
498
+ 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 });
499
+ }
500
+
501
+ export function handlePrune(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
502
+ const keep = params.keep ?? 20;
503
+ if (!params.confirm) return result("prune requires confirm: true.", { action: "prune", status: "error" }, true);
504
+ if (keep < 0 || !Number.isInteger(keep)) return result("keep must be an integer >= 0.", { action: "prune", status: "error" }, true);
505
+ const pruned = pruneFinishedRuns(ctx.cwd, keep);
506
+ 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" });
507
+ }
508
+
509
+ export function handleForget(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
510
+ if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
511
+ if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
512
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
513
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
514
+ const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
515
+ if (cleanup.preserved.length > 0 && !params.force) {
516
+ 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);
517
+ }
518
+ fs.rmSync(loaded.manifest.stateRoot, { recursive: true, force: true });
519
+ fs.rmSync(loaded.manifest.artifactsRoot, { recursive: true, force: true });
520
+ 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 });
521
+ }
522
+
523
+ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
524
+ if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
525
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
526
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
527
+ const cleanup = cleanupRunWorktrees(loaded.manifest, { force: params.force });
528
+ appendEvent(loaded.manifest.eventsPath, { type: "worktree.cleanup", runId: loaded.manifest.runId, data: { removed: cleanup.removed, preserved: cleanup.preserved, artifacts: cleanup.artifactPaths } });
529
+ const lines = [
530
+ `Worktree cleanup for ${loaded.manifest.runId}:`,
531
+ "Removed:",
532
+ ...(cleanup.removed.length ? cleanup.removed.map((item) => `- ${item}`) : ["- (none)"]),
533
+ "Preserved:",
534
+ ...(cleanup.preserved.length ? cleanup.preserved.map((item) => `- ${item.path}: ${item.reason}`) : ["- (none)"]),
535
+ "Artifacts:",
536
+ ...(cleanup.artifactPaths.length ? cleanup.artifactPaths.map((item) => `- ${item}`) : ["- (none)"]),
537
+ ];
538
+ return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
539
+ }
540
+
541
+ export function handleApi(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
542
+ if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
543
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
544
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
545
+ const cfg = configRecord(params.config);
546
+ const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
547
+ if (operation === "read-manifest") {
548
+ return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
549
+ }
550
+ if (operation === "list-tasks") {
551
+ return result(JSON.stringify(loaded.tasks, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
552
+ }
553
+ if (operation === "read-task") {
554
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
555
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
556
+ if (!task) return result("API read-task requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
557
+ return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
558
+ }
559
+ if (operation === "read-events") {
560
+ return result(JSON.stringify(readEvents(loaded.manifest.eventsPath), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
561
+ }
562
+ if (operation === "read-mailbox") {
563
+ const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
564
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
565
+ return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
566
+ }
567
+ if (operation === "validate-mailbox") {
568
+ const report = validateMailbox(loaded.manifest, { repair: cfg.repair === true });
569
+ return result(JSON.stringify(report, null, 2), { action: "api", status: report.issues.some((issue) => issue.level === "error") && cfg.repair !== true ? "error" : "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, report.issues.some((issue) => issue.level === "error") && cfg.repair !== true);
570
+ }
571
+ if (operation === "read-delivery") {
572
+ return result(JSON.stringify(readDeliveryState(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
573
+ }
574
+ if (operation === "send-message") {
575
+ const direction = cfg.direction === "outbox" ? "outbox" : "inbox";
576
+ const from = typeof cfg.from === "string" && cfg.from.trim() ? cfg.from.trim() : "api";
577
+ const to = typeof cfg.to === "string" && cfg.to.trim() ? cfg.to.trim() : "leader";
578
+ const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
579
+ const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
580
+ if (!body) return result("API send-message requires config.body.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
581
+ try {
582
+ return withRunLockSync(loaded.manifest, () => {
583
+ const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
584
+ appendEvent(loaded.manifest.eventsPath, { type: "mailbox.message", runId: loaded.manifest.runId, data: { id: message.id, direction, from, to } });
585
+ return result(JSON.stringify(message, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
586
+ });
587
+ } catch (error) {
588
+ const message = error instanceof Error ? error.message : String(error);
589
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
590
+ }
591
+ }
592
+ if (operation === "ack-message") {
593
+ const messageId = typeof cfg.messageId === "string" ? cfg.messageId : undefined;
594
+ if (!messageId) return result("API ack-message requires config.messageId.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
595
+ try {
596
+ return withRunLockSync(loaded.manifest, () => {
597
+ const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
598
+ appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
599
+ return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
600
+ });
601
+ } catch (error) {
602
+ const message = error instanceof Error ? error.message : String(error);
603
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
604
+ }
605
+ }
606
+ if (operation === "read-heartbeat") {
607
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
608
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
609
+ if (!task) return result("API read-heartbeat requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
610
+ return result(JSON.stringify(task.heartbeat ?? null, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
611
+ }
612
+ if (operation === "claim-task") {
613
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
614
+ const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
615
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
616
+ if (!task) return result("API claim-task requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
617
+ try {
618
+ return withRunLockSync(loaded.manifest, () => {
619
+ const updatedTask = claimTask(task, owner);
620
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
621
+ saveRunTasks(loaded.manifest, tasks);
622
+ appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token: updatedTask.claim?.token, leasedUntil: updatedTask.claim?.leasedUntil } });
623
+ return result(JSON.stringify(updatedTask.claim, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
624
+ });
625
+ } catch (error) {
626
+ const message = error instanceof Error ? error.message : String(error);
627
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
628
+ }
629
+ }
630
+ if (operation === "release-task-claim") {
631
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
632
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
633
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
634
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
635
+ if (!task || !owner || !token) return result("API release-task-claim requires config.taskId, config.owner, and config.token.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
636
+ try {
637
+ return withRunLockSync(loaded.manifest, () => {
638
+ const updatedTask = releaseTaskClaim(task, owner, token);
639
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
640
+ saveRunTasks(loaded.manifest, tasks);
641
+ appendEvent(loaded.manifest.eventsPath, { type: "task.claim_released", runId: loaded.manifest.runId, taskId: task.id, data: { owner } });
642
+ return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
643
+ });
644
+ } catch (error) {
645
+ const message = error instanceof Error ? error.message : String(error);
646
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
647
+ }
648
+ }
649
+ if (operation === "transition-task-status") {
650
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
651
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
652
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
653
+ const to = cfg.status;
654
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
655
+ if (!task || !owner || !token || !isTeamTaskStatus(to)) return result("API transition-task-status requires config.taskId, config.owner, config.token, and valid config.status.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
656
+ if (!canTransitionTaskStatus(task.status, to)) return result(`Invalid task status transition: ${task.status} -> ${to}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
657
+ try {
658
+ return withRunLockSync(loaded.manifest, () => {
659
+ const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
660
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
661
+ saveRunTasks(loaded.manifest, tasks);
662
+ appendEvent(loaded.manifest.eventsPath, { type: "task.status_transitioned", runId: loaded.manifest.runId, taskId: task.id, data: { owner, status: to } });
663
+ return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
664
+ });
665
+ } catch (error) {
666
+ const message = error instanceof Error ? error.message : String(error);
667
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
668
+ }
669
+ }
670
+ if (operation === "write-heartbeat") {
671
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
672
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
673
+ if (!task) return result("API write-heartbeat requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
674
+ try {
675
+ return withRunLockSync(loaded.manifest, () => {
676
+ const heartbeat = touchWorkerHeartbeat(task.heartbeat ?? { workerId: task.id, lastSeenAt: new Date().toISOString() }, { alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined });
677
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? { ...item, heartbeat } : item);
678
+ saveRunTasks(loaded.manifest, tasks);
679
+ appendEvent(loaded.manifest.eventsPath, { type: "worker.heartbeat", runId: loaded.manifest.runId, taskId: task.id, data: { ...heartbeat } });
680
+ return result(JSON.stringify(heartbeat, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
681
+ });
682
+ } catch (error) {
683
+ const message = error instanceof Error ? error.message : String(error);
684
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
685
+ }
686
+ }
687
+ return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
688
+ }
689
+
690
+ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
691
+ const action = params.action ?? "list";
692
+ switch (action) {
693
+ case "list": return handleList(params, ctx);
694
+ case "get": return handleGet(params, ctx);
695
+ case "init": {
696
+ const cfg = configRecord(params.config);
697
+ const initialized = initializeProject(ctx.cwd, { copyBuiltins: cfg.copyBuiltins === true, overwrite: cfg.overwrite === true });
698
+ return result([
699
+ "Initialized pi-crew project layout.",
700
+ "Directories:",
701
+ ...(initialized.createdDirs.length ? initialized.createdDirs.map((dir) => `- created ${dir}`) : ["- already existed"]),
702
+ "Copied builtin files:",
703
+ ...(initialized.copiedFiles.length ? initialized.copiedFiles.map((file) => `- ${file}`) : ["- (none)"]),
704
+ ...(initialized.skippedFiles.length ? ["Skipped existing files:", ...initialized.skippedFiles.map((file) => `- ${file}`)] : []),
705
+ `Gitignore: ${initialized.gitignorePath} (${initialized.gitignoreUpdated ? "updated" : "already configured"})`,
706
+ ].join("\n"), { action: "init", status: "ok" });
707
+ }
708
+ case "help": return result(piTeamsHelp(), { action: "help", status: "ok" });
709
+ case "recommend": {
710
+ const goal = params.goal ?? params.task;
711
+ if (!goal) return result("Recommend requires goal or task.", { action: "recommend", status: "error" }, true);
712
+ const loaded = loadConfig(ctx.cwd);
713
+ const recommendation = recommendTeam(goal, loaded.config.autonomous, { teams: allTeams(discoverTeams(ctx.cwd)), agents: allAgents(discoverAgents(ctx.cwd)) });
714
+ return result(formatRecommendation(goal, recommendation), { action: "recommend", status: "ok" });
715
+ }
716
+ case "autonomy": {
717
+ const patch = autonomousPatchFromConfig(params.config);
718
+ const shouldUpdate = Object.values(patch).some((value) => value !== undefined);
719
+ if (!shouldUpdate) {
720
+ const loaded = loadConfig(ctx.cwd);
721
+ return result(formatAutonomyStatus(loaded.config.autonomous, loaded.path, false), { action: "autonomy", status: loaded.error ? "error" : "ok" }, Boolean(loaded.error));
722
+ }
723
+ try {
724
+ const saved = updateAutonomousConfig(patch);
725
+ return result(formatAutonomyStatus(saved.config.autonomous, saved.path, true), { action: "autonomy", status: "ok" });
726
+ } catch (error) {
727
+ const message = error instanceof Error ? error.message : String(error);
728
+ return result(message, { action: "autonomy", status: "error" }, true);
729
+ }
730
+ }
731
+ case "config": {
732
+ const patch = configPatchFromConfig(params.config);
733
+ const cfg = configRecord(params.config);
734
+ const unsetPaths = Array.isArray(cfg.unset) ? cfg.unset.filter((entry): entry is string => typeof entry === "string") : typeof cfg.unset === "string" ? [cfg.unset] : [];
735
+ const shouldUpdate = Object.values(patch).some((value) => value !== undefined) || unsetPaths.length > 0;
736
+ if (shouldUpdate) {
737
+ try {
738
+ const saved = updateConfig(patch, { cwd: ctx.cwd, scope: cfg.scope === "project" ? "project" : "user", unsetPaths });
739
+ return result(["Updated pi-crew config.", `Path: ${saved.path}`, "Effective config:", JSON.stringify(saved.config, null, 2)].join("\n"), { action: "config", status: "ok" });
740
+ } catch (error) {
741
+ const message = error instanceof Error ? error.message : String(error);
742
+ return result(message, { action: "config", status: "error" }, true);
743
+ }
744
+ }
745
+ const loaded = loadConfig(ctx.cwd);
746
+ const lines = [
747
+ "pi-crew config:",
748
+ `Path: ${loaded.path}`,
749
+ `Status: ${loaded.error ? `error: ${loaded.error}` : "ok"}`,
750
+ "Effective config:",
751
+ JSON.stringify(loaded.config, null, 2),
752
+ "Schema: package export ./schema.json",
753
+ ];
754
+ return result(lines.join("\n"), { action: "config", status: loaded.error ? "error" : "ok" }, Boolean(loaded.error));
755
+ }
756
+ case "validate": {
757
+ const report = validateResources(ctx.cwd);
758
+ const hasErrors = report.issues.some((issue) => issue.level === "error");
759
+ return result(formatValidationReport(report), { action: "validate", status: hasErrors ? "error" : "ok" }, hasErrors);
760
+ }
761
+ case "doctor": return handleDoctor(ctx, params);
762
+ case "cleanup": return handleCleanup(params, ctx);
763
+ case "api": return handleApi(params, ctx);
764
+ case "events": return handleEvents(params, ctx);
765
+ case "artifacts": return handleArtifacts(params, ctx);
766
+ case "worktrees": return handleWorktrees(params, ctx);
767
+ case "summary": return handleSummary(params, ctx);
768
+ case "export": return handleExport(params, ctx);
769
+ case "import": return handleImport(params, ctx);
770
+ case "imports": return handleImports(params, ctx);
771
+ case "prune": return handlePrune(params, ctx);
772
+ case "forget": return handleForget(params, ctx);
773
+ case "run": return handleRun(params, ctx);
774
+ case "status": return handleStatus(params, ctx);
775
+ case "cancel": return handleCancel(params, ctx);
776
+ case "plan": return handlePlan(params, ctx);
777
+ case "resume": return handleResume(params, ctx);
778
+ case "create": return handleCreate(params, ctx);
779
+ case "update": return handleUpdate(params, ctx);
780
+ case "delete": return handleDelete(params, ctx);
781
+ default: return result(`Unknown action: ${action}`, { action: "unknown", status: "error" }, true);
782
+ }
783
+ }