pi-crew 0.2.0 → 0.2.2

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.
@@ -1,441 +1,441 @@
1
- import * as fs from "node:fs";
2
- import { loadConfig } from "../../config/config.ts";
3
- import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
- import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
5
- import { withRunLockSync } from "../../state/locks.ts";
6
- import { canTransitionTaskStatus, isTeamTaskStatus } from "../../state/contracts.ts";
7
- import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../state/task-claims.ts";
8
- import { acknowledgeMailboxMessage, appendFollowUpMessage, appendMailboxMessage, appendSteeringMessage, readDeliveryState, readMailbox, readMailboxMessage, validateMailbox, type MailboxDirection, type MailboxMessageKind } from "../../state/mailbox.ts";
9
- import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log.ts";
10
- import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
11
- import { probeLiveSessionRuntime } from "../../subagents/live/session-runtime.ts";
12
- import { currentCrewRole, permissionForRole } from "../../runtime/role-permission.ts";
13
- import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
14
- import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
15
- import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
16
- import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
17
- import { followUpLiveAgent, getLiveAgent, listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
18
- import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
19
- import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
20
- import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
21
- import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
22
- import type { PiTeamsToolResult } from "../tool-result.ts";
23
- import { configRecord, result, type TeamContext } from "./context.ts";
24
-
25
- function globMatch(value: string, pattern: string): boolean {
26
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\?/g, "\\?").replace(/\*/g, ".*");
27
- return new RegExp(`^${escaped}$`).test(value);
28
- }
29
-
30
- function safeReadContainedFile(baseDir: string, filePath: string | undefined): string | undefined {
31
- if (!filePath) return undefined;
32
- let safePath: string;
33
- try {
34
- safePath = resolveRealContainedPath(baseDir, filePath);
35
- } catch {
36
- return undefined;
37
- }
38
- return fs.existsSync(safePath) ? fs.readFileSync(safePath, "utf-8") : undefined;
39
- }
40
-
41
- function safeContainedPath(baseDir: string, filePath: string | undefined): string | undefined {
42
- if (!filePath) return undefined;
43
- try {
44
- return resolveRealContainedPath(baseDir, filePath);
45
- } catch {
46
- return undefined;
47
- }
48
- }
49
-
50
- function snapshotHasRunId(snapshot: { values?: unknown }, runId: string): boolean {
51
- const values = Array.isArray(snapshot.values) ? snapshot.values : [];
52
- return values.some((value) => {
53
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
54
- const labels = (value as { labels?: unknown }).labels;
55
- return labels && typeof labels === "object" && !Array.isArray(labels) && (labels as Record<string, unknown>).runId === runId;
56
- });
57
- }
58
-
59
- function canApprovePlan(): { allowed: boolean; reason?: string } {
60
- const role = currentCrewRole();
61
- if (!role) return { allowed: true };
62
- if (permissionForRole(role) === "read_only") return { allowed: false, reason: `Role '${role}' is read-only and cannot approve or cancel plan gates.` };
63
- return { allowed: true };
64
- }
65
-
66
- export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
67
- const cfg = configRecord(params.config);
68
- const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
69
- if (operation === "metrics-snapshot") {
70
- const filter = typeof cfg.filter === "string" ? cfg.filter : undefined;
71
- const runIdFilter = typeof cfg.runId === "string" ? cfg.runId : params.runId;
72
- const snapshots = ctx.metricRegistry?.snapshot() ?? [];
73
- const filtered = snapshots.filter((snapshot) => {
74
- if (filter && !globMatch(snapshot.name, filter)) return false;
75
- if (runIdFilter && !snapshotHasRunId(snapshot, runIdFilter)) return false;
76
- return true;
77
- });
78
- return result(JSON.stringify(filtered, null, 2), { action: "api", status: "ok", ...(runIdFilter ? { runId: runIdFilter } : {}) });
79
- }
80
- if (operation === "inventory") {
81
- const inventory = buildCapabilityInventory(ctx.cwd, ctx.config);
82
- return result(JSON.stringify(inventory, null, 2), { action: "api", status: "ok" });
83
- }
84
- if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
85
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
86
- if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
87
- if (operation === "read-manifest") {
88
- return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
89
- }
90
- if (operation === "approve-plan") {
91
- const permission = canApprovePlan();
92
- if (!permission.allowed) return result(permission.reason ?? "Plan approval is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
93
- try {
94
- return withRunLockSync(loaded.manifest, () => {
95
- const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
96
- const approval = current.manifest.planApproval;
97
- if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
98
- const now = new Date().toISOString();
99
- const manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "approved" as const, approvedAt: now, updatedAt: now } };
100
- saveRunManifest(manifest);
101
- appendEvent(manifest.eventsPath, { type: "plan.approved", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan approved; resume the run to execute mutating tasks.", metadata: { provenance: "api" } });
102
- return result(JSON.stringify(manifest.planApproval, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
103
- });
104
- } catch (error) {
105
- const message = error instanceof Error ? error.message : String(error);
106
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
107
- }
108
- }
109
- if (operation === "cancel-plan") {
110
- const permission = canApprovePlan();
111
- if (!permission.allowed) return result(permission.reason ?? "Plan approval cancellation is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
112
- try {
113
- return withRunLockSync(loaded.manifest, () => {
114
- const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
115
- const approval = current.manifest.planApproval;
116
- if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
117
- const now = new Date().toISOString();
118
- const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" || task.status === "waiting" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
119
- let manifest: typeof current.manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "cancelled" as const, cancelledAt: now, updatedAt: now } };
120
- saveRunManifest(manifest);
121
- saveRunTasks(manifest, tasks);
122
- appendEvent(manifest.eventsPath, { type: "plan.cancelled", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan was cancelled.", metadata: { provenance: "api" } });
123
- manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
124
- return result(JSON.stringify({ planApproval: manifest.planApproval, cancelledTasks: tasks.filter((task) => task.status === "cancelled").map((task) => task.id) }, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
125
- });
126
- } catch (error) {
127
- const message = error instanceof Error ? error.message : String(error);
128
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
129
- }
130
- }
131
- if (operation === "list-tasks") {
132
- return result(JSON.stringify(loaded.tasks, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
133
- }
134
- if (operation === "read-task") {
135
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
136
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
137
- 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);
138
- return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
139
- }
140
- if (operation === "read-events") {
141
- const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
142
- const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
143
- const payload = sinceSeq !== undefined || limit !== undefined
144
- ? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
145
- : { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
146
- return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
147
- }
148
- if (operation === "runtime-capabilities") {
149
- const loadedConfig = loadConfig(ctx.cwd);
150
- return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
151
- }
152
- if (operation === "probe-live-session") {
153
- return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
154
- }
155
- if (operation === "list-agents") {
156
- return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
157
- }
158
- if (operation === "get-agent-result") {
159
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
160
- const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
161
- 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);
162
- const task = loaded.tasks.find((item) => item.id === agent.taskId);
163
- const text = safeReadContainedFile(loaded.manifest.artifactsRoot, task?.resultArtifact?.path) ?? JSON.stringify(agent, null, 2);
164
- return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
165
- }
166
- if (operation === "read-agent-status") {
167
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
168
- const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
169
- const status = agent ? readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent : undefined;
170
- 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);
171
- return result(JSON.stringify(status, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
172
- }
173
- if (operation === "read-agent-events") {
174
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
175
- const agents = readCrewAgents(loaded.manifest);
176
- const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
177
- 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);
178
- const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
179
- const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
180
- const cursorPayload = readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit });
181
- const payload = sinceSeq !== undefined || limit !== undefined ? cursorPayload : { path: cursorPayload.path, events: cursorPayload.events };
182
- return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
183
- }
184
- if (operation === "read-agent-transcript") {
185
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
186
- const agents = readCrewAgents(loaded.manifest);
187
- const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
188
- 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);
189
- const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
190
- const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
191
- const artifactText = artifactTranscriptPath ? safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "" : "";
192
- const fallbackText = artifactText ? "" : safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "";
193
- const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
194
- const text = artifactText || fallbackText;
195
- return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
196
- }
197
- if (operation === "read-agent-output") {
198
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
199
- const agents = readCrewAgents(loaded.manifest);
200
- const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
201
- 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);
202
- const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
203
- return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
204
- }
205
- if (operation === "agent-dashboard") {
206
- return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
207
- }
208
- if (operation === "foreground-status") {
209
- return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
210
- }
211
- if (operation === "foreground-interrupt") {
212
- const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
213
- return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
214
- }
215
- if (operation === "nudge-agent") {
216
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
217
- const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
218
- 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);
219
- const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
220
- const message = appendSteeringMessage(loaded.manifest, { taskId: agent.taskId, to: agent.taskId, body: messageText, priority: "normal", data: { source: "nudge-agent" } });
221
- appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
222
- ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction: message.direction, from: message.from, to: message.to, taskId: message.taskId, source: "nudge-agent" });
223
- return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
224
- }
225
- if (operation === "list-live-agents") {
226
- 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 });
227
- }
228
- if (operation === "steer-agent" || operation === "follow-up-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
229
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
230
- if (!agentId) return result(`API ${operation} requires config.agentId.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
231
- const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
232
- const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
233
- try {
234
- const live = getLiveAgent(agentId);
235
- if (live && live.runId !== loaded.manifest.runId) return result(`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
236
- if (!live && (operation === "steer-agent" || operation === "follow-up-agent")) throw new Error(`Live agent '${agentId}' not found.`);
237
- const liveTaskId = live?.taskId;
238
- if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId) throw new Error(`Live agent '${agentId}' not found.`);
239
- const targetTaskId = liveTaskId ?? agentId;
240
- if (operation === "steer-agent") {
241
- const text = message ?? "Please report current status and wrap up if possible.";
242
- const realtime = await steerLiveAgent(agentId, text);
243
- const mailboxMessage = appendSteeringMessage(loaded.manifest, { taskId: targetTaskId, body: text, status: "delivered", data: { source: "steer-agent", realtime: true } });
244
- return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
245
- }
246
- if (operation === "follow-up-agent") {
247
- if (!prompt) return result("API follow-up-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
248
- const realtime = await followUpLiveAgent(agentId, prompt);
249
- const mailboxMessage = appendFollowUpMessage(loaded.manifest, { taskId: targetTaskId, body: prompt, status: "delivered", data: { source: "follow-up-agent", realtime: true } });
250
- return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
251
- }
252
- if (operation === "resume-agent") {
253
- if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
254
- return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
255
- }
256
- return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
257
- } catch (error) {
258
- const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
259
- if (!agent) {
260
- const err = error instanceof Error ? error.message : String(error);
261
- return result(err, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
262
- }
263
- const task = loaded.tasks.find((item) => item.id === agent.taskId);
264
- if (!task) return result(`API ${operation} agent '${agentId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
265
- 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);
266
- if (operation === "follow-up-agent" && !prompt) return result("API follow-up-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
267
- try {
268
- const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "follow-up-agent" ? "follow-up" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message });
269
- const mailboxMessage = operation === "steer-agent" ? appendSteeringMessage(loaded.manifest, { taskId: task.id, to: agent.id, body: message ?? "Please report current status and wrap up if possible.", status: "delivered", data: { source: "steer-agent", liveControlRequestId: request.id } }) : operation === "follow-up-agent" && prompt ? appendFollowUpMessage(loaded.manifest, { taskId: task.id, to: agent.id, body: prompt, status: "delivered", data: { source: "follow-up-agent", liveControlRequestId: request.id } }) : undefined;
270
- publishLiveControlRealtime(request);
271
- ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
272
- 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, mailboxMessageId: mailboxMessage?.id, realtime: true } });
273
- return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
274
- } catch (queueError) {
275
- const message = queueError instanceof Error ? queueError.message : String(queueError);
276
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
277
- }
278
- }
279
- }
280
- if (operation === "read-mailbox") {
281
- const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
282
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
283
- const kind = typeof cfg.kind === "string" && ["message", "steer", "follow-up", "response", "group_join"].includes(cfg.kind) ? cfg.kind as MailboxMessageKind : undefined;
284
- if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API read-mailbox taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
285
- try {
286
- return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId, kind), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
287
- } catch (error) {
288
- const message = error instanceof Error ? error.message : String(error);
289
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
290
- }
291
- }
292
- if (operation === "validate-mailbox") {
293
- const report = validateMailbox(loaded.manifest, { repair: cfg.repair === true });
294
- 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);
295
- }
296
- if (operation === "read-delivery") {
297
- return result(JSON.stringify(readDeliveryState(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
298
- }
299
- if (operation === "send-message") {
300
- const direction = cfg.direction === "outbox" ? "outbox" : "inbox";
301
- const from = typeof cfg.from === "string" && cfg.from.trim() ? cfg.from.trim() : "api";
302
- const to = typeof cfg.to === "string" && cfg.to.trim() ? cfg.to.trim() : "leader";
303
- const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
304
- const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
305
- if (!body) return result("API send-message requires config.body.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
306
- if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API send-message taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
307
- try {
308
- return withRunLockSync(loaded.manifest, () => {
309
- const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
310
- appendEvent(loaded.manifest.eventsPath, { type: "mailbox.message", runId: loaded.manifest.runId, data: { id: message.id, direction, from, to } });
311
- ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction, from, to, taskId, source: "send-message" });
312
- return result(JSON.stringify(message, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
313
- });
314
- } catch (error) {
315
- const message = error instanceof Error ? error.message : String(error);
316
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
317
- }
318
- }
319
- if (operation === "ack-message") {
320
- const messageId = typeof cfg.messageId === "string" ? cfg.messageId : undefined;
321
- if (!messageId) return result("API ack-message requires config.messageId.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
322
- try {
323
- return withRunLockSync(loaded.manifest, () => {
324
- const message = readMailboxMessage(loaded.manifest, messageId);
325
- const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
326
- appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
327
- if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
328
- appendEvent(loaded.manifest.eventsPath, {
329
- type: "agent.group_join.acknowledged",
330
- runId: loaded.manifest.runId,
331
- message: "Group join delivery acknowledged via mailbox ack.",
332
- data: { requestId: message.data.requestId, messageId, batchId: message.data.batchId, partial: message.data.partial, acknowledgedAt: delivery.updatedAt, acknowledgedBy: "leader" },
333
- metadata: { provenance: "api" },
334
- });
335
- }
336
- ctx.events?.emit?.("crew.mailbox.acknowledged", { runId: loaded.manifest.runId, messageId, delivery });
337
- return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
338
- });
339
- } catch (error) {
340
- const message = error instanceof Error ? error.message : String(error);
341
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
342
- }
343
- }
344
- if (operation === "read-heartbeat") {
345
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
346
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
347
- 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);
348
- return result(JSON.stringify(task.heartbeat ?? null, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
349
- }
350
- if (operation === "claim-task") {
351
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
352
- const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
353
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
354
- 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);
355
- try {
356
- return withRunLockSync(loaded.manifest, () => {
357
- const updatedTask = claimTask(task, owner);
358
- const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
359
- saveRunTasks(loaded.manifest, tasks);
360
- appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token: updatedTask.claim?.token, leasedUntil: updatedTask.claim?.leasedUntil } });
361
- return result(JSON.stringify(updatedTask.claim, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
362
- });
363
- } catch (error) {
364
- const message = error instanceof Error ? error.message : String(error);
365
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
366
- }
367
- }
368
- if (operation === "release-task-claim") {
369
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
370
- const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
371
- const token = typeof cfg.token === "string" ? cfg.token : undefined;
372
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
373
- 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);
374
- try {
375
- return withRunLockSync(loaded.manifest, () => {
376
- const updatedTask = releaseTaskClaim(task, owner, token);
377
- const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
378
- saveRunTasks(loaded.manifest, tasks);
379
- appendEvent(loaded.manifest.eventsPath, { type: "task.claim_released", runId: loaded.manifest.runId, taskId: task.id, data: { owner } });
380
- return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
381
- });
382
- } catch (error) {
383
- const message = error instanceof Error ? error.message : String(error);
384
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
385
- }
386
- }
387
- if (operation === "transition-task-status") {
388
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
389
- const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
390
- const token = typeof cfg.token === "string" ? cfg.token : undefined;
391
- const to = cfg.status;
392
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
393
- 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);
394
- if (!canTransitionTaskStatus(task.status, to)) return result(`Invalid task status transition: ${task.status} -> ${to}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
395
- try {
396
- return withRunLockSync(loaded.manifest, () => {
397
- const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
398
- const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
399
- saveRunTasks(loaded.manifest, tasks);
400
- appendEvent(loaded.manifest.eventsPath, { type: "task.status_transitioned", runId: loaded.manifest.runId, taskId: task.id, data: { owner, status: to } });
401
- return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
402
- });
403
- } catch (error) {
404
- const message = error instanceof Error ? error.message : String(error);
405
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
406
- }
407
- }
408
- if (operation === "write-heartbeat") {
409
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
410
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
411
- 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);
412
- try {
413
- return withRunLockSync(loaded.manifest, () => {
414
- const heartbeat = touchWorkerHeartbeat(task.heartbeat ?? { workerId: task.id, lastSeenAt: new Date().toISOString() }, { alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined });
415
- const tasks = loaded.tasks.map((item) => item.id === task.id ? { ...item, heartbeat } : item);
416
- saveRunTasks(loaded.manifest, tasks);
417
- appendEvent(loaded.manifest.eventsPath, { type: "worker.heartbeat", runId: loaded.manifest.runId, taskId: task.id, data: { ...heartbeat } });
418
- return result(JSON.stringify(heartbeat, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
419
- });
420
- } catch (error) {
421
- const message = error instanceof Error ? error.message : String(error);
422
- return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
423
- }
424
- }
425
- if (operation === "diff") {
426
- const diffArtifacts = loaded.manifest.artifacts.filter(a => a.kind === "diff" || a.kind === "patch");
427
- if (diffArtifacts.length === 0) {
428
- return result(`No diff artifacts found for run ${loaded.manifest.runId}. Diffs are captured in worktree mode.`, { action: "api", status: "ok", runId: loaded.manifest.runId, intent: `diff ${loaded.manifest.runId}: no diffs` });
429
- }
430
- const parts: string[] = [`Diff artifacts for run ${loaded.manifest.runId}:`];
431
- for (const artifact of diffArtifacts) {
432
- const content = safeReadContainedFile(loaded.manifest.artifactsRoot, artifact.path);
433
- if (content) {
434
- const display = content.length > 4000 ? content.slice(0, 4000) + "\n... (truncated)" : content;
435
- parts.push(`\n--- ${artifact.path} ---\n${display}`);
436
- }
437
- }
438
- return result(parts.join("\n"), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot, intent: `diff ${loaded.manifest.runId}` });
439
- }
440
- return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
441
- }
1
+ import * as fs from "node:fs";
2
+ import { loadConfig } from "../../config/config.ts";
3
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
4
+ import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
5
+ import { withRunLockSync } from "../../state/locks.ts";
6
+ import { canTransitionTaskStatus, isTeamTaskStatus } from "../../state/contracts.ts";
7
+ import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../state/task-claims.ts";
8
+ import { acknowledgeMailboxMessage, appendFollowUpMessage, appendMailboxMessage, appendSteeringMessage, readDeliveryState, readMailbox, readMailboxMessage, validateMailbox, type MailboxDirection, type MailboxMessageKind } from "../../state/mailbox.ts";
9
+ import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log.ts";
10
+ import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
11
+ import { probeLiveSessionRuntime } from "../../subagents/live/session-runtime.ts";
12
+ import { currentCrewRole, permissionForRole } from "../../runtime/role-permission.ts";
13
+ import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
14
+ import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
15
+ import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
16
+ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
17
+ import { followUpLiveAgent, getLiveAgent, listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
18
+ import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
19
+ import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
20
+ import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
21
+ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
22
+ import type { PiTeamsToolResult } from "../tool-result.ts";
23
+ import { configRecord, result, type TeamContext } from "./context.ts";
24
+
25
+ function globMatch(value: string, pattern: string): boolean {
26
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\?/g, "\\?").replace(/\*/g, ".*");
27
+ return new RegExp(`^${escaped}$`).test(value);
28
+ }
29
+
30
+ function safeReadContainedFile(baseDir: string, filePath: string | undefined): string | undefined {
31
+ if (!filePath) return undefined;
32
+ let safePath: string;
33
+ try {
34
+ safePath = resolveRealContainedPath(baseDir, filePath);
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ return fs.existsSync(safePath) ? fs.readFileSync(safePath, "utf-8") : undefined;
39
+ }
40
+
41
+ function safeContainedPath(baseDir: string, filePath: string | undefined): string | undefined {
42
+ if (!filePath) return undefined;
43
+ try {
44
+ return resolveRealContainedPath(baseDir, filePath);
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ function snapshotHasRunId(snapshot: { values?: unknown }, runId: string): boolean {
51
+ const values = Array.isArray(snapshot.values) ? snapshot.values : [];
52
+ return values.some((value) => {
53
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
54
+ const labels = (value as { labels?: unknown }).labels;
55
+ return labels && typeof labels === "object" && !Array.isArray(labels) && (labels as Record<string, unknown>).runId === runId;
56
+ });
57
+ }
58
+
59
+ function canApprovePlan(): { allowed: boolean; reason?: string } {
60
+ const role = currentCrewRole();
61
+ if (!role) return { allowed: true };
62
+ if (permissionForRole(role) === "read_only") return { allowed: false, reason: `Role '${role}' is read-only and cannot approve or cancel plan gates.` };
63
+ return { allowed: true };
64
+ }
65
+
66
+ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
67
+ const cfg = configRecord(params.config);
68
+ const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
69
+ if (operation === "metrics-snapshot") {
70
+ const filter = typeof cfg.filter === "string" ? cfg.filter : undefined;
71
+ const runIdFilter = typeof cfg.runId === "string" ? cfg.runId : params.runId;
72
+ const snapshots = ctx.metricRegistry?.snapshot() ?? [];
73
+ const filtered = snapshots.filter((snapshot) => {
74
+ if (filter && !globMatch(snapshot.name, filter)) return false;
75
+ if (runIdFilter && !snapshotHasRunId(snapshot, runIdFilter)) return false;
76
+ return true;
77
+ });
78
+ return result(JSON.stringify(filtered, null, 2), { action: "api", status: "ok", ...(runIdFilter ? { runId: runIdFilter } : {}) });
79
+ }
80
+ if (operation === "inventory") {
81
+ const inventory = buildCapabilityInventory(ctx.cwd, ctx.config);
82
+ return result(JSON.stringify(inventory, null, 2), { action: "api", status: "ok" });
83
+ }
84
+ if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
85
+ const loaded = loadRunManifestById(ctx.cwd, params.runId);
86
+ if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
87
+ if (operation === "read-manifest") {
88
+ return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
89
+ }
90
+ if (operation === "approve-plan") {
91
+ const permission = canApprovePlan();
92
+ if (!permission.allowed) return result(permission.reason ?? "Plan approval is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
93
+ try {
94
+ return withRunLockSync(loaded.manifest, () => {
95
+ const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
96
+ const approval = current.manifest.planApproval;
97
+ if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
98
+ const now = new Date().toISOString();
99
+ const manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "approved" as const, approvedAt: now, updatedAt: now } };
100
+ saveRunManifest(manifest);
101
+ appendEvent(manifest.eventsPath, { type: "plan.approved", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan approved; resume the run to execute mutating tasks.", metadata: { provenance: "api" } });
102
+ return result(JSON.stringify(manifest.planApproval, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
103
+ });
104
+ } catch (error) {
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
107
+ }
108
+ }
109
+ if (operation === "cancel-plan") {
110
+ const permission = canApprovePlan();
111
+ if (!permission.allowed) return result(permission.reason ?? "Plan approval cancellation is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
112
+ try {
113
+ return withRunLockSync(loaded.manifest, () => {
114
+ const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
115
+ const approval = current.manifest.planApproval;
116
+ if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
117
+ const now = new Date().toISOString();
118
+ const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" || task.status === "waiting" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
119
+ let manifest: typeof current.manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "cancelled" as const, cancelledAt: now, updatedAt: now } };
120
+ saveRunManifest(manifest);
121
+ saveRunTasks(manifest, tasks);
122
+ appendEvent(manifest.eventsPath, { type: "plan.cancelled", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan was cancelled.", metadata: { provenance: "api" } });
123
+ manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
124
+ return result(JSON.stringify({ planApproval: manifest.planApproval, cancelledTasks: tasks.filter((task) => task.status === "cancelled").map((task) => task.id) }, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
125
+ });
126
+ } catch (error) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
129
+ }
130
+ }
131
+ if (operation === "list-tasks") {
132
+ return result(JSON.stringify(loaded.tasks, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
133
+ }
134
+ if (operation === "read-task") {
135
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
136
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
137
+ 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);
138
+ return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
139
+ }
140
+ if (operation === "read-events") {
141
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
142
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
143
+ const payload = sinceSeq !== undefined || limit !== undefined
144
+ ? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
145
+ : { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
146
+ return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
147
+ }
148
+ if (operation === "runtime-capabilities") {
149
+ const loadedConfig = loadConfig(ctx.cwd);
150
+ return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
151
+ }
152
+ if (operation === "probe-live-session") {
153
+ return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
154
+ }
155
+ if (operation === "list-agents") {
156
+ return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
157
+ }
158
+ if (operation === "get-agent-result") {
159
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
160
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
161
+ 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);
162
+ const task = loaded.tasks.find((item) => item.id === agent.taskId);
163
+ const text = safeReadContainedFile(loaded.manifest.artifactsRoot, task?.resultArtifact?.path) ?? JSON.stringify(agent, null, 2);
164
+ return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
165
+ }
166
+ if (operation === "read-agent-status") {
167
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
168
+ const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
169
+ const status = agent ? readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent : undefined;
170
+ 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);
171
+ return result(JSON.stringify(status, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
172
+ }
173
+ if (operation === "read-agent-events") {
174
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
175
+ const agents = readCrewAgents(loaded.manifest);
176
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
177
+ 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);
178
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
179
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
180
+ const cursorPayload = readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit });
181
+ const payload = sinceSeq !== undefined || limit !== undefined ? cursorPayload : { path: cursorPayload.path, events: cursorPayload.events };
182
+ return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
183
+ }
184
+ if (operation === "read-agent-transcript") {
185
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
186
+ const agents = readCrewAgents(loaded.manifest);
187
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
188
+ 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);
189
+ const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
190
+ const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
191
+ const artifactText = artifactTranscriptPath ? safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "" : "";
192
+ const fallbackText = artifactText ? "" : safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "";
193
+ const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
194
+ const text = artifactText || fallbackText;
195
+ return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
196
+ }
197
+ if (operation === "read-agent-output") {
198
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
199
+ const agents = readCrewAgents(loaded.manifest);
200
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
201
+ 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);
202
+ const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
203
+ return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
204
+ }
205
+ if (operation === "agent-dashboard") {
206
+ return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
207
+ }
208
+ if (operation === "foreground-status") {
209
+ return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
210
+ }
211
+ if (operation === "foreground-interrupt") {
212
+ const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
213
+ return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
214
+ }
215
+ if (operation === "nudge-agent") {
216
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
217
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
218
+ 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);
219
+ const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
220
+ const message = appendSteeringMessage(loaded.manifest, { taskId: agent.taskId, to: agent.taskId, body: messageText, priority: "normal", data: { source: "nudge-agent" } });
221
+ appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
222
+ ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction: message.direction, from: message.from, to: message.to, taskId: message.taskId, source: "nudge-agent" });
223
+ return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
224
+ }
225
+ if (operation === "list-live-agents") {
226
+ 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 });
227
+ }
228
+ if (operation === "steer-agent" || operation === "follow-up-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
229
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
230
+ if (!agentId) return result(`API ${operation} requires config.agentId.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
231
+ const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
232
+ const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
233
+ try {
234
+ const live = getLiveAgent(agentId);
235
+ if (live && live.runId !== loaded.manifest.runId) return result(`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
236
+ if (!live && (operation === "steer-agent" || operation === "follow-up-agent")) throw new Error(`Live agent '${agentId}' not found.`);
237
+ const liveTaskId = live?.taskId;
238
+ if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId) throw new Error(`Live agent '${agentId}' not found.`);
239
+ const targetTaskId = liveTaskId ?? agentId;
240
+ if (operation === "steer-agent") {
241
+ const text = message ?? "Please report current status and wrap up if possible.";
242
+ const realtime = await steerLiveAgent(agentId, text);
243
+ const mailboxMessage = appendSteeringMessage(loaded.manifest, { taskId: targetTaskId, body: text, status: "delivered", data: { source: "steer-agent", realtime: true } });
244
+ return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
245
+ }
246
+ if (operation === "follow-up-agent") {
247
+ if (!prompt) return result("API follow-up-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
248
+ const realtime = await followUpLiveAgent(agentId, prompt);
249
+ const mailboxMessage = appendFollowUpMessage(loaded.manifest, { taskId: targetTaskId, body: prompt, status: "delivered", data: { source: "follow-up-agent", realtime: true } });
250
+ return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
251
+ }
252
+ if (operation === "resume-agent") {
253
+ if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
254
+ return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
255
+ }
256
+ return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
257
+ } catch (error) {
258
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
259
+ if (!agent) {
260
+ const err = error instanceof Error ? error.message : String(error);
261
+ return result(err, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
262
+ }
263
+ const task = loaded.tasks.find((item) => item.id === agent.taskId);
264
+ if (!task) return result(`API ${operation} agent '${agentId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
265
+ 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);
266
+ if (operation === "follow-up-agent" && !prompt) return result("API follow-up-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
267
+ try {
268
+ const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "follow-up-agent" ? "follow-up" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message });
269
+ const mailboxMessage = operation === "steer-agent" ? appendSteeringMessage(loaded.manifest, { taskId: task.id, to: agent.id, body: message ?? "Please report current status and wrap up if possible.", status: "delivered", data: { source: "steer-agent", liveControlRequestId: request.id } }) : operation === "follow-up-agent" && prompt ? appendFollowUpMessage(loaded.manifest, { taskId: task.id, to: agent.id, body: prompt, status: "delivered", data: { source: "follow-up-agent", liveControlRequestId: request.id } }) : undefined;
270
+ publishLiveControlRealtime(request);
271
+ ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
272
+ 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, mailboxMessageId: mailboxMessage?.id, realtime: true } });
273
+ return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
274
+ } catch (queueError) {
275
+ const message = queueError instanceof Error ? queueError.message : String(queueError);
276
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
277
+ }
278
+ }
279
+ }
280
+ if (operation === "read-mailbox") {
281
+ const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
282
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
283
+ const kind = typeof cfg.kind === "string" && ["message", "steer", "follow-up", "response", "group_join"].includes(cfg.kind) ? cfg.kind as MailboxMessageKind : undefined;
284
+ if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API read-mailbox taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
285
+ try {
286
+ return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId, kind), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
287
+ } catch (error) {
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
290
+ }
291
+ }
292
+ if (operation === "validate-mailbox") {
293
+ const report = validateMailbox(loaded.manifest, { repair: cfg.repair === true });
294
+ 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);
295
+ }
296
+ if (operation === "read-delivery") {
297
+ return result(JSON.stringify(readDeliveryState(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
298
+ }
299
+ if (operation === "send-message") {
300
+ const direction = cfg.direction === "outbox" ? "outbox" : "inbox";
301
+ const from = typeof cfg.from === "string" && cfg.from.trim() ? cfg.from.trim() : "api";
302
+ const to = typeof cfg.to === "string" && cfg.to.trim() ? cfg.to.trim() : "leader";
303
+ const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
304
+ const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
305
+ if (!body) return result("API send-message requires config.body.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
306
+ if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API send-message taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
307
+ try {
308
+ return withRunLockSync(loaded.manifest, () => {
309
+ const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
310
+ appendEvent(loaded.manifest.eventsPath, { type: "mailbox.message", runId: loaded.manifest.runId, data: { id: message.id, direction, from, to } });
311
+ ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction, from, to, taskId, source: "send-message" });
312
+ return result(JSON.stringify(message, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
313
+ });
314
+ } catch (error) {
315
+ const message = error instanceof Error ? error.message : String(error);
316
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
317
+ }
318
+ }
319
+ if (operation === "ack-message") {
320
+ const messageId = typeof cfg.messageId === "string" ? cfg.messageId : undefined;
321
+ if (!messageId) return result("API ack-message requires config.messageId.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
322
+ try {
323
+ return withRunLockSync(loaded.manifest, () => {
324
+ const message = readMailboxMessage(loaded.manifest, messageId);
325
+ const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
326
+ appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
327
+ if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
328
+ appendEvent(loaded.manifest.eventsPath, {
329
+ type: "agent.group_join.acknowledged",
330
+ runId: loaded.manifest.runId,
331
+ message: "Group join delivery acknowledged via mailbox ack.",
332
+ data: { requestId: message.data.requestId, messageId, batchId: message.data.batchId, partial: message.data.partial, acknowledgedAt: delivery.updatedAt, acknowledgedBy: "leader" },
333
+ metadata: { provenance: "api" },
334
+ });
335
+ }
336
+ ctx.events?.emit?.("crew.mailbox.acknowledged", { runId: loaded.manifest.runId, messageId, delivery });
337
+ return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
338
+ });
339
+ } catch (error) {
340
+ const message = error instanceof Error ? error.message : String(error);
341
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
342
+ }
343
+ }
344
+ if (operation === "read-heartbeat") {
345
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
346
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
347
+ 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);
348
+ return result(JSON.stringify(task.heartbeat ?? null, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
349
+ }
350
+ if (operation === "claim-task") {
351
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
352
+ const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
353
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
354
+ 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);
355
+ try {
356
+ return withRunLockSync(loaded.manifest, () => {
357
+ const updatedTask = claimTask(task, owner);
358
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
359
+ saveRunTasks(loaded.manifest, tasks);
360
+ appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token: updatedTask.claim?.token, leasedUntil: updatedTask.claim?.leasedUntil } });
361
+ return result(JSON.stringify(updatedTask.claim, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
362
+ });
363
+ } catch (error) {
364
+ const message = error instanceof Error ? error.message : String(error);
365
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
366
+ }
367
+ }
368
+ if (operation === "release-task-claim") {
369
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
370
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
371
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
372
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
373
+ 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);
374
+ try {
375
+ return withRunLockSync(loaded.manifest, () => {
376
+ const updatedTask = releaseTaskClaim(task, owner, token);
377
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
378
+ saveRunTasks(loaded.manifest, tasks);
379
+ appendEvent(loaded.manifest.eventsPath, { type: "task.claim_released", runId: loaded.manifest.runId, taskId: task.id, data: { owner } });
380
+ return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
381
+ });
382
+ } catch (error) {
383
+ const message = error instanceof Error ? error.message : String(error);
384
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
385
+ }
386
+ }
387
+ if (operation === "transition-task-status") {
388
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
389
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
390
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
391
+ const to = cfg.status;
392
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
393
+ 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);
394
+ if (!canTransitionTaskStatus(task.status, to)) return result(`Invalid task status transition: ${task.status} -> ${to}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
395
+ try {
396
+ return withRunLockSync(loaded.manifest, () => {
397
+ const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
398
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
399
+ saveRunTasks(loaded.manifest, tasks);
400
+ appendEvent(loaded.manifest.eventsPath, { type: "task.status_transitioned", runId: loaded.manifest.runId, taskId: task.id, data: { owner, status: to } });
401
+ return result(JSON.stringify(updatedTask, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
402
+ });
403
+ } catch (error) {
404
+ const message = error instanceof Error ? error.message : String(error);
405
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
406
+ }
407
+ }
408
+ if (operation === "write-heartbeat") {
409
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
410
+ const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
411
+ 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);
412
+ try {
413
+ return withRunLockSync(loaded.manifest, () => {
414
+ const heartbeat = touchWorkerHeartbeat(task.heartbeat ?? { workerId: task.id, lastSeenAt: new Date().toISOString() }, { alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined });
415
+ const tasks = loaded.tasks.map((item) => item.id === task.id ? { ...item, heartbeat } : item);
416
+ saveRunTasks(loaded.manifest, tasks);
417
+ appendEvent(loaded.manifest.eventsPath, { type: "worker.heartbeat", runId: loaded.manifest.runId, taskId: task.id, data: { ...heartbeat } });
418
+ return result(JSON.stringify(heartbeat, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
419
+ });
420
+ } catch (error) {
421
+ const message = error instanceof Error ? error.message : String(error);
422
+ return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
423
+ }
424
+ }
425
+ if (operation === "diff") {
426
+ const diffArtifacts = loaded.manifest.artifacts.filter(a => a.kind === "diff" || a.kind === "patch");
427
+ if (diffArtifacts.length === 0) {
428
+ return result(`No diff artifacts found for run ${loaded.manifest.runId}. Diffs are captured in worktree mode.`, { action: "api", status: "ok", runId: loaded.manifest.runId, intent: `diff ${loaded.manifest.runId}: no diffs` });
429
+ }
430
+ const parts: string[] = [`Diff artifacts for run ${loaded.manifest.runId}:`];
431
+ for (const artifact of diffArtifacts) {
432
+ const content = safeReadContainedFile(loaded.manifest.artifactsRoot, artifact.path);
433
+ if (content) {
434
+ const display = content.length > 4000 ? content.slice(0, 4000) + "\n... (truncated)" : content;
435
+ parts.push(`\n--- ${artifact.path} ---\n${display}`);
436
+ }
437
+ }
438
+ return result(parts.join("\n"), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot, intent: `diff ${loaded.manifest.runId}` });
439
+ }
440
+ return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
441
+ }