pi-subagents 0.52.0 → 0.53.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +4 -0
  3. package/docs/configuration.md +11 -1
  4. package/docs/extension-api.md +3 -1
  5. package/docs/workflows.md +2 -0
  6. package/package.json +2 -1
  7. package/prompts/council.md +48 -0
  8. package/skills/council-mode/SKILL.md +230 -0
  9. package/skills/pi-subagents/SKILL.md +2 -0
  10. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
  11. package/skills/pi-subagents/references/execution-controls.md +11 -0
  12. package/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
  13. package/src/agents/agent-management.ts +22 -3
  14. package/src/agents/agent-serializer.ts +2 -0
  15. package/src/agents/agents.ts +29 -13
  16. package/src/agents/builtin-names.ts +9 -0
  17. package/src/agents/runtime-agent-registry.ts +418 -0
  18. package/src/api/agents.ts +7 -0
  19. package/src/api/external-job-provider.ts +3 -2
  20. package/src/api/preflight.ts +1 -1
  21. package/src/extension/config.ts +3 -0
  22. package/src/extension/doctor.ts +1 -0
  23. package/src/extension/index.ts +17 -2
  24. package/src/extension/rpc.ts +41 -1
  25. package/src/extension/schemas.ts +7 -4
  26. package/src/extension/tool-description.ts +2 -2
  27. package/src/runs/background/async-execution.ts +2 -1
  28. package/src/runs/background/async-job-tracker.ts +4 -3
  29. package/src/runs/background/async-resume.ts +2 -1
  30. package/src/runs/background/async-status-snapshot.ts +14 -5
  31. package/src/runs/background/auto-drain.ts +1 -0
  32. package/src/runs/background/result-watcher.ts +8 -0
  33. package/src/runs/background/subagent-runner.ts +7 -4
  34. package/src/runs/background/subagent-wait.ts +9 -5
  35. package/src/runs/background/terminal-run-index.ts +15 -6
  36. package/src/runs/background/wait-tool.ts +1 -0
  37. package/src/runs/foreground/execution.ts +5 -1
  38. package/src/runs/foreground/subagent-executor.ts +182 -46
  39. package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
  40. package/src/runs/shared/acceptance.ts +44 -1
  41. package/src/runs/shared/model-exclusions.ts +242 -0
  42. package/src/runs/shared/model-fallback.ts +55 -2
  43. package/src/runs/shared/subagent-control.ts +25 -3
  44. package/src/shared/fork-context.ts +17 -1
  45. package/src/shared/model-info.ts +20 -0
  46. package/src/shared/settings.ts +2 -2
  47. package/src/shared/types.ts +35 -0
  48. package/src/slash/slash-commands.ts +20 -6
  49. package/src/slash/slash-live-state.ts +3 -3
  50. package/src/tui/fleet-status.ts +86 -1
  51. package/src/tui/fleet.ts +55 -2
  52. package/src/tui/render.ts +73 -3
  53. package/src/workflows/scripted-workflow.ts +100 -12
  54. package/src/workflows/workflow-receipt.ts +140 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.53.0] - 2026-08-20
6
+
7
+ ### Highlights
8
+ - Pi extensions can now register runtime agents without writing user or project config.
9
+ - Async workflows are easier to resume because completed children now have durable keyed receipts.
10
+ - Model fallback is less noisy and less wasteful when a model fails or the prompt is too large.
11
+ - Fleet and `/council` now give clearer supervision cues while keeping control in the parent session.
12
+ - Extension RPC hosts can safely inspect status, launch async work, steer children, and manage schedules.
13
+
14
+ ### Added
15
+ - Carry full model registry metadata, including tiered pricing, into normalized model information. Thanks to [@srcKod](https://github.com/srcKod) for #1317.
16
+ - Add runtime agent registration for Pi extensions, with name and alias collision checks. Thanks to [@fmoda3](https://github.com/fmoda3) for #1310.
17
+ - Skip recently failed fallback models for a TTL-backed window during model selection. Thanks to [@srcKod](https://github.com/srcKod) for #1318.
18
+ - Add a schedule-only `manage` method to extension RPC for list/show/history/pause/resume/run/delete, while rejecting unrelated management actions. Thanks to [@aboubakrine](https://github.com/aboubakrine) for #1319.
19
+ - Let agent definitions and `agentOverrides` set a default `outputMode`, while
20
+ call-level output mode stays higher priority. Thanks to [@bbbRye007](https://github.com/bbbRye007) for #1305.
21
+ - Add `context: "profile"` for workflow children that should use the selected
22
+ agent profile's declared context instead of the global default (#1303).
23
+ - Add durable keyed async workflow receipts and resume-by-key selectors for
24
+ retained workflow children (#1302).
25
+ - Add the `resultScanLogging` config to control result scan logging. Thanks to [@apoapostolov](https://github.com/apoapostolov) for #1293.
26
+ - Add packaged `/council` and `council-mode` resources for a bounded,
27
+ supervisor-mediated advisor loop, plus documented model-based `council-*`
28
+ profile examples (#1295).
29
+
30
+ ### Changed
31
+ - Show bounded workflow progress in Fleet detail views while keeping workflow
32
+ parents as the only actionable async items (#1304).
33
+ - Make `/council` easier to supervise with structured advisor contracts,
34
+ aggregate pass receipts, and visible pass checkpoints (#1301).
35
+ - Reuse validated workflow launch fingerprints during `runs.all` batch setup, reducing focused fingerprint bookkeeping time by 48.7% (#1287).
36
+ - Speed up recent terminal run history reads when the marker history is large and the requested limit is small.
37
+ - Reduce repeated serialization while applying async status snapshot byte caps (#1288).
38
+
39
+ ### Fixed
40
+ - Add tolerant `subagent_wait({ stopOnAttention: false })` blocking waits and scale idle attention defaults for higher-thinking children. Thanks to [@elecnix](https://github.com/elecnix) for #1315 and #1316.
41
+ - Add a separate classifier for model context-overflow errors. Thanks to [@srcKod](https://github.com/srcKod) for #1312.
42
+ - Normalize child result metadata before workflow return persistence (#1307).
43
+ - Quote only confidently identified leading Windows executable paths in acceptance verification commands. Thanks to [@srcKod](https://github.com/srcKod) for #1294.
44
+ - Keep forked subagent sessions out of top-level `pi -c` discovery by storing them under the parent session root. Thanks to [@xz-dev](https://github.com/xz-dev) for #1297.
45
+ - Preserve `/council` advisor context defaults during fallback and cross-exam runs (#1298).
46
+
47
+ ## [0.52.1] - 2026-08-20
48
+
49
+ ### Highlights
50
+ - Model setup errors now point to the right alternate provider when there is one clear match.
51
+ - Surf's optional `gpt-pro` package agent has a smoother path to run ChatGPT Pro web jobs through the external-job bridge when the user is logged in.
52
+ - External-job providers can add metadata or extra operations without breaking provider discovery.
53
+ - The packaged skill now includes a concise guide for coordinating multiple tasks, worktrees, and repositories.
54
+ - Pi extension worktrees now have clearer guidance to avoid duplicate auto-loaded tools and shortcuts.
55
+
56
+ ### Fixed
57
+ - Suggest the unique alternate provider model when an explicit qualified subagent model is unavailable, without resolving across providers. Thanks to [@lallenlowe](https://github.com/lallenlowe) for #1280.
58
+ - Accept extra fields on registered external-job providers, such as `kind`, `wakeChannels`, or additional operations. This keeps integrations such as Surf's `gpt-pro` package agent from breaking provider discovery as they add browser-backed job metadata, while job payload validation stays strict.
59
+
60
+ ### Changed
61
+ - Add a pi-subagents reference for coordinating multiple tasks, worktrees, and repositories, including guidance for keeping Pi extension worktrees outside auto-discovered extension directories.
62
+
5
63
  ## [0.52.0] - 2026-08-19
6
64
 
7
65
  ### Highlights
package/README.md CHANGED
@@ -67,12 +67,16 @@ Rule of thumb: `scout` before you understand the code, `researcher` before you t
67
67
 
68
68
  ## Common workflows
69
69
 
70
+ The package includes `/council` and `council-mode`, plus documented model-based
71
+ `council-*` profile examples that you add in your own agent directory.
72
+
70
73
  | Want | Ask naturally |
71
74
  |------|---------------|
72
75
  | Get a second opinion | "Ask oracle to review this plan and challenge assumptions." |
73
76
  | Solve a hard problem | "Use oracle to investigate this bug before we edit." |
74
77
  | Review a diff | "Use reviewer to review this diff." |
75
78
  | Run parallel reviewers | "Run reviewers for correctness, tests, and cleanup." |
79
+ | Debate a material decision | "Use `/council` to convene architect and skeptic advisors." |
76
80
  | Implement then review | "Implement this, then review it." |
77
81
  | Review until clean | "Run a review loop on this change with a max of 3 rounds." |
78
82
  | Execute a plan carefully | "Have worker implement this approved plan, then run reviewers and apply the feedback." |
@@ -160,10 +160,20 @@ Controls the under-editor widget for active background runs. It defaults to `tru
160
160
 
161
161
  Keeps the `subagent_wait` tool registered but makes direct calls return immediately instead of blocking on active subagent or provider work. The default is enabled. You can also set `"waitTool": false`; set `PI_SUBAGENT_WAIT_TOOL_ENABLED=false` (or `0`, `off`, `disabled`) to override config for one process. The effective value is passed explicitly to child runtimes. Headless `agent_end` auto-drain remains a lifecycle safeguard even when direct wait calls are disabled. Invalid config or environment values fail instead of being coerced.
162
162
 
163
- Blocking `subagent_wait({ id: "..." })` keeps the current tool call open until that run changes. In a long-lived interactive parent session, `subagent_wait({ id: "...", nonBlocking: true })` instead resolves the prefix once, persists the exact run identity, returns a subscription token immediately, and wakes that session on completion, failure, attention, reconciliation failure, or timeout. Armed subscriptions appear in ordinary `subagent({ action: "status" })` output and are not counted as active child work.
163
+ Blocking `subagent_wait({ id: "..." })` keeps the current tool call open until that run changes. By default it returns when a run needs attention. Use `subagent_wait({ stopOnAttention: false })` only for run-to-completion flows that should wait through idle or long-thinking attention; supervisor/contact requests still stop the wait. In a long-lived interactive parent session, `subagent_wait({ id: "...", nonBlocking: true })` instead resolves the prefix once, persists the exact run identity, returns a subscription token immediately, and wakes that session on completion, failure, attention, reconciliation failure, or timeout. Armed subscriptions appear in ordinary `subagent({ action: "status" })` output and are not counted as active child work.
164
164
 
165
165
  This is different from `waitTool.enabled=false`, which returns immediately without registering any future wake. Provider items remain available only to blocking fleet-wide waits; non-blocking subscriptions require one async or remembered detached foreground run id.
166
166
 
167
+ ## `resultScanLogging`
168
+
169
+ ```json
170
+ { "resultScanLogging": "activity" }
171
+ ```
172
+
173
+ Controls how slow result-index scans are logged. Defaults to `"all"`; valid values are `"all"`, `"activity"`, and `"off"`.
174
+
175
+ The watcher logs `Subagent result scan inspected … scheduled …` through `console.error` whenever a result-index scan passes the slow threshold (500ms). With `"all"` (default) every slow scan is logged, including the periodic healthy rescan that inspects zero files while no async runs are pending. Those empty scans add noise to the session transcript with no signal; choose `"activity"` to log only scans that inspected or scheduled actual work, or `"off"` to silence slow-scan logging entirely. `"off"` does not disable result delivery or the watcher itself, only its slow-scan log line.
176
+
167
177
  ## `forceTopLevelAsync`
168
178
 
169
179
  ```json
@@ -23,10 +23,11 @@ pi.events.emit("subagents:rpc:v1:request", {
23
23
  });
24
24
  ```
25
25
 
26
- The RPC methods are `ping`, `status`, `spawn`, `steer`, `interrupt`, `stop`, and `resume`. `status`, `steer`, `interrupt`, and `resume` reuse the normal package-owned actions.
26
+ The RPC methods are `ping`, `status`, `manage`, `spawn`, `steer`, `interrupt`, `stop`, and `resume`. `status`, `manage`, `steer`, `interrupt`, and `resume` reuse normal package-owned actions.
27
27
 
28
28
  Method notes:
29
29
 
30
+ - `manage` exposes a narrow schedule-only allowlist: `schedule.list`, `schedule.show`, `schedule.history`, `schedule.pause`, `schedule.resume`, `schedule.run`, and `schedule.delete`. All actions except `schedule.list` require `id`. Mission, agent, config, worktree, and arbitrary management actions are rejected before executor dispatch. `ping.capabilities.managementActions` advertises the exact allowlist.
30
31
  - `spawn` requires `workflowScript` and is async-only: omit `async` or set `async: true`, omit `clarify`, and do not pass management `action` values. It goes through the same executor as the `subagent` tool, so agent discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status all behave the same.
31
32
  - `steer` requires an async run `id` (plus optional child `index`) and a non-empty `message`; its reply preserves the normal acknowledged-delivery result. Optional `mode` values are `steer` (default), `follow_up`, and `auto`, and receipts include `deliveryStatus: "delivered" | "queued"`. RPC steering disables the direct tool's pause-and-revive recovery in every mode so an extension keeps authority over the exact child it spawned; `ping.capabilities.nonRecoveringSteer` advertises this guarantee.
32
33
  - `resume` requires a run target and non-empty `message`. It delegates to the existing revival path, which validates current-session ownership, persisted session/recovery metadata, stopped/live state, capability ceilings, and the exclusive session lease before returning the new async run details. Callers may request a `file-only` output path for the revived result without overriding its model, tools, or budgets. `ping.capabilities.resume` advertises this seam.
@@ -35,6 +36,7 @@ Method notes:
35
36
  Capability advertisements on `ping`:
36
37
 
37
38
  - `events.asyncComplete` — exact process-local completion correlation after RPC `spawn`.
39
+ - `managementActions` — exact schedule management actions accepted by RPC `manage`.
38
40
  - `launchResolvedExtensions` — the optional launch-resolved extension projection in status details.
39
41
  - `runtimeAcknowledgedExtensions` — the optional child-runtime acknowledgement projection and event name.
40
42
  - `processTerminalProof` — the process-terminal proof status (see [observability.md](observability.md#process-terminal-proof)).
package/docs/workflows.md CHANGED
@@ -37,6 +37,8 @@ Add `autofix` to `/parallel-review` or `/parallel-cleanup` to apply only the syn
37
37
 
38
38
  All model-facing subagent execution is expressed through `workflowScript` in the `subagent` tool. Use stable keys and ordinary JavaScript for one child, sequence, and parallelism. For ordinary parallel fanout, use `await runs.all([{ key, agent, task }, ...])`; do not read `.output` from unawaited `runs.run` launches. Store a `runs.run` promise only when the script later observes it with `await`, `Promise.race`, or `Promise.all`, such as steering a live child before awaiting its result. Scripts are ordinary JavaScript statement bodies. Use an explicit `return` for a useful result:
39
39
 
40
+ Child results cross into the script as plain JSON data. Non-JSON host metadata is omitted, so use returned fields such as `runId`, `ok`, `output`, and `structuredOutput` for workflow control.
41
+
40
42
  ```js
41
43
  subagent({ workflowScript: `
42
44
  const scan = await runs.run("scan", { agent: "scout", task: "Scan the codebase" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@
10
10
  "./background-work": "./src/api/background-work.ts",
11
11
  "./external-job-provider": "./src/api/external-job-provider.ts",
12
12
  "./external-runs": "./src/api/external-runs.ts",
13
+ "./agents": "./src/api/agents.ts",
13
14
  "./delegation": "./src/api/delegation.ts",
14
15
  "./capability-ceiling": "./src/api/capability-ceiling.ts",
15
16
  "./preflight": "./src/api/preflight.ts",
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: Run a bounded supervisor-mediated council of advisors and write a decision memo
3
+ argument-hint: "<question> [--advisors name:role,name:role] [--max-passes 2|3] [--scope ...] [--non-goals ...]"
4
+ ---
5
+
6
+ Run a bounded, supervisor-mediated council on this question. You, the parent
7
+ session, are the supervisor. You select the roster, curate cross-advisor packets,
8
+ decide which feedback is valid, and write the final memo. Advisors do not talk
9
+ directly or see peer transcripts by default. This is not free-form agent chat.
10
+
11
+ Before you orchestrate, read `skills/council-mode/SKILL.md` and
12
+ `skills/pi-subagents/references/execution-controls.md`.
13
+
14
+ Parse the invocation yourself. The flags below are conventions, not runtime
15
+ options. Record a brief with the question, scope, non-goals, evidence targets,
16
+ roster, roles, and pass cap. Default `--max-passes` to 2. Clamp it to 2 or 3. If
17
+ the question is trivial or settled, answer directly instead of convening a council.
18
+
19
+ ## Roster
20
+
21
+ - If `--advisors` is given, use exactly those `name:role` pairs. Fail clearly on an
22
+ unknown agent.
23
+ - Otherwise list agents with `subagent({ action: "list" })`, then prefer 2–3
24
+ executable names that start with `council-`.
25
+ - If fewer than two profiles are available, fill the roster with `oracle`, then
26
+ `reviewer`, until it has two advisors. Launch fallback `oracle` with
27
+ `context: "fork"` so global defaults cannot remove its parent-chat context.
28
+ Let `reviewer` use its normal profile context. Note the fallback and known
29
+ context modes in the memo.
30
+ - Use the normal single-oracle loop only when a requested roster or unavailable
31
+ builtins leaves fewer than two advisors. Label the memo as degraded mode.
32
+
33
+ Roles belong to this request, not to the profiles. Keep the roster at 2–3 and never
34
+ exceed 4.
35
+
36
+ ## Run the protocol
37
+
38
+ Use the canonical workflow, structured advisor contracts, aggregate pass receipts,
39
+ and memo requirements in `skills/council-mode/SKILL.md`. Keep the parent as the
40
+ only synthesizer and decision maker. Do not introduce a chair advisor, peer chat,
41
+ or transcript sharing.
42
+
43
+ Use its required boundary checkpoints, yield for each async workflow without
44
+ polling, and write its required final memo.
45
+
46
+ Question and options from the slash command invocation:
47
+
48
+ $@
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: council-mode
3
+ description: Run a bounded supervisor-mediated advisor council. Use when the user asks to convene advisors, debate a decision, cross-examine recommendations, or run /council.
4
+ ---
5
+
6
+ # Council Mode
7
+
8
+ This skill is for the parent supervisor only. Do not inject it into advisors. The
9
+ parent selects the roster, curates all cross-advisor communication, decides which
10
+ feedback is valid, and writes the decision memo. Advisors do not talk directly or
11
+ see peer transcripts by default. This is not free-form agent chat.
12
+
13
+ Use council mode for a material decision with real tradeoffs. Do not use it for a
14
+ trivial or settled question, or for implementation work. Read
15
+ `skills/pi-subagents/references/execution-controls.md` before you launch advisors.
16
+
17
+ ## Roster and limits
18
+
19
+ Roles such as architect, skeptic, operator, and performance reviewer belong to the
20
+ `/council` request. A `council-*` profile defines only model, tools, context, and
21
+ output defaults. Its profile configuration or explicit invocation owns its context
22
+ choice.
23
+
24
+ Create model-based profiles in your user or project agent directory. Do not add
25
+ them to this package. This is a valid example; roles still come from `/council`:
26
+
27
+ ```markdown
28
+ ---
29
+ name: council-sol
30
+ description: Read-only fresh-context advisor for bounded council decisions
31
+ tools: read, grep, find, ls
32
+ model: openai-codex/gpt-5.6-sol
33
+ thinking: high
34
+ systemPromptMode: replace
35
+ inheritProjectContext: true
36
+ inheritSkills: false
37
+ defaultContext: fresh
38
+ acceptanceRole: read-only
39
+ ---
40
+
41
+ Analyze only the assigned council role. Inspect evidence directly. Do not edit,
42
+ run mutating commands, commit, push, contact peers, or spawn subagents. Return
43
+ concise, cited advice using the report contract in the council task.
44
+ ```
45
+
46
+ After `subagent({ action: "list" })`, prefer 2–3 executable names that start with
47
+ `council-`. The prefix is a naming convention, not runtime selection. If fewer
48
+ than two profiles are available, fill the roster with `oracle`, then `reviewer`,
49
+ until it has two advisors. Launch fallback `oracle` with `context: "fork"` so
50
+ global defaults cannot remove its parent-chat context. Let fallback `reviewer`
51
+ use its normal profile context. Note the fallback and known context modes in the
52
+ memo. Use the normal single-oracle consultation loop only when a requested roster
53
+ or unavailable builtins leaves fewer than two advisors.
54
+ Label that result as degraded mode. Never use more than four advisors.
55
+
56
+ Pass 1 is independent reports. Pass 2 is one cross-exam. The default pass cap is
57
+ 2. Run pass 3 only when `--max-passes 3` was requested and a material dispute can
58
+ be settled by evidence an advisor can produce. Never run an unbounded loop.
59
+
60
+ ## Protocol
61
+
62
+ 1. The parent writes a brief with the question, scope, non-goals, evidence targets,
63
+ roster, roles, and pass cap.
64
+ 2. Before Pass 1, tell the user the roster, roles, requested or known context
65
+ modes, and pass cap. Use a stable key, `phase`, and concise `label` for every
66
+ workflow child. For example, use `advisor-oracle`, `phase: "Council pass 1"`,
67
+ and `label: "Oracle — intent and consistency"`.
68
+ 3. Launch one async `workflowScript` with `runs.all` for independent advisor
69
+ reports. Set `context` when the selected advisor has a known profile context or
70
+ a fallback rule requests one, because a global default can otherwise override
71
+ that profile. Set `context: "fork"` for fallback `oracle`. If no advisor context
72
+ is known, omit `context` and disclose the unknown runtime default in the memo.
73
+ Each advisor is read-only and must not spawn children, edit files, run mutating
74
+ commands, commit, or push. Set `output: false` unless separate advisor artifacts
75
+ are explicitly requested or useful for the decision.
76
+ 4. Return one aggregate Pass 1 receipt. After it completes, tell the user the
77
+ completion count, agreement count, dispute count, and whether Pass 2 is needed.
78
+ 5. The parent synthesizes a claim matrix in session. It contains agreements,
79
+ disputed claims, missing proof, owner decisions, and a relay set of at most five
80
+ high-impact claims per advisor. Do not delegate this synthesis.
81
+ 6. Before Pass 2, tell the user how many claims are relayed and why each is
82
+ material. Launch a second async `workflowScript` with `runs.all` resume calls.
83
+ Each task is a curated challenge packet, not a peer transcript. A resume requires
84
+ a retained run id and a non-empty task. It excludes `agent` and rejects `gate`.
85
+ Record the new run id from every resume. Pass 3 resumes those latest ids. Return
86
+ one aggregate Pass 2 receipt.
87
+ 7. After Pass 2, tell the user whether the council converged or which owner
88
+ decisions remain. The parent writes the final memo. Do not delegate it.
89
+
90
+ If an advisor is not resumable, run the same profile in fresh context with its own
91
+ pass-1 report and the challenge packet. Label that response as a fresh-context
92
+ fallback, not a true cross-exam.
93
+
94
+ Do not set `clarify`, `worktree`, `gate`, turn budgets, tool budgets, or tight usage
95
+ budgets on advisors. Bound work through the roster, pass cap, and report length.
96
+
97
+ ## Advisor contracts and pass receipts
98
+
99
+ Pass-1 reports are at most about 600 words. Give each advisor the same
100
+ `outputSchema`, so reports are comparable without heading cleanup. The following
101
+ shape is a contract template. Use the runtime schema syntax supported by the
102
+ workflow and keep narrative fields as strings:
103
+
104
+ ```js
105
+ const pass1OutputSchema = {
106
+ type: "object",
107
+ required: [
108
+ "recommendation", "evidence", "assumptions", "risks", "confidence",
109
+ "challengeClaims", "ownerDecisions", "changeMyMind"
110
+ ],
111
+ properties: {
112
+ recommendation: { type: "string" },
113
+ evidence: {
114
+ type: "array",
115
+ items: {
116
+ type: "object",
117
+ required: ["claim", "sources"],
118
+ properties: {
119
+ claim: { type: "string" },
120
+ sources: { type: "array", items: { type: "string" } }
121
+ }
122
+ }
123
+ },
124
+ assumptions: {
125
+ type: "array",
126
+ items: {
127
+ type: "object",
128
+ required: ["assumption", "status"],
129
+ properties: {
130
+ assumption: { type: "string" },
131
+ status: { enum: ["verified", "unverified"] }
132
+ }
133
+ }
134
+ },
135
+ risks: { type: "array", items: { type: "string" } },
136
+ confidence: {
137
+ type: "object",
138
+ required: ["level", "reason"],
139
+ properties: {
140
+ level: { enum: ["high", "medium", "low"] },
141
+ reason: { type: "string" }
142
+ }
143
+ },
144
+ challengeClaims: { type: "array", items: { type: "string" }, maxItems: 3 },
145
+ ownerDecisions: { type: "array", items: { type: "string" } },
146
+ changeMyMind: { type: "array", items: { type: "string" } }
147
+ }
148
+ };
149
+ ```
150
+
151
+ Include this contract in each Pass 1 task: inspect supplied evidence directly; do
152
+ not see or ask about other advisors; stay read-only; do not spawn children; return
153
+ only the structured report.
154
+
155
+ After `runs.all`, return one aggregate receipt rather than making the parent find
156
+ separate artifacts. Preserve the result order or map it by stable key so each row
157
+ contains the advisor identity and report:
158
+
159
+ ```js
160
+ return {
161
+ pass: 1,
162
+ advisors: results.map((result, index) => ({
163
+ key: result.key,
164
+ agent: result.agent,
165
+ role: roster[index].role,
166
+ requestedContext: roster[index].context ?? "runtime-default-unknown",
167
+ runId: result.runId,
168
+ report: result.structuredOutput
169
+ }))
170
+ };
171
+ ```
172
+
173
+ Do not replace `runtime-default-unknown` with a guessed context. It records that
174
+ the launch intentionally omitted context.
175
+
176
+ A challenge packet contains only disputed claims, strong conflicting evidence,
177
+ missing proof, owner decisions, and high-impact risks. Attribute peer content as
178
+ "another advisor". Do not include full peer reports. Use a common Pass 2 contract:
179
+
180
+ ```js
181
+ const pass2OutputSchema = {
182
+ type: "object",
183
+ required: ["responses", "recommendationChanged", "outOfScopeFindings"],
184
+ properties: {
185
+ responses: {
186
+ type: "array",
187
+ items: {
188
+ type: "object",
189
+ required: ["claimId", "disposition", "reason", "sources"],
190
+ properties: {
191
+ claimId: { type: "string" },
192
+ disposition: {
193
+ enum: ["accept", "reject", "refine", "owner-decision"]
194
+ },
195
+ reason: { type: "string" },
196
+ sources: { type: "array", items: { type: "string" } }
197
+ }
198
+ }
199
+ },
200
+ recommendationChanged: {
201
+ type: "object",
202
+ required: ["changed", "reason"],
203
+ properties: { changed: { type: "boolean" }, reason: { type: "string" } }
204
+ },
205
+ outOfScopeFindings: { type: "array", items: { type: "string" } }
206
+ }
207
+ };
208
+ ```
209
+
210
+ Use stable resume keys such as `cross-oracle`, `phase: "Council pass 2"`, concise
211
+ labels, and `output: false` unless separate artifacts are requested or useful. The
212
+ aggregate Pass 2 receipt uses the same row shape as Pass 1, with the new `runId`
213
+ and `structuredOutput`.
214
+
215
+ ## Stop and memo
216
+
217
+ Converged means no disputed claim remains that both materially affects the
218
+ recommendation and can plausibly be settled by evidence. Stop at convergence, the
219
+ pass cap, failed fallback, or user interruption. Put unresolved disputes in owner
220
+ decisions. Never add a round for polish or symmetry.
221
+
222
+ The parent memo states the question and scope, recommendation, rationale, accepted
223
+ and rejected feedback with reasons, owner decisions, evidence and run ids,
224
+ confidence, what would change the decision, and the roster, roles, passes,
225
+ fallbacks, and known advisor context modes. State that fallback `oracle` is
226
+ context-aware and forked.
227
+
228
+ Council mode is not agent-to-agent chat, a transcript dump, mutation authority,
229
+ auto-escalation to writer lanes, or a council UI. Escalate to a writer only after
230
+ the parent memo and only when the user explicitly requests it.
@@ -22,6 +22,7 @@ Read the matching reference file before acting. Paths are relative to this `SKIL
22
22
  | --- | --- |
23
23
  | Decide whether to delegate, choose agents, compare tool versus slash commands, apply prompt techniques, or understand builtin roles | `references/prompting-and-roles.md` |
24
24
  | Run one-child, scripted, async, scheduled, mission-backed, forked, watchdog, oracle, or intercom-coordinated workflows | `references/execution-controls.md` |
25
+ | Coordinate several independent tasks, worktrees, repositories, or writer lanes | `references/multi-lane-orchestration.md` |
25
26
  | List/create/update/delete/eject/disable agents, inspect legacy chain records, edit agent files, use prompt-template integration, or expose extension RPC | `references/management-authoring-rpc.md` |
26
27
  | Check safety constraints, best practices, standard workflows, or error handling | `references/constraints-and-recipes.md` |
27
28
 
@@ -30,6 +31,7 @@ For broad or uncertain requests, read more than one reference. For complex work,
30
31
  ## Always-on constraints
31
32
 
32
33
  - Keep the parent as orchestrator and final decision-maker.
34
+ - Before multiple mutation-capable lanes, record a lane board and each lane's isolation path.
33
35
  - For plan, design, or architecture advice that asks to consult, discuss with, or come to agreement with `oracle`, use a short same-session consultation loop: read the first result, resume once with a targeted challenge when material tradeoffs remain, then synthesize the parent decision. Keep explicit one-shot, trivial, and fully settled consultations one-shot.
34
36
  - Use one writer per cwd/worktree unless isolated worktrees are intentional.
35
37
  - For cross-codebase work, record the target repo, explicit `cwd`, authority boundary, and expected output before launch. Do not assume the parent session cwd is the child repo.
@@ -36,6 +36,7 @@ In an interactive chat, do not call `subagent_wait()` merely to wait after launc
36
36
  - `subagent_wait()` — return when the next initially active async run or registered provider item finishes, or a subagent needs attention.
37
37
  - `subagent_wait({ all: true })` — block until every async run and provider item active at call time finishes, or a subagent needs attention.
38
38
  - `subagent_wait({ id: "..." })` — block on one async or remembered detached foreground run (id or prefix). Provider items are not selected through this parameter.
39
+ - `subagent_wait({ stopOnAttention: false })` — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
39
40
  - `subagent_wait({ timeoutMs })` — cap the block; active work keeps running if it elapses.
40
41
 
41
42
  Providers are discovered through the `pi-subagents/background-work` registry and must return stable item IDs with exact owning session IDs. Child agents receive no provider automatically: keep `subagent_wait` in the child `tools` allowlist and load provider extensions through `extensions` or `subagentOnlyExtensions`.
@@ -82,6 +82,17 @@ For one host-run verification command, pass `gate: "npm test"` on a `runs.run`/`
82
82
 
83
83
  Completed workflow children from this parent session stay addressable as retained children. `subagent({ action: "children.list" })` lists up to the last 10 with run ids and reports each row as `resumable` or `not resumable` with a reason. Resume only rows reported `resumable`. For a retained-child challenge, use `resume` instead of `steer` when the child is complete. If no retained child is resumable, launch a same-role fallback challenge and label it as fallback. A later workflow continues a resumable child with `runs.run(key, { resume: "<run-id>", task: "follow-up" })`. Inside `workflowScript`, awaiting that call waits for the revived child to finish and returns its completed output and new `runId`; top-level `{ action: "resume" }` remains detached. Pass explicit follow-up task text. Assign each returned child result back to the loop variable because every resume can return a new retained `runId`; always resume the latest returned id. `resume` and `agent` are mutually exclusive, the revived child keeps its stored agent/model/tool contract, and `gate` is rejected on retained resume items.
84
84
 
85
+ Terminal async workflows also persist `workflow-receipt.json` beside `status.json`. It maps each stable child key to its agent, requested and resolved context when known, latest run id, resumability, output reference, and continuation lineage. A later workflow can resume the latest retained child without copying its run id:
86
+
87
+ ```js
88
+ return runs.run("cross-oracle", {
89
+ resume: { workflowRunId: "<pass-1-workflow-id>", key: "advisor-oracle", latest: true },
90
+ task: "Review the focused challenge packet."
91
+ });
92
+ ```
93
+
94
+ Keyed resume reads that one exact receipt and revalidates the retained run at launch. It fails when the workflow or key is missing, the receipt is stale, `latest` is not `true`, or the recorded child is no longer resumable. Foreground workflow results expose the same receipt in `details.workflow.receipt`, but cross-workflow keyed lookup requires the durable receipt from an async workflow.
95
+
85
96
  ### Async/background
86
97
 
87
98
  Prefer async mode for every subagent launch. Set `async: true` no matter the task unless the parent must block until completion. This applies to scouts, researchers, workers, reviewers, validators, oracle checks, one-off delegates, final review gates, backlog gates, and scripted workflows. Keep the write path single-threaded even when the run is async.
@@ -0,0 +1,39 @@
1
+ # Multi-Lane Orchestration
2
+
3
+ Use this reference when several independent tasks need coordinated workers, worktrees, or repositories. It defines lane ownership; use the other pi-subagents references for run controls, prompts, and mission details. The parent remains the final decision-maker.
4
+
5
+ ## Lane board and authority
6
+
7
+ Before multiple mutation-capable lanes start, record this board in the parent context:
8
+
9
+ `Lane | repo/cwd | exact decision | claimed files or contract | isolation path | authority | next gate | handoff | why independent`
10
+
11
+ Record the isolation path before the first mutation. Do not split one source seam or decision into duplicate lanes. Make overlapping work one lane with one source of truth.
12
+
13
+ For every lane, record the delivery target, allowed actions, required validation, and review rigor. For cross-repository work, name the shared contract and which repository changes first. A blocked decision is a lane state: record the owner, options, recommended default, and evidence needed to continue.
14
+
15
+ ## Partitioned runs
16
+
17
+ Use one writer per repo/cwd or worktree. Mutation lanes need distinct isolation paths and explicit `cwd` values. Set `worktree: true` when a run needs managed worktree isolation within one repository. Read-only runs can share a checkout only when they cannot change state or create generated files.
18
+
19
+ For Pi extension repositories, keep lane worktrees outside auto-discovered extension directories such as `~/.pi/agent/extensions`. A stale extension worktree there can auto-load duplicate tools and shortcuts. Remove or move it only after its handoff is durable, the worktree is clean, and no run owns it.
20
+
21
+ Partition fanout by repository, source seam, decision, or review angle. Each run needs a stable key, lane-specific task, and durable output path. Do not launch prompts that differ only by item name or broad file glob.
22
+
23
+ Use one async `workflowScript` for a coordinated wave. Use `runs.all` for independent lanes and `runs.run` for dependent lane stages. Give cross-repository runs explicit `cwd` values and lane-qualified outputs. Use `outputMode: "file-only"` when a report must survive the run or feed a later stage.
24
+
25
+ ## Keep independent work moving
26
+
27
+ While one lane waits, run safe independent preparation, validation, or fresh read-only review lanes. Do not block the parent just because a run is active. If no safe lane remains, record the blocker and the event that will reopen work.
28
+
29
+ An ordinary coordinated workflow has one mission. Use its durable state, artifacts, run records, and receipts for recovery. Treat a receipt as evidence, not as authority or acceptance.
30
+
31
+ After a writer produces a candidate, run the required fresh-context, read-only reviewer. The reviewer inspects the exact worktree and returns evidence-backed findings. The parent decides which findings are in scope and whether the lane is ready. Send accepted fixes to that lane's sole writer, then rerun only the affected gate.
32
+
33
+ ## Handoff, cleanup, and recovery
34
+
35
+ Use stable lane-qualified paths for reports and review output. A handoff states the lane status, repository and worktree, changed files, validation, open decisions, next action, and artifact or receipt paths.
36
+
37
+ Keep a worktree until its handoff is durable, no run owns it, and no later gate needs it. Clean up only inside the recorded authority boundary. If a run stops or needs attention, preserve its worktree and artifacts, record the last known state and recovery owner, then resume that run or create one replacement lane from the handoff. Do not start another writer while worktree ownership is uncertain.
38
+
39
+ Before completion, inspect the board. Every lane must be terminal or blocked with a named next action. Confirm one writer per repo/cwd or worktree, required validation, required fresh read-only review, and a durable handoff. The parent reports outcomes, evidence, residual risks, and the next decision.
@@ -36,10 +36,11 @@ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
36
36
  import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
37
37
  import { getProjectConfigDir } from "../shared/utils.ts";
38
38
  import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
39
+ import { listRuntimeAgentConfigs, mergeRuntimeAgents, type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
39
40
 
40
41
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
41
42
  type ManagementScope = "user" | "project";
42
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string };
43
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string; runtimeAgentOwner?: RuntimeAgentOwner };
43
44
 
44
45
  interface ManagementParams {
45
46
  action?: string;
@@ -142,7 +143,7 @@ function diagnosticsForScope(diagnostics: AgentDiscoveryDiagnostic[] | undefined
142
143
  return diagnostics?.filter((diagnostic) => diagnostic.source !== excludedSource);
143
144
  }
144
145
 
145
- const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
146
+ const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3, runtime: 4 };
146
147
 
147
148
  // Returns the highest-precedence definition for a resolved canonical name (project > user > package > builtin),
148
149
  // matching mergeAgentsForScope for "both", including disabled agents so disable/enable can locate hidden targets.
@@ -213,6 +214,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
213
214
  const base = agent.override?.base;
214
215
  const {
215
216
  override: _override,
217
+ outputMode: _outputMode,
216
218
  model: _model,
217
219
  fallbackModels: _fallbackModels,
218
220
  thinking: _thinking,
@@ -240,6 +242,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
240
242
 
241
243
  return withDeclaredExtensionPaths({
242
244
  ...editable,
245
+ ...(base.outputMode !== undefined ? { outputMode: base.outputMode } : {}),
243
246
  ...(base.model !== undefined ? { model: base.model } : {}),
244
247
  ...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
245
248
  ...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
@@ -311,6 +314,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
311
314
  if (hasKey(cfg, "acceptance")) changed("acceptance");
312
315
  if (hasKey(cfg, "acceptanceRole")) changed("acceptanceRole");
313
316
  if (hasKey(cfg, "output")) changed("output");
317
+ if (hasKey(cfg, "outputMode")) changed("outputMode");
314
318
  if (hasKey(cfg, "reads")) changed("defaultReads");
315
319
  if (hasKey(cfg, "progress")) changed("defaultProgress");
316
320
  if (hasKey(cfg, "maxSubagentDepth")) changed("maxSubagentDepth");
@@ -501,6 +505,10 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
501
505
  else if (typeof cfg.output === "string") target.output = cfg.output;
502
506
  else return "config.output must be a string or false when provided.";
503
507
  }
508
+ if (hasKey(cfg, "outputMode")) {
509
+ if (cfg.outputMode === "inline" || cfg.outputMode === "file-only") target.outputMode = cfg.outputMode;
510
+ else return "config.outputMode must be 'inline' or 'file-only' when provided.";
511
+ }
504
512
  if (hasKey(cfg, "reads")) {
505
513
  if (cfg.reads === false || cfg.reads === "") delete target.defaultReads;
506
514
  else if (typeof cfg.reads === "string") {
@@ -610,6 +618,7 @@ function formatAgentDetail(agent: AgentConfig): string {
610
618
  if (agent.subagentOnlyExtensions !== undefined) lines.push(`Subagent-only extensions: ${agent.subagentOnlyExtensions.length ? agent.subagentOnlyExtensions.join(", ") : "(none)"}`);
611
619
  if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
612
620
  if (agent.output) lines.push(`Output: ${agent.output}`);
621
+ if (agent.outputMode) lines.push(`Output mode: ${agent.outputMode}`);
613
622
  if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
614
623
  if (agent.defaultProgress) lines.push("Progress: true");
615
624
  if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
@@ -623,7 +632,17 @@ function formatAgentDetail(agent: AgentConfig): string {
623
632
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
624
633
  const scope = normalizeListScope(params.agentScope) ?? "both";
625
634
  const d = discoverAgentsAll(ctx.cwd);
626
- const scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
635
+ let scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
636
+ if (ctx.runtimeAgentOwner && listRuntimeAgentConfigs(ctx.runtimeAgentOwner).length > 0) {
637
+ const configuredAgents: AgentConfig[] = [
638
+ ...d.builtin,
639
+ ...d.package,
640
+ ...d.user,
641
+ ...d.project,
642
+ ];
643
+ scopedAgents = mergeRuntimeAgents(ctx.runtimeAgentOwner, { agents: scopedAgents }, configuredAgents).agents;
644
+ }
645
+ scopedAgents = scopedAgents
627
646
  .sort((a, b) => a.name.localeCompare(b.name));
628
647
  const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
629
648
  const visibleAgents = scopedAgents.filter((a) => !a.disabled);
@@ -28,6 +28,7 @@ export const KNOWN_FIELDS = new Set([
28
28
  "extensions",
29
29
  "subagentOnlyExtensions",
30
30
  "output",
31
+ "outputMode",
31
32
  "defaultReads",
32
33
  "defaultProgress",
33
34
  "interactive",
@@ -113,6 +114,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
113
114
  }
114
115
 
115
116
  if (config.output) lines.push(`output: ${config.output}`);
117
+ if (config.outputMode || preserve("outputMode")) lines.push(`outputMode: ${config.outputMode ?? ""}`);
116
118
 
117
119
  const readsValue = joinComma(config.defaultReads);
118
120
  if (readsValue) lines.push(`defaultReads: ${readsValue}`);