pi-crew 0.3.7 → 0.3.9
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.
- package/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/agents/discover-agents.ts +354 -15
- package/src/config/config.ts +732 -208
- package/src/config/types.ts +34 -5
- package/src/extension/help.ts +1 -0
- package/src/extension/register.ts +1173 -257
- package/src/extension/registration/commands.ts +15 -2
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/session-summary.ts +11 -1
- package/src/extension/team-tool/api.ts +4 -1
- package/src/extension/team-tool/cache-control.ts +23 -0
- package/src/extension/team-tool/cancel.ts +15 -5
- package/src/extension/team-tool/context.ts +2 -0
- package/src/extension/team-tool/handle-settings.ts +2 -0
- package/src/extension/team-tool/health-monitor.ts +563 -0
- package/src/extension/team-tool/inspect.ts +10 -3
- package/src/extension/team-tool/respond.ts +5 -2
- package/src/extension/team-tool/status.ts +4 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +901 -177
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/foreground-watchdog.ts +129 -0
- package/src/runtime/manifest-cache.ts +4 -2
- package/src/runtime/run-tracker.ts +11 -0
- package/src/runtime/runtime-policy.ts +15 -2
- package/src/runtime/skill-instructions.ts +8 -2
- package/src/runtime/stale-reconciler.ts +322 -18
- package/src/runtime/task-packet.ts +48 -1
- package/src/runtime/task-runner.ts +6 -1
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +204 -76
- package/src/state/state-store.ts +9 -1
- package/src/teams/discover-teams.ts +2 -1
- package/src/ui/run-event-bus.ts +2 -1
- package/src/ui/settings-overlay.ts +2 -0
- 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 {
|
|
6
|
-
|
|
7
|
-
|
|
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 {
|
|
45
|
+
import { listImportedRuns } from "./import-index.ts";
|
|
18
46
|
import { handleCreate, handleDelete, handleUpdate } from "./management.ts";
|
|
19
|
-
import {
|
|
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 {
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
import {
|
|
56
|
+
formatValidationReport,
|
|
57
|
+
validateResources,
|
|
58
|
+
} from "./validate-resources.ts";
|
|
59
|
+
|
|
33
60
|
type ExecuteTeamRunFn = typeof _executeTeamRunFn;
|
|
34
|
-
let _cachedExecuteTeamRun: ExecuteTeamRunFn | undefined
|
|
35
|
-
async function executeTeamRun(
|
|
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
|
-
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 {
|
|
52
|
-
|
|
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
|
|
59
|
-
async function handleRun(
|
|
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
|
-
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
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 {
|
|
76
|
-
import {
|
|
155
|
+
import { handleRespond } from "./team-tool/respond.ts";
|
|
156
|
+
import { handleStatus } from "./team-tool/status.ts";
|
|
77
157
|
|
|
78
|
-
export
|
|
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 {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
114
|
-
|
|
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
|
|
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(
|
|
128
|
-
|
|
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
|
|
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(
|
|
140
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 (
|
|
342
|
+
if (
|
|
343
|
+
task.checkpoint.phase === "artifact-written" &&
|
|
344
|
+
task.resultArtifact
|
|
345
|
+
) {
|
|
165
346
|
recovered.push(task.id);
|
|
166
|
-
return {
|
|
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(
|
|
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
|
|
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(
|
|
178
|
-
|
|
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, {
|
|
189
|
-
|
|
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 {
|
|
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(
|
|
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 [
|
|
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 = {
|
|
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(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!
|
|
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(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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(
|
|
505
|
+
const recovered = recoverCheckpointedTasks(
|
|
506
|
+
loaded.manifest,
|
|
507
|
+
loaded.tasks,
|
|
508
|
+
);
|
|
222
509
|
const resumeManifest = recovered.manifest;
|
|
223
|
-
const executedConfig = {
|
|
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 (
|
|
228
|
-
|
|
229
|
-
|
|
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 = {
|
|
532
|
+
const runtimeManifest = {
|
|
533
|
+
...resumeManifest,
|
|
534
|
+
runtimeResolution,
|
|
535
|
+
updatedAt: new Date().toISOString(),
|
|
536
|
+
};
|
|
234
537
|
saveRunManifest(runtimeManifest);
|
|
235
|
-
appendEvent(runtimeManifest.eventsPath, {
|
|
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(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"",
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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) =>
|
|
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, {
|
|
253
|
-
|
|
254
|
-
|
|
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 =
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
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(
|
|
672
|
+
return result(
|
|
673
|
+
"steer requires runId, taskId, and message",
|
|
674
|
+
{ action: "steer", status: "error" },
|
|
675
|
+
true,
|
|
676
|
+
);
|
|
266
677
|
}
|
|
267
|
-
const
|
|
268
|
-
if (!
|
|
269
|
-
|
|
270
|
-
|
|
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, {
|
|
275
|
-
|
|
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
|
-
|
|
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":
|
|
282
|
-
|
|
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 =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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":
|
|
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)
|
|
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(
|
|
304
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
318
|
-
|
|
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)
|
|
325
|
-
|
|
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, {
|
|
329
|
-
|
|
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 =
|
|
332
|
-
|
|
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(
|
|
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(
|
|
349
|
-
|
|
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":
|
|
352
|
-
|
|
353
|
-
case "
|
|
354
|
-
|
|
355
|
-
case "
|
|
356
|
-
|
|
357
|
-
case "
|
|
358
|
-
|
|
359
|
-
case "
|
|
360
|
-
|
|
361
|
-
case "
|
|
362
|
-
|
|
363
|
-
case "
|
|
364
|
-
|
|
365
|
-
case "
|
|
366
|
-
|
|
367
|
-
case "
|
|
368
|
-
|
|
369
|
-
case "
|
|
370
|
-
|
|
371
|
-
case "
|
|
372
|
-
|
|
373
|
-
case "
|
|
374
|
-
|
|
375
|
-
case "
|
|
376
|
-
|
|
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] =
|
|
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>)[
|
|
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. */
|