pi-subagents 0.50.0 → 0.52.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 (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
package/CHANGELOG.md CHANGED
@@ -2,6 +2,109 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.52.0] - 2026-08-19
6
+
7
+ ### Highlights
8
+ - Async workflows are much harder to break mid-flight: a transient status-file lock, a stalled child, or a paused supervisor hand-off no longer fails or loses an otherwise healthy run.
9
+ - Hosts can now inspect a running or completed async child on demand — task, recent transcript, and final output — without spending a model turn, and that output stays available after delivery.
10
+ - macOS and FreeBSD sandboxes stop warning about setuid `/bin/ps`, and Windows stops flashing console windows during busy runs.
11
+ - Gateway and proxy models work better: children can inherit the parent's session model, and Hugging Face-style `owner/name` model ids resolve correctly.
12
+
13
+ ### Added
14
+ - Add `/subagents-inspect-rpc`, a host-facing bridge command that answers on-demand async child inspection requests with a correlated, bounded `PI_SUBAGENT_INSPECT_JSON:` widget payload (task, transcript window, final output), so RPC hosts can inspect children without a model turn while the live status feed stays small. Thanks to [@yanqianglu](https://github.com/yanqianglu) for #1254.
15
+
16
+ ### Changed
17
+ - Guide oracle plan and design advice through a short same-session consultation when a material tradeoff remains, while keeping the parent as final decision-maker (#1245).
18
+ - Improve bundled role and parent prompts for source-first discovery in noisy codebases (#1247).
19
+ - Make Surf's `gpt-pro` agent an optional package integration instead of a pi-subagents builtin. If you disabled the old builtin workaround, remove `agentOverrides.gpt-pro.disabled` before using Surf's package agent. Thanks to [@binhex](https://github.com/binhex) for #1256.
20
+
21
+ ### Fixed
22
+ - Stop spawning setuid `/bin/ps` for process start identity on macOS and FreeBSD. Sandboxes such as nono no longer report `forbidden-exec-sugid` from session leases, retention locks, external-job claims, or mission state. Those platforms stay fail-closed without pid-reuse detection. Thanks to [@jdumas](https://github.com/jdumas) for #1273.
23
+ - Stop a transient lock on `status.json` (seen on Windows) from failing an already-completed workflow child and aborting its still-running siblings. Status updates after launch now degrade to a `subagent.workflow.status_write_failed` event instead of failing the run, and a throwing `onTrace` host callback can no longer reject a child promise. Follow-up to #1143. Thanks to [@MarcusNeufeldt](https://github.com/MarcusNeufeldt) for #1272.
24
+ - Keep a still-paused workflow result when reconcile republishes updated child output during paused delivery, so a same-state revision is not overwritten or deleted as the old payload.
25
+ - Persist async terminal `status.json` before publishing the result file, so observers cannot see a completed result while the run still looks `running`.
26
+ - Stop Windows opening a console window for each helper process (Git, `gh`, PowerShell, `npm root -g`) spawned during a run, which made a busy run disruptive to work alongside. Thanks to [@MarcusNeufeldt](https://github.com/MarcusNeufeldt) for #1274.
27
+ - Keep completed inspect RPC output available from the durable completion replay after result delivery consumes its one-shot payload, including per-child inline result tails (#1254).
28
+ - After a workflow child detaches for supervisor coordination, clear attention once the reply is delivered, keep `subagent_wait` blocked until the child exits, and reconcile the paused workflow when that child completes — even after the paused payload was already delivered. A timed-out workflow can also resume from its persisted child session when the workflow dir has no recovery descriptor. Thanks to [@skystar567](https://github.com/skystar567) for #1263.
29
+ - Wake the idle parent when an async workflow child needs attention, and persist that control event on the enclosing workflow. Status already showed the stall; the parent notice did not. Thanks to [@Yibo-Zhang](https://github.com/Yibo-Zhang) for #1266.
30
+ - Resolve Hugging Face-style `owner/name` model ids against the registry instead of treating every slash as `provider/id`. Fully qualified `huggingface/owner/name` still wins, and a first path segment that matches a registered provider still means `provider/id`. Thanks to [@mr-brobot](https://github.com/mr-brobot) for #1264.
31
+ - Keep public structured single-child calls synchronous when `asyncByDefault:false` and `async` is omitted. Thanks to [@Nofuture123](https://github.com/Nofuture123) for #1257.
32
+ - Trust the running parent session model when no model is configured, so gateway and proxy parent models can launch children outside the host registry. Thanks to [@Nofuture123](https://github.com/Nofuture123) for #1258.
33
+ - Isolate colliding inherited workflow child output defaults while preserving explicit output collision checks. Thanks to [@Reverier-Xu](https://github.com/Reverier-Xu) for #1253.
34
+ - Show a scheduled run's completion and name the schedule that produced it, so scheduled work no longer finishes silently in a session that cannot attribute it. Thanks to [@albertgwo](https://github.com/albertgwo) for #1246.
35
+ - Show resume-first guidance for failed async runs only when a matching recovery descriptor exists, so missing recovery data no longer points users to a resume command that cannot work. Thanks to [@graadient](https://github.com/graadient) for #1241.
36
+ - Keep bundled agent discovery stable across hot package updates, so long-running sessions do not parse newer bundled agent files with older loaded code. Thanks to [@graadient](https://github.com/graadient) for #1242.
37
+ - Resolve relative extension paths against the defining agent file, so portable agent definitions load child extensions from the declared location. Thanks to [@tayiorbeii](https://github.com/tayiorbeii) for #1249.
38
+
39
+ ## [0.51.0] - 2026-08-18
40
+
41
+ ### Highlights
42
+ - Workflow orchestration is easier to control with stable-key steering, clearer fanout guidance, and a supported external-job runner path.
43
+ - Async runs are harder to lose when storage is full, file access is temporarily denied, identifiers are too long, or multiple Pi windows share one session.
44
+ - macOS reloads and idle sessions do less fragile filesystem watching, which avoids reload hangs without adding always-on work.
45
+ - Herdr and Fleet are less disruptive: panes stay in the background by default, trusted transcripts open cleanly, and live workflow children steer through the right route.
46
+ - The workflow API is cleaner: scripted workflows are the supported path, and removed legacy chain surfaces now have direct migration guidance.
47
+
48
+ ### Added
49
+ - Add stable-key `runs.steer` to `workflowScript`, with routing for foreground and async children, structured receipts, trace entries, and checks for unawaited calls (#1186).
50
+ - Add `runner.type: external-job`, the exported provider bridge, the Surf GPT Pro `gpt-pro` profile, and docs for external advisor data boundaries (#1189).
51
+ - Add `defaultSubagentContext: "fork"` for launches that do not set an explicit context (#1161).
52
+ - Allow `defaultSubagentContext: "fresh"` to override agent fork defaults for launches that do not set an explicit context.
53
+ - Add `PI_SUBAGENT_FS_RETRY_MAX_TOTAL_MS` so hosts can cap filesystem retry waits. Unset by default. Thanks to [@MarcusNeufeldt](https://github.com/MarcusNeufeldt) for #1143.
54
+
55
+ ### Changed
56
+ - Document rolling `workflowScript` fanout with `runs.run`, `Promise.race`, `runs.steer`, and `Promise.all` (#1187).
57
+ - Document scripted chaining as the supported workflow API, with migration examples for removed top-level chain and task inputs.
58
+ - Clarify `workflowScript` fanout guidance: use awaited `runs.all` for ordinary parallel work, and use stored `runs.run` promises only for fully observed advanced rolling fanout (#1229, #1230).
59
+ - Clarify that async workflows do not have inline `live-card` projection (#1229, #1230).
60
+ - Describe `async:false` as a blocking parent wait, not a UI or foreground-only mode.
61
+ - Clarify that subagent reviews and gates should stay async unless the parent must block until completion.
62
+ - Document that a host's session lifetime owns completion wakes, and how to key an idle check on live run state rather than parent activity. Thanks to [@MarcusNeufeldt](https://github.com/MarcusNeufeldt) for #1144.
63
+ - Register the default `subagent` tool prompt as split metadata with a short description, `promptSnippet`, and `promptGuidelines`, while keeping explicit `full`, `compact`, and `custom` description modes.
64
+ - Keep `worktree: true` workflow children on the single-child path while preserving managed patch handoffs.
65
+
66
+ ### Removed
67
+ - Remove unused foreground chain and parallel execution and durable chain management surfaces.
68
+ - Remove legacy subagent tool compatibility fields for append-step control, schedule aliases, async recovery metadata, and string mission goals.
69
+ - Remove chain approval checkpoint steps and the `approve-checkpoint` / `reject-checkpoint` controls.
70
+ - Remove `prompts.render` from `workflowScript`; pass explicit task text to `runs.run` or use `/prompt-workflow` for reusable prompt templates.
71
+
72
+ ### Fixed
73
+ - Avoid Darwin reload hangs by disabling idle native filesystem watchers and using demand-gated delivery for live results, supervisor messages, controls, and steering. Thanks to [@youlikemodernart](https://github.com/youlikemodernart) for #1220.
74
+ - Bound async result session, run, active-run, and result-index path segments so long provider IDs do not break launches or waits with `ENAMETOOLONG`. Thanks to [@hlstwizard](https://github.com/hlstwizard) for #1131 and [@zhouatie](https://github.com/zhouatie) for #1135.
75
+ - Hash result-index session segments that look like Windows paths or file names, keep reading previous URI-encoded keys, and treat `EPERM` and `EACCES` as empty scans. Thanks to [@apoapostolov](https://github.com/apoapostolov) for #1211.
76
+ - Sanitize foreground workflow output path segments derived from provider run IDs, so Windows launches do not fail when tool-call IDs contain path-invalid characters. Thanks to [@maxime-louward-shift](https://github.com/maxime-louward-shift) for #1235.
77
+ - Keep async status and result persistence retrying after temporary `ENOSPC`, quota, or file-descriptor exhaustion errors. Thanks to [@ahmadaccino](https://github.com/ahmadaccino) for #1227.
78
+ - Route async completion notifications and cleanup only to the parent Pi process that launched the run, so concurrent windows sharing one session file cannot consume each other's results. Thanks to [@wangjianming](https://github.com/wangjianming) for #1225.
79
+ - Keep extension reload cleanup scoped to the replaced session runtime, so concurrent Pi sessions in one process do not remove each other's subscriptions or parent-session identity. Thanks to [@ryanbbrown](https://github.com/ryanbbrown) for #1222.
80
+ - Stop failing child runs when an explicit allowlist names `contact_supervisor` without the legacy `intercom` companion. A lone `intercom` entry still requires a real external provider. Thanks to [@MingTeer](https://github.com/MingTeer) for #1207.
81
+ - Add explicit `isolation: "none"` for schema-driven workflows without Git worktree setup, while keeping strict `isolation: "worktree"` behavior. Thanks to [@tlsneo](https://github.com/tlsneo) for #1203.
82
+ - Fail closed when an existing external-job `status.json` is unreadable or malformed, including an invalid `steps` shape.
83
+ - Skip malformed agent definitions during discovery so valid agents still list and launch, while showing configuration errors in management diagnostics (#1200).
84
+ - Resolve `/subagents-generate-profiles` provider probes through the shared Pi executable resolver so configured and Windows-specific Pi commands work. Thanks to [@Wumpf](https://github.com/Wumpf) for #1199.
85
+ - Resolve the workflowScript parser from pi-subagents instead of the caller's working directory, so workflows start in projects that do not install Acorn. Thanks to [@xz-dev](https://github.com/xz-dev) for #1214, following up #1190.
86
+ - Keep workflowScript child-launch tracking working on Bun-built Pi without a hard dependency on V8 promise hooks. Thanks to [@rochecompaan](https://github.com/rochecompaan) for #1158 and [@rholak](https://github.com/rholak) for the version-window diagnosis.
87
+ - Treat provider subscription usage-limit errors as retryable model failures so `fallbackModels` can continue to the next configured model. Thanks to [@dwizzle204](https://github.com/dwizzle204) for #1215.
88
+ - Skip fallback models that are unavailable in the active registry, so shared agent configs still run where their primary model is available. Thanks to [@JPFrancoia](https://github.com/JPFrancoia) for #1147.
89
+ - Preserve workflow async session roots for Herdr inspectors so workflow runs open with the same trusted session-root context as standalone runs. Thanks to [@hank-warren](https://github.com/hank-warren) for #1219.
90
+ - Keep Herdr project and inspector panes in the background by default, and move focus only when callers set `focus: true`. The FleetView inspect key still focuses the pane it opens. Thanks to [@boggylp](https://github.com/boggylp) for #1226.
91
+ - Show FleetView transcript fallbacks for trusted session roots instead of warning about an untrusted session file. Thanks to [@aliceisjustplaying](https://github.com/aliceisjustplaying) for #1154.
92
+ - Route Fleet inspector steering for live in-process workflow children through their foreground routes instead of the detached async queue. Thanks to [@ViktorBarzin](https://github.com/ViktorBarzin) for #1218 and #1216.
93
+ - Serialize same-worktree Orca progress-tab creation so numbered tabs appear left to right in sequence. Thanks to [@hyein-cbio](https://github.com/hyein-cbio) for #1196.
94
+ - Bound repeated async-state queries to active, exact-id, and recent-terminal indexes instead of scanning the full async history (#1162).
95
+ - Move retention directory discovery to a read-only worker so full scans do not block the extension event loop (#1188).
96
+ - Reclaim proven-safe async run and orphan result state after 30 days in bounded, locked cleanup passes with rename-first tombstones (#1163).
97
+ - Sweep expired wait subscriptions armed by another session, so stale records stop accumulating in the subscriptions directory. Thanks to [@MarcusNeufeldt](https://github.com/MarcusNeufeldt) for #1142.
98
+ - Restore and list schedules after their project directory is deleted, and skip orphan schedule directories without letting create reuse stale state. Thanks to [@ELA718](https://github.com/ELA718) for #1171 and [@colinb4987](https://github.com/colinb4987) for #1167.
99
+ - Fall back from an implicit `defaultContext: fork` to `fresh` when the parent session file or current leaf is not available yet. Explicit `context: "fork"` remains fail-fast. Thanks to [@hyein-cbio](https://github.com/hyein-cbio) for #1137.
100
+ - Keep retained workflow children resumable when their managed worktree cwd is preserved in the handoff manifest (#1172).
101
+ - Preserve workflow child task output when neither the workflow nor child configures an output file (#1136).
102
+ - Preserve a child's file-only report when its output path also names the workflow summary output.
103
+ - Keep concurrent async result promotion from deleting a newer payload or another promoter's published result. Thanks to [@albertgwo](https://github.com/albertgwo) for #1130.
104
+ - Keep `mcp:<server>` direct tools available when pi-mcp-adapter cache identity includes a request-header command. Thanks to [@xz-dev](https://github.com/xz-dev) for #1141.
105
+ - Isolate test async state from the user temp root and write each missing-mission sync diagnostic only once (#1164, #1165).
106
+ - Keep structured delegation integration coverage active when the test process inherits a subagent-child environment marker.
107
+
5
108
  ## [0.50.0] - 2026-08-15
6
109
 
7
110
  ### Added
package/agents/oracle.md CHANGED
@@ -16,7 +16,9 @@ Your primary job is to prevent the main agent from making hidden, conflicting, o
16
16
 
17
17
  Before you do anything else, reconstruct the key inherited decisions, constraints, and open questions from the forked conversation, codebase state, and task. Those decisions form your baseline contract. Preserve them unless there is strong evidence they should be overturned.
18
18
 
19
- If the task is framed as asking or consulting the oracle, treat it as a live consultation unless the parent explicitly requests a one-shot report. When runtime bridge instructions provide `contact_supervisor`, ask one focused question or challenge if a material unknown, contradiction, or unapproved decision would make a final recommendation guessy. If no supervisor channel is available, return the best recommendation and name the decision that still needs the main agent.
19
+ Match search scope to the question. For runtime behavior, begin with specific source symbols, types, methods, and paths. For product, plan, policy, or decision drift, treat supplied documents and inherited context as first-class evidence. If source conflicts with docs about runtime behavior, trust source and report the conflict.
20
+
21
+ If the task asks about asking or consulting the oracle, or asks to ask, consult, discuss with, or come to agreement with the oracle about a plan, design, or architecture decision, treat it as a short live consultation unless the parent explicitly requests a one-shot report. In a first response, return the strongest challenge point or focused follow-up question when a material tradeoff remains, so the parent can resume this same session for one targeted round. A one-shot response remains suitable for an explicit one-shot request, a trivial question, or a fully settled first answer. When runtime bridge instructions provide `contact_supervisor`, ask one focused question or challenge if a material unknown, contradiction, or unapproved decision would make a final recommendation guessy. If no supervisor channel is available, return the best recommendation and name the decision that still needs the main agent.
20
22
 
21
23
  If you need clarification from the main agent and bridge instructions provide `contact_supervisor`, use it with `reason: "need_decision"` and wait for the reply. Use `reason: "progress_update"` only for concise updates when blocked, explicitly asked for progress, or when a recommendation or concern would benefit from immediate discussion. Keep coordination traffic tight and purposeful. Do not narrate your whole review through `contact_supervisor`.
22
24
 
@@ -50,6 +50,7 @@ Review a PR or issue by understanding the context, then verifying:
50
50
  - Tests and docs are updated as needed.
51
51
 
52
52
  ## Working rules
53
+ - Start from the exact diff and named source seam for code-behavior review. Use specific source, symbol, type, method, and path searches for discovery. Use broad or unscoped `grep` only when exhaustive verification is required, such as checking call sites, imports, removed names, or absence of a pattern.
53
54
  - Read the relevant files first. Read plan and progress when the task supplies them.
54
55
  - Repo-local `progress.md` files are allowed scratch/memory files. Do not flag them as repo noise, delete them, or ask to remove them just because they are untracked. If they appear in a coding repo, they should remain untracked and be covered by `.gitignore`.
55
56
  - Do not use shell commands or write files. Report any test or Git command that a supervisor must run.
package/agents/scout.md CHANGED
@@ -12,7 +12,7 @@ defaultProgress: true
12
12
 
13
13
  You are a scouting subagent running inside pi.
14
14
 
15
- Use the provided tools directly. Move fast, but do not guess. Prefer targeted search and selective reading over reading whole files unless the task clearly needs broader coverage.
15
+ Use the provided tools directly. Move fast, but do not guess. Start discovery with task-provided paths and specific symbols, types, methods, filenames, or likely source roots. Use `find` for path discovery. Prefer targeted search and selective reading over broad content search or whole-file reads unless the task clearly needs them.
16
16
 
17
17
  Focus on the minimum context another agent needs in order to act:
18
18
  - relevant entry points
@@ -22,7 +22,7 @@ Focus on the minimum context another agent needs in order to act:
22
22
  - constraints, risks, and open questions
23
23
 
24
24
  Working rules:
25
- - Use `grep`, `find`, `ls`, and `read` to map the area before diving deeper.
25
+ - Use `grep`, `find`, `ls`, and `read` to map the area before diving deeper. Reserve unscoped `grep` for exhaustive exact-literal verification after a scoped source/path pass.
26
26
  - Use `bash` only for non-interactive inspection commands.
27
27
  - When you cite code, use exact file paths and line ranges.
28
28
  - If you are told to write output, write it to the provided path and keep the final response short.
package/agents/worker.md CHANGED
@@ -16,7 +16,7 @@ You are `worker`: the implementation subagent.
16
16
 
17
17
  You are the single writer thread. Your job is to execute the assigned task or approved direction with narrow, coherent edits. The main agent and user remain the decision authority.
18
18
 
19
- Use the provided tools directly. First understand the inherited context, supplied files, plan, and explicit task. Then implement carefully and minimally.
19
+ Use the provided tools directly. First read the inherited context, supplied files, plan, task paths, and named seams. Then implement carefully and minimally. Use broad search only to verify or expand from that starting point.
20
20
 
21
21
  The builtin worker uses a strict tool allowlist. It does not inherit ambient extension tools from the parent session. To use an extension tool, configure a custom agent with the tool name explicitly listed in `tools` and load its provider through `extensions` or `subagentOnlyExtensions`.
22
22
 
@@ -34,6 +34,7 @@ Default responsibilities:
34
34
 
35
35
  Working rules:
36
36
  - Prefer narrow, correct changes over broad rewrites.
37
+ - Preserve source discoverability: use specific names, clear types, one spelling per concept, source-named tests, and definition comments only when they explain a needed constraint.
37
38
  - Do not add speculative scaffolding or future-proofing unless explicitly required.
38
39
  - Do not leave placeholder code, TODOs, or silent scope changes.
39
40
  - Use `bash` for inspection, validation, and relevant tests.
@@ -0,0 +1,180 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { parentPort } from "node:worker_threads";
4
+
5
+ const ACTIVE_RUN_INDEX_DIR = ".active-runs";
6
+ const RESULT_TOMBSTONE_PREFIX = ".deleting-result-";
7
+
8
+ function compareRelative(left, right) {
9
+ if (left < right) return -1;
10
+ if (left > right) return 1;
11
+ return 0;
12
+ }
13
+
14
+ function insertSmallest(entries, candidate, limit) {
15
+ if (limit <= 0) return;
16
+ const index = entries.findIndex((entry) => compareRelative(candidate.relative, entry.relative) < 0);
17
+ if (index === -1) entries.push(candidate);
18
+ else entries.splice(index, 0, candidate);
19
+ if (entries.length > limit) entries.pop();
20
+ }
21
+
22
+ function streamDirWindow(dir, limit, after, relativePath, usable) {
23
+ if (limit <= 0) return { entries: [], rawReads: 0, exhausted: true, cursorCleared: false };
24
+ let handle;
25
+ try {
26
+ handle = fs.opendirSync(dir);
27
+ const next = [];
28
+ const wrapped = [];
29
+ let rawReads = 0;
30
+ while (true) {
31
+ const entry = handle.readSync();
32
+ if (!entry) break;
33
+ rawReads += 1;
34
+ const relative = relativePath(entry);
35
+ if (!usable(entry, relative)) continue;
36
+ const candidate = { relative, name: entry.name };
37
+ insertSmallest(wrapped, candidate, limit);
38
+ if (after === undefined || compareRelative(relative, after) > 0) insertSmallest(next, candidate, limit);
39
+ }
40
+ const cursorCleared = after !== undefined && next.length === 0;
41
+ return { entries: cursorCleared ? wrapped : next, rawReads, exhausted: true, cursorCleared };
42
+ } catch (error) {
43
+ if (error?.code === "ENOENT") return { entries: [], rawReads: 0, exhausted: true, cursorCleared: false };
44
+ throw error;
45
+ } finally {
46
+ handle?.closeSync();
47
+ }
48
+ }
49
+
50
+ function discover(request) {
51
+ const startedAt = Date.now();
52
+ const cursor = structuredClone(request.cursor);
53
+ const raw = { reads: 0, exhausted: {} };
54
+ const record = (source, scan) => {
55
+ raw.reads += scan.rawReads;
56
+ raw.exhausted[source] = scan.exhausted;
57
+ };
58
+ const runScan = streamDirWindow(
59
+ request.asyncDirRoot,
60
+ request.runBudget,
61
+ cursor.runAfter,
62
+ (entry) => entry.name,
63
+ (entry) => entry.isDirectory() && entry.name !== ACTIVE_RUN_INDEX_DIR,
64
+ );
65
+ record("runs", runScan);
66
+ if (runScan.cursorCleared) delete cursor.runAfter;
67
+ const runCandidates = runScan.entries.slice(0, request.runBudget);
68
+ for (const candidate of runCandidates) cursor.runAfter = candidate.relative;
69
+
70
+ const resultCandidates = [];
71
+ const sourceLimit = Math.max(1, Math.ceil(request.resultBudget / 4));
72
+ const addFiles = (dir, kind, cursorTarget, source, budget = sourceLimit) => {
73
+ const remaining = Math.min(budget, request.resultBudget - resultCandidates.length);
74
+ if (remaining <= 0) return 0;
75
+ const before = resultCandidates.length;
76
+ const after = cursorTarget.type === "pending"
77
+ ? cursor.resultPendingAfterBySession?.[cursorTarget.session]
78
+ : cursor[cursorTarget.key];
79
+ const scan = streamDirWindow(
80
+ dir,
81
+ remaining,
82
+ after,
83
+ (entry) => path.relative(request.resultsDir, path.join(dir, entry.name)),
84
+ (entry) => entry.isFile() && (entry.name.startsWith(RESULT_TOMBSTONE_PREFIX) || entry.name.endsWith(".json")),
85
+ );
86
+ record(source, scan);
87
+ if (scan.cursorCleared) {
88
+ if (cursorTarget.type === "pending") delete cursor.resultPendingAfterBySession?.[cursorTarget.session];
89
+ else delete cursor[cursorTarget.key];
90
+ }
91
+ for (const candidate of scan.entries.slice(0, remaining)) {
92
+ const tombstone = candidate.name.startsWith(RESULT_TOMBSTONE_PREFIX);
93
+ resultCandidates.push({
94
+ name: candidate.name,
95
+ relative: candidate.relative,
96
+ kind: tombstone ? "tombstone" : kind,
97
+ cursor: cursorTarget,
98
+ });
99
+ if (cursorTarget.type === "pending") {
100
+ cursor.resultPendingAfterBySession ??= {};
101
+ cursor.resultPendingAfterBySession[cursorTarget.session] = candidate.relative;
102
+ } else cursor[cursorTarget.key] = candidate.relative;
103
+ }
104
+ return resultCandidates.length - before;
105
+ };
106
+
107
+ addFiles(request.resultsDir, "public", { type: "result", key: "resultPublicAfter" }, "results.public");
108
+ const pendingRoot = path.join(request.resultsDir, "result-pending");
109
+ const pendingRemaining = Math.min(request.resultBudget, sourceLimit, request.resultBudget - resultCandidates.length);
110
+ const pendingScan = streamDirWindow(
111
+ pendingRoot,
112
+ pendingRemaining,
113
+ cursor.pendingSessionAfter,
114
+ (entry) => entry.name,
115
+ (entry) => entry.isDirectory(),
116
+ );
117
+ record("results.pendingSessions", pendingScan);
118
+ const livePendingSessions = new Set(pendingScan.entries.map((entry) => entry.relative));
119
+ if (cursor.resultPendingAfterBySession) {
120
+ let pruned = 0;
121
+ for (const session of Object.keys(cursor.resultPendingAfterBySession).sort()) {
122
+ if (pruned >= request.resultBudget) break;
123
+ if (!livePendingSessions.has(session) && !fs.existsSync(path.join(pendingRoot, session))) {
124
+ delete cursor.resultPendingAfterBySession[session];
125
+ pruned += 1;
126
+ }
127
+ }
128
+ if (Object.keys(cursor.resultPendingAfterBySession).length === 0) delete cursor.resultPendingAfterBySession;
129
+ }
130
+ if (pendingScan.cursorCleared) delete cursor.pendingSessionAfter;
131
+ const pendingSessions = pendingScan.entries.slice(0, pendingRemaining);
132
+ let pendingFileBudget = Math.min(sourceLimit, request.resultBudget - resultCandidates.length);
133
+ for (const session of pendingSessions) {
134
+ if (pendingFileBudget <= 0) break;
135
+ cursor.pendingSessionAfter = session.relative;
136
+ pendingFileBudget -= addFiles(
137
+ path.join(pendingRoot, session.name),
138
+ "pending",
139
+ { type: "pending", session: session.relative },
140
+ `results.pending.${session.name}`,
141
+ pendingFileBudget,
142
+ );
143
+ }
144
+ addFiles(path.join(request.resultsDir, "completion-replay"), "replay", { type: "result", key: "resultReplayAfter" }, "results.replay");
145
+ addFiles(path.join(request.resultsDir, "output-archives"), "archive", { type: "result", key: "resultArchiveAfter" }, "results.archive");
146
+
147
+ const cursorOps = [];
148
+ for (const key of ["runAfter", "resultPublicAfter", "resultReplayAfter", "resultArchiveAfter", "pendingSessionAfter"]) {
149
+ if (cursor[key] === request.cursor[key]) continue;
150
+ cursorOps.push(cursor[key] === undefined ? { type: "delete", key } : { type: "set", key, value: cursor[key] });
151
+ }
152
+ const oldPending = request.cursor.resultPendingAfterBySession ?? {};
153
+ const nextPending = cursor.resultPendingAfterBySession ?? {};
154
+ for (const session of new Set([...Object.keys(oldPending), ...Object.keys(nextPending)])) {
155
+ if (oldPending[session] === nextPending[session]) continue;
156
+ cursorOps.push(nextPending[session] === undefined
157
+ ? { type: "delete-pending", session }
158
+ : { type: "set-pending", session, value: nextPending[session] });
159
+ }
160
+ return {
161
+ type: "result",
162
+ passId: request.passId,
163
+ discoveryDurationMs: Math.max(0, Date.now() - startedAt),
164
+ rawReads: raw.reads,
165
+ sourceExhausted: raw.exhausted,
166
+ runCandidates,
167
+ resultCandidates,
168
+ cursorOps,
169
+ };
170
+ }
171
+
172
+ if (!parentPort) throw new Error("Async retention discovery requires a worker parent port.");
173
+ parentPort.on("message", (request) => {
174
+ const passId = request?.passId;
175
+ try {
176
+ parentPort.postMessage(discover(request));
177
+ } catch (error) {
178
+ parentPort.postMessage({ type: "error", passId, error: error instanceof Error ? error.message : String(error) });
179
+ }
180
+ });
package/docs/agents.md CHANGED
@@ -47,6 +47,44 @@ Rule of thumb: `scout` before you understand the code, `researcher` before you t
47
47
 
48
48
  `oracle` is an advisory reviewer that critiques direction and proposes an execution prompt without editing files. `advisor` is the same bundled role under the Claude Code-compatible name.
49
49
 
50
+ ### Optional Surf integration
51
+
52
+ When `surf-cli` is installed and loaded, Surf can expose a `gpt-pro` package agent through the `surf-oracle` external-job provider. It starts through the same `subagent({ agent: "gpt-pro" })` mental model as any other agent, but Surf owns the package agent and provider. Surf maps `model: pro` to ChatGPT GPT-5.6 Sol Pro web mode. pi-subagents does not own that model mapping.
53
+
54
+ If you disabled the old bundled `gpt-pro` workaround with `agentOverrides.gpt-pro.disabled`, remove that override before using Surf's package agent.
55
+
56
+ The Pi async run remains the source of truth for status, artifacts, wake/wait, mission attachment, retention, and diagnostics.
57
+
58
+ Claude Code can be configured as a read-only advisor with `runner.type: external-cli` when the Claude Code CLI is installed and you have verified the flags for your local version. pi-subagents does not ship or enforce Claude Code flags. Use a project or user agent like this only after checking your CLI help:
59
+
60
+ ```yaml
61
+ ---
62
+ name: claude-advisor
63
+ description: Read-only Claude Code advisor through the local CLI
64
+ runner:
65
+ type: external-cli
66
+ command: claude
67
+ args: ["<verified-read-only-flags>"]
68
+ promptDelivery: stdin
69
+ async: true
70
+ ---
71
+
72
+ Review the task and return advice only. Do not edit files.
73
+ ```
74
+
75
+ ### Advisory runner data boundary
76
+
77
+ Native `oracle` runs inside Pi and can use its configured read tools. `claude-advisor` sends the assembled prompt to the configured local external CLI through stdin. An external-job agent sends the assembled prompt to its registered provider. Provider options and a prompt digest are persisted in Pi run state. The prompt text is delivered through the local host bridge to the provider and is not stored in the public result payload. Do not place secrets in advisory prompts unless the target provider is approved to receive them.
78
+
79
+ ### External-job state table
80
+
81
+ | Durable file | Owner | States | Release predicate | Rollback predicate | Stale-head behavior | Fail-closed cases |
82
+ |--------------|-------|--------|-------------------|--------------------|---------------------|-------------------|
83
+ | `status.json` step `runner` and `externalJob` | pi-subagents async runner | `queued`, `running`, `completed`, `failed`, `stopped`, `blocked` | Provider `result` returns terminal data and the async result is written | Provider start/status/result/reattach returns an error | If a status file already has a provider job id, recovery calls `reattach` and `result`; it refuses to start a new prompt when the provider or prompt digest differs | Missing provider, capacity conflict, malformed provider response, bridge timeout, prompt digest mismatch |
84
+ | `result.json` or session result payload | pi-subagents async runner | `complete`, `failed`, `stopped` | All steps reach terminal state and result publication succeeds or is recoverably indexed | Result write fails and pending result repair records the terminal state | Stale status can repair from an existing result file | Unindexed sessionless stale failure |
85
+ | `external-job-requests/` and `external-job-responses/` | Host-mediated provider bridge | pending request, terminal response | Host process writes a matching response and removes the request | Bridge timeout or malformed request response | Requests are operation-scoped. Recovery sends `reattach`/`result`, not `start`, when job metadata exists | Provider not registered, host bridge not loaded, malformed request, provider exception |
86
+ | Provider artifact path | External provider | provider-defined terminal artifact | Provider returns `artifactPath`, or Pi writes returned text to `external-job-<index>.result.md` | Provider reports failure or no result | Existing artifact path is retained in `status.json` | Missing artifact with no text output returns a terminal message instead of inventing content |
87
+
50
88
  The `researcher` builtin uses `web_search`, `fetch_content`, and `get_search_content`. Those require [pi-web-access](https://github.com/nicobailon/pi-web-access):
51
89
 
52
90
  ```bash
@@ -102,7 +140,7 @@ Use these fields when an agent should see more:
102
140
  | `systemPromptMode: append` | Append the agent prompt to Pi's normal base prompt. |
103
141
  | `inheritProjectContext: true` | Keep inherited project instructions from files like `AGENTS.md` and `CLAUDE.md`. |
104
142
  | `inheritSkills: true` | Let the child see Pi's discovered skills catalog. |
105
- | `defaultContext: fork` | Use forked session context when a launch omits `context`; explicit `context: "fresh"` still wins. |
143
+ | `defaultContext: fork` | Prefer forked session context when a launch omits `context`; if the parent has no persisted session file or current leaf yet, the implicit default falls back to `fresh` without a failed first attempt. Explicit `context: "fork"` remains strict, and explicit `context: "fresh"` still wins. |
106
144
 
107
145
  Builtin agents opt into project instruction inheritance by default so they follow repo-specific rules out of the box. `delegate` also uses append mode because its job is orchestration inside the parent workflow.
108
146
 
@@ -171,7 +209,7 @@ Field notes:
171
209
  | `systemPromptMode` | `replace` by default; `append` keeps Pi's base prompt. |
172
210
  | `inheritProjectContext` | Keeps or strips inherited project instruction blocks. |
173
211
  | `inheritSkills` | Keeps or strips Pi's discovered skills catalog. |
174
- | `defaultContext` | Optional `fresh` or `fork` launch context default for this agent. |
212
+ | `defaultContext` | Optional `fresh` or `fork` launch-context preference. An implicit `fork` falls back to `fresh` when the parent has no persisted session file or current leaf; an explicit launch `context: "fork"` remains strict. |
175
213
  | `skills` | Selects specific skills for the child, regardless of `inheritSkills`. |
176
214
  | `skillPath` | Invocation-private skill files or discovery directories. Relative paths resolve from the agent definition file. Local matches take precedence, while unresolved or unreadable matches fall back to normal skill discovery. This field discovers candidates only; `skills` still selects what the child receives. |
177
215
  | `output` | Default single-agent output file. |
@@ -24,18 +24,10 @@ By default, project settings resolve from the nearest parent directory that cont
24
24
  { "toolDescriptionMode": "compact" }
25
25
  ```
26
26
 
27
- Controls the parent-facing `subagent` tool description registered at startup. `full` is the default. `compact` keeps the execution modes, async/`subagent_wait` guidance, child-safety boundary, management/action split, one-writer review guidance, and artifact/status essentials with less prompt bloat.
27
+ Controls the parent-facing `subagent` tool description registered at startup. The default registers split prompt metadata: a short tool description plus `promptSnippet` and `promptGuidelines`. Set `"full"` to register the complete description as one tool description, or `"compact"` to keep the execution modes, async/`subagent_wait` guidance, child-safety boundary, management/action split, one-writer review guidance, and artifact/status essentials with less prompt bloat.
28
28
 
29
29
  `custom` reads `subagent-tool-description.md` from the project config directory, then from `~/.pi/agent/subagent-tool-description.md`. Missing, empty, unreadable, or oversized custom files fall back to the full description. Custom templates may use `{{fullDescription}}`, `{{compactDescription}}`, `{{safetyGuidance}}`, `{{agentDir}}`, and `{{projectConfigDir}}`; the safety guidance is always present so custom prose cannot remove the runtime guardrails. Restart Pi after changing the mode or custom file.
30
30
 
31
- ## `legacyChainControls`
32
-
33
- ```json
34
- { "legacyChainControls": true }
35
- ```
36
-
37
- Defaults to `false`. The default registered model-facing tool schema and description omit the legacy `append-step` `step` schema and legacy checkpoint controls. This does not change runtime support for existing durable legacy chains. Set this to `true` before directly managing a legacy chain with `append-step`, `approve-checkpoint`, or `reject-checkpoint`.
38
-
39
31
  ## `inlineToolDisplay`
40
32
 
41
33
  ```json
@@ -93,7 +85,7 @@ Pi binds `Ctrl+B` to editor cursor-left by default. The extension shortcut takes
93
85
  }
94
86
  ```
95
87
 
96
- Opt in to a best-effort Orca observer that creates one Orca terminal tab for each subagent child and mirrors its live tool, assistant, stdout, and stderr progress. Tab titles use a persistent worktree-local sequence (`subagent · <agent> · 1`, `... · 2`, and so on), so separate workflows and concurrent children do not reuse the same number. This does **not** replace Pi as the child runner: native Pi children keep the same process, lifecycle, status, control, artifact, and result paths. External CLI profiles also keep their existing runner and can mirror their stdout/stderr.
88
+ Opt in to a best-effort Orca observer that creates one Orca terminal tab for each subagent child and mirrors its live tool, assistant, stdout, and stderr progress. Tab titles use a persistent worktree-local sequence (`subagent · <agent> · 1`, `... · 2`, and so on), so separate workflows and concurrent children do not reuse the same number. For the same worktree, `orca terminal create` runs one at a time in that sequence so the UI can append tabs from left to right as `1`, then `2`, then `3`. This does **not** replace Pi as the child runner: native Pi children keep the same process, lifecycle, status, control, artifact, and result paths. External CLI profiles also keep their existing runner and can mirror their stdout/stderr.
97
89
 
98
90
  The integration is off by default and supports macOS and Linux. It is disabled on Windows. When enabled, `pi-subagents` looks for executable `orca` on `PATH`, or uses the executable path in `PI_SUBAGENT_ORCA_BINARY`. If no executable is available, Orca is not running, the cwd is not an Orca-managed worktree, or `terminal create` fails, the authoritative subagent still runs normally. Tab creation is deliberately best-effort and never changes the child result.
99
91
 
@@ -107,6 +99,16 @@ Set `enabled` to `false` (or remove the block) as a kill switch. In that state,
107
99
 
108
100
  WorkflowScript 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.
109
101
 
102
+ ## `defaultSubagentContext`
103
+
104
+ ```json
105
+ { "defaultSubagentContext": "fresh" }
106
+ ```
107
+
108
+ Sets `fresh` or `fork` for every subagent launch that omits `context`. This global preference replaces each agent-level `defaultContext`. Explicit `context: "fresh"` or `context: "fork"` still wins.
109
+
110
+ With `"fork"`, the setting uses the existing implicit-fork behavior. A launch starts fresh when the parent session file or current leaf is not available. `"fresh"` starts fresh even when the selected agent defaults to fork. Scheduled runs continue to set fresh context explicitly. A runner or provider that does not support fork context keeps its existing rejection behavior.
111
+
110
112
  ## `fleetView`
111
113
 
112
114
  ```json
@@ -406,9 +408,9 @@ Controls where subagent artifact files (inputs, outputs, transcripts, metadata)
406
408
  - `"session"` (default): stores artifacts under pi's session directory (`~/.pi/agent/sessions/<session>/subagent-artifacts/`), keeping the working directory clean. It falls back to the OS temp directory when no session file exists.
407
409
  - `"temp"`: uses the OS temp directory.
408
410
 
409
- This preference also controls the default chain scratch directory. `"project"` uses `<cwd>/.pi/subagents/chain-runs/`, while the default `"session"` and `"temp"` use the user-scoped temp chain directory.
411
+ This preference also controls the default workflow artifact directory used by scripted chaining. `"project"` uses `<cwd>/.pi/subagents/chain-runs/`; the directory keeps its legacy name for compatibility. The default `"session"` and `"temp"` use the user-scoped temp workflow artifact directory.
410
412
 
411
- The `"session"` option uses the same directory that `cleanupAllArtifactDirs` already scans for age-based cleanup, so artifacts are still cleaned up automatically. Temporary chain directories are cleaned up separately after 24 hours.
413
+ The `"session"` option uses the same directory that `cleanupAllArtifactDirs` already scans for age-based cleanup, so artifacts are still cleaned up automatically. Temporary workflow artifact directories are cleaned up separately after 24 hours.
412
414
 
413
415
  When a project-scoped launch runs from an npm package directory, pi-subagents warns if package settings can include `.pi/subagents/` in the published package. Add `.pi/subagents/` to `.npmignore` (or `.gitignore` when no `.npmignore` exists), use a `files` allowlist that does not include `.pi/subagents/`, or select `"session"` or `"temp"`.
414
416
 
@@ -437,3 +439,19 @@ Controls smart batching of async-completion notifications. When several backgrou
437
439
  ## `permissions`
438
440
 
439
441
  Native child tool permission rules. See [watchdog.md](watchdog.md#native-child-tool-permissions).
442
+
443
+ ## `PI_SUBAGENT_FS_RETRY_MAX_TOTAL_MS`
444
+
445
+ Caps the total time a single retried filesystem operation may sleep, in milliseconds. Environment-only; there is no config key.
446
+
447
+ Atomic status and result writes retry on `EACCES`, `EBUSY`, and `EPERM`, which on Windows are usually a scanner or a sibling process holding the destination of a rename for a moment. The retry ladder sleeps up to about 7.9s in total, and it sleeps *synchronously* — `Atomics.wait` parks the calling thread rather than spinning.
448
+
449
+ That is the right trade-off for a CLI. It is the wrong one for a long-lived process that loads `pi-subagents` in-process and runs those writers on its event loop: one contended rename stalls everything it serves for the length of the ladder, and because the thread is parked rather than busy, it presents as an unresponsive process sitting at 0% CPU. A wide fanout makes contention on a single `status.json` likely.
450
+
451
+ Set this to bound that stall. The ladder keeps its number of attempts and only the sleeps shrink, because `run-fanout-budget` and mission state locking use the ladder's length as their attempt budget:
452
+
453
+ ```text
454
+ PI_SUBAGENT_FS_RETRY_MAX_TOTAL_MS=1000
455
+ ```
456
+
457
+ Unset by default, so behaviour is unchanged unless you opt in. Opting in trades lock-wait tolerance for responsiveness: entries clamped to `0` return immediately, so contention that would previously have been waited out surfaces as an error sooner. Values that are not a non-negative integer fail instead of being coerced.
@@ -132,6 +132,7 @@ Boundaries:
132
132
  - Raw prompts are not exposed in public contract output.
133
133
  - It is side-effect-free for launch state: it does not create child sessions, temp prompt files, structured-output runtimes, tool-diagnostic files, or run artifacts.
134
134
  - Some host-owned facts, such as exact fork snapshots, nested async roots, and live model registries, can only be proven by the Pi host; those appear as `host_required` diagnostics instead of silently pretending to be exact.
135
+ - Preflight reads the extension config, so `defaultSubagentContext: "fresh"` or `"fork"` affects omitted context in the same way as execution. Explicit `context` still wins.
135
136
 
136
137
  ## Structured delegation API
137
138
 
@@ -262,6 +263,26 @@ Semantics:
262
263
 
263
264
  Child processes do not gain provider tools or extensions automatically. Add `subagent_wait` to the child agent's `tools` allowlist and load each provider through `extensions` or `subagentOnlyExtensions`. The parent's effective `waitTool` setting is serialized through foreground, async, resume, chain, parallel, and fanout launch paths; `PI_SUBAGENT_WAIT_TOOL_ENABLED` keeps precedence.
264
265
 
266
+ ## External job provider bridge
267
+
268
+ Extensions that own long-running advisor jobs can register a process-local provider for `runner.type: external-job` agents:
269
+
270
+ ```ts
271
+ import { registerExternalJobProvider } from "pi-subagents/external-job-provider";
272
+
273
+ const dispose = registerExternalJobProvider({
274
+ name: "surf-oracle",
275
+ start: ({ prompt, promptDigest, cwd, runId, stepIndex, agent, options }) => startSurfJob({ prompt, promptDigest, cwd, runId, stepIndex, agent, options }),
276
+ status: (providerJobId) => getSurfJobStatus(providerJobId),
277
+ result: (providerJobId) => getSurfJobResult(providerJobId),
278
+ reattach: (providerJobId) => reattachSurfJob(providerJobId),
279
+ });
280
+ ```
281
+
282
+ The provider returns handles with `providerJobId`, `state`, optional `handleUrl`/`conversationUrl`, optional `failureCode`/`failureMessage`, and optional `blockingJobId` for capacity conflicts. `result` can also return `output` and/or `artifactPath`.
283
+
284
+ The async runner process does not import provider internals. It writes operation requests into its async run directory. The parent Pi process services those requests against the registered provider and writes operation responses. If the provider is not registered, the bridge fails closed with an actionable error. If a run is recovered after provider job metadata exists, the runner calls `reattach` and `result`; it does not call `start` again.
285
+
265
286
  ## Herdr integration
266
287
 
267
288
  When Pi runs inside [Herdr](https://herdr.dev), pi-subagents automatically reports active async-run counts through Herdr pane metadata.
@@ -323,6 +344,26 @@ const closed = await closeProjectPane({ cwd: "/path/to/repo", requireIdle: true
323
344
 
324
345
  The API returns discriminated structured results with canonical project root, binding path, pane identity, bounded Herdr runtime fields, and stable error codes. `requireIdle: true` fails closed unless Herdr explicitly reports `agent_status: "idle"`; use it when an owning extension must not close a working or blocked pane. The API deliberately reports `trust: "human-verification-required"`: it never bypasses or claims to attest Pi's project-trust prompt. `PROJECT_PANES_API_VERSION` is currently `1`.
325
346
 
347
+ ## Host session lifetime and completion wakes
348
+
349
+ A host that embeds this extension owns whether completion wakes can be delivered at all.
350
+
351
+ Ordinary async and foreground completion wakes use `registerSubagentNotify` and `sendCompletion`. They listen for completion events and deliver through `pi.sendMessage(..., { triggerTurn })`. Session shutdown stops the result watcher and disposes this completion notifier. `createWaitSubscriptionManager` is separate: it is the explicit non-blocking `subagent_wait` subscription path, not the ordinary completion wake path.
352
+
353
+ Detached children do not stop when the session does. They are the host process's children, not the session's, so the run keeps going, completes, and notifies nobody. What is lost is the notification, not the work.
354
+
355
+ This matters because "is the parent busy?" is the wrong idle signal. A parent that launches a detached run and hands control back — which is what the async launch output tells it to do — is not prompting, streaming, compacting, or running a shell command. A host that reaps sessions on those signals alone will dispose exactly the session that was waiting to be woken.
356
+
357
+ If your host reclaims idle sessions, keep a session alive while it still has live detached work:
358
+
359
+ - Read run state from the status files under the async run directory rather than from event traffic. A long, quiet workflow sends almost nothing to the parent, so recent-activity heuristics conclude the wrong thing.
360
+ - Treat `queued` and `running` as live, matching `isActiveAsyncState`. An interrupted run that is `paused` is finalized. A workflow that paused because a child used `contact_supervisor` still has a live child; keep that parent session until reconcile writes `complete` or `failed`.
361
+ - Do not treat `lastUpdate` as a heartbeat. The runner advances it in memory every second but only rewrites `status.json` when the activity classification changes, so a live run inside one long quiet tool call leaves a stale file behind. Judging liveness by file age will reap exactly the run you meant to protect.
362
+ - Prefer the recorded runner `pid`, which stays true through a silent tool call and goes false when the runner dies. Keep file age only as a fallback for runs that record no pid, and give it a wide window.
363
+ - Match `sessionId` in `status.json` against both forms. It is resolved as `getSessionFile() ?? getSessionId()`, so it is normally the parent's session *file path*, but a session that is not persisted records a bare session id instead.
364
+
365
+ The symptom when this is missed is quiet and easy to misattribute: subagents appear never to report back, which looks like a fault in this extension rather than in the host that disposed the listener.
366
+
326
367
  ## Runtime files
327
368
 
328
369
  The main runtime files in this repository:
@@ -336,7 +377,7 @@ The main runtime files in this repository:
336
377
  | `src/runs/background/subagent-runner.ts` | Detached async runner. |
337
378
  | `src/runs/background/async-execution.ts` | Background launch support. |
338
379
  | `src/runs/background/async-status.ts` | Status discovery and formatting for async runs. |
339
- | `src/runs/foreground/chain-execution.ts` / `src/agents/chain-serializer.ts` | Chain orchestration and `.chain.md` parsing. |
380
+ | `src/workflows/scripted-workflow.ts` / `src/runs/foreground/subagent-executor.ts` | Scripted workflow orchestration and child launch routing. |
340
381
  | `src/shared/settings.ts` | Chain behavior, instructions, and config helpers. |
341
382
  | `src/runs/shared/worktree.ts` | Git worktree isolation. |
342
383
  | `src/intercom/intercom-bridge.ts` | Runtime intercom bridge instructions and diagnostics. |
package/docs/models.md CHANGED
@@ -141,6 +141,8 @@ You do not have to spell a model exactly. Model ids are matched fuzzily against
141
141
 
142
142
  Exact `provider/id` matches still win, and a qualified provider query never silently switches providers — it only matches within the named provider. Ambiguous bare ids that exist under multiple providers still require a provider prefix or the current session's provider to disambiguate.
143
143
 
144
+ Registry ids that themselves contain `/` (Hugging Face `owner/name`) resolve the same way as Pi's main agent: `thinkingmachines/Inkling` becomes `huggingface/thinkingmachines/Inkling` when that id is unique or offered by the current session provider. A first path segment that matches a registered provider still means `provider/id`.
145
+
144
146
  ## Model scope enforcement
145
147
 
146
148
  To keep subagents inside a budget or compliance profile, enforce a model scope. Put `subagents.modelScope` in user or project settings (project overrides user):