@signalridge/pi-subagents 1.11.0 → 1.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.0
4
+ ### Minor Changes
5
+
6
+ - c2fd867: - Add an independently implemented, explicitly opted-in background-only parent clarification broker. Main-only answer_subagent_question validates one-use bounded IDs, original child/session/branch/attempt ownership and offered choice tokens; raw choices stay separate from sanitized request-local display. Coalesce bounded pending questions into ordinary prepared idle/settled extension turns without cloning a model, rewriting user text/images, copying parent history, or adding question storage/settings. Cancellation/timeout/retirement returns structured no-answer, never approval. Default human contact, ask_tools and external permission gates remain unchanged. Reject unsupported foreground/scheduled/mode-changing resume requests before spawn and leave managed protocol-v4 payloads unchanged.
7
+ - Fence parent questions to captured public session-manager identity and complete native branch prefixes at both channel construction and each live question invocation, including resumed-child metadata divergence before delayed committed-tree notifications. Track observed root preparation separately from native isIdle and release package-owned handled mentions with INPUT-scoped readiness leases; native throws continue preparation and another concurrent prompt's lease remains intact. Authenticate factory-local extension wakes with bounded nonce/MAC tokens and constant-memory historical proof recognition. Separate accepted-frame provenance and public ctx.signal/native-end run ownership from the outbox flight: a competing preparation's busy-failure settlement cannot erase the executing frame. Distinct frame tokens and authenticated empty-content native metadata prove only unambiguous admitted frames at public message boundaries, without guessing from promptless agent_start or stripping verbatim user quotations. An INPUT-observed copy of an unprepared frame token retires that bounded admission as ambiguous: neither prompt receives proof, abort, question context, or text/image stripping; affected questions return no-answer, while later ordinary input contexts can receive new questions. Recognize the authenticated leading token boundary plus its retained ambiguous admission even after native SDK image hints are appended; suppress only that question channel without creating control ownership, aborting, or modifying user data. Request-local context filters proven control pairs and only the proven obsolete control run can be aborted. ACK delivery against the exact flight, bound each question to one wake attempt with 30-second unacknowledged no-answer recovery, and retain unresolved accepted preparations within a fixed admission cap. Document external handled/auth-rejection and ambiguous-preparation readiness limits, native concurrent-prompt busy rejection, and the absence of atomic pre-persistence cancellation; verify real SDK later-preparation double barriers, user/proof message-start/end awaits through busy settlements, copied-token release orders with/without images and real default-AUTO 4096x4096 PNG normalization while another child's question remains pending, guard-disabled controls, handled mentions, resumed-child summaries, and live ownership without private scheduler/renderer/history patches. Human permission gates and child answer-tool capability isolation remain unchanged.
8
+ - Exclude pi-background from child resource loading before factories execute, including default inherited loaders and wildcard selection. Refuse intentional extension selections by name and warn on filtered inheritance/tool selectors: delayed process exits must not wake a retained child after manager completion, concurrency-slot release, or worktree cleanup. Merge configured and explicit ownership by canonical real file identity, including configured generic file symlinks and direct glob/conventional entries resolved through Pi's public package manager. Canonical declaration probes remain additive across ancestor manifests and disabled resource preferences, so an ordinary lexical owner cannot conceal a background target. Retain package names and source aliases for loaded-name validation and live `ext:` tool narrowing. Ordinary extensions remain available and no background settings schema is duplicated.
9
+ - Selectively port tintinweb/pi-subagents commit 7da6bef828d7582ee33386ece9379c170e7742c4 (2026-10-09, #374; reported by @BarrMan): FleetView recognizes non-inheriting custom editors by callable getText/setText while preserving dialog/selector input ownership and unknown-focus fallback. Raw inherit_context remains refused; no clone-parent-history behavior is imported.
10
+ - Separate per-resource declaring ownership from admission/exclusion alias unions, probe directory-backed module entries with public Node resolution without factory evaluation, and validate explicit sources before cross-source deduplication. Existing file/package aliases execute once; selecting another package's canonical entry no longer suppresses an alias owner's unrelated entries.
11
+ - Add independently implemented container-first workflow-owner groups and a bounded retained-child inspector, inspired by tintinweb/pi-subagents 221df02323c6b6350ea8474579e0ee0f6cc84cea/fd8d81618d80ae83a3faf40cb87dae99cca33264 and collision-only discriminators from nicobailon/pi-subagents 69673c7fbf3351e54a5b0786a7e9d9fff625a749. Counts describe retained children only; no authoritative workflow status/name/phase inference, upstream engine import, or new RPC. Preserve upstream license attribution.
12
+ - Keep Fleet selection by stable ID across refresh/linger, retain mounting and public structural focus evidence during invalidation, and recover overlay ownership after opener errors/disposal. Bind child conversation controls to their retained session and owner attempt so thread replacement cannot receive stale commands.
13
+ - Preserve rejected steering drafts (including boolean/async false and thrown failures) with an error while retaining legacy void callbacks. Completed viewers use retained lifetime usage and known compaction statistics without unguarded disposed-session access. Encode scheduled-job display copies and UI labels with the shared pi-ui sanitizer without rewriting stored prompts/paths/messages.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [c2fd867]
18
+ - @signalridge/pi-ui@1.4.0
19
+
20
+ ## 1.11.1
21
+ ### Patch Changes
22
+
23
+ - 3364cc0: Gate project agent definitions, own settings, and custom tool descriptions on the host's fail-closed project trust decision. Reject denied definition provenance before child package resolution, including fallbacks and global aliases into denied roots; refresh configuration authority on session replacement and reload.
24
+ - 3364cc0: Use patched Vitest 4.1.11 for development and regression testing without moving to a new major version. Refresh compatible transitive dependencies to include fixed source-map and brace-expansion releases.
25
+ - 3364cc0: Inherit the parent's public Pi project-trust decision before child resource resolution instead of accepting the SDK's trusted default. Unknown trust and different configuration roots deny project settings, extension factories, skills (including named preloading), and MCP configuration without prompting or granting trust. Explicit project resource paths cannot bypass denial. Preserve global resources, child policy restrictions, and trusted parent configuration during worktree execution and nested delegation; bind built-in MCP configuration discovery to that same trusted configuration root.
26
+ - 3364cc0: Preserve denied parent resource provenance when a child changes its configuration root. Reject explicit parent extensions and filter configured/discovered extensions and skills using both denied roots, including canonical aliases, before extension execution or skill injection. Named skill preloading also rejects denied sources behind global-root ancestor aliases. Genuine global resources and trusted parent configuration during worktree execution remain available; this resource trust boundary is not an OS sandbox or containment of arbitrary global extension behavior.
27
+ - 3364cc0: Fingerprint tracked gate inputs without textconv or external diff helpers, and retain full binary-safe changes so transformed or abbreviated diffs cannot reuse a stale passing verdict.
28
+ - 3364cc0: Declare `@sinclair/typebox` as a host-provided peer dependency with a `*` range instead of a runtime dependency, avoiding duplicate runtime modules and Pi extension-loader warnings.
29
+ - 3364cc0: Reject native global settings and MCP configuration aliases into retained denied project roots before SDK parsing or package resolution. Walk raw path components and symlink targets without collapsing dot segments, so a traversed denied directory cannot be hidden by a later `..` or an outward relay. Preserve genuine global configuration and aliases into approved parent roots without duplicating host configuration schemas.
30
+ - 3364cc0: Capture configuration trust before nested agent discovery and invocation defaults, and preserve denied ancestor roots across nested delegation. Reject global aliases into denied parent or child configuration roots before definition admission, model setup, or resource resolution while retaining authorized global definitions and standalone discovery defaults.
31
+ - 3364cc0: Adopt Pi 1.0.4's host-provided dependency contract: declare imported Pi and TypeBox modules as `*` peers rather than installing duplicate runtime modules, and align Pi development pins with the repository's tested 1.0.4 baseline. The wildcard identifies host module ownership; it does not promise compatibility with every Pi version.
32
+ - 3364cc0: Close configuration-admission gaps: apply model scope only from admitted settings at the captured configuration root, retain ancestor denials, and reject resource aliases that traverse denied project paths before escaping outside. Refresh provider-facing Agent descriptions and configuration-derived parameter descriptions through Pi's public tool-registration API when preparing prompts and continuations, preserving inactive tools. Disable gate-result caching while untracked files exist and prevent external diff commands during fingerprint inspection.
33
+ - 3364cc0: Keep Goal terminal tools directly declared but unavailable to nested execution, where termination is not propagated. Verify direct termination and settlement-gated queue advancement against the installed Pi 1.0.4 host and codemode.
34
+
35
+ Translate Pi exec results into the existing gate executor contract, failing killed commands even when their code is zero and enabling successful git fingerprint inspection. Exercise real-SDK exposure, nested approval, policy, and navigation regressions on Pi 1.0.4 instead of skipping them.
36
+ - Updated dependencies [3364cc0]
37
+ - Updated dependencies [3364cc0]
38
+ - Updated dependencies [3364cc0]
39
+ - Updated dependencies [3364cc0]
40
+ - @signalridge/pi-subagents-protocol@1.5.1
41
+ - @signalridge/pi-ui@1.3.3
42
+
3
43
  ## 1.11.0
4
44
  ### Minor Changes
5
45
 
package/README.md CHANGED
@@ -46,6 +46,16 @@ From the repository root:
46
46
  pi -e ./packages/pi-subagents
47
47
  ```
48
48
 
49
+ **Verified host baseline:** Pi 1.0.4. Offline resource-ownership, child tool-policy, and native Fleet input barriers are additionally exercised on Pi 1.1.0; that focused coverage is not complete host qualification. Wildcard peer dependency ranges and retained capability fallbacks do not establish support for older host versions.
50
+
51
+ ## Upstream and local divergence
52
+
53
+ Derived from [tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents); the upstream MIT license is preserved. The FleetView editor-focus fix from [commit `7da6bef828d7582ee33386ece9379c170e7742c4`](https://github.com/tintinweb/pi-subagents/commit/7da6bef828d7582ee33386ece9379c170e7742c4) (2026-10-09, #374; reported by @BarrMan) is selectively ported: callable `getText`/`setText` identifies non-inheriting custom editors without taking dialog or selector keys. No clone-parent-history behavior is imported; raw `inherit_context: true` remains refused. This fork also excludes `pi-background` from child resource loading until asynchronous child continuation has manager-owned lifecycle accounting.
54
+
55
+ The container-first FleetView and bounded drill-down patterns are independently adapted from tintinweb/pi-subagents at [`221df02323c6b6350ea8474579e0ee0f6cc84cea`](https://github.com/tintinweb/pi-subagents/tree/221df02323c6b6350ea8474579e0ee0f6cc84cea/src/ui) and [`fd8d81618d80ae83a3faf40cb87dae99cca33264`](https://github.com/tintinweb/pi-subagents/tree/fd8d81618d80ae83a3faf40cb87dae99cca33264/src/ui). Collision-only child-label discriminators are inspired by [nicobailon/pi-subagents `69673c7fbf3351e54a5b0786a7e9d9fff625a749`](https://github.com/nicobailon/pi-subagents/blob/69673c7fbf3351e54a5b0786a7e9d9fff625a749/src/tui/render-helpers.ts). This is **not** the upstream workflow engine or full phase inspector: there are no inferred workflow names, phases, statuses, or workflow-wide controls, no new RPC, and no workflow source imports.
56
+
57
+ The opt-in parent clarification broker is locally implemented. It uses the existing main session, not an upstream LLM arbitrator, cloned parent history, private UI automation, or an additional Agent per question. Permission gates remain human-owned.
58
+
49
59
  ## Lint policy
50
60
 
51
61
  Formatter checks remain disabled for this package, while legacy `noExplicitAny`, control-character regex, and empty-interface rules remain off. Signalridge additions follow the package AGENTS rules and the strict `--error-on-warnings` lint gate.
@@ -69,6 +79,40 @@ At a TUI prompt, `@explore find it` starts one background Explore agent. In mode
69
79
 
70
80
  **Pi runtime requirement:** On hosts where child sessions require the parent's `ModelRuntime`, Pi currently exposes it to extensions only through the registry's private `.runtime` property. If that bridge is missing or incompatible, Agent, mentions, and managed spawns refuse before allocating an ID or making a provider request; they do not silently construct a fresh runtime that would lose custom providers, credentials, or virtual routes. Existing managed idempotency keys remain readable. An explicit child model is validated against the parent runtime *after* final model selection, so a stale `ctx.model` alone does not block a valid physical child model. The error advises updating to a host that exposes the parent runtime, or disabling dispatch until one does.
71
81
 
82
+ ### Opt-in parent clarification (background only)
83
+
84
+ By default, `contact_supervisor` still asks the human. Explicitly opt a **fresh, non-isolated top-level background** child into ordinary main-agent clarification:
85
+
86
+ ```js
87
+ Agent({
88
+ subagent_type: "Explore",
89
+ prompt: "Investigate the schema. Ask only ordinary task clarifications, never approval.",
90
+ description: "Investigate schema",
91
+ run_in_background: true,
92
+ supervisor_questions: "parent",
93
+ })
94
+ ```
95
+
96
+ The child awaits an in-process answer promise. Only its explicit bounded question reaches the main model: sanitized request-local context contains a one-use `questionId`, original `agentId`, and numbered choice tokens. The main-only `answer_subagent_question({ questionId, answer })` accepts free text, an offered `option:N` token, or an empty answer to decline. Display labels are separate from the original choice data returned to the child; duplicate identical answers never resume it twice. Question text is untrusted data, not new instructions or authorization.
97
+
98
+ The broker coalesces at most 16 pending questions (one per child), issues at most 64 IDs per session/branch generation, and expires unanswered questions after two minutes. Each question is displayed in hidden request-local context at most once. It requests one normal prepared extension turn only when the root is idle, has no queued messages, has no **observed input/preparation** in progress, and is not waiting on a dialog; otherwise questions join the next ordinary model context or wake after final `agent_settled`, never at retry-prone `agent_end`. No question means no extra model request. It does not edit genuine user text or images, transfer parent history/credentials into the child, poll a model, persist a separate question history, or automatically retry unanswered questions.
99
+
100
+ Idle wakes are factory-local authenticated control tokens, not a claim on a familiar sentence. Recognition requires `input.source === "extension"` and an exact namespace/nonce/MAC; an interactive/RPC literal, foreign-namespace extension literal, or invalid MAC remains ordinary input. Admission replaces only the extension-owned input with a distinct frame token. While that admission remains unambiguous, preparation adds an authenticated **empty-content** native custom message as prompt-local frame evidence, never persistent model guidance. Native `message_start` identifies the executing frame; the promptless `agent_start` and old branch messages cannot identify it. Superseding genuine input invalidates wake authorization without forgetting accepted control identity. Request-local context removes only proven control pairs; verbatim user quotations remain unchanged. Historical proof recognition uses a fixed-size secret and constant recognition state, not an accumulating ledger. At most 16 unresolved or ambiguous admissions are retained independently of the flight; an exhausted admission budget refuses further wakes rather than forgetting unresolved frames.
101
+
102
+ `before_agent_start` exposes no input source or delivery ID: token text alone cannot prove identity. If genuine INPUT copies an admitted token **before** its preparation reaches this package, both matching frames become ambiguous. That admission is retained inert within the same cap, neither frame receives a control proof or question context, neither is aborted or stripped by token text, and the flight's unannounced questions receive structured no-answer. The extension does not alter literal text or images. Native SDK image normalization can append a hint and resize an attachment; question suppression still requires the first text's authenticated token boundary and its retained ambiguous admission, never generic hint prose. This evidence cannot authorize a control proof or abort. The package cannot atomically cancel the unidentifiable control request before native persistence/provider dispatch. Ambiguous preparations cannot release a genuine input's readiness; idle wakes remain conservatively fenced until session retirement, but later ordinary inputs can still receive new questions normally. An already-prepared wake retains its positive native proof and cannot donate it to a later literal quotation.
103
+
104
+ An owned request-local `context` delivery is the ACK. Each question has **one wake attempt**: no ACK within 30 seconds clears only that flight and returns structured no-answer for its still-unannounced questions. Async native authentication rejection and earlier input interception cannot occupy the flight indefinitely or retry acknowledged questions. A superseding genuine prompt retains its own preparation readiness; except for the explicit copied-token ambiguity above, its questions join that prompt or expire normally. Executing-frame ownership separately captures the public `ctx.signal` and its matching native `agent_end`. A competing preparation's busy-failure `agent_settled` cannot clear that frame while the core operation still owns a public signal; only a proven ended and released owner permits cleanup. Native preparation can record inert control tokens and empty frame proofs in ordinary session history; question text and answers are not stored as separate broker entries.
105
+
106
+ **Public-hook limits:** handlers run in registration/load order. Observed genuine INPUT retires idle readiness synchronously, before later awaits. This package's mention interception releases only its own INPUT-scoped preparation on a native `handled` result; a thrown handler is not handled (Pi reports it and continues), and another concurrently preparing input is never released by that mention. Pi exposes neither an earlier handler's in-progress input nor a generic input-handled/auth-rejection completion notification. External handling/rejection after observation can therefore retain preparation readiness until session retirement; no timer guesses that a genuine prompt is inactive. Unresolved accepted wake preparations also retain their bounded admission slots until preparation arrives or the activation is replaced.
107
+
108
+ Pi has no cancellable post-preparation/pre-persistence boundary. Already-prepared competing prompts cannot be atomically serialized or cancelled before native persistence through public hooks. Cancellation can leave an inert native control record; a proven invalidated control frame is never reclassified as ordinary instructions or given question context. Abort is issued only after an authenticated native message boundary proves the currently executing owned frame, never from a global accepted-token guess or to stop a genuine/other run. SDK 1.0.4 coverage includes later-preparation release orders, user/proof message-start and message-end awaits, copied unprepared tokens with and without images, default AUTO resizing of real 4096x4096 PNGs in both release orders while another child's question stays pending, and guard-disabled controls. Native concurrent prompts can still reject with “already processing”; the package preserves observed frame identity but does not guarantee native concurrent-prompt acceptance or atomic pre-persistence cancellation. This is not a private SDK/renderer/history patch.
109
+
110
+ Answers are **not human approval or permission grants**. `ask_tools`, `createAskGate`, project trust, external permission dialogs, and human rejections are unchanged. A decision outside existing user task authorization must be declined and left to the human. Cancellation (including an aborted main turn), child stop/removal, timeout, navigation, or reload returns structured no-answer/assumption guidance, never approval. Pending IDs capture the public root session-manager identity, session ID, and complete native branch-entry prefix (including non-model metadata) at child channel construction **and at every question invocation from the live root context**, plus the current child attempt. A terminal child's background resume keeps its channel guard but cannot reuse that older prefix as the origin of a new question. Admission, context reads, and answer dispatch validate both live ancestries, not only generation events or model-text projections. Ordinary appends retain ownership; metadata-distinct divergent ancestry retires it even if an earlier asynchronous committed-tree handler has delayed this extension's notification. Old IDs cannot answer a replacement run.
111
+
112
+ Foreground parent-mode calls are rejected **before spawn**: a main model blocked waiting for a child cannot execute another answering tool. Schedules and changing question mode on `resume` are also unsupported. An existing parent-mode child can resume only in background with `supervisor_questions` omitted; it retains its mode. Do not call `get_subagent_result(wait: true)` while an ordinary clarification is pending: that wait blocks the answering model until cancellation or the question timeout. Nested children cannot opt in or call the root answering tool. Managed protocol-v4 requests do not carry this new mode and remain unchanged; this MVP supports direct `Agent` calls and in-process `SpawnOptions.supervisorQuestions: "parent"` only when the root manager has issued the channel. No new global configuration is added. The existing Supervisor questions setting must remain enabled; `supervisor_questions: "off"` withholds contact for one fresh call.
113
+
114
+ Install the changed package and reload/restart Pi to use this feature; an already running older installed package does not acquire new tools or parameters from checkout edits.
115
+
72
116
  ### Scheduling
73
117
 
74
118
  Add a `schedule` field to register the agent to fire later instead of running now:
@@ -112,7 +156,7 @@ above-editor widget is gone.
112
156
  Runs that enter through the scheduler or a cross-extension spawn appear in the
113
157
  list too, alongside `Agent`-tool runs.
114
158
 
115
- Every string these surfaces take from a child run — its description, activity preview, error, the `Agent` tool's own result and completion notification, and the conversation overlay's messages, tool results and command output — is text the extension did not author, so it is neutralized before rendering: terminal escape sequences are dropped, bidirectional overrides and other cursor-moving code points are shown as `[U+XXXX]`, and binary content is replaced with a placeholder. An agent file's `display_name` and `description` get the same treatment as they are loaded, since a cloned repository supplies them. A subagent that reads a poisoned file or fetches a hostile page cannot repaint or reorder the parent's terminal.
159
+ Every string these surfaces take from a child run — its description, activity preview, error, the `Agent` tool's own result and completion notification, and the conversation overlay's messages, tool results and command output — is text the extension did not author, so it is neutralized before rendering. Fleet, conversation labels and scheduled-job selectors/confirmations/notifications use `@signalridge/pi-ui` display encoding (controls shown as `\\uXXXX`). Transcript previews drop terminal escape sequences, name remaining unsafe code points as `[U+XXXX]`, and replace binary content with a placeholder. These are display copies, not changes to stored prompts, paths, or messages. An agent file's `display_name` and `description` get the same treatment as they are loaded, since a cloned repository supplies them. A subagent that reads a poisoned file or fetches a hostile page cannot repaint or reorder the parent's terminal.
116
160
 
117
161
  The token field is annotated with two optional signals inside parens:
118
162
  - **`NN%`** — context-window utilization (color-coded: <70% dim, 70–85% warning, >=85% error). Omitted when the model has no declared `contextWindow`, or briefly right after compaction.
@@ -141,6 +185,10 @@ the rest of the row out of alignment on some terminals and not others.
141
185
 
142
186
  The list is ordered earliest-launched first, and only shows agents you can actually open (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press down or left to move focus from the prompt into the list. Up and down move the selection, Enter opens the selected agent's live conversation overlay (it auto-updates as the agent works), and Esc (or up above `main`) returns to the prompt. Selecting `main` returns to the normal view. The conversation overlay uses a complete `╭─╮` / `│` / `╰─╯` border. Press Enter to chat with a running agent — type a message and Enter to send it (Esc or an empty submit returns). For a queued agent that has no session yet, type `@handle message` in the main prompt; the message is delivered when it starts. Press `x` twice to stop a running agent. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures navigation keys when the prompt is empty. Disable it entirely via `/agents → Settings → Fleet view`.
143
187
 
188
+ Workflow-owned records appear in container rows **above ordinary agents**: `Workflow <runId> · N retained children`. These groups use only validated, already-available `owner.extension` and `owner.runId`; their counts describe records the manager still retains, not total workflow calls or authoritative run status. Opening one gives a terminal-height-bounded child list with stable-ID selection, Up/Down, PageUp/PageDown, Home/End and Enter to view a conversation; Esc returns from the conversation to the child list, then to FleetView. Sessionless queued children remain visible with an explicit unavailable-session message. Completed owned children remain inspectable while retained, independent of the ordinary four-second linger. Duplicate descriptions receive an ID discriminator only when needed. Detached records are excluded, and controls opened on an old thread attempt cannot steer/stop a replacement attempt.
189
+
190
+ Steering rejection (including a child finishing while the composer is open) preserves the draft and shows an error; Esc explicitly discards it. Completed conversation headers fall back to the retained lifetime usage and compaction count when live activity has been removed.
191
+
144
192
  Individual agent results use restrained text status labels:
145
193
 
146
194
  | State | Example |
@@ -229,7 +277,7 @@ All fields are optional — sensible defaults for everything.
229
277
  | `description` | filename | Agent description shown in tool listings |
230
278
  | `display_name` | — | Display name for UI (e.g. the agent list, the conversation overlay) |
231
279
  | `tools` | all 7 | Which tools the agent can call. Built-in names (`read, grep, …`), `*` / `all` (all built-ins), `none`, and `ext:<extension>` / `ext:<extension>/<tool>` selectors for extension tools. See [Tool & extension scoping](#tool--extension-scoping) below |
232
- | `extensions` | `true` | Which extensions to load for the agent. `true` (all defaults), `false` (none), or an explicit list: `[mcp, "/abs/path.ts", "*"]`. See [Tool & extension scoping](#tool--extension-scoping) below |
280
+ | `extensions` | `true` | Which extensions to load for the agent. `true` (all child-safe defaults), `false` (none), or an explicit list: `[mcp, "/abs/path.ts", "*"]`. `pi-background` is excluded; intentional selection is refused by name. See [Tool & extension scoping](#tool--extension-scoping) below |
233
281
  | `exclude_extensions` | — | Extension denylist applied after `extensions:` — exclude wins. Plain names only (case-insensitive), no paths or `*`. Useful with `extensions: true` to drop one extension (e.g. `pi-notify`) |
234
282
  | `skills` | `true` | `true` uses Pi's discovered skills, including skills supplied by loaded extensions; `false` exposes none; a comma-separated list preloads only those named skills into the system prompt (see [Skill Preloading](#skill-preloading) for discovery locations) |
235
283
  | `memory` | — | Persistent agent memory scope: `project`, `local`, or `user`. Auto-detects read-only agents |
@@ -278,12 +326,22 @@ Nested children don't occupy `maxConcurrent` slots — their parent already hold
278
326
 
279
327
  Because a subagent session never activates this extension (that is what keeps a child from building a second agent manager, and it is why nested tools are injected directly instead), a subagent also gets none of the extension's other surfaces: no `/agents` command, no cross-extension RPC handlers, no `subagents:ready` event.
280
328
 
329
+ ### Child project trust
330
+
331
+ Root startup and reload also gate this package's project `subagents.json`, `.pi/agents`, `.agents/agents`, and custom tool description on the public project-trust decision. Unknown or throwing trust checks skip project configuration before parsing; global definitions/settings and embedded defaults remain available. Global aliases into denied roots are not treated as global authority. The runner rejects a denied project-owned definition (including the actual fallback definition) before package resolution, even when it requests an extension outside the project or in a global package cache.
332
+
333
+ On Pi 1.0.4, children capture the parent's public `isProjectTrusted()` decision and configuration root before resolving child resources. Missing or failed trust checks mean denial; children never prompt for or grant project trust. The child passes its derived trust explicitly to `SettingsManager`, governing project settings merging, resource/package discovery, and project MCP configuration. Denied project settings and MCP configuration are not merged. Before the SDK reads native global `settings.json` or `mcp.json`, the child checks their lexical and canonical sources against every retained denied root. A global file or agent-directory alias into a denied root fails child admission, even with extensions disabled; genuine global files and aliases into an approved parent remain usable.
334
+
335
+ Trust belongs to the **configuration root**, not the tool execution directory. Execution in a worktree with `configCwd` pinned to the trusted parent retains that parent's resources (including MCP configuration), also for nested children. A different configuration root, including a newly copied worktree, is not implicitly trusted. Resource checks retain **all denied ancestor roots**, including the captured parent root whose trust was denied and the child's untrusted `configCwd`. Lexical and canonical path checks apply to explicit, configured, and discovered extension paths before their factories execute, and to discovered/preloaded skills before instruction injection. Changing `cwd`/`configCwd`, naming an explicit parent extension, or using a symlink/package alias cannot re-admit the denied source. Named skill preloading also checks global-root ancestor aliases against all retained denied roots.
336
+
337
+ Genuine user/global resources outside denied roots remain available subject to the child's normal policy. This is a resource-source and instruction-loading boundary, **not an OS sandbox**: arbitrary allowed global extension code can perform its own I/O or register MCP servers; project trust does not contain that code's behavior. `isolated`, `extensions: false`, named extension selection, and skill restrictions still narrow access independently. Project context files and appended prompts remain suppressed in child resource loading; append-mode agents receive only the parent's already-built system prompt.
338
+
281
339
  ### Tool & extension scoping
282
340
 
283
341
  `extensions:` decides **which extensions load**, `tools:` decides **which tools surface to the LLM**. They compose:
284
342
 
285
343
  ```yaml
286
- # Default (both omitted): all extensions load, all 7 built-ins surface
344
+ # Default (both omitted): child-safe extensions load, all 7 built-ins surface
287
345
 
288
346
  tools: read, grep, find # narrow to listed built-ins; extensions still load
289
347
  tools: "*" # all 7 built-ins (alias: `all`)
@@ -305,16 +363,18 @@ isolated: true # hermetic: built-ins only, no extensions/skil
305
363
 
306
364
  A few rules the examples don't make obvious:
307
365
 
308
- - `extensions:` is the sole loading authority. `ext:foo` in `tools:` narrows what surfaces; it can't load `foo` on its own. Mismatches fire `extension-error:…` warnings.
366
+ - `extensions:` is the loading authority, subject to child lifecycle safety. `pi-background` is excluded **before its factory loads**, including under default inheritance and `"*"`; an `extension-error:…` warning names it. Its jobs can wake a retained child after `session.prompt()` finishes, when the manager has marked the record terminal, released its concurrency slot, and removed its worktree. Until manager-owned asynchronous continuation exists, explicitly selecting `pi-background` (by name, alias, package path, or entry path) is refused by name rather than silently dropped. `ext:pi-background` cannot override this boundary and warns as an unloaded selector. Ordinary extensions remain available; parent sessions are unaffected. This does not add or change another extension's settings.
367
+ - `ext:foo` in `tools:` narrows what surfaces; it can't load `foo` on its own. Mismatches fire `extension-error:…` warnings.
309
368
  - Any `ext:` entry flips extension tools to an explicit allowlist — unnamed extensions still load (handlers fire) but expose no tools. So `tools: "*, ext:mcp/search"` exposes only `search` from `mcp`, nothing from any other extension.
310
369
  - Extension names match case-insensitively (`[Mcp]` = `[mcp]`); tool names in `ext:foo/bar` stay case-sensitive.
311
370
  - Extensions that register tools **lazily** work too. MCP-backed extensions typically can't enumerate their tools until their servers connect, so they register from `session_start` or `before_agent_start` rather than at load. Subagent scoping is re-derived as tools appear, including under `ext:` selectors. Scoping does not override Pi's own exposure: `deferred`/`codemode` tools and `defaultActive: false` tools are not activated for the model, and tools deliberately deactivated during a session stay inactive on later turns. Pi may still allow nested `ctx.executeTool()` calls to deferred/codemode tools, but the child's selector and `ask_tools:` approval gate both model-issued and nested calls.
312
371
  - A configured `defaultToolTimeoutMs` bounds the public `tool_call` policy and each selected extension handler using one deadline per call, including nested calls and handlers registered later with `pi.on`. On expiry the call is blocked; a Promise race returns control but **cannot cancel side effects** of a misbehaving hook that keeps running. Interactive `ask_tools:` confirmation is human decision time, not tool execution time: the deadline starts after approval. The separate execution timeout can still abort a tool whose `execute()` hangs.
313
- - On Pi 0.99+, the SDK's public codemode, tool-search, and MCP factories are included as built-in extensions in children. `extensions: false` omits them; `extensions: [mcp]` selects MCP; `exclude_extensions: mcp` removes it; and `tools: ext:codemode`, `ext:tool-search`, or `ext:mcp/<tool>` narrows their tools like any other extension. Earlier supported Pi hosts use their own available extensions, without importing these newer factories. For restrictive `extensions:` or `exclude_extensions:` policies, the child uses Pi's public package resolver to select discovered paths **before** loading factories. Thus an excluded extension cannot register a conflicting `/mcp` command and displace a selected replaceable built-in before filtering. Resolution can perform configured package installs, as Pi's normal loader does, and a selected extension that fails to load is reported rather than silently dropped.
372
+ - When the SDK exports public codemode, tool-search, and MCP factories (as on the verified Pi 1.0.4 baseline), they are included as built-in extensions in children. `extensions: false` omits them; `extensions: [mcp]` selects MCP; `exclude_extensions: mcp` removes it; and `tools: ext:codemode`, `ext:tool-search`, or `ext:mcp/<tool>` narrows their tools like any other extension. Capability checks omit factories that the host does not export; this fallback is not an older-host support guarantee. Whenever extensions are enabled, the child uses Pi's public package resolver to select discovered paths **before** loading factories. Thus an excluded extension cannot register a conflicting `/mcp` command and displace a selected replaceable built-in before filtering. Resolution can perform configured package installs, as Pi's normal loader does, and a selected extension that fails to load is reported rather than silently dropped.
373
+ - Explicit sources are validated separately before canonical deduplication, so a real file plus its file alias, or two package-directory aliases, load once without falsely reporting a missing source. Ownership names are unioned for admission/exclusion, but whole-package priority uses each resource's declaring package; direct entries use the canonical declaring owner. A package that aliases another package's entry does not lose its unrelated entries when the target is explicitly selected. Directory-backed modules are matched to Node's supported module entry without evaluating factories; undeclared siblings are not attributed to that package.
314
374
  - An installed **package** extension matches by its package short name (`@scope/pi-subagents` → `[pi-subagents]`), in addition to its path-derived name (a package whose entry is `src/index.ts` also answers to `[src]`). Prefer the package name — the path-derived one is incidental.
315
375
  - Plain `tools:` typos fail loudly: `tools: reed, grep` fires `tools-error:…` instead of silently producing an under-tooled agent.
316
376
  - `exclude_extensions:` wins over `extensions:` and over `ext:` selectors — an excluded extension never loads and a `tools: ext:` entry can't pull it back. Plain names only (no paths, no `*`); a name matching nothing fires an `extension-error:…` warning.
317
- - `exclude_extensions:` is **not a sandbox**: on Pi 0.99+ the restrictive prefilter prevents excluded discovered factories from loading into the child, but resolving configured packages can still install code. On earlier hosts, the post-load filter can run an excluded factory before suppressing its tools and bound lifecycle hooks; a factory that subscribes directly to the shared `pi.events` bus may remain live. Don't rely on extension selection to contain untrusted packages.
377
+ - `exclude_extensions:` is **not a sandbox**: preselection prevents excluded discovered factories from loading into the child, but resolving configured packages can still install code. Allowed extensions run with the Pi process's permissions. Don't rely on extension selection to contain untrusted packages.
318
378
  - Array and string forms are equivalent: `[a, b]` == `"a, b"`.
319
379
 
320
380
  ## Tools
@@ -331,6 +391,7 @@ Launch a sub-agent.
331
391
  | `tier` | string | no | Model tier for this spawn, by name. Overrides the agent's own default tier. Unknown tiers are rejected, not substituted — see [Model tiers](#model-tiers) |
332
392
  | `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
333
393
  | `run_in_background` | boolean | no | Run without blocking |
394
+ | `supervisor_questions` | `"human"` / `"parent"` / `"off"` | no | Default human; parent routes only ordinary clarification from fresh non-isolated background calls. No schedules or mode-changing resumes |
334
395
  | `resume` | string | no | Agent ID to resume a previous session |
335
396
  | `isolated` | boolean | no | No extension/MCP tools |
336
397
  | `isolation` | `"worktree"` | no | Run in an isolated git worktree |
@@ -357,6 +418,10 @@ Send a chat message to a running or queued agent. A running agent receives it af
357
418
  | `agent_id` | string | yes | Agent ID to steer |
358
419
  | `message` | string | yes | Message to inject into agent conversation |
359
420
 
421
+ ### `answer_subagent_question` (main only)
422
+
423
+ Answer a current opted-in background child's ordinary clarification: `questionId` is the bounded one-use ID in extension context; `answer` is at most 2,000 characters, an offered `option:N` token for choices, or empty to decline. Forged, expired, cancelled, or stale IDs and unoffered choices are rejected. The tool is never inherited or injected into children and cannot grant permission or override a human rejection.
424
+
360
425
  ## Commands
361
426
 
362
427
  | Command | Description |
@@ -804,7 +869,7 @@ Workflow-owned orchestration uses the `subagents:rpc:spawn-managed` channel. Its
804
869
  }
805
870
  ```
806
871
 
807
- The manager validates and resolves the tier, agent configuration, queue, tool, session, and worktree policy against its own Agent-tier catalogue and model scope. The resolved tier and its snapshot are retained on the managed invocation/tombstone. `spawnKey` is idempotent within a root manager; the same normalized request returns the existing agent id and a conflicting request is rejected. A named managed `thread` re-enters one sequential session only while its effective model, thinking, toolset, denylist, isolation, and agent policy fingerprint remain unchanged — including the model and thinking its tier currently resolves to, so switching the session model interrupts a thread whose tier inherits it; a policy change or concurrent call is rejected rather than silently reusing the old session. Managed agents use the normal Agent execution path, queue, FleetView, activity, transcript, compaction, and lifecycle events. Only the automatic main-session completion nudge is suppressed for an owner-scoped record. Managed requests must carry an attempt-scoped owner, and `stop-owned`/`quiesce-owned` fail closed when exact node/generation metadata is missing. During branch replacement, timed-out records are detached and late callbacks are suppressed. Pre-tree bus notifications carry the host navigation attempt's signal so workflow consumers can distinguish retries after a veto; older hosts without the signal remain supported. Active or unsettled child work vetoes a tree-navigation attempt rather than being aborted by that attempt. Pi has no post-veto, pre-leaf commit hook, so work started after preflight during asynchronous summarization can be quarantined at commit but cannot be guaranteed to finish.
872
+ The manager validates and resolves the tier, agent configuration, queue, tool, session, and worktree policy against its own Agent-tier catalogue and model scope. The resolved tier and its snapshot are retained on the managed invocation/tombstone. `spawnKey` is idempotent within a root manager; the same normalized request returns the existing agent id and a conflicting request is rejected. A named managed `thread` re-enters one sequential session only while its effective model, thinking, toolset, denylist, isolation, and agent policy fingerprint remain unchanged — including the model and thinking its tier currently resolves to, so switching the session model interrupts a thread whose tier inherits it; a policy change or concurrent call is rejected rather than silently reusing the old session. Managed agents use the normal Agent execution path, queue, FleetView, activity, transcript, compaction, and lifecycle events. Only the automatic main-session completion nudge is suppressed for an owner-scoped record. Managed requests must carry an attempt-scoped owner, and `stop-owned`/`quiesce-owned` fail closed when exact node/generation metadata is missing. During branch replacement, timed-out records are detached and late callbacks are suppressed. Pre-tree bus notifications carry the host navigation attempt's signal so workflow consumers can distinguish retries after a veto; the event payload retains a fallback for an absent signal, without establishing support for older hosts. Active or unsettled child work vetoes a tree-navigation attempt rather than being aborted by that attempt. Pi has no post-veto, pre-leaf commit hook, so work started after preflight during asynchronous summarization can be quarantined at commit but cannot be guaranteed to finish.
808
873
 
809
874
  ### Spawn
810
875
 
@@ -900,7 +965,7 @@ skills: api-conventions, error-handling
900
965
  ---
901
966
  ```
902
967
 
903
- **Discovery roots** (checked in this order, first match wins):
968
+ **Discovery roots** (checked in this order, first match wins). Both project roots are skipped when the child's configuration project is untrusted or trust is unknown; global named skills still preload unless their lexical or canonical source belongs to either the captured denied parent root or the untrusted child configuration root. Skill names are not explicit paths and cannot bypass that decision:
904
969
 
905
970
  | Scope | Path | Source |
906
971
  |---|---|---|
@@ -954,11 +1019,14 @@ src/
954
1019
  prompts.ts # Config-driven system prompt builder
955
1020
  context.ts # Pure parent-context renderer (not used for child dispatch)
956
1021
  context-boundary.ts # Raw inheritance and persisted-session guards
1022
+ parent-questions.ts # Opt-in clarification broker, authenticated bounded outbox, public preparation/ACK gates
1023
+ question-origin.ts # Public manager identity and complete native branch-prefix ownership
957
1024
  model-runtime-bridge.ts # Checked parent runtime compatibility bridge
958
1025
  env.ts # Environment detection (git, platform)
959
1026
  ui/
960
1027
  agent-display.ts # Shared agent formatting: spinner frames, activity, stats, names
961
1028
  conversation-viewer.ts # Live conversation overlay for viewing agent sessions
1029
+ owner-group.ts # Bounded retained workflow-child inspector (no engine coupling)
962
1030
  select-item.ts # Numbered identity-safe selectors for duplicate labels
963
1031
  ```
964
1032
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-subagents",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Signalridge's managed subagent runtime with workflow-owned orchestration RPC.",
5
5
  "author": "tintinweb and signalridge contributors",
6
6
  "license": "MIT",
@@ -25,15 +25,15 @@
25
25
  "autonomous"
26
26
  ],
27
27
  "peerDependencies": {
28
- "@earendil-works/pi-ai": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1",
29
- "@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1",
30
- "@earendil-works/pi-tui": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1"
28
+ "@earendil-works/pi-ai": "*",
29
+ "@earendil-works/pi-coding-agent": "*",
30
+ "@earendil-works/pi-tui": "*",
31
+ "@sinclair/typebox": "*"
31
32
  },
32
33
  "dependencies": {
33
- "@sinclair/typebox": "^0.34.50",
34
34
  "croner": "^10.0.1",
35
- "@signalridge/pi-subagents-protocol": "^1.5.0",
36
- "@signalridge/pi-ui": "^1.3.2",
35
+ "@signalridge/pi-subagents-protocol": "^1.5.1",
36
+ "@signalridge/pi-ui": "^1.4.0",
37
37
  "minimatch": "^10.2.6",
38
38
  "nanoid": "^5.1.16"
39
39
  },
@@ -53,7 +53,7 @@
53
53
  "@biomejs/biome": "^2.4.14",
54
54
  "@types/node": "^25.5.0",
55
55
  "typescript": "^6.0.0",
56
- "vitest": "^4.0.18"
56
+ "vitest": "^4.1.11"
57
57
  },
58
58
  "pi": {
59
59
  "extensions": [
@@ -18,8 +18,8 @@
18
18
  * interpolated from the file, so nothing else can ride along inside it.
19
19
  */
20
20
 
21
+ import { sanitizeDisplayText } from "@signalridge/pi-ui";
21
22
  import { getConfig } from "./agent-types.js";
22
- import { sanitizeDisplayText } from "./ui/safe-text.js";
23
23
 
24
24
  const NAMED_AGENT_COLORS: Readonly<Record<string, string>> = {
25
25
  // Claude Code's eight subagent colors, as its default theme renders them.
@@ -43,6 +43,7 @@ import {
43
43
  assertParentModelRuntimeAvailable,
44
44
  parentModelSessionOptions,
45
45
  } from "./model-runtime-bridge.js";
46
+ import type { ParentQuestionHandler } from "./parent-questions.js";
46
47
  import { shutdownAndDisposeSession } from "./session-lifecycle.js";
47
48
  import type { TierThinking } from "./settings.js";
48
49
  import type {
@@ -314,6 +315,7 @@ interface SpawnArgs {
314
315
  prompt: string;
315
316
  options: SpawnOptions;
316
317
  internalOverride?: InternalAgentConfigOverride;
318
+ parentQuestion?: ParentQuestionHandler;
317
319
  }
318
320
 
319
321
  export type ManagedSpawnRequest = ProtocolManagedSpawnRequest;
@@ -912,6 +914,8 @@ export interface SpawnOptions {
912
914
  /** Named sequential-thread hint for workflow orchestration. */
913
915
  thread?: string;
914
916
  isBackground?: boolean;
917
+ /** Explicit clarification routing. Parent mode requires a fresh root background spawn. */
918
+ supervisorQuestions?: boolean | "parent";
915
919
  /**
916
920
  * Skip the maxConcurrent queue check for this spawn — start immediately even
917
921
  * if the configured concurrency limit would otherwise queue it. Used by the
@@ -1074,6 +1078,13 @@ export class AgentManager {
1074
1078
  private rememberAgents = true;
1075
1079
  /** Whether spawned agents get `contact_supervisor` (settings). */
1076
1080
  private supervisorQuestions = true;
1081
+ private parentQuestionFactory?: (record: AgentRecord, ctx: ExtensionContext) => ParentQuestionHandler;
1082
+ private parentQuestionAgents = new WeakSet<AgentRecord>();
1083
+
1084
+ /** Package-local root capability; no managed RPC schema or global settings change. */
1085
+ setParentQuestionFactory(factory: (record: AgentRecord, ctx: ExtensionContext) => ParentQuestionHandler): void {
1086
+ this.parentQuestionFactory = factory;
1087
+ }
1077
1088
  /**
1078
1089
  * Descendants spawned so far under each top-level agent, keyed by that
1079
1090
  * agent's id. Cumulative for the branch's lifetime — see the budget check in
@@ -1663,6 +1674,14 @@ export class AgentManager {
1663
1674
  // Legacy in-process callers omitted description before it became required.
1664
1675
  // Normalize that shape to an inert string while rejecting reference values.
1665
1676
  options = { ...options, description: options.description ?? type };
1677
+ if (options.supervisorQuestions !== undefined && typeof options.supervisorQuestions !== "boolean" && options.supervisorQuestions !== "parent") {
1678
+ throw new Error('supervisorQuestions must be boolean or "parent".');
1679
+ }
1680
+ if (options.supervisorQuestions === "parent" &&
1681
+ (options.isBackground !== true || options.parentAgentId !== undefined || options.resumeSessionFile ||
1682
+ options.isolated || getAgentConfig(type)?.isolated || !this.supervisorQuestions || !this.parentQuestionFactory)) {
1683
+ throw new Error('Parent clarification requires a fresh, non-isolated top-level background Agent with supervisor questions enabled. Set run_in_background: true; foreground calls cannot answer while blocked.');
1684
+ }
1666
1685
  // Covers top-level, nested, managed, and queued spawns before allocating a
1667
1686
  // record, consuming a branch budget, or creating an isolation worktree.
1668
1687
  assertNoRawInheritance(
@@ -1811,7 +1830,9 @@ export class AgentManager {
1811
1830
  prompt,
1812
1831
  options,
1813
1832
  internalOverride,
1833
+ ...(options.supervisorQuestions === "parent" ? { parentQuestion: this.parentQuestionFactory!(record, ctx) } : {}),
1814
1834
  };
1835
+ if (options.supervisorQuestions === "parent") this.parentQuestionAgents.add(record);
1815
1836
  // Lifecycle observers may synchronously stop or dispose a freshly allocated
1816
1837
  // record. Do not let the normal queue/start path resurrect that decision.
1817
1838
  if (
@@ -2215,7 +2236,7 @@ export class AgentManager {
2215
2236
  private startAgent(
2216
2237
  id: string,
2217
2238
  record: AgentRecord,
2218
- { pi, ctx, type, prompt, options, internalOverride }: SpawnArgs,
2239
+ { pi, ctx, type, prompt, options, internalOverride, parentQuestion }: SpawnArgs,
2219
2240
  ) {
2220
2241
  if (this.disposed || record.detached || this.agents.get(id) !== record)
2221
2242
  return;
@@ -2330,7 +2351,8 @@ export class AgentManager {
2330
2351
  // Top-level conversations persist by default so `@handle` has something
2331
2352
  // to reopen after the record is evicted; frontmatter still overrides.
2332
2353
  rememberAgents: this.rememberAgents,
2333
- supervisorQuestions: this.supervisorQuestions,
2354
+ supervisorQuestions: options.supervisorQuestions ?? this.supervisorQuestions,
2355
+ parentQuestion,
2334
2356
  resumeSessionFile: options.resumeSessionFile,
2335
2357
  toolset: options.toolset,
2336
2358
  excludeTools: options.excludeTools,
@@ -2948,6 +2970,9 @@ export class AgentManager {
2948
2970
  )
2949
2971
  return undefined;
2950
2972
  if (!record.session) return undefined;
2973
+ if (this.parentQuestionAgents.has(record) && options?.isBackground !== true) {
2974
+ throw new Error('This child uses parent clarification; resume it with run_in_background: true to avoid blocking the answering model.');
2975
+ }
2951
2976
  assertSafeChildSession(
2952
2977
  record.session,
2953
2978
  record.invocation?.inheritContext === true,