@ferris1225/pi-subagents 2.1.0 → 2.2.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/README.md CHANGED
@@ -32,15 +32,23 @@ The common quality loop now runs end to end without waking the main agent betwee
32
32
  steps:
33
33
 
34
34
  ```text
35
- reviewer (find blockers) → worker (fix) → reviewer (verify) → final PASS/FAIL
35
+ reviewer (find issues) → worker (fix every finding) → reviewer (verify) → final PASS/FAIL
36
36
  ```
37
37
 
38
+ Reviews use a single flat findings list — no severity triage. Every reported
39
+ finding is fixed before the change is accepted, and each re-review converges on
40
+ an open-finding set: the worker's explicit rejections are adjudicated once, only
41
+ defects a fix round introduced or exposed are added, and resolved items never
42
+ re-open. `maxFixRounds` stays the hard cap, so a chain always settles and wakes
43
+ the main agent with the full picture.
44
+
38
45
  Cleanup stays a separate lifecycle: explicit cleanup intent can dispatch the
39
46
  evidence-first `cleaner`; any edits still go through the independent `reviewer`
40
47
  gate.
41
48
 
42
- Each chain is delivered as one concise completion group, while full per-run
43
- reports remain available through `subagent_status`. Its parent stays `running`
49
+ Each chain is delivered as one concise completion group whose footer totals the
50
+ aggregate token usage and cost of every included run, while full per-run reports
51
+ remain available through `subagent_status`. Its parent stays `running`
44
52
  until the whole chain settles; completed internal rounds leave active status
45
53
  immediately, so no `done` row keeps accumulating elapsed time. Selected-to-main
46
54
  model handoffs keep the same retained context, and isolated parallel workers use
@@ -63,12 +71,16 @@ index.
63
71
  "go check" step. `subagent_wait` is a **non-blocking** in-turn lookup by default
64
72
  (pass `timeoutMs` to block); `subagent_status` inspects runs; `subagent_stop`
65
73
  cancels one and delivers its partial output.
66
- - **Active-only live widget** — each queued or running sub-agent gets one compact
67
- width-aware primary line with task, effective model/thinking, and elapsed time;
68
- current activity appears only when present on an indented second line:
74
+ - **Active-only live widget, as a tree** — each queued or running sub-agent gets one compact
75
+ width-aware primary line with task, effective model/thinking, and elapsed time; current
76
+ activity appears only when present on an indented second line. Auto-fix rounds nest under
77
+ the triggering reviewer row that owns the chain, so it is always visible who dispatched
78
+ what; no run ids appear here — the tree and the task label identify each row:
69
79
  ```text
70
- #7 cleaner · remove dead code in src/cache.ts · claude-sonnet-4-5/high · 42s
71
- grep cacheKey
80
+ reviewer · review diff of src/foo.ts · claude-sonnet-4-5/high · 42s
81
+ ● worker · fix round 1 · src/foo.ts · claude-sonnet-4-5/high · 10s
82
+ │ grep cacheKey
83
+ └ ○ reviewer · re-review round 1 · claude-sonnet-4-5/high · 3s
72
84
  ```
73
85
  Long tasks and activity paths truncate first (preserving a useful path tail when
74
86
  possible), groups have no blank rows, and settled/parked runs disappear immediately.
@@ -83,7 +95,10 @@ index.
83
95
  - **A quality gate that closes the loop** — when a reviewer returns `REVIEW_FAIL`,
84
96
  the extension dispatches a worker briefed with the concrete findings, then a
85
97
  re-review, up to `maxFixRounds` times — and only then wakes the main agent.
86
- Every round stays in the triggering reviewer's cwd, and chains that target the
98
+ Every reported finding gets fixed (no severity triage), and re-reviews converge
99
+ on an open-finding set instead of ping-ponging: worker rejections are adjudicated
100
+ once, only defects the fix round introduced are added, and resolved items never
101
+ re-open. Every round stays in the triggering reviewer's cwd, and chains that target the
87
102
  same repository are serialized so shared-checkout edits cannot race.
88
103
  - **Direct fallback with real thinking capabilities** — each agent has at most
89
104
  one selected model. An unavailable selection, rate limit, invalid key, quota,
@@ -98,9 +113,9 @@ index.
98
113
  `subagent_control` can steer active work, retarget it after a stable abort,
99
114
  park/resume it under the same run id, or fork a parked/settled checkpoint into
100
115
  a new independent run. Concurrent resume calls are serialized.
101
- - **Concise but honest completions** — a clean process that contained failed tool
102
- calls adds one short warning with its run id; individual errors stay out of the
103
- main context and remain available through `subagent_status`. Actual process,
116
+ - **Concise but honest completions** — group completions end with aggregate token
117
+ and cost totals across every included run; failed-tool diagnostics stay out of the
118
+ delivered message and remain one `subagent_status` call away. Actual process,
104
119
  model, and integration failures still surface as failures.
105
120
  - **Parallel fan-out with filesystem isolation** — independent tasks run up to a
106
121
  configurable limit (default 4). Parallel workers default to detached Git
@@ -123,8 +138,9 @@ an undifferentiated child-agent launcher:
123
138
  independent reviewer gate;
124
139
  - isolated, retained threads that can be steered, parked, resumed, retargeted, or
125
140
  forked under stable run ids;
126
- - the reviewer → worker auto-fix → reviewer loop;
127
- - compact failed-tool warnings with full diagnostics available by run id;
141
+ - the reviewer → worker auto-fix → reviewer loop, fixing every finding under a
142
+ convergence contract with a hard round cap;
143
+ - failed-tool diagnostics available by run id through `subagent_status`;
128
144
  - direct selected→main fallback plus capability-aware Auto thinking;
129
145
  - detached Git worktree isolation for parallel workers and opt-in write-capable
130
146
  cleaner runs.
@@ -226,7 +242,8 @@ because they may need to inspect the same images.
226
242
  ### Controlling and stopping
227
243
 
228
244
  Dispatch confirmations, tool result rows, and completion blocks all show the
229
- stable `#id`, so a thread remains directly controllable after its live UI is gone.
245
+ stable `#id`, so a thread remains directly controllable after its live UI is gone
246
+ (the widget itself identifies rows by tree position and task instead of ids).
230
247
 
231
248
  - `subagent_control` — `steer`, `retarget`, `park`, `resume`, or `fork` a logical
232
249
  thread by stable run id. Resume accepts an optional replacement objective;
@@ -317,7 +334,7 @@ direct-file settings.
317
334
  | `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
318
335
  | `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
319
336
  | `maxConcurrency` | Max sub-agent processes running at once (1–16, default 4), and the max tasks one parallel `subagent` call accepts. Extra work waits in the queue. |
320
- | `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). |
337
+ | `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). Hard cap: the chain always settles, delivers its condensed summary, and wakes the main agent. |
321
338
  | `idleTimeoutSec` | Idle watchdog: a sub-agent whose stdout goes silent for this long is terminated; a selected model then hands to current main. `0` disables it. Default 90. |
322
339
 
323
340
  ### Model routing and thinking
@@ -42,25 +42,23 @@ Understand the context first, then verify: the fix addresses the root cause, cha
42
42
  - Concurrency: shared mutable state, locks held across await, races.
43
43
  - Encoding/Unicode: assuming `char*`/files/CLI text is UTF-8; wrong `A` vs `W` Win32 APIs; boundary conversions.
44
44
  - Resource leaks; violations of the project's stated conventions.
45
- - Classify severity honestly. Distinguish blockers from nits; do not pad with style preferences.
45
+ - Every finding you report gets fixed by the auto-fix loop — there are no severity tiers and no optional nits. Report genuine defects and risks only, each defensible with file:line evidence; leave out style preferences and anything you would merely "suggest". Order findings most important first.
46
46
 
47
47
  ## Collaboration
48
48
  - Independent of `worker` by design — your verdict is the gate before commit. Fix nothing yourself; report so the caller can dispatch a worker.
49
+ - Re-reviews converge on an open-finding set: rule on each previously reported finding once (adjudicating the worker's explicit rejections — uphold only with a concrete refutation), add only defects the fix round introduced or exposed, and never re-open an item you verified resolved. Rounds are hard-capped, so padding a re-review with restated findings just burns them.
49
50
 
50
51
  ## Output format
51
52
  ## Files Reviewed
52
53
  - `path/to/file.ts`
53
- ## Critical (must fix)
54
+ ## Findings
54
55
  - `file.ts:42` — concrete issue and why it breaks.
55
- ## Warnings (should fix)
56
- - `file.ts:10` — issue and suggested direction.
57
- ## Suggestions (consider)
58
- - Optional improvements.
56
+ (Write "None" when you found nothing; an empty findings list is a valid, honest result.)
59
57
  ## Verdict
60
- One of: APPROVE / APPROVE_WITH_NITS / REQUEST_CHANGES, plus a 2-3 sentence rationale.
61
- End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE or APPROVE_WITH_NITS; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
58
+ One of: APPROVE / REQUEST_CHANGES, plus a 2-3 sentence rationale.
59
+ End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
62
60
 
63
- REQUEST_CHANGES (or review-blocking concern) on this extension automatically starts an auto-fix loop: a worker is briefed with your findings, then you re-review, up to the configured round limit. Choose REVIEW_PASS when nothing objectively blocks the change (style preferences, optional refactors, or items the caller can knowingly accept are not blockers) do not force a verdict that would trigger another loop just to hedge.
61
+ REQUEST_CHANGES whenever even one finding remains. On this extension REQUEST_CHANGES automatically starts an auto-fix loop: a worker is briefed with your findings, fixes every one of them, and you re-review, up to the configured round limit. APPROVE means the findings list is empty or, on a re-review, that every previously reported finding is resolved. Never quietly wave an issue through, and never invent findings to hedge: report each genuine issue once, accurately.
64
62
 
65
63
  ## Quality standards
66
64
  Specific file paths and line numbers. No vague feedback. A clean report means you looked hard, not that you found nothing to say.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Controllable background sub-agent threads for pi: specialized roles, capability-aware thinking, direct main-model fallback, auto-fix chains, and Git worktree isolation.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/completion.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  */
9
9
 
10
10
  import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
11
+ import { formatUsageCompact, sumUsage } from "./monitor.ts";
12
+ import type { UsageStats } from "./rpc-run.ts";
11
13
 
12
14
  export interface CompletionBatchTimings {
13
15
  debounceMs: number;
@@ -98,14 +100,20 @@ export interface CompletionMessageItem {
98
100
  agent: string;
99
101
  block: string;
100
102
  triggerTurn: boolean;
103
+ /** Final usage of the underlying run (or chain); aggregated into the group totals. */
104
+ usage?: UsageStats;
101
105
  }
102
106
 
103
- /** Keep the established single-result shape; add a summary only for real groups. */
107
+ /** Keep the established single-result shape; add a group header and an aggregate
108
+ * token/cost footer only for real groups. */
104
109
  export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
105
110
  if (items.length === 0) return "";
106
111
  if (items.length === 1) return items[0].block;
107
112
  const agents = items.map((item) => item.agent).join(", ");
108
- return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}`;
113
+ const withUsage = items.filter((item) => item.usage !== undefined);
114
+ const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
115
+ const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
116
+ return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
109
117
  }
110
118
 
111
119
  /** A grouped completion wakes the main agent when any member requires a turn. */
package/src/dispatch.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  formatToolActivity,
35
35
  monitor,
36
36
  statusIcon,
37
+ sumUsage,
37
38
  type RunChainMeta,
38
39
  } from "./monitor.ts";
39
40
  import type { SubagentRuntime } from "./runtime.ts";
@@ -423,6 +424,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
423
424
  const workerStep = await launchInLoop("worker", fixBrief, executionCwd, signal, {
424
425
  groupId: parentGroupId,
425
426
  relationLabel: `fix round ${round}`,
427
+ parentRunId,
426
428
  }, vision);
427
429
  // Preserve the newest sub-step before checking chain ownership. A
428
430
  // destructive stop invalidates ownsParent() while this child is
@@ -442,10 +444,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
442
444
  if (!ownsParent()) return;
443
445
  chain.push({ ...workerStep, relation: `fix round ${round}` });
444
446
  if (!runtime.sessionActive || isFailedResult(workerStep.result)) break;
445
- const reReviewBrief = buildReReviewBrief(lastReviewer, round);
447
+ const reReviewBrief = buildReReviewBrief(lastReviewer, round, workerStep.result);
446
448
  const reviewStep = await launchInLoop("reviewer", reReviewBrief, executionCwd, signal, {
447
449
  groupId: parentGroupId,
448
450
  relationLabel: `re-review round ${round}`,
451
+ parentRunId,
449
452
  }, vision);
450
453
  if (
451
454
  runtime.threads.get(parentRunId) === parentThreadAtStart &&
@@ -527,6 +530,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
527
530
  agent: `auto-fix chain (${last.result.agent})`,
528
531
  block,
529
532
  triggerTurn: true,
533
+ usage: sumUsage(chain.map((step) => step.result.usage)),
530
534
  },
531
535
  ]);
532
536
  runtime.completionBatcher.flush();
@@ -570,6 +574,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
570
574
  agent: initialReviewerResult.agent,
571
575
  block: `${formatCompletionBlock(initialReviewerResult, config.maxResultLines, executionCwd)}\n\nAuto-fix chain crashed before completion: ${errorMessage}. The planned fix rounds did not run; the review above is the triggering reviewer's full output.`,
572
576
  triggerTurn: true,
577
+ usage: initialReviewerResult.usage,
573
578
  },
574
579
  ]);
575
580
  runtime.completionBatcher.flush();
package/src/fixloop.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
15
- import { extractKeyFragments, formatUsageCompact } from "./monitor.ts";
15
+ import { extractKeyFragments, formatUsageCompact, sumUsage } from "./monitor.ts";
16
16
  import type { SubagentsConfig } from "./config.ts";
17
17
 
18
18
  /**
@@ -40,7 +40,7 @@ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConf
40
40
  /**
41
41
  * Build the worker task brief for one fix round from a reviewer's findings.
42
42
  * The worker gets the full review text so it can address concrete file:line
43
- * issues, with instructions to fix only blockers and self-verify.
43
+ * issues, with instructions to fix every reported finding and self-verify.
44
44
  */
45
45
  export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
46
46
  const review = getResultOutput(reviewerResult);
@@ -53,8 +53,9 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
53
53
  review,
54
54
  `---`,
55
55
  ``,
56
- `Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
57
- `Address every "Critical" item; address "Warnings" only if they are genuine.`,
56
+ `Fix EVERY finding in the reviewer's findings list there is no severity triage; all of them get fixed.`,
57
+ `If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
58
+ `Do NOT refactor unrelated code beyond what the findings require.`,
58
59
  `After editing, run the project's format/build/tests when they exist and report`,
59
60
  `exactly what you changed (paths + short rationale) so a reviewer can verify.`,
60
61
  remaining > 0
@@ -120,18 +121,7 @@ export function formatChainSummary(steps: readonly ChainStep[]): string {
120
121
  const id = step.runId !== undefined ? `#${step.runId} ` : "";
121
122
  lines.push(`- ${id}${step.result.agent} · ${step.relation} · ${stepStatus(step)}${suffix}`);
122
123
  }
123
- const total = steps.reduce(
124
- (acc, step) => {
125
- acc.input += step.result.usage.input;
126
- acc.output += step.result.usage.output;
127
- acc.cacheRead += step.result.usage.cacheRead;
128
- acc.cacheWrite += step.result.usage.cacheWrite;
129
- acc.cost += step.result.usage.cost;
130
- acc.turns += step.result.usage.turns;
131
- return acc;
132
- },
133
- { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
134
- );
124
+ const total = sumUsage(steps.map((step) => step.result.usage));
135
125
  const usage = formatUsageCompact(total);
136
126
  lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
137
127
  const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
@@ -141,11 +131,14 @@ export function formatChainSummary(steps: readonly ChainStep[]): string {
141
131
 
142
132
  /**
143
133
  * The re-review brief handed to the reviewer after a worker fix round. Includes
144
- * the prior review so the reviewer can verify the fixes without re-discovering
145
- * the original issues.
134
+ * the prior review AND the worker's report so the reviewer can adjudicate
135
+ * rejections instead of restating findings. The convergence contract keeps
136
+ * rounds from ping-ponging: rule on the open findings once, add only defects
137
+ * this round's edits introduced, never re-open a verified resolution.
146
138
  */
147
- export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
139
+ export function buildReReviewBrief(reviewerResult: SingleResult, round: number, workerResult: SingleResult): string {
148
140
  const review = getResultOutput(reviewerResult);
141
+ const workerReport = getResultOutput(workerResult);
149
142
  return [
150
143
  `Re-review after auto-fix round ${round}.`,
151
144
  ``,
@@ -154,8 +147,18 @@ export function buildReReviewBrief(reviewerResult: SingleResult, round: number):
154
147
  review,
155
148
  `---`,
156
149
  ``,
157
- `Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
158
- `Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
150
+ `The worker's report (what it changed, plus any finding it rejected as factually wrong or out of scope):`,
151
+ `---`,
152
+ workerReport,
153
+ `---`,
154
+ ``,
155
+ `Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
156
+ `adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
157
+ `never simply restate the finding for another round.`,
158
+ `Run \`git diff\` to see what changed, then add NEW findings only when they are defects this round's`,
159
+ `edits introduced or exposed (or a load-bearing issue the earlier review genuinely missed).`,
160
+ `Do NOT re-open a finding you verified as resolved.`,
161
+ `REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
159
162
  `End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
160
163
  ].join("\n");
161
164
  }
package/src/format.ts CHANGED
@@ -127,18 +127,15 @@ export function formatCompletionBlock(
127
127
  lines.push(`Relation: ${relations.join(" · ")}`, "");
128
128
  }
129
129
  lines.push(text);
130
- // Explicit status always exposes every retained diagnostic, including when
131
- // the overall run failed or was aborted. Automatic delivery adds only a
132
- // compact pointer for otherwise-clean runs.
130
+ // Failed-tool diagnostics are deliberate opt-in via subagent_status: agents
131
+ // report their own verification in the output above, and a transient failed
132
+ // call (no-match grep, rejected edit) is noise in an automatic delivery.
133
133
  if (options.failedToolDetails && failedTools.length > 0) {
134
134
  lines.push(
135
135
  "",
136
136
  `⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}:`,
137
137
  ...failedTools.map((tool) => `- ${tool.toolName}: ${tool.error.trim() || "(no output)"}`),
138
138
  );
139
- } else if (!failed && failedTools.length > 0) {
140
- const lookup = result.runId !== undefined ? ` · details: subagent_status #${result.runId}` : "";
141
- lines.push("", `⚠ ${failedTools.length} failed tool call${failedTools.length === 1 ? "" : "s"}${lookup}`);
142
139
  }
143
140
  if (truncated) {
144
141
  // The full text lives on disk so the main agent can read it on demand.
package/src/monitor.ts CHANGED
@@ -53,12 +53,15 @@ export interface RunView {
53
53
  groupId?: string;
54
54
  /** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
55
55
  relationLabel?: string;
56
+ /** Owning run for chain children: the triggering reviewer whose row represents the chain. */
57
+ parentRunId?: number;
56
58
  }
57
59
 
58
60
  /** Optional chain metadata for runs spawned by an auto-fix loop. */
59
61
  export interface RunChainMeta {
60
62
  groupId?: string;
61
63
  relationLabel?: string;
64
+ parentRunId?: number;
62
65
  isolation?: IsolationMode;
63
66
  forkedFromRunId?: number;
64
67
  }
@@ -248,6 +251,21 @@ export function formatUsageCompact(usage: UsageStats): string {
248
251
  return parts.join(" ");
249
252
  }
250
253
 
254
+ /** Aggregate usage across several runs (chain steps or a completion group). */
255
+ export function sumUsage(parts: readonly UsageStats[]): UsageStats {
256
+ const total: UsageStats = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
257
+ for (const part of parts) {
258
+ total.input += part.input;
259
+ total.output += part.output;
260
+ total.cacheRead += part.cacheRead;
261
+ total.cacheWrite += part.cacheWrite;
262
+ total.cost += part.cost;
263
+ total.contextTokens += part.contextTokens;
264
+ total.turns += part.turns;
265
+ }
266
+ return total;
267
+ }
268
+
251
269
  export function formatDuration(ms: number): string {
252
270
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
253
271
  if (totalSeconds < 60) return `${totalSeconds}s`;
@@ -393,6 +411,7 @@ export class MonitorStore {
393
411
  usage: emptyUsage(),
394
412
  ...(meta?.groupId ? { groupId: meta.groupId } : {}),
395
413
  ...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
414
+ ...(meta?.parentRunId !== undefined ? { parentRunId: meta.parentRunId } : {}),
396
415
  ...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
397
416
  ...(meta?.forkedFromRunId !== undefined ? { forkedFromRunId: meta.forkedFromRunId } : {}),
398
417
  });
package/src/prompt.ts CHANGED
@@ -78,7 +78,7 @@ Result handoff (do not re-state):
78
78
 
79
79
  Review & verification:
80
80
  - Never report an unrun check as passed; report it as unavailable or as a pre-existing failure.
81
- ${hasReviewer ? `- For non-trivial diffs${hasCleaner ? " (including cleaner edits)" : ""}, run one fresh read-only \`reviewer\` sub-agent before reporting done. Fix only concrete blockers and re-review at most once.
81
+ ${hasReviewer ? `- For non-trivial diffs${hasCleaner ? " (including cleaner edits)" : ""}, run one fresh read-only \`reviewer\` sub-agent before reporting done. Fix every finding the reviewer reports and re-review at most once.
82
82
  - Use multi-model cross-review only when explicitly requested or for genuinely high-risk changes (security, unsafe/FFI, persistence-migration, concurrency). Reviewers are read-only; only the main agent edits.
83
- ` : ""}- Commit or push only when explicitly requested, applicable checks pass, and no accepted blockers remain.`;
83
+ ` : ""}- Commit or push only when explicitly requested, applicable checks pass, and no unresolved review findings remain.`;
84
84
  }
@@ -966,6 +966,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
966
966
  ? `${formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd)}\n\n${modelLevelTakeoverNote(result, { runId })}`
967
967
  : formatCompletionBlock(result, runConfig.maxResultLines, result.projectCwd ?? originalCwd),
968
968
  triggerTurn: completionTriggersTurn(result, runConfig.notifyOnReviewPass),
969
+ usage: result.usage,
969
970
  };
970
971
  if (modelLevel) {
971
972
  const detail = result.errorMessage?.trim() || "model unavailable or broken";
@@ -1040,6 +1041,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
1040
1041
  agent: agent.name,
1041
1042
  block: formatCompletionBlock(crashed, runConfig.maxResultLines, crashed.projectCwd ?? originalCwd),
1042
1043
  triggerTurn: true,
1044
+ usage: crashed.usage,
1043
1045
  },
1044
1046
  ]);
1045
1047
  runtime.completionBatcher.flush();
package/src/tools.ts CHANGED
@@ -695,6 +695,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
695
695
  agent: result.agent,
696
696
  block: formatCompletionBlock(result, maxResultLines, result.projectCwd ?? ctx.cwd),
697
697
  triggerTurn: true,
698
+ usage: result.usage,
698
699
  })));
699
700
  runtime.completionBatcher.flush();
700
701
  }
package/src/widget.ts CHANGED
@@ -29,65 +29,107 @@ function compactLine(left: string, right: string, width: number): string {
29
29
  * activity line. The primary line reserves effective model/thinking and elapsed
30
30
  * width before truncating the task. Settled and parked threads never appear, so
31
31
  * elapsed time cannot keep ticking beside a terminal status. */
32
+ function runPrimaryLine(
33
+ run: RunView,
34
+ theme: Theme,
35
+ width: number,
36
+ now: number,
37
+ prefix: string,
38
+ ): string {
39
+ const dim = (text: string): string => theme.fg("dim", text);
40
+ const icon = statusIcon(run.status, theme);
41
+ const name = theme.fg("accent", theme.bold(run.agent));
42
+ const identity = `${prefix}${icon} ${name}`;
43
+ const elapsed = formatElapsed(run, now);
44
+ // Render only the resolved model id plus thinking level. Provider auth and
45
+ // other configuration never enter monitor state or this line.
46
+ const modelId = run.model?.split("/").at(-1);
47
+ const modelSource = formatTaskSummary(
48
+ modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
49
+ 64,
50
+ false,
51
+ );
52
+ // A chain child shows its role in the chain plus a task-derived label; the
53
+ // templated fix brief itself would only repeat the parent review's content.
54
+ const taskSource = run.parentRunId !== undefined
55
+ ? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
56
+ : formatTaskSummary(run.task, 64);
57
+ const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
58
+ const contentWidth = Math.max(
59
+ 0,
60
+ width -
61
+ visibleWidth(identity) -
62
+ visibleWidth(elapsed) -
63
+ (primaryPartCount - 1) * visibleWidth(" · "),
64
+ );
65
+ const modelDesired = visibleWidth(modelSource);
66
+ const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
67
+ let modelWidth = modelSource
68
+ ? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
69
+ : 0;
70
+ let taskWidth = contentWidth - modelWidth;
71
+ if (visibleWidth(taskSource) < taskWidth) {
72
+ modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
73
+ taskWidth = contentWidth - modelWidth;
74
+ }
75
+ const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined) : "";
76
+ const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
77
+ const primaryLeft = [
78
+ identity,
79
+ task ? dim(task) : undefined,
80
+ modelThinking ? dim(modelThinking) : undefined,
81
+ ].filter((part): part is string => Boolean(part)).join(" · ");
82
+ return compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
83
+ }
84
+
85
+ function runActivityLine(run: RunView, theme: Theme, width: number, indent: string): string[] {
86
+ const dim = (text: string): string => theme.fg("dim", text);
87
+ const activity = run.activity?.trim();
88
+ if (!activity) return [];
89
+ const activityWidth = width - visibleWidth(indent);
90
+ if (activityWidth <= 0) return [];
91
+ const activitySummary = formatTaskSummary(activity, activityWidth);
92
+ if (!activitySummary) return [];
93
+ return [truncateToWidth(`${indent}${dim(activitySummary)}`, width, "")];
94
+ }
95
+
96
+ /** Render active runs as a tree: main-agent dispatches are roots, auto-fix chain
97
+ * rounds nest under the triggering reviewer row that owns the chain. No run ids
98
+ * appear here — the tree and the task label say what each row is, and ids stay
99
+ * available through subagent_status when a thread must be controlled. */
32
100
  export function formatActiveRunLines(
33
101
  runs: readonly RunView[],
34
102
  theme: Theme,
35
103
  width: number,
36
104
  now: number = Date.now(),
37
105
  ): string[] {
38
- const dim = (text: string): string => theme.fg("dim", text);
39
- return runs
40
- .filter((run) => isRunActiveStatus(run.status))
41
- .flatMap((run) => {
42
- const icon = statusIcon(run.status, theme);
43
- const name = theme.fg("accent", theme.bold(run.agent));
44
- const identity = `${icon} ${dim(`#${run.id}`)} ${name}`;
45
- const elapsed = formatElapsed(run, now);
46
- // Render only the resolved model id plus thinking level. Provider auth and
47
- // other configuration never enter monitor state or this line.
48
- const modelId = run.model?.split("/").at(-1);
49
- const modelSource = formatTaskSummary(
50
- modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
51
- 64,
52
- false,
53
- );
54
- const taskSource = formatTaskSummary(run.task, 64);
55
- const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
56
- const contentWidth = Math.max(
57
- 0,
58
- width -
59
- visibleWidth(identity) -
60
- visibleWidth(elapsed) -
61
- (primaryPartCount - 1) * visibleWidth(" · "),
62
- );
63
- const modelDesired = visibleWidth(modelSource);
64
- const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
65
- let modelWidth = modelSource
66
- ? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
67
- : 0;
68
- let taskWidth = contentWidth - modelWidth;
69
- if (visibleWidth(taskSource) < taskWidth) {
70
- modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
71
- taskWidth = contentWidth - modelWidth;
72
- }
73
- const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth) : "";
74
- const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
75
- const primaryLeft = [
76
- identity,
77
- task ? dim(task) : undefined,
78
- modelThinking ? dim(modelThinking) : undefined,
79
- ].filter((part): part is string => Boolean(part)).join(" · ");
80
- const primary = compactLine(primaryLeft, elapsed ? dim(`· ${elapsed}`) : "", width);
81
-
82
- const activity = run.activity?.trim();
83
- if (!activity) return [primary];
84
- const activityIndent = " ";
85
- const activityWidth = width - visibleWidth(activityIndent);
86
- if (activityWidth <= 0) return [primary];
87
- const activitySummary = formatTaskSummary(activity, activityWidth);
88
- if (!activitySummary) return [primary];
89
- return [primary, truncateToWidth(`${activityIndent}${dim(activitySummary)}`, width, "")];
106
+ const active = runs.filter((run) => isRunActiveStatus(run.status));
107
+ const activeIds = new Set(active.map((run) => run.id));
108
+ const childrenOf = new Map<number, RunView[]>();
109
+ const roots: RunView[] = [];
110
+ for (const run of active) {
111
+ if (run.parentRunId !== undefined && activeIds.has(run.parentRunId)) {
112
+ const siblings = childrenOf.get(run.parentRunId);
113
+ if (siblings) siblings.push(run);
114
+ else childrenOf.set(run.parentRunId, [run]);
115
+ } else {
116
+ roots.push(run);
117
+ }
118
+ }
119
+ const lines: string[] = [];
120
+ for (const root of roots) {
121
+ const children = childrenOf.get(root.id) ?? [];
122
+ lines.push(runPrimaryLine(root, theme, width, now, ""));
123
+ // The parent's "auto-fix chain running" placeholder is redundant while its
124
+ // child rows show live progress; keep it only between rounds.
125
+ if (children.length === 0) lines.push(...runActivityLine(root, theme, width, " "));
126
+ children.forEach((child, index) => {
127
+ const connector = index === children.length - 1 ? "└ " : "├ ";
128
+ lines.push(runPrimaryLine(child, theme, width, now, theme.fg("dim", ` ${connector}`)));
129
+ lines.push(...runActivityLine(child, theme, width, " "));
90
130
  });
131
+ }
132
+ return lines;
91
133
  }
92
134
 
93
135
  function hasTickingRun(): boolean {