pi-crew 0.1.17 → 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 +10 -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 +29 -2
- package/src/extension/team-recommendation.ts +9 -3
- package/src/extension/team-tool.ts +61 -4
- package/src/runtime/child-pi.ts +7 -3
- package/src/runtime/task-display.ts +38 -0
- 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/teams/parallel-research.team.md +14 -0
- package/workflows/parallel-research.workflow.md +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.18
|
|
4
|
+
|
|
5
|
+
- Added a built-in `parallel-research` team/workflow for map-reduce style source audits with dynamic `Source/pi-*` fanout and parallel explorer shards.
|
|
6
|
+
- Made the live right sidebar the default foreground UI: active foreground runs auto-open a top-right live sidebar when the terminal is wide enough.
|
|
7
|
+
- Added live sidebar sections for active agents, waiting tasks, completed agents, task graph, model, tool, and token/usage details.
|
|
8
|
+
- Stopped materializing queued dependency tasks as child-process agents; status now separates active agents, waiting tasks, and completed agents.
|
|
9
|
+
- Added workflow-aware default concurrency so research/parallel-research can use ready parallel work instead of always running one worker.
|
|
10
|
+
- Dropped user/system prompt messages from child event persistence to avoid prompt/context leakage in agent event logs.
|
|
11
|
+
- Tightened child event compaction with separate assistant/tool input/tool result caps and improved powerbar active/waiting/model/token summaries.
|
|
12
|
+
|
|
3
13
|
## 0.1.17
|
|
4
14
|
|
|
5
15
|
- Fixed terminal/completed workers being incorrectly escalated as stale heartbeat blockers after all tasks completed.
|
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Current highlights:
|
|
|
52
52
|
- run-level and task-level mailbox files with validation/repair support
|
|
53
53
|
- `/team-manager` interactive helper
|
|
54
54
|
- `/team-dashboard` custom TUI overlay with progress preview, action shortcuts, and reload
|
|
55
|
+
- `parallel-research` team/workflow for dynamic `Source/pi-*` fanout and parallel shard exploration
|
|
55
56
|
- package polish: `schema.json`, TypeScript semantic check, strip-types import smoke, cross-platform CI workflow, dry-run package verification
|
|
56
57
|
|
|
57
58
|
## Install
|
|
@@ -171,7 +172,10 @@ Supported config:
|
|
|
171
172
|
"widgetMaxLines": 8,
|
|
172
173
|
"powerbar": true,
|
|
173
174
|
"dashboardPlacement": "right",
|
|
174
|
-
"dashboardWidth":
|
|
175
|
+
"dashboardWidth": 56,
|
|
176
|
+
"dashboardLiveRefreshMs": 1000,
|
|
177
|
+
"autoOpenDashboard": true,
|
|
178
|
+
"autoOpenDashboardForForegroundRuns": true,
|
|
175
179
|
"showModel": true,
|
|
176
180
|
"showTokens": true,
|
|
177
181
|
"showTools": true
|
|
@@ -186,7 +190,9 @@ Safety notes:
|
|
|
186
190
|
UI notes:
|
|
187
191
|
|
|
188
192
|
- `widgetPlacement`/`widgetMaxLines` keep the persistent active-run widget compact.
|
|
189
|
-
- `dashboardPlacement: "right"`
|
|
193
|
+
- `dashboardPlacement: "right"` is the default; foreground runs auto-open a live top-right sidebar when the terminal is wide enough.
|
|
194
|
+
- `autoOpenDashboard`/`autoOpenDashboardForForegroundRuns` control whether the live sidebar opens automatically.
|
|
195
|
+
- `dashboardLiveRefreshMs` controls the live sidebar refresh cadence.
|
|
190
196
|
- `showModel`, `showTokens`, and `showTools` show worker model attempts, token usage, and tool activity in dashboard agent rows.
|
|
191
197
|
|
|
192
198
|
Show config:
|
package/docs/usage.md
CHANGED
|
@@ -34,7 +34,10 @@ Supported fields:
|
|
|
34
34
|
"widgetMaxLines": 8,
|
|
35
35
|
"powerbar": true,
|
|
36
36
|
"dashboardPlacement": "right",
|
|
37
|
-
"dashboardWidth":
|
|
37
|
+
"dashboardWidth": 56,
|
|
38
|
+
"dashboardLiveRefreshMs": 1000,
|
|
39
|
+
"autoOpenDashboard": true,
|
|
40
|
+
"autoOpenDashboardForForegroundRuns": true,
|
|
38
41
|
"showModel": true,
|
|
39
42
|
"showTokens": true,
|
|
40
43
|
"showTools": true
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -87,9 +87,12 @@
|
|
|
87
87
|
"widgetPlacement": { "type": "string", "enum": ["aboveEditor", "belowEditor"] },
|
|
88
88
|
"widgetMaxLines": { "type": "integer", "minimum": 1, "maximum": 50 },
|
|
89
89
|
"powerbar": { "type": "boolean" },
|
|
90
|
-
"dashboardPlacement": { "type": "string", "enum": ["center", "right"], "description": "Place /team-dashboard as a centered overlay or right-side panel." },
|
|
91
|
-
"dashboardWidth": { "type": "integer", "minimum": 32, "maximum": 120 },
|
|
92
|
-
"
|
|
90
|
+
"dashboardPlacement": { "type": "string", "enum": ["center", "right"], "default": "right", "description": "Place /team-dashboard as a centered overlay or right-side panel." },
|
|
91
|
+
"dashboardWidth": { "type": "integer", "minimum": 32, "maximum": 120, "default": 56 },
|
|
92
|
+
"dashboardLiveRefreshMs": { "type": "integer", "minimum": 250, "maximum": 60000, "default": 1000 },
|
|
93
|
+
"autoOpenDashboard": { "type": "boolean", "default": true, "description": "Automatically open the live right sidebar for foreground runs when UI is available." },
|
|
94
|
+
"autoOpenDashboardForForegroundRuns": { "type": "boolean", "default": true },
|
|
95
|
+
"showModel": { "type": "boolean", "default": true, "description": "Show worker model attempts in dashboard agent rows." },
|
|
93
96
|
"showTokens": { "type": "boolean", "description": "Show token usage in dashboard agent rows." },
|
|
94
97
|
"showTools": { "type": "boolean", "description": "Show tool activity in dashboard agent rows." }
|
|
95
98
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -53,6 +53,9 @@ export interface CrewUiConfig {
|
|
|
53
53
|
powerbar?: boolean;
|
|
54
54
|
dashboardPlacement?: "center" | "right";
|
|
55
55
|
dashboardWidth?: number;
|
|
56
|
+
dashboardLiveRefreshMs?: number;
|
|
57
|
+
autoOpenDashboard?: boolean;
|
|
58
|
+
autoOpenDashboardForForegroundRuns?: boolean;
|
|
56
59
|
showModel?: boolean;
|
|
57
60
|
showTokens?: boolean;
|
|
58
61
|
showTools?: boolean;
|
|
@@ -316,6 +319,9 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
|
316
319
|
powerbar: typeof obj.powerbar === "boolean" ? obj.powerbar : undefined,
|
|
317
320
|
dashboardPlacement: obj.dashboardPlacement === "center" || obj.dashboardPlacement === "right" ? obj.dashboardPlacement : undefined,
|
|
318
321
|
dashboardWidth: parsePositiveInteger(obj.dashboardWidth, 120),
|
|
322
|
+
dashboardLiveRefreshMs: parsePositiveInteger(obj.dashboardLiveRefreshMs, 60_000),
|
|
323
|
+
autoOpenDashboard: typeof obj.autoOpenDashboard === "boolean" ? obj.autoOpenDashboard : undefined,
|
|
324
|
+
autoOpenDashboardForForegroundRuns: typeof obj.autoOpenDashboardForForegroundRuns === "boolean" ? obj.autoOpenDashboardForForegroundRuns : undefined,
|
|
319
325
|
showModel: typeof obj.showModel === "boolean" ? obj.showModel : undefined,
|
|
320
326
|
showTokens: typeof obj.showTokens === "boolean" ? obj.showTokens : undefined,
|
|
321
327
|
showTools: typeof obj.showTools === "boolean" ? obj.showTools : undefined,
|
|
@@ -9,6 +9,7 @@ import { handleTeamManagerCommand } from "./team-manager-command.ts";
|
|
|
9
9
|
import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
|
|
10
10
|
import { listRecentRuns } from "./run-index.ts";
|
|
11
11
|
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
12
|
+
import { LiveRunSidebar } from "../ui/live-run-sidebar.ts";
|
|
12
13
|
import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
|
|
13
14
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
14
15
|
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
@@ -112,6 +113,29 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
112
113
|
let cleanedUp = false;
|
|
113
114
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
114
115
|
const foregroundControllers = new Set<AbortController>();
|
|
116
|
+
let liveSidebarRunId: string | undefined;
|
|
117
|
+
let liveSidebarTimer: ReturnType<typeof setInterval> | undefined;
|
|
118
|
+
const requestRender = (ctx: ExtensionContext): void => (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
119
|
+
const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
|
|
120
|
+
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
121
|
+
const autoOpen = uiConfig?.autoOpenDashboard ?? true;
|
|
122
|
+
const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns ?? true;
|
|
123
|
+
if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? "right") !== "right") return;
|
|
124
|
+
if (liveSidebarRunId === runId) return;
|
|
125
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
126
|
+
liveSidebarRunId = runId;
|
|
127
|
+
const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
|
|
128
|
+
liveSidebarTimer = setInterval(() => requestRender(ctx), uiConfig?.dashboardLiveRefreshMs ?? 1000);
|
|
129
|
+
liveSidebarTimer.unref?.();
|
|
130
|
+
void ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new LiveRunSidebar({ cwd: ctx.cwd, runId, done, theme, config: uiConfig }), {
|
|
131
|
+
overlay: true,
|
|
132
|
+
overlayOptions: { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 }, visible: (termWidth: number) => termWidth >= 100 },
|
|
133
|
+
}).finally(() => {
|
|
134
|
+
if (liveSidebarRunId === runId) liveSidebarRunId = undefined;
|
|
135
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
136
|
+
liveSidebarTimer = undefined;
|
|
137
|
+
});
|
|
138
|
+
};
|
|
115
139
|
const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>): void => {
|
|
116
140
|
const controller = new AbortController();
|
|
117
141
|
foregroundControllers.add(controller);
|
|
@@ -141,6 +165,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
141
165
|
terminateActiveChildPiProcesses();
|
|
142
166
|
stopAsyncRunNotifier(notifierState);
|
|
143
167
|
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
168
|
+
if (liveSidebarTimer) clearInterval(liveSidebarTimer);
|
|
169
|
+
liveSidebarTimer = undefined;
|
|
170
|
+
liveSidebarRunId = undefined;
|
|
144
171
|
clearPiCrewPowerbar(pi.events);
|
|
145
172
|
rpcHandle?.unsubscribe();
|
|
146
173
|
rpcHandle = undefined;
|
|
@@ -182,7 +209,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
182
209
|
const abort = (): void => controller.abort();
|
|
183
210
|
signal?.addEventListener("abort", abort, { once: true });
|
|
184
211
|
try {
|
|
185
|
-
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner) });
|
|
212
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
|
|
186
213
|
const config = loadConfig(ctx.cwd).config.ui;
|
|
187
214
|
updateCrewWidget(ctx, widgetState, config);
|
|
188
215
|
updatePiCrewPowerbar(pi.events, ctx.cwd, config);
|
|
@@ -207,7 +234,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
207
234
|
pi.registerCommand("team-run", {
|
|
208
235
|
description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
|
|
209
236
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
210
|
-
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner) });
|
|
237
|
+
const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner), onRunStarted: (runId) => openLiveSidebar(ctx as ExtensionContext, runId) });
|
|
211
238
|
await notifyCommandResult(ctx, commandText(result));
|
|
212
239
|
},
|
|
213
240
|
});
|
|
@@ -12,8 +12,8 @@ export interface RecommendedSubtask {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface TeamRecommendation {
|
|
15
|
-
team:
|
|
16
|
-
workflow:
|
|
15
|
+
team: string;
|
|
16
|
+
workflow: string;
|
|
17
17
|
action: "plan" | "run";
|
|
18
18
|
async: boolean;
|
|
19
19
|
workspaceMode: "single" | "worktree";
|
|
@@ -23,7 +23,8 @@ export interface TeamRecommendation {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const REVIEW_TERMS = ["review", "audit", "security", "vulnerability", "diff", "pr", "pull request"];
|
|
26
|
-
const RESEARCH_TERMS = ["research", "investigate", "compare", "analyze", "document", "docs", "explain", "architecture"];
|
|
26
|
+
const RESEARCH_TERMS = ["research", "investigate", "compare", "analyze", "document", "docs", "explain", "architecture", "đọc sâu", "source", "projects"];
|
|
27
|
+
const PARALLEL_RESEARCH_RE = /(?:đọc sâu|deep read|deep research|source audit|multiple projects|các project|pi-\*|source\/|@source)/i;
|
|
27
28
|
const FAST_FIX_TERMS = ["quick fix", "fast-fix", "small bug", "typo", "one-line", "minor", "lint"];
|
|
28
29
|
const IMPLEMENTATION_TERMS = ["implement", "refactor", "migrate", "feature", "tests", "test", "integration", "upgrade", "build", "create", "add"];
|
|
29
30
|
const RISKY_TERMS = ["migration", "refactor", "large", "multiple", "parallel", "concurrent", "risky", "critical"];
|
|
@@ -122,6 +123,11 @@ export function recommendTeam(goal: string, config: PiTeamsAutonomousConfig = {}
|
|
|
122
123
|
workflow = "review";
|
|
123
124
|
confidence = "high";
|
|
124
125
|
reasons.push(`Review/audit terms detected: ${reviewMatches.join(", ") || "explicit review intent"}.`);
|
|
126
|
+
} else if (PARALLEL_RESEARCH_RE.test(goal) || (researchMatches.length >= 2 && (normalized.includes("multiple") || normalized.includes("source") || normalized.includes("project") || normalized.includes("pi-")))) {
|
|
127
|
+
team = "parallel-research";
|
|
128
|
+
workflow = "parallel-research";
|
|
129
|
+
confidence = "high";
|
|
130
|
+
reasons.push("Deep/multi-source research detected; use parallel shard exploration.");
|
|
125
131
|
} else if (intents.includes("research") || (researchMatches.length > 0 && implementationMatches.length === 0)) {
|
|
126
132
|
team = "research";
|
|
127
133
|
workflow = "research";
|
|
@@ -5,6 +5,7 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
|
5
5
|
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
6
6
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
7
7
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
8
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
8
9
|
import { effectiveAutonomousConfig, loadConfig, updateAutonomousConfig, updateConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../config/config.ts";
|
|
9
10
|
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
10
11
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
@@ -44,6 +45,7 @@ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "..
|
|
|
44
45
|
import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../runtime/live-agent-manager.ts";
|
|
45
46
|
import { appendLiveAgentControlRequest } from "../runtime/live-agent-control.ts";
|
|
46
47
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
48
|
+
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
47
49
|
|
|
48
50
|
export interface TeamToolDetails {
|
|
49
51
|
action: string;
|
|
@@ -58,6 +60,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
|
|
|
58
60
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
61
|
signal?: AbortSignal;
|
|
60
62
|
startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>) => void;
|
|
63
|
+
onRunStarted?: (runId: string) => void;
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
@@ -168,6 +171,48 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
|
|
|
168
171
|
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
function sourcePiProjects(cwd: string): string[] {
|
|
175
|
+
const sourceDir = path.join(cwd, "Source");
|
|
176
|
+
try {
|
|
177
|
+
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
178
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
179
|
+
.map((entry) => `Source/${entry.name}`)
|
|
180
|
+
.sort();
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function chunkProjects(projects: string[], target = 4): string[][] {
|
|
187
|
+
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
188
|
+
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
189
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
193
|
+
if (workflow.name !== "parallel-research") return workflow;
|
|
194
|
+
const projects = sourcePiProjects(cwd);
|
|
195
|
+
if (projects.length === 0) return workflow;
|
|
196
|
+
const chunks = chunkProjects(projects, Math.min(6, Math.max(4, Math.ceil(projects.length / 4))));
|
|
197
|
+
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
198
|
+
id: `explore-shard-${index + 1}`,
|
|
199
|
+
role: "explorer",
|
|
200
|
+
dependsOn: ["discover"],
|
|
201
|
+
parallelGroup: "explore",
|
|
202
|
+
reads: paths,
|
|
203
|
+
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
204
|
+
}));
|
|
205
|
+
return {
|
|
206
|
+
...workflow,
|
|
207
|
+
steps: [
|
|
208
|
+
{ id: "discover", role: "explorer", task: `Discover and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}` },
|
|
209
|
+
...exploreSteps,
|
|
210
|
+
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations." },
|
|
211
|
+
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
|
|
172
217
|
const patch = configPatchFromConfig(rawOverride);
|
|
173
218
|
return {
|
|
@@ -259,8 +304,9 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
259
304
|
const team = teams.find((item) => item.name === teamName);
|
|
260
305
|
if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
|
|
261
306
|
const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
307
|
+
const baseWorkflow = workflows.find((item) => item.name === workflowName);
|
|
308
|
+
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
309
|
+
const workflow = expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
|
|
264
310
|
|
|
265
311
|
const validationErrors = validateWorkflowForTeam(workflow, team);
|
|
266
312
|
if (validationErrors.length > 0) {
|
|
@@ -309,6 +355,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
309
355
|
const executeWorkers = runtime.kind === "child-process";
|
|
310
356
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
311
357
|
if (executeWorkers && ctx.startForegroundRun) {
|
|
358
|
+
ctx.onRunStarted?.(updatedManifest.runId);
|
|
312
359
|
ctx.startForegroundRun(async (signal) => {
|
|
313
360
|
await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal });
|
|
314
361
|
});
|
|
@@ -368,6 +415,10 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
368
415
|
const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
|
|
369
416
|
const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
|
|
370
417
|
const totalUsage = aggregateUsage(tasks);
|
|
418
|
+
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
419
|
+
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
420
|
+
const waitingTasks = tasks.filter((task) => task.status === "queued");
|
|
421
|
+
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
371
422
|
const lines = [
|
|
372
423
|
`Run: ${manifest.runId}`,
|
|
373
424
|
`Team: ${manifest.team}`,
|
|
@@ -380,11 +431,17 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
380
431
|
`State: ${manifest.stateRoot}`,
|
|
381
432
|
`Artifacts: ${manifest.artifactsRoot}`,
|
|
382
433
|
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|
|
434
|
+
"Task graph:",
|
|
435
|
+
...formatTaskGraphLines(tasks),
|
|
383
436
|
"Tasks:",
|
|
384
437
|
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
385
438
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
386
|
-
"
|
|
387
|
-
...(
|
|
439
|
+
"Active agents:",
|
|
440
|
+
...(activeAgents.length ? activeAgents.map(agentLine) : ["- (none)"]),
|
|
441
|
+
"Waiting tasks:",
|
|
442
|
+
...(waitingTasks.length ? waitingTasks.map((task) => `- ${task.id} [queued] ${task.role} -> ${task.agent} ${waitingReason(task, tasks) ?? "waiting"}`) : ["- (none)"]),
|
|
443
|
+
"Completed agents:",
|
|
444
|
+
...(completedAgents.length ? completedAgents.map(agentLine) : ["- (none)"]),
|
|
388
445
|
"Policy decisions:",
|
|
389
446
|
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
390
447
|
`Total usage: ${formatUsage(totalUsage)}`,
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -9,6 +9,9 @@ const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
|
9
9
|
const FINAL_DRAIN_MS = 5000;
|
|
10
10
|
const HARD_KILL_MS = 3000;
|
|
11
11
|
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
12
|
+
const MAX_ASSISTANT_TEXT_CHARS = 8192;
|
|
13
|
+
const MAX_TOOL_RESULT_CHARS = 1024;
|
|
14
|
+
const MAX_TOOL_INPUT_CHARS = 2048;
|
|
12
15
|
const MAX_COMPACT_CONTENT_CHARS = 4096;
|
|
13
16
|
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
14
17
|
|
|
@@ -87,9 +90,9 @@ function compactValue(value: unknown): unknown {
|
|
|
87
90
|
function compactContentPart(part: unknown): unknown | undefined {
|
|
88
91
|
const record = asRecord(part);
|
|
89
92
|
if (!record) return undefined;
|
|
90
|
-
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text) : "" };
|
|
91
|
-
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(record.input) };
|
|
92
|
-
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(record.content) };
|
|
93
|
+
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text, MAX_ASSISTANT_TEXT_CHARS) : "" };
|
|
94
|
+
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(typeof record.input === "string" ? compactString(record.input, MAX_TOOL_INPUT_CHARS) : record.input) };
|
|
95
|
+
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(typeof record.content === "string" ? compactString(record.content, MAX_TOOL_RESULT_CHARS) : record.content) };
|
|
93
96
|
return undefined;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -102,6 +105,7 @@ function compactChildPiEvent(event: unknown): unknown | undefined {
|
|
|
102
105
|
}
|
|
103
106
|
if (record.type === "tool_result_end" || record.type === "message_end" || record.type === "message") {
|
|
104
107
|
const message = asRecord(record.message);
|
|
108
|
+
if (message?.role === "user" || message?.role === "system") return undefined;
|
|
105
109
|
const content = Array.isArray(message?.content) ? message.content.map(compactContentPart).filter((part) => part !== undefined) : undefined;
|
|
106
110
|
return {
|
|
107
111
|
type: record.type,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
2
|
+
import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
3
|
+
import { recordFromTask } from "./crew-agent-records.ts";
|
|
4
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
5
|
+
|
|
6
|
+
export function shouldMaterializeAgent(task: TeamTaskState): boolean {
|
|
7
|
+
return task.status !== "queued" && task.status !== "skipped";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
|
|
11
|
+
return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
15
|
+
const map = new Map<string, TeamTaskState>();
|
|
16
|
+
for (const task of tasks) {
|
|
17
|
+
map.set(task.id, task);
|
|
18
|
+
if (task.stepId) map.set(task.stepId, task);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
|
|
24
|
+
if (task.status !== "queued") return undefined;
|
|
25
|
+
const byId = taskById(tasks);
|
|
26
|
+
const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
|
|
27
|
+
if (waiting.length === 0) return "ready";
|
|
28
|
+
return `waiting for ${waiting.join(", ")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
|
|
32
|
+
if (tasks.length === 0) return ["- (none)"];
|
|
33
|
+
return tasks.map((task) => {
|
|
34
|
+
const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
|
|
35
|
+
const wait = waitingReason(task, tasks);
|
|
36
|
+
return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { CrewUiConfig } from "../config/config.ts";
|
|
2
2
|
import { listRecentRuns } from "../extension/run-index.ts";
|
|
3
|
+
import * as fs from "node:fs";
|
|
3
4
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
|
+
import type { TeamTaskState } from "../state/types.ts";
|
|
6
|
+
import { aggregateUsage } from "../state/usage.ts";
|
|
4
7
|
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
5
8
|
|
|
6
9
|
type EventBus = { emit?: (event: string, data: unknown) => void } | undefined;
|
|
@@ -9,6 +12,14 @@ function safeEmit(events: EventBus, event: string, data: unknown): void {
|
|
|
9
12
|
try { events?.emit?.(event, data); } catch {}
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
function readTasks(tasksPath: string): TeamTaskState[] {
|
|
16
|
+
try { const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8")); return Array.isArray(parsed) ? parsed as TeamTaskState[] : []; } catch { return []; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function compactTokens(total: number): string {
|
|
20
|
+
return total >= 1000 ? `${Math.round(total / 1000)}k` : `${total}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUiConfig): void {
|
|
13
24
|
if (config?.powerbar === false) return;
|
|
14
25
|
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-active", label: "pi-crew active agents" });
|
|
@@ -28,22 +39,26 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
|
|
|
28
39
|
return;
|
|
29
40
|
}
|
|
30
41
|
const agents = active.flatMap((item) => item.agents);
|
|
42
|
+
const tasks = active.flatMap((item) => readTasks(item.run.tasksPath));
|
|
31
43
|
const running = agents.filter((agent) => agent.status === "running").length;
|
|
32
|
-
const
|
|
44
|
+
const waiting = tasks.filter((task) => task.status === "queued").length;
|
|
33
45
|
const completed = agents.filter((agent) => agent.status === "completed").length;
|
|
34
|
-
const total = Math.max(1, agents.length);
|
|
46
|
+
const total = Math.max(1, agents.length + waiting);
|
|
47
|
+
const usage = aggregateUsage(tasks);
|
|
48
|
+
const tokenTotal = usage ? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) : 0;
|
|
49
|
+
const model = agents.find((agent) => agent.model)?.model?.split("/").at(-1);
|
|
35
50
|
safeEmit(events, "powerbar:update", {
|
|
36
51
|
id: "pi-crew-active",
|
|
37
52
|
icon: "⚙",
|
|
38
|
-
text: `crew ${running
|
|
39
|
-
suffix:
|
|
53
|
+
text: `crew ${running}a/${waiting}w`,
|
|
54
|
+
suffix: [model, tokenTotal ? compactTokens(tokenTotal) : undefined].filter(Boolean).join(" · ") || undefined,
|
|
40
55
|
color: running ? "accent" : "warning",
|
|
41
56
|
});
|
|
42
57
|
safeEmit(events, "powerbar:update", {
|
|
43
58
|
id: "pi-crew-progress",
|
|
44
59
|
text: active[0]?.run.team ?? "crew",
|
|
45
60
|
bar: Math.round((completed / total) * 100),
|
|
46
|
-
suffix: `${completed}/${total}`,
|
|
61
|
+
suffix: `${completed}/${total}${tokenTotal ? ` · ${compactTokens(tokenTotal)}` : ""}`,
|
|
47
62
|
color: completed === total ? "success" : "accent",
|
|
48
63
|
barSegments: 8,
|
|
49
64
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: parallel-research
|
|
3
|
+
description: Parallel research team for multi-project/source audits
|
|
4
|
+
workspaceMode: single
|
|
5
|
+
defaultWorkflow: parallel-research
|
|
6
|
+
maxConcurrency: 4
|
|
7
|
+
triggers: đọc sâu, deep read, deep research, source audit, multiple projects, parallel research, pi-*
|
|
8
|
+
category: research
|
|
9
|
+
cost: cheap
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
- explorer: agent=explorer gather source facts in parallel shards
|
|
13
|
+
- analyst: agent=analyst synthesize shard findings
|
|
14
|
+
- writer: agent=writer produce final notes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: parallel-research
|
|
3
|
+
description: Parallel research with shard exploration and synthesis
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## discover
|
|
7
|
+
role: explorer
|
|
8
|
+
|
|
9
|
+
Discover the relevant files/projects for: {goal}. Return a shard plan with paths grouped by topic. Do not deeply read every file yet; focus on routing the work.
|
|
10
|
+
|
|
11
|
+
## explore-core
|
|
12
|
+
role: explorer
|
|
13
|
+
dependsOn: discover
|
|
14
|
+
parallelGroup: explore
|
|
15
|
+
|
|
16
|
+
Explore the core/runtime shard from the discover output. Focus on architecture, package config, docs, and reusable patterns for: {goal}
|
|
17
|
+
|
|
18
|
+
## explore-ui
|
|
19
|
+
role: explorer
|
|
20
|
+
dependsOn: discover
|
|
21
|
+
parallelGroup: explore
|
|
22
|
+
|
|
23
|
+
Explore the UI/TUI/extension-interface shard from the discover output. Focus on widgets, overlays, commands, status bars, package config, docs, and reusable patterns for: {goal}
|
|
24
|
+
|
|
25
|
+
## explore-runtime
|
|
26
|
+
role: explorer
|
|
27
|
+
dependsOn: discover
|
|
28
|
+
parallelGroup: explore
|
|
29
|
+
|
|
30
|
+
Explore the worker/runtime/subagent/runtime-control shard from the discover output. Focus on process/session/runtime orchestration, event streams, logs, package config, docs, and reusable patterns for: {goal}
|
|
31
|
+
|
|
32
|
+
## explore-extensions
|
|
33
|
+
role: explorer
|
|
34
|
+
dependsOn: discover
|
|
35
|
+
parallelGroup: explore
|
|
36
|
+
|
|
37
|
+
Explore the extension bundle/small-package shard from the discover output. Focus on package config, extension registration, commands/tools, docs, and reusable patterns for: {goal}
|
|
38
|
+
|
|
39
|
+
## synthesize
|
|
40
|
+
role: analyst
|
|
41
|
+
dependsOn: explore-core, explore-ui, explore-runtime, explore-extensions
|
|
42
|
+
|
|
43
|
+
Synthesize all shard findings. Identify common patterns, gaps, and concrete recommendations.
|
|
44
|
+
|
|
45
|
+
## write
|
|
46
|
+
role: writer
|
|
47
|
+
dependsOn: synthesize
|
|
48
|
+
output: research-summary.md
|
|
49
|
+
|
|
50
|
+
Write a concise final summary with evidence, risks, and actionable next steps.
|