brl-subagent 2.3.6 → 2.3.8

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/AGENT.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  > The `delegate_task` prompt guidelines are a derived summary of this file; if they disagree, this file wins.
4
4
 
5
+ > **Naming:** deliberately `AGENT.md`, **not** `AGENTS.md`. This is *not* a pi
6
+ > context file — it is read on demand via the extension's `delegate_task`
7
+ > guidance, not auto-loaded into the system prompt. Renaming it would change
8
+ > nothing for users (a package directory is not in pi's context-file lookup
9
+ > chain) while adding its full contents to every session run inside this repo.
10
+
5
11
  This file is the authoritative reference for how the brl-subagent extension behaves
6
12
  and how you (the conductor) should judge a delegation. Read it when planning
7
13
  delegation-heavy work.
@@ -13,12 +19,16 @@ delegation-heavy work.
13
19
  | single, foreground | blocks until it returns | — (result comes back inline) | no |
14
20
  | single, background | returns immediately with an agent id | yes — a `subagent-completion` message on terminal state | no |
15
21
  | chain | blocks until the chain finishes or fails | — | yes (foreground) |
16
- | parallel (`tasks`) | blocks until every task finishes | — | yes (foreground) |
22
+ | parallel (`tasks`), foreground | blocks until every task finishes | — | yes (foreground) |
23
+ | parallel, background (fan-out) | returns immediately with one agent id per task | yes — a `subagent-completion` message per agent, on each agent's terminal state | yes |
17
24
  | graph | blocks; runs in waves via `dependsOn` | — | yes (foreground) |
18
25
 
19
26
  - Foreground single blocks: you wait for the result inline.
20
27
  - Background single returns immediately with an agent id; a completion message wakes you (see below).
21
- - Chain, parallel, and graph are batch — one call, many subtasks. They are foreground today; background batch is future work.
28
+ - Chain, parallel, and graph are batch — one call, many subtasks. Chain and graph are foreground; `tasks` + `background: true` fans out: every task starts as its own background agent, the call returns with one id per task in task order, and you are woken once per agent as each finishes.
29
+ - Fan-out validates the whole batch before spawning anything: any invalid task (cwd, `outputFile`, or a pre-task check) rejects the entire batch before a single spawn starts, and the error names the task (`Task N ("label")`).
30
+ - Fan-out rejects `gitMode: "branch"` for the batch — the git lock is awaited inside the first spawn and held until that agent settles, so same-cwd branch-mode spawns would serialize and block the call (the rule is blanket because the lock is keyed by the cwd path, not the repository root). It also rejects `approvalMode: "always"` up front, and the session cost limit is checked as per-task estimate × N.
31
+ - Spawn failure mid-loop stops further spawns and reports the failed task plus the ids already started; abort mid-loop stops further spawns and reports the ids already started. Either way those agents are detached and still wake you.
22
32
  - Chain stops at the first failure; max 10 steps.
23
33
  - Parallel runs every task regardless of the others — no short-circuit.
24
34
  - Graph runs in waves, ordered by `dependsOn` edges.
@@ -29,13 +39,13 @@ delegation-heavy work.
29
39
  - The wake triggers a turn even when you are idle. Do not poll for it.
30
40
  - Delivery mode by status holds at knob `"all"`: `failed`/`stopped` → steering you to act; `completed` → follow-up. The `completionNotify` knob changes both the delivery mode and the wake: under `"failed"` a `completed` run is delivered as a passive `nextTurn` (no wake) while `failed`/`stopped` still steer; under `"off"` everything is a passive `nextTurn` and nothing wakes you. (The knob controls whether an idle conductor is triggered — see below.)
31
41
  - Knob dependency: polling is correct only when wakes are disabled (`completionNotify: "off"`). The wake is per-terminal-status and knob-scoped: under `"all"` a terminal run always wakes you; under `"failed"` only a `failed` or `stopped` run wakes you — a `completed` run stays a passive `nextTurn` (no wake); under `"off"` nothing wakes you. So polling is wrong unless wakes are disabled. One status check as a stall check is legitimate; repeated polling is not.
32
- - One message per run — the extension deduplicates on the first terminal event for a run id, so don't expect multiple messages for a single run.
42
+ - One message per run — the extension deduplicates on the first terminal event for a run id, so don't expect multiple messages for a single run. A fan-out wakes you once per agent — still one message per run (each agent is its own run), never coalesced.
33
43
  - Honest records: trust the message's `category`/abort source over your own guess. The provider/abort origin is authoritative on why the run stopped.
34
44
  - Steering is not an abort. Being steered to act on a terminal run is not terminating it. A real `stop` is an abort — and a stopped run still wakes you.
35
45
 
36
46
  ## Delegation judgment
37
47
 
38
- - Foreground vs background: if your next step needs the result inline, run foreground. If the user can keep working or is AFK, run background and act on the wake.
48
+ - Foreground vs background: if your next step needs the result inline, run foreground. If the user can keep working or is AFK, run background and act on the wake. Pre-declared independent work while the user is away → `tasks` + `background: true` (fan-out); act on each wake.
39
49
  - Batch vs sequential: if the work is pre-declared, use chain/parallel/graph. If you're steering incrementally with the user, issue sequential calls.
40
50
  - Retry taxonomy: `retryRunId` is a retry — it re-runs with the recorded task and params. A re-dispatch (a fresh call with the same task text) is not a retry. Never re-issue an identical spec after a termination without confirming the cause with the user (Rule 18).
41
51
  - When to delegate: when the task needs an isolated context, a deep investigation, parallel research, or a long-running analysis.
@@ -46,6 +56,7 @@ delegation-heavy work.
46
56
  - Batch chain with `{previous}`: `chain: [{ task: "…" }, { task: "Use {previous} …" }]` — each step gets the prior step's output.
47
57
  - Graph with `dependsOn`: subtasks declare edges; the scheduler runs them in waves.
48
58
  - Background + act on wake: spawn a background run, then act when the completion message arrives. Never poll between spawn and wake.
59
+ - Background fan-out: `tasks: [...], background: true` — one background agent per task, one id per task in task order, one wake per agent.
49
60
  - retryRunId: pass a failed run's id to re-run it with its recorded task and params; explicit parameters override the originals.
50
61
 
51
62
  ## Mechanics pointer
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Multi-agent orchestration for [pi](https://github.com/earendil-works/pi) — chain, parallel, and dependency-graph delegation to isolated subagents with per-step model routing, preset-driven tool scoping, thinking-level control, and background execution with live monitoring, real abort, and per-agent timeouts.
4
4
 
5
- **Version:** 2.3.6 · **Author:** Azmeen Afandi / [Beeroo Labs](https://beeroolabs.com) · **License:** MIT
5
+ **Version:** 2.3.8 · **Author:** Azmeen Afandi / [Beeroo Labs](https://beeroolabs.com) · **License:** MIT
6
6
 
7
7
  ---
8
8
 
@@ -84,13 +84,15 @@ All settings persist across sessions.
84
84
  | `outputFile` | string | — | Path for the subagent to write full findings. Returns only a summary. |
85
85
  | `timeout` | number | — | Max milliseconds. Exceeded → SIGTERM (5s grace) → SIGKILL. |
86
86
  | `cwd` | string | — | Working directory. Defaults to conductor's cwd. |
87
- | `background` | boolean | `false` | Spawn as an independent background session; returns an ID immediately. See [Background execution](#background-execution). |
87
+ | `background` | boolean | `false` | Spawn as an independent background session; returns an ID immediately. With `tasks` it fans out (one background agent per task, one ID per task), and the conductor is woken once per agent as each finishes. See [Background execution](#background-execution). |
88
88
  | `priority` | string | — | Concurrency priority: `critical` / `high` / `normal` / `low`. Defaults to `normal`; higher-priority delegations queue ahead. `tasks[]` / `graph[]` steps can set `priority` per unit (see below). |
89
89
 
90
90
  ## Multi-step modes (chain, tasks, graph)
91
91
 
92
92
  Beyond a single `task`, `delegate_task` accepts three multi-step shapes: `chain` (sequential steps, `{previous}` references the prior step's output), `tasks` (parallel, independent), and `graph` (dependency-ordered, `{otherId}` references another task's output).
93
93
 
94
+ `background: true` fans out the `tasks` shape into one background agent per task (one ID per task, one completion wake per agent); `chain` and `graph` reject it.
95
+
94
96
  Every execution knob can be set per step: `model`, `thinkingLevel`, `tools`, `excludeTools`, `noBuiltinTools`, `systemPrompt`, `inheritSystemPrompt`, `outputFile`, `timeout`, `cwd`. Unset fields inherit from the global parameters (and their preset defaults).
95
97
 
96
98
  **Per-step priority:** `tasks[]` and `graph[]` items accept a `priority` (`critical` / `high` / `normal` / `low`) — the per-unit priority decides which unit wins a concurrency slot first, overriding the call-level default for that unit only. `chain[]` steps deliberately take NO `priority`: a chain holds one slot for its whole duration, so steps never compete for slots — array order is the priority.
@@ -253,9 +255,20 @@ Set `background: true` to spawn the subagent as an independent session that retu
253
255
  **Background safety controls (issue #28):** background agents honor the same safety controls as foreground runs — no more unattended sessions that bypass approval, git isolation, deadlines, or cost:
254
256
 
255
257
  - **Per-agent timeout** — the deadline is armed before the prompt starts (preflight time counts toward it). On expiry the session is aborted and the agent ends with status `stopped` and the timeout reason. Timeout values are normalized (`0`/negative/`NaN`/`Infinity`/`≥ 2^31` → no timeout) and a double-fire guard prevents the timer from acting on an already-settled agent.
256
- - **Session cost limit (R5)** — the cost check runs before the background spawn, so a session at its limit cannot bypass it by delegating to background.
257
- - **Approval mode** — `approvalMode: 'always'` is rejected for background agents (there is no interactive dialog to approve a diff while running unattended); `'writes'` silently auto-approves with a warning logged.
258
- - **gitMode branch isolation** — with `gitMode: 'branch'` a work branch is created before the run, the agent's changes are committed at teardown so the diff is real, the diff is captured and surfaced via `get_subagent_result`, and the branch is then switched away from and deleted. This requires a clean working tree — a dirty tree is refused loudly rather than risking the base branch.
258
+ - **Session cost limit (R5)** — the cost check runs before the background spawn, so a session at its limit cannot bypass it by delegating to background. For a background fan-out the whole batch is checked up front as per-task estimate × N.
259
+ - **Approval mode** — `approvalMode: 'always'` is rejected for background agents (there is no interactive dialog to approve a diff while running unattended); `'writes'` silently auto-approves with a warning logged. For a fan-out, `'always'` rejects the whole batch before any spawn and `'writes'` warns once for the batch.
260
+ - **gitMode branch isolation** — with `gitMode: 'branch'` a work branch is created before the run, the agent's changes are committed at teardown so the diff is real, the diff is captured and surfaced via `get_subagent_result`, and the branch is then switched away from and deleted. This requires a clean working tree — a dirty tree is refused loudly rather than risking the base branch. `gitMode: 'branch'` is rejected for background fan-out — see [Background fan-out](#background-fan-out).
261
+
262
+ ### Background fan-out
263
+
264
+ `background: true` with `tasks: [...]` fans out: every task starts as its own background agent, the call returns immediately with **one ID per task** in task order, and the conductor is woken with **one completion message per agent** as each finishes — no coalescing, no batch wake. `chain` and `graph` cannot be combined with `background: true` (rejected loudly): sequential dependencies and dependency waves do not survive fan-out.
265
+
266
+ Fan-out validates the entire batch before spawning anything. Any invalid task — `cwd`, `outputFile`, or a pre-task check — rejects the whole batch before a single spawn starts, and the error names the task (`Task N ("label")`). Per-task overrides are honoured: `model`, `thinkingLevel`, `tools` / `excludeTools` / `noBuiltinTools`, `systemPrompt` / `inheritSystemPrompt`, `outputFile`, `timeout`, `cwd`, `priority`, `label`.
267
+
268
+ Three checks reject the fan-out up front, before any spawn: `approvalMode: 'always'`; `gitMode: 'branch'` for the batch (the git lock is awaited inside the first spawn and held until that agent settles, so same-cwd branch-mode spawns would serialize and block the call — the rule is blanket because the lock is keyed by the cwd path, not the repository root); and the session cost limit, checked as per-task estimate × N.
269
+
270
+ If a spawn fails mid-loop, fan-out stops and reports the failed task plus the IDs already started; an abort mid-loop stops further spawns. Either way, the agents already started stay detached and still wake the conductor. Everything else is unchanged: single background, foreground parallel, the `MAX_PARALLEL_TASKS` cap (8), a live-monitor row per agent, `get_subagent_result` / `steer_subagent` / `stop_subagent` by agent ID, per-agent timeouts, and a single `'writes'` approval warning for the batch.
271
+
259
272
  ## Changelog
260
273
 
261
274
  See [CHANGELOG.md](CHANGELOG.md) for the release history.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brl-subagent",
3
- "version": "2.3.6",
3
+ "version": "2.3.8",
4
4
  "description": "Subagent extension for pi with configurable model and thinking mode via selection UI",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -35,10 +35,10 @@
35
35
  "typebox": "*"
36
36
  },
37
37
  "devDependencies": {
38
- "@earendil-works/pi-agent-core": "^0.85.1",
39
- "@earendil-works/pi-ai": "^0.85.1",
40
- "@earendil-works/pi-coding-agent": "^0.85.1",
41
- "@earendil-works/pi-tui": "^0.85.1",
38
+ "@earendil-works/pi-agent-core": "^0.87.0",
39
+ "@earendil-works/pi-ai": "^0.87.0",
40
+ "@earendil-works/pi-coding-agent": "^0.87.0",
41
+ "@earendil-works/pi-tui": "^0.87.0",
42
42
  "jiti": "^2.7.0",
43
43
  "typebox": "^1.3.25",
44
44
  "typescript": "^5.9.3",
package/src/history.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { SubagentRun, SubagentResult } from "./types";
9
- import { isSubagentError, getFinalOutput, isSubagentRunShape, CUSTOM_ENTRY_TYPES, MAX_RUN_HISTORY_ENTRIES } from "./types";
9
+ import { isSubagentError, getFinalOutput, isSubagentRunShape, classifyError, coherentFailureReason, CUSTOM_ENTRY_TYPES, MAX_RUN_HISTORY_ENTRIES } from "./types";
10
10
  import type { ExtensionContext, SessionManager } from "@earendil-works/pi-coding-agent";
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -107,6 +107,13 @@ export function finalizeRunRecord(
107
107
  run.tokensIn = result.usage.input;
108
108
  run.tokensOut = result.usage.output;
109
109
  run.errorMessage = result.errorMessage;
110
+ // Issue #187: mirror the background finalize rule — a FAILED run always
111
+ // records a terminal reason that AGREES with its status (coherentFailureReason
112
+ // through the shared helper), while a non-failed run records whatever reason
113
+ // the result carried (may be undefined, e.g. a success with no stopReason).
114
+ run.stopReason = error
115
+ ? coherentFailureReason(result.stopReason, classifyError(result))
116
+ : result.stopReason;
110
117
  run.outputSummary = finalOutput.slice(0, 200);
111
118
  run.fullOutput = finalOutput || undefined;
112
119
  }