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
@@ -0,0 +1,443 @@
1
+ /**
2
+ * Extracted `api` operation handlers — read/inspect group (H3 phase 2).
3
+ *
4
+ * Extracted from `src/extension/team-tool/api.ts` on 2026-08-10. Behaviour is
5
+ * byte-identical to the inline `if` blocks they replace (mechanical move +
6
+ * re-indent + ctx pass-through). The existing api-ops-coverage test suite
7
+ * guards against drift.
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import { loadConfig } from "../../../config/config.ts";
12
+ import { buildAgentDashboard, readAgentOutput } from "../../../runtime/agent-observability.ts";
13
+ import { buildCapabilityInventory } from "../../../runtime/capability-inventory.ts";
14
+ import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../../runtime/crew-agent-records.ts";
15
+ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../../runtime/foreground-control.ts";
16
+ import { probeLiveSessionRuntime } from "../../../runtime/live-session/live-session-runtime.ts";
17
+ import { resolveCrewRuntime } from "../../../runtime/model/runtime-resolver.ts";
18
+ import { readEvents, readEventsCursor } from "../../../state/event-log/event-log.ts";
19
+ import { globMatch } from "../../../utils/glob-match.ts";
20
+ import { resolveRealContainedPath } from "../../../utils/safe-paths.ts";
21
+ import type { ApiHandlerContext, ApiOperationHandler, ApiPreHandler } from "./handler-context.ts";
22
+
23
+ function safeReadContainedFile(baseDir: string, filePath: string | undefined): string | undefined {
24
+ if (!filePath) return undefined;
25
+ let safePath: string;
26
+ try {
27
+ safePath = resolveRealContainedPath(baseDir, filePath);
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ return fs.existsSync(safePath) ? fs.readFileSync(safePath, "utf-8") : undefined;
32
+ }
33
+
34
+ function safeContainedPath(baseDir: string, filePath: string | undefined): string | undefined {
35
+ if (!filePath) return undefined;
36
+ try {
37
+ return resolveRealContainedPath(baseDir, filePath);
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ function snapshotHasRunId(snapshot: { values?: unknown }, runId: string): boolean {
44
+ const values = Array.isArray(snapshot.values) ? snapshot.values : [];
45
+ return values.some((value) => {
46
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
47
+ const labels = (value as { labels?: unknown }).labels;
48
+ return labels && typeof labels === "object" && !Array.isArray(labels) && (labels as Record<string, unknown>).runId === runId;
49
+ });
50
+ }
51
+
52
+ function findTaskByIdOrStepId(ctx: ApiHandlerContext, taskId: string | undefined) {
53
+ if (!taskId) return undefined;
54
+ return ctx.loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
55
+ }
56
+
57
+ /** Pre-runId operation (runs before the runId guard): `metrics-snapshot`. */
58
+ export const handleMetricsSnapshot: ApiPreHandler = (hctx) => {
59
+ const { cfg, params, ctx, result } = hctx;
60
+ const filter = typeof cfg.filter === "string" ? cfg.filter : undefined;
61
+ const runIdFilter = typeof cfg.runId === "string" ? cfg.runId : params.runId;
62
+ const snapshots = ctx.metricRegistry?.snapshot() ?? [];
63
+ const filtered = snapshots.filter((snapshot) => {
64
+ if (filter && !globMatch(snapshot.name, filter)) return false;
65
+ if (runIdFilter && !snapshotHasRunId(snapshot, runIdFilter)) return false;
66
+ return true;
67
+ });
68
+ return result(JSON.stringify(filtered, null, 2), {
69
+ action: "api",
70
+ status: "ok",
71
+ ...(runIdFilter ? { runId: runIdFilter } : {}),
72
+ });
73
+ };
74
+
75
+ /** Pre-runId operation (runs before the runId guard): `inventory`. */
76
+ export const handleInventory: ApiPreHandler = (hctx) => {
77
+ const { ctx, result } = hctx;
78
+ const inventory = buildCapabilityInventory(ctx.cwd, ctx.config);
79
+ return result(JSON.stringify(inventory, null, 2), {
80
+ action: "api",
81
+ status: "ok",
82
+ });
83
+ };
84
+
85
+ export const handleReadManifest: ApiOperationHandler = (hctx) => {
86
+ const { loaded, result } = hctx;
87
+ return result(JSON.stringify(loaded.manifest, null, 2), {
88
+ action: "api",
89
+ status: "ok",
90
+ runId: loaded.manifest.runId,
91
+ artifactsRoot: loaded.manifest.artifactsRoot,
92
+ });
93
+ };
94
+
95
+ export const handleListTasks: ApiOperationHandler = (hctx) => {
96
+ const { loaded, result } = hctx;
97
+ return result(JSON.stringify(loaded.tasks, null, 2), {
98
+ action: "api",
99
+ status: "ok",
100
+ runId: loaded.manifest.runId,
101
+ artifactsRoot: loaded.manifest.artifactsRoot,
102
+ });
103
+ };
104
+
105
+ export const handleReadTask: ApiOperationHandler = (hctx) => {
106
+ const { cfg, loaded, result, paramRequired } = hctx;
107
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
108
+ const task = findTaskByIdOrStepId(hctx, taskId);
109
+ if (!task)
110
+ return result(
111
+ paramRequired(
112
+ "API read-task",
113
+ "config.taskId matching a task id or step id",
114
+ "{ action: 'api', runId: 'team_...', config: { operation: 'read-task', taskId: '01_01-agent' } }",
115
+ ),
116
+ {
117
+ action: "api",
118
+ status: "error",
119
+ runId: loaded.manifest.runId,
120
+ },
121
+ true,
122
+ );
123
+ return result(JSON.stringify(task, null, 2), {
124
+ action: "api",
125
+ status: "ok",
126
+ runId: loaded.manifest.runId,
127
+ artifactsRoot: loaded.manifest.artifactsRoot,
128
+ });
129
+ };
130
+
131
+ export const handleReadEvents: ApiOperationHandler = (hctx) => {
132
+ const { cfg, loaded, result } = hctx;
133
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
134
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
135
+ const payload =
136
+ sinceSeq !== undefined || limit !== undefined
137
+ ? readEventsCursor(loaded.manifest.eventsPath, {
138
+ sinceSeq,
139
+ limit,
140
+ })
141
+ : {
142
+ events: readEvents(loaded.manifest.eventsPath),
143
+ nextSeq: undefined,
144
+ total: undefined,
145
+ };
146
+ return result(JSON.stringify(payload, null, 2), {
147
+ action: "api",
148
+ status: "ok",
149
+ runId: loaded.manifest.runId,
150
+ artifactsRoot: loaded.manifest.artifactsRoot,
151
+ });
152
+ };
153
+
154
+ export const handleRuntimeCapabilities: ApiOperationHandler = async (hctx) => {
155
+ const { ctx, loaded, result } = hctx;
156
+ const loadedConfig = loadConfig(ctx.cwd);
157
+ return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), {
158
+ action: "api",
159
+ status: "ok",
160
+ runId: loaded.manifest.runId,
161
+ artifactsRoot: loaded.manifest.artifactsRoot,
162
+ });
163
+ };
164
+
165
+ export const handleProbeLiveSession: ApiOperationHandler = async (hctx) => {
166
+ const { loaded, result } = hctx;
167
+ return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), {
168
+ action: "api",
169
+ status: "ok",
170
+ runId: loaded.manifest.runId,
171
+ artifactsRoot: loaded.manifest.artifactsRoot,
172
+ });
173
+ };
174
+
175
+ export const handleListAgents: ApiOperationHandler = (hctx) => {
176
+ const { loaded, result } = hctx;
177
+ return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), {
178
+ action: "api",
179
+ status: "ok",
180
+ runId: loaded.manifest.runId,
181
+ artifactsRoot: loaded.manifest.artifactsRoot,
182
+ });
183
+ };
184
+
185
+ export const handleGetAgentResult: ApiOperationHandler = (hctx) => {
186
+ const { cfg, loaded, result, paramRequired } = hctx;
187
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
188
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
189
+ if (!agent)
190
+ return result(
191
+ paramRequired(
192
+ "API get-agent-result",
193
+ "config.agentId matching an agent id or task id",
194
+ "{ action: 'api', runId: 'team_...', config: { operation: 'get-agent-result', agentId: 'agent-1' } }",
195
+ ),
196
+ {
197
+ action: "api",
198
+ status: "error",
199
+ runId: loaded.manifest.runId,
200
+ },
201
+ true,
202
+ );
203
+ const task = loaded.tasks.find((item) => item.id === agent.taskId);
204
+ const text = safeReadContainedFile(loaded.manifest.artifactsRoot, task?.resultArtifact?.path) ?? JSON.stringify(agent, null, 2);
205
+ return result(text, {
206
+ action: "api",
207
+ status: "ok",
208
+ runId: loaded.manifest.runId,
209
+ artifactsRoot: loaded.manifest.artifactsRoot,
210
+ });
211
+ };
212
+
213
+ export const handleReadAgentStatus: ApiOperationHandler = (hctx) => {
214
+ const { cfg, loaded, result, paramRequired } = hctx;
215
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
216
+ const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
217
+ const status = agent ? (readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent) : undefined;
218
+ if (!status)
219
+ return result(
220
+ paramRequired(
221
+ "API read-agent-status",
222
+ "config.agentId matching an agent id or task id",
223
+ "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-status', agentId: 'agent-1' } }",
224
+ ),
225
+ {
226
+ action: "api",
227
+ status: "error",
228
+ runId: loaded.manifest.runId,
229
+ },
230
+ true,
231
+ );
232
+ return result(JSON.stringify(status, null, 2), {
233
+ action: "api",
234
+ status: "ok",
235
+ runId: loaded.manifest.runId,
236
+ artifactsRoot: loaded.manifest.artifactsRoot,
237
+ });
238
+ };
239
+
240
+ export const handleReadAgentEvents: ApiOperationHandler = (hctx) => {
241
+ const { cfg, loaded, result, paramRequired } = hctx;
242
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
243
+ const agents = readCrewAgents(loaded.manifest);
244
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
245
+ if (!agent)
246
+ return result(
247
+ paramRequired(
248
+ "API read-agent-events",
249
+ "config.agentId matching an agent id or task id, or at least one agent in the run",
250
+ "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-events', agentId: 'agent-1' } }",
251
+ ),
252
+ {
253
+ action: "api",
254
+ status: "error",
255
+ runId: loaded.manifest.runId,
256
+ },
257
+ true,
258
+ );
259
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
260
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
261
+ const cursorPayload = readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit });
262
+ const payload =
263
+ sinceSeq !== undefined || limit !== undefined ? cursorPayload : { path: cursorPayload.path, events: cursorPayload.events };
264
+ return result(JSON.stringify(payload, null, 2), {
265
+ action: "api",
266
+ status: "ok",
267
+ runId: loaded.manifest.runId,
268
+ artifactsRoot: loaded.manifest.artifactsRoot,
269
+ });
270
+ };
271
+
272
+ export const handleReadAgentTranscript: ApiOperationHandler = (hctx) => {
273
+ const { cfg, loaded, result, paramRequired } = hctx;
274
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
275
+ const agents = readCrewAgents(loaded.manifest);
276
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
277
+ if (!agent)
278
+ return result(
279
+ paramRequired(
280
+ "API read-agent-transcript",
281
+ "config.agentId matching an agent id or task id, or at least one agent in the run",
282
+ "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-transcript', agentId: 'agent-1' } }",
283
+ ),
284
+ {
285
+ action: "api",
286
+ status: "error",
287
+ runId: loaded.manifest.runId,
288
+ },
289
+ true,
290
+ );
291
+ const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
292
+ const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
293
+ const artifactText = artifactTranscriptPath ? (safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "") : "";
294
+ const fallbackText = artifactText ? "" : (safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "");
295
+ const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
296
+ const text = artifactText || fallbackText;
297
+ return result(text || `(no transcript at ${transcriptPath})`, {
298
+ action: "api",
299
+ status: "ok",
300
+ runId: loaded.manifest.runId,
301
+ artifactsRoot: loaded.manifest.artifactsRoot,
302
+ });
303
+ };
304
+
305
+ export const handleReadAgentOutput: ApiOperationHandler = (hctx) => {
306
+ const { cfg, loaded, result, paramRequired } = hctx;
307
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
308
+ const agents = readCrewAgents(loaded.manifest);
309
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
310
+ if (!agent)
311
+ return result(
312
+ paramRequired(
313
+ "API read-agent-output",
314
+ "config.agentId matching an agent id or task id, or at least one agent in the run",
315
+ "{ action: 'api', runId: 'team_...', config: { operation: 'read-agent-output', agentId: 'agent-1' } }",
316
+ ),
317
+ {
318
+ action: "api",
319
+ status: "error",
320
+ runId: loaded.manifest.runId,
321
+ },
322
+ true,
323
+ );
324
+ const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
325
+ return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), {
326
+ action: "api",
327
+ status: "ok",
328
+ runId: loaded.manifest.runId,
329
+ artifactsRoot: loaded.manifest.artifactsRoot,
330
+ });
331
+ };
332
+
333
+ export const handleAgentDashboard: ApiOperationHandler = (hctx) => {
334
+ const { loaded, result } = hctx;
335
+ return result(buildAgentDashboard(loaded.manifest).text, {
336
+ action: "api",
337
+ status: "ok",
338
+ runId: loaded.manifest.runId,
339
+ artifactsRoot: loaded.manifest.artifactsRoot,
340
+ });
341
+ };
342
+
343
+ export const handleForegroundStatus: ApiOperationHandler = (hctx) => {
344
+ const { loaded, result } = hctx;
345
+ return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), {
346
+ action: "api",
347
+ status: "ok",
348
+ runId: loaded.manifest.runId,
349
+ artifactsRoot: loaded.manifest.artifactsRoot,
350
+ });
351
+ };
352
+
353
+ export const handleForegroundInterrupt: ApiOperationHandler = (hctx) => {
354
+ const { cfg, loaded, result } = hctx;
355
+ const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
356
+ return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), {
357
+ action: "api",
358
+ status: "ok",
359
+ runId: loaded.manifest.runId,
360
+ artifactsRoot: loaded.manifest.artifactsRoot,
361
+ });
362
+ };
363
+
364
+ export const handleReadHeartbeat: ApiOperationHandler = (hctx) => {
365
+ const { cfg, loaded, result, paramRequired } = hctx;
366
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
367
+ const task = findTaskByIdOrStepId(hctx, taskId);
368
+ if (!task)
369
+ return result(
370
+ paramRequired(
371
+ "API read-heartbeat",
372
+ "config.taskId matching a task id or step id",
373
+ "{ action: 'api', runId: 'team_...', config: { operation: 'read-heartbeat', taskId: '01_01-agent' } }",
374
+ ),
375
+ {
376
+ action: "api",
377
+ status: "error",
378
+ runId: loaded.manifest.runId,
379
+ },
380
+ true,
381
+ );
382
+ return result(JSON.stringify(task.heartbeat ?? null, null, 2), {
383
+ action: "api",
384
+ status: "ok",
385
+ runId: loaded.manifest.runId,
386
+ artifactsRoot: loaded.manifest.artifactsRoot,
387
+ });
388
+ };
389
+
390
+ export const handleDiff: ApiOperationHandler = (hctx) => {
391
+ const { loaded, result } = hctx;
392
+ const diffArtifacts = loaded.manifest.artifacts.filter((a) => a.kind === "diff" || a.kind === "patch");
393
+ if (diffArtifacts.length === 0) {
394
+ return result(`No diff artifacts found for run ${loaded.manifest.runId}. Diffs are captured in worktree mode.`, {
395
+ action: "api",
396
+ status: "ok",
397
+ runId: loaded.manifest.runId,
398
+ intent: `diff ${loaded.manifest.runId}: no diffs`,
399
+ });
400
+ }
401
+ const parts: string[] = [`Diff artifacts for run ${loaded.manifest.runId}:`];
402
+ for (const artifact of diffArtifacts) {
403
+ const content = safeReadContainedFile(loaded.manifest.artifactsRoot, artifact.path);
404
+ if (content) {
405
+ const display = content.length > 4000 ? content.slice(0, 4000) + "\n... (truncated)" : content;
406
+ parts.push(`\n--- ${artifact.path} ---\n${display}`);
407
+ }
408
+ }
409
+ return result(parts.join("\n"), {
410
+ action: "api",
411
+ status: "ok",
412
+ runId: loaded.manifest.runId,
413
+ artifactsRoot: loaded.manifest.artifactsRoot,
414
+ intent: `diff ${loaded.manifest.runId}`,
415
+ });
416
+ };
417
+
418
+ /** Pre-runId dispatcher map. Consumed by handleApi BEFORE the runId guard. */
419
+ export const PRE_RUNID_OPERATIONS: Record<string, ApiPreHandler> = {
420
+ "metrics-snapshot": handleMetricsSnapshot,
421
+ inventory: handleInventory,
422
+ };
423
+
424
+ /** Dispatcher map for the read/inspect group. Consumed by handleApi. */
425
+ export const READ_OPERATIONS: Record<string, ApiOperationHandler> = {
426
+ "read-manifest": handleReadManifest,
427
+ "list-tasks": handleListTasks,
428
+ "read-task": handleReadTask,
429
+ "read-events": handleReadEvents,
430
+ "runtime-capabilities": handleRuntimeCapabilities,
431
+ "probe-live-session": handleProbeLiveSession,
432
+ "list-agents": handleListAgents,
433
+ "get-agent-result": handleGetAgentResult,
434
+ "read-agent-status": handleReadAgentStatus,
435
+ "read-agent-events": handleReadAgentEvents,
436
+ "read-agent-transcript": handleReadAgentTranscript,
437
+ "read-agent-output": handleReadAgentOutput,
438
+ "agent-dashboard": handleAgentDashboard,
439
+ "foreground-status": handleForegroundStatus,
440
+ "foreground-interrupt": handleForegroundInterrupt,
441
+ "read-heartbeat": handleReadHeartbeat,
442
+ diff: handleDiff,
443
+ };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Extracted `api` operation handlers for task-claim lifecycle (H3 phase 1).
3
+ *
4
+ * Extracted from `src/extension/team-tool/api.ts` on 2026-08-10 to prove the
5
+ * per-group extraction pattern described in improvement-plan-2026-08-10 H3.
6
+ * The three operations here (claim-task, release-task-claim,
7
+ * transition-task-status) are a cohesive group: all mutate task state under
8
+ * the run lock and emit a corresponding event. Each is a thin wrapper around
9
+ * the shared `task-claims.ts` coordination primitives.
10
+ *
11
+ * Behaviour is byte-identical to the inline `if` blocks they replace — the
12
+ * extraction is mechanical (move + re-indent + pass ctx). The existing
13
+ * `test/unit/extension/core/api-claim.test.ts` suite guards against drift.
14
+ */
15
+
16
+ import { canTransitionTaskStatus, isTeamTaskStatus } from "../../../state/contracts.ts";
17
+ import { withRunLockSync } from "../../../state/coordination/locks.ts";
18
+ import { claimTask, releaseTaskClaim, transitionClaimedTaskStatus } from "../../../state/coordination/task-claims.ts";
19
+ import { appendEvent } from "../../../state/event-log/event-log.ts";
20
+ import { saveRunTasks } from "../../../state/stores/state-store.ts";
21
+ import type { ApiHandlerContext, ApiOperationHandler } from "./handler-context.ts";
22
+
23
+ /** Find a task by id OR stepId — the convention used by every claim op. */
24
+ function findTaskByIdOrStepId(ctx: ApiHandlerContext, taskId: string | undefined) {
25
+ if (!taskId) return undefined;
26
+ return ctx.loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
27
+ }
28
+
29
+ /** handleApi operation: `claim-task`. */
30
+ export const handleClaimTask: ApiOperationHandler = (ctx) => {
31
+ const { cfg, loaded, result, paramRequired } = ctx;
32
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
33
+ const owner = typeof cfg.owner === "string" ? cfg.owner : "api";
34
+ const task = findTaskByIdOrStepId(ctx, taskId);
35
+ if (!task)
36
+ return result(
37
+ paramRequired(
38
+ "API claim-task",
39
+ "config.taskId matching a task id or step id",
40
+ "{ action: 'api', runId: 'team_...', config: { operation: 'claim-task', taskId: '01_01-agent' } }",
41
+ ),
42
+ {
43
+ action: "api",
44
+ status: "error",
45
+ runId: loaded.manifest.runId,
46
+ },
47
+ true,
48
+ );
49
+ try {
50
+ return withRunLockSync(loaded.manifest, () => {
51
+ const updatedTask = claimTask(task, owner);
52
+ const tasks = loaded.tasks.map((item) => (item.id === task.id ? updatedTask : item));
53
+ saveRunTasks(loaded.manifest, tasks);
54
+ appendEvent(loaded.manifest.eventsPath, {
55
+ type: "task.claimed",
56
+ runId: loaded.manifest.runId,
57
+ taskId: task.id,
58
+ data: {
59
+ owner,
60
+ token: "[REDACTED]",
61
+ leasedUntil: updatedTask.claim?.leasedUntil,
62
+ },
63
+ });
64
+ return result(JSON.stringify(updatedTask.claim, null, 2), {
65
+ action: "api",
66
+ status: "ok",
67
+ runId: loaded.manifest.runId,
68
+ artifactsRoot: loaded.manifest.artifactsRoot,
69
+ });
70
+ });
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ return result(
74
+ message,
75
+ {
76
+ action: "api",
77
+ status: "error",
78
+ runId: loaded.manifest.runId,
79
+ },
80
+ true,
81
+ );
82
+ }
83
+ };
84
+
85
+ /** handleApi operation: `release-task-claim`. */
86
+ export const handleReleaseTaskClaim: ApiOperationHandler = (ctx) => {
87
+ const { cfg, loaded, result, paramRequired } = ctx;
88
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
89
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
90
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
91
+ const task = findTaskByIdOrStepId(ctx, taskId);
92
+ if (!task || !owner || !token)
93
+ return result(
94
+ paramRequired(
95
+ "API release-task-claim",
96
+ "config.taskId, config.owner, and config.token",
97
+ "{ action: 'api', runId: 'team_...', config: { operation: 'release-task-claim', taskId: '01_01-agent', owner: 'worker-1', token: 'tok-1' } }",
98
+ ),
99
+ {
100
+ action: "api",
101
+ status: "error",
102
+ runId: loaded.manifest.runId,
103
+ },
104
+ true,
105
+ );
106
+ try {
107
+ return withRunLockSync(loaded.manifest, () => {
108
+ const updatedTask = releaseTaskClaim(task, owner, token);
109
+ const tasks = loaded.tasks.map((item) => (item.id === task.id ? updatedTask : item));
110
+ saveRunTasks(loaded.manifest, tasks);
111
+ appendEvent(loaded.manifest.eventsPath, {
112
+ type: "task.claim_released",
113
+ runId: loaded.manifest.runId,
114
+ taskId: task.id,
115
+ data: { owner },
116
+ });
117
+ return result(JSON.stringify(updatedTask, null, 2), {
118
+ action: "api",
119
+ status: "ok",
120
+ runId: loaded.manifest.runId,
121
+ artifactsRoot: loaded.manifest.artifactsRoot,
122
+ });
123
+ });
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ return result(
127
+ message,
128
+ {
129
+ action: "api",
130
+ status: "error",
131
+ runId: loaded.manifest.runId,
132
+ },
133
+ true,
134
+ );
135
+ }
136
+ };
137
+
138
+ /** handleApi operation: `transition-task-status`. */
139
+ export const handleTransitionTaskStatus: ApiOperationHandler = (ctx) => {
140
+ const { cfg, loaded, result, paramRequired } = ctx;
141
+ const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
142
+ const owner = typeof cfg.owner === "string" ? cfg.owner : undefined;
143
+ const token = typeof cfg.token === "string" ? cfg.token : undefined;
144
+ const to = cfg.status;
145
+ const task = findTaskByIdOrStepId(ctx, taskId);
146
+ if (!task || !owner || !token || !isTeamTaskStatus(to))
147
+ return result(
148
+ paramRequired(
149
+ "API transition-task-status",
150
+ "config.taskId, config.owner, config.token, and valid config.status",
151
+ "{ action: 'api', runId: 'team_...', config: { operation: 'transition-task-status', taskId: '01_01-agent', owner: 'worker-1', token: 'tok-1', status: 'done' } }",
152
+ ),
153
+ {
154
+ action: "api",
155
+ status: "error",
156
+ runId: loaded.manifest.runId,
157
+ },
158
+ true,
159
+ );
160
+ if (!canTransitionTaskStatus(task.status, to))
161
+ return result(
162
+ `Invalid task status transition: ${task.status} -> ${to}`,
163
+ {
164
+ action: "api",
165
+ status: "error",
166
+ runId: loaded.manifest.runId,
167
+ },
168
+ true,
169
+ );
170
+ try {
171
+ return withRunLockSync(loaded.manifest, () => {
172
+ const updatedTask = transitionClaimedTaskStatus(task, owner, token, to);
173
+ const tasks = loaded.tasks.map((item) => (item.id === task.id ? updatedTask : item));
174
+ saveRunTasks(loaded.manifest, tasks);
175
+ appendEvent(loaded.manifest.eventsPath, {
176
+ type: "task.status_transitioned",
177
+ runId: loaded.manifest.runId,
178
+ taskId: task.id,
179
+ data: { owner, status: to },
180
+ });
181
+ return result(JSON.stringify(updatedTask, null, 2), {
182
+ action: "api",
183
+ status: "ok",
184
+ runId: loaded.manifest.runId,
185
+ artifactsRoot: loaded.manifest.artifactsRoot,
186
+ });
187
+ });
188
+ } catch (error) {
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ return result(
191
+ message,
192
+ {
193
+ action: "api",
194
+ status: "error",
195
+ runId: loaded.manifest.runId,
196
+ },
197
+ true,
198
+ );
199
+ }
200
+ };
201
+
202
+ /** Dispatcher map for the task-claim group. Consumed by handleApi. */
203
+ export const TASK_CLAIM_OPERATIONS: Record<string, ApiOperationHandler> = {
204
+ "claim-task": handleClaimTask,
205
+ "release-task-claim": handleReleaseTaskClaim,
206
+ "transition-task-status": handleTransitionTaskStatus,
207
+ };