@ferris1225/pi-subagents 0.9.0 → 0.11.0

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/src/fixloop.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Auto-fix loop: when a reviewer returns REVIEW_FAIL, the extension dispatches a
3
+ * worker (briefed with the review's concrete findings) and then a reviewer
4
+ * re-review, repeating up to maxFixRounds times before waking the main agent with
5
+ * the full chain. The reviewer stays read-only and in its own context; the loop
6
+ * is orchestrated by the extension layer, not by the reviewer itself, so the
7
+ * independence guarantee (no self-confirmation bias) is preserved.
8
+ *
9
+ * The main agent is never woken mid-loop: the reviewer's FAIL result is intercepted
10
+ * before delivery, the chain runs in the background, and only the final group
11
+ * (initial review → worker fixes → re-reviews) is delivered at the end.
12
+ */
13
+
14
+ import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
15
+ import type { SubagentsConfig } from "./config.ts";
16
+
17
+ /**
18
+ * Whether a completed result should trigger the auto-fix loop instead of being
19
+ * delivered to the main agent. Only a REVIEW_FAIL verdict from a healthy
20
+ * reviewer run counts; failed processes and passing reviews are delivered
21
+ * normally. Loop-internal re-review results never reach this path (they are
22
+ * awaited inside the loop, not delivered through the completion flow).
23
+ */
24
+ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConfig): boolean {
25
+ if (config.maxFixRounds <= 0) return false;
26
+ if (result.agent !== "reviewer") return false;
27
+ if (isFailedResult(result)) return false;
28
+ return reviewVerdict(getResultOutput(result)) === "fail";
29
+ }
30
+
31
+ /**
32
+ * Build the worker task brief for one fix round from a reviewer's findings.
33
+ * The worker gets the full review text so it can address concrete file:line
34
+ * issues, with instructions to fix only blockers and self-verify.
35
+ */
36
+ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
37
+ const review = getResultOutput(reviewerResult);
38
+ const remaining = maxRounds - round;
39
+ return [
40
+ `Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
41
+ ``,
42
+ `A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
43
+ `---`,
44
+ review,
45
+ `---`,
46
+ ``,
47
+ `Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
48
+ `Address every "Critical" item; address "Warnings" only if they are genuine.`,
49
+ `After editing, run the project's format/build/tests when they exist and report`,
50
+ `exactly what you changed (paths + short rationale) so a reviewer can verify.`,
51
+ remaining > 0
52
+ ? `A reviewer will re-review your changes automatically after you finish.`
53
+ : `This is the last auto-fix round; the main agent will be woken with the full chain.`,
54
+ ].join("\n");
55
+ }
56
+
57
+ /**
58
+ * The re-review brief handed to the reviewer after a worker fix round. Includes
59
+ * the prior review so the reviewer can verify the fixes without re-discovering
60
+ * the original issues.
61
+ */
62
+ export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
63
+ const review = getResultOutput(reviewerResult);
64
+ return [
65
+ `Re-review after auto-fix round ${round}.`,
66
+ ``,
67
+ `The previous review (REQUEST_CHANGES) found these issues:`,
68
+ `---`,
69
+ review,
70
+ `---`,
71
+ ``,
72
+ `Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
73
+ `Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
74
+ `End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
75
+ ].join("\n");
76
+ }
package/src/index.ts CHANGED
@@ -18,6 +18,13 @@ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
18
18
  import { Type } from "typebox";
19
19
  import { discoverAgents, type AgentConfig } from "./agents.ts";
20
20
  import { BackgroundTaskQueue } from "./background.ts";
21
+ import {
22
+ completionGroupTriggersTurn,
23
+ completionTriggersTurn,
24
+ createCompletionBatcher,
25
+ formatCompletionMessage,
26
+ type CompletionMessageItem,
27
+ } from "./completion.ts";
21
28
  import { getConfigPath, loadConfig, loadConfigSync, saveConfig } from "./config.ts";
22
29
  import { repairUnavailableModelOverrides } from "./models.ts";
23
30
  import { buildDelegationDirective } from "./prompt.ts";
@@ -27,13 +34,17 @@ import {
27
34
  getFinalOutput,
28
35
  getResultOutput,
29
36
  isFailedResult,
37
+ reviewVerdict,
30
38
  runSingleAgent,
39
+ truncateResultOutput,
40
+ writeResultArtifact,
31
41
  type SingleResult,
32
42
  type SubagentDetails,
33
43
  type SubagentLiveEvent,
34
44
  type UsageStats,
35
45
  } from "./spawn.ts";
36
- import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
46
+ import { buildFixTaskBrief, buildReReviewBrief, shouldTriggerFixLoop } from "./fixloop.ts";
47
+ import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel, type RunChainMeta } from "./monitor.ts";
37
48
 
38
49
  const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
39
50
 
@@ -59,7 +70,7 @@ function emptyUsage(): UsageStats {
59
70
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
60
71
  }
61
72
 
62
- function queuedResult(agent: AgentConfig, task: string): SingleResult {
73
+ function queuedResult(agent: AgentConfig, task: string, thinking?: string): SingleResult {
63
74
  return {
64
75
  agent: agent.name,
65
76
  agentSource: agent.source,
@@ -69,6 +80,7 @@ function queuedResult(agent: AgentConfig, task: string): SingleResult {
69
80
  stderr: "",
70
81
  usage: emptyUsage(),
71
82
  model: agent.model,
83
+ ...(thinking ? { thinking } : {}),
72
84
  };
73
85
  }
74
86
 
@@ -114,6 +126,19 @@ function formatUsage(usage: UsageStats): string {
114
126
  return parts.join(" ");
115
127
  }
116
128
 
129
+ function formatCompletionBlock(result: SingleResult, maxResultLines: number): string {
130
+ const status = isFailedResult(result) ? "failed" : "completed";
131
+ const usage = formatUsage(result.usage);
132
+ const output = getResultOutput(result);
133
+ const { text, truncated } = truncateResultOutput(output, maxResultLines);
134
+ const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}`, "", `Task: ${formatTaskSummary(result.task)}`, "", text];
135
+ if (truncated) {
136
+ // The full text lives on disk so the main agent can read it on demand.
137
+ lines.push("", `(output truncated to ${maxResultLines} lines; full result: ${writeResultArtifact(output, result.agent)})`);
138
+ }
139
+ return lines.join("\n");
140
+ }
141
+
117
142
  export default function (pi: ExtensionAPI): void {
118
143
  const configPath = getConfigPath(getAgentDir());
119
144
  // Init-time decisions need the config synchronously; the full (migrating)
@@ -121,6 +146,23 @@ export default function (pi: ExtensionAPI): void {
121
146
  const initialConfig = loadConfigSync(configPath);
122
147
  const backgroundQueue = new BackgroundTaskQueue(initialConfig.maxConcurrency);
123
148
  let sessionActive = true;
149
+ const sendCompletionGroup = (items: CompletionMessageItem[]): void => {
150
+ if (!sessionActive || items.length === 0) return;
151
+ const message = {
152
+ customType: "subagent-result",
153
+ content: formatCompletionMessage(items),
154
+ display: true,
155
+ };
156
+ if (completionGroupTriggersTurn(items)) {
157
+ pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
158
+ } else {
159
+ // No-wake delivery: nextTurn rides along with the next user turn and can
160
+ // never start a continuation by itself. followUp would auto-continue
161
+ // whenever pi is already streaming, defeating the opt-out.
162
+ pi.sendMessage(message, { deliverAs: "nextTurn" });
163
+ }
164
+ };
165
+ const completionBatcher = createCompletionBatcher<CompletionMessageItem>({ emit: sendCompletionGroup });
124
166
 
125
167
  // Recursion guard: sub-agents at the configured depth are leaf processes and
126
168
  // cannot delegate again. maxSubagentDepth 0 disables the tool entirely.
@@ -148,6 +190,7 @@ export default function (pi: ExtensionAPI): void {
148
190
 
149
191
  pi.on("session_shutdown", () => {
150
192
  sessionActive = false;
193
+ completionBatcher.dispose();
151
194
  backgroundQueue.cancelAll();
152
195
  });
153
196
 
@@ -162,11 +205,11 @@ export default function (pi: ExtensionAPI): void {
162
205
  "Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output)."
163
206
  ].join(" "),
164
207
  promptSnippet:
165
- "Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent.",
208
+ "Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent. Simple tasks: use direct tools, not subagents.",
166
209
  promptGuidelines: [
167
- "Use subagent to delegate discrete, self-contained tasks so the main context stays clean; do orchestration and verification yourself.",
168
- "Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
169
- "Use subagent with agent 'worker' to implement a well-scoped task; it plans internally.",
210
+ "Delegate only when an isolated context genuinely pays: broad exploration, a self-contained implementation, or a review gate. Handle simple lookups and one-line edits inline with direct tools — never spawn a sub-agent for them.",
211
+ "Use subagent with agent 'explore' for broad or open-ended code search before large changes; a targeted 'where is X' is a direct grep/read.",
212
+ "Use subagent with agent 'worker' for a self-contained implementation task worth a separate context; it plans internally.",
170
213
  "Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
171
214
  "subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
172
215
  "Run independent tasks in parallel by passing a tasks array to subagent; let the automatically resumed main agent start dependent work after results arrive.",
@@ -292,12 +335,103 @@ export default function (pi: ExtensionAPI): void {
292
335
  };
293
336
  }
294
337
 
338
+ /**
339
+ * Dispatch one agent inside an auto-fix chain: tracked in the widget with a
340
+ * groupId/relationLabel, but NOT delivered through the completion flow — the
341
+ * chain owner assembles and delivers the whole group at the end.
342
+ */
343
+ const launchInLoop = async (
344
+ agentName: string,
345
+ task: string,
346
+ signal: AbortSignal,
347
+ meta: RunChainMeta,
348
+ ): Promise<SingleResult> => {
349
+ const agent = agents.find((candidate) => candidate.name === agentName);
350
+ if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
351
+ const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
352
+ const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel, meta);
353
+ const onLive = makeLiveHandler(runId);
354
+ try {
355
+ const result = await runSingleAgent({
356
+ defaultCwd: ctx.cwd,
357
+ agent,
358
+ agentName,
359
+ task,
360
+ thinkingLevel,
361
+ signal,
362
+ onLive,
363
+ makeDetails: makeDetails("single", true),
364
+ });
365
+ finishRun(runId, isFailedResult(result) ? "failed" : "done");
366
+ return result;
367
+ } catch (error) {
368
+ finishRun(runId, "failed");
369
+ const errorMessage = error instanceof Error ? error.message : String(error);
370
+ return {
371
+ ...queuedResult(agent, task, thinkingLevel),
372
+ exitCode: 1,
373
+ stderr: errorMessage,
374
+ stopReason: signal.aborted ? "aborted" : "error",
375
+ errorMessage,
376
+ };
377
+ }
378
+ };
379
+
380
+ /**
381
+ * Run the auto-fix chain in the background: worker (briefed with the review's
382
+ * findings) → reviewer re-review, up to maxFixRounds times. The main agent is
383
+ * not woken mid-loop; the full chain is delivered as one group at the end.
384
+ * Failures short-circuit: a crashed worker skips its re-review and delivers.
385
+ */
386
+ const startFixLoop = (initialReviewerResult: SingleResult, parentGroupId: string): void => {
387
+ backgroundQueue.enqueue(
388
+ async (signal) => {
389
+ const chain: SingleResult[] = [initialReviewerResult];
390
+ let lastReviewer = initialReviewerResult;
391
+ for (let round = 1; round <= config.maxFixRounds; round++) {
392
+ if (!sessionActive) break;
393
+ const fixBrief = buildFixTaskBrief(lastReviewer, round, config.maxFixRounds);
394
+ const workerResult = await launchInLoop("worker", fixBrief, signal, {
395
+ groupId: parentGroupId,
396
+ relationLabel: `fix round ${round}`,
397
+ });
398
+ chain.push(workerResult);
399
+ if (!sessionActive || isFailedResult(workerResult)) break;
400
+ const reReviewBrief = buildReReviewBrief(lastReviewer, round);
401
+ const reviewResult = await launchInLoop("reviewer", reReviewBrief, signal, {
402
+ groupId: parentGroupId,
403
+ relationLabel: `re-review round ${round}`,
404
+ });
405
+ chain.push(reviewResult);
406
+ lastReviewer = reviewResult;
407
+ if (!sessionActive) break;
408
+ if (reviewVerdict(getResultOutput(reviewResult)) === "pass") break;
409
+ }
410
+ if (!sessionActive) return;
411
+ // Deliver the whole chain as one group; the loop's outcome always wakes
412
+ // the main agent (a passing chain reports success, a stuck one needs a human).
413
+ const items: CompletionMessageItem[] = chain.map((r) => ({
414
+ agent: r.agent,
415
+ block: formatCompletionBlock(r, config.maxResultLines),
416
+ triggerTurn: true,
417
+ }));
418
+ sendCompletionGroup(items);
419
+ completionBatcher.flush();
420
+ },
421
+ () => {
422
+ // Cancelled: each in-flight run was already finished by its launchInLoop path.
423
+ },
424
+ );
425
+ };
426
+
295
427
  const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
296
428
  const agent = agents.find((candidate) => candidate.name === agentName);
297
429
  if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
298
430
 
299
- const pending = queuedResult(agent, task);
300
- const runId = monitor.addRun(agent.name, task, agent.model);
431
+ // Effective strength: config override > agent frontmatter default > global default.
432
+ const thinkingLevel = config.agentThinkingLevels[agent.name] ?? agent.thinking ?? config.thinkingLevel;
433
+ const pending = queuedResult(agent, task, thinkingLevel);
434
+ const runId = monitor.addRun(agent.name, task, agent.model, thinkingLevel);
301
435
  const onLive = makeLiveHandler(runId);
302
436
 
303
437
  backgroundQueue.enqueue(
@@ -310,7 +444,7 @@ export default function (pi: ExtensionAPI): void {
310
444
  agentName,
311
445
  task,
312
446
  cwd,
313
- thinkingLevel: config.thinkingLevel,
447
+ thinkingLevel,
314
448
  signal: backgroundSignal,
315
449
  onLive,
316
450
  makeDetails: makeDetails("single", true),
@@ -328,18 +462,29 @@ export default function (pi: ExtensionAPI): void {
328
462
  }
329
463
 
330
464
  if (!sessionActive) return;
331
- const status = isFailedResult(result) ? "failed" : "completed";
332
- const usage = formatUsage(result.usage);
333
- pi.sendMessage(
334
- {
335
- customType: "subagent-result",
336
- content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\nTask: ${formatTaskSummary(result.task)}\n\n${getResultOutput(result)}`,
337
- display: true,
338
- },
339
- // The result is both durable context and a wake-up signal. If the
340
- // main agent is busy, followUp queues it until the current turn ends.
341
- { deliverAs: "followUp", triggerTurn: true },
342
- );
465
+ // Auto-fix loop: a REVIEW_FAIL from a main-agent-dispatched reviewer
466
+ // triggers a worker→reviewer chain (up to maxFixRounds) without waking
467
+ // the main agent. Loop-internal re-reviews never reach here (they are
468
+ // awaited inside launchInLoop); the initial review is delivered with
469
+ // the chain at the end.
470
+ if (shouldTriggerFixLoop(result, config)) {
471
+ startFixLoop(result, `fix-${runId}`);
472
+ return;
473
+ }
474
+ const failed = isFailedResult(result);
475
+ const completion: CompletionMessageItem = {
476
+ agent: result.agent,
477
+ block: formatCompletionBlock(result, config.maxResultLines),
478
+ triggerTurn: completionTriggersTurn(result, config.notifyOnReviewPass),
479
+ };
480
+ if (failed) {
481
+ // Failures never wait and never hide behind a success turn: deliver
482
+ // first so the wake-up leads with the failure; held successes follow.
483
+ sendCompletionGroup([completion]);
484
+ completionBatcher.flush();
485
+ } else {
486
+ completionBatcher.push(completion);
487
+ }
343
488
  },
344
489
  () => finishRun(runId, "failed"),
345
490
  );
@@ -426,7 +571,7 @@ export default function (pi: ExtensionAPI): void {
426
571
  const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
427
572
  const usage = formatUsage(r.usage);
428
573
  const model = r.model ?? "?";
429
- const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
574
+ const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
430
575
  return new Text(line, 0, 0);
431
576
  }
432
577
 
@@ -439,7 +584,7 @@ export default function (pi: ExtensionAPI): void {
439
584
  const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
440
585
  const usage = formatUsage(r.usage);
441
586
  const model = r.model ?? "?";
442
- lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
587
+ lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
443
588
  }
444
589
  return new Text(lines.join("\n"), 0, 0);
445
590
  },
@@ -473,7 +618,10 @@ export default function (pi: ExtensionAPI): void {
473
618
  for (const r of runs) {
474
619
  const icon = statusIcon(r.status, theme);
475
620
  const label = theme.fg(statusColor(r.status), statusLabel(r.status));
476
- lines.push(truncateToWidth(` ${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
621
+ // Chain-internal runs (auto-fix worker/reviewer) indent under their
622
+ // parent reviewer; summarize() already carries the relationLabel.
623
+ const head = r.groupId ? theme.fg("dim", " ↳ ") : " ";
624
+ lines.push(truncateToWidth(`${head}${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
477
625
  if (r.status === "queued" || r.status === "running") {
478
626
  lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
479
627
  }
package/src/monitor.ts CHANGED
@@ -26,6 +26,8 @@ export interface RunView {
26
26
  agent: string;
27
27
  task: string;
28
28
  model?: string;
29
+ /** Effective thinking strength this run was launched with (frontmatter/config/global). */
30
+ thinking?: string;
29
31
  status: RunStatus;
30
32
  usage: UsageStats;
31
33
  /** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
@@ -34,6 +36,16 @@ export interface RunView {
34
36
  startedAt?: number;
35
37
  /** Epoch ms when the run finished (set on "done"/"failed"). */
36
38
  endedAt?: number;
39
+ /** When set, this run belongs to an auto-fix chain (e.g. worker fixing a reviewer's findings). */
40
+ groupId?: string;
41
+ /** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
42
+ relationLabel?: string;
43
+ }
44
+
45
+ /** Optional chain metadata for runs spawned by an auto-fix loop. */
46
+ export interface RunChainMeta {
47
+ groupId?: string;
48
+ relationLabel?: string;
37
49
  }
38
50
 
39
51
  // ---------------------------------------------------------------------------
@@ -166,15 +178,18 @@ export class MonitorStore {
166
178
  this.notify();
167
179
  }
168
180
 
169
- addRun(agent: string, task: string, model?: string): number {
181
+ addRun(agent: string, task: string, model?: string, thinking?: string, meta?: RunChainMeta): number {
170
182
  const id = this.nextId++;
171
183
  this.runs.push({
172
184
  id,
173
185
  agent,
174
186
  task,
175
187
  model,
188
+ thinking,
176
189
  status: "queued",
177
190
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
191
+ ...(meta?.groupId ? { groupId: meta.groupId } : {}),
192
+ ...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
178
193
  });
179
194
  this.notify();
180
195
  return id;
@@ -230,7 +245,9 @@ export class MonitorStore {
230
245
  summarize(run: RunView): string {
231
246
  const usage = formatUsageCompact(run.usage);
232
247
  const parts = [run.agent];
248
+ if (run.relationLabel) parts.push(run.relationLabel);
233
249
  if (run.model) parts.push(run.model);
250
+ if (run.thinking) parts.push(`thinking ${run.thinking}`);
234
251
  if (usage) parts.push(usage);
235
252
  const elapsed = formatElapsed(run);
236
253
  if (elapsed) parts.push(elapsed);
package/src/prompt.ts CHANGED
@@ -16,8 +16,8 @@ import { formatCatalogEntry } from "./agents.ts";
16
16
 
17
17
  /** Compact role routing hints, emitted only for roles that are enabled. */
18
18
  const ROLE_ROUTING: Record<string, string> = {
19
- explore: "explore — broad/open-ended code search, \"where is X\", multi-file lookups (read-only, cheap).",
20
- worker: "worker — implement/fix/refactor/test a well-scoped task (full tools; plans internally).",
19
+ explore: "explore — broad/open-ended code search, multi-file lookups (read-only, cheap); NOT for one-line lookups.",
20
+ worker: "worker — implement/fix/refactor/test a self-contained task worth a separate context (full tools; plans internally).",
21
21
  reviewer: "reviewer — adversarial pre-commit review of a diff (read-only; independent context).",
22
22
  };
23
23
 
@@ -45,8 +45,9 @@ Available agents:
45
45
  ${catalog}
46
46
 
47
47
  ${routing ? `Routing:\n${routing}\n` : ""}Dispatch discipline:
48
- - Default to delegating every discrete task to a sub-agent; use the automatically delivered findings for orchestration and verification.
49
- - Only handle inline: pure Q&A, a single trivial edit/lookup, or when the user explicitly says to do it directly. When in doubt, delegate.
48
+ - Handle SIMPLE work INLINE with direct tools: a single lookup, one-line edit, or a quick question is a grep/read/edit in the main context never a sub-agent. Sub-agents cost startup time, tokens, and a context switch.
49
+ - Delegate only when isolation genuinely pays: broad exploration of an unfamiliar area, a self-contained implementation/fix with its own validation, or a fresh-context review gate.
50
+ - When in doubt, start with a direct tool call in the main context; escalate to a sub-agent only if the work turns out broad.
50
51
  - For an already-known or trivial target, use a direct search/read tool (e.g. grep/find/read) — do not over-delegate a one-line lookup.
51
52
  ${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Let the automatically resumed main agent launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
52
53
  - Treat delegated agents as leaf workers: do not ask a sub-agent to dispatch another sub-agent; child processes do not have this tool.