pi-subagents 0.81.0 → 0.82.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 (141) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/bundle/agents.js +3 -3
  3. package/bundle/child-tool-plan.js +2 -2
  4. package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
  5. package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
  6. package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
  7. package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
  8. package/bundle/{chunk-CEKS3TFO.js → chunk-7IVSUMQQ.js} +6 -6
  9. package/bundle/{chunk-KSWQVGDW.js → chunk-7PCKZHR5.js} +225 -18
  10. package/bundle/{chunk-563BHAZ5.js → chunk-7YMXOYEU.js} +33 -25
  11. package/bundle/{chunk-XMFLCKMQ.js → chunk-B5EKXDHE.js} +336 -130
  12. package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
  13. package/bundle/{chunk-L5ZHE4L4.js → chunk-GC4DSPMH.js} +4 -4
  14. package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
  15. package/bundle/{chunk-2ZSNH26H.js → chunk-M2CTFYT3.js} +7 -7
  16. package/bundle/{chunk-TUWLXTAM.js → chunk-MHZ4G23A.js} +681 -320
  17. package/bundle/{chunk-WSBNENFS.js → chunk-NGGAFDL6.js} +14 -4
  18. package/bundle/{chunk-VJHOELQP.js → chunk-NRQ75MGH.js} +160 -78
  19. package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
  20. package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
  21. package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
  22. package/bundle/{chunk-V6MR5TOQ.js → chunk-SIJ4S3EN.js} +25 -19
  23. package/bundle/{chunk-NNLASTQE.js → chunk-VH5OAWVG.js} +19 -5
  24. package/bundle/{chunk-BPS2AGBZ.js → chunk-W2SZ22EM.js} +93 -52
  25. package/bundle/{chunk-V3BIUPLU.js → chunk-WVD4UEL4.js} +256 -52
  26. package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
  27. package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
  28. package/bundle/{chunk-26C3WLYQ.js → chunk-ZLINIYTP.js} +497 -272
  29. package/bundle/{chunk-2XYWN7QJ.js → chunk-ZQGSDHL6.js} +2 -2
  30. package/bundle/control-channel.js +2 -2
  31. package/bundle/index.js +1 -1
  32. package/bundle/intercom-bridge.js +2 -2
  33. package/bundle/preflight.js +8 -8
  34. package/bundle/project-panes.js +3 -3
  35. package/docs/agents.md +31 -0
  36. package/docs/configuration.md +9 -1
  37. package/docs/tool-reference.md +3 -1
  38. package/docs/workflows.md +16 -0
  39. package/package.json +2 -2
  40. package/skills/pi-subagents/references/execution-controls.md +5 -4
  41. package/src/agents/agent-management.js +3 -3
  42. package/src/agents/agent-memory.js +3 -1
  43. package/src/agents/agents.js +6 -6
  44. package/src/agents/frontmatter.d.ts +2 -0
  45. package/src/agents/frontmatter.js +15 -1
  46. package/src/agents/skills.js +2 -1
  47. package/src/extension/config.js +3 -0
  48. package/src/extension/herdr-pi-bridge.js +17 -10
  49. package/src/extension/index.js +82 -2
  50. package/src/extension/schemas.js +3 -3
  51. package/src/intercom/native-supervisor-channel.js +20 -0
  52. package/src/missions/lifecycle.js +7 -1
  53. package/src/runs/background/active-async-capacity.d.ts +1 -0
  54. package/src/runs/background/active-async-capacity.js +9 -3
  55. package/src/runs/background/async-execution.js +20 -10
  56. package/src/runs/background/async-job-tracker.d.ts +6 -1
  57. package/src/runs/background/async-job-tracker.js +9 -4
  58. package/src/runs/background/async-status.d.ts +2 -0
  59. package/src/runs/background/async-status.js +21 -2
  60. package/src/runs/background/await-async-run.js +3 -7
  61. package/src/runs/background/boot-id.d.ts +12 -0
  62. package/src/runs/background/boot-id.js +47 -0
  63. package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
  64. package/src/runs/background/interrupted-elsewhere.js +68 -0
  65. package/src/runs/background/notify.d.ts +2 -0
  66. package/src/runs/background/notify.js +13 -0
  67. package/src/runs/background/result-delivery-ownership.d.ts +6 -0
  68. package/src/runs/background/result-delivery-ownership.js +19 -1
  69. package/src/runs/background/result-watcher.d.ts +3 -1
  70. package/src/runs/background/result-watcher.js +24 -8
  71. package/src/runs/background/run-child-session.d.ts +1 -6
  72. package/src/runs/background/run-child-session.js +1 -1
  73. package/src/runs/background/run-status.js +2 -1
  74. package/src/runs/background/stale-run-reconciler.d.ts +25 -0
  75. package/src/runs/background/stale-run-reconciler.js +52 -13
  76. package/src/runs/background/subagent-runner-bootstrap.js +3 -0
  77. package/src/runs/background/subagent-runner.d.ts +4 -0
  78. package/src/runs/background/subagent-runner.js +13 -5
  79. package/src/runs/background/subagent-wait.d.ts +1 -1
  80. package/src/runs/background/subagent-wait.js +6 -1
  81. package/src/runs/background/terminal-run-index.d.ts +1 -0
  82. package/src/runs/background/terminal-run-index.js +4 -2
  83. package/src/runs/foreground/async-stop-action.js +3 -1
  84. package/src/runs/foreground/execution.js +6 -3
  85. package/src/runs/foreground/foreground-history.d.ts +16 -2
  86. package/src/runs/foreground/foreground-history.js +133 -31
  87. package/src/runs/foreground/subagent-executor.d.ts +11 -0
  88. package/src/runs/foreground/subagent-executor.js +277 -74
  89. package/src/runs/shared/child-launch.d.ts +2 -2
  90. package/src/runs/shared/child-session.d.ts +3 -2
  91. package/src/runs/shared/child-session.js +51 -8
  92. package/src/runs/shared/child-tool-plan.d.ts +7 -0
  93. package/src/runs/shared/child-tool-plan.js +21 -6
  94. package/src/runs/shared/herdr-connection.d.ts +17 -8
  95. package/src/runs/shared/herdr-connection.js +19 -7
  96. package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
  97. package/src/runs/shared/herdr-external-adapters.js +1 -1
  98. package/src/runs/shared/herdr-machine.d.ts +12 -2
  99. package/src/runs/shared/herdr-machine.js +55 -9
  100. package/src/runs/shared/herdr-pane-host.d.ts +88 -0
  101. package/src/runs/shared/herdr-pane-host.js +155 -0
  102. package/src/runs/shared/herdr-placed-run.d.ts +27 -150
  103. package/src/runs/shared/herdr-placed-run.js +48 -395
  104. package/src/runs/shared/local-machine-access.d.ts +5 -0
  105. package/src/runs/shared/local-machine-access.js +28 -0
  106. package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
  107. package/src/runs/shared/orca-progress-tabs.js +1 -24
  108. package/src/runs/shared/parallel-utils.d.ts +1 -1
  109. package/src/runs/shared/placed-run-owner.d.ts +49 -0
  110. package/src/runs/shared/placed-run-owner.js +93 -0
  111. package/src/runs/shared/placement-host.d.ts +99 -0
  112. package/src/runs/shared/placement-host.js +157 -0
  113. package/src/runs/shared/rex-machine.d.ts +25 -0
  114. package/src/runs/shared/rex-machine.js +83 -0
  115. package/src/runs/shared/rex-pane-host.d.ts +62 -0
  116. package/src/runs/shared/rex-pane-host.js +327 -0
  117. package/src/shared/completion-owner.d.ts +5 -0
  118. package/src/shared/completion-owner.js +5 -0
  119. package/src/shared/session-identity.d.ts +2 -0
  120. package/src/shared/session-identity.js +26 -0
  121. package/src/shared/types.d.ts +45 -6
  122. package/src/tui/fleet.d.ts +1 -0
  123. package/src/tui/fleet.js +24 -15
  124. package/src/tui/render.d.ts +6 -0
  125. package/src/tui/render.js +3 -3
  126. package/src/tui/subagent-messages.js +1 -0
  127. package/src/workflows/scripted-workflow.d.ts +1 -1
  128. package/src/workflows/workflow-auto-resume.d.ts +32 -0
  129. package/src/workflows/workflow-auto-resume.js +102 -0
  130. package/src/workflows/workflow-launch-record.d.ts +27 -0
  131. package/src/workflows/workflow-launch-record.js +48 -0
  132. package/src/workflows/workflow-resume-claim.d.ts +82 -0
  133. package/src/workflows/workflow-resume-claim.js +210 -0
  134. package/src/workflows/workflow-resume.d.ts +56 -0
  135. package/src/workflows/workflow-resume.js +106 -0
  136. package/src/workflows/workflow-reuse.d.ts +15 -9
  137. package/src/workflows/workflow-reuse.js +77 -14
  138. package/src/workflows/workflow-revival.d.ts +2 -0
  139. package/src/workflows/workflow-revival.js +2 -1
  140. package/src/workflows/workflow-stop-cause.d.ts +6 -0
  141. package/src/workflows/workflow-stop-cause.js +8 -0
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "pi-subagents",
4
- version: "0.81.0",
4
+ version: "0.82.0",
5
5
  private: true,
6
6
  description: "Pi extension for single-agent delegation and scripted multi-agent workflows",
7
7
  author: "Nico Bailon",
@@ -106,7 +106,7 @@ var package_default = {
106
106
  "@js-temporal/polyfill": "0.5.1",
107
107
  acorn: "8.18.0",
108
108
  jiti: "2.7.0",
109
- undici: "8.10.2",
109
+ undici: "8.12.3",
110
110
  yaml: "2.8.3"
111
111
  },
112
112
  devDependencies: {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  requestAsyncStop
3
- } from "./chunk-BJN5LUQY.js";
3
+ } from "./chunk-OYF6AFDY.js";
4
4
  import "./chunk-NIXWDZZV.js";
5
- import "./chunk-ZBC44JJ3.js";
5
+ import "./chunk-CIZQXJ5Q.js";
6
6
  import "./chunk-5LFLVSTM.js";
7
7
  import "./chunk-YFCK7H2S.js";
8
8
  import "./chunk-Z7KVHQHL.js";
package/bundle/index.js CHANGED
@@ -17,7 +17,7 @@ function readInstalledPackageVersion() {
17
17
  var loadedPackageVersion = readInstalledPackageVersion();
18
18
 
19
19
  // index.ts
20
- var registerExtension = process.env[HERDR_PI_MODE_ENV] === "1" ? (await import("./chunk-V6MR5TOQ.js")).default : process.env.PI_SUBAGENT_CHILD === "1" ? void 0 : (await import("./chunk-V3BIUPLU.js")).default;
20
+ var registerExtension = process.env[HERDR_PI_MODE_ENV] === "1" ? (await import("./chunk-SIJ4S3EN.js")).default : process.env.PI_SUBAGENT_CHILD === "1" ? void 0 : (await import("./chunk-WVD4UEL4.js")).default;
21
21
  function registerSubagentExtension(pi) {
22
22
  const installedVersion = readInstalledPackageVersion();
23
23
  if (installedVersion !== loadedPackageVersion) {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  resolveIntercomSessionTarget
3
- } from "./chunk-ZP3HODFH.js";
3
+ } from "./chunk-XSWOTIA3.js";
4
4
  import "./chunk-7F6RKT6W.js";
5
- import "./chunk-ZBC44JJ3.js";
5
+ import "./chunk-CIZQXJ5Q.js";
6
6
  import "./chunk-5LFLVSTM.js";
7
7
  import "./chunk-YFCK7H2S.js";
8
8
  import "./chunk-Z7KVHQHL.js";
@@ -2,22 +2,22 @@ import {
2
2
  buildEffectiveSystemPrompt,
3
3
  canPreferForkFromSnapshot,
4
4
  resolveSubagentLaunchContext
5
- } from "./chunk-ARRKBB2B.js";
6
- import "./chunk-EYNJ6VL6.js";
5
+ } from "./chunk-GJECHSAR.js";
6
+ import "./chunk-PLCCBLPY.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-WSBNENFS.js";
9
+ } from "./chunk-NGGAFDL6.js";
10
10
  import {
11
11
  nestedResultsPath,
12
12
  processTerminalCandidatePath,
13
13
  processTerminalPath,
14
14
  resultFilePath
15
- } from "./chunk-VJHOELQP.js";
15
+ } from "./chunk-NRQ75MGH.js";
16
16
  import {
17
17
  applyIntercomBridgeToAgent,
18
18
  resolveIntercomBridge,
19
19
  validateIntercomBridgeConfig
20
- } from "./chunk-ZP3HODFH.js";
20
+ } from "./chunk-XSWOTIA3.js";
21
21
  import {
22
22
  AGENT_DEFINITION_PROJECTION_VERSION,
23
23
  resolveLaunchBinding,
@@ -26,7 +26,7 @@ import {
26
26
  import {
27
27
  applyThinkingSuffix,
28
28
  resolvePiLaunchToolPlan
29
- } from "./chunk-G5DKFLLI.js";
29
+ } from "./chunk-4SDU2BZX.js";
30
30
  import {
31
31
  assertRequiredChildExtensionsAdmitted,
32
32
  resolveRequiredChildExtensions
@@ -34,7 +34,7 @@ import {
34
34
  import {
35
35
  getArtifactPaths,
36
36
  getArtifactsDir
37
- } from "./chunk-3DC5KBVP.js";
37
+ } from "./chunk-5TYMSZX3.js";
38
38
  import {
39
39
  assertClaudeCodeModelScope,
40
40
  assertThinkingWithinCeiling,
@@ -61,7 +61,7 @@ import {
61
61
  resolveSkillsWithFallback,
62
62
  resolveStepBehavior,
63
63
  unknownAgentDiagnosticContext
64
- } from "./chunk-ZBC44JJ3.js";
64
+ } from "./chunk-CIZQXJ5Q.js";
65
65
  import {
66
66
  DIRS,
67
67
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -8,10 +8,10 @@ import {
8
8
  openProjectPane,
9
9
  projectPaneBindingPath,
10
10
  readProjectPaneBinding
11
- } from "./chunk-V2JWDB5U.js";
11
+ } from "./chunk-52UBALJL.js";
12
12
  import "./chunk-7J4ZXMY7.js";
13
- import "./chunk-3DC5KBVP.js";
14
- import "./chunk-ZBC44JJ3.js";
13
+ import "./chunk-5TYMSZX3.js";
14
+ import "./chunk-CIZQXJ5Q.js";
15
15
  import "./chunk-5LFLVSTM.js";
16
16
  import "./chunk-YFCK7H2S.js";
17
17
  import "./chunk-Z7KVHQHL.js";
package/docs/agents.md CHANGED
@@ -282,6 +282,36 @@ Placed external profiles are one-shot and stop-only: they cannot steer, resume,
282
282
 
283
283
  pi-subagents never clones, pulls, or checks out on the machine. Generic `external-cli` commands and managed worktrees are rejected before launch; saved-machine placement accepts native Pi and only the six code-owned external profiles.
284
284
 
285
+ ## Running a Pi child in a Rex pane on this machine
286
+
287
+ `machine: "local"` runs a native Pi child as a visible Pi in a new background tab of your own Rex session, labeled `subagent-<run id tail>`. It works wherever `machine` works: frontmatter, settings overrides, the `subagent` tool, workflow `runs.run`, chain, parallel, and dynamic-fanout steps. `local` is reserved and never looks in the Herdr catalog. The parent Pi must run inside Rex: with no `REX_SESSION`, or a session the Rex server does not confirm, the launch fails before anything starts.
288
+
289
+ The parent drives the child through the same bridge as a Herdr-placed Pi: prompt, steer, follow-up, abort, `contact_supervisor`, and results. The child Pi loads pi-subagents from its own extensions, so it must be installed at the same version. `cwd` is a directory on this machine; a relative path joins the launch directory. pi-subagents finds `rex` in `$REX_BIN_DIR`, then on `PATH`, then in the Rex server app.
290
+
291
+ A finished child keeps its tab open, with the idle Pi, until you close it; an aborted or stopped child's tab is closed. Limits: native Pi only (Claude Code, Codex, Cursor, and generic `external-cli` agents are rejected), fresh context only, and the same restrictions as a Herdr-placed Pi (no local extension paths, process environment, nested delegation, permission rules, or tool budgets).
292
+
293
+ ## Running a Pi child in a Rex pane on another Mac
294
+
295
+ Name a Mac you reach over SSH in your user settings (`~/.pi/agent/settings.json`), with its repo root:
296
+
297
+ ```json
298
+ {
299
+ "subagents": {
300
+ "machines": {
301
+ "workmac": { "rex": { "ssh": "100.82.67.118" }, "cwd": "~/dev/app" }
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ `machine: "workmac"` then runs a native Pi child as a visible Pi in a new background tab of the Rex session labeled `subagents` on that Mac. pi-subagents creates the session the first time and reuses it after that; if two sessions there have that label, the launch fails until you close or rename one. Every `rex` command runs on that Mac over SSH, and the bridge socket comes back through an SSH forward, so SSH must work without a prompt (`ssh <target>` once to accept the host key). A name with a `rex` entry never looks in the Herdr catalog, so Herdr does not need to be installed.
308
+
309
+ Only user settings may name a Rex host: a `rex` entry in a project's `.pi/settings.json` fails the launch, so a cloned repo cannot choose where a task runs. `cwd` can come from project or user settings, as for Herdr machines: a relative step `cwd` joins it, and an absolute or `~` path is used as given. The other Mac needs Rex, Pi on its login shell's `PATH`, and pi-subagents installed at the same version. The child edits that Mac's disk, so its changed files come from that Mac's Git state, as for a Herdr-placed child.
310
+
311
+ Programs in a Rex tab run under the Rex app's macOS privacy permissions. If Pi on that Mac reads files in Desktop, Documents, or Downloads (for example a package path in its settings), allow Rex there first: open a Rex tab on that Mac, run `ls ~/Documents`, and accept the prompt. Until then Pi waits on the prompt and the launch times out.
312
+
313
+ To watch the tab, open the `subagents` session in Rex on that Mac. To watch it from this Mac, add that Mac's Rex server to your hosts here once (`rex hosts add workmac <endpoint>`; after `rex server tailscale on` there, `rex whoami` on that Mac shows its Tailscale address). Tab lifecycle and limits are the same as for `machine: "local"`.
314
+
285
315
  ## Sandboxing background children with a launcher
286
316
 
287
317
  A launcher wraps an agent's background runner in a command you choose, so that agent can run in a different sandbox from the parent session. Define the command in the user config key [`runnerLaunchers`](configuration.md#runnerlaunchers), then name it in the agent file:
@@ -512,6 +542,7 @@ How `tools` behaves:
512
542
  - `tools` omitted: the child session gets Pi's normal builtin tools.
513
543
  - `tools` present: regular tool names become an explicit allowlist.
514
544
  - `tools:` empty: the child session gets no tools.
545
+ - `tools: +codemode` or `tools: -write`: rejected. Pi's `--tools` and `defaultTools` accept `+name`/`-name` entries that edit the default tool set, but a child's `tools` list replaces that set, so name every tool the child needs, or omit `tools` and use `excludeTools` to drop one. A local agent with such an entry fails before launch. A Herdr- or Rex-placed child resolves its agent on that machine, so an entry only there fails when the pane bridge is configured, before the first prompt.
515
546
  - `allowNestedSubagents: true`: explicitly enables child-safe nested fanout without turning omitted `tools` into an allowlist. Depth and inherited capability ceilings still apply.
516
547
  - `allowedAgents: scout, reviewer`: if nesting is separately enabled, restricts this agent's descendant launches to those names. `allowedAgents:` denies all descendants and omission is unrestricted except for inherited/runtime policy.
517
548
 
@@ -244,6 +244,14 @@ Set `enabled` to `false` (or remove the block) as a kill switch. In that state,
244
244
 
245
245
  Workflow script calls use background execution when the request omits `async`. Set `asyncByDefault` to `false` to restore foreground-by-default behavior for tool launches that still use the internal single-run primitive. Callers can still force foreground with `async: false` unless `forceTopLevelAsync` is enabled.
246
246
 
247
+ ## `autoResume`
248
+
249
+ ```json
250
+ { "autoResume": false }
251
+ ```
252
+
253
+ Default `true`. When a session is reopened (`pi -c`, `pi --session`, `/resume`) or reloaded (`/reload`), its own async workflows that a Pi exit or a reload interrupted in the last 24 hours are resumed once, without delaying session start; set `autoResume` to `false` to resume them only by hand. A one-shot `pi -p` run does not auto-resume, so it does not wait on the workflow. See [Workflows](workflows.md).
254
+
247
255
  ## `runnerLaunchers`
248
256
 
249
257
  ```json
@@ -387,8 +395,8 @@ Adds a short `active_subagents` section to the parent's system prompt while this
387
395
 
388
396
  `defaultTimeoutMs` sets the blocking window used when a `bg_wait` call omits `timeoutMs`; explicit call values win, followed by this setting, then the 30-minute fallback. `bg_wait` is the only registered wait tool. When the window elapses, the tool returns a non-error `window_elapsed` result with the still-active work identities, and that work keeps running. Set `enabled` to `false` to make direct calls return immediately instead of blocking. 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 enabled and default-timeout values are passed explicitly to child runtimes. Headless `agent_end` auto-drain retains its own strict deadline and fails if required work remains unresolved. Invalid config or environment values fail instead of being coerced.
389
397
 
390
- Blocking `bg_wait({ id: "..." })` keeps the current tool call open until that run changes. By default it returns when a run needs attention. Use `bg_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. A steer or follow-up the operator sends while a blocking wait is open also ends it, with a non-error `user_input` result listing the still-active work, so the message reaches the model without waiting out the window. In a long-lived interactive parent session, `bg_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. Use it for provider, detached, or other background work without a native completion notification; ordinary async subagent runs notify the parent natively and do not need a wait subscription. Armed subscriptions appear in ordinary `subagent({ action: "status" })` output and are not counted as active child work. Native completion notifications wake the session through the Pi process that launched the run, while that process is still running; a session reopened in a new process does not get that wake and should read the result with `subagent({ action: "status" })`. `nonBlocking` subscriptions need a host with a UI context, such as interactive or RPC mode; plain SDK embedding without a UI context rejects them.
391
398
 
399
+ Blocking `bg_wait({ id: "..." })` keeps the current tool call open until that run changes. By default it returns when a run needs attention, even when the run is still working normally, for example a `bash` command open longer than `activeNoticeAfterMs`; the result then says the run is still running. Use `bg_wait({ stopOnAttention: false })` for run-to-completion flows: it waits through idle time, a tool held open, or repeated tool failures, and still ends for a supervisor/contact request, a finished run, the timeout, user input, or abort. A steer or follow-up the operator sends while a blocking wait is open also ends it, with a non-error `user_input` result listing the still-active work, so the message reaches the model without waiting out the window. In a long-lived interactive parent session, `bg_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. Use it for provider, detached, or other background work without a native completion notification; ordinary async subagent runs notify the parent natively and do not need a wait subscription. Armed subscriptions appear in ordinary `subagent({ action: "status" })` output and are not counted as active child work. A native completion notification goes to the Pi process that launched the run while that process is running; once it has exited, the session gets the result the next time it is open in any Pi. `nonBlocking` subscriptions need a host with a UI context, such as interactive or RPC mode; plain SDK embedding without a UI context rejects them.
392
400
  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.
393
401
 
394
402
  ## `resultScanLogging`
@@ -82,7 +82,7 @@ The `subagent` tool schema lists `agent`, `task`, `workflow`, `args`, `async`, `
82
82
  | `options.context` | `fresh \| fork \| profile` | global or per-agent default, else `fresh` | Explicit `fresh` or `fork` overrides every workflow child. `profile` requires the selected agent's declared `defaultContext` and ignores config `defaultSubagentContext`; missing agent defaults fail. When omitted, [`defaultSubagentContext`](configuration.md#defaultsubagentcontext) wins over each agent's `defaultContext`; implicit fork falls back to fresh without a persisted parent session and leaf. Explicit fork is strict. Packaged `worker` defaults to `fresh`; packaged `oracle` and `advisor` default to `fork`. |
83
83
  | `model` | string | agent default | An exact `provider/id`; bare ids resolve only if unique, and agent names are not model ids. An unknown or ambiguous model fails with up to 5 closest registry ids to copy; `{action:"models"}` lists them all. A suffix such as `provider/id:high` (`off/minimal/low/medium/high/xhigh/max`) overrides agent thinking. The `thinking` field is only for `watchdog.configure`, ignored on dispatch. On the `claude-code` and `claude-code-writer` adapters the model is Claude Code's own alias or id, and the same suffix becomes its `--effort`, bounded by `subagents.maxThinking` and by an enforced `subagents.modelScope`. |
84
84
  | `options.missionId` | string | - | Attach a workflow, including each fire of `schedule.create`, to an existing project mission. Scheduled attachment uses ordinary lifecycle and retention rules. |
85
- | `options.reuseFrom` | string | - | Exact id of an async workflow run stopped by a runtime replacement (`workflow.stopCause: "runtime-replaced"`). An async relaunch of the same script in the same cwd, from any session, reuses that run's finished children and waits for its running ones; see [Workflows](workflows.md). |
85
+ | `options.reuseFrom` | string | - | Exact id of an async workflow run stopped by a runtime replacement or whose Pi process exited (`workflow.stopCause: "runtime-replaced"` or `"owner-exited"`). An async relaunch of the same script in the same cwd, from any session, reuses that run's finished children and waits for its running ones; see [Workflows](workflows.md). |
86
86
  | `options.mission` | object/false | auto-create | Override the default enclosing mission with `{ title \| summary, objective?, goal?, budget?, labels? }`. Set exactly one non-empty `title` or `summary`; `objective` and `labels` are optional. `goal` may only be `true`, requires `budget.tokens`, and enables continuation notices. Pass `false` for an intentionally ephemeral workflow with no mission for it or its children and no `state` global. Explicit mission persistence failures are strict. |
87
87
  | `options.handoffPath` | string | - | Aggregate handoff manifest for `action: "worktree.discard"` or lane evidence actions, or optional explicit metadata for `action: "worktree.cleanup"`. |
88
88
  | `options.repo` | string | runtime cwd | Repository path for `action: "worktree.cleanup"`; required explicitly for apply. The configured worktree base filters candidates by their per-project folder under it but never discovers them. |
@@ -329,6 +329,7 @@ subagent({ action: "stop", id: "<run-id>" })
329
329
  subagent({ action: "resume", id: "<run-id>", message: "follow-up question after it pauses or finishes" })
330
330
  subagent({ action: "resume", id: "<run-id>", message: "follow-up for child 2", options: { index: 1 } })
331
331
  subagent({ action: "resume", id: "<nested-run-id>", message: "follow-up for a nested child" })
332
+ subagent({ action: "resume", id: "<stopped-workflow-run-id>" })
332
333
  subagent({ action: "steer", id: "<run-id>", message: "guidance for the running child" })
333
334
  subagent({ action: "steer", id: "<run-id>", message: "check this after the current turn", options: { mode: "follow_up" } })
334
335
  subagent({ action: "steer", id: "<run-id>", message: "guidance for child 2", options: { index: 1, mode: "auto" } })
@@ -379,6 +380,7 @@ Commands remain owned by the child session. Normal completion with unfinished co
379
380
  - Completed external-job runs, including nested runs, can use the same `resume` action as a provider follow-up when the registered provider exposes `followUp(input)`. Running external-job parents fail closed with guidance to wait for completion. Unsupported providers fail with an update/reload message.
380
381
  - Revive starts a new child session from the old session context; it does not resume the live session, and it requires the chosen child to have a persisted `.jsonl` session file.
381
382
  - Direct revival takes an exclusive cross-process lease on the canonical session file until the new child finishes. A concurrent attempt fails before Pi is spawned and identifies the owning revived run; dead-owner leases are reclaimed only when staleness can be proved.
383
+ - Without `message` (and without `index` or `chain`), `resume` on a top-level async workflow run that stopped because its session was replaced or its Pi process exited (`workflow.stopCause: "runtime-replaced"` or `"owner-exited"`) relaunches it once from the launch it saved, reusing its finished children, and its receipt starts with `Resumed workflow '<id>' as <new id>.`; a run that cannot be resumed is refused as `Cannot resume workflow '<id>': <reason>.` The session that owns the run also resumes it on its own when reopened or reloaded, unless [`autoResume`](configuration.md#autoresume) is off; see [Workflows](workflows.md).
382
384
 
383
385
  ### stop
384
386
 
package/docs/workflows.md CHANGED
@@ -14,6 +14,8 @@ Packaged `worker` defaults to fresh context so implementation starts from its as
14
14
 
15
15
  Launch children in the background (`async` follows `asyncByDefault`, normally true) and consume each result at the point a later step depends on it. Native completion wakes the parent session, so the parent returns control instead of sleeping or polling. Use `async: false` only when the parent must block, not for final reviews or gates. When an oracle or advisor reaches an unknown that needs a decision, it asks through supervisor dialogue; use a one-shot oracle call only when one was requested.
16
16
 
17
+ A background child awaited by an `async: false` workflow cannot receive a supervisor decision while its parent is blocked in that call. Its native `contact_supervisor` decision or interview request returns a tool error promptly, asking it to report the blocker and partial results and stop. The transport error is not a supervisor reply or approval. Use `async: true` for workflows whose children may need a decision. Progress updates remain non-blocking, and an explicit child `async: true` still returns its launch receipt without being awaited.
18
+
17
19
  Child-safety boundaries are enforced at runtime:
18
20
 
19
21
  - Child sessions do not receive the bundled `pi-subagents` skill.
@@ -31,8 +33,22 @@ Failed workflow details and async `status.json` include `workflow.failureKind` a
31
33
 
32
34
  When `/reload`, a session resume, or a pi-web project switch replaces the extension runtime, a running async workflow stops with `workflow.stopCause: "runtime-replaced"` in `status.json`, and its awaited async children keep running. Its in-process foreground children are stopped, including ones that detached to ask their supervisor. When the same workflow script is launched again with the same `args` in that session, a `runs.run` with the same key and params returns a child that already finished successfully without launching it, and waits for a child that is still running instead of starting another. Those results carry `reused: true`, their step and trace entry are marked `reused`, and the new run records `workflow.reusedFrom`. Failed and stopped children, and children that ran in-process, launch again. Reuse applies only when the newest run of that script and args in the session was stopped this way; a user stop or a completed run ends it. From another session, such as one that took over after a handoff, relaunch the same script as an async workflow with `options: { reuseFrom: "<stopped run id>" }`; the run must have stopped this way in the same cwd, and `args` may differ, because each child is still reused only when its key and params match.
33
35
 
36
+ When the Pi process that ran an async workflow exits before the workflow finishes (a crash, a kill, or a machine restart), the stale-run check that runs when its session reopens, or when `options.reuseFrom` names the run, marks it failed with `workflow.stopCause: "owner-exited"`, and a relaunch reuses its finished children the same way. On any relaunch, `runs.steer` and `runs.host` calls run again and only `runs.run` results are reused, so a replayed check such as "tests passed" can be stale.
37
+
38
+ A stopped run is continued by one relaunch only. While that relaunch exists, another relaunch of the same stopped run, by the same script or by `options.reuseFrom`, is refused with `workflow run '<id>' was resumed as <new id>; wait on it or stop it.` If that relaunch stops the same way, relaunch it instead: it carries the children of every run before it. If the Pi process that started the relaunch exits before the relaunch begins, the stopped run can be relaunched again.
39
+
40
+ To continue a stopped run without sending its script again, resume it by id from the session that owns it: `subagent({ action: "resume", id: "<stopped run id>" })` with no `message`. Every async top-level workflow saves its launch (the script or named resource, its `args` and options) when it starts, and the resume relaunches that launch with `options.reuseFrom` set to the stopped run, in the stopped run's cwd, with no more authority than it had. A named resource is resolved again and must still have the same script. The new run records `workflow.resumeOrigin: { sourceRunId, attempt }`, where `attempt` counts the resumes that followed a Pi exit. Another session cannot resume the run by id; reopen the session that started it, or relaunch the same script with `options.reuseFrom`. When the run cannot be resumed this way (it started before launches were saved, was launched from `tasks` or `chain`, or its resource changed), the refusal says so; relaunch the same script by hand with `options: { reuseFrom: "<stopped run id>" }`.
41
+
42
+ When the session that owns such a run is reopened or reloaded, the run is resumed this way automatically, once, if it was interrupted in the last 24 hours: for a Pi exit, measured from the last time the dead Pi process updated the run. Session start does not wait for the relaunch. A run that cannot be relaunched is named in its notice (`Not auto-resumed: …`, with how to continue by hand); a launch that fails after session start also posts one `Workflow <id> was not auto-resumed: <reason>.` message. Either way the run is not tried again. `/new` and forks do not resume the runs of the session they replaced. The `autoResume` setting turns this off; see [Configuration](configuration.md#autoresume).
43
+
44
+ Starting Pi in a folder where another session's subagent runs were cut off by its Pi dying in the last 24 hours shows one info line per such session, such as ``2 subagent runs were interrupted in another session. Reopen it to resume them: `pi --session <id>` ``; it only points to that session, changes nothing in it, and is not sent to the model.
45
+
46
+ The completion notice of a workflow stopped by a Pi exit or a runtime replacement has one line right after `Workflow run: <id>` that says what happens next: `Auto-resumed as <new id>.`, `Resumed as <new id>.` (resumed by id), `Relaunched as <new id>.` (relaunched with the same script or `options.reuseFrom`), `Not auto-resumed: <reason>.` followed by the way to continue by hand, or, when nothing continues it, `Resume by hand: subagent({ action: "resume", id: "<id>" })` (or the `options.reuseFrom` form when its launch cannot be resumed by id).
47
+
34
48
  After `/new`, `/resume`, or a fork in the same Pi process, the new session owns the async runs of the session it replaced, and of the sessions that one replaced. It can list, `bg_wait` on, steer, stop, and resume them, receives their completion notices, and answers their children's pending supervisor requests. Over RPC, a host can steer, interrupt, stop, resume, and inspect them, and sees them in the fleet and `subagent-async` snapshots. Startup and other Pi processes inherit nothing; `/reload` adds nothing new but keeps what the session already inherited. To relaunch a stopped workflow instead of controlling its children one by one, use `options.reuseFrom`.
35
49
 
50
+ Once the Pi process that started a background single, parallel, or chain run has exited, the run is reported once to a Pi window that has its session open, whether it finished while Pi was closed or finishes later. A workflow's children are reported through their workflow.
51
+
36
52
  Pi core may print a generic `pi -ne` extension-load hint; that out-of-repo hint is not protocol-approved fallback. A verified compaction abort may continue the retained child once on its already resolved model; it does not authorize an execution-mode or model switch.
37
53
 
38
54
  ## Prompt shortcuts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "pi-subagents",
4
- "version": "0.81.0",
4
+ "version": "0.82.0",
5
5
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
6
6
  "author": "Nico Bailon",
7
7
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "@js-temporal/polyfill": "0.5.1",
21
21
  "acorn": "8.18.0",
22
22
  "jiti": "2.7.0",
23
- "undici": "8.10.2",
23
+ "undici": "8.12.3",
24
24
  "yaml": "2.8.3"
25
25
  },
26
26
  "peerDependencies": {
@@ -169,10 +169,11 @@ subagent runs notify the parent automatically. Use `bg_wait({ all: true })` for
169
169
  all work active at call time, `bg_wait({ id: "..." })` for one async or
170
170
  remembered detached foreground run, and `bg_wait({ timeoutMs })` to cap the
171
171
  block; active work keeps running if it elapses. `bg_wait({ stopOnAttention:
172
- false })` keeps a blocking wait through idle or long-thinking attention, but
173
- supervisor/contact requests still stop it. In a long-lived interactive parent
174
- session, use `bg_wait({ id: "...", nonBlocking: true })` only for a known
175
- detached or otherwise non-notifying run to resolve the prefix to one exact run,
172
+ false })` keeps a blocking wait through idle time, a tool held open, or repeated
173
+ tool failures; supervisor/contact requests, finished runs, timeout and user
174
+ input still stop it. In a long-lived interactive parent session, use
175
+ `bg_wait({ id: "...", nonBlocking: true })` only for a known detached or
176
+ otherwise non-notifying run to resolve the prefix to one exact run,
176
177
  persist an armed subscription, return immediately, and wake later on
177
178
  completion, failure, attention, reconciliation failure, or timeout. Ordinary
178
179
  status lists armed subscriptions separately from active children. This differs
@@ -5,7 +5,7 @@ import { serializeAgent } from "./agent-serializer.js";
5
5
  import { mergeAgentsForScope } from "./agent-selection.js";
6
6
  import { discoverAvailableSkills, resolveSkills } from "./skills.js";
7
7
  import { buildProactiveSkillSubagentRecommendationLines, } from "./proactive-skills.js";
8
- import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.js";
8
+ import { parseFrontmatter, parseUnquotedList } from "./frontmatter.js";
9
9
  import { resolveEffectiveThinking, toModelInfo } from "../shared/model-info.js";
10
10
  import { resolveSubagentModelOverride } from "../runs/shared/model-resolution.js";
11
11
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.js";
@@ -175,9 +175,9 @@ function withDeclaredExtensionPaths(config, filePath) {
175
175
  const { extensions: _extensions, subagentOnlyExtensions: _subagentOnlyExtensions, ...withoutResolvedExtensions } = config;
176
176
  return {
177
177
  ...withoutResolvedExtensions,
178
- ...(frontmatter.extensions !== undefined ? { extensions: parseFrontmatterList(frontmatter.extensions) } : {}),
178
+ ...(frontmatter.extensions !== undefined ? { extensions: parseUnquotedList(frontmatter.extensions) } : {}),
179
179
  ...(frontmatter.subagentOnlyExtensions !== undefined
180
- ? { subagentOnlyExtensions: parseFrontmatterList(frontmatter.subagentOnlyExtensions) }
180
+ ? { subagentOnlyExtensions: parseUnquotedList(frontmatter.subagentOnlyExtensions) }
181
181
  : {}),
182
182
  };
183
183
  }
@@ -236,7 +236,9 @@ export function readMemoryFile(memoryDir) {
236
236
  let fd;
237
237
  try {
238
238
  const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
239
- fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
239
+ // A FIFO must not block the launch before fstat can reject non-files.
240
+ const nonBlock = typeof fs.constants.O_NONBLOCK === "number" ? fs.constants.O_NONBLOCK : 0;
241
+ fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow | nonBlock);
240
242
  }
241
243
  catch (error) {
242
244
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
@@ -19,7 +19,7 @@ import { parseChain, parseJsonChain } from "./chain-serializer.js";
19
19
  import { mergeAgentsForScope } from "./agent-selection.js";
20
20
  import { projectScopeRequiresTrustMessage } from "./agent-scope.js";
21
21
  import { closestMatch } from "../shared/edit-distance.js";
22
- import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.js";
22
+ import { parseFrontmatter, parseFrontmatterList, parseUnquotedList } from "./frontmatter.js";
23
23
  import { buildRuntimeName, parsePackageName } from "./identity.js";
24
24
  import { parseModelScopeConfig } from "../runs/shared/model-scope.js";
25
25
  export { BUILTIN_AGENT_NAMES } from "./builtin-names.js";
@@ -1919,11 +1919,11 @@ function loadAgentsFromDefinitionFiles(files, source, discoveryPriority, package
1919
1919
  else
1920
1920
  throw new Error(`Agent '${localName}' has invalid advertise frontmatter; expected true or false.`);
1921
1921
  }
1922
- const rawTools = parseFrontmatterList(frontmatter.tools);
1922
+ const rawTools = parseUnquotedList(frontmatter.tools);
1923
1923
  const parsedTools = splitToolList(rawTools);
1924
1924
  const tools = parsedTools.tools ?? [];
1925
1925
  const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
1926
- const excludeTools = parseFrontmatterList(frontmatter.excludeTools);
1926
+ const excludeTools = parseUnquotedList(frontmatter.excludeTools);
1927
1927
  const parsedAllowedAgents = parseFrontmatterList(frontmatter.allowedAgents);
1928
1928
  const allowedAgents = parsedAllowedAgents === undefined ? undefined : normalizeCapabilityCeilingAllowedAgents(parsedAllowedAgents);
1929
1929
  const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
@@ -1997,9 +1997,9 @@ function loadAgentsFromDefinitionFiles(files, source, discoveryPriority, package
1997
1997
  else
1998
1998
  throw new Error(`Agent '${localName}' has invalid acceptanceRole frontmatter; expected 'read-only' or 'writer'.`);
1999
1999
  }
2000
- const extensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.extensions), filePath);
2001
- const subagentOnlyExtensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.subagentOnlyExtensions), filePath);
2002
- const mutationTools = parseFrontmatterList(frontmatter.mutationTools);
2000
+ const extensions = resolveAgentRelativeExtensionPaths(parseUnquotedList(frontmatter.extensions), filePath);
2001
+ const subagentOnlyExtensions = resolveAgentRelativeExtensionPaths(parseUnquotedList(frontmatter.subagentOnlyExtensions), filePath);
2002
+ const mutationTools = parseUnquotedList(frontmatter.mutationTools);
2003
2003
  let fast;
2004
2004
  if (frontmatter.fast !== undefined) {
2005
2005
  if (frontmatter.fast === "true")
@@ -5,6 +5,8 @@
5
5
  * alone because parseFrontmatter has already removed their quotes, so `"notes #1.md"` must survive.
6
6
  */
7
7
  export declare function parseFrontmatterList(raw: string | undefined): string[] | undefined;
8
+ /** A tool or extension name list whose items may each be quoted, as in `"read", "bash"` or `- "read"`. */
9
+ export declare function parseUnquotedList(raw: string | undefined): string[] | undefined;
8
10
  /**
9
11
  * Parse YAML frontmatter from agent/chain files.
10
12
  * Handles both flat (key: value) and nested block (key: \n sub: val) values.
@@ -56,6 +56,16 @@ export function parseFrontmatterList(raw) {
56
56
  .map((value) => value.trim())
57
57
  .filter(Boolean);
58
58
  }
59
+ function unquoteName(value) {
60
+ if (value.length >= 2 && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) {
61
+ return value.slice(1, -1);
62
+ }
63
+ return value;
64
+ }
65
+ /** A tool or extension name list whose items may each be quoted, as in `"read", "bash"` or `- "read"`. */
66
+ export function parseUnquotedList(raw) {
67
+ return parseFrontmatterList(raw)?.map(unquoteName);
68
+ }
59
69
  /**
60
70
  * Parse YAML frontmatter from agent/chain files.
61
71
  * Handles both flat (key: value) and nested block (key: \n sub: val) values.
@@ -115,7 +125,11 @@ export function parseFrontmatter(content) {
115
125
  if (key === undefined || rawValueValue === undefined)
116
126
  continue;
117
127
  const rawValue = rawValueValue.trim();
118
- const isQuoted = (rawValue.startsWith('"') && rawValue.endsWith('"')) || (rawValue.startsWith("'") && rawValue.endsWith("'"));
128
+ const quote = rawValue[0];
129
+ const nameList = key === "tools" || key === "excludeTools" || key === "mutationTools" || key === "extensions" || key === "subagentOnlyExtensions";
130
+ // `"read", "bash"` starts and ends with a quote, but it is a list. Keep those item quotes for the splitter.
131
+ const quotedList = nameList && (quote === '"' || quote === "'") && (rawValue.includes(`${quote},`) || rawValue.includes(`, ${quote}`));
132
+ const isQuoted = !quotedList && (quote === '"' || quote === "'") && rawValue.endsWith(quote);
119
133
  const value = isQuoted ? rawValue.slice(1, -1) : rawValue;
120
134
  const isFolded = !isQuoted && (rawValue === ">" || rawValue === ">-");
121
135
  const isLiteral = !isQuoted && (rawValue === "|" || rawValue === "|-");
@@ -45,7 +45,8 @@ function readOptionalJsonFile(filePath, label) {
45
45
  const code = typeof error === "object" && error !== null && "code" in error
46
46
  ? error.code
47
47
  : undefined;
48
- if (code === "ENOENT")
48
+ // ENOTDIR: a settings package entry may name a single extension file, so `<file>/package.json` is no manifest.
49
+ if (code === "ENOENT" || code === "ENOTDIR")
49
50
  return null;
50
51
  const message = error instanceof Error ? error.message : String(error);
51
52
  throw new Error(`Failed to read ${label} '${filePath}': ${message}`, {
@@ -217,6 +217,9 @@ function validateConfig(config) {
217
217
  if (config.enableSlashCommands !== undefined && typeof config.enableSlashCommands !== "boolean") {
218
218
  throw new Error("config.enableSlashCommands must be a boolean");
219
219
  }
220
+ if (config.autoResume !== undefined && typeof config.autoResume !== "boolean") {
221
+ throw new Error("config.autoResume must be a boolean");
222
+ }
220
223
  if (config.toolActivation !== undefined && config.toolActivation !== "auto" && config.toolActivation !== "dynamic" && config.toolActivation !== "eager") {
221
224
  throw new Error('config.toolActivation must be "auto", "dynamic", or "eager"');
222
225
  }
@@ -10,6 +10,7 @@ import { buildSkillInjection, resolveSkills } from "../agents/skills.js";
10
10
  import { buildAgentMemoryInjection } from "../agents/agent-memory.js";
11
11
  import { appendAgentRefinementOverlay } from "../agents/agent-refinements.js";
12
12
  import { rewriteSubagentPrompt } from "../runs/shared/subagent-prompt-runtime.js";
13
+ import { assertNoToolModifierEntries } from "../runs/shared/child-tool-plan.js";
13
14
  import { resolveExistingReadPaths } from "../shared/settings.js";
14
15
  import { MODEL_ONLY_TOOL } from "../shared/extension-context.js";
15
16
  function gitEvidence(cwd) {
@@ -37,6 +38,7 @@ export function resolveRemoteHerdrResources(cwd, resources, remoteDefaultTools =
37
38
  const reads = resources.reads === undefined ? agent.defaultReads ?? false : resources.reads;
38
39
  const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, cwd) : [];
39
40
  const systemPrompt = appendAgentRefinementOverlay(`${agent.systemPrompt}${buildAgentMemoryInjection(agent, cwd)}${skillPrompt ? `\n\n${skillPrompt}` : ""}${readPaths.length ? `\n\n[Read from: ${readPaths.join(", ")}]` : ""}`, { cwd, agentName: agent.name });
41
+ assertNoToolModifierEntries(agent.name, agent.tools ?? []);
40
42
  const denied = new Set(agent.excludeTools ?? []);
41
43
  const tools = (agent.tools === undefined ? remoteDefaultTools : agent.tools).filter((tool) => !denied.has(tool) && (!resources.toolCeiling || resources.toolCeiling.includes(tool)));
42
44
  if (agent.tools === undefined && !tools.length)
@@ -91,7 +93,6 @@ export default function registerHerdrPiBridge(pi) {
91
93
  client.write(encodeHerdrPiFrame(record));
92
94
  };
93
95
  const sessionId = (ctx) => ctx.sessionManager?.getSessionId?.();
94
- const activeTools = (ctx) => ctx.getActiveTools?.() ?? [];
95
96
  pi.registerTool({
96
97
  name: "contact_supervisor",
97
98
  ...MODEL_ONLY_TOOL,
@@ -104,11 +105,11 @@ export default function registerHerdrPiBridge(pi) {
104
105
  const id = `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
105
106
  send({ type: "supervisor-request", requestId: id, reason: input.reason, message: input.message?.slice(0, 65_536) ?? "", ...(input.interview !== undefined ? { interview: input.interview } : {}), expectsReply: input.reason !== "progress_update", nativeSessionId });
106
107
  if (input.reason === "progress_update") {
107
- await new Promise((resolve) => supervisor.set(id, { reason: input.reason, resolve }));
108
+ await new Promise((resolve, reject) => supervisor.set(id, { reason: input.reason, resolve, reject }));
108
109
  return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId: id, reason: input.reason } };
109
110
  }
110
111
  const reason = input.reason;
111
- const answer = await new Promise((resolve) => supervisor.set(id, { reason, resolve }));
112
+ const answer = await new Promise((resolve, reject) => supervisor.set(id, { reason, resolve, reject }));
112
113
  const details = { requestId: id, reason };
113
114
  if (reason === "interview_request")
114
115
  try {
@@ -145,7 +146,10 @@ export default function registerHerdrPiBridge(pi) {
145
146
  if (!entry)
146
147
  throw new Error("Unknown supervisor request.");
147
148
  supervisor.delete(request.supervisorId);
148
- entry.resolve(request.text ?? "");
149
+ if (request.error !== undefined)
150
+ entry.reject(new Error(request.error));
151
+ else
152
+ entry.resolve(request.text ?? "");
149
153
  }
150
154
  send({ type: "control-ack", requestId, operation: request.operation, nativeSessionId });
151
155
  };
@@ -165,7 +169,7 @@ export default function registerHerdrPiBridge(pi) {
165
169
  if (!nativeSessionId)
166
170
  throw new Error("Pane-native Pi bridge requires a persisted native session identity.");
167
171
  fs.writeFileSync(manifestPath, JSON.stringify({ protocol: HERDR_PI_PROTOCOL, packageVersion: packageJson.version, runId, socketPath, nativeSessionId, pid: process.pid }), { mode: 0o600, flag: "wx" });
168
- send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(ctx), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
172
+ send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: pi.getActiveTools(), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
169
173
  });
170
174
  on("message_end", (event) => {
171
175
  const message = event.message;
@@ -215,11 +219,11 @@ export default function registerHerdrPiBridge(pi) {
215
219
  const invalidReads = resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string"));
216
220
  if (typeof resources.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || invalidSkills || invalidReads)
217
221
  throw new Error("Remote logical resource names are invalid.");
218
- const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling } : {}), ...(resources.reads !== undefined ? { reads: resources.reads } : {}) }, context ? activeTools(context) : []);
222
+ const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling } : {}), ...(resources.reads !== undefined ? { reads: resources.reads } : {}) }, context ? pi.getActiveTools() : []);
219
223
  resolvedSystemPrompt = resolved.systemPrompt;
220
224
  resolvedContextPolicy = resolved;
221
- pi.setActiveTools?.(resolved.tools);
222
- const acknowledged = context ? activeTools(context) : [];
225
+ pi.setActiveTools(resolved.tools);
226
+ const acknowledged = context ? pi.getActiveTools() : [];
223
227
  if (acknowledged.some((tool) => !resolved.tools.includes(tool)) || resolved.tools.some((tool) => !acknowledged.includes(tool)))
224
228
  throw new Error("Remote Pi did not apply the resolved active-tool set exactly.");
225
229
  configured = true;
@@ -234,7 +238,10 @@ export default function registerHerdrPiBridge(pi) {
234
238
  const text = frame.text;
235
239
  if (text !== undefined && (typeof text !== "string" || Buffer.byteLength(text) > 256 * 1024))
236
240
  throw new Error("Bridge request text is invalid or too large.");
237
- pending.set(frame.requestId, { operation: operation, ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}) });
241
+ const error = frame.error;
242
+ if (error !== undefined && (operation !== "supervisor-reply" || typeof error !== "string" || Buffer.byteLength(error) > 256 * 1024))
243
+ throw new Error("Bridge supervisor error is invalid or too large.");
244
+ pending.set(frame.requestId, { operation: operation, ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}), ...(error !== undefined ? { error } : {}) });
238
245
  send({ type: "prepared", requestId: frame.requestId, nativeSessionId });
239
246
  }
240
247
  }
@@ -245,7 +252,7 @@ export default function registerHerdrPiBridge(pi) {
245
252
  socket.on("close", () => { if (client === socket)
246
253
  client = undefined; });
247
254
  if (context && nativeSessionId)
248
- send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(context), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
255
+ send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: pi.getActiveTools(), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
249
256
  });
250
257
  server.listen(socketPath, () => { fs.chmodSync(socketPath, 0o600); });
251
258
  }