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 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": 52,
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"` opens `/team-dashboard` as a right-side overlay panel instead of a centered modal.
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": 52,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
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
- "showModel": { "type": "boolean", "description": "Show worker model attempts in dashboard agent rows." },
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
  }
@@ -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: "default" | "implementation" | "review" | "fast-fix" | "research";
16
- workflow: "default" | "implementation" | "review" | "fast-fix" | "research";
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 workflow = workflows.find((item) => item.name === workflowName);
263
- if (!workflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
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
- "Agents:",
387
- ...(crewAgents.length ? crewAgents.map((agent) => `- ${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}` : ""}`) : ["- (none)"]),
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)}`,
@@ -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 { recordFromTask, saveCrewAgents } from "./crew-agent-records.ts";
17
- import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
18
- import { runTeamTask } from "./task-runner.ts";
19
-
20
- export interface ExecuteTeamRunInput {
21
- manifest: TeamRunManifest;
22
- tasks: TeamTaskState[];
23
- team: TeamConfig;
24
- workflow: WorkflowConfig;
25
- agents: AgentConfig[];
26
- executeWorkers: boolean;
27
- limits?: CrewLimitsConfig;
28
- runtime?: CrewRuntimeCapabilities;
29
- runtimeConfig?: CrewRuntimeConfig;
30
- parentContext?: string;
31
- parentModel?: unknown;
32
- modelRegistry?: unknown;
33
- modelOverride?: string;
34
- signal?: AbortSignal;
35
- }
36
-
37
- function findReadyTask(tasks: TeamTaskState[]): TeamTaskState | undefined {
38
- return getReadyTasks(tasks, 1)[0];
39
- }
40
-
41
- function findStep(workflow: WorkflowConfig, task: TeamTaskState): WorkflowStep {
42
- const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
43
- if (!step) throw new Error(`Workflow step '${task.stepId}' not found for task '${task.id}'.`);
44
- return step;
45
- }
46
-
47
- function findAgent(agents: AgentConfig[], task: TeamTaskState): AgentConfig {
48
- const agent = agents.find((candidate) => candidate.name === task.agent);
49
- if (!agent) throw new Error(`Agent '${task.agent}' not found for task '${task.id}'.`);
50
- return agent;
51
- }
52
-
53
- function markBlocked(tasks: TeamTaskState[], reason: string): TeamTaskState[] {
54
- 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);
55
- }
56
-
57
- function mergeArtifacts(items: ArtifactDescriptor[]): ArtifactDescriptor[] {
58
- const byPath = new Map<string, ArtifactDescriptor>();
59
- for (const item of items) byPath.set(item.path, item);
60
- return [...byPath.values()];
61
- }
62
-
63
- function mergeTaskUpdates(base: TeamTaskState[], results: Array<{ tasks: TeamTaskState[] }>): TeamTaskState[] {
64
- let merged = base;
65
- for (const result of results) {
66
- for (const updated of result.tasks) {
67
- const current = merged.find((task) => task.id === updated.id);
68
- if (!current) continue;
69
- if (updated.status !== current.status || updated.finishedAt || updated.startedAt || updated.resultArtifact || updated.error) {
70
- merged = merged.map((task) => task.id === updated.id ? updated : task);
71
- }
72
- }
73
- }
74
- return refreshTaskGraphQueues(merged);
75
- }
76
-
77
- function formatTaskProgress(task: TeamTaskState): string {
78
- 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}` : ""}`;
79
- }
80
-
81
- function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string): TeamRunManifest {
82
- const counts = new Map<string, number>();
83
- for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
84
- const queue = taskGraphSnapshot(tasks);
85
- const progress = writeArtifact(manifest.artifactsRoot, {
86
- kind: "progress",
87
- relativePath: "progress.md",
88
- producer,
89
- content: [
90
- `# pi-crew progress ${manifest.runId}`,
91
- "",
92
- `Status: ${manifest.status}`,
93
- `Team: ${manifest.team}`,
94
- `Workflow: ${manifest.workflow ?? "(none)"}`,
95
- `Updated: ${new Date().toISOString()}`,
96
- `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
97
- `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}`,
98
- "",
99
- "## Tasks",
100
- ...tasks.map(formatTaskProgress),
101
- "",
102
- ].join("\n"),
103
- });
104
- return { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)), progress] };
105
- }
106
-
107
- function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?: CrewLimitsConfig): TeamRunManifest {
108
- const branchFreshness = checkBranchFreshness(manifest.cwd);
109
- const branchArtifact = writeArtifact(manifest.artifactsRoot, {
110
- kind: "metadata",
111
- relativePath: "metadata/branch-freshness.json",
112
- producer: "branch-freshness",
113
- content: `${JSON.stringify(branchFreshness, null, 2)}\n`,
114
- });
115
- let decisions: PolicyDecision[] = evaluateCrewPolicy({ manifest, tasks, limits });
116
- if (branchFreshness.status === "stale" || branchFreshness.status === "diverged") {
117
- const branchDecision: PolicyDecision = {
118
- action: "notify",
119
- reason: "branch_stale",
120
- message: branchFreshness.message,
121
- createdAt: new Date().toISOString(),
122
- };
123
- decisions = [...decisions, branchDecision];
124
- appendEvent(manifest.eventsPath, { type: "branch.stale", runId: manifest.runId, message: branchFreshness.message, data: { branchFreshness } });
125
- }
126
- const policyArtifact = writeArtifact(manifest.artifactsRoot, {
127
- kind: "metadata",
128
- relativePath: "policy-decisions.json",
129
- producer: "policy-engine",
130
- content: `${JSON.stringify(decisions, null, 2)}\n`,
131
- });
132
- const recoveryLedger = buildRecoveryLedger(decisions);
133
- const recoveryArtifact = writeArtifact(manifest.artifactsRoot, {
134
- kind: "metadata",
135
- relativePath: "recovery-ledger.json",
136
- producer: "recovery-engine",
137
- content: `${JSON.stringify(recoveryLedger, null, 2)}\n`,
138
- });
139
- 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 } });
140
- 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 } });
141
- 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] };
142
- }
143
-
144
- export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
145
- let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
146
- let tasks = refreshTaskGraphQueues(input.tasks);
147
- manifest = writeProgress(manifest, tasks, "team-runner");
148
- saveRunManifest(manifest);
149
- const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
150
- saveCrewAgents(manifest, tasks.map((task) => recordFromTask(manifest, task, runtimeKind)));
151
-
152
- while (tasks.some((task) => task.status === "queued")) {
153
- if (input.signal?.aborted) {
154
- tasks = tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: "Run cancelled." } : task);
155
- saveRunTasks(manifest, tasks);
156
- manifest = updateRunStatus(manifest, "cancelled", "Run cancelled.");
157
- return { manifest, tasks };
158
- }
159
-
160
- const failed = tasks.find((task) => task.status === "failed");
161
- if (failed) {
162
- tasks = markBlocked(tasks, `Blocked by failed task '${failed.id}'.`);
163
- saveRunTasks(manifest, tasks);
164
- saveCrewAgents(manifest, tasks.map((task) => recordFromTask(manifest, task, runtimeKind)));
165
- manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
166
- return { manifest, tasks };
167
- }
168
-
169
- const maxConcurrent = Math.max(1, input.limits?.maxConcurrentWorkers ?? 1);
170
- const readyBatch = getReadyTasks(tasks, maxConcurrent);
171
- if (readyBatch.length === 0) {
172
- tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
173
- saveRunTasks(manifest, tasks);
174
- saveCrewAgents(manifest, tasks.map((task) => recordFromTask(manifest, task, runtimeKind)));
175
- manifest = updateRunStatus(manifest, "blocked", "No ready queued task.");
176
- return { manifest, tasks };
177
- }
178
-
179
- 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 } });
180
- const results = await Promise.all(readyBatch.map((task) => {
181
- const step = findStep(input.workflow, task);
182
- const agent = findAgent(input.agents, task);
183
- 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 });
184
- }));
185
- manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
186
- tasks = mergeTaskUpdates(tasks, results);
187
- saveRunTasks(manifest, tasks);
188
- saveCrewAgents(manifest, tasks.map((task) => recordFromTask(manifest, task, runtimeKind)));
189
- const completedBatch = readyBatch.map((task) => tasks.find((item) => item.id === task.id) ?? task);
190
- const batchArtifact = writeArtifact(manifest.artifactsRoot, {
191
- kind: "summary",
192
- relativePath: `batches/${readyBatch.map((task) => task.id).join("+")}.md`,
193
- producer: "team-runner",
194
- content: aggregateTaskOutputs(completedBatch),
195
- });
196
- const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: readyBatch, allTasks: tasks });
197
- manifest = { ...manifest, artifacts: mergeArtifacts([...manifest.artifacts, batchArtifact, ...(groupDelivery?.artifact ? [groupDelivery.artifact] : [])]) };
198
- manifest = writeProgress(manifest, tasks, "team-runner");
199
- saveRunManifest(manifest);
200
- }
201
-
202
- const failed = tasks.find((task) => task.status === "failed");
203
- manifest = applyPolicy(manifest, tasks, input.limits);
204
- const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
205
- if (failed) {
206
- manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
207
- } else if (blockingDecision) {
208
- manifest = updateRunStatus(manifest, "blocked", blockingDecision.message);
209
- } else {
210
- manifest = updateRunStatus(manifest, "completed", input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.");
211
- }
212
- manifest = writeProgress(manifest, tasks, "team-runner");
213
- saveRunManifest(manifest);
214
- const usage = aggregateUsage(tasks);
215
- const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
216
- kind: "summary",
217
- relativePath: "summary.md",
218
- producer: "team-runner",
219
- content: [
220
- `# pi-crew run ${manifest.runId}`,
221
- "",
222
- `Status: ${manifest.status}`,
223
- `Team: ${manifest.team}`,
224
- `Workflow: ${manifest.workflow ?? "(none)"}`,
225
- `Goal: ${manifest.goal}`,
226
- `Usage: ${formatUsage(usage)}`,
227
- "",
228
- "## Tasks",
229
- ...tasks.map(formatTaskProgress),
230
- "",
231
- "## Policy decisions",
232
- ...(manifest.policyDecisions?.length ? summarizePolicyDecisions(manifest.policyDecisions) : ["- (none)"]),
233
- "",
234
- ].join("\n"),
235
- });
236
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, summaryArtifact] };
237
- saveRunManifest(manifest);
238
- saveRunTasks(manifest, tasks);
239
- return { manifest, tasks };
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 queued = agents.filter((agent) => agent.status === "queued").length;
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 || active.length}`,
39
- suffix: queued ? `${queued}q` : undefined,
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.