pi-crew 0.1.28 → 0.1.30

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 (59) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/NOTICE.md +1 -0
  3. package/docs/architecture.md +164 -92
  4. package/docs/refactor-tasks-phase6.md +662 -0
  5. package/docs/runtime-flow.md +148 -0
  6. package/package.json +1 -1
  7. package/schema.json +1 -0
  8. package/skills/git-master/SKILL.md +19 -0
  9. package/skills/read-only-explorer/SKILL.md +21 -0
  10. package/skills/safe-bash/SKILL.md +16 -0
  11. package/skills/task-packet/SKILL.md +23 -0
  12. package/skills/verify-evidence/SKILL.md +22 -0
  13. package/src/config/config.ts +2 -0
  14. package/src/config/defaults.ts +1 -0
  15. package/src/extension/async-notifier.ts +33 -4
  16. package/src/extension/register.ts +15 -522
  17. package/src/extension/registration/artifact-cleanup.ts +14 -0
  18. package/src/extension/registration/commands.ts +208 -0
  19. package/src/extension/registration/subagent-helpers.ts +1 -1
  20. package/src/extension/registration/subagent-tools.ts +110 -0
  21. package/src/extension/registration/team-tool.ts +44 -0
  22. package/src/extension/team-tool/api.ts +4 -4
  23. package/src/extension/team-tool/cancel.ts +31 -0
  24. package/src/extension/team-tool/inspect.ts +41 -0
  25. package/src/extension/team-tool/lifecycle-actions.ts +79 -0
  26. package/src/extension/team-tool/plan.ts +19 -0
  27. package/src/extension/team-tool/run.ts +41 -3
  28. package/src/extension/team-tool/status.ts +73 -0
  29. package/src/extension/team-tool.ts +57 -224
  30. package/src/runtime/async-marker.ts +26 -0
  31. package/src/runtime/async-runner.ts +44 -9
  32. package/src/runtime/background-runner.ts +2 -0
  33. package/src/runtime/child-pi.ts +5 -1
  34. package/src/runtime/concurrency.ts +9 -3
  35. package/src/runtime/crew-agent-records.ts +1 -0
  36. package/src/runtime/crew-agent-runtime.ts +2 -1
  37. package/src/runtime/model-fallback.ts +21 -4
  38. package/src/runtime/pi-args.ts +2 -0
  39. package/src/runtime/process-status.ts +1 -0
  40. package/src/runtime/role-permission.ts +11 -0
  41. package/src/runtime/task-runner/live-executor.ts +98 -0
  42. package/src/runtime/task-runner/progress.ts +111 -0
  43. package/src/runtime/task-runner/prompt-builder.ts +72 -0
  44. package/src/runtime/task-runner/result-utils.ts +14 -0
  45. package/src/runtime/task-runner/state-helpers.ts +22 -0
  46. package/src/runtime/task-runner.ts +38 -283
  47. package/src/runtime/team-runner.ts +116 -7
  48. package/src/schema/config-schema.ts +1 -0
  49. package/src/state/mailbox.ts +28 -0
  50. package/src/state/types.ts +16 -0
  51. package/src/subagents/async-entry.ts +1 -0
  52. package/src/subagents/index.ts +3 -0
  53. package/src/subagents/live/control.ts +1 -0
  54. package/src/subagents/live/manager.ts +1 -0
  55. package/src/subagents/live/realtime.ts +1 -0
  56. package/src/subagents/live/session-runtime.ts +1 -0
  57. package/src/subagents/manager.ts +1 -0
  58. package/src/subagents/spawn.ts +1 -0
  59. package/src/ui/live-run-sidebar.ts +1 -1
@@ -4,15 +4,15 @@ import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
4
4
  import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
5
5
  import { writeArtifact } from "../state/artifact-store.ts";
6
6
  import { appendEvent } from "../state/event-log.ts";
7
- import { loadRunManifestById, saveRunManifest, saveRunTasks } from "../state/state-store.ts";
7
+ import { saveRunManifest } from "../state/state-store.ts";
8
8
  import { createTaskClaim } from "../state/task-claims.ts";
9
9
  import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
10
10
  import type { WorkflowStep } from "../workflows/workflow-config.ts";
11
11
  import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
12
- import { buildConfiguredModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
12
+ import { buildConfiguredModelRouting, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
13
13
  import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
14
14
  import { runChildPi } from "./child-pi.ts";
15
- import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
15
+ import { buildTaskPacket } from "./task-packet.ts";
16
16
  import { createVerificationEvidence } from "./green-contract.ts";
17
17
  import { createStartupEvidence } from "./worker-startup.ts";
18
18
  import { permissionForRole } from "./role-permission.ts";
@@ -20,9 +20,11 @@ import { collectDependencyOutputContext, renderDependencyOutputContext, writeTas
20
20
  import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
21
21
  import { parseSessionUsage } from "./session-usage.ts";
22
22
  import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
23
- import { buildMemoryBlock } from "./agent-memory.ts";
24
- import { runLiveSessionTask } from "./live-session-runtime.ts";
25
23
  import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
24
+ import { coordinationBridgeInstructions, renderTaskPrompt } from "./task-runner/prompt-builder.ts";
25
+ import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, shouldFlushProgressEvent } from "./task-runner/progress.ts";
26
+ import { checkpointTask, persistSingleTaskUpdate, updateTask } from "./task-runner/state-helpers.ts";
27
+ import { cleanResultText, isFinalChildEvent } from "./task-runner/result-utils.ts";
26
28
 
27
29
  export interface TaskRunnerInput {
28
30
  manifest: TeamRunManifest;
@@ -42,218 +44,6 @@ export interface TaskRunnerInput {
42
44
  dependencyContextText?: string;
43
45
  }
44
46
 
45
- function readOnlyRoleInstructions(role: string): string {
46
- if (permissionForRole(role) !== "read_only") return "";
47
- return [
48
- "# READ-ONLY ROLE CONTRACT",
49
- "You are running in READ-ONLY mode for this task.",
50
- "- Do not create, modify, delete, move, or copy files.",
51
- "- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
52
- "- If implementation changes are needed, report exact recommendations instead of applying them.",
53
- "- Prefer read/grep/find/listing tools and read-only git inspection commands.",
54
- ].join("\n");
55
- }
56
-
57
- function coordinationBridgeInstructions(task: TeamTaskState): string {
58
- return [
59
- "# Crew Coordination Channel",
60
- `Mailbox target for this task: ${task.id}`,
61
- "Use the run mailbox contract for coordination with the leader/orchestrator:",
62
- "- If blocked or uncertain, report the blocker in your final result and, when mailbox tools/API are available, send an inbox/outbox message addressed to the leader.",
63
- "- If nudged, answer with current status, blocker, or smallest next step.",
64
- "- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
65
- "- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
66
- ].join("\n");
67
- }
68
-
69
- function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig): string {
70
- const memoryBlock = agent?.memory ? buildMemoryBlock(agent.name, agent.memory, task.cwd, Boolean(agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
71
- return [
72
- "# pi-crew Worker Runtime Context",
73
- `Run ID: ${manifest.runId}`,
74
- `Team: ${manifest.team}`,
75
- `Workflow: ${manifest.workflow ?? "(none)"}`,
76
- `State root: ${manifest.stateRoot}`,
77
- `Artifacts root: ${manifest.artifactsRoot}`,
78
- `Events path: ${manifest.eventsPath}`,
79
- `Task ID: ${task.id}`,
80
- `Task cwd: ${task.cwd}`,
81
- `Workspace mode: ${manifest.workspaceMode}`,
82
- "",
83
- `Goal:\n${manifest.goal}`,
84
- "",
85
- `Step: ${step.id}`,
86
- `Role: ${step.role}`,
87
- "",
88
- "Protocol:",
89
- "- Stay within the task scope unless the prompt explicitly says otherwise.",
90
- "- Report blockers and verification evidence in the final result.",
91
- "- Do not claim completion without evidence.",
92
- "- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
93
- "",
94
- readOnlyRoleInstructions(task.role),
95
- "",
96
- coordinationBridgeInstructions(task),
97
- "",
98
- task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
99
- "",
100
- (inputDependencyContext(task) || ""),
101
- memoryBlock,
102
- "Task:",
103
- step.task.replaceAll("{goal}", manifest.goal),
104
- ].join("\n");
105
- }
106
-
107
- function inputDependencyContext(task: TeamTaskState): string {
108
- return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
109
- }
110
-
111
- function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
112
- return tasks.map((task) => task.id === updated.id ? updated : task);
113
- }
114
-
115
- function persistSingleTaskUpdate(manifest: TeamRunManifest, fallbackTasks: TeamTaskState[], updated: TeamTaskState): TeamTaskState[] {
116
- const latest = loadRunManifestById(manifest.cwd, manifest.runId)?.tasks ?? fallbackTasks;
117
- const merged = updateTask(latest, updated);
118
- saveRunTasks(manifest, merged);
119
- return merged;
120
- }
121
-
122
- function asRecord(value: unknown): Record<string, unknown> | undefined {
123
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
124
- }
125
-
126
- function textFromContent(content: unknown): string[] {
127
- if (typeof content === "string") return [content];
128
- if (!Array.isArray(content)) return [];
129
- const text: string[] = [];
130
- for (const part of content) {
131
- const obj = asRecord(part);
132
- if (!obj) continue;
133
- if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
134
- else if (typeof obj.content === "string") text.push(obj.content);
135
- }
136
- return text;
137
- }
138
-
139
- function eventText(event: unknown): string[] {
140
- const obj = asRecord(event);
141
- if (!obj) return [];
142
- const text: string[] = [];
143
- if (typeof obj.text === "string") text.push(obj.text);
144
- if (typeof obj.output === "string") text.push(obj.output);
145
- text.push(...textFromContent(obj.content));
146
- const message = asRecord(obj.message);
147
- if (message) text.push(...textFromContent(message.content));
148
- return text.filter((entry) => entry.trim());
149
- }
150
-
151
- function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
152
- for (const key of keys) {
153
- const value = obj[key];
154
- if (typeof value === "number" && Number.isFinite(value)) return value;
155
- }
156
- return undefined;
157
- }
158
-
159
- function eventUsage(event: unknown): { input?: number; output?: number; turns?: number } | undefined {
160
- const obj = asRecord(event);
161
- if (!obj) return undefined;
162
- const direct = {
163
- input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
164
- output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
165
- turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
166
- };
167
- if (Object.values(direct).some((value) => value !== undefined)) return direct;
168
- for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
169
- const nested = eventUsage(obj[key]);
170
- if (nested) return nested;
171
- }
172
- const message = asRecord(obj.message);
173
- return message ? eventUsage(message.usage) : undefined;
174
- }
175
-
176
- function previewArgs(args: unknown): string | undefined {
177
- if (!args) return undefined;
178
- try {
179
- const text = typeof args === "string" ? args : JSON.stringify(args);
180
- return text.length > 240 ? `${text.slice(0, 240)}…` : text;
181
- } catch {
182
- return undefined;
183
- }
184
- }
185
-
186
- function applyUsageToProgress(progress: CrewAgentProgress | undefined, usage: UsageState | undefined): CrewAgentProgress | undefined {
187
- if (!usage) return progress;
188
- const base = progress ?? emptyCrewAgentProgress();
189
- return {
190
- ...base,
191
- tokens: (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0),
192
- turns: usage.turns ?? base.turns,
193
- };
194
- }
195
-
196
- function shouldFlushProgressEvent(event: unknown): boolean {
197
- const type = asRecord(event)?.type;
198
- return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
199
- }
200
-
201
- function cleanResultText(text: string | undefined): string | undefined {
202
- const trimmed = text?.trim();
203
- if (!trimmed) return undefined;
204
- const doneIndex = trimmed.lastIndexOf("\nDONE\n");
205
- if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
206
- if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
207
- const fencedPromptIndex = trimmed.lastIndexOf("</file>");
208
- if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
209
- return trimmed;
210
- }
211
-
212
- function progressEventSummary(task: TeamTaskState, event: unknown): ProgressEventSummary {
213
- const type = asRecord(event)?.type;
214
- return {
215
- eventType: typeof type === "string" ? type : "event",
216
- currentTool: task.agentProgress?.currentTool,
217
- toolCount: task.agentProgress?.toolCount,
218
- tokens: task.agentProgress?.tokens,
219
- turns: task.agentProgress?.turns,
220
- activityState: task.agentProgress?.activityState,
221
- lastActivityAt: task.agentProgress?.lastActivityAt,
222
- };
223
- }
224
-
225
- function applyAgentProgressEvent(progress: CrewAgentProgress, event: unknown, startedAt: string | undefined): CrewAgentProgress {
226
- const obj = asRecord(event);
227
- const now = new Date().toISOString();
228
- const next: CrewAgentProgress = { ...progress, recentTools: [...progress.recentTools], recentOutput: [...progress.recentOutput], lastActivityAt: now, activityState: "active" };
229
- if (startedAt) next.durationMs = Date.now() - new Date(startedAt).getTime();
230
- if (obj?.type === "tool_execution_start") {
231
- next.toolCount += 1;
232
- next.currentTool = typeof obj.toolName === "string" ? obj.toolName : typeof obj.name === "string" ? obj.name : "tool";
233
- next.currentToolArgs = previewArgs(obj.args);
234
- next.currentToolStartedAt = now;
235
- }
236
- if (obj?.type === "tool_execution_end") {
237
- if (next.currentTool) next.recentTools.push({ tool: next.currentTool, args: next.currentToolArgs, endedAt: now });
238
- next.currentTool = undefined;
239
- next.currentToolArgs = undefined;
240
- next.currentToolStartedAt = undefined;
241
- }
242
- if ((obj?.type === "tool_execution_error" || obj?.type === "tool_execution_failed") && next.currentTool) {
243
- next.failedTool = next.currentTool;
244
- }
245
- const usage = eventUsage(event);
246
- if (usage) {
247
- next.tokens = (usage.input ?? 0) + (usage.output ?? 0);
248
- next.turns = usage.turns ?? next.turns;
249
- }
250
- const text = eventText(event);
251
- if (text.length > 0) next.recentOutput.push(...text.flatMap((entry) => entry.split(/\r?\n/)).filter(Boolean).slice(-10));
252
- if (next.recentTools.length > 25) next.recentTools.splice(0, next.recentTools.length - 25);
253
- if (next.recentOutput.length > 50) next.recentOutput.splice(0, next.recentOutput.length - 50);
254
- return next;
255
- }
256
-
257
47
  export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
258
48
  let manifest = input.manifest;
259
49
  const workspace = prepareTaskWorkspace(manifest, input.task);
@@ -276,6 +66,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
276
66
  let tasks = updateTask(input.tasks, task);
277
67
  const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
278
68
  tasks = persistSingleTaskUpdate(manifest, tasks, task);
69
+ if (runtimeKind === "child-process") ({ task, tasks } = checkpointTask(manifest, tasks, task, "started"));
279
70
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
280
71
  appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, runtime: runtimeKind, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
281
72
  const permissionMode = permissionForRole(task.role);
@@ -305,13 +96,15 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
305
96
  producer: task.id,
306
97
  });
307
98
  if (runtimeKind === "child-process") {
308
- const candidates = buildConfiguredModelCandidates({ overrideModel: input.modelOverride, stepModel: input.step.model, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: manifest.cwd });
99
+ const modelRoutingPlan = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: manifest.cwd });
100
+ const candidates = modelRoutingPlan.candidates;
309
101
  const attemptModels = candidates.length > 0 ? candidates : [undefined];
310
102
  const logs: string[] = [];
311
103
  let finalStdout = "";
312
104
  let finalStderr = "";
313
105
  modelAttempts = [];
314
106
  const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
107
+ let finalCheckpointWritten = false;
315
108
  let lastAgentRecordPersistedAt = 0;
316
109
  let lastRunProgressPersistedAt = 0;
317
110
  let lastRunProgressSummary: ProgressEventSummary | undefined;
@@ -344,11 +137,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
344
137
  signal: input.signal,
345
138
  transcriptPath,
346
139
  maxDepth: input.limits?.maxTaskDepth,
140
+ onSpawn: (pid) => {
141
+ ({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
142
+ },
347
143
  onStdoutLine: (line) => appendCrewAgentOutput(manifest, task.id, line),
348
144
  onJsonEvent: (event) => {
349
145
  appendCrewAgentEvent(manifest, task.id, event);
350
146
  task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
351
147
  tasks = updateTask(tasks, task);
148
+ if (!finalCheckpointWritten && isFinalChildEvent(event)) {
149
+ finalCheckpointWritten = true;
150
+ ({ task, tasks } = checkpointTask(manifest, tasks, task, "child-stdout-final"));
151
+ }
352
152
  persistChildProgress(event);
353
153
  },
354
154
  });
@@ -381,6 +181,12 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
381
181
  content: [...logs, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${parsedOutput?.jsonEvents ?? 0}`, parsedOutput?.usage ? `usage=${JSON.stringify(parsedOutput.usage)}` : "", "", "STDOUT:", finalStdout, "", "STDERR:", finalStderr].join("\n"),
382
182
  producer: task.id,
383
183
  });
184
+ const successfulAttemptIndex = modelAttempts.findIndex((attempt) => attempt.success);
185
+ const usedAttempt = successfulAttemptIndex === -1 ? Math.max(0, modelAttempts.length - 1) : successfulAttemptIndex;
186
+ const resolvedModel = modelAttempts[usedAttempt]?.model ?? candidates[0] ?? "default";
187
+ const fallbackReason = usedAttempt > 0 ? modelAttempts[usedAttempt - 1]?.error : undefined;
188
+ task = { ...task, modelRouting: { requested: modelRoutingPlan.requested, resolved: resolvedModel, fallbackChain: candidates, reason: fallbackReason ?? modelRoutingPlan.reason, usedAttempt } };
189
+ tasks = updateTask(tasks, task);
384
190
  const sessionUsage = parseSessionUsage(transcriptPath);
385
191
  const effectiveUsage = parsedOutput?.usage ?? sessionUsage;
386
192
  if (effectiveUsage) {
@@ -397,72 +203,21 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
397
203
  producer: task.id,
398
204
  });
399
205
  }
206
+ task = { ...task, resultArtifact, ...(logArtifact ? { logArtifact } : {}), ...(transcriptArtifact ? { transcriptArtifact } : {}) };
207
+ tasks = updateTask(tasks, task);
208
+ ({ task, tasks } = checkpointTask(manifest, tasks, task, "artifact-written"));
400
209
  } else if (runtimeKind === "live-session") {
401
- const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
402
- let lastAgentRecordPersistedAt = 0;
403
- let lastRunProgressPersistedAt = 0;
404
- let lastRunProgressSummary: ProgressEventSummary | undefined;
405
- const persistLiveProgress = (event: unknown, force = false): void => {
406
- const now = Date.now();
407
- if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
408
- upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
409
- lastAgentRecordPersistedAt = now;
410
- }
411
- const summary = progressEventSummary(task, event);
412
- const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
413
- if (decision.shouldAppend) {
414
- appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
415
- lastRunProgressSummary = summary;
416
- lastRunProgressPersistedAt = now;
417
- }
418
- };
419
- const attemptStartedAt = new Date();
420
- const liveResult = await runLiveSessionTask({
421
- manifest,
422
- task,
423
- step: input.step,
424
- agent: input.agent,
425
- prompt,
426
- signal: input.signal,
427
- transcriptPath,
428
- runtimeConfig: input.runtimeConfig,
429
- parentContext: input.parentContext,
430
- parentModel: input.parentModel,
431
- modelRegistry: input.modelRegistry,
432
- onOutput: (text) => appendCrewAgentOutput(manifest, task.id, text),
433
- onEvent: (event) => {
434
- appendCrewAgentEvent(manifest, task.id, event);
435
- task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
436
- tasks = updateTask(tasks, task);
437
- persistLiveProgress(event);
438
- },
439
- });
440
- startupEvidence = createStartupEvidence({ command: "live-session", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: liveResult.exitCode === 0 && !liveResult.error, stderr: liveResult.stderr, error: liveResult.error, exitCode: liveResult.exitCode });
441
- exitCode = liveResult.exitCode;
442
- error = liveResult.error || (liveResult.exitCode && liveResult.exitCode !== 0 ? liveResult.stderr || `Live session exited with ${liveResult.exitCode}` : undefined);
443
- parsedOutput = { finalText: liveResult.stdout, textEvents: liveResult.stdout ? [liveResult.stdout] : [], jsonEvents: liveResult.jsonEvents, usage: liveResult.usage };
444
- if (liveResult.usage) task = { ...task, usage: liveResult.usage, agentProgress: applyUsageToProgress(task.agentProgress, liveResult.usage) };
445
- persistLiveProgress({ type: "attempt_finished" }, true);
446
- resultArtifact = writeArtifact(manifest.artifactsRoot, {
447
- kind: "result",
448
- relativePath: `results/${task.id}.txt`,
449
- content: liveResult.stdout || liveResult.stderr || "(no output)",
450
- producer: task.id,
451
- });
452
- logArtifact = writeArtifact(manifest.artifactsRoot, {
453
- kind: "log",
454
- relativePath: `logs/${task.id}.log`,
455
- content: [`runtime=live-session`, `finalExitCode=${exitCode ?? "null"}`, `jsonEvents=${liveResult.jsonEvents}`, liveResult.usage ? `usage=${JSON.stringify(liveResult.usage)}` : "", "", "STDOUT:", liveResult.stdout, "", "STDERR:", liveResult.stderr].join("\n"),
456
- producer: task.id,
457
- });
458
- if (fs.existsSync(transcriptPath)) {
459
- transcriptArtifact = writeArtifact(manifest.artifactsRoot, {
460
- kind: "log",
461
- relativePath: `transcripts/${task.id}.jsonl`,
462
- content: fs.readFileSync(transcriptPath, "utf-8"),
463
- producer: task.id,
464
- });
465
- }
210
+ const { runLiveTask } = await import("./task-runner/live-executor.ts");
211
+ const live = await runLiveTask({ manifest, tasks, task, step: input.step, agent: input.agent, prompt, signal: input.signal, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry });
212
+ task = live.task;
213
+ tasks = live.tasks;
214
+ startupEvidence = live.startupEvidence;
215
+ exitCode = live.exitCode;
216
+ error = live.error;
217
+ parsedOutput = live.parsedOutput;
218
+ resultArtifact = live.resultArtifact;
219
+ logArtifact = live.logArtifact;
220
+ transcriptArtifact = live.transcriptArtifact;
466
221
  } else {
467
222
  resultArtifact = writeArtifact(manifest.artifactsRoot, {
468
223
  kind: "result",
@@ -6,7 +6,7 @@ import { writeArtifact } from "../state/artifact-store.ts";
6
6
  import { appendEvent } from "../state/event-log.ts";
7
7
  import type { TeamConfig } from "../teams/team-config.ts";
8
8
  import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TeamTaskState } from "../state/types.ts";
9
- import { saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
9
+ import { saveRunManifest, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
10
10
  import { aggregateUsage, formatUsage } from "../state/usage.ts";
11
11
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
12
12
  import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
@@ -109,10 +109,14 @@ function slug(value: string): string {
109
109
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
110
110
  }
111
111
 
112
- export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]): AdaptivePlan | undefined {
112
+ function extractAdaptivePlanJson(text: string): string | undefined {
113
113
  const markerMatch = text.match(/ADAPTIVE_PLAN_JSON_START\s*([\s\S]*?)\s*ADAPTIVE_PLAN_JSON_END/);
114
114
  const fencedMatch = markerMatch ? undefined : text.match(/```(?:json)?\s*([\s\S]*?)```/i);
115
- const raw = markerMatch?.[1] ?? fencedMatch?.[1];
115
+ return markerMatch?.[1] ?? fencedMatch?.[1];
116
+ }
117
+
118
+ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]): AdaptivePlan | undefined {
119
+ const raw = extractAdaptivePlanJson(text);
116
120
  if (!raw) return undefined;
117
121
  let parsed: unknown;
118
122
  try { parsed = JSON.parse(raw); } catch { return undefined; }
@@ -141,6 +145,98 @@ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]):
141
145
  return phases.length ? { phases } : undefined;
142
146
  }
143
147
 
148
+ function closeUnbalancedJson(raw: string): string {
149
+ let result = raw.trim();
150
+ const stack: string[] = [];
151
+ let inString = false;
152
+ let escaped = false;
153
+ for (const char of result) {
154
+ if (escaped) {
155
+ escaped = false;
156
+ continue;
157
+ }
158
+ if (char === "\\" && inString) {
159
+ escaped = true;
160
+ continue;
161
+ }
162
+ if (char === '"') {
163
+ inString = !inString;
164
+ continue;
165
+ }
166
+ if (inString) continue;
167
+ if (char === "{") stack.push("}");
168
+ else if (char === "[") stack.push("]");
169
+ else if ((char === "}" || char === "]") && stack.at(-1) === char) stack.pop();
170
+ }
171
+ while (stack.length) result += stack.pop();
172
+ return result;
173
+ }
174
+
175
+ function adaptiveRoleAlias(role: string, allowed: Set<string>): string | undefined {
176
+ if (allowed.has(role)) return role;
177
+ const normalized = slug(role);
178
+ const aliases: Record<string, string[]> = {
179
+ reviewer: ["code-reviewer", "review", "code-review", "critic"],
180
+ "security-reviewer": ["security", "security-review", "sec-review"],
181
+ "test-engineer": ["tester", "qa", "test"],
182
+ executor: ["developer", "implementer", "coder", "engineer"],
183
+ explorer: ["researcher", "scout"],
184
+ analyst: ["analysis", "analyzer"],
185
+ };
186
+ for (const [target, names] of Object.entries(aliases)) if (allowed.has(target) && names.includes(normalized)) return target;
187
+ return undefined;
188
+ }
189
+
190
+ export function __test__repairAdaptivePlan(text: string, allowedRoles: string[]): { plan?: AdaptivePlan; repaired: boolean; reason?: string } {
191
+ const raw = extractAdaptivePlanJson(text);
192
+ if (!raw) return { repaired: false, reason: "missing-json" };
193
+ const candidates = [raw, closeUnbalancedJson(raw)];
194
+ let parsed: unknown;
195
+ for (const candidate of candidates) {
196
+ try {
197
+ parsed = JSON.parse(candidate);
198
+ break;
199
+ } catch {
200
+ // Try the next repair candidate.
201
+ }
202
+ }
203
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { repaired: false, reason: "invalid-json" };
204
+ const phasesRaw = Array.isArray((parsed as { phases?: unknown }).phases) ? (parsed as { phases: unknown[] }).phases : Array.isArray((parsed as { tasks?: unknown }).tasks) ? [{ name: "adaptive", tasks: (parsed as { tasks: unknown[] }).tasks }] : undefined;
205
+ if (!phasesRaw) return { repaired: false, reason: "missing-phases" };
206
+ const allowed = new Set(allowedRoles);
207
+ const phases: AdaptivePlanPhase[] = [];
208
+ let total = 0;
209
+ let repaired = raw !== closeUnbalancedJson(raw);
210
+ for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
211
+ if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) continue;
212
+ const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
213
+ if (!Array.isArray(phaseObj.tasks)) continue;
214
+ const tasks: AdaptivePlanTask[] = [];
215
+ for (const taskRaw of phaseObj.tasks) {
216
+ if (total >= MAX_ADAPTIVE_TASKS) {
217
+ repaired = true;
218
+ break;
219
+ }
220
+ if (!taskRaw || typeof taskRaw !== "object" || Array.isArray(taskRaw)) {
221
+ repaired = true;
222
+ continue;
223
+ }
224
+ const taskObj = taskRaw as { role?: unknown; title?: unknown; task?: unknown };
225
+ const role = typeof taskObj.role === "string" ? adaptiveRoleAlias(taskObj.role, allowed) : undefined;
226
+ const taskText = typeof taskObj.task === "string" ? taskObj.task.trim() : "";
227
+ if (!role || !taskText) {
228
+ repaired = true;
229
+ continue;
230
+ }
231
+ tasks.push({ role, title: typeof taskObj.title === "string" ? taskObj.title : undefined, task: taskText });
232
+ total++;
233
+ }
234
+ if (tasks.length) phases.push({ name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`, tasks });
235
+ if (total >= MAX_ADAPTIVE_TASKS) break;
236
+ }
237
+ return phases.length ? { plan: { phases }, repaired: true, reason: repaired ? "repaired" : "normalized" } : { repaired: false, reason: "empty-plan" };
238
+ }
239
+
144
240
  function reconstructAdaptiveWorkflow(workflow: WorkflowConfig, tasks: TeamTaskState[]): WorkflowConfig {
145
241
  const existing = new Set(workflow.steps.map((step) => step.id));
146
242
  const steps: WorkflowStep[] = [];
@@ -167,10 +263,20 @@ function injectAdaptivePlanIfReady(input: { manifest: TeamRunManifest; tasks: Te
167
263
  appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner result artifact could not be read." });
168
264
  return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
169
265
  }
170
- const plan = __test__parseAdaptivePlan(text, input.team.roles.map((role) => role.name));
266
+ const allowedRoles = input.team.roles.map((role) => role.name);
267
+ let plan = __test__parseAdaptivePlan(text, allowedRoles);
171
268
  if (!plan) {
172
- appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner did not produce a valid plan; no dynamic subagents were spawned." });
173
- return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
269
+ const repair = process.env.PI_CREW_ADAPTIVE_REPAIR === "0" || process.env.PI_TEAMS_ADAPTIVE_REPAIR === "0" ? { repaired: false, reason: "disabled" } : __test__repairAdaptivePlan(text, allowedRoles);
270
+ if (repair.plan) {
271
+ plan = repair.plan;
272
+ const repairArtifact = writeArtifact(input.manifest.artifactsRoot, { kind: "metadata", relativePath: "metadata/adaptive-repair.json", producer: assessTask.id, content: `${JSON.stringify({ reason: repair.reason, phases: repair.plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) }, null, 2)}\n` });
273
+ saveRunManifest({ ...input.manifest, updatedAt: new Date().toISOString(), artifacts: [...input.manifest.artifacts, repairArtifact] });
274
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_repaired", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner output was repaired before dynamic subagents were spawned.", data: { reason: repair.reason } });
275
+ } else {
276
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_repair_failed", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner output could not be repaired.", data: { reason: repair.reason } });
277
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner did not produce a valid plan; no dynamic subagents were spawned." });
278
+ return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
279
+ }
174
280
  }
175
281
  const steps: WorkflowStep[] = [];
176
282
  const tasks: TeamTaskState[] = [];
@@ -327,7 +433,10 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
327
433
 
328
434
  const snapshot = taskGraphSnapshot(tasks, queueIndex);
329
435
  const readyRoles = snapshot.ready.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
330
- const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
436
+ const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, workflowMaxConcurrency: workflow.maxConcurrency, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, allowUnboundedConcurrency: input.limits?.allowUnboundedConcurrency, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
437
+ if (concurrency.reason.includes(";unbounded:")) {
438
+ appendEvent(manifest.eventsPath, { type: "limits.unbounded", runId: manifest.runId, message: "Unbounded worker concurrency was explicitly enabled for this run.", data: { concurrencyReason: concurrency.reason, maxConcurrent: concurrency.maxConcurrent } });
439
+ }
331
440
  const readyBatch = getReadyTasks(tasks, concurrency.selectedCount, queueIndex);
332
441
  if (readyBatch.length === 0) {
333
442
  tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
@@ -18,6 +18,7 @@ export const PiTeamsAutonomousConfigSchema = Type.Object({
18
18
 
19
19
  export const PiTeamsLimitsConfigSchema = Type.Object({
20
20
  maxConcurrentWorkers: Type.Optional(Type.Integer({ minimum: 1 })),
21
+ allowUnboundedConcurrency: Type.Optional(Type.Boolean()),
21
22
  maxTaskDepth: Type.Optional(Type.Integer({ minimum: 1 })),
22
23
  maxChildrenPerTask: Type.Optional(Type.Integer({ minimum: 1 })),
23
24
  maxRunMinutes: Type.Optional(Type.Integer({ minimum: 1 })),
@@ -34,6 +34,11 @@ export interface MailboxValidationReport {
34
34
  repaired: string[];
35
35
  }
36
36
 
37
+ export interface MailboxReplayResult {
38
+ messages: MailboxMessage[];
39
+ updatedAt: string;
40
+ }
41
+
37
42
  function mailboxDir(manifest: TeamRunManifest): string {
38
43
  return path.join(manifest.stateRoot, "mailbox");
39
44
  }
@@ -110,6 +115,18 @@ export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirect
110
115
  return directions.flatMap((item) => safeReadMailboxFile(mailboxPath(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
111
116
  }
112
117
 
118
+ function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
119
+ const messages = [...safeReadMailboxFile(mailboxPath(manifest, "inbox"), "inbox")];
120
+ const tasksDir = path.join(mailboxDir(manifest), "tasks");
121
+ if (fs.existsSync(tasksDir)) {
122
+ for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
123
+ if (!entry.isDirectory()) continue;
124
+ messages.push(...safeReadMailboxFile(mailboxPath(manifest, "inbox", entry.name), "inbox"));
125
+ }
126
+ }
127
+ return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
128
+ }
129
+
113
130
  export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
114
131
  ensureRunMailbox(manifest);
115
132
  try {
@@ -163,6 +180,17 @@ export function acknowledgeMailboxMessage(manifest: TeamRunManifest, messageId:
163
180
  return delivery;
164
181
  }
165
182
 
183
+ export function replayPendingMailboxMessages(manifest: TeamRunManifest): MailboxReplayResult {
184
+ const delivery = readDeliveryState(manifest);
185
+ const pending = readAllInboxMessages(manifest).filter((message) => message.status !== "acknowledged" && delivery.messages[message.id] !== "acknowledged");
186
+ if (!pending.length) return { messages: [], updatedAt: delivery.updatedAt };
187
+ const updatedAt = new Date().toISOString();
188
+ for (const message of pending) delivery.messages[message.id] = "delivered";
189
+ delivery.updatedAt = updatedAt;
190
+ writeDeliveryState(manifest, delivery);
191
+ return { messages: pending, updatedAt };
192
+ }
193
+
166
194
  export function validateMailbox(manifest: TeamRunManifest, options: { repair?: boolean } = {}): MailboxValidationReport {
167
195
  ensureRunMailbox(manifest);
168
196
  const issues: MailboxValidationIssue[] = [];
@@ -118,12 +118,26 @@ export interface ModelAttemptState {
118
118
  error?: string;
119
119
  }
120
120
 
121
+ export interface ModelRoutingState {
122
+ requested?: string;
123
+ resolved: string;
124
+ fallbackChain: string[];
125
+ reason?: string;
126
+ usedAttempt: number;
127
+ }
128
+
121
129
  export interface TaskWorktreeState {
122
130
  path: string;
123
131
  branch: string;
124
132
  reused: boolean;
125
133
  }
126
134
 
135
+ export interface TaskCheckpointState {
136
+ phase: "started" | "child-spawned" | "child-stdout-final" | "artifact-written";
137
+ updatedAt: string;
138
+ childPid?: number;
139
+ }
140
+
127
141
  export interface TeamTaskState {
128
142
  id: string;
129
143
  runId: string;
@@ -144,12 +158,14 @@ export interface TeamTaskState {
144
158
  exitCode?: number | null;
145
159
  model?: string;
146
160
  modelAttempts?: ModelAttemptState[];
161
+ modelRouting?: ModelRoutingState;
147
162
  usage?: UsageState;
148
163
  jsonEvents?: number;
149
164
  agentProgress?: CrewAgentProgress;
150
165
  error?: string;
151
166
  claim?: TaskClaimState;
152
167
  heartbeat?: WorkerHeartbeatState;
168
+ checkpoint?: TaskCheckpointState;
153
169
  taskPacket?: TaskPacket;
154
170
  verification?: VerificationEvidence;
155
171
  graph?: TaskGraphNode;
@@ -0,0 +1 @@
1
+ export * from "../runtime/async-runner.ts";
@@ -0,0 +1,3 @@
1
+ export * from "./spawn.ts";
2
+ export * from "./manager.ts";
3
+ export * from "./async-entry.ts";