pi-crew 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
13
+ ## 0.1.17
14
+
15
+ - Fixed terminal/completed workers being incorrectly escalated as stale heartbeat blockers after all tasks completed.
16
+ - Cleaned child-process result extraction so result artifacts prefer final assistant output and no longer include worker prompt/context.
17
+ - Made `/team-dashboard` visibly render as a top-right sidebar by default with explicit right-sidebar title text.
18
+ - Added per-subagent model and usage fields to agent records, status output, and dashboard fallbacks so model/token totals stay visible while and after workers run.
19
+
3
20
  ## 0.1.16
4
21
 
5
22
  - Added right-side `/team-dashboard` placement with model, token, and tool detail rows for subagents.
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.16",
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,
@@ -7,8 +7,9 @@ import { notifyActiveRuns } from "./session-summary.ts";
7
7
  import { piTeamsHelp } from "./help.ts";
8
8
  import { handleTeamManagerCommand } from "./team-manager-command.ts";
9
9
  import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
10
- import { listRuns } from "./run-index.ts";
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
  });
@@ -381,13 +408,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
381
408
  description: "Open a pi-crew run dashboard overlay",
382
409
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
383
410
  for (;;) {
384
- const runs = listRuns(ctx.cwd).slice(0, 50);
411
+ const runs = listRecentRuns(ctx.cwd, 50);
385
412
  const uiConfig = loadConfig(ctx.cwd).config.ui;
386
413
  const rightPanel = uiConfig?.dashboardPlacement !== "center";
387
- const width = rightPanel ? Math.min(120, Math.max(32, uiConfig?.dashboardWidth ?? 52)) : "90%";
388
- const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
414
+ const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
415
+ const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
389
416
  overlay: true,
390
- overlayOptions: { width, minWidth: rightPanel ? 32 : undefined, maxHeight: "90%", anchor: rightPanel ? "right-center" : "center", offsetX: rightPanel ? -1 : 0, margin: rightPanel ? { top: 1, right: 1, bottom: 1, left: 0 } : 2 },
417
+ overlayOptions: rightPanel
418
+ ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } }
419
+ : { width, maxHeight: "90%", anchor: "center", margin: 2 },
391
420
  });
392
421
  if (!selection) return;
393
422
  if (selection.action === "reload") continue;
@@ -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.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${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,10 @@ 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;
15
+ const MAX_COMPACT_CONTENT_CHARS = 4096;
12
16
  const activeChildProcesses = new Map<number, ChildProcess>();
13
17
 
14
18
  function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
@@ -68,12 +72,27 @@ function appendTranscript(input: ChildPiRunInput, line: string): void {
68
72
  fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
69
73
  }
70
74
 
75
+ function compactString(value: string, maxChars = MAX_COMPACT_CONTENT_CHARS): string {
76
+ if (value.length <= maxChars) return value;
77
+ return `${value.slice(0, maxChars)}\n[pi-crew compacted ${value.length - maxChars} chars]`;
78
+ }
79
+
80
+ function compactValue(value: unknown): unknown {
81
+ if (typeof value === "string") return compactString(value);
82
+ if (Array.isArray(value)) return value.slice(0, 20).map(compactValue);
83
+ const record = asRecord(value);
84
+ if (!record) return value;
85
+ const compacted: Record<string, unknown> = {};
86
+ for (const [key, entry] of Object.entries(record).slice(0, 20)) compacted[key] = compactValue(entry);
87
+ return compacted;
88
+ }
89
+
71
90
  function compactContentPart(part: unknown): unknown | undefined {
72
91
  const record = asRecord(part);
73
92
  if (!record) return undefined;
74
- if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? record.text : "" };
75
- if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: record.input };
76
- if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: 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) };
77
96
  return undefined;
78
97
  }
79
98
 
@@ -86,6 +105,7 @@ function compactChildPiEvent(event: unknown): unknown | undefined {
86
105
  }
87
106
  if (record.type === "tool_result_end" || record.type === "message_end" || record.type === "message") {
88
107
  const message = asRecord(record.message);
108
+ if (message?.role === "user" || message?.role === "system") return undefined;
89
109
  const content = Array.isArray(message?.content) ? message.content.map(compactContentPart).filter((part) => part !== undefined) : undefined;
90
110
  return {
91
111
  type: record.type,
@@ -106,7 +126,9 @@ function displayTextFromCompactEvent(event: unknown): string | undefined {
106
126
  if (record.type === "tool_execution_start") {
107
127
  return typeof record.toolName === "string" ? `tool: ${record.toolName}` : "tool started";
108
128
  }
129
+ if (record.type !== "message" && record.type !== "message_end") return undefined;
109
130
  const message = asRecord(record.message);
131
+ if (message?.role !== undefined && message.role !== "assistant") return undefined;
110
132
  const content = Array.isArray(message?.content) ? message.content : [];
111
133
  const text = content.flatMap((part) => {
112
134
  const item = asRecord(part);
@@ -124,6 +124,12 @@ export function emptyCrewAgentProgress(): CrewAgentProgress {
124
124
  return { recentTools: [], recentOutput: [], toolCount: 0 };
125
125
  }
126
126
 
127
+ function modelFromTask(task: TeamTaskState): string | undefined {
128
+ const attempts = task.modelAttempts;
129
+ if (!attempts?.length) return undefined;
130
+ return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
131
+ }
132
+
127
133
  export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, runtime: CrewRuntimeKind): CrewAgentRecord {
128
134
  return {
129
135
  id: `${manifest.runId}:${task.id}`,
@@ -142,6 +148,8 @@ export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, r
142
148
  outputPath: agentOutputPath(manifest, task.id),
143
149
  toolUses: task.agentProgress?.toolCount,
144
150
  jsonEvents: task.jsonEvents,
151
+ model: modelFromTask(task),
152
+ usage: task.usage,
145
153
  progress: task.agentProgress,
146
154
  error: task.error,
147
155
  };
@@ -1,4 +1,5 @@
1
1
  import type { TeamTaskStatus } from "../state/contracts.ts";
2
+ import type { UsageState } from "../state/types.ts";
2
3
 
3
4
  export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
4
5
  export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
@@ -41,6 +42,8 @@ export interface CrewAgentRecord {
41
42
  outputPath?: string;
42
43
  toolUses?: number;
43
44
  jsonEvents?: number;
45
+ model?: string;
46
+ usage?: UsageState;
44
47
  progress?: CrewAgentProgress;
45
48
  error?: string;
46
49
  }
@@ -72,13 +72,14 @@ function textFromContent(content: unknown): string[] {
72
72
  function extractText(value: unknown): string[] {
73
73
  const obj = asRecord(value);
74
74
  if (!obj) return [];
75
+ const message = asRecord(obj.message);
76
+ if (message?.role !== undefined && message.role !== "assistant") return [];
75
77
  const text: string[] = [];
76
78
  if (typeof obj.text === "string") text.push(obj.text);
77
79
  if (typeof obj.output === "string") text.push(obj.output);
78
80
  if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
79
81
  if (typeof obj.final_output === "string") text.push(obj.final_output);
80
- text.push(...textFromContent(obj.content));
81
- const message = asRecord(obj.message);
82
+ if (!message) text.push(...textFromContent(obj.content));
82
83
  if (message) text.push(...textFromContent(message.content));
83
84
  return text.filter((entry) => entry.trim().length > 0);
84
85
  }
@@ -56,7 +56,7 @@ export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
56
56
  const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
57
57
  decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
58
58
  }
59
- if (task.heartbeat && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
59
+ if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
60
60
  decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
61
61
  }
62
62
  if (task.taskPacket?.verification) {
@@ -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
+ }
@@ -190,6 +190,17 @@ function shouldFlushProgressEvent(event: unknown): boolean {
190
190
  return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
191
191
  }
192
192
 
193
+ function cleanResultText(text: string | undefined): string | undefined {
194
+ const trimmed = text?.trim();
195
+ if (!trimmed) return undefined;
196
+ const doneIndex = trimmed.lastIndexOf("\nDONE\n");
197
+ if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
198
+ if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
199
+ const fencedPromptIndex = trimmed.lastIndexOf("</file>");
200
+ if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
201
+ return trimmed;
202
+ }
203
+
193
204
  function progressEventSummary(task: TeamTaskState, event: unknown): Record<string, unknown> {
194
205
  const type = asRecord(event)?.type;
195
206
  return {
@@ -309,6 +320,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
309
320
  for (let i = 0; i < attemptModels.length; i++) {
310
321
  const model = attemptModels[i];
311
322
  const attemptStartedAt = new Date();
323
+ const pendingAttempt: ModelAttemptSummary = { model: model ?? "default", success: false };
324
+ task = { ...task, modelAttempts: [...modelAttempts, pendingAttempt] };
325
+ tasks = updateTask(tasks, task);
326
+ upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
312
327
  const childResult = await runChildPi({
313
328
  cwd: task.cwd,
314
329
  task: prompt,
@@ -329,11 +344,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
329
344
  exitCode = childResult.exitCode;
330
345
  finalStdout = childResult.stdout;
331
346
  finalStderr = childResult.stderr;
332
- parsedOutput = parsePiJsonOutput(childResult.stdout);
347
+ parsedOutput = parsePiJsonOutput(fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : childResult.stdout);
333
348
  error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
334
349
  persistChildProgress({ type: "attempt_finished" }, true);
335
350
  const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
336
351
  modelAttempts.push(attempt);
352
+ task = { ...task, modelAttempts: [...modelAttempts] };
353
+ tasks = updateTask(tasks, task);
337
354
  logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
338
355
  if (!error) break;
339
356
  const nextModel = attemptModels[i + 1];
@@ -343,7 +360,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
343
360
  resultArtifact = writeArtifact(manifest.artifactsRoot, {
344
361
  kind: "result",
345
362
  relativePath: `results/${task.id}.txt`,
346
- content: parsedOutput?.finalText || finalStdout || finalStderr || "(no output)",
363
+ content: cleanResultText(parsedOutput?.finalText) ?? cleanResultText(finalStdout) ?? cleanResultText(finalStderr) ?? "(no output)",
347
364
  producer: task.id,
348
365
  });
349
366
  logArtifact = writeArtifact(manifest.artifactsRoot, {