@selesai/code 0.13.3 → 0.13.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/defaults/models.json +17 -1
  3. package/dist/extensions/cost-reconcile.test.ts +24 -5
  4. package/dist/extensions/cost-reconcile.ts +14 -7
  5. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  6. package/dist/extensions/pi-intercom/README.md +4 -5
  7. package/dist/extensions/pi-intercom/config.test.ts +3 -31
  8. package/dist/extensions/pi-intercom/config.ts +0 -15
  9. package/dist/extensions/pi-intercom/index.ts +9 -46
  10. package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
  11. package/dist/extensions/pi-intercom/package.json +1 -1
  12. package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
  13. package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
  14. package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
  15. package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
  16. package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
  17. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  18. package/dist/extensions/pi-subagents/package.json +1 -1
  19. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
  20. package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
  21. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
  22. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  23. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
  24. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  25. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  27. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
  28. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
  29. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  30. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  31. package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
  32. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
  37. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
  39. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
  40. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
  41. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
  42. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
  43. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
  44. package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
  45. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  46. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
  47. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
  48. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
  49. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
  50. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
  52. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
  53. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
  54. package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
  55. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
  56. package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
  57. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
  58. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
  59. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
  60. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
  61. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
  62. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
  63. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
  64. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
  65. package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
  66. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
  68. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
  69. package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
  70. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
  71. package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
  72. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
  73. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
  74. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
  75. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
  76. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
  77. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
  78. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
  79. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
  80. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
  81. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
  82. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
  83. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
  84. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
  85. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
  86. package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
  87. package/dist/extensions/pi-web-agent/README.md +131 -0
  88. package/dist/extensions/pi-web-agent/package.json +4 -2
  89. package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
  90. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  91. package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
  92. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
  93. package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
  94. package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
  95. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
  96. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
  97. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
  98. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
  99. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
  100. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
  101. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
  102. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
  103. package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
  104. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
  105. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
  106. package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
  107. package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
  108. package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
  109. package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
  110. package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
  111. package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
  112. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
  113. package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
  114. package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
  115. package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
  116. package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
  117. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
  118. package/dist/extensions/pi-web-agent/src/types.ts +19 -4
  119. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
+ ## [0.13.4] - 2026-08-31
6
+
7
+ ### Added
8
+ - **Bundled `pi-subagents` 0.60.0.** `subagent({ action: "list", capabilities: true })` returns a compact prompt-free capability catalog (`details.catalog`: agent name, description, source, runner, tools/model/execution/output/extensions snapshot, executable and restriction status), and `list` results stay human text. Forked children now stamp an OpenAI-compatible `prompt_cache_key` (`pi-fork:<sha256>`) on provider requests for prompt-cache reuse; timeout recovery evidence (report status, changed tracked files, dirty-worktree classification) is projected into async status and `subagent_wait` completions; `runs.lanes` lane plans (stages, phase, label, structured output) are emitted and rendered as workflow-graph stages; `runs.host` rejects per-step `cwd` with a hint; watchdog repo signatures skip untracked scans for home-root and entry-less repos; single-output snapshots propagate non-ENOENT stat errors; background processes use `windowsHide` and detached-only-on-unix.
9
+ - **Bundled `pi-web-agent` 1.10.0.** Search backends: You.com (`YDC_API_KEY`), Exa (`EXA_API_KEY`), Tavily (`TAVILY_API_KEY`) in addition to DuckDuckGo, SearXNG and Brave; search fanout across configured providers with dedupe/agreement ranking (`backends.search.fanout` with `off`/`on`/`auto` modes and provider selection); keyless Tavily fallback when DuckDuckGo is bot-blocked (disable with `PI_WEB_AGENT_DISABLE_KEYLESS_FALLBACK=1`); DuckDuckGo hardened against bot-walls (browser headers + one retry); GitHub/PDF/YouTube direct readers wired into the fetch path; canonical URL normalization (tracking-param stripping); the model tool result now carries the full synthesized findings/sources/caveats regardless of terminal presentation.
10
+ - **GLM-5.3 models in the default catalog.** Added `glm-5.3` (GLM-5.3) to the bundled `tokenin` provider and upgraded `glm-5.3-flash` to text+image input with a full `thinkingLevelMap`.
11
+
12
+ ### Changed
13
+ - **Cost reconciliation keys every streamed response id.** Streamed bodies repeat the response id across chunks and carry tool-call ids (`call_*`), so the fetch wrapper now keys the billed cost by every id found and `message_end` consumes only the one matching the finalized message's `responseId`; a duplicate capture (retry) is marked ambiguous instead of assigning either bill.
14
+ - **Bundled `pi-intercom` 0.12.1.** During a turn triggered by an inbound ask, a non-`reply` `send` to a different target is refused so guessed parent/root CWD cannot receive an accidental reply; the `toolVisibility` setting and `after-first-use` reveal path were removed (the generic `intercom` tool stays in the active tool set for prompt-cache stability; obsolete `toolVisibility` config keys are ignored).
15
+
5
16
  ## [0.13.3] - 2026-08-31
6
17
 
7
18
  ### Added
@@ -224,7 +224,23 @@
224
224
  "id": "glm-5.3-flash",
225
225
  "name": "GLM-5.3 Flash",
226
226
  "reasoning": true,
227
- "input": ["text"],
227
+ "input": ["text", "image"],
228
+ "contextWindow": 1000000,
229
+ "maxTokens": 64000,
230
+ "thinkingLevelMap": {
231
+ "minimal": null,
232
+ "low": "low",
233
+ "medium": null,
234
+ "high": "high",
235
+ "xhigh": null,
236
+ "max": "max"
237
+ }
238
+ },
239
+ {
240
+ "id": "glm-5.3",
241
+ "name": "GLM-5.3",
242
+ "reasoning": true,
243
+ "input": ["text", "image"],
228
244
  "contextWindow": 1000000,
229
245
  "maxTokens": 64000,
230
246
  "thinkingLevelMap": {
@@ -180,17 +180,36 @@ describe("live assistant usage reconciliation", () => {
180
180
  expect(result.message.usage.cost.total).toBe(0);
181
181
  });
182
182
 
183
- it("falls back when a captured body has multiple candidate response ids", async () => {
184
- const responseId = "live-ambiguous-response-3";
185
- installFetch(
186
- `data: {"id":"${responseId}","usage":{"cost":0.0123},"tool_calls":[{"id":"tool-ambiguous-3"}]}\n\n`,
187
- );
183
+ it("reconciles a tool-call stream: keys cost by every id, message_end consumes the matching one", async () => {
184
+ const responseId = "live-toolcall-3";
185
+ const body =
186
+ `data: {"id":"${responseId}","choices":[{"delta":{"tool_calls":[{"id":"call_abc123","function":{"name":"bash","arguments":""}}]}}]}\n\n` +
187
+ `data: {"id":"${responseId}","usage":{"cost":0.0123}}\n\n` +
188
+ "data: [DONE]\n\n";
189
+ installFetch(body);
188
190
  const { appendEntry, handlers } = createHarness();
189
191
  await startSession(handlers);
190
192
 
191
193
  const response = await globalThis.fetch("https://gateway.example/v1/chat/completions");
192
194
  await response.text();
193
195
  const message = assistantMessage(responseId);
196
+ const result = await handlers.get("message_end")!({ message }, {});
197
+ expect(result.message.usage.cost.total).toBe(0.0123);
198
+ expect(appendEntry).toHaveBeenCalledWith(
199
+ "cost-reconcile",
200
+ expect.objectContaining({ provider: "openrouter", responseId, cost: 0.0123, source: "payload" }),
201
+ );
202
+ });
203
+
204
+ it("does not apply a captured cost to a message whose id was not in the body", async () => {
205
+ const body = `data: {"id":"other-response","usage":{"cost":0.0123}}\n\ndata: [DONE]\n\n`;
206
+ installFetch(body);
207
+ const { appendEntry, handlers } = createHarness();
208
+ await startSession(handlers);
209
+
210
+ const response = await globalThis.fetch("https://gateway.example/v1/chat/completions");
211
+ await response.text();
212
+ const message = assistantMessage("unrelated-message");
194
213
  expect(await handlers.get("message_end")!({ message }, {})).toBeUndefined();
195
214
  expect(message.usage.cost.total).toBe(0.75);
196
215
  expect(appendEntry).not.toHaveBeenCalled();
@@ -16,8 +16,11 @@
16
16
  * finishes capture before the provider SDK finalizes its assistant message, so
17
17
  * `message_end` can replace `usage.cost.total` before session persistence.
18
18
  *
19
- * Providers whose payloads carry no cost retain their rate-card estimate.
20
- * Reconciled entries remain persisted for zentui and existing-session
19
+ * Streamed bodies repeat the response id across chunks and also carry
20
+ * tool-call ids (`call_*`), so the billed cost is keyed by every id found and
21
+ * `message_end` consumes only the one matching the finalized message's
22
+ * `responseId`. Providers whose payloads carry no cost retain their rate-card
23
+ * estimate. Reconciled entries remain persisted for zentui and existing-session
21
24
  * compatibility.
22
25
  */
23
26
 
@@ -146,11 +149,15 @@ export default function costReconcileExtension(pi: ExtensionAPI): void {
146
149
  // LiteLLM's header is the amount the gateway billed; prefer it over any
147
150
  // cost the upstream payload reports.
148
151
  const effectiveCost = headerCost ?? cost;
149
- // A cost must identify exactly one response. Applying it to every `id` in
150
- // a body can mistake nested tool/message ids for the assistant response.
151
- if (effectiveCost === undefined || ids.length !== 1) return;
152
- const responseId = ids[0]!;
153
- processCaptured.set(responseId, processCaptured.has(responseId) ? null : effectiveCost);
152
+ if (effectiveCost === undefined || ids.length === 0) return;
153
+ // A streamed response repeats its own id across chunks while tool-call
154
+ // ids (`call_*`) appear once, so a body legitimately carries several ids.
155
+ // Key the billed cost by every id; message_end consumes only the one
156
+ // matching the finalized message's responseId. A duplicate capture of the
157
+ // same id (retry) is marked ambiguous rather than assigning either bill.
158
+ for (const responseId of ids) {
159
+ processCaptured.set(responseId, processCaptured.has(responseId) ? null : effectiveCost);
160
+ }
154
161
  if (processCaptured.size > 500) {
155
162
  for (const key of [...processCaptured.keys()].slice(0, processCaptured.size - 500))
156
163
  processCaptured.delete(key);
@@ -4,6 +4,19 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.12.1] - 2026-08-29
8
+
9
+ ### Highlights
10
+ - Replies to inbound asks are now harder to send to the wrong local session by mistake.
11
+ - The `intercom` tool now stays in the active tool set, which avoids a late-session prompt-cache reset when intercom first becomes useful.
12
+ - Existing configs that still mention `toolVisibility` keep loading; the old setting is simply ignored.
13
+
14
+ ### Fixed
15
+ - Refuse non-reply `send` calls to a different target during a turn triggered by an inbound ask, preventing CWD hierarchy or roster guesses from misdirecting replies. Thanks to [@yceachan](https://github.com/yceachan) for issue #117.
16
+
17
+ ### Removed
18
+ - Removed `toolVisibility` and the `after-first-use` reveal path. The generic `intercom` schema and prompt snippet now stay stable for provider prompt caches, and existing `toolVisibility` config keys are ignored. Thanks to [@XWIlluDelu](https://github.com/XWIlluDelu) for issue #118.
19
+
7
20
  ## [0.12.0] - 2026-08-22
8
21
 
9
22
  ### Highlights
@@ -218,7 +218,7 @@ This matters because the agent receiving the message doesn't need to reconstruct
218
218
 
219
219
  ### `send` vs `ask`
220
220
 
221
- `send` is fire-and-forget — the tool returns immediately after delivery. When the destination has exactly one pending inbound ask, `send` infers that it is the answer, attaches the ask's `replyTo`, and reports `Reply sent to <target> (inferred from pending ask)`. With zero or multiple matching asks, it remains an ordinary unthreaded send. An inferred answer still uses the `confirmSend` dialog when configured; only a caller-supplied `replyTo` skips confirmation.
221
+ `send` is fire-and-forget — the tool returns immediately after delivery. When the destination has exactly one pending inbound ask, `send` infers that it is the answer, attaches the ask's `replyTo`, and reports `Reply sent to <target> (inferred from pending ask)`. During a turn triggered by an inbound ask, `send` refuses a different non-reply target instead of treating CWD, roster position, or project hierarchy as reply authority. With zero or multiple matching asks, it remains an ordinary unthreaded send. An inferred answer still uses the `confirmSend` dialog when configured; only a caller-supplied `replyTo` skips confirmation.
222
222
 
223
223
  `ask` requires a currently connected recipient, then blocks until it responds (10-minute timeout by default; set `PI_INTERCOM_ASK_TIMEOUT_MS` to a positive millisecond value to change it). If the target is disconnected, `ask` fails immediately; use `send` when queued, non-blocking mailbox delivery is appropriate. The reply comes back as the tool result, so the agent continues in the same turn with full context. No confirmation dialog — if you're asking and waiting, the intent is clear.
224
224
 
@@ -366,7 +366,7 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
366
366
 
367
367
  **`list`** — Returns the current session plus other active intercom-connected sessions with name, short ID, working directory, model, and live status. Status is derived automatically from Selesai lifecycle events: `idle`, `thinking`, or `tool:<name>`.
368
368
 
369
- **`send`** — Sends a message to the specified session and returns immediately after delivery. If the destination has exactly one pending inbound ask, `send` infers the message is its answer and returns `Reply sent to <target> (inferred from pending ask)`; zero or multiple matches remain unthreaded sends. Set `confirmSend: true` to confirm ordinary and inferred sends. A caller-supplied `replyTo` skips confirmation. `to` alone resolves globally across all live sessions. `cwd` alone targets the sole live peer in that directory. `to` plus `cwd` requires that peer to be in the directory. With `openProjectPaneIfMissing: true`, pi-intercom opens a visible Herdr project pane, starts Selesai there, waits for that session to register, then delivers the message through normal intercom routing.
369
+ **`send`** — Sends a message to the specified session and returns immediately after delivery. If the destination has exactly one pending inbound ask, `send` infers the message is its answer and returns `Reply sent to <target> (inferred from pending ask)`. During a turn triggered by an inbound ask, a non-reply `send` to a different target is rejected so a guessed parent/root CWD cannot receive an accidental reply. Zero or multiple pending-ask matches remain unthreaded sends outside the active ask turn. Set `confirmSend: true` to confirm ordinary and inferred sends. A caller-supplied `replyTo` skips confirmation. `to` alone resolves globally across all live sessions. `cwd` alone targets the sole live peer in that directory. `to` plus `cwd` requires that peer to be in the directory. With `openProjectPaneIfMissing: true`, pi-intercom opens a visible Herdr project pane, starts Selesai there, waits for that session to register, then delivers the message through normal intercom routing.
370
370
 
371
371
  **`ask`** — Requires a currently connected recipient, sends a message, and waits for the recipient to reply (10-minute timeout by default; configurable with `PI_INTERCOM_ASK_TIMEOUT_MS`). A disconnected target fails immediately rather than queueing a blocking request. The reply is returned as the tool result. No confirmation dialog. Only one pending `ask` is allowed per session at a time. Use this when the agent needs the answer to continue working. The same `to`, `cwd`, and `openProjectPaneIfMissing` targeting rules apply.
372
372
 
@@ -397,7 +397,6 @@ Create `~/.selesai/agent/intercom/config.json`:
397
397
  "brokerArgs": ["--no-install", "tsx"],
398
398
  "confirmSend": false,
399
399
  "inboundTrigger": "always",
400
- "toolVisibility": "always",
401
400
  "enabled": true,
402
401
  "replyHint": true,
403
402
  "status": "researching"
@@ -410,12 +409,12 @@ Create `~/.selesai/agent/intercom/config.json`:
410
409
  | `brokerArgs` | `["--no-install", "tsx"]` | Advanced trusted arguments passed to custom `brokerCommand` before the broker script path |
411
410
  | `confirmSend` | false | Show a confirmation dialog before ordinary or inferred sends from an interactive session with UI; caller-supplied `replyTo` skips it |
412
411
  | `inboundTrigger` | `"always"` | Auto-trigger policy for inbound broker messages: `"always"`, `"replies"`, or `"never"`. Local in-process subagent relay events still trigger the addressed session. |
413
- | `toolVisibility` | `"always"` | When the generic `intercom` tool enters the active model tool set: `"always"` or `"after-first-use"`. Lazy visibility reveals it after an inbound broker message, a successful overlay send, or loading the bundled skill. It does not hide the child-only `contact_supervisor` tool. |
414
412
  | `enabled` | true | Enable/disable intercom entirely |
415
413
  | `replyHint` | true | Include reply instruction in incoming messages |
416
414
  | `status` | — | Optional custom status suffix shown after the automatic lifecycle status, for example `thinking · researching` |
417
415
 
418
416
  If `config.json` cannot be parsed or contains an invalid value, pi-intercom logs the error and fails closed for inbound broker auto-triggering by using `inboundTrigger: "never"` until the config is fixed.
417
+ Obsolete `toolVisibility` values are ignored; the generic `intercom` tool remains stable in the active tool set for prompt-cache friendliness.
419
418
 
420
419
  Custom broker commands are trusted local configuration: anyone who can edit this config can choose the executable used for future broker auto-spawns. For example, if you have Bun installed and want it to start the broker directly, use:
421
420
 
@@ -542,7 +541,7 @@ Runtime files live at `~/.selesai/agent/intercom/` by default, or `$SELESAI_CODI
542
541
  - `broker.port.json` — Dynamic localhost TCP endpoint, only when Windows TCP transport is explicitly enabled
543
542
  - `config.json` — User configuration
544
543
 
545
- Supported `config.json` keys include `stableId` for restart-stable addressing, `status` for a custom status suffix, `inboundTrigger` (`always`, `replies`, or `never`), `toolVisibility` (`always` or `after-first-use`), `replyHint`, `confirmSend`, and advanced broker launch overrides.
544
+ Supported `config.json` keys include `stableId` for restart-stable addressing, `status` for a custom status suffix, `inboundTrigger` (`always`, `replies`, or `never`), `replyHint`, `confirmSend`, and advanced broker launch overrides.
546
545
 
547
546
  ## Design Decisions
548
547
 
@@ -60,24 +60,13 @@ test("loadConfig accepts inboundTrigger replies policy", async () => {
60
60
  }
61
61
  });
62
62
 
63
- test("loadConfig defaults intercom tool visibility to always", async () => {
64
- const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
65
- try {
66
- await withAgentDir(root, () => {
67
- assert.equal(loadConfig().toolVisibility, "always");
68
- });
69
- } finally {
70
- rmSync(root, { recursive: true, force: true });
71
- }
72
- });
73
-
74
- test("loadConfig accepts lazy intercom tool visibility", async () => {
63
+ test("loadConfig ignores obsolete toolVisibility values", async () => {
75
64
  const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
76
65
  try {
77
66
  mkdirSync(join(root, "intercom"), { recursive: true });
78
- writeFileSync(join(root, "intercom", "config.json"), JSON.stringify({ toolVisibility: "after-first-use" }));
67
+ writeFileSync(join(root, "intercom", "config.json"), JSON.stringify({ toolVisibility: "lazy", replyHint: false }));
79
68
  await withAgentDir(root, () => {
80
- assert.equal(loadConfig().toolVisibility, "after-first-use");
69
+ assert.equal(loadConfig().replyHint, false);
81
70
  });
82
71
  } finally {
83
72
  rmSync(root, { recursive: true, force: true });
@@ -97,23 +86,6 @@ test("loadConfig accepts a restart-stable intercom id", async () => {
97
86
  }
98
87
  });
99
88
 
100
- test("loadConfig rejects invalid toolVisibility values", async () => {
101
- const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
102
- try {
103
- mkdirSync(join(root, "intercom"), { recursive: true });
104
- writeFileSync(join(root, "intercom", "config.json"), JSON.stringify({ toolVisibility: "lazy" }));
105
-
106
- await withAgentDir(root, () => {
107
- assert.throws(
108
- () => loadConfig(),
109
- /Failed to load intercom config.*"toolVisibility" must be "always" or "after-first-use"/,
110
- );
111
- });
112
- } finally {
113
- rmSync(root, { recursive: true, force: true });
114
- }
115
- });
116
-
117
89
  test("loadConfig rejects invalid inboundTrigger values", async () => {
118
90
  const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
119
91
  try {
@@ -24,7 +24,6 @@ export function getIntercomScopeId(env: NodeJS.ProcessEnv = process.env): string
24
24
  }
25
25
 
26
26
  export type InboundTriggerPolicy = "always" | "replies" | "never";
27
- export type IntercomToolVisibility = "always" | "after-first-use";
28
27
 
29
28
  export interface IntercomConfig {
30
29
  /** Broker command used to spawn the broker process (e.g. "npx" or "bun") */
@@ -39,9 +38,6 @@ export interface IntercomConfig {
39
38
  /** Controls whether inbound broker messages may automatically trigger a model turn */
40
39
  inboundTrigger: InboundTriggerPolicy;
41
40
 
42
- /** Controls when the intercom tool enters the active model tool set */
43
- toolVisibility: IntercomToolVisibility;
44
-
45
41
  /** Optional custom status suffix shown after automatic lifecycle status */
46
42
  status?: string;
47
43
 
@@ -64,7 +60,6 @@ const defaults: IntercomConfig = {
64
60
  brokerArgs: ["--no-install", "tsx"],
65
61
  confirmSend: false,
66
62
  inboundTrigger: "always",
67
- toolVisibility: "always",
68
63
  enabled: true,
69
64
  replyHint: true,
70
65
  };
@@ -135,16 +130,6 @@ export function loadConfig(): IntercomConfig {
135
130
  config.inboundTrigger = parsedConfig.inboundTrigger;
136
131
  }
137
132
 
138
- if (Object.hasOwn(parsedConfig, "toolVisibility")) {
139
- if (
140
- parsedConfig.toolVisibility !== "always"
141
- && parsedConfig.toolVisibility !== "after-first-use"
142
- ) {
143
- throw new Error(`"toolVisibility" must be "always" or "after-first-use"`);
144
- }
145
- config.toolVisibility = parsedConfig.toolVisibility;
146
- }
147
-
148
133
  if (Object.hasOwn(parsedConfig, "replyHint")) {
149
134
  if (typeof parsedConfig.replyHint !== "boolean") {
150
135
  throw new Error(`"replyHint" must be a boolean`);
@@ -27,15 +27,12 @@ import {
27
27
  type IntercomOutboxResultV1,
28
28
  } from "./extension-api.ts";
29
29
  import { ReplyTracker } from "./reply-tracker.ts";
30
- import { realpathSync } from "node:fs";
31
30
  import { resolve as resolvePath } from "node:path";
32
- import { fileURLToPath } from "node:url";
33
31
  import { sameCwd } from "./cwd.ts";
34
32
  import { formatContextUsage } from "./format-context.ts";
35
33
  import { openProjectPane, resolveTargetInCwd, waitForProjectSession, type ProjectPaneLaunch } from "./project-agent.ts";
36
34
 
37
35
  const INTERCOM_TOOL_NAME = "intercom";
38
- const INTERCOM_SKILL_PATH = realpathSync(fileURLToPath(new URL("./skills/pi-intercom/SKILL.md", import.meta.url)));
39
36
  const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
40
37
  const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
41
38
  const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
@@ -116,18 +113,6 @@ function getErrorMessage(error: unknown): string {
116
113
  return error instanceof Error ? error.message : String(error);
117
114
  }
118
115
 
119
- function isIntercomSkillRead(input: unknown, cwd: string): boolean {
120
- if (!input || typeof input !== "object") return false;
121
- const inputPath = Reflect.get(input, "path");
122
- if (typeof inputPath !== "string") return false;
123
- const normalizedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
124
- try {
125
- return realpathSync(resolvePath(cwd, normalizedPath)) === INTERCOM_SKILL_PATH;
126
- } catch {
127
- return false;
128
- }
129
- }
130
-
131
116
  function deliveryDetails(result: SendResult): Record<string, unknown> {
132
117
  return {
133
118
  messageId: result.id,
@@ -622,23 +607,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
622
607
  let runtimeGeneration = 0;
623
608
  let agentRunning = false;
624
609
  const activeTools = new Map<string, string>();
625
- let intercomToolHiddenByPolicy = false;
626
610
  const replyTracker = new ReplyTracker();
627
- function hideIntercomTool(): void {
628
- if (config.toolVisibility !== "after-first-use") return;
629
- const activeToolNames = pi.getActiveTools();
630
- if (!activeToolNames.includes(INTERCOM_TOOL_NAME)) return;
631
- pi.setActiveTools(activeToolNames.filter((name) => name !== INTERCOM_TOOL_NAME));
632
- intercomToolHiddenByPolicy = true;
633
- }
634
- function activateIntercomTool(): void {
635
- if (!intercomToolHiddenByPolicy) return;
636
- const activeToolNames = pi.getActiveTools();
637
- if (!activeToolNames.includes(INTERCOM_TOOL_NAME)) {
638
- pi.setActiveTools([...activeToolNames, INTERCOM_TOOL_NAME]);
639
- }
640
- intercomToolHiddenByPolicy = false;
641
- }
642
611
 
643
612
  const seenInboundMessages = new Map<string, number>();
644
613
  const latestOutboundReceipts = new Map<string, { status: MessageReceiptStatus; timestamp: number; detail?: string }>();
@@ -1221,7 +1190,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1221
1190
  );
1222
1191
  }
1223
1192
  function sendIncomingBrokerMessage(entry: InboundMessageEntry, delivery: "trigger" | "steer", generation = runtimeGeneration): void {
1224
- activateIntercomTool();
1225
1193
  sendIncomingMessage(entry, delivery, generation);
1226
1194
  }
1227
1195
  function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message): void {
@@ -1572,7 +1540,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1572
1540
  });
1573
1541
  }
1574
1542
  function startSessionRuntime(ctx: ExtensionContext): void {
1575
- hideIntercomTool();
1576
1543
  const previousClient = client;
1577
1544
  failPendingOutboxRequests(runtimeGeneration, "session_ended", "Session replaced");
1578
1545
  shuttingDown = false;
@@ -1819,17 +1786,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1819
1786
  return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText, !options.expanded);
1820
1787
  });
1821
1788
 
1822
- pi.on("input", (event) => {
1823
- if (/^\/skill: pi-intercom(?:\s|$)/u.test(event.text.trimStart())) {
1824
- activateIntercomTool();
1825
- }
1826
- });
1827
-
1828
- pi.on("tool_result", (event, ctx) => {
1829
- if (event.toolName === "read" && event.isError !== true && isIntercomSkillRead(event.input, ctx.cwd)) {
1830
- activateIntercomTool();
1831
- return;
1832
- }
1789
+ pi.on("tool_result", (event) => {
1833
1790
  if (event.toolName !== "intercom" && event.toolName !== "contact_supervisor") {
1834
1791
  return;
1835
1792
  }
@@ -2181,7 +2138,6 @@ Usage:
2181
2138
  }),
2182
2139
 
2183
2140
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2184
- activateIntercomTool();
2185
2141
  let connectedClient: IntercomClient;
2186
2142
  try {
2187
2143
  connectedClient = await ensureConnected("tool");
@@ -2350,6 +2306,14 @@ Usage:
2350
2306
  details: { error: true },
2351
2307
  };
2352
2308
  }
2309
+ const activeReplyMismatch = replyTo ? null : replyTracker.findActiveReplyTargetMismatch(sendTo);
2310
+ if (activeReplyMismatch) {
2311
+ const senderLabel = activeReplyMismatch.from.name || activeReplyMismatch.from.id;
2312
+ return {
2313
+ content: [{ type: "text", text: `This turn is responding to an intercom ask from "${senderLabel}". Use intercom({ action: "reply", message: "..." }) or set replyTo: "${activeReplyMismatch.message.id}". Refusing non-reply send to "${targetDisplay}" to avoid a misdirected reply.` }],
2314
+ details: { error: true, replyTo: activeReplyMismatch.message.id },
2315
+ };
2316
+ }
2353
2317
  const inferredAsk = replyTo ? null : replyTracker.findUniquePendingAskFrom(sendTo);
2354
2318
  const effectiveReplyTo = replyTo ?? inferredAsk?.message.id;
2355
2319
  if (confirmSend && !(cwd && openProjectPaneIfMissing)) {
@@ -2761,7 +2725,6 @@ Usage:
2761
2725
  ).catch(() => undefined);
2762
2726
 
2763
2727
  if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) {
2764
- activateIntercomTool();
2765
2728
  pi.appendEntry("intercom_sent", {
2766
2729
  to: selectedSession.name || selectedSession.id,
2767
2730
  message: { text: result.text },
@@ -1672,59 +1672,7 @@ test("intercom tool result hook marks failed details as errors", async () => {
1672
1672
  assert.deepEqual(okResults.filter(Boolean), []);
1673
1673
  });
1674
1674
 
1675
- test("lazy tool visibility reveals intercom on bundled skill use only", { concurrency: false }, async () => {
1676
- const { default: piIntercomExtension } = await import("./index.ts");
1677
-
1678
- await withIntercomConfig({ toolVisibility: "after-first-use" }, () => withChildOrchestratorEnv({
1679
- orchestratorTarget: "orchestrator",
1680
- runId: "78f659a3",
1681
- agent: "worker",
1682
- index: "0",
1683
- }, async () => {
1684
- const harness = createExtensionHarness("lazy-skill-worker", { activeTools: ["read", "bash"] });
1685
- piIntercomExtension(harness.pi as never);
1686
-
1687
- try {
1688
- await harness.emitLifecycle("session_start");
1689
- assert.deepEqual(harness.getActiveTools(), ["read", "bash", "contact_supervisor"]);
1690
-
1691
- harness.pi.events.emit("subagent:control-intercom", {
1692
- to: "session-child-test",
1693
- message: "Local relay traffic should not reveal the generic tool.",
1694
- });
1695
- assert.equal(harness.sentMessages.at(-1)?.activeTools.includes("intercom"), false);
1696
- assert.equal(harness.getActiveTools().includes("intercom"), false);
1697
-
1698
- await harness.emitLifecycle("tool_result", {
1699
- toolName: "read",
1700
- input: { path: path.join(repoDir, "README.md") },
1701
- isError: false,
1702
- });
1703
- await harness.emitLifecycle("tool_result", {
1704
- toolName: "read",
1705
- input: { path: path.join(repoDir, "skills", "pi-intercom", "SKILL.md") },
1706
- isError: true,
1707
- });
1708
- assert.equal(harness.getActiveTools().includes("intercom"), false);
1709
-
1710
- await harness.emitLifecycle("tool_result", {
1711
- toolName: "read",
1712
- input: { path: path.join(repoDir, "skills", "pi-intercom", "SKILL.md") },
1713
- isError: false,
1714
- });
1715
- assert.deepEqual(harness.getActiveTools(), ["read", "bash", "contact_supervisor", "intercom"]);
1716
-
1717
- await harness.emitLifecycle("session_start");
1718
- assert.equal(harness.getActiveTools().includes("intercom"), false);
1719
- await harness.emitLifecycle("input", { text: " /skill: pi-intercom", source: "user" });
1720
- assert.equal(harness.getActiveTools().includes("intercom"), true);
1721
- } finally {
1722
- await harness.emitLifecycle("session_shutdown");
1723
- }
1724
- }));
1725
- });
1726
-
1727
- test("lazy tool visibility reveals intercom before broker injection and after an overlay send", { concurrency: false }, async () => {
1675
+ test("obsolete toolVisibility config never hides or reveals the intercom tool", { concurrency: false }, async () => {
1728
1676
  const { default: piIntercomExtension } = await import("./index.ts");
1729
1677
 
1730
1678
  await withIntercomConfig({ toolVisibility: "after-first-use" }, async () => {
@@ -1754,14 +1702,14 @@ test("lazy tool visibility reveals intercom before broker injection and after an
1754
1702
  piIntercomExtension(inboundHarness.pi as never);
1755
1703
  await overlayHarness.emitLifecycle("session_start");
1756
1704
  await inboundHarness.emitLifecycle("session_start");
1757
- assert.equal(overlayHarness.getActiveTools().includes("intercom"), false);
1758
- assert.equal(inboundHarness.getActiveTools().includes("intercom"), false);
1705
+ assert.equal(overlayHarness.getActiveTools().includes("intercom"), true);
1706
+ assert.equal(inboundHarness.getActiveTools().includes("intercom"), true);
1759
1707
 
1760
1708
  selectedSession = await waitForSessionByName(planner, "planner");
1761
1709
  const inboundSession = await waitForSessionByName(planner, "lazy-inbound-worker");
1762
1710
  const delivered = await planner.send(inboundSession.id, {
1763
- messageId: "lazy-inbound-message",
1764
- text: "Reveal intercom before injecting this message.",
1711
+ messageId: "stable-inbound-message",
1712
+ text: "Keep intercom active before injecting this message.",
1765
1713
  });
1766
1714
  assert.equal(delivered.delivered, true);
1767
1715
  const deadline = Date.now() + 1000;
@@ -1770,6 +1718,13 @@ test("lazy tool visibility reveals intercom before broker injection and after an
1770
1718
  }
1771
1719
  assert.equal(inboundHarness.sentMessages[0]?.activeTools.includes("intercom"), true);
1772
1720
 
1721
+ await inboundHarness.emitLifecycle("tool_result", {
1722
+ toolName: "read",
1723
+ input: { path: path.join(repoDir, "skills", "pi-intercom", "SKILL.md") },
1724
+ isError: false,
1725
+ });
1726
+ assert.equal(inboundHarness.getActiveTools().includes("intercom"), true);
1727
+
1773
1728
  await overlayHarness.commands.get("intercom")!("", overlayHarness.ctx);
1774
1729
  assert.equal(overlayStep, 2);
1775
1730
  assert.equal(overlayHarness.getActiveTools().includes("intercom"), true);
@@ -3148,6 +3103,43 @@ test("intercom reply sends attachments", { concurrency: false }, async () => {
3148
3103
  }
3149
3104
  });
3150
3105
 
3106
+ test("intercom send refuses a different target during an active inbound ask turn", { concurrency: false }, async () => {
3107
+ const { planner, orchestrator, cleanup } = await setupClients();
3108
+ const { default: piIntercomExtension } = await import("./index.ts");
3109
+ const harness = createExtensionHarness("cwd-reply-worker");
3110
+
3111
+ try {
3112
+ piIntercomExtension(harness.pi as never);
3113
+ await harness.emitLifecycle("session_start");
3114
+ const worker = await waitForSessionByName(planner, "cwd-reply-worker");
3115
+
3116
+ assert.equal((await planner.send(worker.id, {
3117
+ messageId: "cwd-hierarchy-ask",
3118
+ text: "Please answer me, not the repo-root session.",
3119
+ expectsReply: true,
3120
+ })).delivered, true);
3121
+ const deadline = Date.now() + 1000;
3122
+ while (harness.sentMessages.length === 0 && Date.now() < deadline) {
3123
+ await new Promise((resolve) => setTimeout(resolve, 20));
3124
+ }
3125
+ await harness.emitLifecycle("turn_start");
3126
+
3127
+ const intercomTool = harness.tools.find((tool) => tool.name === "intercom")!;
3128
+ const result = await intercomTool.execute("misdirected-send", {
3129
+ action: "send",
3130
+ to: "orchestrator",
3131
+ message: "This was meant as the ask answer.",
3132
+ }, new AbortController().signal, undefined, harness.ctx);
3133
+
3134
+ assert.equal(result.details?.error, true);
3135
+ assert.equal(result.details?.replyTo, "cwd-hierarchy-ask");
3136
+ assert.match(result.content[0]?.text ?? "", /Refusing non-reply send to "orchestrator"/);
3137
+ } finally {
3138
+ await harness.emitLifecycle("session_shutdown");
3139
+ await cleanup();
3140
+ }
3141
+ });
3142
+
3151
3143
  test("intercom reply targets one of multiple pending asks by short session ID", { concurrency: false }, async () => {
3152
3144
  const { planner, orchestrator, cleanup } = await setupClients();
3153
3145
  const { default: piIntercomExtension } = await import("./index.ts");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-intercom",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -93,6 +93,26 @@ test("explicit to overrides the current turn context", () => {
93
93
  assert.throws(() => tracker.resolveReplyTarget({ to: "missing" }, 1003), /No pending ask from/);
94
94
  });
95
95
 
96
+ test("active ask context flags non-reply sends to a different target", () => {
97
+ const tracker = new ReplyTracker();
98
+ const current = tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "Need a reply"), 1000);
99
+ tracker.queueTurnContext(current);
100
+ tracker.beginTurn(1001);
101
+
102
+ assert.equal(tracker.findActiveReplyTargetMismatch("planner-id", 1002), null);
103
+ assert.equal(tracker.findActiveReplyTargetMismatch("planner", 1002)?.message.id, "ask-1");
104
+ assert.equal(tracker.findActiveReplyTargetMismatch("repo-root", 1002)?.message.id, "ask-1");
105
+ });
106
+
107
+ test("active ask context does not trust sender names as destination identity", () => {
108
+ const tracker = new ReplyTracker();
109
+ const current = tracker.recordIncomingMessage(createSession("asker-session", "root-session"), createMessage("ask-1", "Need a reply"), 1000);
110
+ tracker.queueTurnContext(current);
111
+ tracker.beginTurn(1001);
112
+
113
+ assert.equal(tracker.findActiveReplyTargetMismatch("root-session", 1002)?.message.id, "ask-1");
114
+ });
115
+
96
116
  test("replyTo resolves the exact pending ask", () => {
97
117
  const tracker = new ReplyTracker();
98
118
  tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "First"), 1000);
@@ -121,6 +121,14 @@ export class ReplyTracker {
121
121
  return candidates.length === 1 ? candidates[0]! : null;
122
122
  }
123
123
 
124
+ findActiveReplyTargetMismatch(to: string, now = Date.now()): IntercomContext | null {
125
+ this.pruneExpired(now);
126
+ if (!this.currentTurnContext?.message.expectsReply) {
127
+ return null;
128
+ }
129
+ return this.currentTurnContext.from.id === to ? null : this.currentTurnContext;
130
+ }
131
+
124
132
  markReplied(replyTo: string): void {
125
133
  this.dismissPendingAsk(replyTo);
126
134
  }
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.60.0] - 2026-08-31
6
+
7
+ ### Highlights
8
+ - Ask for the runtime delegation catalog as clean rows: `subagent({ action: "list", capabilities: true })` returns compact, prompt-free capability rows and machine metadata.
9
+ - Timeout recovery is explainable: async status and `subagent_wait` completions report the missing/written report and changed tracked files for timed-out children with dirty worktrees.
10
+ - Forked children reuse prompt caches: fork-context launches stamp an OpenAI-compatible `prompt_cache_key` on provider requests.
11
+ - `runs.lanes` plans show up as workflow-graph stages in status and TUI views.
12
+ - Watchdog and single-output evidence are cheaper and kinder: home-root/entry-less repos skip untracked scans, and stat errors propagate instead of being swallowed.
13
+
14
+ ### Added
15
+ - Add `capabilities` option to management `list`: returns executable/restricted status, restriction sources, aliases, runner (pi / external-cli / external-job with capabilities), tools (ambient, mcp-direct, mutation), model (value/fallbacks/thinking), execution (defaultAsync, timeoutMs), output, and extensions snapshot per agent, plus total restricted count and capability-ceiling sources, as versioned `details.agentCapabilities` (`details.catalog`).
16
+ - Emit `lanePlan` metadata from `runs.lanes` (stages, phase, label, agent, output name, structured flag) and render workflow-graph stage nodes in async status and TUI views.
17
+ - Stamp `prompt_cache_key` (`pi-fork:<sha256>`) on supported provider requests for fork-context children via `before_provider_request`.
18
+ - Project timeout-recovery evidence (termination, changed files, report status, dirty-worktree reason) into async status steps and `subagent_wait` completion output.
19
+
20
+ ### Changed
21
+ - `runs.host` rejects per-step `cwd` with an actionable hint (use outer request cwd or a trusted `cd` in the command).
22
+ - Watchdog repo-change signatures skip `--untracked-files=all` for home-repo roots and repositories with no tracked entries, keeping status probes cheap and bounded.
23
+ - Single-output snapshots propagate non-ENOENT stat errors instead of treating every failure as "file missing".
24
+ - Background spawns use `windowsHide` consistently and `detached` only on non-Windows.
25
+ - `workflowScript` guidance documents the portability rules (no nested async helpers) and `runs.host` cwd semantics.
26
+
27
+ ### Fixed
28
+ - "Running" spinners in the TUI advance on wall-clock time instead of only on model activity.
29
+ - Noisy repeated single-step status rows are suppressed in workflow status rendering.
30
+ - Timeout recovery summaries no longer claim a written report when the required output is missing.
31
+
5
32
  ## [0.59.0] - 2026-08-28
6
33
 
7
34
  ### Highlights