pi-crew 0.9.65 → 0.9.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +2 -3
  3. package/agents/executor.md +1 -1
  4. package/agents/test-engineer.md +1 -1
  5. package/agents/verifier.md +1 -1
  6. package/dist/index.mjs +4526 -4100
  7. package/package.json +7 -4
  8. package/skills/real-test-pi-crew/SKILL.md +10 -9
  9. package/src/config/config.ts +19 -3
  10. package/src/config/role-tools.ts +2 -2
  11. package/src/config/types.ts +2 -0
  12. package/src/extension/knowledge-injection.ts +17 -0
  13. package/src/extension/pi-api.ts +0 -16
  14. package/src/extension/team-tool/api/agent-control.ts +358 -0
  15. package/src/extension/team-tool/api/handler-context.ts +57 -0
  16. package/src/extension/team-tool/api/heartbeat.ts +75 -0
  17. package/src/extension/team-tool/api/mailbox.ts +242 -0
  18. package/src/extension/team-tool/api/plan-approval.ts +190 -0
  19. package/src/extension/team-tool/api/read.ts +443 -0
  20. package/src/extension/team-tool/api/task-claims.ts +207 -0
  21. package/src/extension/team-tool/api.ts +56 -1301
  22. package/src/extension/team-tool/cancel.ts +49 -4
  23. package/src/extension/team-tool/dispatch/manage.ts +7 -4
  24. package/src/extension/team-tool/explain.ts +3 -1
  25. package/src/extension/team-tool/goal-wrap.ts +2 -2
  26. package/src/extension/team-tool/goal.ts +4 -4
  27. package/src/extension/team-tool/lifecycle-actions.ts +9 -6
  28. package/src/extension/team-tool/parallel-dispatch.ts +2 -2
  29. package/src/extension/team-tool/respond.ts +11 -3
  30. package/src/extension/team-tool/run-intent.ts +357 -0
  31. package/src/extension/team-tool/run.ts +8 -285
  32. package/src/extension/team-tool/status.ts +56 -21
  33. package/src/extension/team-tool-types.ts +2 -0
  34. package/src/extension/team-tool.ts +10 -8
  35. package/src/prompt/scratchpad-lifecycle.ts +80 -5
  36. package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
  37. package/src/runtime/child-pi/child-pi.ts +22 -144
  38. package/src/runtime/child-pi/mock-fixtures.ts +171 -0
  39. package/src/runtime/crew-agent-records.ts +18 -1
  40. package/src/runtime/output/output-validator.ts +34 -6
  41. package/src/runtime/scheduling/scheduler.ts +67 -19
  42. package/src/runtime/scratchpad/README.md +6 -0
  43. package/src/runtime/scratchpad/guest.ts +54 -5
  44. package/src/runtime/scratchpad/snapshot-hmac.ts +7 -1
  45. package/src/runtime/supervisor-contact.ts +0 -16
  46. package/src/runtime/task-runner/child-executor.ts +1 -0
  47. package/src/runtime/task-runner/state-helpers.ts +9 -1
  48. package/src/runtime/team-runner.ts +39 -2
  49. package/src/state/contracts.ts +3 -0
  50. package/src/state/event-log/event-log.ts +19 -27
  51. package/src/state/gitignore-manager.ts +61 -7
  52. package/src/utils/glob-match.ts +29 -0
  53. package/types/dwf.d.ts +1 -1
  54. package/src/types/new-api-types.ts +0 -35
@@ -1,1322 +1,77 @@
1
- import * as fs from "node:fs";
2
- import { loadConfig } from "../../config/config.ts";
3
- import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
4
- import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
5
- import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
6
- import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
7
- import { touchWorkerHeartbeat } from "../../runtime/heartbeat/worker-heartbeat.ts";
8
- import { appendLiveAgentControlRequest } from "../../runtime/live-session/live-agent-control.ts";
9
- import {
10
- followUpLiveAgent,
11
- getLiveAgent,
12
- listActiveLiveAgents,
13
- resumeLiveAgent,
14
- steerLiveAgent,
15
- stopLiveAgent,
16
- terminateLiveAgentsForRun,
17
- } from "../../runtime/live-session/live-agent-manager.ts";
18
- import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../runtime/live-session/live-control-realtime.ts";
19
- import { probeLiveSessionRuntime } from "../../runtime/live-session/live-session-runtime.ts";
20
- import { resolveCrewRuntime } from "../../runtime/model/runtime-resolver.ts";
21
- import { currentCrewRole, permissionForRole } from "../../runtime/role-permission.ts";
1
+ /**
2
+ * handleApi — `team action="api"` dispatcher.
3
+ *
4
+ * The original 1222-line `if (operation === "…")` chain was split into
5
+ * per-group modules under `./api/` (H3, improvement-plan-2026-08-10):
6
+ *
7
+ * - api/read.ts — read/inspect ops + pre-runId ops
8
+ * - api/plan-approval.ts — approve-plan, cancel-plan
9
+ * - api/agent-control.ts — steer/follow-up/stop/resume/interrupt/nudge
10
+ * - api/mailbox.ts — read/validate/read-delivery/send/ack
11
+ * - api/heartbeat.ts — write-heartbeat
12
+ * - api/task-claims.ts — claim-task, release-task-claim, transition-task-status
13
+ *
14
+ * Each handler is a pure function receiving `ApiHandlerContext`
15
+ * (cfg + loaded + result + paramRequired + params + ctx). This file keeps
16
+ * only the request flow: config parse → pre-runId ops → runId guard →
17
+ * dispatcher → unknown-op error.
18
+ */
22
19
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
23
- import { canTransitionTaskStatus, isTeamTaskStatus } from "../../state/contracts.ts";
24
- import { withRunLock, withRunLockSync } from "../../state/coordination/locks.ts";
25
- import {
26
- acknowledgeMailboxMessage,
27
- appendFollowUpMessage,
28
- appendFollowUpMessageAsync,
29
- appendMailboxMessage,
30
- appendSteeringMessage,
31
- appendSteeringMessageAsync,
32
- type MailboxDirection,
33
- type MailboxMessageKind,
34
- readDeliveryState,
35
- readMailbox,
36
- readMailboxMessage,
37
- validateMailbox,
38
- } from "../../state/coordination/mailbox.ts";
39
- import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../state/coordination/task-claims.ts";
40
- import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log/event-log.ts";
41
- import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
42
- import { logInternalError } from "../../utils/internal-error.ts";
43
- import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
20
+ import { loadRunManifestById } from "../../state/stores/state-store.ts";
44
21
  import { locateRunCwd } from "../team-tool.ts";
45
22
  import type { PiTeamsToolResult } from "../tool-result.ts";
23
+ import { AGENT_CONTROL_OPERATIONS } from "./api/agent-control.ts";
24
+ import { HEARTBEAT_OPERATIONS } from "./api/heartbeat.ts";
25
+ import { MAILBOX_OPERATIONS } from "./api/mailbox.ts";
26
+ import { PLAN_APPROVAL_OPERATIONS } from "./api/plan-approval.ts";
27
+ import { PRE_RUNID_OPERATIONS, READ_OPERATIONS } from "./api/read.ts";
28
+ import { TASK_CLAIM_OPERATIONS } from "./api/task-claims.ts";
46
29
  import { configRecord, result, type TeamContext } from "./context.ts";
47
30
  import { paramRequired } from "./param-error.ts";
48
31
  import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
49
32
 
50
- export function globMatch(value: string, pattern: string): boolean {
51
- // Prevent ReDoS: reject excessively long patterns
52
- if (pattern.length > 200) return false;
53
- const regex = pattern
54
- .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex special chars
55
- .replace(/\*/g, "[^/]*") // * matches non-slash characters only
56
- .replace(/\?/g, "[^/]"); // ? matches single non-slash
57
- return new RegExp(`^${regex}$`).test(value);
58
- }
59
-
60
- function safeReadContainedFile(baseDir: string, filePath: string | undefined): string | undefined {
61
- if (!filePath) return undefined;
62
- let safePath: string;
63
- try {
64
- safePath = resolveRealContainedPath(baseDir, filePath);
65
- } catch {
66
- return undefined;
67
- }
68
- return fs.existsSync(safePath) ? fs.readFileSync(safePath, "utf-8") : undefined;
69
- }
70
-
71
- function safeContainedPath(baseDir: string, filePath: string | undefined): string | undefined {
72
- if (!filePath) return undefined;
73
- try {
74
- return resolveRealContainedPath(baseDir, filePath);
75
- } catch {
76
- return undefined;
77
- }
78
- }
79
-
80
- function snapshotHasRunId(snapshot: { values?: unknown }, runId: string): boolean {
81
- const values = Array.isArray(snapshot.values) ? snapshot.values : [];
82
- return values.some((value) => {
83
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
84
- const labels = (value as { labels?: unknown }).labels;
85
- return labels && typeof labels === "object" && !Array.isArray(labels) && (labels as Record<string, unknown>).runId === runId;
86
- });
87
- }
33
+ export { globMatch } from "../../utils/glob-match.ts";
88
34
 
89
- function canApprovePlan(): { allowed: boolean; reason?: string } {
90
- const role = currentCrewRole();
91
- if (!role) return { allowed: true };
92
- if (permissionForRole(role) === "read_only")
93
- return {
94
- allowed: false,
95
- reason: `Role '${role}' is read-only and cannot approve or cancel plan gates.`,
96
- };
97
- return { allowed: true };
98
- }
35
+ /** Union of all post-load operation dispatchers. */
36
+ const API_OPERATIONS: Record<string, import("./api/handler-context.ts").ApiOperationHandler> = {
37
+ ...READ_OPERATIONS,
38
+ ...PLAN_APPROVAL_OPERATIONS,
39
+ ...AGENT_CONTROL_OPERATIONS,
40
+ ...MAILBOX_OPERATIONS,
41
+ ...HEARTBEAT_OPERATIONS,
42
+ ...TASK_CLAIM_OPERATIONS,
43
+ };
99
44
 
100
45
  export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
101
46
  const cfg = configRecord(params.config);
102
47
  const operation = typeof cfg.operation === "string" ? cfg.operation : "read-manifest";
103
- if (operation === "metrics-snapshot") {
104
- const filter = typeof cfg.filter === "string" ? cfg.filter : undefined;
105
- const runIdFilter = typeof cfg.runId === "string" ? cfg.runId : params.runId;
106
- const snapshots = ctx.metricRegistry?.snapshot() ?? [];
107
- const filtered = snapshots.filter((snapshot) => {
108
- if (filter && !globMatch(snapshot.name, filter)) return false;
109
- if (runIdFilter && !snapshotHasRunId(snapshot, runIdFilter)) return false;
110
- return true;
111
- });
112
- return result(JSON.stringify(filtered, null, 2), {
113
- action: "api",
114
- status: "ok",
115
- ...(runIdFilter ? { runId: runIdFilter } : {}),
116
- });
117
- }
118
- if (operation === "inventory") {
119
- const inventory = buildCapabilityInventory(ctx.cwd, ctx.config);
120
- return result(JSON.stringify(inventory, null, 2), {
121
- action: "api",
122
- status: "ok",
123
- });
48
+
49
+ // Pre-runId operations (metrics-snapshot, inventory) — no loaded run needed.
50
+ const preHandler = PRE_RUNID_OPERATIONS[operation];
51
+ if (preHandler) {
52
+ return preHandler({ cfg, result, paramRequired, params, ctx });
124
53
  }
54
+
125
55
  if (!params.runId)
126
56
  return result(paramRequired("api", "runId", "{ action: 'api', runId: 'team_...' }"), { action: "api", status: "error" }, true);
127
57
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
128
58
  if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "api", status: "error" }, true);
129
59
  const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
130
60
  if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "api", status: "error" }, true);
131
- if (operation === "read-manifest") {
132
- return result(JSON.stringify(loaded.manifest, null, 2), {
133
- action: "api",
134
- status: "ok",
135
- runId: loaded.manifest.runId,
136
- artifactsRoot: loaded.manifest.artifactsRoot,
137
- });
138
- }
139
- if (operation === "approve-plan") {
140
- const permission = canApprovePlan();
141
- if (!permission.allowed)
142
- return result(
143
- permission.reason ?? "Plan approval is not allowed in this context.",
144
- {
145
- action: "api",
146
- status: "error",
147
- runId: loaded.manifest.runId,
148
- },
149
- true,
150
- );
151
- try {
152
- return await withRunLock(loaded.manifest, async () => {
153
- const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLock - consistent read
154
- const approval = current.manifest.planApproval;
155
- if (!approval?.required || approval.status !== "pending")
156
- return result(
157
- "Run has no pending plan approval request.",
158
- {
159
- action: "api",
160
- status: "error",
161
- runId: loaded.manifest.runId,
162
- },
163
- true,
164
- );
165
- const now = new Date().toISOString();
166
- const manifest = {
167
- ...current.manifest,
168
- updatedAt: now,
169
- planApproval: {
170
- ...approval,
171
- status: "approved" as const,
172
- approvedAt: now,
173
- updatedAt: now,
174
- },
175
- };
176
- await saveRunManifestAsync(manifest);
177
- appendEvent(manifest.eventsPath, {
178
- type: "plan.approved",
179
- runId: manifest.runId,
180
- taskId: approval.planTaskId,
181
- message: "Adaptive implementation plan approved; resume the run to execute mutating tasks.",
182
- metadata: { provenance: "api" },
183
- });
184
- return result(JSON.stringify(manifest.planApproval, null, 2), {
185
- action: "api",
186
- status: "ok",
187
- runId: manifest.runId,
188
- artifactsRoot: manifest.artifactsRoot,
189
- });
190
- });
191
- } catch (error) {
192
- const message = error instanceof Error ? error.message : String(error);
193
- return result(
194
- message,
195
- {
196
- action: "api",
197
- status: "error",
198
- runId: loaded.manifest.runId,
199
- },
200
- true,
201
- );
202
- }
203
- }
204
- if (operation === "cancel-plan") {
205
- const permission = canApprovePlan();
206
- if (!permission.allowed)
207
- return result(
208
- permission.reason ?? "Plan approval cancellation is not allowed in this context.",
209
- {
210
- action: "api",
211
- status: "error",
212
- runId: loaded.manifest.runId,
213
- },
214
- true,
215
- );
216
- try {
217
- return await withRunLock(loaded.manifest, async () => {
218
- const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLock - consistent read
219
- const approval = current.manifest.planApproval;
220
- if (!approval?.required || approval.status !== "pending")
221
- return result(
222
- "Run has no pending plan approval request.",
223
- {
224
- action: "api",
225
- status: "error",
226
- runId: loaded.manifest.runId,
227
- },
228
- true,
229
- );
230
- const now = new Date().toISOString();
231
- const tasks = current.tasks.map((task) =>
232
- task.status === "queued" || task.status === "running" || task.status === "waiting"
233
- ? {
234
- ...task,
235
- status: "cancelled" as const,
236
- finishedAt: now,
237
- error: "Plan approval was cancelled.",
238
- }
239
- : task,
240
- );
241
- let manifest: typeof current.manifest = {
242
- ...current.manifest,
243
- updatedAt: now,
244
- planApproval: {
245
- ...approval,
246
- status: "cancelled" as const,
247
- cancelledAt: now,
248
- updatedAt: now,
249
- },
250
- };
251
- await saveRunManifestAsync(manifest);
252
- saveRunTasks(manifest, tasks);
253
- appendEvent(manifest.eventsPath, {
254
- type: "plan.cancelled",
255
- runId: manifest.runId,
256
- taskId: approval.planTaskId,
257
- message: "Adaptive implementation plan was cancelled.",
258
- metadata: { provenance: "api" },
259
- });
260
- manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
261
- void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch((error) =>
262
- logInternalError("team-tool.cancel-plan.terminate", error, `runId=${manifest.runId}`),
263
- );
264
- return result(
265
- JSON.stringify(
266
- {
267
- planApproval: manifest.planApproval,
268
- cancelledTasks: tasks.filter((task) => task.status === "cancelled").map((task) => task.id),
269
- },
270
- null,
271
- 2,
272
- ),
273
- {
274
- action: "api",
275
- status: "ok",
276
- runId: manifest.runId,
277
- artifactsRoot: manifest.artifactsRoot,
278
- },
279
- );
280
- });
281
- } catch (error) {
282
- const message = error instanceof Error ? error.message : String(error);
283
- return result(
284
- message,
285
- {
286
- action: "api",
287
- status: "error",
288
- runId: loaded.manifest.runId,
289
- },
290
- true,
291
- );
292
- }
293
- }
294
- if (operation === "list-tasks") {
295
- return result(JSON.stringify(loaded.tasks, null, 2), {
296
- action: "api",
297
- status: "ok",
298
- runId: loaded.manifest.runId,
299
- artifactsRoot: loaded.manifest.artifactsRoot,
300
- });
301
- }
302
- if (operation === "read-task") {
303
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
304
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
305
- if (!task)
306
- return result(
307
- paramRequired(
308
- "API read-task",
309
- "config.taskId matching a task id or step id",
310
- "{ action: 'api', runId: 'team_...', config: { operation: 'read-task', taskId: '01_01-agent' } }",
311
- ),
312
- {
313
- action: "api",
314
- status: "error",
315
- runId: loaded.manifest.runId,
316
- },
317
- true,
318
- );
319
- return result(JSON.stringify(task, null, 2), {
320
- action: "api",
321
- status: "ok",
322
- runId: loaded.manifest.runId,
323
- artifactsRoot: loaded.manifest.artifactsRoot,
324
- });
325
- }
326
- if (operation === "read-events") {
327
- const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
328
- const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
329
- const payload =
330
- sinceSeq !== undefined || limit !== undefined
331
- ? readEventsCursor(loaded.manifest.eventsPath, {
332
- sinceSeq,
333
- limit,
334
- })
335
- : {
336
- events: readEvents(loaded.manifest.eventsPath),
337
- nextSeq: undefined,
338
- total: undefined,
339
- };
340
- return result(JSON.stringify(payload, null, 2), {
341
- action: "api",
342
- status: "ok",
343
- runId: loaded.manifest.runId,
344
- artifactsRoot: loaded.manifest.artifactsRoot,
345
- });
346
- }
347
- if (operation === "runtime-capabilities") {
348
- const loadedConfig = loadConfig(ctx.cwd);
349
- return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), {
350
- action: "api",
351
- status: "ok",
352
- runId: loaded.manifest.runId,
353
- artifactsRoot: loaded.manifest.artifactsRoot,
354
- });
355
- }
356
- if (operation === "probe-live-session") {
357
- return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), {
358
- action: "api",
359
- status: "ok",
360
- runId: loaded.manifest.runId,
361
- artifactsRoot: loaded.manifest.artifactsRoot,
362
- });
363
- }
364
- if (operation === "list-agents") {
365
- return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), {
366
- action: "api",
367
- status: "ok",
368
- runId: loaded.manifest.runId,
369
- artifactsRoot: loaded.manifest.artifactsRoot,
370
- });
371
- }
372
- if (operation === "get-agent-result") {
373
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
374
- const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
375
- if (!agent)
376
- return result(
377
- paramRequired(
378
- "API get-agent-result",
379
- "config.agentId matching an agent id or task id",
380
- "{ action: 'api', runId: 'team_...', config: { operation: 'get-agent-result', agentId: 'agent-1' } }",
381
- ),
382
- {
383
- action: "api",
384
- status: "error",
385
- runId: loaded.manifest.runId,
386
- },
387
- true,
388
- );
389
- const task = loaded.tasks.find((item) => item.id === agent.taskId);
390
- const text = safeReadContainedFile(loaded.manifest.artifactsRoot, task?.resultArtifact?.path) ?? JSON.stringify(agent, null, 2);
391
- return result(text, {
392
- action: "api",
393
- status: "ok",
394
- runId: loaded.manifest.runId,
395
- artifactsRoot: loaded.manifest.artifactsRoot,
396
- });
397
- }
398
- if (operation === "read-agent-status") {
399
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
400
- const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
401
- const status = agent ? (readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent) : undefined;
402
- if (!status)
403
- return result(
404
- paramRequired(
405
- "API read-agent-status",
406
- "config.agentId matching an agent id or task id",
407
- "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-status', agentId: 'agent-1' } }",
408
- ),
409
- {
410
- action: "api",
411
- status: "error",
412
- runId: loaded.manifest.runId,
413
- },
414
- true,
415
- );
416
- return result(JSON.stringify(status, null, 2), {
417
- action: "api",
418
- status: "ok",
419
- runId: loaded.manifest.runId,
420
- artifactsRoot: loaded.manifest.artifactsRoot,
421
- });
422
- }
423
- if (operation === "read-agent-events") {
424
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
425
- const agents = readCrewAgents(loaded.manifest);
426
- const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
427
- if (!agent)
428
- return result(
429
- paramRequired(
430
- "API read-agent-events",
431
- "config.agentId matching an agent id or task id, or at least one agent in the run",
432
- "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-events', agentId: 'agent-1' } }",
433
- ),
434
- {
435
- action: "api",
436
- status: "error",
437
- runId: loaded.manifest.runId,
438
- },
439
- true,
440
- );
441
- const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
442
- const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
443
- const cursorPayload = readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit });
444
- const payload =
445
- sinceSeq !== undefined || limit !== undefined ? cursorPayload : { path: cursorPayload.path, events: cursorPayload.events };
446
- return result(JSON.stringify(payload, null, 2), {
447
- action: "api",
448
- status: "ok",
449
- runId: loaded.manifest.runId,
450
- artifactsRoot: loaded.manifest.artifactsRoot,
451
- });
452
- }
453
- if (operation === "read-agent-transcript") {
454
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
455
- const agents = readCrewAgents(loaded.manifest);
456
- const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
457
- if (!agent)
458
- return result(
459
- paramRequired(
460
- "API read-agent-transcript",
461
- "config.agentId matching an agent id or task id, or at least one agent in the run",
462
- "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-transcript', agentId: 'agent-1' } }",
463
- ),
464
- {
465
- action: "api",
466
- status: "error",
467
- runId: loaded.manifest.runId,
468
- },
469
- true,
470
- );
471
- const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
472
- const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
473
- const artifactText = artifactTranscriptPath
474
- ? (safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "")
475
- : "";
476
- const fallbackText = artifactText ? "" : (safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "");
477
- const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
478
- const text = artifactText || fallbackText;
479
- return result(text || `(no transcript at ${transcriptPath})`, {
480
- action: "api",
481
- status: "ok",
482
- runId: loaded.manifest.runId,
483
- artifactsRoot: loaded.manifest.artifactsRoot,
484
- });
485
- }
486
- if (operation === "read-agent-output") {
487
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
488
- const agents = readCrewAgents(loaded.manifest);
489
- const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
490
- if (!agent)
491
- return result(
492
- paramRequired(
493
- "API read-agent-output",
494
- "config.agentId matching an agent id or task id, or at least one agent in the run",
495
- "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-output', agentId: 'agent-1' } }",
496
- ),
497
- {
498
- action: "api",
499
- status: "error",
500
- runId: loaded.manifest.runId,
501
- },
502
- true,
503
- );
504
- const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
505
- return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), {
506
- action: "api",
507
- status: "ok",
508
- runId: loaded.manifest.runId,
509
- artifactsRoot: loaded.manifest.artifactsRoot,
510
- });
511
- }
512
- if (operation === "agent-dashboard") {
513
- return result(buildAgentDashboard(loaded.manifest).text, {
514
- action: "api",
515
- status: "ok",
516
- runId: loaded.manifest.runId,
517
- artifactsRoot: loaded.manifest.artifactsRoot,
518
- });
519
- }
520
- if (operation === "foreground-status") {
521
- return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), {
522
- action: "api",
523
- status: "ok",
524
- runId: loaded.manifest.runId,
525
- artifactsRoot: loaded.manifest.artifactsRoot,
526
- });
527
- }
528
- if (operation === "foreground-interrupt") {
529
- const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
530
- return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), {
531
- action: "api",
532
- status: "ok",
533
- runId: loaded.manifest.runId,
534
- artifactsRoot: loaded.manifest.artifactsRoot,
535
- });
536
- }
537
- if (operation === "nudge-agent") {
538
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
539
- const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
540
- if (!agent)
541
- return result(
542
- paramRequired(
543
- "API nudge-agent",
544
- "config.agentId matching an agent id or task id",
545
- "{ action: 'api', runId: 'team_...', config: { operation: 'nudge-agent', agentId: 'agent-1' } }",
546
- ),
547
- {
548
- action: "api",
549
- status: "error",
550
- runId: loaded.manifest.runId,
551
- },
552
- true,
553
- );
554
- const messageText =
555
- typeof cfg.message === "string" && cfg.message.trim()
556
- ? cfg.message.trim()
557
- : "Please report your current status, blocker, or smallest next step.";
558
- const message = appendSteeringMessage(loaded.manifest, {
559
- taskId: agent.taskId,
560
- to: agent.taskId,
561
- body: messageText,
562
- priority: "normal",
563
- data: { source: "nudge-agent" },
564
- });
565
- appendEvent(loaded.manifest.eventsPath, {
566
- type: "agent.nudged",
567
- runId: loaded.manifest.runId,
568
- taskId: agent.taskId,
569
- message: messageText,
570
- data: { agentId: agent.id, mailboxMessageId: message.id },
571
- });
572
- ctx.events?.emit?.("crew.mailbox.message", {
573
- runId: loaded.manifest.runId,
574
- id: message.id,
575
- direction: message.direction,
576
- from: message.from,
577
- to: message.to,
578
- taskId: message.taskId,
579
- source: "nudge-agent",
580
- });
581
- return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), {
582
- action: "api",
583
- status: "ok",
584
- runId: loaded.manifest.runId,
585
- artifactsRoot: loaded.manifest.artifactsRoot,
586
- });
587
- }
588
- if (operation === "list-live-agents") {
589
- return result(
590
- JSON.stringify(
591
- listActiveLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId),
592
- null,
593
- 2,
594
- ),
595
- {
596
- action: "api",
597
- status: "ok",
598
- runId: loaded.manifest.runId,
599
- artifactsRoot: loaded.manifest.artifactsRoot,
600
- },
601
- );
602
- }
603
- if (
604
- operation === "steer-agent" ||
605
- operation === "follow-up-agent" ||
606
- operation === "stop-agent" ||
607
- operation === "resume-agent" ||
608
- operation === "interrupt-agent"
609
- ) {
610
- const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
611
- if (!agentId)
612
- return result(
613
- paramRequired(
614
- `API ${operation}`,
615
- "config.agentId",
616
- `{ action: 'api', runId: 'team_...', config: { operation: '${operation}', agentId: 'agent-1' } }`,
617
- ),
618
- {
619
- action: "api",
620
- status: "error",
621
- runId: loaded.manifest.runId,
622
- },
623
- true,
624
- );
625
- const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
626
- const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
627
- try {
628
- const live = getLiveAgent(agentId);
629
- if (live && live.runId !== loaded.manifest.runId)
630
- return result(
631
- `Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`,
632
- {
633
- action: "api",
634
- status: "error",
635
- runId: loaded.manifest.runId,
636
- },
637
- true,
638
- );
639
- if (live && live.workspaceId !== loaded.manifest.cwd)
640
- return result(
641
- `Live agent '${agentId}' does not belong to workspace ${loaded.manifest.cwd}.`,
642
- {
643
- action: "api",
644
- status: "error",
645
- runId: loaded.manifest.runId,
646
- },
647
- true,
648
- );
649
- if (!live && (operation === "steer-agent" || operation === "follow-up-agent"))
650
- throw new Error(`Live agent '${agentId}' not found.`);
651
- const liveTaskId = live?.taskId;
652
- if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId)
653
- throw new Error(`Live agent '${agentId}' not found.`);
654
- const targetTaskId = liveTaskId ?? agentId;
655
- if (operation === "steer-agent") {
656
- const text = message ?? "Please report current status and wrap up if possible.";
657
- const realtime = await steerLiveAgent(agentId, text);
658
- const mailboxMessage = await appendSteeringMessageAsync(loaded.manifest, {
659
- taskId: targetTaskId,
660
- body: text,
661
- status: "delivered",
662
- data: { source: "steer-agent", realtime: true },
663
- });
664
- return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
665
- action: "api",
666
- status: "ok",
667
- runId: loaded.manifest.runId,
668
- artifactsRoot: loaded.manifest.artifactsRoot,
669
- });
670
- }
671
- if (operation === "follow-up-agent") {
672
- if (!prompt)
673
- return result(
674
- paramRequired(
675
- "API follow-up-agent",
676
- "config.prompt or config.message",
677
- "{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
678
- ),
679
- {
680
- action: "api",
681
- status: "error",
682
- runId: loaded.manifest.runId,
683
- },
684
- true,
685
- );
686
- const realtime = await followUpLiveAgent(agentId, prompt);
687
- const mailboxMessage = await appendFollowUpMessageAsync(loaded.manifest, {
688
- taskId: targetTaskId,
689
- body: prompt,
690
- status: "delivered",
691
- data: { source: "follow-up-agent", realtime: true },
692
- });
693
- return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
694
- action: "api",
695
- status: "ok",
696
- runId: loaded.manifest.runId,
697
- artifactsRoot: loaded.manifest.artifactsRoot,
698
- });
699
- }
700
- if (operation === "resume-agent") {
701
- if (!prompt)
702
- return result(
703
- paramRequired(
704
- "API resume-agent",
705
- "config.prompt or config.message",
706
- "{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
707
- ),
708
- {
709
- action: "api",
710
- status: "error",
711
- runId: loaded.manifest.runId,
712
- },
713
- true,
714
- );
715
- return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), {
716
- action: "api",
717
- status: "ok",
718
- runId: loaded.manifest.runId,
719
- artifactsRoot: loaded.manifest.artifactsRoot,
720
- });
721
- }
722
- return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), {
723
- action: "api",
724
- status: "ok",
725
- runId: loaded.manifest.runId,
726
- artifactsRoot: loaded.manifest.artifactsRoot,
727
- });
728
- } catch (error) {
729
- const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
730
- if (!agent) {
731
- const err = error instanceof Error ? error.message : String(error);
732
- return result(
733
- err,
734
- {
735
- action: "api",
736
- status: "error",
737
- runId: loaded.manifest.runId,
738
- },
739
- true,
740
- );
741
- }
742
- const task = loaded.tasks.find((item) => item.id === agent.taskId);
743
- if (!task)
744
- return result(
745
- `API ${operation} agent '${agentId}' does not match a run task.`,
746
- {
747
- action: "api",
748
- status: "error",
749
- runId: loaded.manifest.runId,
750
- },
751
- true,
752
- );
753
- if (operation === "resume-agent" && !prompt)
754
- return result(
755
- paramRequired(
756
- "API resume-agent",
757
- "config.prompt or config.message",
758
- "{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
759
- ),
760
- {
761
- action: "api",
762
- status: "error",
763
- runId: loaded.manifest.runId,
764
- },
765
- true,
766
- );
767
- if (operation === "follow-up-agent" && !prompt)
768
- return result(
769
- paramRequired(
770
- "API follow-up-agent",
771
- "config.prompt or config.message",
772
- "{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
773
- ),
774
- {
775
- action: "api",
776
- status: "error",
777
- runId: loaded.manifest.runId,
778
- },
779
- true,
780
- );
781
- try {
782
- const request = appendLiveAgentControlRequest(loaded.manifest, {
783
- taskId: task.id,
784
- agentId: agent.id,
785
- operation:
786
- operation === "resume-agent"
787
- ? "resume"
788
- : operation === "follow-up-agent"
789
- ? "follow-up"
790
- : operation === "steer-agent"
791
- ? "steer"
792
- : "stop",
793
- message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message,
794
- });
795
- const mailboxMessage =
796
- operation === "steer-agent"
797
- ? appendSteeringMessage(loaded.manifest, {
798
- taskId: task.id,
799
- to: agent.id,
800
- body: message ?? "Please report current status and wrap up if possible.",
801
- status: "delivered",
802
- data: {
803
- source: "steer-agent",
804
- liveControlRequestId: request.id,
805
- },
806
- })
807
- : operation === "follow-up-agent" && prompt
808
- ? appendFollowUpMessage(loaded.manifest, {
809
- taskId: task.id,
810
- to: agent.id,
811
- body: prompt,
812
- status: "delivered",
813
- data: {
814
- source: "follow-up-agent",
815
- liveControlRequestId: request.id,
816
- },
817
- })
818
- : undefined;
819
- publishLiveControlRealtime(request);
820
- ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
821
- appendEvent(loaded.manifest.eventsPath, {
822
- type: "agent.control.queued",
823
- runId: loaded.manifest.runId,
824
- taskId: agent.taskId,
825
- message: `Queued ${request.operation} control request for live agent.`,
826
- data: {
827
- request,
828
- mailboxMessageId: mailboxMessage?.id,
829
- realtime: true,
830
- },
831
- });
832
- return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), {
833
- action: "api",
834
- status: "ok",
835
- runId: loaded.manifest.runId,
836
- artifactsRoot: loaded.manifest.artifactsRoot,
837
- });
838
- } catch (queueError) {
839
- const message = queueError instanceof Error ? queueError.message : String(queueError);
840
- return result(
841
- message,
842
- {
843
- action: "api",
844
- status: "error",
845
- runId: loaded.manifest.runId,
846
- },
847
- true,
848
- );
849
- }
850
- }
851
- }
852
- if (operation === "read-mailbox") {
853
- const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? (cfg.direction as MailboxDirection) : undefined;
854
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
855
- const kind =
856
- typeof cfg.kind === "string" && ["message", "steer", "follow-up", "response", "group_join"].includes(cfg.kind)
857
- ? (cfg.kind as MailboxMessageKind)
858
- : undefined;
859
- if (taskId && !loaded.tasks.some((task) => task.id === taskId))
860
- return result(
861
- `API read-mailbox taskId '${taskId}' does not match a run task.`,
862
- {
863
- action: "api",
864
- status: "error",
865
- runId: loaded.manifest.runId,
866
- },
867
- true,
868
- );
869
- try {
870
- return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId, kind), null, 2), {
871
- action: "api",
872
- status: "ok",
873
- runId: loaded.manifest.runId,
874
- artifactsRoot: loaded.manifest.artifactsRoot,
875
- });
876
- } catch (error) {
877
- const message = error instanceof Error ? error.message : String(error);
878
- return result(
879
- message,
880
- {
881
- action: "api",
882
- status: "error",
883
- runId: loaded.manifest.runId,
884
- },
885
- true,
886
- );
887
- }
888
- }
889
- if (operation === "validate-mailbox") {
890
- const report = validateMailbox(loaded.manifest, {
891
- repair: cfg.repair === true,
892
- });
893
- return result(
894
- JSON.stringify(report, null, 2),
895
- {
896
- action: "api",
897
- status: report.issues.some((issue) => issue.level === "error") && cfg.repair !== true ? "error" : "ok",
898
- runId: loaded.manifest.runId,
899
- artifactsRoot: loaded.manifest.artifactsRoot,
900
- },
901
- report.issues.some((issue) => issue.level === "error") && cfg.repair !== true,
902
- );
903
- }
904
- if (operation === "read-delivery") {
905
- return result(JSON.stringify(readDeliveryState(loaded.manifest), null, 2), {
906
- action: "api",
907
- status: "ok",
908
- runId: loaded.manifest.runId,
909
- artifactsRoot: loaded.manifest.artifactsRoot,
910
- });
911
- }
912
- if (operation === "send-message") {
913
- const direction = cfg.direction === "outbox" ? "outbox" : "inbox";
914
- const from = typeof cfg.from === "string" && cfg.from.trim() ? cfg.from.trim() : "api";
915
- const to = typeof cfg.to === "string" && cfg.to.trim() ? cfg.to.trim() : "leader";
916
- const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
917
- const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
918
- if (!body)
919
- return result(
920
- paramRequired(
921
- "API send-message",
922
- "config.body",
923
- "{ action: 'api', runId: 'team_...', config: { operation: 'send-message', body: '<message>' } }",
924
- ),
925
- {
926
- action: "api",
927
- status: "error",
928
- runId: loaded.manifest.runId,
929
- },
930
- true,
931
- );
932
- if (taskId && !loaded.tasks.some((task) => task.id === taskId))
933
- return result(
934
- `API send-message taskId '${taskId}' does not match a run task.`,
935
- {
936
- action: "api",
937
- status: "error",
938
- runId: loaded.manifest.runId,
939
- },
940
- true,
941
- );
942
- try {
943
- return withRunLockSync(loaded.manifest, () => {
944
- const message = appendMailboxMessage(loaded.manifest, {
945
- direction,
946
- from,
947
- to,
948
- body,
949
- taskId,
950
- });
951
- appendEvent(loaded.manifest.eventsPath, {
952
- type: "mailbox.message",
953
- runId: loaded.manifest.runId,
954
- data: { id: message.id, direction, from, to },
955
- });
956
- ctx.events?.emit?.("crew.mailbox.message", {
957
- runId: loaded.manifest.runId,
958
- id: message.id,
959
- direction,
960
- from,
961
- to,
962
- taskId,
963
- source: "send-message",
964
- });
965
- return result(JSON.stringify(message, null, 2), {
966
- action: "api",
967
- status: "ok",
968
- runId: loaded.manifest.runId,
969
- artifactsRoot: loaded.manifest.artifactsRoot,
970
- });
971
- });
972
- } catch (error) {
973
- const message = error instanceof Error ? error.message : String(error);
974
- return result(
975
- message,
976
- {
977
- action: "api",
978
- status: "error",
979
- runId: loaded.manifest.runId,
980
- },
981
- true,
982
- );
983
- }
984
- }
985
- if (operation === "ack-message") {
986
- const messageId = typeof cfg.messageId === "string" ? cfg.messageId : undefined;
987
- if (!messageId)
988
- return result(
989
- paramRequired(
990
- "API ack-message",
991
- "config.messageId",
992
- "{ action: 'api', runId: 'team_...', config: { operation: 'ack-message', messageId: 'msg-1' } }",
993
- ),
994
- {
995
- action: "api",
996
- status: "error",
997
- runId: loaded.manifest.runId,
998
- },
999
- true,
1000
- );
1001
- try {
1002
- return withRunLockSync(loaded.manifest, () => {
1003
- const message = readMailboxMessage(loaded.manifest, messageId);
1004
- const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
1005
- appendEvent(loaded.manifest.eventsPath, {
1006
- type: "mailbox.acknowledged",
1007
- runId: loaded.manifest.runId,
1008
- data: { messageId },
1009
- });
1010
- if (message?.data?.kind === "group_join" && typeof message.data.requestId === "string") {
1011
- appendEvent(loaded.manifest.eventsPath, {
1012
- type: "agent.group_join.acknowledged",
1013
- runId: loaded.manifest.runId,
1014
- message: "Group join delivery acknowledged via mailbox ack.",
1015
- data: {
1016
- requestId: message.data.requestId,
1017
- messageId,
1018
- batchId: message.data.batchId,
1019
- partial: message.data.partial,
1020
- acknowledgedAt: delivery.updatedAt,
1021
- acknowledgedBy: "leader",
1022
- },
1023
- metadata: { provenance: "api" },
1024
- });
1025
- }
1026
- ctx.events?.emit?.("crew.mailbox.acknowledged", {
1027
- runId: loaded.manifest.runId,
1028
- messageId,
1029
- delivery,
1030
- });
1031
- return result(JSON.stringify(delivery, null, 2), {
1032
- action: "api",
1033
- status: "ok",
1034
- runId: loaded.manifest.runId,
1035
- artifactsRoot: loaded.manifest.artifactsRoot,
1036
- });
1037
- });
1038
- } catch (error) {
1039
- const message = error instanceof Error ? error.message : String(error);
1040
- return result(
1041
- message,
1042
- {
1043
- action: "api",
1044
- status: "error",
1045
- runId: loaded.manifest.runId,
1046
- },
1047
- true,
1048
- );
1049
- }
1050
- }
1051
- if (operation === "read-heartbeat") {
1052
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
1053
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
1054
- if (!task)
1055
- return result(
1056
- paramRequired(
1057
- "API read-heartbeat",
1058
- "config.taskId matching a task id or step id",
1059
- "{ action: 'api', runId: 'team_...', config: { operation: 'read-heartbeat', taskId: '01_01-agent' } }",
1060
- ),
1061
- {
1062
- action: "api",
1063
- status: "error",
1064
- runId: loaded.manifest.runId,
1065
- },
1066
- true,
1067
- );
1068
- return result(JSON.stringify(task.heartbeat ?? null, null, 2), {
1069
- action: "api",
1070
- status: "ok",
1071
- runId: loaded.manifest.runId,
1072
- artifactsRoot: loaded.manifest.artifactsRoot,
1073
- });
1074
- }
1075
- if (operation === "claim-task") {
1076
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
1077
- const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
1078
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
1079
- if (!task)
1080
- return result(
1081
- paramRequired(
1082
- "API claim-task",
1083
- "config.taskId matching a task id or step id",
1084
- "{ action: 'api', runId: 'team_...', config: { operation: 'claim-task', taskId: '01_01-agent' } }",
1085
- ),
1086
- {
1087
- action: "api",
1088
- status: "error",
1089
- runId: loaded.manifest.runId,
1090
- },
1091
- true,
1092
- );
1093
- try {
1094
- return withRunLockSync(loaded.manifest, () => {
1095
- const updatedTask = claimTask(task, owner);
1096
- const tasks = loaded.tasks.map((item) => (item.id === task.id ? updatedTask : item));
1097
- saveRunTasks(loaded.manifest, tasks);
1098
- appendEvent(loaded.manifest.eventsPath, {
1099
- type: "task.claimed",
1100
- runId: loaded.manifest.runId,
1101
- taskId: task.id,
1102
- data: {
1103
- owner,
1104
- token: "[REDACTED]",
1105
- leasedUntil: updatedTask.claim?.leasedUntil,
1106
- },
1107
- });
1108
- return result(JSON.stringify(updatedTask.claim, null, 2), {
1109
- action: "api",
1110
- status: "ok",
1111
- runId: loaded.manifest.runId,
1112
- artifactsRoot: loaded.manifest.artifactsRoot,
1113
- });
1114
- });
1115
- } catch (error) {
1116
- const message = error instanceof Error ? error.message : String(error);
1117
- return result(
1118
- message,
1119
- {
1120
- action: "api",
1121
- status: "error",
1122
- runId: loaded.manifest.runId,
1123
- },
1124
- true,
1125
- );
1126
- }
1127
- }
1128
- if (operation === "release-task-claim") {
1129
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
1130
- const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
1131
- const token = typeof cfg.token === "string" ? cfg.token : undefined;
1132
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
1133
- if (!task || !owner || !token)
1134
- return result(
1135
- paramRequired(
1136
- "API release-task-claim",
1137
- "config.taskId, config.owner, and config.token",
1138
- "{ action: 'api', runId: 'team_...', config: { operation: 'release-task-claim', taskId: '01_01-agent', owner: 'worker-1', token: 'tok-1' } }",
1139
- ),
1140
- {
1141
- action: "api",
1142
- status: "error",
1143
- runId: loaded.manifest.runId,
1144
- },
1145
- true,
1146
- );
1147
- try {
1148
- return withRunLockSync(loaded.manifest, () => {
1149
- const updatedTask = releaseTaskClaim(task, owner, token);
1150
- const tasks = loaded.tasks.map((item) => (item.id === task.id ? updatedTask : item));
1151
- saveRunTasks(loaded.manifest, tasks);
1152
- appendEvent(loaded.manifest.eventsPath, {
1153
- type: "task.claim_released",
1154
- runId: loaded.manifest.runId,
1155
- taskId: task.id,
1156
- data: { owner },
1157
- });
1158
- return result(JSON.stringify(updatedTask, null, 2), {
1159
- action: "api",
1160
- status: "ok",
1161
- runId: loaded.manifest.runId,
1162
- artifactsRoot: loaded.manifest.artifactsRoot,
1163
- });
1164
- });
1165
- } catch (error) {
1166
- const message = error instanceof Error ? error.message : String(error);
1167
- return result(
1168
- message,
1169
- {
1170
- action: "api",
1171
- status: "error",
1172
- runId: loaded.manifest.runId,
1173
- },
1174
- true,
1175
- );
1176
- }
1177
- }
1178
- if (operation === "transition-task-status") {
1179
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
1180
- const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
1181
- const token = typeof cfg.token === "string" ? cfg.token : undefined;
1182
- const to = cfg.status;
1183
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
1184
- if (!task || !owner || !token || !isTeamTaskStatus(to))
1185
- return result(
1186
- paramRequired(
1187
- "API transition-task-status",
1188
- "config.taskId, config.owner, config.token, and valid config.status",
1189
- "{ action: 'api', runId: 'team_...', config: { operation: 'transition-task-status', taskId: '01_01-agent', owner: 'worker-1', token: 'tok-1', status: 'done' } }",
1190
- ),
1191
- {
1192
- action: "api",
1193
- status: "error",
1194
- runId: loaded.manifest.runId,
1195
- },
1196
- true,
1197
- );
1198
- if (!canTransitionTaskStatus(task.status, to))
1199
- return result(
1200
- `Invalid task status transition: ${task.status} -> ${to}`,
1201
- {
1202
- action: "api",
1203
- status: "error",
1204
- runId: loaded.manifest.runId,
1205
- },
1206
- true,
1207
- );
1208
- try {
1209
- return withRunLockSync(loaded.manifest, () => {
1210
- const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
1211
- const tasks = loaded.tasks.map((item) => (item.id === task.id ? updatedTask : item));
1212
- saveRunTasks(loaded.manifest, tasks);
1213
- appendEvent(loaded.manifest.eventsPath, {
1214
- type: "task.status_transitioned",
1215
- runId: loaded.manifest.runId,
1216
- taskId: task.id,
1217
- data: { owner, status: to },
1218
- });
1219
- return result(JSON.stringify(updatedTask, null, 2), {
1220
- action: "api",
1221
- status: "ok",
1222
- runId: loaded.manifest.runId,
1223
- artifactsRoot: loaded.manifest.artifactsRoot,
1224
- });
1225
- });
1226
- } catch (error) {
1227
- const message = error instanceof Error ? error.message : String(error);
1228
- return result(
1229
- message,
1230
- {
1231
- action: "api",
1232
- status: "error",
1233
- runId: loaded.manifest.runId,
1234
- },
1235
- true,
1236
- );
1237
- }
1238
- }
1239
- if (operation === "write-heartbeat") {
1240
- const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
1241
- const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
1242
- if (!task)
1243
- return result(
1244
- paramRequired(
1245
- "API write-heartbeat",
1246
- "config.taskId matching a task id or step id",
1247
- "{ action: 'api', runId: 'team_...', config: { operation: 'write-heartbeat', taskId: '01_01-agent' } }",
1248
- ),
1249
- {
1250
- action: "api",
1251
- status: "error",
1252
- runId: loaded.manifest.runId,
1253
- },
1254
- true,
1255
- );
1256
- try {
1257
- return withRunLockSync(loaded.manifest, () => {
1258
- const heartbeat = touchWorkerHeartbeat(
1259
- task.heartbeat ?? {
1260
- workerId: task.id,
1261
- lastSeenAt: new Date().toISOString(),
1262
- },
1263
- {
1264
- alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined,
1265
- },
1266
- );
1267
- const tasks = loaded.tasks.map((item) => (item.id === task.id ? { ...item, heartbeat } : item));
1268
- saveRunTasks(loaded.manifest, tasks);
1269
- appendEvent(loaded.manifest.eventsPath, {
1270
- type: "worker.heartbeat",
1271
- runId: loaded.manifest.runId,
1272
- taskId: task.id,
1273
- data: { ...heartbeat },
1274
- });
1275
- return result(JSON.stringify(heartbeat, null, 2), {
1276
- action: "api",
1277
- status: "ok",
1278
- runId: loaded.manifest.runId,
1279
- artifactsRoot: loaded.manifest.artifactsRoot,
1280
- });
1281
- });
1282
- } catch (error) {
1283
- const message = error instanceof Error ? error.message : String(error);
1284
- return result(
1285
- message,
1286
- {
1287
- action: "api",
1288
- status: "error",
1289
- runId: loaded.manifest.runId,
1290
- },
1291
- true,
1292
- );
1293
- }
1294
- }
1295
- if (operation === "diff") {
1296
- const diffArtifacts = loaded.manifest.artifacts.filter((a) => a.kind === "diff" || a.kind === "patch");
1297
- if (diffArtifacts.length === 0) {
1298
- return result(`No diff artifacts found for run ${loaded.manifest.runId}. Diffs are captured in worktree mode.`, {
1299
- action: "api",
1300
- status: "ok",
1301
- runId: loaded.manifest.runId,
1302
- intent: `diff ${loaded.manifest.runId}: no diffs`,
1303
- });
1304
- }
1305
- const parts: string[] = [`Diff artifacts for run ${loaded.manifest.runId}:`];
1306
- for (const artifact of diffArtifacts) {
1307
- const content = safeReadContainedFile(loaded.manifest.artifactsRoot, artifact.path);
1308
- if (content) {
1309
- const display = content.length > 4000 ? content.slice(0, 4000) + "\n... (truncated)" : content;
1310
- parts.push(`\n--- ${artifact.path} ---\n${display}`);
1311
- }
1312
- }
1313
- return result(parts.join("\n"), {
1314
- action: "api",
1315
- status: "ok",
1316
- runId: loaded.manifest.runId,
1317
- artifactsRoot: loaded.manifest.artifactsRoot,
1318
- intent: `diff ${loaded.manifest.runId}`,
1319
- });
61
+
62
+ const handler = API_OPERATIONS[operation];
63
+ if (handler) {
64
+ return handler({ cfg, loaded, result, paramRequired, params, ctx });
1320
65
  }
1321
- return result(`Unknown API operation: ${operation}`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
66
+
67
+ // H4 (2026-08-10): list valid operations in the error so a typo (e.g.
68
+ // "claim_task" with an underscore instead of "claim-task") is self-
69
+ // correctable. The list derives from the dispatcher keys, so adding a
70
+ // new operation cannot drift from the error message.
71
+ const known = [...Object.keys(PRE_RUNID_OPERATIONS), ...Object.keys(API_OPERATIONS)];
72
+ return result(
73
+ `Unknown API operation: ${operation}\nValid operations: ${known.join(", ")}`,
74
+ { action: "api", status: "error", runId: loaded.manifest.runId },
75
+ true,
76
+ );
1322
77
  }