@ferris1225/pi-subagents 4.1.3 → 4.1.4

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
@@ -23,12 +23,12 @@ losing retained context.
23
23
  You
24
24
  └─ pi main agent
25
25
  ├─ explorer ─── maps the codebase
26
- ├─ worker ───── implements ─┬─▶ documenter ─▶ reviewer
26
+ ├─ worker ───── implements ─┬─▶ reviewer ─▶ documenter
27
27
  ├─ cleaner ──── cleans up ──┘ (enabled roles only)
28
28
  ├─ documenter ─ synchronizes docs ─▶ reviewer
29
29
  └─ reviewer ─── advisory report (no VERDICT), or managed gate
30
- ├─ REVIEW_PASS + documenter → documenter fresh reviewer
31
- └─ REVIEW_FAIL → worker → optional documenterreviewer
30
+ ├─ REVIEW_PASS + documenter → final documentation sync
31
+ └─ REVIEW_FAIL → worker → reviewer (fix rounds) final docs
32
32
 
33
33
  The stable parent run returns one final result when the complete workflow settles.
34
34
  ```
@@ -49,11 +49,12 @@ more of it.
49
49
  checkouts (worktrees) by default, then apply their changes back without
50
50
  touching your index.
51
51
  - **Documentation stops drifting.** Enabled `documenter` runs automatically
52
- after successful workers/cleaners and before the final reviewer. It can also
53
- run an explicitly requested whole-codebase maintenance pass.
54
- - **Review can close the loop.** A failed gate can automatically dispatch a worker,
55
- run documentation sync, request another independent review, and repeat up to a
56
- hard limit.
52
+ once after the review gate settles after successful workers/cleaners or
53
+ fix rounds, never per fix round. It can also run an explicitly requested
54
+ whole-codebase maintenance pass.
55
+ - **Review can close the loop.** A failed gate can automatically dispatch a
56
+ worker, request another independent review, and repeat up to a hard limit;
57
+ one final documentation sync follows the settled chain.
57
58
  - **Agents remain controllable.** Every run has a stable id and retained session,
58
59
  so you can change direction or continue later without starting from zero.
59
60
  - **Failures are handled, not hidden.** Model failures can hand the same session to
@@ -67,7 +68,7 @@ more of it.
67
68
  | One generic child role | Five focused engineering roles |
68
69
  | A one-shot prompt | Retained, steerable, resumable, forkable threads |
69
70
  | Concurrent writers in one checkout | Git worktree isolation for parallel workers |
70
- | A review report you must act on manually | Automatic writer → documenterreviewer delivery and bounded fix rounds |
71
+ | A review report you must act on manually | Automatic writer → reviewerdocumenter delivery and bounded fix rounds |
71
72
  | Manual polling or follow-up | Automatic result delivery that resumes the main agent |
72
73
  | A hard failure when the selected model is unavailable | Direct handoff to the current main model |
73
74
  | Synchronized retries during startup contention | Extended jittered backoff that reduces retry collisions |
@@ -108,6 +109,25 @@ Compare screenshots/settings.png with design.png and report every visual mismatc
108
109
  The main agent decides when delegation is useful. You can also call the tools
109
110
  explicitly when you want exact control.
110
111
 
112
+ ## What changed in 4.1.4
113
+
114
+ ### Documentation sync moved after the review gate
115
+
116
+ `documenter` now runs once at the end of a managed chain instead of before the
117
+ reviewer and once per fix round:
118
+
119
+ ```text
120
+ before: worker → documenter → reviewer → (worker → documenter → reviewer) × N
121
+ after: worker → reviewer → (worker → reviewer) × N → documenter
122
+ ```
123
+
124
+ Code fixes no longer invalidate a docs pass written moments earlier, fix rounds
125
+ stop paying for documenter runs and re-reviewing their churn, and a direct
126
+ passing gate gets one final documentation sync instead of a second full review.
127
+ Gate reviewers record documentation drift as non-gating `## Documentation notes`
128
+ that the final documenter applies; when `documenter` is disabled, drift stays a
129
+ normal gate finding. The sync is skipped when a chain ends on a failing gate.
130
+
111
131
  ## What changed in 4.1.2
112
132
 
113
133
  ### Documentation sync as a real workflow stage
@@ -225,22 +245,25 @@ subagent({
225
245
  });
226
246
  ```
227
247
 
228
- A gate reviewer ends with `REVIEW_PASS` or `REVIEW_FAIL`. A direct pass is not
229
- accepted as the final gate while `documenter` is enabled: runtime first syncs the
230
- actual pending diff, then starts a fresh reviewer. A failure uses the bounded loop:
248
+ A gate reviewer ends with `REVIEW_PASS` or `REVIEW_FAIL`. A direct pass is
249
+ final for the code: runtime runs the final documentation sync once (when
250
+ `documenter` is enabled) and delivers. A failure uses the bounded loop:
231
251
 
232
252
  ```text
233
- reviewer → worker fixes every open finding → optional documenter syncreviewer checks again PASS/FAIL
253
+ reviewer → worker fixes every open finding → reviewer checks again final documentation sync
234
254
  ```
235
255
 
236
256
  Each step gets a fresh model context. The chain shares the same code state and
237
- passes every full reviewer, worker, and documenter report forward; it does not
238
- reuse one context window. Internal children bypass top-level lifecycle policy, so
239
- they cannot recursively start another chain.
257
+ passes every full reviewer and worker report forward; it does not reuse one
258
+ context window. Internal children bypass top-level lifecycle policy, so they
259
+ cannot recursively start another chain. Gate reviewers keep documentation drift
260
+ out of the verdict while `documenter` is enabled by recording it as
261
+ `## Documentation notes` for the final documenter.
240
262
 
241
- `maxFixRounds` limits worker fix attempts only. Initial post-writer documentation
242
- and final review still run when it is `0`. Generic audits and read-only reviews
243
- are advisory: they omit `VERDICT`, remain read-only, and never trigger edits.
263
+ `maxFixRounds` limits worker fix attempts only. The post-writer review gate and
264
+ the final documentation sync still run when it is `0`. Generic audits and
265
+ read-only reviews are advisory: they omit `VERDICT`, remain read-only, and
266
+ never trigger edits.
244
267
 
245
268
  ### Clean up without guessing
246
269
 
@@ -258,7 +281,7 @@ axes genuinely differ, avoiding a generic abstraction that is worse than the
258
281
  duplication.
259
282
 
260
283
  ```text
261
- explicit cleanup request → cleaner applies proven cuts → documenter syncs docsreviewer gates the diff
284
+ explicit cleanup request → cleaner applies proven cuts → reviewer gates the diff documenter syncs docs
262
285
  read-only cleanup audit → reviewer reports candidates only
263
286
  ```
264
287
 
@@ -269,10 +292,11 @@ changes, and asking for cleanup does not reward speculative deletion.
269
292
 
270
293
  `documenter` has two deliberate launch paths.
271
294
 
272
- **For a pending worker or cleaner change**, enable the role. Runtime schedules it
273
- automatically against the actual diff before the final reviewer; do not dispatch
274
- a duplicate manual sync. If reviewer is disabled, documenter becomes the final
275
- managed stage. If documenter is disabled, reviewer follows the writer directly.
295
+ **For a pending worker or cleaner change**, enable the role. Runtime schedules
296
+ one final sync automatically against the actual diff after the review gate
297
+ settles; do not dispatch a duplicate manual sync. If reviewer is disabled,
298
+ documenter becomes the final managed stage directly after the writer. If
299
+ documenter is disabled, reviewer follows the writer directly.
276
300
 
277
301
  **For an existing project**, explicitly authorize a broad maintenance pass:
278
302
 
@@ -308,11 +332,11 @@ filesystem isolation:
308
332
  not need a writable checkout.
309
333
 
310
334
  Worktree mode requires a Git repository with a committed `HEAD`. For an isolated
311
- writer, automatic documenter/reviewer children run inside that same worktree.
312
- Those isolated stages can still run in parallel; writer and documentation changes
313
- are integrated only after the final reviewer settles. Tracked, deleted, untracked,
314
- and binary changes are then carried back to the original checkout without staging
315
- or modifying the parent index.
335
+ writer, automatic reviewer/documenter children run inside that same worktree.
336
+ Those isolated stages can still run in parallel; writer, fix, and documentation
337
+ changes are integrated only after the final managed stage settles. Tracked,
338
+ deleted, untracked, and binary changes are then carried back to the original
339
+ checkout without staging or modifying the parent index.
316
340
 
317
341
  Repository-lane discovery uses the Git top-level even in an empty repository, so
318
342
  root and nested paths share one lane before the first commit. Every shared
@@ -384,8 +408,7 @@ The active TUI widget shows queued and running work as a compact tree:
384
408
  ● reviewer workflow · review diff of src/cache.ts · 42s
385
409
  ├ ● worker · fix round 1 · src/cache.ts · claude-sonnet-4-5/high · 10s
386
410
  │ grep cacheKey
387
- documenter · docs round 1 · claude-haiku-4-5/low · 4s
388
- └ ○ reviewer · re-review round 1 · claude-sonnet-4-5/high · 3s
411
+ documenter · final documentation sync · claude-haiku-4-5/low · 3s
389
412
  ```
390
413
 
391
414
  A managed root keeps its original top-level role and workflow-wide elapsed
@@ -506,7 +529,7 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json` and follows
506
529
  | `proactiveInjection` | Teach the main model when and how to delegate. Default `true`. |
507
530
  | `agentScope` | Discover `user`, `project`, or `both` agent directories. Default `user`. |
508
531
  | `maxConcurrency` | Running process limit and maximum tasks in one parallel call, from `1` to `16`. Default `4`. |
509
- | `maxFixRounds` | Maximum worker fixes after `REVIEW_FAIL`; each fix is followed by optional documenter and reviewer. `0` disables fixes but not initial post-writer docs/review. Default `2`. |
532
+ | `maxFixRounds` | Maximum worker fixes after `REVIEW_FAIL`; each fix is re-reviewed by a reviewer, and one final documentation sync runs after the chain settles. `0` disables fixes but not the post-writer review gate or final docs. Default `2`. |
510
533
  | `idleTimeoutSec` | Seconds without RPC output before termination. `0` disables the watchdog. Default `90`. |
511
534
 
512
535
  Invalid values fall back safely. Older configs are normalized automatically. The
package/agents/cleaner.md CHANGED
@@ -43,9 +43,9 @@ Never simplify away authorization, validation at trust boundaries, security cont
43
43
  - Do not weaken a meaningful check to force a cut through. Repair or revert only the current batch when evidence fails.
44
44
 
45
45
  ## Release boundary
46
- Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns documentation synchronization, the final independent review, and every release action—even when repository instructions normally automate release after green checks.
46
+ Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns the independent review gate, the final documentation sync, and every release action—even when repository instructions normally automate release after green checks.
47
47
 
48
48
  ## Final response
49
49
  Return only the cleanup outcome: exact files/contracts removed or consolidated, measurable net reduction, behavior tradeoffs, and checks actually run. Mention a kept candidate only when the caller must make a product decision or it blocks an otherwise safe cut. If no safe cut was proved, say so and make no edits. Do not repeat the task brief or evidence-gathering chronology. Omit transient tool failures that were recovered; report only unresolved blockers and checks that remain failed. Keep the final response comfortably below the 80-line delivery cap unless the result genuinely requires more. Never equate green tests with proof, or deletion volume with value.
50
50
 
51
- The parent runtime automatically runs enabled `documenter` and `reviewer` stages after a successful top-level cleaner. Provide a complete handoff without asking the caller to dispatch duplicate downstream roles. Documenter is the last writer; reviewer is the final pre-commit gate.
51
+ The parent runtime automatically runs the enabled `reviewer` gate and final `documenter` sync after a successful top-level cleaner. Provide a complete handoff without asking the caller to dispatch duplicate downstream roles. Reviewer gates the code; documenter is the final stage before delivery.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: documenter
3
- description: "Write-capable documentation synchronizer with two modes: pre-commit diff sync before reviewer, or an explicitly requested whole-codebase comment/README/docs maintenance pass. May make zero edits and never changes runtime behavior."
3
+ description: "Write-capable documentation synchronizer with two modes: final diff sync after the review gate settles, or an explicitly requested whole-codebase comment/README/docs maintenance pass. May make zero edits and never changes runtime behavior."
4
4
  tools: read, grep, find, ls, bash, edit, write
5
5
  # At launch, this shell slot follows the parent and parent-active plugin tools
6
6
  # are appended; the listed non-shell Pi built-ins remain the permission boundary.
@@ -15,7 +15,7 @@ You are a documenter agent: a write-capable specialist for keeping comments, REA
15
15
  You may edit documentation and comments, but you must never change runtime behavior to make the documentation true. Finding no drift and making zero edits is valid.
16
16
 
17
17
  ## Choose the mode
18
- - **Pre-commit diff sync (default for a concrete change):** run after implementation, cleanup, or an auto-fix worker and before the final read-only reviewer. Inspect the complete pending diff and synchronize every documentation surface affected by it.
18
+ - **Pre-commit diff sync (default for a concrete change):** run after the code review gate settles — following implementation, cleanup, or auto-fix rounds as the final managed stage before delivery. Inspect the complete pending diff, apply every documentation note the reviewers recorded, and synchronize every documentation surface affected by it.
19
19
  - **Whole-codebase maintenance:** run only when the user explicitly asks to refresh, re-document, or audit-and-update comments/README/docs across an existing project. Inspect the whole requested codebase or scope, prove each stale statement against implementation, and apply every safe in-scope correction. Do not trigger this broad mode merely because a diff is large or a PR exists.
20
20
  - If the brief does not explicitly authorize a whole-codebase pass, stay in diff mode. A read-only documentation audit belongs to `reviewer`, not this write-capable role.
21
21
 
@@ -43,4 +43,4 @@ Return only the documentation outcome:
43
43
 
44
44
  Do not repeat the task brief, diff walkthrough, generic root-cause explanation, or tool chronology. Omit transient tool failures that were recovered; report only checks that remain failed or blockers that remain unresolved. Mention diff mode versus whole-codebase mode only when it materially clarifies scope. Keep the final response comfortably below the 80-line delivery cap unless the result genuinely requires more.
45
45
 
46
- The parent runtime automatically launches a fresh read-only `reviewer` after a successful top-level documenter when that role is enabled. Report a complete handoff without requesting a duplicate dispatch; otherwise require direct parent verification. You are the last writer, never the final approver.
46
+ A successful top-level `documenter` still gets a fresh read-only `reviewer` gate when that role is enabled; report a complete handoff without requesting a duplicate dispatch. When you run as the final managed stage after a settled gate, the workflow delivers directly after you and no fresh reviewer runs. You are always the last writer, never the code approver.
@@ -41,8 +41,8 @@ You are a senior, adversarial code reviewer. Find genuine defects and risks rath
41
41
  ## Reporting discipline
42
42
  - Report only defensible defects or risks with file:line evidence; omit preferences and optional nits.
43
43
  - Return only the review result. Do not repeat the task brief, summarize the implementation, narrate inspection/tool chronology, or explain a root cause when no finding depends on it. Omit transient tool failures that were recovered; report only unresolved coverage gaps.
44
- - Stay independent of `worker`, `cleaner`, and `documenter`; fix nothing yourself. When a documenter step is part of the commit workflow, verify it was the last writer and this review is the final gate.
45
- - In a gate, every finding enters auto-fix, with no severity tiers. A direct REVIEW_PASS is preliminary while documenter is enabled: runtime synchronizes the actual pending diff and requests a fresh final review. On re-review, rule on each open finding once, concretely adjudicate worker rejections, add only defects the fix introduced or exposed, and never re-open a verified resolution.
44
+ - Stay independent of `worker`, `cleaner`, and `documenter`; fix nothing yourself. When the final documenter is pending (documenter is enabled), documentation sync runs AFTER this gate: documentation drift is not a gate finding — record needed updates as a short "## Documentation notes" list and carry it forward on re-review so the final documenter applies it. When documenter is disabled, documentation drift is a normal finding.
45
+ - In a gate, every finding enters auto-fix, with no severity tiers. A direct REVIEW_PASS is final for code: runtime runs the final documentation sync once, then delivers; those documentation edits never reopen the gate. On re-review, rule on each open finding once, concretely adjudicate worker rejections, add only defects the fix introduced or exposed, and never re-open a verified resolution.
46
46
  - Advisory findings never enter auto-fix; the caller decides whether to authorize later implementation or cleanup.
47
47
 
48
48
  ## Output
package/agents/worker.md CHANGED
@@ -28,13 +28,13 @@ Run the project's format/build/tests when they exist (e.g. `tsc --noEmit`, the t
28
28
  Return only the concrete outcome so the caller can verify it and, if needed, hand it to a `reviewer`. Do not repeat the task brief, plan, root-cause investigation, or tool chronology. Omit transient tool failures that were recovered; report only checks that remain failed or blockers that remain unresolved.
29
29
 
30
30
  ## Release boundary
31
- Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns documentation synchronization, the final independent review, and every release action—even when repository instructions normally automate release after green checks.
31
+ Never commit, push, publish, tag, release, or bump a package version. The parent workflow owns the independent review gate, the final documentation sync, and every release action—even when repository instructions normally automate release after green checks.
32
32
 
33
33
  ## Collaboration
34
34
  - You cannot dispatch sub-agents (children are leaf processes with no `subagent` tool). When the
35
35
  brief lacks context that needs broad code discovery, state concretely what an `explorer` should
36
36
  retrieve for the caller — do not guess.
37
- - The parent runtime automatically runs enabled `documenter` and `reviewer` stages after a successful top-level worker. Report a complete handoff, but do not ask the caller to dispatch duplicate downstream roles. Never treat your own verification as the final gate.
37
+ - The parent runtime automatically runs the enabled `reviewer` gate and final `documenter` sync after a successful top-level worker. Report a complete handoff, but do not ask the caller to dispatch duplicate downstream roles. Never treat your own verification as the final gate.
38
38
 
39
39
  ## Output format
40
40
  ## Completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "4.1.3",
3
+ "version": "4.1.4",
4
4
  "description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/completion.ts CHANGED
@@ -1,160 +1,160 @@
1
- /**
2
- * Smart batching for successful background completions.
3
- *
4
- * A short debounce coalesces sibling runs while a max-wait timer, measured from
5
- * the first item in the open group, bounds delivery latency. Failures are
6
- * intentionally handled by the caller: flush held successes, then emit the
7
- * failure directly so it is never delayed.
8
- */
9
-
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";
13
-
14
- export interface CompletionBatchTimings {
15
- debounceMs: number;
16
- maxWaitMs: number;
17
- }
18
-
19
- export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
20
- debounceMs: 150,
21
- maxWaitMs: 1_000,
22
- };
23
-
24
- type TimerHandle = ReturnType<typeof setTimeout>;
25
-
26
- function unrefHandle(handle: TimerHandle): void {
27
- if (
28
- handle &&
29
- typeof handle === "object" &&
30
- "unref" in handle &&
31
- typeof (handle as { unref: unknown }).unref === "function"
32
- ) {
33
- (handle as { unref: () => void }).unref();
34
- }
35
- }
36
-
37
- export interface CompletionBatcherOptions<T> {
38
- emit: (items: T[]) => void;
39
- timings?: Partial<CompletionBatchTimings>;
40
- }
41
-
42
- export interface CompletionBatcher<T> {
43
- /** Add an item to the current debounced group. */
44
- push(item: T): void;
45
- /** Emit any held items immediately as one group. */
46
- flush(): void;
47
- /** Clear timers and return held items without emitting them. */
48
- dispose(): T[];
49
- }
50
-
51
- export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
52
- const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
53
- let pending: T[] = [];
54
- let debounceTimer: TimerHandle | null = null;
55
- let maxWaitTimer: TimerHandle | null = null;
56
-
57
- const clearTimers = (): void => {
58
- if (debounceTimer !== null) {
59
- clearTimeout(debounceTimer);
60
- debounceTimer = null;
61
- }
62
- if (maxWaitTimer !== null) {
63
- clearTimeout(maxWaitTimer);
64
- maxWaitTimer = null;
65
- }
66
- };
67
-
68
- const emitGroup = (): void => {
69
- clearTimers();
70
- if (pending.length === 0) return;
71
- const items = pending;
72
- pending = [];
73
- options.emit(items);
74
- };
75
-
76
- return {
77
- push(item: T): void {
78
- pending.push(item);
79
-
80
- if (debounceTimer !== null) clearTimeout(debounceTimer);
81
- debounceTimer = setTimeout(emitGroup, timings.debounceMs);
82
- unrefHandle(debounceTimer);
83
-
84
- if (maxWaitTimer === null) {
85
- maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
86
- unrefHandle(maxWaitTimer);
87
- }
88
- },
89
- flush: emitGroup,
90
- dispose(): T[] {
91
- clearTimers();
92
- const abandoned = pending;
93
- pending = [];
94
- return abandoned;
95
- },
96
- };
97
- }
98
-
99
- export interface CompletionMessageItem {
100
- agent: string;
101
- block: string;
102
- triggerTurn: boolean;
103
- /** Final usage of the underlying run (or chain); aggregated into the group totals. */
104
- usage?: UsageStats;
105
- }
106
-
107
- /** Keep the established single-result shape; add a group header and an aggregate
108
- * token/cost footer only for real groups. */
109
- export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
110
- if (items.length === 0) return "";
111
- if (items.length === 1) return items[0].block;
112
- const agents = items.map((item) => item.agent).join(", ");
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}`;
117
- }
118
-
119
- /** A grouped completion wakes the main agent when any member requires a turn. */
120
- export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
121
- return items.some((item) => item.triggerTurn);
122
- }
123
-
124
- /** Passing reviewer notifications may opt out of waking; every other result wakes. */
125
- export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
126
- if (isFailedResult(result)) return true;
127
- return !(
128
- notifyOnReviewPass &&
129
- result.agent === "reviewer" &&
130
- reviewVerdict(getResultOutput(result)) === "pass"
131
- );
132
- }
133
-
134
- /** Minimal shape of an active run, for the "others still running" footer. Kept
135
- * decoupled from the monitor's RunView so this stays a pure, easily tested
136
- * formatter; the caller maps its live runs into this shape. */
137
- export interface ActiveRunFoot {
138
- id: number;
139
- agent: string;
140
- /** Optional content label (task-derived) shown next to the agent name. */
141
- label?: string;
142
- }
143
-
144
- /**
145
- * Footer appended to a completion message when OTHER runs are still active, so
146
- * the main agent does not declare the overall task done prematurely. A result
147
- * arriving for one run does not mean sibling runs are finished; naming them
148
- * gives the main agent concrete, in-context awareness to keep waiting.
149
- *
150
- * Returns "" when nothing is active (the common, single-run case stays quiet).
151
- */
152
- export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
153
- if (runs.length === 0) return "";
154
- const listed = runs.slice(0, maxListed);
155
- const items = listed
156
- .map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}`)
157
- .join(", ");
158
- const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
159
- return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
160
- }
1
+ /**
2
+ * Smart batching for successful background completions.
3
+ *
4
+ * A short debounce coalesces sibling runs while a max-wait timer, measured from
5
+ * the first item in the open group, bounds delivery latency. Failures are
6
+ * intentionally handled by the caller: flush held successes, then emit the
7
+ * failure directly so it is never delayed.
8
+ */
9
+
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";
13
+
14
+ export interface CompletionBatchTimings {
15
+ debounceMs: number;
16
+ maxWaitMs: number;
17
+ }
18
+
19
+ export const DEFAULT_COMPLETION_BATCH_TIMINGS: CompletionBatchTimings = {
20
+ debounceMs: 150,
21
+ maxWaitMs: 1_000,
22
+ };
23
+
24
+ type TimerHandle = ReturnType<typeof setTimeout>;
25
+
26
+ function unrefHandle(handle: TimerHandle): void {
27
+ if (
28
+ handle &&
29
+ typeof handle === "object" &&
30
+ "unref" in handle &&
31
+ typeof (handle as { unref: unknown }).unref === "function"
32
+ ) {
33
+ (handle as { unref: () => void }).unref();
34
+ }
35
+ }
36
+
37
+ export interface CompletionBatcherOptions<T> {
38
+ emit: (items: T[]) => void;
39
+ timings?: Partial<CompletionBatchTimings>;
40
+ }
41
+
42
+ export interface CompletionBatcher<T> {
43
+ /** Add an item to the current debounced group. */
44
+ push(item: T): void;
45
+ /** Emit any held items immediately as one group. */
46
+ flush(): void;
47
+ /** Clear timers and return held items without emitting them. */
48
+ dispose(): T[];
49
+ }
50
+
51
+ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
52
+ const timings = { ...DEFAULT_COMPLETION_BATCH_TIMINGS, ...options.timings };
53
+ let pending: T[] = [];
54
+ let debounceTimer: TimerHandle | null = null;
55
+ let maxWaitTimer: TimerHandle | null = null;
56
+
57
+ const clearTimers = (): void => {
58
+ if (debounceTimer !== null) {
59
+ clearTimeout(debounceTimer);
60
+ debounceTimer = null;
61
+ }
62
+ if (maxWaitTimer !== null) {
63
+ clearTimeout(maxWaitTimer);
64
+ maxWaitTimer = null;
65
+ }
66
+ };
67
+
68
+ const emitGroup = (): void => {
69
+ clearTimers();
70
+ if (pending.length === 0) return;
71
+ const items = pending;
72
+ pending = [];
73
+ options.emit(items);
74
+ };
75
+
76
+ return {
77
+ push(item: T): void {
78
+ pending.push(item);
79
+
80
+ if (debounceTimer !== null) clearTimeout(debounceTimer);
81
+ debounceTimer = setTimeout(emitGroup, timings.debounceMs);
82
+ unrefHandle(debounceTimer);
83
+
84
+ if (maxWaitTimer === null) {
85
+ maxWaitTimer = setTimeout(emitGroup, timings.maxWaitMs);
86
+ unrefHandle(maxWaitTimer);
87
+ }
88
+ },
89
+ flush: emitGroup,
90
+ dispose(): T[] {
91
+ clearTimers();
92
+ const abandoned = pending;
93
+ pending = [];
94
+ return abandoned;
95
+ },
96
+ };
97
+ }
98
+
99
+ export interface CompletionMessageItem {
100
+ agent: string;
101
+ block: string;
102
+ triggerTurn: boolean;
103
+ /** Final usage of the underlying run (or chain); aggregated into the group totals. */
104
+ usage?: UsageStats;
105
+ }
106
+
107
+ /** Keep the established single-result shape; add a group header and an aggregate
108
+ * token/cost footer only for real groups. */
109
+ export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
110
+ if (items.length === 0) return "";
111
+ if (items.length === 1) return items[0].block;
112
+ const agents = items.map((item) => item.agent).join(", ");
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}`;
117
+ }
118
+
119
+ /** A grouped completion wakes the main agent when any member requires a turn. */
120
+ export function completionGroupTriggersTurn(items: readonly CompletionMessageItem[]): boolean {
121
+ return items.some((item) => item.triggerTurn);
122
+ }
123
+
124
+ /** Passing reviewer notifications may opt out of waking; every other result wakes. */
125
+ export function completionTriggersTurn(result: SingleResult, notifyOnReviewPass: boolean): boolean {
126
+ if (isFailedResult(result)) return true;
127
+ return !(
128
+ notifyOnReviewPass &&
129
+ result.agent === "reviewer" &&
130
+ reviewVerdict(getResultOutput(result)) === "pass"
131
+ );
132
+ }
133
+
134
+ /** Minimal shape of an active run, for the "others still running" footer. Kept
135
+ * decoupled from the monitor's RunView so this stays a pure, easily tested
136
+ * formatter; the caller maps its live runs into this shape. */
137
+ export interface ActiveRunFoot {
138
+ id: number;
139
+ agent: string;
140
+ /** Optional content label (task-derived) shown next to the agent name. */
141
+ label?: string;
142
+ }
143
+
144
+ /**
145
+ * Footer appended to a completion message when OTHER runs are still active, so
146
+ * the main agent does not declare the overall task done prematurely. A result
147
+ * arriving for one run does not mean sibling runs are finished; naming them
148
+ * gives the main agent concrete, in-context awareness to keep waiting.
149
+ *
150
+ * Returns "" when nothing is active (the common, single-run case stays quiet).
151
+ */
152
+ export function formatActiveRunsFooter(runs: readonly ActiveRunFoot[], maxListed = 4): string {
153
+ if (runs.length === 0) return "";
154
+ const listed = runs.slice(0, maxListed);
155
+ const items = listed
156
+ .map((run) => `#${run.id} ${run.agent}${run.label ? `·${run.label}` : ""}`)
157
+ .join(", ");
158
+ const more = runs.length > listed.length ? `, +${runs.length - listed.length} more` : "";
159
+ return `\n\n⚠ ${runs.length} other run${runs.length === 1 ? "" : "s"} still active: ${items}${more}. Do not conclude the overall task yet — wait for their results (they wake you automatically) or check subagent_status.`;
160
+ }
package/src/config.ts CHANGED
@@ -72,9 +72,9 @@ export const DEFAULT_MAX_CONCURRENCY = 4;
72
72
  /** Upper bound accepted for maxConcurrency (defensive clamp). */
73
73
  export const MAX_CONCURRENCY_LIMIT = 16;
74
74
  /**
75
- * Maximum worker fixes after REVIEW_FAIL. Each fix is followed by optional
76
- * documenter and reviewer; this cap does not suppress the initial post-writer
77
- * documentation/final-review workflow. 0 disables fixes. Default: 2.
75
+ * Maximum worker fixes after REVIEW_FAIL. Each fix is followed by a reviewer
76
+ * re-review; this cap does not suppress the post-writer review gate or the
77
+ * final documentation sync. 0 disables fixes. Default: 2.
78
78
  */
79
79
  export const DEFAULT_MAX_FIX_ROUNDS = 2;
80
80
  /** Upper bound accepted for maxFixRounds (defensive clamp). 0 disables the loop. */
@@ -116,8 +116,8 @@ export interface SubagentsConfig {
116
116
  maxConcurrency: number;
117
117
  /**
118
118
  * Maximum worker fixes after REVIEW_FAIL. Every fix receives the full review,
119
- * then enabled documenter/reviewer stages run. Initial post-writer docs/review
120
- * do not consume this budget. 0 disables fixes. Default: 2.
119
+ * then a re-review runs; one final documentation sync follows the settled
120
+ * chain and does not consume this budget. 0 disables fixes. Default: 2.
121
121
  */
122
122
  maxFixRounds: number;
123
123
  /**