pi-crew 0.3.6 → 0.3.8

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 (44) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +1 -1
  3. package/src/agents/discover-agents.ts +2 -1
  4. package/src/config/config.ts +760 -229
  5. package/src/config/types.ts +34 -5
  6. package/src/extension/help.ts +1 -0
  7. package/src/extension/management.ts +2 -1
  8. package/src/extension/register.ts +1176 -255
  9. package/src/extension/registration/commands.ts +15 -2
  10. package/src/extension/registration/team-tool.ts +1 -1
  11. package/src/extension/session-summary.ts +11 -1
  12. package/src/extension/team-tool/api.ts +4 -1
  13. package/src/extension/team-tool/cache-control.ts +23 -0
  14. package/src/extension/team-tool/cancel.ts +27 -16
  15. package/src/extension/team-tool/context.ts +2 -0
  16. package/src/extension/team-tool/handle-settings.ts +2 -0
  17. package/src/extension/team-tool/health-monitor.ts +563 -0
  18. package/src/extension/team-tool/inspect.ts +10 -3
  19. package/src/extension/team-tool/lifecycle-actions.ts +12 -5
  20. package/src/extension/team-tool/respond.ts +6 -3
  21. package/src/extension/team-tool/status.ts +4 -1
  22. package/src/extension/team-tool-types.ts +2 -0
  23. package/src/extension/team-tool.ts +901 -177
  24. package/src/runtime/adaptive-plan.ts +1 -1
  25. package/src/runtime/child-pi.ts +15 -2
  26. package/src/runtime/crash-recovery.ts +30 -0
  27. package/src/runtime/foreground-watchdog.ts +129 -0
  28. package/src/runtime/manifest-cache.ts +4 -2
  29. package/src/runtime/pi-args.ts +3 -2
  30. package/src/runtime/run-tracker.ts +11 -0
  31. package/src/runtime/runtime-policy.ts +15 -2
  32. package/src/runtime/skill-instructions.ts +11 -0
  33. package/src/runtime/stale-reconciler.ts +322 -18
  34. package/src/runtime/task-runner.ts +8 -1
  35. package/src/schema/config-schema.ts +1 -0
  36. package/src/schema/team-tool-schema.ts +204 -76
  37. package/src/state/atomic-write.ts +2 -2
  38. package/src/state/locks.ts +19 -0
  39. package/src/state/mailbox.ts +22 -5
  40. package/src/state/state-store.ts +13 -3
  41. package/src/teams/discover-teams.ts +2 -1
  42. package/src/ui/run-event-bus.ts +2 -1
  43. package/src/ui/settings-overlay.ts +2 -0
  44. package/src/workflows/discover-workflows.ts +5 -1
@@ -1,38 +1,67 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { allAgents, discoverAgents, invalidateAgentDiscoveryCache, registerDynamicAgent, unregisterDynamicAgent, listDynamicAgents } from "../agents/discover-agents.ts";
4
3
  import type { AgentConfig } from "../agents/agent-config.ts";
5
- import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
6
- import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
7
- import { loadConfig, updateAutonomousConfig, updateConfig } from "../config/config.ts";
4
+ import {
5
+ allAgents,
6
+ discoverAgents,
7
+ invalidateAgentDiscoveryCache,
8
+ listDynamicAgents,
9
+ registerDynamicAgent,
10
+ unregisterDynamicAgent,
11
+ } from "../agents/discover-agents.ts";
12
+ import {
13
+ loadConfig,
14
+ updateAutonomousConfig,
15
+ updateConfig,
16
+ } from "../config/config.ts";
17
+ // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
18
+ // executeTeamRun is only called when a team run actually executes.
19
+ import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
8
20
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
9
- import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
10
- import { withRunLock, withRunLockSync } from "../state/locks.ts";
11
- import { aggregateUsage, formatUsage } from "../state/usage.ts";
12
- import { appendEvent, readEvents } from "../state/event-log.ts";
13
21
  import { writeArtifact } from "../state/artifact-store.ts";
22
+ import { appendEvent, readEvents } from "../state/event-log.ts";
23
+ import { withRunLock, withRunLockSync } from "../state/locks.ts";
14
24
  import { replayPendingMailboxMessages } from "../state/mailbox.ts";
25
+ import {
26
+ loadRunManifestById,
27
+ saveRunManifest,
28
+ saveRunTasks,
29
+ updateRunStatus,
30
+ } from "../state/state-store.ts";
31
+ import type {
32
+ ArtifactDescriptor,
33
+ TeamRunManifest,
34
+ TeamTaskState,
35
+ } from "../state/types.ts";
36
+ import { aggregateUsage, formatUsage } from "../state/usage.ts";
37
+ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
38
+ import {
39
+ allWorkflows,
40
+ discoverWorkflows,
41
+ } from "../workflows/discover-workflows.ts";
42
+ import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
15
43
  import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
16
44
  import { piTeamsHelp } from "./help.ts";
17
- import { initializeProject } from "./project-init.ts";
45
+ import { listImportedRuns } from "./import-index.ts";
18
46
  import { handleCreate, handleDelete, handleUpdate } from "./management.ts";
19
- import { pruneFinishedRuns } from "./run-maintenance.ts";
47
+ import { initializeProject } from "./project-init.ts";
20
48
  import { exportRunBundle } from "./run-export.ts";
21
49
  import { importRunBundle } from "./run-import.ts";
22
- import { listImportedRuns } from "./import-index.ts";
23
- import { handleSettings } from "./team-tool/handle-settings.ts";
24
50
  import { listRuns } from "./run-index.ts";
25
- import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
26
- import { formatValidationReport, validateResources } from "./validate-resources.ts";
51
+ import { pruneFinishedRuns } from "./run-maintenance.ts";
27
52
  import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
53
+ import { handleSettings } from "./team-tool/handle-settings.ts";
28
54
  import type { PiTeamsToolResult } from "./tool-result.ts";
29
- import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
30
- // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
31
- // executeTeamRun is only called when a team run actually executes.
32
- import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
55
+ import {
56
+ formatValidationReport,
57
+ validateResources,
58
+ } from "./validate-resources.ts";
59
+
33
60
  type ExecuteTeamRunFn = typeof _executeTeamRunFn;
34
- let _cachedExecuteTeamRun: ExecuteTeamRunFn | undefined = undefined;
35
- async function executeTeamRun(...args: Parameters<ExecuteTeamRunFn>): Promise<Awaited<ReturnType<ExecuteTeamRunFn>>> {
61
+ let _cachedExecuteTeamRun: ExecuteTeamRunFn | undefined;
62
+ async function executeTeamRun(
63
+ ...args: Parameters<ExecuteTeamRunFn>
64
+ ): Promise<Awaited<ReturnType<ExecuteTeamRunFn>>> {
36
65
  if (_cachedExecuteTeamRun === undefined) {
37
66
  // LAZY: heavy runtime — defer 1.4s import cost until team run actually executes.
38
67
  const mod = await import("../runtime/team-runner.ts");
@@ -40,23 +69,55 @@ async function executeTeamRun(...args: Parameters<ExecuteTeamRunFn>): Promise<Aw
40
69
  }
41
70
  return _cachedExecuteTeamRun(...args);
42
71
  }
43
- import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
44
- import { saveCrewAgents, readCrewAgents, recordFromTask } from "../runtime/crew-agent-records.ts";
45
- import { resolveCrewRuntime, runtimeResolutionState } from "../runtime/runtime-resolver.ts";
46
- import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
47
- import { writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
48
- import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
72
+
73
+ import {
74
+ applyAttentionState,
75
+ formatActivityAge,
76
+ resolveCrewControlConfig,
77
+ } from "../runtime/agent-control.ts";
78
+ import {
79
+ readCrewAgents,
80
+ recordFromTask,
81
+ saveCrewAgents,
82
+ } from "../runtime/crew-agent-records.ts";
49
83
  import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
84
+ import { writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
50
85
  import { parsePiJsonOutput } from "../runtime/pi-json-output.ts";
51
- import { buildParentContext, configRecord, formatScoped, result, type TeamContext } from "./team-tool/context.ts";
52
- import { autonomousPatchFromConfig, configPatchFromConfig, effectiveRunConfig, formatAutonomyStatus } from "./team-tool/config-patch.ts";
86
+ import {
87
+ checkProcessLiveness,
88
+ isActiveRunStatus,
89
+ } from "../runtime/process-status.ts";
90
+ import {
91
+ resolveCrewRuntime,
92
+ runtimeResolutionState,
93
+ } from "../runtime/runtime-resolver.ts";
94
+ import {
95
+ formatTaskGraphLines,
96
+ waitingReason,
97
+ } from "../runtime/task-display.ts";
53
98
  import { handleApi } from "./team-tool/api.ts";
99
+ import {
100
+ autonomousPatchFromConfig,
101
+ configPatchFromConfig,
102
+ effectiveRunConfig,
103
+ formatAutonomyStatus,
104
+ } from "./team-tool/config-patch.ts";
105
+ import {
106
+ buildParentContext,
107
+ configRecord,
108
+ formatScoped,
109
+ result,
110
+ type TeamContext,
111
+ } from "./team-tool/context.ts";
54
112
  // Lazy-loaded: run.ts pulls in spawnBackgroundTeamRun, resolveCrewRuntime, etc.
55
113
  // Static import fails silently in some jiti contexts (child-process), leaving handleRun undefined.
56
114
  import type { handleRun as _handleRunFn } from "./team-tool/run.ts";
115
+
57
116
  type HandleRunFn = typeof _handleRunFn;
58
- let _cachedHandleRun: HandleRunFn | undefined = undefined;
59
- async function handleRun(...args: Parameters<HandleRunFn>): Promise<Awaited<ReturnType<HandleRunFn>>> {
117
+ let _cachedHandleRun: HandleRunFn | undefined;
118
+ async function handleRun(
119
+ ...args: Parameters<HandleRunFn>
120
+ ): Promise<Awaited<ReturnType<HandleRunFn>>> {
60
121
  if (_cachedHandleRun === undefined) {
61
122
  // LAZY: run.ts pulls in spawnBackgroundTeamRun + resolveCrewRuntime; also avoids jiti import race in child-process contexts.
62
123
  const mod = await import("./team-tool/run.ts");
@@ -64,54 +125,138 @@ async function handleRun(...args: Parameters<HandleRunFn>): Promise<Awaited<Retu
64
125
  }
65
126
  return _cachedHandleRun(...args);
66
127
  }
67
- import { handleDoctor } from "./team-tool/doctor.ts";
68
- import { handleStatus } from "./team-tool/status.ts";
69
- import { handleArtifacts, handleEvents, handleSummary } from "./team-tool/inspect.ts";
70
- import { handleCleanup, handleExport, handleForget, handleImport, handleImports, handlePrune, handleWorktrees } from "./team-tool/lifecycle-actions.ts";
128
+
129
+ import { waitForRun } from "../runtime/run-tracker.ts";
130
+ import { normalizeSkillOverride } from "../runtime/skill-instructions.ts";
131
+ import { logInternalError } from "../utils/internal-error.ts";
132
+ import {
133
+ type CacheControlDeps,
134
+ invalidateSnapshot,
135
+ } from "./team-tool/cache-control.ts";
71
136
  import { handleCancel, handleRetry } from "./team-tool/cancel.ts";
137
+ import { handleDoctor } from "./team-tool/doctor.ts";
138
+ import { handleHealthMonitor } from "./team-tool/health-monitor.ts";
139
+ import {
140
+ handleArtifacts,
141
+ handleEvents,
142
+ handleSummary,
143
+ } from "./team-tool/inspect.ts";
144
+ import {
145
+ handleCleanup,
146
+ handleExport,
147
+ handleForget,
148
+ handleImport,
149
+ handleImports,
150
+ handlePrune,
151
+ handleWorktrees,
152
+ } from "./team-tool/lifecycle-actions.ts";
72
153
  import { handleParallel } from "./team-tool/parallel-dispatch.ts";
73
- import { handleRespond } from "./team-tool/respond.ts";
74
154
  import { handlePlan } from "./team-tool/plan.ts";
75
- import { logInternalError } from "../utils/internal-error.ts";
76
- import { normalizeSkillOverride } from "../runtime/skill-instructions.ts";
155
+ import { handleRespond } from "./team-tool/respond.ts";
156
+ import { handleStatus } from "./team-tool/status.ts";
77
157
 
78
- export type { TeamToolDetails } from "./team-tool-types.ts";
158
+ export { handleApi } from "./team-tool/api.ts";
159
+ export { handleRetry } from "./team-tool/cancel.ts";
79
160
  export type { TeamContext } from "./team-tool/context.ts";
80
- export { handleRun };
81
161
  export { handleDoctor } from "./team-tool/doctor.ts";
82
- export { handleStatus } from "./team-tool/status.ts";
83
- export { handleArtifacts, handleEvents, handleSummary } from "./team-tool/inspect.ts";
84
- export { handleCleanup, handleExport, handleForget, handleImport, handleImports, handlePrune, handleWorktrees } from "./team-tool/lifecycle-actions.ts";
85
- export { handleRetry } from "./team-tool/cancel.ts";
162
+ export {
163
+ handleArtifacts,
164
+ handleEvents,
165
+ handleSummary,
166
+ } from "./team-tool/inspect.ts";
167
+ export {
168
+ handleCleanup,
169
+ handleExport,
170
+ handleForget,
171
+ handleImport,
172
+ handleImports,
173
+ handlePrune,
174
+ handleWorktrees,
175
+ } from "./team-tool/lifecycle-actions.ts";
86
176
  export { handlePlan } from "./team-tool/plan.ts";
87
- export { handleApi } from "./team-tool/api.ts";
177
+ export { handleStatus } from "./team-tool/status.ts";
178
+ export type { TeamToolDetails } from "./team-tool-types.ts";
179
+ export { handleRun };
88
180
 
89
- export function handleList(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
181
+ export function handleList(
182
+ params: TeamToolParamsValue,
183
+ ctx: TeamContext,
184
+ ): PiTeamsToolResult {
90
185
  const resource = params.resource;
91
186
  const blocks: string[] = [];
92
187
  if (!resource || resource === "team") {
93
188
  const teams = allTeams(discoverTeams(ctx.cwd));
94
- blocks.push("Teams:", ...(teams.length ? teams.map((team) => formatScoped(team.name, team.source, team.description)) : ["- (none)"]));
189
+ blocks.push(
190
+ "Teams:",
191
+ ...(teams.length
192
+ ? teams.map((team) =>
193
+ formatScoped(team.name, team.source, team.description),
194
+ )
195
+ : ["- (none)"]),
196
+ );
95
197
  }
96
198
  if (!resource || resource === "workflow") {
97
199
  const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
98
- blocks.push("", "Workflows:", ...(workflows.length ? workflows.map((workflow) => formatScoped(workflow.name, workflow.source, workflow.description)) : ["- (none)"]));
200
+ blocks.push(
201
+ "",
202
+ "Workflows:",
203
+ ...(workflows.length
204
+ ? workflows.map((workflow) =>
205
+ formatScoped(
206
+ workflow.name,
207
+ workflow.source,
208
+ workflow.description,
209
+ ),
210
+ )
211
+ : ["- (none)"]),
212
+ );
99
213
  }
100
214
  if (!resource || resource === "agent") {
101
215
  const agents = allAgents(discoverAgents(ctx.cwd));
102
- blocks.push("", "Agents:", ...(agents.length ? agents.map((agent) => formatScoped(agent.name, agent.source, agent.description)) : ["- (none)"]));
216
+ blocks.push(
217
+ "",
218
+ "Agents:",
219
+ ...(agents.length
220
+ ? agents.map((agent) =>
221
+ formatScoped(
222
+ agent.name,
223
+ agent.source,
224
+ agent.description,
225
+ ),
226
+ )
227
+ : ["- (none)"]),
228
+ );
103
229
  }
104
230
  if (!resource) {
105
231
  const runs = listRuns(ctx.cwd).slice(0, 10);
106
- blocks.push("", "Recent runs:", ...(runs.length ? runs.map((run) => `- ${run.runId} [${run.status}] ${run.team}/${run.workflow ?? "none"}: ${run.goal}`) : ["- (none)"]));
232
+ blocks.push(
233
+ "",
234
+ "Recent runs:",
235
+ ...(runs.length
236
+ ? runs.map(
237
+ (run) =>
238
+ `- ${run.runId} [${run.status}] ${run.team}/${run.workflow ?? "none"}: ${run.goal}`,
239
+ )
240
+ : ["- (none)"]),
241
+ );
107
242
  }
108
243
  return result(blocks.join("\n"), { action: "list", status: "ok" });
109
244
  }
110
245
 
111
- export function handleGet(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
246
+ export function handleGet(
247
+ params: TeamToolParamsValue,
248
+ ctx: TeamContext,
249
+ ): PiTeamsToolResult {
112
250
  if (params.team) {
113
- const team = allTeams(discoverTeams(ctx.cwd)).find((item) => item.name === params.team);
114
- if (!team) return result(`Team '${params.team}' not found.`, { action: "get", status: "error" }, true);
251
+ const team = allTeams(discoverTeams(ctx.cwd)).find(
252
+ (item) => item.name === params.team,
253
+ );
254
+ if (!team)
255
+ return result(
256
+ `Team '${params.team}' not found.`,
257
+ { action: "get", status: "error" },
258
+ true,
259
+ );
115
260
  const lines = [
116
261
  `Team: ${team.name} (${team.source})`,
117
262
  `Path: ${team.filePath}`,
@@ -119,63 +264,118 @@ export function handleGet(params: TeamToolParamsValue, ctx: TeamContext): PiTeam
119
264
  `Default workflow: ${team.defaultWorkflow ?? "(none)"}`,
120
265
  `Workspace mode: ${team.workspaceMode ?? "single"}`,
121
266
  "Roles:",
122
- ...(team.roles.length ? team.roles.map((role) => `- ${role.name} -> ${role.agent}${role.description ? `: ${role.description}` : ""}`) : ["- (none)"]),
267
+ ...(team.roles.length
268
+ ? team.roles.map(
269
+ (role) =>
270
+ `- ${role.name} -> ${role.agent}${role.description ? `: ${role.description}` : ""}`,
271
+ )
272
+ : ["- (none)"]),
123
273
  ];
124
274
  return result(lines.join("\n"), { action: "get", status: "ok" });
125
275
  }
126
276
  if (params.workflow) {
127
- const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((item) => item.name === params.workflow);
128
- if (!workflow) return result(`Workflow '${params.workflow}' not found.`, { action: "get", status: "error" }, true);
277
+ const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find(
278
+ (item) => item.name === params.workflow,
279
+ );
280
+ if (!workflow)
281
+ return result(
282
+ `Workflow '${params.workflow}' not found.`,
283
+ { action: "get", status: "error" },
284
+ true,
285
+ );
129
286
  const lines = [
130
287
  `Workflow: ${workflow.name} (${workflow.source})`,
131
288
  `Path: ${workflow.filePath}`,
132
289
  `Description: ${workflow.description}`,
133
290
  "Steps:",
134
- ...(workflow.steps.length ? workflow.steps.map((step) => `- ${step.id} [${step.role}] dependsOn=${step.dependsOn?.join(",") ?? "none"}`) : ["- (none)"]),
291
+ ...(workflow.steps.length
292
+ ? workflow.steps.map(
293
+ (step) =>
294
+ `- ${step.id} [${step.role}] dependsOn=${step.dependsOn?.join(",") ?? "none"}`,
295
+ )
296
+ : ["- (none)"]),
135
297
  ];
136
298
  return result(lines.join("\n"), { action: "get", status: "ok" });
137
299
  }
138
300
  if (params.agent) {
139
- const agent = allAgents(discoverAgents(ctx.cwd)).find((item) => item.name === params.agent);
140
- if (!agent) return result(`Agent '${params.agent}' not found.`, { action: "get", status: "error" }, true);
301
+ const agent = allAgents(discoverAgents(ctx.cwd)).find(
302
+ (item) => item.name === params.agent,
303
+ );
304
+ if (!agent)
305
+ return result(
306
+ `Agent '${params.agent}' not found.`,
307
+ { action: "get", status: "error" },
308
+ true,
309
+ );
141
310
  const lines = [
142
311
  `Agent: ${agent.name} (${agent.source})`,
143
312
  `Path: ${agent.filePath}`,
144
313
  `Description: ${agent.description}`,
145
314
  agent.model ? `Model: ${agent.model}` : undefined,
146
- agent.skills?.length ? `Skills: ${agent.skills.join(", ")}` : undefined,
315
+ agent.skills?.length
316
+ ? `Skills: ${agent.skills.join(", ")}`
317
+ : undefined,
147
318
  "",
148
319
  agent.systemPrompt || "(empty system prompt)",
149
320
  ].filter((line): line is string => line !== undefined);
150
321
  return result(lines.join("\n"), { action: "get", status: "ok" });
151
322
  }
152
- return result("Specify team, workflow, or agent for get.", { action: "get", status: "error" }, true);
323
+ return result(
324
+ "Specify team, workflow, or agent for get.",
325
+ { action: "get", status: "error" },
326
+ true,
327
+ );
153
328
  }
154
329
 
155
330
  function artifactKey(artifact: ArtifactDescriptor): string {
156
331
  return `${artifact.kind}:${artifact.path}`;
157
332
  }
158
333
 
159
- function recoverCheckpointedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[]): { manifest: TeamRunManifest; tasks: TeamTaskState[]; recovered: string[] } {
334
+ function recoverCheckpointedTasks(
335
+ manifest: TeamRunManifest,
336
+ tasks: TeamTaskState[],
337
+ ): { manifest: TeamRunManifest; tasks: TeamTaskState[]; recovered: string[] } {
160
338
  const recovered: string[] = [];
161
339
  let nextManifest = manifest;
162
340
  const nextTasks = tasks.map((task) => {
163
341
  if (task.status !== "running" || !task.checkpoint) return task;
164
- if (task.checkpoint.phase === "artifact-written" && task.resultArtifact) {
342
+ if (
343
+ task.checkpoint.phase === "artifact-written" &&
344
+ task.resultArtifact
345
+ ) {
165
346
  recovered.push(task.id);
166
- return { ...task, status: "completed" as const, finishedAt: task.finishedAt ?? task.checkpoint.updatedAt, error: undefined, claim: undefined };
347
+ return {
348
+ ...task,
349
+ status: "completed" as const,
350
+ finishedAt: task.finishedAt ?? task.checkpoint.updatedAt,
351
+ error: undefined,
352
+ claim: undefined,
353
+ };
167
354
  }
168
355
  if (task.checkpoint.phase === "child-stdout-final") {
169
356
  // transcripts are written with .attempt-${i}.jsonl suffix; find the most recent one
170
- const transcriptsDir = path.join(manifest.artifactsRoot, "transcripts");
357
+ const transcriptsDir = path.join(
358
+ manifest.artifactsRoot,
359
+ "transcripts",
360
+ );
171
361
  let transcriptPath: string | undefined;
172
362
  if (fs.existsSync(transcriptsDir)) {
173
- const files = fs.readdirSync(transcriptsDir).filter((f) => f.startsWith(`${task.id}.attempt-`) && f.endsWith(".jsonl"));
363
+ const files = fs
364
+ .readdirSync(transcriptsDir)
365
+ .filter(
366
+ (f) =>
367
+ f.startsWith(`${task.id}.attempt-`) &&
368
+ f.endsWith(".jsonl"),
369
+ );
174
370
  if (files.length > 0) {
175
371
  // Sort by attempt index descending to get the most recent
176
372
  files.sort((a, b) => {
177
- const idxA = parseInt(a.match(/\.attempt-(\d+)\./)?.[1] ?? "0");
178
- const idxB = parseInt(b.match(/\.attempt-(\d+)\./)?.[1] ?? "0");
373
+ const idxA = parseInt(
374
+ a.match(/\.attempt-(\d+)\./)?.[1] ?? "0",
375
+ );
376
+ const idxB = parseInt(
377
+ b.match(/\.attempt-(\d+)\./)?.[1] ?? "0",
378
+ );
179
379
  return idxB - idxA;
180
380
  });
181
381
  transcriptPath = path.join(transcriptsDir, files[0]);
@@ -185,151 +385,626 @@ function recoverCheckpointedTasks(manifest: TeamRunManifest, tasks: TeamTaskStat
185
385
  const transcript = fs.readFileSync(transcriptPath, "utf-8");
186
386
  const parsed = parsePiJsonOutput(transcript);
187
387
  if (!parsed.finalText && !parsed.usage) return task;
188
- const resultArtifact = writeArtifact(manifest.artifactsRoot, { kind: "result", relativePath: `results/${task.id}.txt`, content: parsed.finalText ?? "(recovered from completed child transcript)", producer: task.id });
189
- const transcriptArtifact = writeArtifact(manifest.artifactsRoot, { kind: "log", relativePath: `transcripts/${task.id}.jsonl`, content: transcript, producer: task.id });
388
+ const resultArtifact = writeArtifact(manifest.artifactsRoot, {
389
+ kind: "result",
390
+ relativePath: `results/${task.id}.txt`,
391
+ content:
392
+ parsed.finalText ??
393
+ "(recovered from completed child transcript)",
394
+ producer: task.id,
395
+ });
396
+ const transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
397
+ kind: "log",
398
+ relativePath: `transcripts/${task.id}.jsonl`,
399
+ content: transcript,
400
+ producer: task.id,
401
+ });
190
402
  recovered.push(task.id);
191
- return { ...task, status: "completed" as const, finishedAt: task.finishedAt ?? task.checkpoint.updatedAt, error: undefined, claim: undefined, resultArtifact, transcriptArtifact, usage: parsed.usage, jsonEvents: parsed.jsonEvents };
403
+ return {
404
+ ...task,
405
+ status: "completed" as const,
406
+ finishedAt: task.finishedAt ?? task.checkpoint.updatedAt,
407
+ error: undefined,
408
+ claim: undefined,
409
+ resultArtifact,
410
+ transcriptArtifact,
411
+ usage: parsed.usage,
412
+ jsonEvents: parsed.jsonEvents,
413
+ };
192
414
  }
193
415
  return task;
194
416
  });
195
417
  if (recovered.length) {
196
- const artifacts = new Map(nextManifest.artifacts.map((artifact) => [artifactKey(artifact), artifact]));
418
+ const artifacts = new Map(
419
+ nextManifest.artifacts.map((artifact) => [
420
+ artifactKey(artifact),
421
+ artifact,
422
+ ]),
423
+ );
197
424
  for (const task of nextTasks) {
198
425
  if (!recovered.includes(task.id)) continue;
199
- for (const artifact of [task.promptArtifact, task.resultArtifact, task.logArtifact, task.transcriptArtifact].filter(Boolean) as ArtifactDescriptor[]) artifacts.set(artifactKey(artifact), artifact);
426
+ for (const artifact of [
427
+ task.promptArtifact,
428
+ task.resultArtifact,
429
+ task.logArtifact,
430
+ task.transcriptArtifact,
431
+ ].filter(Boolean) as ArtifactDescriptor[])
432
+ artifacts.set(artifactKey(artifact), artifact);
200
433
  }
201
- nextManifest = { ...nextManifest, artifacts: [...artifacts.values()], updatedAt: new Date().toISOString() };
434
+ nextManifest = {
435
+ ...nextManifest,
436
+ artifacts: [...artifacts.values()],
437
+ updatedAt: new Date().toISOString(),
438
+ };
202
439
  saveRunManifest(nextManifest);
203
440
  saveRunTasks(nextManifest, nextTasks);
204
441
  }
205
442
  return { manifest: nextManifest, tasks: nextTasks, recovered };
206
443
  }
207
444
 
208
- export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
209
- if (!params.runId) return result("Resume requires runId.", { action: "resume", status: "error" }, true);
210
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
211
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "resume", status: "error" }, true);
212
- if (!loaded.manifest.workflow) return result(`Run '${params.runId}' has no workflow to resume.`, { action: "resume", status: "error" }, true);
445
+ export async function handleResume(
446
+ params: TeamToolParamsValue,
447
+ ctx: TeamContext,
448
+ ): Promise<PiTeamsToolResult> {
449
+ if (!params.runId)
450
+ return result(
451
+ "Resume requires runId.",
452
+ { action: "resume", status: "error" },
453
+ true,
454
+ );
455
+ const runCwd = locateRunCwd(params.runId, ctx.cwd);
456
+ if (!runCwd)
457
+ return result(
458
+ `Run '${params.runId}' not found.`,
459
+ { action: "resume", status: "error" },
460
+ true,
461
+ );
462
+ const loaded = loadRunManifestById(runCwd, params.runId);
463
+ if (!loaded)
464
+ return result(
465
+ `Run '${params.runId}' not found.`,
466
+ { action: "resume", status: "error" },
467
+ true,
468
+ );
469
+ if (!loaded.manifest.workflow)
470
+ return result(
471
+ `Run '${params.runId}' has no workflow to resume.`,
472
+ { action: "resume", status: "error" },
473
+ true,
474
+ );
213
475
  const agents = allAgents(discoverAgents(ctx.cwd));
214
- const direct = directTeamAndWorkflowFromRun(loaded.manifest, loaded.tasks, agents);
215
- const team = direct?.team ?? allTeams(discoverTeams(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.team);
216
- if (!team) return result(`Team '${loaded.manifest.team}' not found.`, { action: "resume", status: "error" }, true);
217
- const workflow = direct?.workflow ?? allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
218
- if (!workflow) return result(`Workflow '${loaded.manifest.workflow}' not found.`, { action: "resume", status: "error" }, true);
476
+ const direct = directTeamAndWorkflowFromRun(
477
+ loaded.manifest,
478
+ loaded.tasks,
479
+ agents,
480
+ );
481
+ const team =
482
+ direct?.team ??
483
+ allTeams(discoverTeams(ctx.cwd)).find(
484
+ (candidate) => candidate.name === loaded.manifest.team,
485
+ );
486
+ if (!team)
487
+ return result(
488
+ `Team '${loaded.manifest.team}' not found.`,
489
+ { action: "resume", status: "error" },
490
+ true,
491
+ );
492
+ const workflow =
493
+ direct?.workflow ??
494
+ allWorkflows(discoverWorkflows(ctx.cwd)).find(
495
+ (candidate) => candidate.name === loaded.manifest.workflow,
496
+ );
497
+ if (!workflow)
498
+ return result(
499
+ `Workflow '${loaded.manifest.workflow}' not found.`,
500
+ { action: "resume", status: "error" },
501
+ true,
502
+ );
219
503
  return await withRunLock(loaded.manifest, async () => {
220
504
  const loadedConfig = loadConfig(ctx.cwd);
221
- const recovered = recoverCheckpointedTasks(loaded.manifest, loaded.tasks);
505
+ const recovered = recoverCheckpointedTasks(
506
+ loaded.manifest,
507
+ loaded.tasks,
508
+ );
222
509
  const resumeManifest = recovered.manifest;
223
- const executedConfig = { ...effectiveRunConfig(loadedConfig.config, params.config) };
510
+ const executedConfig = {
511
+ ...effectiveRunConfig(loadedConfig.config, params.config),
512
+ };
224
513
  // Preserve original manifest scaffold mode when resume has no explicit mode override
225
514
  // AND workers are not explicitly disabled. If workers are disabled, let
226
515
  // resolveCrewRuntime detect it and return blocked safety.
227
- if (!executedConfig.runtime?.mode && resumeManifest.runtimeResolution?.safety === "explicit_dry_run") {
228
- const workersDisabled = executedConfig.executeWorkers === false || process.env.PI_CREW_EXECUTE_WORKERS === "0" || process.env.PI_TEAMS_EXECUTE_WORKERS === "0";
229
- if (!workersDisabled) executedConfig.runtime = { ...executedConfig.runtime, mode: "scaffold" };
516
+ if (
517
+ !executedConfig.runtime?.mode &&
518
+ resumeManifest.runtimeResolution?.safety === "explicit_dry_run"
519
+ ) {
520
+ const workersDisabled =
521
+ executedConfig.executeWorkers === false ||
522
+ process.env.PI_CREW_EXECUTE_WORKERS === "0" ||
523
+ process.env.PI_TEAMS_EXECUTE_WORKERS === "0";
524
+ if (!workersDisabled)
525
+ executedConfig.runtime = {
526
+ ...executedConfig.runtime,
527
+ mode: "scaffold",
528
+ };
230
529
  }
231
530
  const runtime = await resolveCrewRuntime(executedConfig);
232
531
  const runtimeResolution = runtimeResolutionState(runtime);
233
- const runtimeManifest = { ...resumeManifest, runtimeResolution, updatedAt: new Date().toISOString() };
532
+ const runtimeManifest = {
533
+ ...resumeManifest,
534
+ runtimeResolution,
535
+ updatedAt: new Date().toISOString(),
536
+ };
234
537
  saveRunManifest(runtimeManifest);
235
- appendEvent(runtimeManifest.eventsPath, { type: "runtime.resolved", runId: runtimeManifest.runId, message: `Runtime resolved for resume: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution, action: "resume" } });
538
+ appendEvent(runtimeManifest.eventsPath, {
539
+ type: "runtime.resolved",
540
+ runId: runtimeManifest.runId,
541
+ message: `Runtime resolved for resume: ${runtime.kind} safety=${runtime.safety}`,
542
+ data: { runtimeResolution, action: "resume" },
543
+ });
236
544
  if (runtime.safety === "blocked") {
237
- const runningManifest = updateRunStatus(runtimeManifest, "running", "Checking worker runtime availability before resume.");
238
- const blocked = updateRunStatus(runningManifest, "blocked", runtime.reason ?? "Child worker execution is disabled; refusing to resume with no-op scaffold subagents.");
239
- appendEvent(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime, action: "resume" } });
240
- return result([
241
- `Blocked resume for pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
242
- `Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
243
- runtime.reason ?? "Child worker execution is disabled.",
244
- "",
245
- "To resume effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
246
- "Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
247
- ].join("\n"), { action: "resume", status: "error", runId: blocked.runId, artifactsRoot: blocked.artifactsRoot }, true);
545
+ const runningManifest = updateRunStatus(
546
+ runtimeManifest,
547
+ "running",
548
+ "Checking worker runtime availability before resume.",
549
+ );
550
+ const blocked = updateRunStatus(
551
+ runningManifest,
552
+ "blocked",
553
+ runtime.reason ??
554
+ "Child worker execution is disabled; refusing to resume with no-op scaffold subagents.",
555
+ );
556
+ appendEvent(blocked.eventsPath, {
557
+ type: "run.blocked",
558
+ runId: blocked.runId,
559
+ message: blocked.summary,
560
+ data: { runtime, action: "resume" },
561
+ });
562
+ return result(
563
+ [
564
+ `Blocked resume for pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
565
+ `Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
566
+ runtime.reason ?? "Child worker execution is disabled.",
567
+ "",
568
+ "To resume effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
569
+ "Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
570
+ ].join("\n"),
571
+ {
572
+ action: "resume",
573
+ status: "error",
574
+ runId: blocked.runId,
575
+ artifactsRoot: blocked.artifactsRoot,
576
+ },
577
+ true,
578
+ );
248
579
  }
249
- const resetTasks = recovered.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);
580
+ const resetTasks = recovered.tasks.map((task) =>
581
+ task.status === "failed" ||
582
+ task.status === "cancelled" ||
583
+ task.status === "skipped" ||
584
+ task.status === "running"
585
+ ? {
586
+ ...task,
587
+ status: "queued" as const,
588
+ error: undefined,
589
+ startedAt: undefined,
590
+ finishedAt: undefined,
591
+ claim: undefined,
592
+ }
593
+ : task,
594
+ );
250
595
  saveRunTasks(runtimeManifest, resetTasks);
251
596
  const replay = replayPendingMailboxMessages(runtimeManifest);
252
- appendEvent(runtimeManifest.eventsPath, { type: "run.resume_requested", runId: runtimeManifest.runId, data: { replayedMailboxMessages: replay.messages.length, recoveredCheckpointTasks: recovered.recovered } });
253
- if (recovered.recovered.length) appendEvent(runtimeManifest.eventsPath, { type: "task.checkpoint_recovered", runId: runtimeManifest.runId, message: `Recovered ${recovered.recovered.length} task(s) from artifact-written checkpoints.`, data: { taskIds: recovered.recovered } });
254
- if (replay.messages.length) appendEvent(runtimeManifest.eventsPath, { type: "mailbox.replayed", runId: runtimeManifest.runId, message: `Replayed ${replay.messages.length} pending inbox message(s).`, data: { messageIds: replay.messages.map((message) => message.id), taskIds: replay.messages.map((message) => message.taskId).filter(Boolean) } });
597
+ appendEvent(runtimeManifest.eventsPath, {
598
+ type: "run.resume_requested",
599
+ runId: runtimeManifest.runId,
600
+ data: {
601
+ replayedMailboxMessages: replay.messages.length,
602
+ recoveredCheckpointTasks: recovered.recovered,
603
+ },
604
+ });
605
+ if (recovered.recovered.length)
606
+ appendEvent(runtimeManifest.eventsPath, {
607
+ type: "task.checkpoint_recovered",
608
+ runId: runtimeManifest.runId,
609
+ message: `Recovered ${recovered.recovered.length} task(s) from artifact-written checkpoints.`,
610
+ data: { taskIds: recovered.recovered },
611
+ });
612
+ if (replay.messages.length)
613
+ appendEvent(runtimeManifest.eventsPath, {
614
+ type: "mailbox.replayed",
615
+ runId: runtimeManifest.runId,
616
+ message: `Replayed ${replay.messages.length} pending inbox message(s).`,
617
+ data: {
618
+ messageIds: replay.messages.map((message) => message.id),
619
+ taskIds: replay.messages
620
+ .map((message) => message.taskId)
621
+ .filter(Boolean),
622
+ },
623
+ });
255
624
  const executeWorkers = runtime.kind !== "scaffold";
256
- const resumeSkillOverride = normalizeSkillOverride(params.skill) ?? runtimeManifest.skillOverride;
257
- const executed = await executeTeamRun({ manifest: runtimeManifest, tasks: resetTasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, skillOverride: resumeSkillOverride, signal: ctx.signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, workspaceId: ctx.sessionId ?? ctx.cwd });
258
- 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");
625
+ const resumeSkillOverride =
626
+ normalizeSkillOverride(params.skill) ??
627
+ runtimeManifest.skillOverride;
628
+ const executed = await executeTeamRun({
629
+ manifest: runtimeManifest,
630
+ tasks: resetTasks,
631
+ team,
632
+ workflow,
633
+ agents,
634
+ executeWorkers,
635
+ limits: executedConfig.limits,
636
+ runtime,
637
+ runtimeConfig: executedConfig.runtime,
638
+ parentContext: buildParentContext(ctx),
639
+ parentModel: ctx.model,
640
+ modelRegistry: ctx.modelRegistry,
641
+ modelOverride: params.model,
642
+ skillOverride: resumeSkillOverride,
643
+ signal: ctx.signal,
644
+ reliability: executedConfig.reliability,
645
+ metricRegistry: ctx.metricRegistry,
646
+ workspaceId: ctx.sessionId ?? ctx.cwd,
647
+ });
648
+ return result(
649
+ [
650
+ `Resumed run ${executed.manifest.runId}.`,
651
+ `Status: ${executed.manifest.status}`,
652
+ `Tasks: ${executed.tasks.length}`,
653
+ `Artifacts: ${executed.manifest.artifactsRoot}`,
654
+ ].join("\n"),
655
+ {
656
+ action: "resume",
657
+ status: executed.manifest.status === "failed" ? "error" : "ok",
658
+ runId: executed.manifest.runId,
659
+ artifactsRoot: executed.manifest.artifactsRoot,
660
+ },
661
+ executed.manifest.status === "failed",
662
+ );
259
663
  });
260
664
  }
261
665
 
262
- export function handleSteer(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
666
+ export function handleSteer(
667
+ params: TeamToolParamsValue,
668
+ ctx: TeamContext,
669
+ ): PiTeamsToolResult {
263
670
  const { runId, taskId, message } = params;
264
671
  if (!runId || !taskId || !message) {
265
- return result("steer requires runId, taskId, and message", { action: "steer", status: "error" }, true);
672
+ return result(
673
+ "steer requires runId, taskId, and message",
674
+ { action: "steer", status: "error" },
675
+ true,
676
+ );
266
677
  }
267
- const loaded = loadRunManifestById(ctx.cwd, runId);
268
- if (!loaded) return result(`Run '${runId}' not found`, { action: "steer", status: "error" }, true);
269
- const task = loaded.tasks.find(t => t.id === taskId);
270
- if (!task) return result(`Task '${taskId}' not found`, { action: "steer", status: "error" }, true);
678
+ const runCwd = locateRunCwd(runId, ctx.cwd);
679
+ if (!runCwd)
680
+ return result(
681
+ `Run '${runId}' not found`,
682
+ { action: "steer", status: "error" },
683
+ true,
684
+ );
685
+ const loaded = loadRunManifestById(runCwd, runId);
686
+ if (!loaded)
687
+ return result(
688
+ `Run '${runId}' not found`,
689
+ { action: "steer", status: "error" },
690
+ true,
691
+ );
692
+ const task = loaded.tasks.find((t) => t.id === taskId);
693
+ if (!task)
694
+ return result(
695
+ `Task '${taskId}' not found`,
696
+ { action: "steer", status: "error" },
697
+ true,
698
+ );
271
699
  if (!task.pendingSteers) task.pendingSteers = [];
272
700
  task.pendingSteers.push(message);
273
701
  saveRunTasks(loaded.manifest, loaded.tasks);
274
- appendEvent(loaded.manifest.eventsPath, { type: "task.steer_queued", runId, taskId, data: { message } });
275
- return result(`Steer queued for task '${taskId}'. It will be delivered when the task's session is ready.`, { action: "steer", status: "ok" });
702
+ appendEvent(loaded.manifest.eventsPath, {
703
+ type: "task.steer_queued",
704
+ runId,
705
+ taskId,
706
+ data: { message },
707
+ });
708
+ return result(
709
+ `Steer queued for task '${taskId}'. It will be delivered when the task's session is ready.`,
710
+ { action: "steer", status: "ok" },
711
+ );
712
+ }
713
+
714
+ function cacheControlDepsFromContext(
715
+ ctx: TeamContext,
716
+ ): CacheControlDeps | undefined {
717
+ if (!ctx.getRunSnapshotCache) return undefined;
718
+ return { getRunSnapshotCache: ctx.getRunSnapshotCache };
276
719
  }
277
720
 
278
- export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
721
+ function handleInvalidate(
722
+ params: TeamToolParamsValue,
723
+ ctx: TeamContext,
724
+ ): PiTeamsToolResult {
725
+ const runId = params.runId;
726
+ if (!runId)
727
+ return result(
728
+ "Invalidate requires runId.",
729
+ { action: "invalidate", status: "error" },
730
+ true,
731
+ );
732
+ const runCwd = locateRunCwd(runId, ctx.cwd);
733
+ if (!runCwd)
734
+ return result(
735
+ `Run '${runId}' not found.`,
736
+ { action: "invalidate", status: "error" },
737
+ true,
738
+ );
739
+ const deps = cacheControlDepsFromContext(ctx);
740
+ if (!deps)
741
+ return result(
742
+ "Cache invalidation not available (no snapshot cache).",
743
+ { action: "invalidate", status: "error" },
744
+ true,
745
+ );
746
+ invalidateSnapshot(runId, runCwd, deps);
747
+ return result(`Cache invalidated for run ${runId}.`, {
748
+ action: "invalidate",
749
+ status: "ok",
750
+ runId,
751
+ });
752
+ }
753
+
754
+ /**
755
+ * Locate the CWD where a run's state is stored.
756
+ * Tries ctx.cwd first, then scans immediate child directories for .crew/state/runs/<runId>.
757
+ */
758
+ export function locateRunCwd(runId: string, baseCwd: string): string | undefined {
759
+ // Fast path: run is in the current CWD
760
+ if (loadRunManifestById(baseCwd, runId)) return baseCwd;
761
+
762
+ // Scan immediate child directories
763
+ try {
764
+ for (const entry of fs.readdirSync(baseCwd, { withFileTypes: true })) {
765
+ if (!entry.isDirectory()) continue;
766
+ const candidate = path.join(baseCwd, entry.name);
767
+ if (loadRunManifestById(candidate, runId)) return candidate;
768
+ }
769
+ } catch {
770
+ /* ignore unreadable dirs */
771
+ }
772
+
773
+ return undefined;
774
+ }
775
+
776
+ async function handleWait(
777
+ params: TeamToolParamsValue,
778
+ ctx: TeamContext,
779
+ ): Promise<PiTeamsToolResult> {
780
+ const { runId } = params;
781
+ if (!runId)
782
+ return result(
783
+ "wait requires runId.",
784
+ { action: "wait", status: "error" },
785
+ true,
786
+ );
787
+
788
+ const timeoutMs = Math.min(
789
+ Math.max(
790
+ typeof params.config?.timeoutMs === "number" &&
791
+ Number.isFinite(params.config.timeoutMs)
792
+ ? params.config.timeoutMs
793
+ : 300_000,
794
+ 1_000, // minimum 1 s
795
+ ),
796
+ 3_600_000, // maximum 1 h
797
+ );
798
+ const pollIntervalMs = Math.max(
799
+ Math.min(
800
+ typeof params.config?.pollIntervalMs === "number" &&
801
+ Number.isFinite(params.config.pollIntervalMs)
802
+ ? params.config.pollIntervalMs
803
+ : 2000,
804
+ 60_000, // maximum 60 s
805
+ ),
806
+ 500, // minimum 500 ms
807
+ );
808
+
809
+ // Resolve the run's CWD: try ctx.cwd first, then scan child dirs with .crew/
810
+ const runCwd = locateRunCwd(runId, ctx.cwd);
811
+ if (!runCwd) {
812
+ return result(
813
+ `Run '${runId}' not found in '${ctx.cwd}' or its subdirectories.`,
814
+ { action: "wait", status: "error", runId },
815
+ true,
816
+ );
817
+ }
818
+
819
+ try {
820
+ const { manifest, tasks } = await waitForRun(runId, runCwd, {
821
+ timeoutMs,
822
+ pollIntervalMs,
823
+ });
824
+ const taskSummary = tasks
825
+ .map((t) => ` ${t.id}: ${t.status}`)
826
+ .join("\n");
827
+ return result(
828
+ [
829
+ `Run ${runId} finished: ${manifest.status}`,
830
+ `Summary: ${manifest.summary ?? "(none)"}`,
831
+ `Tasks:`,
832
+ taskSummary,
833
+ ].join("\n"),
834
+ {
835
+ action: "wait",
836
+ status: manifest.status === "failed" ? "error" : "ok",
837
+ runId: manifest.runId,
838
+ },
839
+ manifest.status === "failed",
840
+ );
841
+ } catch (err) {
842
+ const msg = err instanceof Error ? err.message : String(err);
843
+ return result(
844
+ `wait failed: ${msg}`,
845
+ { action: "wait", status: "error", runId },
846
+ true,
847
+ );
848
+ }
849
+ }
850
+
851
+ export async function handleTeamTool(
852
+ params: TeamToolParamsValue,
853
+ ctx: TeamContext,
854
+ ): Promise<PiTeamsToolResult> {
279
855
  const action = params.action ?? "list";
280
856
  switch (action) {
281
- case "list": return handleList(params, ctx);
282
- case "get": return handleGet(params, ctx);
857
+ case "list":
858
+ return handleList(params, ctx);
859
+ case "get":
860
+ return handleGet(params, ctx);
283
861
  case "init": {
284
862
  const cfg = configRecord(params.config);
285
- const ignoreMethod = typeof cfg.ignoreMethod === "string" && (cfg.ignoreMethod === "gitignore" || cfg.ignoreMethod === "exclude") ? cfg.ignoreMethod : undefined;
286
- const initialized = initializeProject(ctx.cwd, { copyBuiltins: cfg.copyBuiltins === true, overwrite: cfg.overwrite === true, configScope: cfg.configScope === "project" || cfg.scope === "project" ? "project" : cfg.configScope === "none" || cfg.scope === "none" ? "none" : "global", ignoreMethod });
287
- return result([
288
- "Initialized pi-crew project layout.",
289
- "Directories:",
290
- ...(initialized.createdDirs.length ? initialized.createdDirs.map((dir) => `- created ${dir}`) : ["- already existed"]),
291
- "Copied builtin files:",
292
- ...(initialized.copiedFiles.length ? initialized.copiedFiles.map((file) => `- ${file}`) : ["- (none)"]),
293
- ...(initialized.skippedFiles.length ? ["Skipped existing files:", ...initialized.skippedFiles.map((file) => `- ${file}`)] : []),
294
- `Config: ${initialized.configPath || "(none)"} (${initialized.configScope}${initialized.configCreated ? "; created" : initialized.configSkipped ? "; already existed" : "; unchanged"})`,
295
- `Ignore: ${initialized.gitignorePath} (${initialized.gitignoreUpdated ? "updated" : "already configured"})`,
296
- ].join("\n"), { action: "init", status: "ok" });
863
+ const ignoreMethod =
864
+ typeof cfg.ignoreMethod === "string" &&
865
+ (cfg.ignoreMethod === "gitignore" ||
866
+ cfg.ignoreMethod === "exclude")
867
+ ? cfg.ignoreMethod
868
+ : undefined;
869
+ const initialized = initializeProject(ctx.cwd, {
870
+ copyBuiltins: cfg.copyBuiltins === true,
871
+ overwrite: cfg.overwrite === true,
872
+ configScope:
873
+ cfg.configScope === "project" || cfg.scope === "project"
874
+ ? "project"
875
+ : cfg.configScope === "none" || cfg.scope === "none"
876
+ ? "none"
877
+ : "global",
878
+ ignoreMethod,
879
+ });
880
+ return result(
881
+ [
882
+ "Initialized pi-crew project layout.",
883
+ "Directories:",
884
+ ...(initialized.createdDirs.length
885
+ ? initialized.createdDirs.map(
886
+ (dir) => `- created ${dir}`,
887
+ )
888
+ : ["- already existed"]),
889
+ "Copied builtin files:",
890
+ ...(initialized.copiedFiles.length
891
+ ? initialized.copiedFiles.map((file) => `- ${file}`)
892
+ : ["- (none)"]),
893
+ ...(initialized.skippedFiles.length
894
+ ? [
895
+ "Skipped existing files:",
896
+ ...initialized.skippedFiles.map(
897
+ (file) => `- ${file}`,
898
+ ),
899
+ ]
900
+ : []),
901
+ `Config: ${initialized.configPath || "(none)"} (${initialized.configScope}${initialized.configCreated ? "; created" : initialized.configSkipped ? "; already existed" : "; unchanged"})`,
902
+ `Ignore: ${initialized.gitignorePath} (${initialized.gitignoreUpdated ? "updated" : "already configured"})`,
903
+ ].join("\n"),
904
+ { action: "init", status: "ok" },
905
+ );
297
906
  }
298
- case "help": return result(piTeamsHelp(), { action: "help", status: "ok" });
907
+ case "help":
908
+ return result(piTeamsHelp(), { action: "help", status: "ok" });
299
909
  case "recommend": {
300
910
  const goal = params.goal ?? params.task;
301
- if (!goal) return result("Recommend requires goal or task.", { action: "recommend", status: "error" }, true);
911
+ if (!goal)
912
+ return result(
913
+ "Recommend requires goal or task.",
914
+ { action: "recommend", status: "error" },
915
+ true,
916
+ );
302
917
  const loaded = loadConfig(ctx.cwd);
303
- const recommendation = recommendTeam(goal, loaded.config.autonomous, { teams: allTeams(discoverTeams(ctx.cwd)), agents: allAgents(discoverAgents(ctx.cwd)) });
304
- return result(formatRecommendation(goal, recommendation), { action: "recommend", status: "ok" });
918
+ const recommendation = recommendTeam(
919
+ goal,
920
+ loaded.config.autonomous,
921
+ {
922
+ teams: allTeams(discoverTeams(ctx.cwd)),
923
+ agents: allAgents(discoverAgents(ctx.cwd)),
924
+ },
925
+ );
926
+ return result(formatRecommendation(goal, recommendation), {
927
+ action: "recommend",
928
+ status: "ok",
929
+ });
305
930
  }
306
931
  case "autonomy": {
307
932
  const patch = autonomousPatchFromConfig(params.config);
308
- const shouldUpdate = Object.values(patch).some((value) => value !== undefined);
933
+ const shouldUpdate = Object.values(patch).some(
934
+ (value) => value !== undefined,
935
+ );
309
936
  if (!shouldUpdate) {
310
937
  const loaded = loadConfig(ctx.cwd);
311
- return result(formatAutonomyStatus(loaded.config.autonomous, loaded.path, false), { action: "autonomy", status: loaded.error ? "error" : "ok" }, Boolean(loaded.error));
938
+ return result(
939
+ formatAutonomyStatus(
940
+ loaded.config.autonomous,
941
+ loaded.path,
942
+ false,
943
+ ),
944
+ {
945
+ action: "autonomy",
946
+ status: loaded.error ? "error" : "ok",
947
+ },
948
+ Boolean(loaded.error),
949
+ );
312
950
  }
313
951
  try {
314
952
  const saved = updateAutonomousConfig(patch);
315
- return result(formatAutonomyStatus(saved.config.autonomous, saved.path, true), { action: "autonomy", status: "ok" });
953
+ return result(
954
+ formatAutonomyStatus(
955
+ saved.config.autonomous,
956
+ saved.path,
957
+ true,
958
+ ),
959
+ { action: "autonomy", status: "ok" },
960
+ );
316
961
  } catch (error) {
317
- const message = error instanceof Error ? error.message : String(error);
318
- return result(message, { action: "autonomy", status: "error" }, true);
962
+ const message =
963
+ error instanceof Error ? error.message : String(error);
964
+ return result(
965
+ message,
966
+ { action: "autonomy", status: "error" },
967
+ true,
968
+ );
319
969
  }
320
970
  }
321
971
  case "config": {
322
972
  const patch = configPatchFromConfig(params.config);
323
973
  const cfg = configRecord(params.config);
324
- const unsetPaths = Array.isArray(cfg.unset) ? cfg.unset.filter((entry): entry is string => typeof entry === "string") : typeof cfg.unset === "string" ? [cfg.unset] : [];
325
- const shouldUpdate = Object.values(patch).some((value) => value !== undefined) || unsetPaths.length > 0;
974
+ const unsetPaths = Array.isArray(cfg.unset)
975
+ ? cfg.unset.filter(
976
+ (entry): entry is string => typeof entry === "string",
977
+ )
978
+ : typeof cfg.unset === "string"
979
+ ? [cfg.unset]
980
+ : [];
981
+ const shouldUpdate =
982
+ Object.values(patch).some((value) => value !== undefined) ||
983
+ unsetPaths.length > 0;
326
984
  if (shouldUpdate) {
327
985
  try {
328
- const saved = updateConfig(patch, { cwd: ctx.cwd, scope: cfg.scope === "project" ? "project" : "user", unsetPaths });
329
- return result(["Updated pi-crew config.", `Path: ${saved.path}`, "Effective config:", JSON.stringify(saved.config, null, 2)].join("\n"), { action: "config", status: "ok" });
986
+ const saved = updateConfig(patch, {
987
+ cwd: ctx.cwd,
988
+ scope: cfg.scope === "project" ? "project" : "user",
989
+ unsetPaths,
990
+ });
991
+ return result(
992
+ [
993
+ "Updated pi-crew config.",
994
+ `Path: ${saved.path}`,
995
+ "Effective config:",
996
+ JSON.stringify(saved.config, null, 2),
997
+ ].join("\n"),
998
+ { action: "config", status: "ok" },
999
+ );
330
1000
  } catch (error) {
331
- const message = error instanceof Error ? error.message : String(error);
332
- return result(message, { action: "config", status: "error" }, true);
1001
+ const message =
1002
+ error instanceof Error ? error.message : String(error);
1003
+ return result(
1004
+ message,
1005
+ { action: "config", status: "error" },
1006
+ true,
1007
+ );
333
1008
  }
334
1009
  }
335
1010
  const loaded = loadConfig(ctx.cwd);
@@ -341,39 +1016,85 @@ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamConte
341
1016
  JSON.stringify(loaded.config, null, 2),
342
1017
  "Schema: package export ./schema.json",
343
1018
  ];
344
- return result(lines.join("\n"), { action: "config", status: loaded.error ? "error" : "ok" }, Boolean(loaded.error));
1019
+ return result(
1020
+ lines.join("\n"),
1021
+ { action: "config", status: loaded.error ? "error" : "ok" },
1022
+ Boolean(loaded.error),
1023
+ );
345
1024
  }
346
1025
  case "validate": {
347
1026
  const report = validateResources(ctx.cwd);
348
- const hasErrors = report.issues.some((issue) => issue.level === "error");
349
- return result(formatValidationReport(report), { action: "validate", status: hasErrors ? "error" : "ok" }, hasErrors);
1027
+ const hasErrors = report.issues.some(
1028
+ (issue) => issue.level === "error",
1029
+ );
1030
+ return result(
1031
+ formatValidationReport(report),
1032
+ { action: "validate", status: hasErrors ? "error" : "ok" },
1033
+ hasErrors,
1034
+ );
350
1035
  }
351
- case "doctor": return handleDoctor(ctx, params);
352
- case "cleanup": return handleCleanup(params, ctx);
353
- case "api": return await handleApi(params, ctx);
354
- case "events": return handleEvents(params, ctx);
355
- case "artifacts": return handleArtifacts(params, ctx);
356
- case "worktrees": return handleWorktrees(params, ctx);
357
- case "summary": return handleSummary(params, ctx);
358
- case "export": return handleExport(params, ctx);
359
- case "import": return handleImport(params, ctx);
360
- case "imports": return handleImports(params, ctx);
361
- case "settings": return handleSettings(params, ctx);
362
- case "prune": return handlePrune(params, ctx);
363
- case "forget": return handleForget(params, ctx);
364
- case "run": return handleRun(params, ctx);
365
- case "status": return handleStatus(params, ctx);
366
- case "cancel": return handleCancel(params, ctx);
367
- case "retry": return handleRetry(params, ctx);
368
- case "respond": return handleRespond(params, ctx);
369
- case "parallel": return await handleParallel(params, ctx);
370
- case "plan": return handlePlan(params, ctx);
371
- case "resume": return handleResume(params, ctx);
372
- case "create": return handleCreate(params, ctx);
373
- case "update": return handleUpdate(params, ctx);
374
- case "delete": return handleDelete(params, ctx);
375
- case "steer": return handleSteer(params, ctx);
376
- default: return result(`Unknown action: ${action}`, { action: "unknown", status: "error" }, true);
1036
+ case "doctor":
1037
+ return handleDoctor(ctx, params);
1038
+ case "cleanup":
1039
+ return handleCleanup(params, ctx);
1040
+ case "api":
1041
+ return await handleApi(params, ctx);
1042
+ case "events":
1043
+ return handleEvents(params, ctx);
1044
+ case "artifacts":
1045
+ return handleArtifacts(params, ctx);
1046
+ case "worktrees":
1047
+ return handleWorktrees(params, ctx);
1048
+ case "summary":
1049
+ return handleSummary(params, ctx);
1050
+ case "export":
1051
+ return handleExport(params, ctx);
1052
+ case "import":
1053
+ return handleImport(params, ctx);
1054
+ case "imports":
1055
+ return handleImports(params, ctx);
1056
+ case "settings":
1057
+ return handleSettings(params, ctx);
1058
+ case "prune":
1059
+ return handlePrune(params, ctx);
1060
+ case "forget":
1061
+ return handleForget(params, ctx);
1062
+ case "run":
1063
+ return handleRun(params, ctx);
1064
+ case "status":
1065
+ return handleStatus(params, ctx);
1066
+ case "cancel":
1067
+ return handleCancel(params, ctx, cacheControlDepsFromContext(ctx));
1068
+ case "retry":
1069
+ return handleRetry(params, ctx, cacheControlDepsFromContext(ctx));
1070
+ case "invalidate":
1071
+ return handleInvalidate(params, ctx);
1072
+ case "respond":
1073
+ return handleRespond(params, ctx);
1074
+ case "parallel":
1075
+ return await handleParallel(params, ctx);
1076
+ case "plan":
1077
+ return handlePlan(params, ctx);
1078
+ case "resume":
1079
+ return handleResume(params, ctx);
1080
+ case "create":
1081
+ return handleCreate(params, ctx);
1082
+ case "update":
1083
+ return handleUpdate(params, ctx);
1084
+ case "delete":
1085
+ return handleDelete(params, ctx);
1086
+ case "steer":
1087
+ return handleSteer(params, ctx);
1088
+ case "health":
1089
+ return handleHealthMonitor(ctx, params);
1090
+ case "wait":
1091
+ return handleWait(params, ctx);
1092
+ default:
1093
+ return result(
1094
+ `Unknown action: ${action}`,
1095
+ { action: "unknown", status: "error" },
1096
+ true,
1097
+ );
377
1098
  }
378
1099
  }
379
1100
 
@@ -404,11 +1125,14 @@ interface CrewRegistry {
404
1125
  // registerAgent/unregisterAgent/listDynamicAgents for cross-extension access.
405
1126
 
406
1127
  export function registerCrewGlobalRegistry(registry: CrewRegistry): void {
407
- (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] = registry;
1128
+ (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] =
1129
+ registry;
408
1130
  }
409
1131
 
410
1132
  export function getCrewGlobalRegistry(): CrewRegistry | undefined {
411
- return (globalThis as Record<symbol | string, unknown>)[CREW_REGISTRY_KEY] as CrewRegistry | undefined;
1133
+ return (globalThis as Record<symbol | string, unknown>)[
1134
+ CREW_REGISTRY_KEY
1135
+ ] as CrewRegistry | undefined;
412
1136
  }
413
1137
 
414
1138
  /** Create and install the global CrewRegistry singleton. Call once at extension init. */