pi-crew 0.1.18 → 0.1.20

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.20
4
+
5
+ - Reworked the implementation workflow into an adaptive planner-led orchestration flow that decides the number, roles, and phases of subagents from the task instead of using a fixed fanout template.
6
+ - Added dynamic adaptive task injection, persisted adaptive task metadata, and resume reconstruction for planner-selected subagent steps.
7
+ - Block implementation runs when the planner does not produce a valid adaptive plan, including missing/unreadable planner artifacts and malformed/oversized plans.
8
+ - Added tests for adaptive plan parsing, dynamic batch fanout, invalid-plan blocking, writer-role support, and adaptive resume recovery.
9
+ - Hardened subagent/runtime fixes from post-0.1.19 review: env-isolated depth tests, foreground failure status updates, generic tool conflict aliases, and max_turns propagation.
10
+
11
+ ## 0.1.19
12
+
13
+ - Added Claude-style `Agent`, `get_subagent_result`, and `steer_subagent` tools backed by pi-crew's durable worker runtime, plus conflict-safe `crew_agent`, `crew_agent_result`, and `crew_agent_steer` aliases.
14
+ - Added a durable subagent manager with background queueing, completion notifications, result joins, session-bound cleanup, and direct single-agent runs via `team run agent=...`.
15
+ - Disabled risky auto-opening of the right sidebar by default, added foreground completion notifications, and reduced duplicate widget/sidebar UI.
16
+ - Added progress coalescing and workflow concurrency helpers to keep foreground sessions responsive during busy worker output.
17
+ - Fixed live-session runs being classified as scaffold when workers are enabled and hardened session switch/shutdown cleanup for foreground child processes.
18
+
3
19
  ## 0.1.18
4
20
 
5
21
  - Added a built-in `parallel-research` team/workflow for map-reduce style source audits with dynamic `Source/pi-*` fanout and parallel explorer shards.
package/README.md CHANGED
@@ -174,7 +174,7 @@ Supported config:
174
174
  "dashboardPlacement": "right",
175
175
  "dashboardWidth": 56,
176
176
  "dashboardLiveRefreshMs": 1000,
177
- "autoOpenDashboard": true,
177
+ "autoOpenDashboard": false,
178
178
  "autoOpenDashboardForForegroundRuns": true,
179
179
  "showModel": true,
180
180
  "showTokens": true,
@@ -190,7 +190,7 @@ Safety notes:
190
190
  UI notes:
191
191
 
192
192
  - `widgetPlacement`/`widgetMaxLines` keep the persistent active-run widget compact.
193
- - `dashboardPlacement: "right"` is the default; foreground runs auto-open a live top-right sidebar when the terminal is wide enough.
193
+ - `dashboardPlacement: "right"` is the default for `/team-dashboard`; automatic overlay opening is opt-in because Pi custom overlays can be modal/focus-capturing in some terminals.
194
194
  - `autoOpenDashboard`/`autoOpenDashboardForForegroundRuns` control whether the live sidebar opens automatically.
195
195
  - `dashboardLiveRefreshMs` controls the live sidebar refresh cadence.
196
196
  - `showModel`, `showTokens`, and `showTools` show worker model attempts, token usage, and tool activity in dashboard agent rows.
package/docs/usage.md CHANGED
@@ -36,7 +36,7 @@ Supported fields:
36
36
  "dashboardPlacement": "right",
37
37
  "dashboardWidth": 56,
38
38
  "dashboardLiveRefreshMs": 1000,
39
- "autoOpenDashboard": true,
39
+ "autoOpenDashboard": false,
40
40
  "autoOpenDashboardForForegroundRuns": true,
41
41
  "showModel": true,
42
42
  "showTokens": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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
@@ -90,7 +90,7 @@
90
90
  "dashboardPlacement": { "type": "string", "enum": ["center", "right"], "default": "right", "description": "Place /team-dashboard as a centered overlay or right-side panel." },
91
91
  "dashboardWidth": { "type": "integer", "minimum": 32, "maximum": 120, "default": 56 },
92
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." },
93
+ "autoOpenDashboard": { "type": "boolean", "default": false, "description": "Opt in to automatically opening the live right sidebar for foreground runs when UI is available. Disabled by default because Pi overlays are modal in some terminals." },
94
94
  "autoOpenDashboardForForegroundRuns": { "type": "boolean", "default": true },
95
95
  "showModel": { "type": "boolean", "default": true, "description": "Show worker model attempts in dashboard agent rows." },
96
96
  "showTokens": { "type": "boolean", "description": "Show token usage in dashboard agent rows." },
@@ -1,4 +1,6 @@
1
+ import * as fs from "node:fs";
1
2
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
3
+ import { Type } from "typebox";
2
4
  import { loadConfig } from "../config/config.ts";
3
5
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
4
6
  import { TeamToolParams, type TeamToolParamsValue } from "../schema/team-tool-schema.ts";
@@ -14,9 +16,10 @@ import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.t
14
16
  import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
15
17
  import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
16
18
  import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
17
- import { loadRunManifestById } from "../state/state-store.ts";
19
+ import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
18
20
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
19
21
  import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
22
+ import { SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
20
23
 
21
24
  function parseRunArgs(args: string): TeamToolParamsValue {
22
25
  const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -100,6 +103,55 @@ function setNestedConfig(config: Record<string, unknown>, key: string, value: un
100
103
  target[parts[parts.length - 1]!] = value;
101
104
  }
102
105
 
106
+ function sendFollowUp(pi: ExtensionAPI, content: string): void {
107
+ const sender = (pi as unknown as { sendMessage?: (message: unknown, options?: unknown) => void }).sendMessage;
108
+ if (typeof sender !== "function") return;
109
+ sender.call(pi, { customType: "pi-crew-subagent-notification", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
110
+ }
111
+
112
+ function formatSubagentRecord(record: SubagentRecord): string {
113
+ const duration = record.completedAt ? `${Math.round((record.completedAt - record.startedAt) / 1000)}s` : "running";
114
+ return [
115
+ `Agent: ${record.id}`,
116
+ `Type: ${record.type}`,
117
+ `Status: ${record.status}`,
118
+ record.runId ? `Run: ${record.runId}` : undefined,
119
+ `Description: ${record.description}`,
120
+ record.model ? `Model: ${record.model}` : undefined,
121
+ `Duration: ${duration}`,
122
+ record.error ? `Error: ${record.error}` : undefined,
123
+ ].filter((line): line is string => Boolean(line)).join("\n");
124
+ }
125
+
126
+ function readSubagentRunResult(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): string | undefined {
127
+ if (!record.runId) return record.result;
128
+ const loaded = loadRunManifestById(ctx.cwd, record.runId);
129
+ const task = loaded?.tasks.find((item) => item.resultArtifact) ?? loaded?.tasks[0];
130
+ const path = task?.resultArtifact?.path;
131
+ if (!path) return undefined;
132
+ try {
133
+ return fs.readFileSync(path, "utf-8").trim();
134
+ } catch {
135
+ return undefined;
136
+ }
137
+ }
138
+
139
+ function subagentToolResult(text: string, details: Record<string, unknown> = {}, isError = false) {
140
+ return { content: [{ type: "text" as const, text }], details, isError };
141
+ }
142
+
143
+ export function __test__subagentSpawnParams(params: Record<string, unknown>, ctx: Pick<ExtensionContext, "cwd">): SubagentSpawnOptions {
144
+ return {
145
+ cwd: ctx.cwd,
146
+ type: typeof params.subagent_type === "string" && params.subagent_type.trim() ? params.subagent_type.trim() : "executor",
147
+ description: typeof params.description === "string" && params.description.trim() ? params.description.trim() : "pi-crew subagent",
148
+ prompt: typeof params.prompt === "string" ? params.prompt : "",
149
+ background: params.run_in_background === true,
150
+ model: typeof params.model === "string" && params.model.trim() ? params.model.trim() : undefined,
151
+ maxTurns: typeof params.max_turns === "number" && Number.isFinite(params.max_turns) ? params.max_turns : undefined,
152
+ };
153
+ }
154
+
103
155
  export function registerPiTeams(pi: ExtensionAPI): void {
104
156
  const globalStore = globalThis as Record<string, unknown>;
105
157
  const runtimeCleanupStoreKey = "__piCrewRuntimeCleanup";
@@ -112,18 +164,37 @@ export function registerPiTeams(pi: ExtensionAPI): void {
112
164
  let rpcHandle: PiCrewRpcHandle | undefined;
113
165
  let cleanedUp = false;
114
166
  const widgetState: CrewWidgetState = { frame: 0 };
167
+ const subagentManager = new SubagentManager(4, (record) => {
168
+ if (!record.background || record.resultConsumed) return;
169
+ if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "error") {
170
+ sendFollowUp(pi, [`pi-crew subagent ${record.id} ${record.status}.`, record.runId ? `Run: ${record.runId}` : undefined, `Use get_subagent_result with agent_id=${record.id} for output.`].filter((line): line is string => Boolean(line)).join("\n"));
171
+ }
172
+ });
115
173
  const foregroundControllers = new Set<AbortController>();
116
174
  let liveSidebarRunId: string | undefined;
117
175
  let liveSidebarTimer: ReturnType<typeof setInterval> | undefined;
118
176
  const requestRender = (ctx: ExtensionContext): void => (ctx.ui as { requestRender?: () => void }).requestRender?.();
177
+ const stopSessionBoundSubagents = (): void => {
178
+ for (const controller of foregroundControllers) controller.abort();
179
+ foregroundControllers.clear();
180
+ subagentManager.abortAll();
181
+ terminateActiveChildPiProcesses();
182
+ if (liveSidebarTimer) clearInterval(liveSidebarTimer);
183
+ liveSidebarTimer = undefined;
184
+ liveSidebarRunId = undefined;
185
+ if (currentCtx) stopCrewWidget(currentCtx, widgetState, loadConfig(currentCtx.cwd).config.ui);
186
+ clearPiCrewPowerbar(pi.events);
187
+ };
119
188
  const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
120
189
  const uiConfig = loadConfig(ctx.cwd).config.ui;
121
- const autoOpen = uiConfig?.autoOpenDashboard ?? true;
122
- const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns ?? true;
190
+ const autoOpen = uiConfig?.autoOpenDashboard === true;
191
+ const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns !== false;
123
192
  if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? "right") !== "right") return;
124
193
  if (liveSidebarRunId === runId) return;
125
194
  if (liveSidebarTimer) clearInterval(liveSidebarTimer);
126
195
  liveSidebarRunId = runId;
196
+ ctx.ui.setWidget("pi-crew", undefined, { placement: uiConfig?.widgetPlacement ?? "aboveEditor" });
197
+ ctx.ui.setWidget("pi-crew-active", undefined, { placement: uiConfig?.widgetPlacement ?? "aboveEditor" });
127
198
  const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
128
199
  liveSidebarTimer = setInterval(() => requestRender(ctx), uiConfig?.dashboardLiveRefreshMs ?? 1000);
129
200
  liveSidebarTimer.unref?.();
@@ -134,19 +205,32 @@ export function registerPiTeams(pi: ExtensionAPI): void {
134
205
  if (liveSidebarRunId === runId) liveSidebarRunId = undefined;
135
206
  if (liveSidebarTimer) clearInterval(liveSidebarTimer);
136
207
  liveSidebarTimer = undefined;
208
+ updateCrewWidget(ctx, widgetState, loadConfig(ctx.cwd).config.ui);
137
209
  });
138
210
  };
139
- const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>): void => {
211
+ const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
140
212
  const controller = new AbortController();
141
213
  foregroundControllers.add(controller);
142
214
  setImmediate(() => {
143
215
  void runner(controller.signal)
144
216
  .catch((error) => {
145
217
  const message = error instanceof Error ? error.message : String(error);
218
+ if (runId) {
219
+ try {
220
+ const loaded = loadRunManifestById(ctx.cwd, runId);
221
+ if (loaded && loaded.manifest.status !== "completed" && loaded.manifest.status !== "failed" && loaded.manifest.status !== "cancelled" && loaded.manifest.status !== "blocked") updateRunStatus(loaded.manifest, "failed", message);
222
+ } catch {}
223
+ }
146
224
  ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
147
225
  })
148
226
  .finally(() => {
149
227
  foregroundControllers.delete(controller);
228
+ if (runId) {
229
+ const loaded = loadRunManifestById(ctx.cwd, runId);
230
+ const status = loaded?.manifest.status ?? "finished";
231
+ const level = status === "failed" || status === "blocked" ? "error" : status === "cancelled" ? "warning" : "info";
232
+ ctx.ui.notify(`pi-crew run ${runId} ${status}. Use /team-summary ${runId} or /team-status ${runId}.`, level as "info" | "warning" | "error");
233
+ }
150
234
  if (currentCtx) {
151
235
  const config = loadConfig(currentCtx.cwd).config.ui;
152
236
  updateCrewWidget(currentCtx, widgetState, config);
@@ -160,14 +244,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
160
244
  const cleanupRuntime = (): void => {
161
245
  if (cleanedUp) return;
162
246
  cleanedUp = true;
163
- for (const controller of foregroundControllers) controller.abort();
164
- foregroundControllers.clear();
165
- terminateActiveChildPiProcesses();
247
+ stopSessionBoundSubagents();
166
248
  stopAsyncRunNotifier(notifierState);
167
249
  stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
168
- if (liveSidebarTimer) clearInterval(liveSidebarTimer);
169
- liveSidebarTimer = undefined;
170
- liveSidebarRunId = undefined;
171
250
  clearPiCrewPowerbar(pi.events);
172
251
  rpcHandle?.unsubscribe();
173
252
  rpcHandle = undefined;
@@ -179,6 +258,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
179
258
  pi.on("session_start", (_event, ctx) => {
180
259
  cleanedUp = false;
181
260
  currentCtx = ctx;
261
+ if (widgetState.interval) clearInterval(widgetState.interval);
262
+ widgetState.interval = undefined;
182
263
  notifyActiveRuns(ctx);
183
264
  const loadedConfig = loadConfig(ctx.cwd);
184
265
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
@@ -188,11 +269,19 @@ export function registerPiTeams(pi: ExtensionAPI): void {
188
269
  widgetState.interval = setInterval(() => {
189
270
  if (!currentCtx) return;
190
271
  const config = loadConfig(currentCtx.cwd).config.ui;
191
- updateCrewWidget(currentCtx, widgetState, config);
272
+ if (liveSidebarRunId) {
273
+ currentCtx.ui.setWidget("pi-crew", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
274
+ currentCtx.ui.setWidget("pi-crew-active", undefined, { placement: config?.widgetPlacement ?? "aboveEditor" });
275
+ } else {
276
+ updateCrewWidget(currentCtx, widgetState, config);
277
+ }
192
278
  updatePiCrewPowerbar(pi.events, currentCtx.cwd, config);
193
279
  }, 1000);
194
280
  widgetState.interval.unref?.();
195
281
  });
282
+ pi.on("session_before_switch", () => {
283
+ stopSessionBoundSubagents();
284
+ });
196
285
  pi.on("session_shutdown", () => {
197
286
  cleanupRuntime();
198
287
  });
@@ -209,7 +298,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
209
298
  const abort = (): void => controller.abort();
210
299
  signal?.addEventListener("abort", abort, { once: true });
211
300
  try {
212
- const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner) => startForegroundRun(ctx, runner), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
301
+ const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal, startForegroundRun: (runner, runId) => startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) });
213
302
  const config = loadConfig(ctx.cwd).config.ui;
214
303
  updateCrewWidget(ctx, widgetState, config);
215
304
  updatePiCrewPowerbar(pi.events, ctx.cwd, config);
@@ -223,6 +312,108 @@ export function registerPiTeams(pi: ExtensionAPI): void {
223
312
 
224
313
  pi.registerTool(tool);
225
314
 
315
+ const agentTool: ToolDefinition = {
316
+ name: "Agent",
317
+ label: "Agent",
318
+ description: "Launch a real pi-crew subagent. Uses pi-crew's durable child-process runtime by default; set run_in_background=true for parallel/background work, then use get_subagent_result.",
319
+ promptSnippet: "Use Agent to delegate focused work to a real pi-crew subagent. Use run_in_background=true for parallel work and get_subagent_result to join results.",
320
+ promptGuidelines: [
321
+ "Use Agent for independent exploration, review, verification, or implementation subtasks instead of doing all work in the parent turn.",
322
+ "For parallel work, launch multiple Agent calls with run_in_background=true, then call get_subagent_result for each result.",
323
+ "Available pi-crew subagent types include explorer, planner, analyst, executor, reviewer, verifier, writer, security-reviewer, and test-engineer.",
324
+ ],
325
+ parameters: Type.Object({
326
+ prompt: Type.String({ description: "The task for the subagent to perform." }),
327
+ description: Type.String({ description: "Short 3-5 word task description." }),
328
+ subagent_type: Type.String({ description: "pi-crew agent name, e.g. explorer, planner, executor, reviewer, verifier, writer, security-reviewer, test-engineer." }),
329
+ model: Type.Optional(Type.String({ description: "Optional model override. If omitted, pi-crew uses Pi-configured model fallback." })),
330
+ max_turns: Type.Optional(Type.Number({ description: "Reserved for live-session subagents; child-process runtime may ignore this." })),
331
+ run_in_background: Type.Optional(Type.Boolean({ description: "Run in background and return an agent ID immediately." })),
332
+ }) as never,
333
+ async execute(_id, params, signal, _onUpdate, ctx) {
334
+ const options = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
335
+ if (!options.prompt.trim()) return subagentToolResult("Agent requires prompt.", {}, true);
336
+ const runner = async (spawnOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({
337
+ action: "run",
338
+ agent: spawnOptions.type,
339
+ goal: spawnOptions.prompt,
340
+ model: spawnOptions.model,
341
+ async: false,
342
+ config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined,
343
+ }, spawnOptions.background ? { ...ctx, signal: childSignal, startForegroundRun: (run, runId) => startForegroundRun(ctx, run, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) } : { ...ctx, signal: childSignal });
344
+ const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
345
+ if (options.background || record.status === "queued") {
346
+ return subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status });
347
+ }
348
+ await record.promise;
349
+ const output = readSubagentRunResult(ctx, record) ?? record.result ?? "No output.";
350
+ return subagentToolResult([`Agent ${record.id} ${record.status}.`, "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
351
+ },
352
+ };
353
+
354
+ const getSubagentResultTool: ToolDefinition = {
355
+ name: "get_subagent_result",
356
+ label: "Get Agent Result",
357
+ description: "Check status and retrieve results from a pi-crew background subagent.",
358
+ parameters: Type.Object({
359
+ agent_id: Type.String({ description: "Agent ID returned by Agent." }),
360
+ wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })),
361
+ verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })),
362
+ }) as never,
363
+ async execute(_id, params, _signal, _onUpdate, ctx) {
364
+ const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
365
+ if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
366
+ const record = subagentManager.getRecord(p.agent_id);
367
+ if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
368
+ let current = record;
369
+ if (p.wait && (current.status === "running" || current.status === "queued")) {
370
+ current.resultConsumed = true;
371
+ current = await subagentManager.waitForRecord(current.id) ?? current;
372
+ }
373
+ const output = readSubagentRunResult(ctx, current);
374
+ if (current.status !== "running" && current.status !== "queued") current.resultConsumed = true;
375
+ const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
376
+ return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
377
+ },
378
+ };
379
+
380
+ const steerSubagentTool: ToolDefinition = {
381
+ name: "steer_subagent",
382
+ label: "Steer Agent",
383
+ description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
384
+ parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
385
+ async execute(_id, params) {
386
+ const p = params as { agent_id?: string; message?: string };
387
+ const record = p.agent_id ? subagentManager.getRecord(p.agent_id) : undefined;
388
+ if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
389
+ return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
390
+ },
391
+ };
392
+
393
+ const crewAgentTool: ToolDefinition = {
394
+ ...agentTool,
395
+ name: "crew_agent",
396
+ label: "Crew Agent",
397
+ description: "Launch a real pi-crew subagent using a conflict-safe pi-crew-specific tool name.",
398
+ promptSnippet: "Use crew_agent when you need pi-crew subagents and another extension may own the generic Agent tool.",
399
+ };
400
+ const crewAgentResultTool: ToolDefinition = {
401
+ ...getSubagentResultTool,
402
+ name: "crew_agent_result",
403
+ label: "Get Crew Agent Result",
404
+ description: "Check status and retrieve results from a pi-crew subagent using the conflict-safe tool name.",
405
+ };
406
+ const crewAgentSteerTool: ToolDefinition = {
407
+ ...steerSubagentTool,
408
+ name: "crew_agent_steer",
409
+ label: "Steer Crew Agent",
410
+ description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name.",
411
+ };
412
+ for (const extraTool of [crewAgentTool, crewAgentResultTool, crewAgentSteerTool]) pi.registerTool(extraTool);
413
+ for (const extraTool of [agentTool, getSubagentResultTool, steerSubagentTool]) {
414
+ try { pi.registerTool(extraTool); } catch {}
415
+ }
416
+
226
417
  pi.registerCommand("teams", {
227
418
  description: "List pi-crew teams, workflows, and agents",
228
419
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
@@ -234,7 +425,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
234
425
  pi.registerCommand("team-run", {
235
426
  description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
236
427
  handler: async (args: string, ctx: ExtensionCommandContext) => {
237
- const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner) => startForegroundRun(ctx as ExtensionContext, runner), onRunStarted: (runId) => openLiveSidebar(ctx as ExtensionContext, runId) });
428
+ const result = await handleTeamTool(parseRunArgs(args), { ...ctx, startForegroundRun: (runner, runId) => startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => openLiveSidebar(ctx as ExtensionContext, runId) });
238
429
  await notifyCommandResult(ctx, commandText(result));
239
430
  },
240
431
  });
@@ -59,7 +59,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
59
59
  sessionManager?: { getBranch?: () => unknown[] };
60
60
  events?: { emit?: (event: string, data: unknown) => void };
61
61
  signal?: AbortSignal;
62
- startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>) => void;
62
+ startForegroundRun?: (runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
63
63
  onRunStarted?: (runId: string) => void;
64
64
  };
65
65
 
@@ -300,13 +300,29 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
300
300
  const teams = allTeams(discoverTeams(ctx.cwd));
301
301
  const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
302
302
  const agents = allAgents(discoverAgents(ctx.cwd));
303
+ const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
304
+ if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
303
305
  const teamName = params.team ?? "default";
304
- const team = teams.find((item) => item.name === teamName);
306
+ const team = directAgent ? {
307
+ name: `direct-${directAgent.name}`,
308
+ description: `Direct subagent run for ${directAgent.name}`,
309
+ source: "builtin" as const,
310
+ filePath: "<generated>",
311
+ roles: [{ name: params.role ?? "agent", agent: directAgent.name, description: directAgent.description }],
312
+ defaultWorkflow: "direct-agent",
313
+ workspaceMode: params.workspaceMode,
314
+ } : teams.find((item) => item.name === teamName);
305
315
  if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
306
- const workflowName = params.workflow ?? team.defaultWorkflow ?? "default";
307
- const baseWorkflow = workflows.find((item) => item.name === workflowName);
316
+ const workflowName = directAgent ? "direct-agent" : params.workflow ?? team.defaultWorkflow ?? "default";
317
+ const baseWorkflow = directAgent ? {
318
+ name: "direct-agent",
319
+ description: `Direct task for ${directAgent.name}`,
320
+ source: "builtin" as const,
321
+ filePath: "<generated>",
322
+ steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
323
+ } : workflows.find((item) => item.name === workflowName);
308
324
  if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
309
- const workflow = expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
325
+ const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
310
326
 
311
327
  const validationErrors = validateWorkflowForTeam(workflow, team);
312
328
  if (validationErrors.length > 0) {
@@ -352,13 +368,13 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
352
368
  }
353
369
 
354
370
  const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
355
- const executeWorkers = runtime.kind === "child-process";
371
+ const executeWorkers = runtime.kind !== "scaffold";
356
372
  const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
357
373
  if (executeWorkers && ctx.startForegroundRun) {
358
374
  ctx.onRunStarted?.(updatedManifest.runId);
359
375
  ctx.startForegroundRun(async (signal) => {
360
376
  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 });
361
- });
377
+ }, updatedManifest.runId);
362
378
  const text = [
363
379
  `Started foreground pi-crew run ${updatedManifest.runId}.`,
364
380
  `Team: ${team.name}`,
@@ -507,7 +523,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
507
523
  appendEvent(loaded.manifest.eventsPath, { type: "run.resume_requested", runId: loaded.manifest.runId });
508
524
  const loadedConfig = loadConfig(ctx.cwd);
509
525
  const runtime = await resolveCrewRuntime(loadedConfig.config);
510
- const executeWorkers = runtime.kind === "child-process";
526
+ const executeWorkers = runtime.kind !== "scaffold";
511
527
  const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
512
528
  return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
513
529
  });
@@ -210,8 +210,11 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
210
210
  observeStdoutChunk(input, stdout);
211
211
  return { exitCode: 0, stdout, stderr: "" };
212
212
  }
213
- if (mock === "json-success") {
214
- const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text: `Mock JSON success for ${input.agent.name}` }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
213
+ if (mock === "json-success" || mock === "adaptive-plan") {
214
+ const text = mock === "adaptive-plan" && input.task.includes("ADAPTIVE_PLAN_JSON_START")
215
+ ? `Adaptive mock plan\nADAPTIVE_PLAN_JSON_START\n${JSON.stringify({ phases: [{ name: "research", tasks: [{ role: "explorer", task: "Explore adaptive target" }, { role: "analyst", task: "Analyze adaptive target" }, { role: "planner", task: "Plan adaptive target" }] }, { name: "build", tasks: [{ role: "executor", task: "Implement adaptive target" }] }, { name: "check", tasks: [{ role: "reviewer", task: "Review adaptive target" }, { role: "test-engineer", task: "Test adaptive target" }, { role: "writer", task: "Summarize adaptive target" }] }] })}\nADAPTIVE_PLAN_JSON_END`
216
+ : `Mock JSON success for ${input.agent.name}`;
217
+ const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
215
218
  observeStdoutChunk(input, stdout);
216
219
  return { exitCode: 0, stdout, stderr: "" };
217
220
  }
@@ -0,0 +1,42 @@
1
+ export interface ResolveBatchConcurrencyInput {
2
+ workflowName: string;
3
+ teamMaxConcurrency?: number;
4
+ limitMaxConcurrentWorkers?: number;
5
+ readyCount: number;
6
+ workspaceMode?: "single" | "worktree";
7
+ readyRoles?: string[];
8
+ }
9
+
10
+ export interface BatchConcurrencyDecision {
11
+ maxConcurrent: number;
12
+ selectedCount: number;
13
+ defaultConcurrency: number;
14
+ reason: string;
15
+ }
16
+
17
+ export function defaultWorkflowConcurrency(workflowName: string): number {
18
+ if (workflowName === "parallel-research") return 4;
19
+ if (workflowName === "research") return 2;
20
+ if (workflowName === "implementation" || workflowName === "review" || workflowName === "default") return 2;
21
+ return 1;
22
+ }
23
+
24
+ function positiveInteger(value: number | undefined): number | undefined {
25
+ if (value === undefined || !Number.isFinite(value)) return undefined;
26
+ return Math.max(1, Math.trunc(value));
27
+ }
28
+
29
+ export function resolveBatchConcurrency(input: ResolveBatchConcurrencyInput): BatchConcurrencyDecision {
30
+ const defaultConcurrency = defaultWorkflowConcurrency(input.workflowName);
31
+ const limitMax = positiveInteger(input.limitMaxConcurrentWorkers);
32
+ const teamMax = positiveInteger(input.teamMaxConcurrency);
33
+ const requested = limitMax ?? teamMax ?? defaultConcurrency;
34
+ const source = limitMax !== undefined ? "limit" : teamMax !== undefined ? "team" : "workflow";
35
+ const readyCount = Math.max(0, Math.trunc(input.readyCount));
36
+ return {
37
+ maxConcurrent: requested,
38
+ selectedCount: readyCount === 0 ? 0 : Math.min(readyCount, requested),
39
+ defaultConcurrency,
40
+ reason: `${source}:${requested};ready:${readyCount}`,
41
+ };
42
+ }
@@ -15,6 +15,7 @@ export interface BuildPiWorkerArgsInput {
15
15
  model?: string;
16
16
  sessionEnabled?: boolean;
17
17
  maxDepth?: number;
18
+ env?: NodeJS.ProcessEnv;
18
19
  }
19
20
 
20
21
  export interface BuildPiWorkerArgsResult {
@@ -82,8 +83,9 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
82
83
  args.push(`Task: ${input.task}`);
83
84
  }
84
85
 
85
- const parentDepth = currentCrewDepth();
86
- const maxDepth = resolveCrewMaxDepth(input.maxDepth);
86
+ const env = input.env ?? process.env;
87
+ const parentDepth = currentCrewDepth(env);
88
+ const maxDepth = resolveCrewMaxDepth(input.maxDepth, env);
87
89
  return {
88
90
  args,
89
91
  env: {
@@ -0,0 +1,43 @@
1
+ export interface ProgressEventSummary {
2
+ eventType: string;
3
+ currentTool?: string;
4
+ toolCount?: number;
5
+ tokens?: number;
6
+ turns?: number;
7
+ activityState?: string;
8
+ lastActivityAt?: string;
9
+ }
10
+
11
+ export interface ProgressEventCoalesceDecision {
12
+ shouldAppend: boolean;
13
+ reason: string;
14
+ }
15
+
16
+ export interface ProgressEventCoalesceInput {
17
+ previous?: ProgressEventSummary;
18
+ next: ProgressEventSummary;
19
+ nowMs: number;
20
+ lastAppendMs?: number;
21
+ minIntervalMs: number;
22
+ force?: boolean;
23
+ tokenThreshold?: number;
24
+ }
25
+
26
+ const DEFAULT_TOKEN_THRESHOLD = 256;
27
+
28
+ function numericIncrease(previous: number | undefined, next: number | undefined): number {
29
+ return next !== undefined && previous !== undefined ? next - previous : next !== undefined ? next : 0;
30
+ }
31
+
32
+ export function shouldAppendProgressEventUpdate(input: ProgressEventCoalesceInput): ProgressEventCoalesceDecision {
33
+ if (input.force) return { shouldAppend: true, reason: "force" };
34
+ if (!input.previous) return { shouldAppend: true, reason: "first" };
35
+ if (input.previous.activityState !== input.next.activityState) return { shouldAppend: true, reason: "activity_changed" };
36
+ if (input.previous.currentTool !== input.next.currentTool) return { shouldAppend: true, reason: "tool_changed" };
37
+ if (numericIncrease(input.previous.toolCount, input.next.toolCount) > 0) return { shouldAppend: true, reason: "tool_count_increased" };
38
+ if (numericIncrease(input.previous.turns, input.next.turns) > 0) return { shouldAppend: true, reason: "turns_increased" };
39
+ const tokenIncrease = numericIncrease(input.previous.tokens, input.next.tokens);
40
+ if (tokenIncrease >= (input.tokenThreshold ?? DEFAULT_TOKEN_THRESHOLD)) return { shouldAppend: true, reason: "tokens_increased" };
41
+ if (input.lastAppendMs === undefined || input.nowMs - input.lastAppendMs >= input.minIntervalMs) return { shouldAppend: true, reason: "interval" };
42
+ return { shouldAppend: false, reason: "coalesced" };
43
+ }
@@ -0,0 +1,202 @@
1
+ import { loadRunManifestById } from "../state/state-store.ts";
2
+ import type { PiTeamsToolResult } from "../extension/tool-result.ts";
3
+
4
+ export type SubagentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "error" | "stopped";
5
+
6
+ export interface SubagentSpawnOptions {
7
+ cwd: string;
8
+ type: string;
9
+ description: string;
10
+ prompt: string;
11
+ background: boolean;
12
+ model?: string;
13
+ maxTurns?: number;
14
+ }
15
+
16
+ export interface SubagentRecord {
17
+ id: string;
18
+ runId?: string;
19
+ type: string;
20
+ description: string;
21
+ prompt: string;
22
+ status: SubagentStatus;
23
+ startedAt: number;
24
+ completedAt?: number;
25
+ result?: string;
26
+ error?: string;
27
+ resultConsumed?: boolean;
28
+ model?: string;
29
+ background: boolean;
30
+ promise?: Promise<void>;
31
+ }
32
+
33
+ type SpawnRunner = (options: SubagentSpawnOptions, signal?: AbortSignal) => Promise<PiTeamsToolResult>;
34
+ type Notify = (record: SubagentRecord) => void;
35
+
36
+ interface QueuedSpawn {
37
+ record: SubagentRecord;
38
+ options: SubagentSpawnOptions;
39
+ runner: SpawnRunner;
40
+ signal?: AbortSignal;
41
+ }
42
+
43
+ const TERMINAL_RUN_STATUS = new Set(["completed", "failed", "cancelled", "blocked"]);
44
+
45
+ function resultText(result: PiTeamsToolResult): string {
46
+ return result.content?.map((item) => item.type === "text" ? item.text : "").filter(Boolean).join("\n") ?? "";
47
+ }
48
+
49
+ function detailsRunId(result: PiTeamsToolResult): string | undefined {
50
+ const details = result.details as { runId?: unknown } | undefined;
51
+ return typeof details?.runId === "string" ? details.runId : undefined;
52
+ }
53
+
54
+ export class SubagentManager {
55
+ private readonly records = new Map<string, SubagentRecord>();
56
+ private queue: QueuedSpawn[] = [];
57
+ private runningBackground = 0;
58
+ private counter = 0;
59
+ private maxConcurrent: number;
60
+ private readonly onComplete?: Notify;
61
+ private readonly pollIntervalMs: number;
62
+
63
+ constructor(maxConcurrent = 4, onComplete?: Notify, pollIntervalMs = 1000) {
64
+ this.maxConcurrent = maxConcurrent;
65
+ this.onComplete = onComplete;
66
+ this.pollIntervalMs = pollIntervalMs;
67
+ }
68
+
69
+ spawn(options: SubagentSpawnOptions, runner: SpawnRunner, signal?: AbortSignal): SubagentRecord {
70
+ const record: SubagentRecord = {
71
+ id: `agent_${Date.now().toString(36)}_${(++this.counter).toString(36)}`,
72
+ type: options.type,
73
+ description: options.description,
74
+ prompt: options.prompt,
75
+ status: options.background && this.runningBackground >= this.maxConcurrent ? "queued" : "running",
76
+ startedAt: Date.now(),
77
+ model: options.model,
78
+ background: options.background,
79
+ };
80
+ this.records.set(record.id, record);
81
+ if (record.status === "queued") {
82
+ this.queue.push({ record, options, runner, signal });
83
+ return record;
84
+ }
85
+ this.start(record, options, runner, signal);
86
+ return record;
87
+ }
88
+
89
+ getRecord(id: string): SubagentRecord | undefined {
90
+ return this.records.get(id);
91
+ }
92
+
93
+ listAgents(): SubagentRecord[] {
94
+ return [...this.records.values()].sort((a, b) => b.startedAt - a.startedAt);
95
+ }
96
+
97
+ abort(id: string): boolean {
98
+ const record = this.records.get(id);
99
+ if (!record) return false;
100
+ if (record.status === "queued") {
101
+ this.queue = this.queue.filter((entry) => entry.record.id !== id);
102
+ record.status = "stopped";
103
+ record.completedAt = Date.now();
104
+ return true;
105
+ }
106
+ if (record.status !== "running") return false;
107
+ record.status = "stopped";
108
+ record.completedAt = Date.now();
109
+ return true;
110
+ }
111
+
112
+ abortAll(): number {
113
+ let count = 0;
114
+ for (const entry of this.queue) {
115
+ entry.record.status = "stopped";
116
+ entry.record.completedAt = Date.now();
117
+ count++;
118
+ }
119
+ this.queue = [];
120
+ for (const record of this.records.values()) {
121
+ if (record.status === "running") {
122
+ record.status = "stopped";
123
+ record.completedAt = Date.now();
124
+ count++;
125
+ }
126
+ }
127
+ return count;
128
+ }
129
+
130
+ async waitForAll(): Promise<void> {
131
+ while (true) {
132
+ this.drainQueue();
133
+ const pending = this.listAgents().filter((record) => record.status === "running" || record.status === "queued").map((record) => record.promise).filter((promise): promise is Promise<void> => Boolean(promise));
134
+ if (!pending.length) break;
135
+ await Promise.allSettled(pending);
136
+ }
137
+ }
138
+
139
+ async waitForRecord(id: string): Promise<SubagentRecord | undefined> {
140
+ while (true) {
141
+ const record = this.records.get(id);
142
+ if (!record) return undefined;
143
+ if (record.status !== "running" && record.status !== "queued") return record;
144
+ if (record.promise) await record.promise;
145
+ else await new Promise((resolve) => setTimeout(resolve, 100));
146
+ }
147
+ }
148
+
149
+ setMaxConcurrent(value: number): void {
150
+ this.maxConcurrent = Math.max(1, Math.floor(value));
151
+ this.drainQueue();
152
+ }
153
+
154
+ private start(record: SubagentRecord, options: SubagentSpawnOptions, runner: SpawnRunner, signal?: AbortSignal): void {
155
+ if (options.background) this.runningBackground++;
156
+ record.status = "running";
157
+ record.startedAt = Date.now();
158
+ record.promise = (async () => {
159
+ try {
160
+ const result = await runner(options, signal);
161
+ record.runId = detailsRunId(result);
162
+ record.result = resultText(result);
163
+ if (result.isError) {
164
+ record.status = "error";
165
+ record.error = record.result;
166
+ return;
167
+ }
168
+ if (record.runId) await this.pollRunToTerminal(options.cwd, record);
169
+ else record.status = "completed";
170
+ } catch (error) {
171
+ record.status = "error";
172
+ record.error = error instanceof Error ? error.message : String(error);
173
+ } finally {
174
+ if (options.background) this.runningBackground = Math.max(0, this.runningBackground - 1);
175
+ record.completedAt = record.completedAt ?? Date.now();
176
+ this.onComplete?.(record);
177
+ this.drainQueue();
178
+ }
179
+ })();
180
+ }
181
+
182
+ private drainQueue(): void {
183
+ while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
184
+ const next = this.queue.shift();
185
+ if (!next || next.record.status !== "queued") continue;
186
+ this.start(next.record, next.options, next.runner, next.signal);
187
+ }
188
+ }
189
+
190
+ private async pollRunToTerminal(cwd: string, record: SubagentRecord): Promise<void> {
191
+ while (record.runId && record.status === "running") {
192
+ const loaded = loadRunManifestById(cwd, record.runId);
193
+ if (loaded && TERMINAL_RUN_STATUS.has(loaded.manifest.status)) {
194
+ record.status = loaded.manifest.status === "completed" ? "completed" : loaded.manifest.status === "cancelled" ? "cancelled" : "failed";
195
+ record.error = record.status === "completed" ? undefined : loaded.manifest.summary;
196
+ record.completedAt = Date.now();
197
+ return;
198
+ }
199
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
200
+ }
201
+ }
202
+ }
@@ -22,6 +22,7 @@ import { parseSessionUsage } from "./session-usage.ts";
22
22
  import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
23
23
  import { buildMemoryBlock } from "./agent-memory.ts";
24
24
  import { runLiveSessionTask } from "./live-session-runtime.ts";
25
+ import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
25
26
 
26
27
  export interface TaskRunnerInput {
27
28
  manifest: TeamRunManifest;
@@ -201,7 +202,7 @@ function cleanResultText(text: string | undefined): string | undefined {
201
202
  return trimmed;
202
203
  }
203
204
 
204
- function progressEventSummary(task: TeamTaskState, event: unknown): Record<string, unknown> {
205
+ function progressEventSummary(task: TeamTaskState, event: unknown): ProgressEventSummary {
205
206
  const type = asRecord(event)?.type;
206
207
  return {
207
208
  eventType: typeof type === "string" ? type : "event",
@@ -306,14 +307,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
306
307
  const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
307
308
  let lastAgentRecordPersistedAt = 0;
308
309
  let lastRunProgressPersistedAt = 0;
310
+ let lastRunProgressSummary: ProgressEventSummary | undefined;
309
311
  const persistChildProgress = (event: unknown, force = false): void => {
310
312
  const now = Date.now();
311
313
  if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
312
314
  upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
313
315
  lastAgentRecordPersistedAt = now;
314
316
  }
315
- if (force || shouldFlushProgressEvent(event) || now - lastRunProgressPersistedAt >= 1000) {
316
- appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: progressEventSummary(task, event) });
317
+ const summary = progressEventSummary(task, event);
318
+ const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
319
+ if (decision.shouldAppend) {
320
+ appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
321
+ lastRunProgressSummary = summary;
317
322
  lastRunProgressPersistedAt = now;
318
323
  }
319
324
  };
@@ -389,14 +394,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
389
394
  const transcriptPath = `${manifest.artifactsRoot}/transcripts/${task.id}.jsonl`;
390
395
  let lastAgentRecordPersistedAt = 0;
391
396
  let lastRunProgressPersistedAt = 0;
397
+ let lastRunProgressSummary: ProgressEventSummary | undefined;
392
398
  const persistLiveProgress = (event: unknown, force = false): void => {
393
399
  const now = Date.now();
394
400
  if (force || shouldFlushProgressEvent(event) || now - lastAgentRecordPersistedAt >= 500) {
395
401
  upsertCrewAgent(manifest, recordFromTask(manifest, task, "live-session"));
396
402
  lastAgentRecordPersistedAt = now;
397
403
  }
398
- if (force || shouldFlushProgressEvent(event) || now - lastRunProgressPersistedAt >= 1000) {
399
- appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: progressEventSummary(task, event) });
404
+ const summary = progressEventSummary(task, event);
405
+ const decision = shouldAppendProgressEventUpdate({ previous: lastRunProgressSummary, next: summary, nowMs: now, lastAppendMs: lastRunProgressPersistedAt || undefined, minIntervalMs: 1000, force });
406
+ if (decision.shouldAppend) {
407
+ appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, taskId: task.id, data: { ...summary, coalesceReason: decision.reason } });
408
+ lastRunProgressSummary = summary;
400
409
  lastRunProgressPersistedAt = now;
401
410
  }
402
411
  };
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import type { AgentConfig } from "../agents/agent-config.ts";
2
3
  import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
3
4
  import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
@@ -17,6 +18,7 @@ import { saveCrewAgents } from "./crew-agent-records.ts";
17
18
  import { recordsForMaterializedTasks } from "./task-display.ts";
18
19
  import { deliverGroupJoin, resolveGroupJoinMode } from "./group-join.ts";
19
20
  import { runTeamTask } from "./task-runner.ts";
21
+ import { resolveBatchConcurrency } from "./concurrency.ts";
20
22
 
21
23
  export interface ExecuteTeamRunInput {
22
24
  manifest: TeamRunManifest;
@@ -75,6 +77,129 @@ function mergeTaskUpdates(base: TeamTaskState[], results: Array<{ tasks: TeamTas
75
77
  return refreshTaskGraphQueues(merged);
76
78
  }
77
79
 
80
+ interface AdaptivePlanTask {
81
+ role: string;
82
+ title?: string;
83
+ task: string;
84
+ }
85
+
86
+ interface AdaptivePlanPhase {
87
+ name: string;
88
+ tasks: AdaptivePlanTask[];
89
+ }
90
+
91
+ interface AdaptivePlan {
92
+ phases: AdaptivePlanPhase[];
93
+ }
94
+
95
+ const MAX_ADAPTIVE_TASKS = 12;
96
+
97
+ function slug(value: string): string {
98
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
99
+ }
100
+
101
+ export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]): AdaptivePlan | undefined {
102
+ const markerMatch = text.match(/ADAPTIVE_PLAN_JSON_START\s*([\s\S]*?)\s*ADAPTIVE_PLAN_JSON_END/);
103
+ const fencedMatch = markerMatch ? undefined : text.match(/```(?:json)?\s*([\s\S]*?)```/i);
104
+ const raw = markerMatch?.[1] ?? fencedMatch?.[1];
105
+ if (!raw) return undefined;
106
+ let parsed: unknown;
107
+ try { parsed = JSON.parse(raw); } catch { return undefined; }
108
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
109
+ const phasesRaw = Array.isArray((parsed as { phases?: unknown }).phases) ? (parsed as { phases: unknown[] }).phases : Array.isArray((parsed as { tasks?: unknown }).tasks) ? [{ name: "adaptive", tasks: (parsed as { tasks: unknown[] }).tasks }] : undefined;
110
+ if (!phasesRaw) return undefined;
111
+ const allowed = new Set(allowedRoles);
112
+ const phases: AdaptivePlanPhase[] = [];
113
+ let total = 0;
114
+ for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
115
+ if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) return undefined;
116
+ const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
117
+ if (!Array.isArray(phaseObj.tasks) || phaseObj.tasks.length === 0) return undefined;
118
+ const tasks: AdaptivePlanTask[] = [];
119
+ for (const taskRaw of phaseObj.tasks) {
120
+ if (!taskRaw || typeof taskRaw !== "object" || Array.isArray(taskRaw)) return undefined;
121
+ const taskObj = taskRaw as { role?: unknown; title?: unknown; task?: unknown };
122
+ if (typeof taskObj.role !== "string" || !allowed.has(taskObj.role)) return undefined;
123
+ if (typeof taskObj.task !== "string" || !taskObj.task.trim()) return undefined;
124
+ if (total >= MAX_ADAPTIVE_TASKS) return undefined;
125
+ tasks.push({ role: taskObj.role, title: typeof taskObj.title === "string" ? taskObj.title : undefined, task: taskObj.task.trim() });
126
+ total++;
127
+ }
128
+ phases.push({ name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`, tasks });
129
+ }
130
+ return phases.length ? { phases } : undefined;
131
+ }
132
+
133
+ function reconstructAdaptiveWorkflow(workflow: WorkflowConfig, tasks: TeamTaskState[]): WorkflowConfig {
134
+ const existing = new Set(workflow.steps.map((step) => step.id));
135
+ const steps: WorkflowStep[] = [];
136
+ for (const task of tasks) {
137
+ if (!task.stepId?.startsWith("adaptive-") || !task.adaptive?.task || existing.has(task.stepId)) continue;
138
+ steps.push({ id: task.stepId, role: task.role, dependsOn: task.graph?.dependencies ?? task.dependsOn, parallelGroup: `adaptive-${slug(task.adaptive.phase)}`, task: task.adaptive.task });
139
+ }
140
+ return steps.length ? { ...workflow, steps: [...workflow.steps, ...steps] } : workflow;
141
+ }
142
+
143
+ function injectAdaptivePlanIfReady(input: { manifest: TeamRunManifest; tasks: TeamTaskState[]; workflow: WorkflowConfig; team: TeamConfig }): { tasks: TeamTaskState[]; workflow: WorkflowConfig; injected: boolean; missingPlan: boolean } {
144
+ if (input.workflow.name !== "implementation") return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: false };
145
+ if (input.tasks.some((task) => task.stepId?.startsWith("adaptive-"))) return { tasks: input.tasks, workflow: reconstructAdaptiveWorkflow(input.workflow, input.tasks), injected: false, missingPlan: false };
146
+ const completedAssess = input.tasks.find((task) => task.stepId === "assess" && task.status === "completed");
147
+ if (!completedAssess) return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: false };
148
+ if (!completedAssess.resultArtifact?.path) {
149
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: completedAssess.id, message: "Adaptive planner result artifact is missing." });
150
+ return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
151
+ }
152
+ const assessTask = completedAssess;
153
+ const resultPath = completedAssess.resultArtifact.path;
154
+ let text = "";
155
+ try { text = fs.readFileSync(resultPath, "utf-8"); } catch {
156
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner result artifact could not be read." });
157
+ return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
158
+ }
159
+ const plan = __test__parseAdaptivePlan(text, input.team.roles.map((role) => role.name));
160
+ if (!plan) {
161
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner did not produce a valid plan; no dynamic subagents were spawned." });
162
+ return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
163
+ }
164
+ const steps: WorkflowStep[] = [];
165
+ const tasks: TeamTaskState[] = [];
166
+ let previousStepIds = ["assess"];
167
+ let counter = 0;
168
+ for (const [phaseIndex, phase] of plan.phases.entries()) {
169
+ const currentStepIds: string[] = [];
170
+ for (const [taskIndex, planned] of phase.tasks.entries()) {
171
+ counter++;
172
+ const stepId = `adaptive-${phaseIndex + 1}-${taskIndex + 1}-${slug(planned.role)}`;
173
+ const taskId = `adaptive-${String(counter).padStart(2, "0")}-${slug(planned.role)}`;
174
+ steps.push({ id: stepId, role: planned.role, dependsOn: previousStepIds, parallelGroup: `adaptive-${slug(phase.name)}`, task: planned.task });
175
+ tasks.push({
176
+ id: taskId,
177
+ runId: input.manifest.runId,
178
+ stepId,
179
+ role: planned.role,
180
+ agent: input.team.roles.find((role) => role.name === planned.role)?.agent ?? planned.role,
181
+ title: planned.title ?? stepId,
182
+ status: "queued",
183
+ dependsOn: previousStepIds,
184
+ cwd: input.manifest.cwd,
185
+ adaptive: { phase: phase.name, task: planned.task },
186
+ graph: { taskId, dependencies: previousStepIds, children: [], queue: "blocked" },
187
+ });
188
+ currentStepIds.push(stepId);
189
+ }
190
+ previousStepIds = currentStepIds;
191
+ }
192
+ const dependencyTaskIdByStep = new Map<string, string>([["assess", assessTask.id], ...tasks.map((task) => [task.stepId ?? task.id, task.id] as const)]);
193
+ const withGraph = tasks.map((task) => ({
194
+ ...task,
195
+ dependsOn: task.dependsOn.map((dep) => dependencyTaskIdByStep.get(dep) ?? dep),
196
+ graph: task.graph ? { ...task.graph, dependencies: task.dependsOn.map((dep) => dependencyTaskIdByStep.get(dep) ?? dep), queue: "blocked" as const } : task.graph,
197
+ }));
198
+ const allTasks = refreshTaskGraphQueues([...input.tasks, ...withGraph]);
199
+ appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_injected", runId: input.manifest.runId, taskId: assessTask.id, message: `Injected ${withGraph.length} adaptive subagent task(s) across ${plan.phases.length} phase(s).`, data: { phases: plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) } });
200
+ return { tasks: allTasks, workflow: { ...input.workflow, steps: [...input.workflow.steps, ...steps] }, injected: true, missingPlan: false };
201
+ }
202
+
78
203
  function formatTaskProgress(task: TeamTaskState): string {
79
204
  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
205
  }
@@ -143,8 +268,22 @@ function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?:
143
268
  }
144
269
 
145
270
  export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
271
+ let workflow = input.workflow;
146
272
  let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
147
273
  let tasks = refreshTaskGraphQueues(input.tasks);
274
+ const initialAdaptive = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
275
+ if (initialAdaptive.missingPlan) {
276
+ tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
277
+ saveRunTasks(manifest, tasks);
278
+ manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
279
+ return { manifest, tasks };
280
+ }
281
+ if (initialAdaptive.injected) {
282
+ tasks = initialAdaptive.tasks;
283
+ workflow = initialAdaptive.workflow;
284
+ } else {
285
+ workflow = initialAdaptive.workflow;
286
+ }
148
287
  manifest = writeProgress(manifest, tasks, "team-runner");
149
288
  saveRunManifest(manifest);
150
289
  const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
@@ -167,9 +306,26 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
167
306
  return { manifest, tasks };
168
307
  }
169
308
 
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);
309
+ const injectedPlan = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
310
+ if (injectedPlan.missingPlan) {
311
+ tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
312
+ saveRunTasks(manifest, tasks);
313
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
314
+ manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
315
+ return { manifest, tasks };
316
+ }
317
+ if (injectedPlan.injected) {
318
+ tasks = injectedPlan.tasks;
319
+ workflow = injectedPlan.workflow;
320
+ saveRunTasks(manifest, tasks);
321
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
322
+ } else {
323
+ workflow = injectedPlan.workflow;
324
+ }
325
+ const snapshot = taskGraphSnapshot(tasks);
326
+ const readyRoles = snapshot.ready.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
327
+ const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
328
+ const readyBatch = getReadyTasks(tasks, concurrency.selectedCount);
173
329
  if (readyBatch.length === 0) {
174
330
  tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
175
331
  saveRunTasks(manifest, tasks);
@@ -178,14 +334,28 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
178
334
  return { manifest, tasks };
179
335
  }
180
336
 
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 } });
337
+ 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), readyCount: snapshot.ready.length, blockedCount: snapshot.blocked.length, runningCount: snapshot.running.length, doneCount: snapshot.done.length, selectedCount: readyBatch.length, maxConcurrent: concurrency.maxConcurrent, defaultConcurrency: concurrency.defaultConcurrency, concurrencyReason: concurrency.reason } });
182
338
  const results = await Promise.all(readyBatch.map((task) => {
183
- const step = findStep(input.workflow, task);
339
+ const step = findStep(workflow, task);
184
340
  const agent = findAgent(input.agents, task);
185
341
  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
342
  }));
187
343
  manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
188
344
  tasks = mergeTaskUpdates(tasks, results);
345
+ const injectedAfterBatch = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
346
+ if (injectedAfterBatch.missingPlan) {
347
+ tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
348
+ saveRunTasks(manifest, tasks);
349
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
350
+ manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
351
+ return { manifest, tasks };
352
+ }
353
+ if (injectedAfterBatch.injected) {
354
+ tasks = injectedAfterBatch.tasks;
355
+ workflow = injectedAfterBatch.workflow;
356
+ } else {
357
+ workflow = injectedAfterBatch.workflow;
358
+ }
189
359
  saveRunTasks(manifest, tasks);
190
360
  saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
191
361
  const completedBatch = readyBatch.map((task) => tasks.find((item) => item.id === task.id) ?? task);
@@ -152,6 +152,10 @@ export interface TeamTaskState {
152
152
  taskPacket?: TaskPacket;
153
153
  verification?: VerificationEvidence;
154
154
  graph?: TaskGraphNode;
155
+ adaptive?: {
156
+ phase: string;
157
+ task: string;
158
+ };
155
159
  policy?: {
156
160
  retryCount?: number;
157
161
  lastDecision?: PolicyDecision;
@@ -16,7 +16,7 @@ function readTasks(tasksPath: string): TeamTaskState[] {
16
16
  try { const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8")); return Array.isArray(parsed) ? parsed as TeamTaskState[] : []; } catch { return []; }
17
17
  }
18
18
 
19
- function compactTokens(total: number): string {
19
+ export function compactTokens(total: number): string {
20
20
  return total >= 1000 ? `${Math.round(total / 1000)}k` : `${total}`;
21
21
  }
22
22
 
@@ -42,23 +42,24 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
42
42
  const tasks = active.flatMap((item) => readTasks(item.run.tasksPath));
43
43
  const running = agents.filter((agent) => agent.status === "running").length;
44
44
  const waiting = tasks.filter((task) => task.status === "queued").length;
45
- const completed = agents.filter((agent) => agent.status === "completed").length;
46
- const total = Math.max(1, agents.length + waiting);
45
+ const completed = tasks.filter((task) => task.status === "completed").length;
46
+ const total = Math.max(1, tasks.length || agents.length);
47
47
  const usage = aggregateUsage(tasks);
48
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);
49
+ const model = config?.showModel === false ? undefined : agents.find((agent) => agent.model)?.model?.split("/").at(-1);
50
+ const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
50
51
  safeEmit(events, "powerbar:update", {
51
52
  id: "pi-crew-active",
52
53
  icon: "⚙",
53
54
  text: `crew ${running}a/${waiting}w`,
54
- suffix: [model, tokenTotal ? compactTokens(tokenTotal) : undefined].filter(Boolean).join(" · ") || undefined,
55
+ suffix: [model, tokenText].filter(Boolean).join(" · ") || undefined,
55
56
  color: running ? "accent" : "warning",
56
57
  });
57
58
  safeEmit(events, "powerbar:update", {
58
59
  id: "pi-crew-progress",
59
60
  text: active[0]?.run.team ?? "crew",
60
61
  bar: Math.round((completed / total) * 100),
61
- suffix: `${completed}/${total}${tokenTotal ? ` · ${compactTokens(tokenTotal)}` : ""}`,
62
+ suffix: `${completed}/${total}${tokenText ? ` · ${tokenText}` : ""}`,
62
63
  color: completed === total ? "success" : "accent",
63
64
  barSegments: 8,
64
65
  });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: implementation
3
- description: Full implementation team with analysis, critique, execution, review, and verification
3
+ description: Full implementation team with parallel specialists, critique, execution, review, and verification
4
4
  defaultWorkflow: implementation
5
5
  workspaceMode: single
6
6
  maxConcurrency: 3
@@ -9,7 +9,10 @@ maxConcurrency: 3
9
9
  - explorer: agent=explorer map the codebase
10
10
  - analyst: agent=analyst clarify requirements and constraints
11
11
  - planner: agent=planner create execution plan
12
- - critic: agent=critic challenge the plan
12
+ - critic: agent=critic challenge and synthesize specialist findings
13
13
  - executor: agent=executor implement the plan
14
14
  - reviewer: agent=reviewer review the implementation
15
+ - security-reviewer: agent=security-reviewer review security and trust boundaries
16
+ - test-engineer: agent=test-engineer design and run verification
15
17
  - verifier: agent=verifier verify done
18
+ - writer: agent=writer summarize documentation or release notes when needed
@@ -1,47 +1,38 @@
1
1
  ---
2
2
  name: implementation
3
- description: Full implementation workflow with critique and review gates
3
+ description: Adaptive implementation workflow where a planner agent decides the subagent fanout
4
4
  ---
5
5
 
6
- ## explore
7
- role: explorer
8
-
9
- Map relevant files, APIs, and constraints for: {goal}
10
-
11
- ## analyze
12
- role: analyst
13
- dependsOn: explore
14
-
15
- Analyze requirements, ambiguities, risks, and acceptance criteria for: {goal}
16
-
17
- ## plan
6
+ ## assess
18
7
  role: planner
19
- dependsOn: analyze
20
- output: plan.md
21
-
22
- Create an execution plan for: {goal}
23
-
24
- ## critique
25
- role: critic
26
- dependsOn: plan
27
-
28
- Critique the plan and identify required improvements.
29
-
30
- ## execute
31
- role: executor
32
- dependsOn: critique
33
-
34
- Implement the improved plan for: {goal}
35
-
36
- ## review
37
- role: reviewer
38
- dependsOn: execute
39
-
40
- Review the implementation.
41
-
42
- ## verify
43
- role: verifier
44
- dependsOn: review
45
- verify: true
46
-
47
- Verify the final result.
8
+ output: adaptive-plan.json
9
+
10
+ Assess this task and decide how many subagents are actually needed for: {goal}
11
+
12
+ You are the orchestration planner. Inspect the repository enough to choose an efficient crew; do not use a fixed template. Small/simple tasks may need one executor plus one verifier. Risky or broad tasks may need parallel explorers, specialists, implementers, reviewers, security reviewers, or test engineers.
13
+
14
+ Return a concise rationale, then include exactly one JSON block between these markers:
15
+
16
+ ADAPTIVE_PLAN_JSON_START
17
+ {
18
+ "phases": [
19
+ {
20
+ "name": "short-phase-name",
21
+ "tasks": [
22
+ {
23
+ "role": "explorer|analyst|planner|critic|executor|reviewer|security-reviewer|test-engineer|verifier|writer",
24
+ "title": "short task title",
25
+ "task": "specific autonomous task prompt for this subagent"
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
31
+ ADAPTIVE_PLAN_JSON_END
32
+
33
+ Rules:
34
+ - Choose the smallest effective number of subagents.
35
+ - Use parallel tasks in the same phase only when their work is independent.
36
+ - Later phases depend on all tasks in the previous phase.
37
+ - Include verification/review tasks when implementation is requested.
38
+ - Do not include more than 12 total subagents unless the user explicitly asks for a large crew.