pi-crew 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +8 -2
- package/docs/usage.md +4 -1
- package/package.json +1 -1
- package/schema.json +6 -3
- package/src/config/config.ts +6 -0
- package/src/extension/register.ts +36 -7
- package/src/extension/team-recommendation.ts +9 -3
- package/src/extension/team-tool.ts +61 -4
- package/src/runtime/child-pi.ts +25 -3
- package/src/runtime/crew-agent-records.ts +8 -0
- package/src/runtime/crew-agent-runtime.ts +3 -0
- package/src/runtime/pi-json-output.ts +3 -2
- package/src/runtime/policy-engine.ts +1 -1
- package/src/runtime/task-display.ts +38 -0
- package/src/runtime/task-runner.ts +19 -2
- package/src/runtime/team-runner.ts +242 -240
- package/src/ui/live-run-sidebar.ts +95 -0
- package/src/ui/powerbar-publisher.ts +20 -5
- package/src/ui/run-dashboard.ts +13 -3
- package/teams/parallel-research.team.md +14 -0
- package/workflows/parallel-research.workflow.md +50 -0
|
@@ -1,240 +1,242 @@
|
|
|
1
|
-
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
|
-
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
3
|
-
import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
|
|
4
|
-
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
|
-
import { appendEvent } from "../state/event-log.ts";
|
|
6
|
-
import type { TeamConfig } from "../teams/team-config.ts";
|
|
7
|
-
import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
8
|
-
import { saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
9
|
-
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
10
|
-
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
11
|
-
import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
|
|
12
|
-
import { buildRecoveryLedger } from "./recovery-recipes.ts";
|
|
13
|
-
import { getReadyTasks, refreshTaskGraphQueues, taskGraphSnapshot } from "./task-graph-scheduler.ts";
|
|
14
|
-
import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
|
|
15
|
-
import { aggregateTaskOutputs } from "./task-output-context.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
`
|
|
95
|
-
`
|
|
96
|
-
`
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
for (const item of
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
manifest
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
manifest = updateRunStatus(manifest, "
|
|
209
|
-
} else {
|
|
210
|
-
manifest = updateRunStatus(manifest, "
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`
|
|
225
|
-
`
|
|
226
|
-
`
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"",
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"",
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
1
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
|
+
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
3
|
+
import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
|
|
4
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
|
+
import { appendEvent } from "../state/event-log.ts";
|
|
6
|
+
import type { TeamConfig } from "../teams/team-config.ts";
|
|
7
|
+
import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
8
|
+
import { saveRunManifest, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
|
|
9
|
+
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
10
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
11
|
+
import { evaluateCrewPolicy, summarizePolicyDecisions } from "./policy-engine.ts";
|
|
12
|
+
import { buildRecoveryLedger } from "./recovery-recipes.ts";
|
|
13
|
+
import { getReadyTasks, refreshTaskGraphQueues, taskGraphSnapshot } from "./task-graph-scheduler.ts";
|
|
14
|
+
import { checkBranchFreshness } from "../worktree/branch-freshness.ts";
|
|
15
|
+
import { aggregateTaskOutputs } from "./task-output-context.ts";
|
|
16
|
+
import { saveCrewAgents } from "./crew-agent-records.ts";
|
|
17
|
+
import { recordsForMaterializedTasks } from "./task-display.ts";
|
|
18
|
+
import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
|
|
19
|
+
import { runTeamTask } from "./task-runner.ts";
|
|
20
|
+
|
|
21
|
+
export interface ExecuteTeamRunInput {
|
|
22
|
+
manifest: TeamRunManifest;
|
|
23
|
+
tasks: TeamTaskState[];
|
|
24
|
+
team: TeamConfig;
|
|
25
|
+
workflow: WorkflowConfig;
|
|
26
|
+
agents: AgentConfig[];
|
|
27
|
+
executeWorkers: boolean;
|
|
28
|
+
limits?: CrewLimitsConfig;
|
|
29
|
+
runtime?: CrewRuntimeCapabilities;
|
|
30
|
+
runtimeConfig?: CrewRuntimeConfig;
|
|
31
|
+
parentContext?: string;
|
|
32
|
+
parentModel?: unknown;
|
|
33
|
+
modelRegistry?: unknown;
|
|
34
|
+
modelOverride?: string;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findReadyTask(tasks: TeamTaskState[]): TeamTaskState | undefined {
|
|
39
|
+
return getReadyTasks(tasks, 1)[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findStep(workflow: WorkflowConfig, task: TeamTaskState): WorkflowStep {
|
|
43
|
+
const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
|
|
44
|
+
if (!step) throw new Error(`Workflow step '${task.stepId}' not found for task '${task.id}'.`);
|
|
45
|
+
return step;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findAgent(agents: AgentConfig[], task: TeamTaskState): AgentConfig {
|
|
49
|
+
const agent = agents.find((candidate) => candidate.name === task.agent);
|
|
50
|
+
if (!agent) throw new Error(`Agent '${task.agent}' not found for task '${task.id}'.`);
|
|
51
|
+
return agent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function markBlocked(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
|
|
55
|
+
return tasks.map((task) => task.status === "queued" ? { ...task, status: "skipped", error: reason, finishedAt: new Date().toISOString(), graph: task.graph ? { ...task.graph, queue: "blocked" } : undefined } : task);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mergeArtifacts(items: ArtifactDescriptor[]): ArtifactDescriptor[] {
|
|
59
|
+
const byPath = new Map<string, ArtifactDescriptor>();
|
|
60
|
+
for (const item of items) byPath.set(item.path, item);
|
|
61
|
+
return [...byPath.values()];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function mergeTaskUpdates(base: TeamTaskState[], results: Array<{ tasks: TeamTaskState[] }>): TeamTaskState[] {
|
|
65
|
+
let merged = base;
|
|
66
|
+
for (const result of results) {
|
|
67
|
+
for (const updated of result.tasks) {
|
|
68
|
+
const current = merged.find((task) => task.id === updated.id);
|
|
69
|
+
if (!current) continue;
|
|
70
|
+
if (updated.status !== current.status || updated.finishedAt || updated.startedAt || updated.resultArtifact || updated.error) {
|
|
71
|
+
merged = merged.map((task) => task.id === updated.id ? updated : task);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return refreshTaskGraphQueues(merged);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatTaskProgress(task: TeamTaskState): string {
|
|
79
|
+
return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string): TeamRunManifest {
|
|
83
|
+
const counts = new Map<string, number>();
|
|
84
|
+
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
85
|
+
const queue = taskGraphSnapshot(tasks);
|
|
86
|
+
const progress = writeArtifact(manifest.artifactsRoot, {
|
|
87
|
+
kind: "progress",
|
|
88
|
+
relativePath: "progress.md",
|
|
89
|
+
producer,
|
|
90
|
+
content: [
|
|
91
|
+
`# pi-crew progress ${manifest.runId}`,
|
|
92
|
+
"",
|
|
93
|
+
`Status: ${manifest.status}`,
|
|
94
|
+
`Team: ${manifest.team}`,
|
|
95
|
+
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
96
|
+
`Updated: ${new Date().toISOString()}`,
|
|
97
|
+
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
98
|
+
`Queue: ready=${queue.ready.length}, blocked=${queue.blocked.length}, running=${queue.running.length}, done=${queue.done.length}, failed=${queue.failed.length}, cancelled=${queue.cancelled.length}`,
|
|
99
|
+
"",
|
|
100
|
+
"## Tasks",
|
|
101
|
+
...tasks.map(formatTaskProgress),
|
|
102
|
+
"",
|
|
103
|
+
].join("\n"),
|
|
104
|
+
});
|
|
105
|
+
return { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)), progress] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?: CrewLimitsConfig): TeamRunManifest {
|
|
109
|
+
const branchFreshness = checkBranchFreshness(manifest.cwd);
|
|
110
|
+
const branchArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
111
|
+
kind: "metadata",
|
|
112
|
+
relativePath: "metadata/branch-freshness.json",
|
|
113
|
+
producer: "branch-freshness",
|
|
114
|
+
content: `${JSON.stringify(branchFreshness, null, 2)}\n`,
|
|
115
|
+
});
|
|
116
|
+
let decisions: PolicyDecision[] = evaluateCrewPolicy({ manifest, tasks, limits });
|
|
117
|
+
if (branchFreshness.status === "stale" || branchFreshness.status === "diverged") {
|
|
118
|
+
const branchDecision: PolicyDecision = {
|
|
119
|
+
action: "notify",
|
|
120
|
+
reason: "branch_stale",
|
|
121
|
+
message: branchFreshness.message,
|
|
122
|
+
createdAt: new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
decisions = [...decisions, branchDecision];
|
|
125
|
+
appendEvent(manifest.eventsPath, { type: "branch.stale", runId: manifest.runId, message: branchFreshness.message, data: { branchFreshness } });
|
|
126
|
+
}
|
|
127
|
+
const policyArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
128
|
+
kind: "metadata",
|
|
129
|
+
relativePath: "policy-decisions.json",
|
|
130
|
+
producer: "policy-engine",
|
|
131
|
+
content: `${JSON.stringify(decisions, null, 2)}\n`,
|
|
132
|
+
});
|
|
133
|
+
const recoveryLedger = buildRecoveryLedger(decisions);
|
|
134
|
+
const recoveryArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
135
|
+
kind: "metadata",
|
|
136
|
+
relativePath: "recovery-ledger.json",
|
|
137
|
+
producer: "recovery-engine",
|
|
138
|
+
content: `${JSON.stringify(recoveryLedger, null, 2)}\n`,
|
|
139
|
+
});
|
|
140
|
+
for (const item of decisions) appendEvent(manifest.eventsPath, { type: item.action === "escalate" ? "policy.escalated" : "policy.action", runId: manifest.runId, taskId: item.taskId, message: item.message, data: { action: item.action, reason: item.reason } });
|
|
141
|
+
for (const item of recoveryLedger.entries) appendEvent(manifest.eventsPath, { type: item.state === "escalation_required" ? "recovery.escalated" : "recovery.attempted", runId: manifest.runId, taskId: item.taskId, message: item.message, data: { scenario: item.scenario, steps: item.steps, attempt: item.attempt, state: item.state } });
|
|
142
|
+
return { ...manifest, updatedAt: new Date().toISOString(), policyDecisions: decisions, artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "metadata" && (artifact.path.endsWith("policy-decisions.json") || artifact.path.endsWith("recovery-ledger.json") || artifact.path.endsWith("branch-freshness.json")))), branchArtifact, policyArtifact, recoveryArtifact] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
146
|
+
let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
|
|
147
|
+
let tasks = refreshTaskGraphQueues(input.tasks);
|
|
148
|
+
manifest = writeProgress(manifest, tasks, "team-runner");
|
|
149
|
+
saveRunManifest(manifest);
|
|
150
|
+
const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
151
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
152
|
+
|
|
153
|
+
while (tasks.some((task) => task.status === "queued")) {
|
|
154
|
+
if (input.signal?.aborted) {
|
|
155
|
+
tasks = tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: "Run cancelled." } : task);
|
|
156
|
+
saveRunTasks(manifest, tasks);
|
|
157
|
+
manifest = updateRunStatus(manifest, "cancelled", "Run cancelled.");
|
|
158
|
+
return { manifest, tasks };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const failed = tasks.find((task) => task.status === "failed");
|
|
162
|
+
if (failed) {
|
|
163
|
+
tasks = markBlocked(tasks, `Blocked by failed task '${failed.id}'.`);
|
|
164
|
+
saveRunTasks(manifest, tasks);
|
|
165
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
166
|
+
manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
|
|
167
|
+
return { manifest, tasks };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const defaultConcurrency = input.workflow.name === "parallel-research" ? 4 : input.workflow.name === "research" ? 2 : input.workflow.name === "implementation" || input.workflow.name === "review" || input.workflow.name === "default" ? 2 : 1;
|
|
171
|
+
const maxConcurrent = Math.max(1, input.limits?.maxConcurrentWorkers ?? input.team.maxConcurrency ?? defaultConcurrency);
|
|
172
|
+
const readyBatch = getReadyTasks(tasks, maxConcurrent);
|
|
173
|
+
if (readyBatch.length === 0) {
|
|
174
|
+
tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
|
|
175
|
+
saveRunTasks(manifest, tasks);
|
|
176
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
177
|
+
manifest = updateRunStatus(manifest, "blocked", "No ready queued task.");
|
|
178
|
+
return { manifest, tasks };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, message: `Starting ready batch with ${readyBatch.length} task(s).`, data: { taskIds: readyBatch.map((task) => task.id), maxConcurrent } });
|
|
182
|
+
const results = await Promise.all(readyBatch.map((task) => {
|
|
183
|
+
const step = findStep(input.workflow, task);
|
|
184
|
+
const agent = findAgent(input.agents, task);
|
|
185
|
+
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, limits: input.limits });
|
|
186
|
+
}));
|
|
187
|
+
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
188
|
+
tasks = mergeTaskUpdates(tasks, results);
|
|
189
|
+
saveRunTasks(manifest, tasks);
|
|
190
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
191
|
+
const completedBatch = readyBatch.map((task) => tasks.find((item) => item.id === task.id) ?? task);
|
|
192
|
+
const batchArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
193
|
+
kind: "summary",
|
|
194
|
+
relativePath: `batches/${readyBatch.map((task) => task.id).join("+")}.md`,
|
|
195
|
+
producer: "team-runner",
|
|
196
|
+
content: aggregateTaskOutputs(completedBatch),
|
|
197
|
+
});
|
|
198
|
+
const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: readyBatch, allTasks: tasks });
|
|
199
|
+
manifest = { ...manifest, artifacts: mergeArtifacts([...manifest.artifacts, batchArtifact, ...(groupDelivery?.artifact ? [groupDelivery.artifact] : [])]) };
|
|
200
|
+
manifest = writeProgress(manifest, tasks, "team-runner");
|
|
201
|
+
saveRunManifest(manifest);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const failed = tasks.find((task) => task.status === "failed");
|
|
205
|
+
manifest = applyPolicy(manifest, tasks, input.limits);
|
|
206
|
+
const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
|
|
207
|
+
if (failed) {
|
|
208
|
+
manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
|
|
209
|
+
} else if (blockingDecision) {
|
|
210
|
+
manifest = updateRunStatus(manifest, "blocked", blockingDecision.message);
|
|
211
|
+
} else {
|
|
212
|
+
manifest = updateRunStatus(manifest, "completed", input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.");
|
|
213
|
+
}
|
|
214
|
+
manifest = writeProgress(manifest, tasks, "team-runner");
|
|
215
|
+
saveRunManifest(manifest);
|
|
216
|
+
const usage = aggregateUsage(tasks);
|
|
217
|
+
const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
218
|
+
kind: "summary",
|
|
219
|
+
relativePath: "summary.md",
|
|
220
|
+
producer: "team-runner",
|
|
221
|
+
content: [
|
|
222
|
+
`# pi-crew run ${manifest.runId}`,
|
|
223
|
+
"",
|
|
224
|
+
`Status: ${manifest.status}`,
|
|
225
|
+
`Team: ${manifest.team}`,
|
|
226
|
+
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
227
|
+
`Goal: ${manifest.goal}`,
|
|
228
|
+
`Usage: ${formatUsage(usage)}`,
|
|
229
|
+
"",
|
|
230
|
+
"## Tasks",
|
|
231
|
+
...tasks.map(formatTaskProgress),
|
|
232
|
+
"",
|
|
233
|
+
"## Policy decisions",
|
|
234
|
+
...(manifest.policyDecisions?.length ? summarizePolicyDecisions(manifest.policyDecisions) : ["- (none)"]),
|
|
235
|
+
"",
|
|
236
|
+
].join("\n"),
|
|
237
|
+
});
|
|
238
|
+
manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, summaryArtifact] };
|
|
239
|
+
saveRunManifest(manifest);
|
|
240
|
+
saveRunTasks(manifest, tasks);
|
|
241
|
+
return { manifest, tasks };
|
|
242
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { CrewUiConfig } from "../config/config.ts";
|
|
3
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
|
+
import { applyAttentionState, resolveCrewControlConfig } from "../runtime/agent-control.ts";
|
|
5
|
+
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
6
|
+
import { loadRunManifestById } from "../state/state-store.ts";
|
|
7
|
+
import { aggregateUsage, formatUsage } from "../state/usage.ts";
|
|
8
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
9
|
+
|
|
10
|
+
const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
11
|
+
type ThemeLike = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
12
|
+
type Done = (value: undefined) => void;
|
|
13
|
+
|
|
14
|
+
function visibleLength(value: string): number { return value.replace(ANSI_PATTERN, "").length; }
|
|
15
|
+
function truncate(value: string, width: number): string {
|
|
16
|
+
if (width <= 0) return "";
|
|
17
|
+
if (visibleLength(value) <= width) return value;
|
|
18
|
+
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
19
|
+
}
|
|
20
|
+
function pad(value: string, width: number): string { return `${value}${" ".repeat(Math.max(0, width - visibleLength(value)))}`; }
|
|
21
|
+
function line(text: string, width: number): string { return `│ ${pad(truncate(text, width - 4), width - 4)} │`; }
|
|
22
|
+
function border(left: string, fill: string, right: string, width: number): string { return `${left}${fill.repeat(Math.max(0, width - 2))}${right}`; }
|
|
23
|
+
function readTasks(path: string): TeamTaskState[] {
|
|
24
|
+
try { const parsed = JSON.parse(fs.readFileSync(path, "utf-8")); return Array.isArray(parsed) ? parsed as TeamTaskState[] : []; } catch { return []; }
|
|
25
|
+
}
|
|
26
|
+
function shortUsage(tasks: TeamTaskState[]): string {
|
|
27
|
+
const usage = aggregateUsage(tasks);
|
|
28
|
+
return usage ? formatUsage(usage) : "usage=(none)";
|
|
29
|
+
}
|
|
30
|
+
function glyph(status: string): string {
|
|
31
|
+
if (status === "running") return "⠋";
|
|
32
|
+
if (status === "completed") return "✓";
|
|
33
|
+
if (status === "failed") return "✗";
|
|
34
|
+
if (status === "cancelled" || status === "stopped") return "■";
|
|
35
|
+
return "◦";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class LiveRunSidebar {
|
|
39
|
+
private readonly cwd: string;
|
|
40
|
+
private readonly runId: string;
|
|
41
|
+
private readonly done: Done;
|
|
42
|
+
private readonly theme: ThemeLike;
|
|
43
|
+
private readonly config: CrewUiConfig;
|
|
44
|
+
|
|
45
|
+
constructor(input: { cwd: string; runId: string; done: Done; theme?: unknown; config?: CrewUiConfig }) {
|
|
46
|
+
this.cwd = input.cwd;
|
|
47
|
+
this.runId = input.runId;
|
|
48
|
+
this.done = input.done;
|
|
49
|
+
this.theme = (input.theme ?? {}) as ThemeLike;
|
|
50
|
+
this.config = input.config ?? {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
invalidate(): void {}
|
|
54
|
+
|
|
55
|
+
render(width: number): string[] {
|
|
56
|
+
const fg = this.theme.fg?.bind(this.theme) ?? ((_color: string, text: string) => text);
|
|
57
|
+
const bold = this.theme.bold?.bind(this.theme) ?? ((text: string) => text);
|
|
58
|
+
const w = Math.max(36, width);
|
|
59
|
+
const loaded = loadRunManifestById(this.cwd, this.runId);
|
|
60
|
+
if (!loaded) return [border("╭", "─", "╮", w), line(`${bold("pi-crew live sidebar")} · run not found`, w), border("╰", "─", "╯", w)];
|
|
61
|
+
const tasks = readTasks(loaded.manifest.tasksPath);
|
|
62
|
+
const controlConfig = resolveCrewControlConfig({ ui: this.config });
|
|
63
|
+
const agents = readCrewAgents(loaded.manifest).map((agent) => applyAttentionState(loaded.manifest, agent, controlConfig));
|
|
64
|
+
const active = agents.filter((agent) => agent.status === "running");
|
|
65
|
+
const completed = agents.filter((agent) => agent.status !== "running").slice(-5);
|
|
66
|
+
const waiting = tasks.filter((task) => task.status === "queued");
|
|
67
|
+
const lines: string[] = [
|
|
68
|
+
border("╭", "─", "╮", w),
|
|
69
|
+
line(`${fg("accent", "▐")} ${bold("pi-crew live sidebar")} · right default`, w),
|
|
70
|
+
line(`run ${loaded.manifest.runId.slice(-12)} · ${loaded.manifest.status}`, w),
|
|
71
|
+
line(`${loaded.manifest.team}/${loaded.manifest.workflow ?? "none"} · ${shortUsage(tasks)}`, w),
|
|
72
|
+
border("├", "─", "┤", w),
|
|
73
|
+
line(`Active agents (${active.length})`, w),
|
|
74
|
+
];
|
|
75
|
+
for (const agent of active.slice(0, 8)) {
|
|
76
|
+
const usage = agent.usage ? formatUsage(agent.usage) : agent.progress?.tokens ? `tokens=${agent.progress.tokens}` : "usage=pending";
|
|
77
|
+
lines.push(line(`${glyph(agent.status)} ${agent.taskId} ${agent.role}->${agent.agent}`, w));
|
|
78
|
+
lines.push(line(` ${agent.model ? `model ${agent.model}` : "model pending"}`, w));
|
|
79
|
+
lines.push(line(` ${agent.progress?.currentTool ? `tool ${agent.progress.currentTool} · ` : ""}${agent.toolUses ?? 0} tools · ${usage}`, w));
|
|
80
|
+
}
|
|
81
|
+
if (active.length === 0) lines.push(line("- none", w));
|
|
82
|
+
lines.push(border("├", "─", "┤", w), line(`Waiting tasks (${waiting.length})`, w));
|
|
83
|
+
for (const task of waiting.slice(0, 8)) lines.push(line(`◦ ${task.id} ${waitingReason(task, tasks) ?? "waiting"}`, w));
|
|
84
|
+
if (waiting.length === 0) lines.push(line("- none", w));
|
|
85
|
+
lines.push(border("├", "─", "┤", w), line(`Completed agents (${completed.length})`, w));
|
|
86
|
+
for (const agent of completed) lines.push(line(`${glyph(agent.status)} ${agent.taskId} ${agent.model ? `· ${agent.model}` : ""}${agent.usage ? ` · ${formatUsage(agent.usage)}` : ""}`, w));
|
|
87
|
+
if (completed.length === 0) lines.push(line("- none", w));
|
|
88
|
+
lines.push(border("├", "─", "┤", w), ...formatTaskGraphLines(tasks).slice(0, 6).map((entry) => line(entry, w)), line("q close · /team-dashboard details", w), border("╰", "─", "╯", w));
|
|
89
|
+
return lines.map((entry) => truncate(entry, w));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
handleInput(data: string): void {
|
|
93
|
+
if (data === "q" || data === "\u001b") this.done(undefined);
|
|
94
|
+
}
|
|
95
|
+
}
|