pi-crew 0.1.24 → 0.1.26
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/docs/refactor-tasks-phase3.md +394 -0
- package/docs/refactor-tasks-phase4.md +564 -0
- package/docs/refactor-tasks-phase5.md +402 -0
- package/docs/refactor-tasks.md +1484 -0
- package/package.json +98 -95
- package/src/agents/agent-config.ts +30 -30
- package/src/config/config.ts +153 -89
- package/src/config/defaults.ts +60 -0
- package/src/extension/autonomous-policy.ts +1 -1
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +15 -2
- package/src/extension/register.ts +124 -170
- package/src/extension/registration/command-utils.ts +54 -0
- package/src/extension/registration/subagent-helpers.ts +70 -0
- package/src/extension/registration/viewers.ts +32 -0
- package/src/extension/result-watcher.ts +98 -89
- package/src/extension/team-tool/api.ts +276 -0
- package/src/extension/team-tool/config-patch.ts +36 -0
- package/src/extension/team-tool/context.ts +48 -0
- package/src/extension/team-tool/doctor.ts +178 -0
- package/src/extension/team-tool/run.ts +133 -0
- package/src/extension/team-tool-types.ts +6 -0
- package/src/extension/team-tool.ts +31 -623
- package/src/extension/tool-result.ts +16 -16
- package/src/runtime/async-runner.ts +42 -60
- package/src/runtime/child-pi.ts +434 -332
- package/src/runtime/concurrency.ts +50 -42
- package/src/runtime/crew-agent-records.ts +166 -156
- package/src/runtime/manifest-cache.ts +214 -0
- package/src/runtime/parallel-utils.ts +99 -0
- package/src/runtime/post-exit-stdio-guard.ts +86 -0
- package/src/runtime/runtime-resolver.ts +77 -74
- package/src/runtime/subagent-manager.ts +291 -236
- package/src/runtime/task-graph-scheduler.ts +122 -107
- package/src/runtime/team-runner.ts +46 -51
- package/src/schema/config-schema.ts +92 -0
- package/src/state/artifact-store.ts +108 -36
- package/src/state/atomic-write.ts +114 -49
- package/src/state/event-log.ts +189 -138
- package/src/state/jsonl-writer.ts +77 -0
- package/src/state/locks.ts +149 -40
- package/src/state/mailbox.ts +200 -188
- package/src/state/state-store.ts +104 -15
- package/src/teams/discover-teams.ts +94 -84
- package/src/teams/team-config.ts +26 -22
- package/src/ui/crew-footer.ts +101 -0
- package/src/ui/crew-select-list.ts +111 -0
- package/src/ui/crew-widget.ts +285 -219
- package/src/ui/dynamic-border.ts +25 -0
- package/src/ui/layout-primitives.ts +106 -0
- package/src/ui/live-run-sidebar.ts +163 -95
- package/src/ui/loaders.ts +158 -0
- package/src/ui/mascot.ts +441 -0
- package/src/ui/powerbar-publisher.ts +94 -71
- package/src/ui/render-diff.ts +119 -0
- package/src/ui/run-dashboard.ts +155 -120
- package/src/ui/status-colors.ts +54 -0
- package/src/ui/syntax-highlight.ts +116 -0
- package/src/ui/theme-adapter.ts +190 -0
- package/src/ui/transcript-viewer.ts +194 -111
- package/src/utils/completion-dedupe.ts +63 -0
- package/src/utils/file-coalescer.ts +84 -33
- package/src/utils/fs-watch.ts +31 -0
- package/src/utils/git.ts +262 -0
- package/src/utils/internal-error.ts +6 -0
- package/src/utils/paths.ts +33 -15
- package/src/utils/sleep.ts +32 -0
- package/src/utils/timings.ts +31 -0
- package/src/utils/visual.ts +159 -0
- package/src/workflows/discover-workflows.ts +109 -101
- package/src/workflows/workflow-config.ts +25 -24
- package/src/workflows/workflow-serializer.ts +32 -31
- package/tsconfig.json +19 -19
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
2
|
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
6
3
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
7
4
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
8
|
-
import
|
|
9
|
-
import { effectiveAutonomousConfig, loadConfig, updateAutonomousConfig, updateConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../config/config.ts";
|
|
10
|
-
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
5
|
+
import { loadConfig, updateAutonomousConfig, updateConfig } from "../config/config.ts";
|
|
11
6
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
12
|
-
import {
|
|
13
|
-
import { createRunManifest, loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
7
|
+
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
14
8
|
import { withRunLock, withRunLockSync } from "../state/locks.ts";
|
|
15
|
-
import { canTransitionTaskStatus, isTeamTaskStatus } from "../state/contracts.ts";
|
|
16
|
-
import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../state/task-claims.ts";
|
|
17
|
-
import { acknowledgeMailboxMessage, appendMailboxMessage, readDeliveryState, readMailbox, validateMailbox, type MailboxDirection } from "../state/mailbox.ts";
|
|
18
9
|
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
19
|
-
import {
|
|
20
|
-
import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
|
|
21
|
-
import { getPiSpawnCommand } from "../runtime/pi-spawn.ts";
|
|
22
|
-
import { executeTeamRun } from "../runtime/team-runner.ts";
|
|
23
|
-
import { spawnBackgroundTeamRun } from "../runtime/async-runner.ts";
|
|
24
|
-
import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
|
|
25
|
-
import { appendEvent, readEvents, readEventsCursor } from "../state/event-log.ts";
|
|
10
|
+
import { appendEvent, readEvents } from "../state/event-log.ts";
|
|
26
11
|
import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
|
|
27
12
|
import { piTeamsHelp } from "./help.ts";
|
|
28
13
|
import { initializeProject } from "./project-init.ts";
|
|
@@ -32,69 +17,30 @@ import { exportRunBundle } from "./run-export.ts";
|
|
|
32
17
|
import { importRunBundle } from "./run-import.ts";
|
|
33
18
|
import { listImportedRuns } from "./import-index.ts";
|
|
34
19
|
import { listRuns } from "./run-index.ts";
|
|
20
|
+
import { validateWorkflowForTeam } from "../workflows/validate-workflow.ts";
|
|
35
21
|
import { formatValidationReport, validateResources } from "./validate-resources.ts";
|
|
36
22
|
import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
23
|
+
import type { PiTeamsToolResult } from "./tool-result.ts";
|
|
24
|
+
import { executeTeamRun } from "../runtime/team-runner.ts";
|
|
25
|
+
import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
|
|
26
|
+
import { saveCrewAgents, readCrewAgents, recordFromTask } from "../runtime/crew-agent-records.ts";
|
|
40
27
|
import { resolveCrewRuntime } from "../runtime/runtime-resolver.ts";
|
|
41
|
-
import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
|
|
42
28
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
43
|
-
import {
|
|
44
|
-
import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
|
|
45
|
-
import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../runtime/live-agent-manager.ts";
|
|
46
|
-
import { appendLiveAgentControlRequest } from "../runtime/live-agent-control.ts";
|
|
47
|
-
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
29
|
+
import { writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
|
|
48
30
|
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
49
31
|
import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
events?: { emit?: (event: string, data: unknown) => void };
|
|
63
|
-
signal?: AbortSignal;
|
|
64
|
-
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
65
|
-
onRunStarted?: (runId: string) => void;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
69
|
-
return toolResult(text, details, isError);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function formatScoped(name: string, source: string, description: string): string {
|
|
73
|
-
return `- ${name} (${source}): ${description}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function extractTextContent(content: unknown): string {
|
|
77
|
-
if (typeof content === "string") return content;
|
|
78
|
-
if (!Array.isArray(content)) return "";
|
|
79
|
-
return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && typeof (part as { text?: unknown }).text === "string" ? (part as { text: string }).text : "").filter(Boolean).join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function buildParentContext(ctx: TeamContext): string | undefined {
|
|
83
|
-
const branch = ctx.sessionManager?.getBranch?.();
|
|
84
|
-
if (!Array.isArray(branch) || branch.length === 0) return undefined;
|
|
85
|
-
const parts: string[] = [];
|
|
86
|
-
for (const entry of branch.slice(-20)) {
|
|
87
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
88
|
-
const record = entry as { type?: unknown; message?: unknown; summary?: unknown };
|
|
89
|
-
if (record.type === "compaction" && typeof record.summary === "string") parts.push(`[Summary]: ${record.summary}`);
|
|
90
|
-
const message = record.message && typeof record.message === "object" && !Array.isArray(record.message) ? record.message as { role?: unknown; content?: unknown } : undefined;
|
|
91
|
-
if (!message || (message.role !== "user" && message.role !== "assistant")) continue;
|
|
92
|
-
const text = extractTextContent(message.content).trim();
|
|
93
|
-
if (text) parts.push(`[${message.role === "user" ? "User" : "Assistant"}]: ${text}`);
|
|
94
|
-
}
|
|
95
|
-
if (!parts.length) return undefined;
|
|
96
|
-
return [`# Parent Conversation Context`, "The following context was inherited from the parent Pi session. Treat it as reference-only.", "", parts.join("\n\n")].join("\n");
|
|
97
|
-
}
|
|
32
|
+
import { buildParentContext, configRecord, formatScoped, result, type TeamContext } from "./team-tool/context.ts";
|
|
33
|
+
import { autonomousPatchFromConfig, configPatchFromConfig, formatAutonomyStatus } from "./team-tool/config-patch.ts";
|
|
34
|
+
import { handleApi } from "./team-tool/api.ts";
|
|
35
|
+
import { handleRun } from "./team-tool/run.ts";
|
|
36
|
+
import { handleDoctor } from "./team-tool/doctor.ts";
|
|
37
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
38
|
+
|
|
39
|
+
export type { TeamToolDetails } from "./team-tool-types.ts";
|
|
40
|
+
export type { TeamContext } from "./team-tool/context.ts";
|
|
41
|
+
export { handleRun } from "./team-tool/run.ts";
|
|
42
|
+
export { handleDoctor } from "./team-tool/doctor.ts";
|
|
43
|
+
export { handleApi } from "./team-tool/api.ts";
|
|
98
44
|
|
|
99
45
|
export function handleList(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
100
46
|
const resource = params.resource;
|
|
@@ -162,213 +108,6 @@ export function handleGet(params: TeamToolParamsValue, ctx: TeamContext): PiTeam
|
|
|
162
108
|
return result("Specify team, workflow, or agent for get.", { action: "get", status: "error" }, true);
|
|
163
109
|
}
|
|
164
110
|
|
|
165
|
-
function firstOutputLine(stdout: string | null | undefined, stderr: string | null | undefined): string {
|
|
166
|
-
const output = `${stdout ?? ""}\n${stderr ?? ""}`.trim();
|
|
167
|
-
return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "available";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function commandExists(command: string, args: string[]): { ok: boolean; detail: string } {
|
|
171
|
-
const output = spawnSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
172
|
-
if (!output.error && output.status === 0) return { ok: true, detail: firstOutputLine(output.stdout, output.stderr) };
|
|
173
|
-
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
|
|
177
|
-
const patch = configPatchFromConfig(rawOverride);
|
|
178
|
-
return {
|
|
179
|
-
...base,
|
|
180
|
-
...patch,
|
|
181
|
-
limits: patch.limits ? { ...(base.limits ?? {}), ...patch.limits } : base.limits,
|
|
182
|
-
runtime: patch.runtime ? { ...(base.runtime ?? {}), ...patch.runtime } : base.runtime,
|
|
183
|
-
control: patch.control ? { ...(base.control ?? {}), ...patch.control } : base.control,
|
|
184
|
-
worktree: patch.worktree ? { ...(base.worktree ?? {}), ...patch.worktree } : base.worktree,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function piCommandExists(): { ok: boolean; detail: string } {
|
|
189
|
-
const spec = getPiSpawnCommand(["--version"]);
|
|
190
|
-
const output = spawnSync(spec.command, spec.args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
191
|
-
if (!output.error && output.status === 0) {
|
|
192
|
-
const executable = spec.command === "pi" ? "pi" : `${spec.command} ${spec.args[0] ?? ""}`.trim();
|
|
193
|
-
return { ok: true, detail: `${firstOutputLine(output.stdout, output.stderr)} (${executable})` };
|
|
194
|
-
}
|
|
195
|
-
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function checkWritableDir(dir: string): { ok: boolean; detail: string } {
|
|
199
|
-
try {
|
|
200
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
201
|
-
const probe = path.join(dir, `.pi-crew-write-${process.pid}-${Date.now()}`);
|
|
202
|
-
fs.writeFileSync(probe, "ok", "utf-8");
|
|
203
|
-
fs.unlinkSync(probe);
|
|
204
|
-
return { ok: true, detail: dir };
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
-
return { ok: false, detail: `${dir}: ${message}` };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function handleDoctor(ctx: TeamContext, params: TeamToolParamsValue = {}): PiTeamsToolResult {
|
|
212
|
-
const discoveredAgents = allAgents(discoverAgents(ctx.cwd));
|
|
213
|
-
const agentCount = discoveredAgents.length;
|
|
214
|
-
const teamCount = allTeams(discoverTeams(ctx.cwd)).length;
|
|
215
|
-
const workflowCount = allWorkflows(discoverWorkflows(ctx.cwd)).length;
|
|
216
|
-
const git = commandExists("git", ["--version"]);
|
|
217
|
-
const pi = piCommandExists();
|
|
218
|
-
const loadedConfig = loadConfig(ctx.cwd);
|
|
219
|
-
const userWritable = checkWritableDir(path.join(userPiRoot(), "extensions", "pi-crew"));
|
|
220
|
-
const projectWritable = checkWritableDir(path.join(projectPiRoot(ctx.cwd), "teams"));
|
|
221
|
-
const validation = validateResources(ctx.cwd);
|
|
222
|
-
const validationErrors = validation.issues.filter((issue) => issue.level === "error").length;
|
|
223
|
-
const validationWarnings = validation.issues.filter((issue) => issue.level === "warning").length;
|
|
224
|
-
let smokeChildPi: { ok: boolean; detail: string } | undefined;
|
|
225
|
-
const doctorCfg = configRecord(params.config);
|
|
226
|
-
if (doctorCfg.smokeChildPi === true) {
|
|
227
|
-
try {
|
|
228
|
-
const spec = getPiSpawnCommand(["--mode", "json", "-p", "Reply with exactly PI-TEAMS-SMOKE-OK"]);
|
|
229
|
-
const output = execFileSync(spec.command, spec.args, { cwd: ctx.cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15_000 }).trim();
|
|
230
|
-
smokeChildPi = { ok: output.includes("PI-TEAMS-SMOKE-OK"), detail: output.split("\n").slice(-1)[0] ?? "completed" };
|
|
231
|
-
} catch (error) {
|
|
232
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
233
|
-
smokeChildPi = { ok: false, detail: message };
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const checks = [
|
|
237
|
-
{ label: "cwd", ok: fs.existsSync(ctx.cwd), detail: ctx.cwd },
|
|
238
|
-
{ label: "platform", ok: true, detail: `${process.platform}/${process.arch} node=${process.version}` },
|
|
239
|
-
{ label: "pi command", ok: pi.ok, detail: pi.detail },
|
|
240
|
-
{ label: "git command", ok: git.ok, detail: git.detail },
|
|
241
|
-
{ label: "user state writable", ok: userWritable.ok, detail: userWritable.detail },
|
|
242
|
-
{ label: "project state writable", ok: projectWritable.ok, detail: projectWritable.detail },
|
|
243
|
-
{ label: "config", ok: !loadedConfig.error, detail: loadedConfig.error ? `${loadedConfig.path}: ${loadedConfig.error}` : loadedConfig.path },
|
|
244
|
-
{ label: "current model", ok: true, detail: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "not available in this context" },
|
|
245
|
-
{ label: "resource model hints", ok: true, detail: `${discoveredAgents.filter((agent) => agent.model || agent.fallbackModels?.length).length} agents declare model/fallback preferences` },
|
|
246
|
-
{ label: "agents", ok: agentCount > 0, detail: `${agentCount} discovered` },
|
|
247
|
-
{ label: "teams", ok: teamCount > 0, detail: `${teamCount} discovered` },
|
|
248
|
-
{ label: "workflows", ok: workflowCount > 0, detail: `${workflowCount} discovered` },
|
|
249
|
-
{ label: "resource validation", ok: validationErrors === 0, detail: `${validationErrors} errors, ${validationWarnings} warnings` },
|
|
250
|
-
...(smokeChildPi ? [{ label: "child Pi smoke", ok: smokeChildPi.ok, detail: smokeChildPi.detail }] : []),
|
|
251
|
-
];
|
|
252
|
-
const text = ["pi-crew doctor:", ...checks.map((check) => `- ${check.ok ? "OK" : "FAIL"} ${check.label}: ${check.detail}`)].join("\n");
|
|
253
|
-
return result(text, { action: "doctor", status: checks.every((check) => check.ok) ? "ok" : "error" }, checks.some((check) => !check.ok));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
257
|
-
const goal = params.goal ?? params.task;
|
|
258
|
-
if (!goal) return result("Run requires goal or task.", { action: "run", status: "error" }, true);
|
|
259
|
-
|
|
260
|
-
const teams = allTeams(discoverTeams(ctx.cwd));
|
|
261
|
-
const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
|
|
262
|
-
const agents = allAgents(discoverAgents(ctx.cwd));
|
|
263
|
-
const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
|
|
264
|
-
if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
|
|
265
|
-
const teamName = params.team ?? "default";
|
|
266
|
-
const team = directAgent ? {
|
|
267
|
-
name: `direct-${directAgent.name}`,
|
|
268
|
-
description: `Direct subagent run for ${directAgent.name}`,
|
|
269
|
-
source: "builtin" as const,
|
|
270
|
-
filePath: "<generated>",
|
|
271
|
-
roles: [{ name: params.role ?? "agent", agent: directAgent.name, description: directAgent.description }],
|
|
272
|
-
defaultWorkflow: "direct-agent",
|
|
273
|
-
workspaceMode: params.workspaceMode,
|
|
274
|
-
} : teams.find((item) => item.name === teamName);
|
|
275
|
-
if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
|
|
276
|
-
const workflowName = directAgent ? "direct-agent" : params.workflow ?? team.defaultWorkflow ?? "default";
|
|
277
|
-
const baseWorkflow = directAgent ? {
|
|
278
|
-
name: "direct-agent",
|
|
279
|
-
description: `Direct task for ${directAgent.name}`,
|
|
280
|
-
source: "builtin" as const,
|
|
281
|
-
filePath: "<generated>",
|
|
282
|
-
steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
|
|
283
|
-
} : workflows.find((item) => item.name === workflowName);
|
|
284
|
-
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
285
|
-
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
|
|
286
|
-
|
|
287
|
-
const validationErrors = validateWorkflowForTeam(workflow, team);
|
|
288
|
-
if (validationErrors.length > 0) {
|
|
289
|
-
return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...validationErrors.map((error) => `- ${error}`)].join("\n"), { action: "run", status: "error" }, true);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const { manifest, tasks, paths } = createRunManifest({
|
|
293
|
-
cwd: ctx.cwd,
|
|
294
|
-
team,
|
|
295
|
-
workflow,
|
|
296
|
-
goal,
|
|
297
|
-
workspaceMode: params.workspaceMode,
|
|
298
|
-
});
|
|
299
|
-
const goalArtifact = writeArtifact(paths.artifactsRoot, {
|
|
300
|
-
kind: "prompt",
|
|
301
|
-
relativePath: "goal.md",
|
|
302
|
-
content: `${goal}\n`,
|
|
303
|
-
producer: "team-tool",
|
|
304
|
-
});
|
|
305
|
-
const updatedManifest = { ...manifest, artifacts: [goalArtifact], summary: "Run manifest created; worker execution is not implemented yet." };
|
|
306
|
-
atomicWriteJson(paths.manifestPath, updatedManifest);
|
|
307
|
-
|
|
308
|
-
const loadedConfig = loadConfig(ctx.cwd);
|
|
309
|
-
const runAsync = params.async ?? loadedConfig.config.asyncByDefault ?? false;
|
|
310
|
-
if (runAsync) {
|
|
311
|
-
const spawned = spawnBackgroundTeamRun(updatedManifest);
|
|
312
|
-
const asyncManifest = { ...updatedManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
|
|
313
|
-
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
314
|
-
appendEvent(updatedManifest.eventsPath, { type: "async.spawned", runId: updatedManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
315
|
-
const text = [
|
|
316
|
-
`Started async pi-crew run ${updatedManifest.runId}.`,
|
|
317
|
-
`Team: ${team.name}`,
|
|
318
|
-
`Workflow: ${workflow.name}`,
|
|
319
|
-
`Status: ${updatedManifest.status}`,
|
|
320
|
-
`Tasks: ${tasks.length}`,
|
|
321
|
-
`State: ${updatedManifest.stateRoot}`,
|
|
322
|
-
`Artifacts: ${updatedManifest.artifactsRoot}`,
|
|
323
|
-
`Background log: ${spawned.logPath}`,
|
|
324
|
-
"",
|
|
325
|
-
`Check status with: team status runId=${updatedManifest.runId}`,
|
|
326
|
-
].join("\n");
|
|
327
|
-
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
331
|
-
const executeWorkers = runtime.kind !== "scaffold";
|
|
332
|
-
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
333
|
-
if (executeWorkers && ctx.startForegroundRun) {
|
|
334
|
-
ctx.onRunStarted?.(updatedManifest.runId);
|
|
335
|
-
ctx.startForegroundRun(async (signal) => {
|
|
336
|
-
await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal });
|
|
337
|
-
}, updatedManifest.runId);
|
|
338
|
-
const text = [
|
|
339
|
-
`Started foreground pi-crew run ${updatedManifest.runId}.`,
|
|
340
|
-
`Team: ${team.name}`,
|
|
341
|
-
`Workflow: ${workflow.name}`,
|
|
342
|
-
"Status: running",
|
|
343
|
-
`Tasks: ${tasks.length}`,
|
|
344
|
-
`Runtime: ${runtime.kind}`,
|
|
345
|
-
`State: ${updatedManifest.stateRoot}`,
|
|
346
|
-
`Artifacts: ${updatedManifest.artifactsRoot}`,
|
|
347
|
-
"",
|
|
348
|
-
"The run continues in this Pi session without blocking the chat. It will be interrupted on session shutdown. Use /team-dashboard or /team-status to watch it.",
|
|
349
|
-
].join("\n");
|
|
350
|
-
return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
|
|
351
|
-
}
|
|
352
|
-
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
353
|
-
const text = [
|
|
354
|
-
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
355
|
-
`Team: ${team.name}`,
|
|
356
|
-
`Workflow: ${workflow.name}`,
|
|
357
|
-
`Status: ${executed.manifest.status}`,
|
|
358
|
-
`Tasks: ${executed.tasks.length}`,
|
|
359
|
-
`State: ${executed.manifest.stateRoot}`,
|
|
360
|
-
`Artifacts: ${executed.manifest.artifactsRoot}`,
|
|
361
|
-
"",
|
|
362
|
-
`Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`,
|
|
363
|
-
runtime.kind === "child-process"
|
|
364
|
-
? "Child Pi worker execution is enabled by default; each task is launched as a separate Pi process. Set runtime.mode=scaffold or executeWorkers=false only for dry runs."
|
|
365
|
-
: runtime.kind === "live-session"
|
|
366
|
-
? "Experimental live-session worker execution was enabled."
|
|
367
|
-
: "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
|
|
368
|
-
].join("\n");
|
|
369
|
-
return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
370
|
-
}
|
|
371
|
-
|
|
372
111
|
export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
373
112
|
if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
|
|
374
113
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
@@ -461,8 +200,16 @@ export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
461
200
|
}
|
|
462
201
|
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
463
202
|
saveRunTasks(loaded.manifest, tasks);
|
|
464
|
-
try {
|
|
465
|
-
|
|
203
|
+
try {
|
|
204
|
+
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
212
|
+
}
|
|
466
213
|
const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
|
|
467
214
|
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
468
215
|
});
|
|
@@ -538,89 +285,6 @@ export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
538
285
|
return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
539
286
|
}
|
|
540
287
|
|
|
541
|
-
function configRecord(config: unknown): Record<string, unknown> {
|
|
542
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) return {};
|
|
543
|
-
return config as Record<string, unknown>;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
|
|
547
|
-
const cfg = configRecord(config);
|
|
548
|
-
const profile = cfg.profile === "manual" || cfg.profile === "suggested" || cfg.profile === "assisted" || cfg.profile === "aggressive" ? cfg.profile : undefined;
|
|
549
|
-
const magicKeywords = cfg.magicKeywords && typeof cfg.magicKeywords === "object" && !Array.isArray(cfg.magicKeywords)
|
|
550
|
-
? Object.fromEntries(Object.entries(cfg.magicKeywords as Record<string, unknown>).filter((entry): entry is [string, string[]] => Array.isArray(entry[1]) && entry[1].every((item) => typeof item === "string")))
|
|
551
|
-
: undefined;
|
|
552
|
-
return {
|
|
553
|
-
profile,
|
|
554
|
-
enabled: typeof cfg.enabled === "boolean" ? cfg.enabled : undefined,
|
|
555
|
-
injectPolicy: typeof cfg.injectPolicy === "boolean" ? cfg.injectPolicy : undefined,
|
|
556
|
-
preferAsyncForLongTasks: typeof cfg.preferAsyncForLongTasks === "boolean" ? cfg.preferAsyncForLongTasks : undefined,
|
|
557
|
-
allowWorktreeSuggestion: typeof cfg.allowWorktreeSuggestion === "boolean" ? cfg.allowWorktreeSuggestion : undefined,
|
|
558
|
-
magicKeywords,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
563
|
-
const cfg = configRecord(config);
|
|
564
|
-
const control = configRecord(cfg.control);
|
|
565
|
-
const runtime = configRecord(cfg.runtime);
|
|
566
|
-
const limits = configRecord(cfg.limits);
|
|
567
|
-
const worktree = configRecord(cfg.worktree);
|
|
568
|
-
const ui = configRecord(cfg.ui);
|
|
569
|
-
return {
|
|
570
|
-
asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
|
|
571
|
-
executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
|
|
572
|
-
notifierIntervalMs: typeof cfg.notifierIntervalMs === "number" && Number.isFinite(cfg.notifierIntervalMs) ? cfg.notifierIntervalMs : undefined,
|
|
573
|
-
requireCleanWorktreeLeader: typeof cfg.requireCleanWorktreeLeader === "boolean" ? cfg.requireCleanWorktreeLeader : undefined,
|
|
574
|
-
autonomous: typeof cfg.autonomous === "object" && cfg.autonomous !== null && !Array.isArray(cfg.autonomous) ? autonomousPatchFromConfig(cfg.autonomous) : undefined,
|
|
575
|
-
limits: Object.keys(limits).length > 0 ? {
|
|
576
|
-
maxConcurrentWorkers: typeof limits.maxConcurrentWorkers === "number" && Number.isInteger(limits.maxConcurrentWorkers) && limits.maxConcurrentWorkers > 0 ? limits.maxConcurrentWorkers : undefined,
|
|
577
|
-
maxTaskDepth: typeof limits.maxTaskDepth === "number" && Number.isInteger(limits.maxTaskDepth) && limits.maxTaskDepth > 0 ? limits.maxTaskDepth : undefined,
|
|
578
|
-
maxChildrenPerTask: typeof limits.maxChildrenPerTask === "number" && Number.isInteger(limits.maxChildrenPerTask) && limits.maxChildrenPerTask > 0 ? limits.maxChildrenPerTask : undefined,
|
|
579
|
-
maxRunMinutes: typeof limits.maxRunMinutes === "number" && Number.isInteger(limits.maxRunMinutes) && limits.maxRunMinutes > 0 ? limits.maxRunMinutes : undefined,
|
|
580
|
-
maxRetriesPerTask: typeof limits.maxRetriesPerTask === "number" && Number.isInteger(limits.maxRetriesPerTask) && limits.maxRetriesPerTask > 0 ? limits.maxRetriesPerTask : undefined,
|
|
581
|
-
maxTasksPerRun: typeof limits.maxTasksPerRun === "number" && Number.isInteger(limits.maxTasksPerRun) && limits.maxTasksPerRun > 0 ? limits.maxTasksPerRun : undefined,
|
|
582
|
-
heartbeatStaleMs: typeof limits.heartbeatStaleMs === "number" && Number.isInteger(limits.heartbeatStaleMs) && limits.heartbeatStaleMs > 0 ? limits.heartbeatStaleMs : undefined,
|
|
583
|
-
} : undefined,
|
|
584
|
-
runtime: Object.keys(runtime).length > 0 ? {
|
|
585
|
-
mode: runtime.mode === "auto" || runtime.mode === "scaffold" || runtime.mode === "child-process" || runtime.mode === "live-session" ? runtime.mode : undefined,
|
|
586
|
-
preferLiveSession: typeof runtime.preferLiveSession === "boolean" ? runtime.preferLiveSession : undefined,
|
|
587
|
-
allowChildProcessFallback: typeof runtime.allowChildProcessFallback === "boolean" ? runtime.allowChildProcessFallback : undefined,
|
|
588
|
-
maxTurns: typeof runtime.maxTurns === "number" && Number.isInteger(runtime.maxTurns) && runtime.maxTurns > 0 ? runtime.maxTurns : undefined,
|
|
589
|
-
graceTurns: typeof runtime.graceTurns === "number" && Number.isInteger(runtime.graceTurns) && runtime.graceTurns > 0 ? runtime.graceTurns : undefined,
|
|
590
|
-
inheritContext: typeof runtime.inheritContext === "boolean" ? runtime.inheritContext : undefined,
|
|
591
|
-
promptMode: runtime.promptMode === "replace" || runtime.promptMode === "append" ? runtime.promptMode : undefined,
|
|
592
|
-
groupJoin: runtime.groupJoin === "off" || runtime.groupJoin === "group" || runtime.groupJoin === "smart" ? runtime.groupJoin : undefined,
|
|
593
|
-
} : undefined,
|
|
594
|
-
worktree: Object.keys(worktree).length > 0 ? {
|
|
595
|
-
setupHook: typeof worktree.setupHook === "string" && worktree.setupHook.trim() ? worktree.setupHook.trim() : undefined,
|
|
596
|
-
setupHookTimeoutMs: typeof worktree.setupHookTimeoutMs === "number" && Number.isInteger(worktree.setupHookTimeoutMs) && worktree.setupHookTimeoutMs > 0 ? worktree.setupHookTimeoutMs : undefined,
|
|
597
|
-
linkNodeModules: typeof worktree.linkNodeModules === "boolean" ? worktree.linkNodeModules : undefined,
|
|
598
|
-
} : undefined,
|
|
599
|
-
control: Object.keys(control).length > 0 ? {
|
|
600
|
-
enabled: typeof control.enabled === "boolean" ? control.enabled : undefined,
|
|
601
|
-
needsAttentionAfterMs: typeof control.needsAttentionAfterMs === "number" && Number.isInteger(control.needsAttentionAfterMs) && control.needsAttentionAfterMs > 0 ? control.needsAttentionAfterMs : undefined,
|
|
602
|
-
} : undefined,
|
|
603
|
-
ui: Object.keys(ui).length > 0 ? {
|
|
604
|
-
widgetPlacement: ui.widgetPlacement === "aboveEditor" || ui.widgetPlacement === "belowEditor" ? ui.widgetPlacement : undefined,
|
|
605
|
-
widgetMaxLines: typeof ui.widgetMaxLines === "number" && Number.isInteger(ui.widgetMaxLines) && ui.widgetMaxLines > 0 ? ui.widgetMaxLines : undefined,
|
|
606
|
-
powerbar: typeof ui.powerbar === "boolean" ? ui.powerbar : undefined,
|
|
607
|
-
} : undefined,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function formatAutonomyStatus(config: PiTeamsAutonomousConfig | undefined, pathValue: string, updated: boolean): string {
|
|
612
|
-
const effective = effectiveAutonomousConfig(config);
|
|
613
|
-
return [
|
|
614
|
-
updated ? "Updated pi-crew autonomous mode." : "pi-crew autonomous mode:",
|
|
615
|
-
`Path: ${pathValue}`,
|
|
616
|
-
`Profile: ${effective.profile}`,
|
|
617
|
-
`Enabled: ${effective.enabled}`,
|
|
618
|
-
`Inject policy: ${effective.injectPolicy}`,
|
|
619
|
-
`Prefer async for long tasks: ${effective.preferAsyncForLongTasks}`,
|
|
620
|
-
`Allow worktree suggestion: ${effective.allowWorktreeSuggestion}`,
|
|
621
|
-
].join("\n");
|
|
622
|
-
}
|
|
623
|
-
|
|
624
288
|
export function handleImports(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
625
289
|
const imports = listImportedRuns(ctx.cwd);
|
|
626
290
|
const lines = [
|
|
@@ -693,262 +357,6 @@ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
693
357
|
return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
694
358
|
}
|
|
695
359
|
|
|
696
|
-
export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
697
|
-
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
698
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
699
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
700
|
-
const cfg = configRecord(params.config);
|
|
701
|
-
const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
|
|
702
|
-
if (operation === "read-manifest") {
|
|
703
|
-
return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
704
|
-
}
|
|
705
|
-
if (operation === "list-tasks") {
|
|
706
|
-
return result(JSON.stringify(loaded.tasks, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
707
|
-
}
|
|
708
|
-
if (operation === "read-task") {
|
|
709
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
710
|
-
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
711
|
-
if (!task) return result("API read-task requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
712
|
-
return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
713
|
-
}
|
|
714
|
-
if (operation === "read-events") {
|
|
715
|
-
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
716
|
-
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
717
|
-
const payload = sinceSeq !== undefined || limit !== undefined
|
|
718
|
-
? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
|
|
719
|
-
: { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
|
|
720
|
-
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
721
|
-
}
|
|
722
|
-
if (operation === "runtime-capabilities") {
|
|
723
|
-
const loadedConfig = loadConfig(ctx.cwd);
|
|
724
|
-
return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
725
|
-
}
|
|
726
|
-
if (operation === "probe-live-session") {
|
|
727
|
-
return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
728
|
-
}
|
|
729
|
-
if (operation === "list-agents") {
|
|
730
|
-
return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
731
|
-
}
|
|
732
|
-
if (operation === "get-agent-result") {
|
|
733
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
734
|
-
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
735
|
-
if (!agent) return result("API get-agent-result requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
736
|
-
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
737
|
-
const text = task?.resultArtifact && fs.existsSync(task.resultArtifact.path) ? fs.readFileSync(task.resultArtifact.path, "utf-8") : JSON.stringify(agent, null, 2);
|
|
738
|
-
return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
739
|
-
}
|
|
740
|
-
if (operation === "read-agent-status") {
|
|
741
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
742
|
-
const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
|
|
743
|
-
const status = agent ? readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent : undefined;
|
|
744
|
-
if (!status) return result("API read-agent-status requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
745
|
-
return result(JSON.stringify(status, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
746
|
-
}
|
|
747
|
-
if (operation === "read-agent-events") {
|
|
748
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
749
|
-
const agents = readCrewAgents(loaded.manifest);
|
|
750
|
-
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
751
|
-
if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
752
|
-
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
753
|
-
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
754
|
-
const payload = sinceSeq !== undefined || limit !== undefined
|
|
755
|
-
? readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit })
|
|
756
|
-
: { path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) };
|
|
757
|
-
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
758
|
-
}
|
|
759
|
-
if (operation === "read-agent-transcript") {
|
|
760
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
761
|
-
const agents = readCrewAgents(loaded.manifest);
|
|
762
|
-
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
763
|
-
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
764
|
-
const transcriptPath = agent.transcriptPath && fs.existsSync(agent.transcriptPath) ? agent.transcriptPath : agentOutputPath(loaded.manifest, agent.taskId);
|
|
765
|
-
const text = fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : "";
|
|
766
|
-
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
767
|
-
}
|
|
768
|
-
if (operation === "read-agent-output") {
|
|
769
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
770
|
-
const agents = readCrewAgents(loaded.manifest);
|
|
771
|
-
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
772
|
-
if (!agent) return result("API read-agent-output requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
773
|
-
const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
|
|
774
|
-
return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
775
|
-
}
|
|
776
|
-
if (operation === "agent-dashboard") {
|
|
777
|
-
return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
778
|
-
}
|
|
779
|
-
if (operation === "foreground-status") {
|
|
780
|
-
return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
781
|
-
}
|
|
782
|
-
if (operation === "foreground-interrupt") {
|
|
783
|
-
const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
|
|
784
|
-
return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
785
|
-
}
|
|
786
|
-
if (operation === "nudge-agent") {
|
|
787
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
788
|
-
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
789
|
-
if (!agent) return result("API nudge-agent requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
790
|
-
const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
|
|
791
|
-
const message = appendMailboxMessage(loaded.manifest, { direction: "inbox", from: "leader", to: agent.taskId, taskId: agent.taskId, body: messageText });
|
|
792
|
-
appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
|
|
793
|
-
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
794
|
-
}
|
|
795
|
-
if (operation === "list-live-agents") {
|
|
796
|
-
return result(JSON.stringify(listLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
797
|
-
}
|
|
798
|
-
if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
|
|
799
|
-
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
800
|
-
if (!agentId) return result(`API ${operation} requires config.agentId.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
801
|
-
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
802
|
-
const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
|
|
803
|
-
try {
|
|
804
|
-
if (operation === "steer-agent") return result(JSON.stringify(await steerLiveAgent(agentId, message ?? "Please report current status and wrap up if possible."), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
805
|
-
if (operation === "resume-agent") {
|
|
806
|
-
if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
807
|
-
return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
808
|
-
}
|
|
809
|
-
return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
810
|
-
} catch (error) {
|
|
811
|
-
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
812
|
-
if (!agent) {
|
|
813
|
-
const err = error instanceof Error ? error.message : String(error);
|
|
814
|
-
return result(err, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
815
|
-
}
|
|
816
|
-
if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
817
|
-
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: agent.taskId, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
|
|
818
|
-
publishLiveControlRealtime(request);
|
|
819
|
-
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
820
|
-
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
|
|
821
|
-
return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
if (operation === "read-mailbox") {
|
|
825
|
-
const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
|
|
826
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
827
|
-
return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
828
|
-
}
|
|
829
|
-
if (operation === "validate-mailbox") {
|
|
830
|
-
const report = validateMailbox(loaded.manifest, { repair: cfg.repair === true });
|
|
831
|
-
return result(JSON.stringify(report, null, 2), { action: "api", status: report.issues.some((issue) => issue.level === "error") && cfg.repair !== true ? "error" : "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot }, report.issues.some((issue) => issue.level === "error") && cfg.repair !== true);
|
|
832
|
-
}
|
|
833
|
-
if (operation === "read-delivery") {
|
|
834
|
-
return result(JSON.stringify(readDeliveryState(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
835
|
-
}
|
|
836
|
-
if (operation === "send-message") {
|
|
837
|
-
const direction = cfg.direction === "outbox" ? "outbox" : "inbox";
|
|
838
|
-
const from = typeof cfg.from === "string" && cfg.from.trim() ? cfg.from.trim() : "api";
|
|
839
|
-
const to = typeof cfg.to === "string" && cfg.to.trim() ? cfg.to.trim() : "leader";
|
|
840
|
-
const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
|
|
841
|
-
const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
|
|
842
|
-
if (!body) return result("API send-message requires config.body.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
843
|
-
try {
|
|
844
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
845
|
-
const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
|
|
846
|
-
appendEvent(loaded.manifest.eventsPath, { type: "mailbox.message", runId: loaded.manifest.runId, data: { id: message.id, direction, from, to } });
|
|
847
|
-
return result(JSON.stringify(message, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
848
|
-
});
|
|
849
|
-
} catch (error) {
|
|
850
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
851
|
-
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
if (operation === "ack-message") {
|
|
855
|
-
const messageId = typeof cfg.messageId === "string" ? cfg.messageId : undefined;
|
|
856
|
-
if (!messageId) return result("API ack-message requires config.messageId.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
857
|
-
try {
|
|
858
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
859
|
-
const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
|
|
860
|
-
appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
|
|
861
|
-
return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
862
|
-
});
|
|
863
|
-
} catch (error) {
|
|
864
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
865
|
-
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
if (operation === "read-heartbeat") {
|
|
869
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
870
|
-
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
871
|
-
if (!task) return result("API read-heartbeat requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
872
|
-
return result(JSON.stringify(task.heartbeat ?? null, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
873
|
-
}
|
|
874
|
-
if (operation === "claim-task") {
|
|
875
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
876
|
-
const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
|
|
877
|
-
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
878
|
-
if (!task) return result("API claim-task requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
879
|
-
try {
|
|
880
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
881
|
-
const updatedTask = claimTask(task, owner);
|
|
882
|
-
const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
|
|
883
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
884
|
-
appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token: updatedTask.claim?.token, leasedUntil: updatedTask.claim?.leasedUntil } });
|
|
885
|
-
return result(JSON.stringify(updatedTask.claim, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
886
|
-
});
|
|
887
|
-
} catch (error) {
|
|
888
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
889
|
-
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
if (operation === "release-task-claim") {
|
|
893
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
894
|
-
const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
|
|
895
|
-
const token = typeof cfg.token === "string" ? cfg.token : undefined;
|
|
896
|
-
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
897
|
-
if (!task || !owner || !token) return result("API release-task-claim requires config.taskId, config.owner, and config.token.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
898
|
-
try {
|
|
899
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
900
|
-
const updatedTask = releaseTaskClaim(task, owner, token);
|
|
901
|
-
const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
|
|
902
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
903
|
-
appendEvent(loaded.manifest.eventsPath, { type: "task.claim_released", runId: loaded.manifest.runId, taskId: task.id, data: { owner } });
|
|
904
|
-
return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
905
|
-
});
|
|
906
|
-
} catch (error) {
|
|
907
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
908
|
-
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (operation === "transition-task-status") {
|
|
912
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
913
|
-
const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
|
|
914
|
-
const token = typeof cfg.token === "string" ? cfg.token : undefined;
|
|
915
|
-
const to = cfg.status;
|
|
916
|
-
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
917
|
-
if (!task || !owner || !token || !isTeamTaskStatus(to)) return result("API transition-task-status requires config.taskId, config.owner, config.token, and valid config.status.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
918
|
-
if (!canTransitionTaskStatus(task.status, to)) return result(`Invalid task status transition: ${task.status} -> ${to}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
919
|
-
try {
|
|
920
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
921
|
-
const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
|
|
922
|
-
const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
|
|
923
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
924
|
-
appendEvent(loaded.manifest.eventsPath, { type: "task.status_transitioned", runId: loaded.manifest.runId, taskId: task.id, data: { owner, status: to } });
|
|
925
|
-
return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
926
|
-
});
|
|
927
|
-
} catch (error) {
|
|
928
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
929
|
-
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
if (operation === "write-heartbeat") {
|
|
933
|
-
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
934
|
-
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
935
|
-
if (!task) return result("API write-heartbeat requires config.taskId matching a task id or step id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
936
|
-
try {
|
|
937
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
938
|
-
const heartbeat = touchWorkerHeartbeat(task.heartbeat ?? { workerId: task.id, lastSeenAt: new Date().toISOString() }, { alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined });
|
|
939
|
-
const tasks = loaded.tasks.map((item) => item.id === task.id ? { ...item, heartbeat } : item);
|
|
940
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
941
|
-
appendEvent(loaded.manifest.eventsPath, { type: "worker.heartbeat", runId: loaded.manifest.runId, taskId: task.id, data: { ...heartbeat } });
|
|
942
|
-
return result(JSON.stringify(heartbeat, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
943
|
-
});
|
|
944
|
-
} catch (error) {
|
|
945
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
946
|
-
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
360
|
export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
953
361
|
const action = params.action ?? "list";
|
|
954
362
|
switch (action) {
|