pi-squire 0.15.0 → 0.17.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 (61) hide show
  1. package/CHANGELOG.md +343 -1
  2. package/README.md +931 -118
  3. package/package.json +2 -1
  4. package/src/agents.ts +73 -43
  5. package/src/child-tasks.ts +221 -0
  6. package/src/child.ts +35 -41
  7. package/src/completion-holds.ts +104 -0
  8. package/src/dashboard.ts +144 -0
  9. package/src/git.ts +6 -1
  10. package/src/identity.ts +152 -0
  11. package/src/index.ts +342 -56
  12. package/src/local-state.ts +21 -0
  13. package/src/navigator-view.ts +186 -0
  14. package/src/navigator.ts +152 -107
  15. package/src/poll.ts +1 -0
  16. package/src/render.ts +4 -1
  17. package/src/result-close.ts +37 -0
  18. package/src/result-restore.ts +95 -0
  19. package/src/result-types.ts +29 -1
  20. package/src/results-presentation.ts +47 -0
  21. package/src/results.ts +174 -108
  22. package/src/resume-checks.ts +14 -2
  23. package/src/resume-picker.ts +9 -2
  24. package/src/resume.ts +62 -13
  25. package/src/session-owner.ts +279 -0
  26. package/src/task-replies.ts +247 -0
  27. package/src/task-settlement.ts +81 -0
  28. package/src/task-store.ts +26 -0
  29. package/src/tmux.ts +134 -54
  30. package/src/tool-bundles.ts +84 -0
  31. package/src/tool-names.ts +29 -0
  32. package/src/tracking.ts +72 -65
  33. package/src/tree-close.ts +236 -0
  34. package/src/tree-fence.ts +51 -0
  35. package/src/tree-lock.ts +77 -0
  36. package/src/tree-notice.ts +124 -0
  37. package/src/tree-registry.ts +365 -0
  38. package/src/tree-settings.ts +31 -0
  39. package/src/tree-types.ts +32 -0
  40. package/src/tree-view.ts +246 -0
  41. package/src/trust-control.ts +214 -0
  42. package/src/trust-prompt.ts +163 -0
  43. package/src/trust-wire.ts +203 -0
  44. package/src/widget-activity-join.ts +24 -0
  45. package/src/widget-activity-runtime.ts +104 -0
  46. package/src/widget-activity.ts +104 -0
  47. package/src/widget-ancestors.ts +142 -0
  48. package/src/widget-current.ts +41 -0
  49. package/src/widget-data.ts +147 -0
  50. package/src/widget-focus.ts +133 -0
  51. package/src/widget-live-process.ts +31 -0
  52. package/src/widget-live.ts +356 -0
  53. package/src/widget-render.ts +181 -0
  54. package/src/widget-selection.ts +58 -0
  55. package/src/widget-settings.ts +29 -0
  56. package/src/widget-summary.ts +51 -0
  57. package/src/widget-title-file.ts +127 -0
  58. package/src/widget-title.ts +118 -0
  59. package/src/widget.ts +272 -0
  60. package/src/window-placement.ts +30 -5
  61. package/src/workspace.ts +38 -90
package/README.md CHANGED
@@ -21,13 +21,15 @@ You need:
21
21
  relative to the pane's session (see [Where delegate windows
22
22
  open](#where-delegate-windows-open)), so a client attached elsewhere does
23
23
  not matter.
24
- - A provider of `message:*`;
25
- [pi-session-mail](https://github.com/gvanderclay/pi-session-mail) is one
26
- example. Without a provider there is no way to send the task or get an
24
+ - A provider of the four `message:*` hooks, including `message:reply`;
25
+ [pi-session-mail](https://github.com/gvanderclay/pi-session-mail) **0.6.0 or
26
+ later** supplies them. Without a provider there is no way to send the task or get an
27
27
  answer, so `/delegate` refuses rather than start a delegate that cannot
28
28
  report back.
29
29
 
30
- With pi-session-mail as the provider, for example, install both from npm:
30
+ Upgrade pi-session-mail and pi-squire together, then restart or reload both
31
+ parent and delegate sessions. Older mail providers are unsupported; there is
32
+ no version negotiation or legacy automatic-answer path. Install both from npm:
31
33
 
32
34
  ```bash
33
35
  pi install npm:pi-session-mail
@@ -140,28 +142,270 @@ temporary folder, not as text on the command line.
140
142
  resumes with another storage directory selected. A parent whose session
141
143
  directory is not an absolute path refuses to launch, before sending the
142
144
  task.
143
- - Inside a delegate (`PI_DELEGATE_PARENT` set) the extension registers only
144
- auto-exit's side, the `/auto-exit` command and its handlers, so a delegate
145
- cannot start delegates of its own.
145
+ - Delegates restore their identity and task state at session start. Delegation
146
+ tools are inactive unless the current agent opts in below `maxDepth`; launch
147
+ and resume admission enforce that boundary even through commands (see
148
+ [Nested delegation](#nested-delegation-unreleased)).
146
149
  - Without a UI the command and the `delegate` tool still work, except a
147
- `--cwd` launch whose project trust needs a question (see
150
+ top-level launch whose project trust needs a question (a nested launch can
151
+ ask its attended root; see
148
152
  [Project trust](#project-trust)). They need tmux
149
153
  and a provider of `message:*`, and refuse with a message when either is
150
154
  missing. In print mode (`pi -p`) Pi does not show the refusal notices, so
151
155
  the refusal is silent there.
152
156
 
157
+ ## Nested delegation (unreleased)
158
+
159
+ This integration is not ready for production nested agents: descendant tree
160
+ controls and full milestone validation remain required slices. Use only isolated
161
+ development fixtures until the full nesting
162
+ milestone is integrated and tested.
163
+
164
+ The default `maxDepth` in `<agent dir>/squire/settings.json` is **1**: root
165
+ (depth 0) → delegate (depth 1), with no grandchildren. Set `maxDepth: 2` for
166
+ root → coordinator → worker. A readable value must be a positive safe integer;
167
+ an absent/unreadable settings file uses the safe depth-1 default for exposure.
168
+ Launch/resume refuses malformed, unreadable or non-object settings rather than
169
+ risk silently losing configured tree caps.
170
+
171
+ A coordinator's agent file opts in with
172
+ `allow_nested_delegation: [worker, reviewer]`, or `true` for any current roster
173
+ agent. Absent, false or an empty list grants no nesting. A list names exact
174
+ agent identities; duplicates are removed. Other values (including null, empty
175
+ scalars, quoted booleans, malformed lists and patterns) disable the agent with
176
+ a path-specific warning. Unknown or unavailable names warn and cannot launch;
177
+ known entries remain usable, and later roster changes apply normally.
178
+ Below the limit the grant adds `delegate`,
179
+ `delegation_status`, `delegation_close` and `delegation_resume` even with an
180
+ explicit `tools` list. Merely listing those
181
+ tools (including by pattern) does not opt in and produces
182
+ a roster warning. At the depth limit they are inactive and not passed onward.
183
+
184
+ Every launch/resume re-reads the direct parent's current grant before side
185
+ effects. A refusal names the permitted agents or depth/setting. Removing an
186
+ agent from the grant blocks future launches/resumes, not existing work.
187
+ Trusted project agents still shadow global definitions. Each launching session
188
+ keeps the existing throttled quota checks and shared reactive usage-limit marks.
189
+
190
+ A nested child's tools are worked out as at top level, from its own agent file:
191
+ its `tools` list, or, with no `tools` field, its direct parent's available
192
+ tools (active ones plus codemode/deferred operations), plus `delegateTools` and
193
+ the automatic nesting grant. They are not cut back to the parent's tools. The
194
+ `allow_nested_delegation` list is the boundary: a coordinator that must not
195
+ start a child with more tools lists only children whose agent files give fewer.
196
+
197
+ Each agent keeps its own permission policy; squire does not inherit or compare
198
+ effective policies. A granting agent with a `permission:` map (even `{}`) warns
199
+ when a listed child lacks one; `true` checks all applicable roster agents.
200
+ This is only a heuristic warning, not authorization or proof of lesser privilege.
201
+ Permission policy parsing/enforcement remains the permission provider’s job.
202
+ Tool-name restrictions and configuration permission rules are not filesystem
203
+ isolation: tools such as bash can write files or start processes. A tool list
204
+ bounds tool names, not arbitrary extension code or the implementation an
205
+ extension registers under an allowed name. Rules asking
206
+ before changes to agent/squire configuration cannot promise interception of
207
+ every indirect write by scripts or repository changes; exact shell handling
208
+ belongs to the permission provider.
209
+
210
+ A coordinator's task finishes child-first: no final task reply or auto-exit
211
+ while any direct-child task is unfinished. Idle runs and open windows are not
212
+ task completion; a finished `auto_exit: false` child does not hold the barrier.
213
+ Child results steer only their direct parent. The coordinator must receive
214
+ their outcomes in a model request and subsequently settle before reporting
215
+ upstream. A result arriving during final settlement therefore requires another
216
+ processing run, not just decrementing a count. Out-of-order work and rolling
217
+ fan-out are supported; raw grandchild results are not independently forwarded
218
+ to root.
219
+
220
+ Task takeover and per-run guidance use the same current agent grant/depth check
221
+ as tool exposure. Leaves keep the original final-message wording; only effective
222
+ coordinators receive child-first final-report instructions. Manual reopening and
223
+ grant changes use the current role, not the launch-time role.
224
+
225
+ Intermediate failures, including exhausted fallbacks, keep the coordinator open
226
+ while workers run. Child results can wake it again; its eventual final run
227
+ determines done/failed. A user stop holds its task through failed runs as before.
228
+ A confirmed vanished worker produces the existing closed-without-result
229
+ outcome, which the coordinator processes without treating it as successful work.
230
+ Unknown tmux state is not disappearance and does not release the barrier.
231
+
232
+ Identity (direct parent, root, depth, own agent and ordered agent-name chain)
233
+ and child-task facts survive manual reopening of the same Pi session with the
234
+ same agent directory, without launch environment variables. Reopening does not
235
+ adopt another parent's children. Closing/reloading/exiting a parent does not
236
+ terminate its workers. Descendant views and exact-session restoration are
237
+ available below, including explicit ancestor-scoped subtree close. Full integrated
238
+ real-model/permission-forwarding validation is still required before production use.
239
+
240
+ A nested launch/resume needing an unresolved trust decision asks the attended
241
+ root directly, outside model task mail (see [Project trust](#project-trust)).
242
+ Already-decided trust, inherited-directory trust and explicit approve/no-approve flags remain.
243
+
244
+ ### Tree-wide launch budgets
245
+
246
+ All sessions beneath a root share these call-time settings in
247
+ `<agent dir>/squire/settings.json`:
248
+
249
+ | Setting | Meaning | Absent with maxDepth > 1 | Absent with maxDepth 1 |
250
+ | --- | --- | --- | --- |
251
+ | `maxConcurrentDelegates` | Unfinished delegations across the tree, including coordinators waiting for children | 16 | unlimited |
252
+ | `maxDelegatesPerRoot` | Launches over that root session's lifetime, including resumes | 64 | unlimited |
253
+
254
+ Both accept nonnegative safe integers; **0 means unlimited**. A changed setting
255
+ applies to subsequent admissions. Lowering it never kills existing work. At a
256
+ cap, launch/resume refuses with the current count and limit, before mail,
257
+ worktree creation or killing an exited resume window. There is no launch queue.
258
+ A finished task frees concurrency even if its `auto_exit: false` window stays
259
+ open. It never refunds its lifetime launch. A coordinator's intermediate settle
260
+ or a held user stop does not finish its task.
261
+
262
+ Shared structural facts and reservations live under
263
+ `<agent dir>/squire/trees/<encoded root session id>.json`. The registry indexes
264
+ ownership, ancestry, resume chains and launch locators; local child-task records
265
+ still own result delivery/processing. Publishing those saved parent facts is
266
+ best-effort: failure warns but does not suppress result/gone messages, steer,
267
+ close tracking or local outcome processing. Every admission reconciles durable
268
+ facts under the registry lock before checking caps, so missed publication does
269
+ not create spare capacity. True local child-task storage failures still block
270
+ completion and admission. Frozen worker replies publish terminal accounting
271
+ before transport, so a finished worker frees a slot even while its parent is
272
+ absent. A failed worker-terminal publication retains the outbox and blocks auto-exit;
273
+ retry/reopen preserves the existing answer. This accounting publication does
274
+ not claim that the parent received or processed the reply.
275
+
276
+ Admissions use a cross-process mutex on a **local filesystem**, not NFS, and
277
+ atomically reserve both budgets. Short lock contention can wait up to five
278
+ seconds, then refuses; cap exhaustion never waits for a slot. Lock contenders
279
+ have unique files; only a process proven absent permits cleanup, never a lease
280
+ expiry. A reused PID or unknown process state conservatively blocks cleanup.
281
+ Keep the same agent directory when reopening a session. Registry corruption
282
+ refuses admission instead of resetting the budget. Atomic replacement protects
283
+ ordinary process restarts; these records do not promise power-loss durability.
284
+
285
+ Confirmed pre-start failure refunds both reservations. After a tmux create may
286
+ have been dispatched, an error/timeout is **uncertain**, not a refund. A child
287
+ that starts publishes its own window locator to recover a lost tmux response.
288
+ Subsequent admissions reconcile durable parent/worker facts and observed windows,
289
+ even with the root/intermediate parent absent. Confirmed disappearance frees
290
+ concurrency but is not success; the direct parent still processes its own
291
+ missing-result notification or late reply. Unknown tmux state releases nothing.
292
+ If neither start nor no-start can be established, the reservation remains
293
+ charged; no timeout guesses it away or relaunches it. No parent-death watcher,
294
+ repair daemon, mailbox scraping or implicit ownership transfer is involved.
295
+
296
+ ## Restoring and inspecting a delegation tree
297
+
298
+ Reopen the **same Pi session id**, using the same agent directory, to restore its
299
+ identity, direct-child tracking, polling, pending results and owed answers.
300
+ Still-running windows are not recreated. Shared counts survive; reopening a
301
+ historically gone but unfinished coordinator counts it toward concurrency again,
302
+ without charging another lifetime launch. A Squire resume is different: it
303
+ starts another delegation and spends another lifetime launch.
304
+
305
+ Only one Squire runtime may act for an exact session identity. Process ownership
306
+ uses the same cross-process lock as admission, with a per-runtime token and no
307
+ time lease. A live/paused duplicate is refused and asked to shut down; it cannot
308
+ launch, process outcomes, send final replies or start a trust service. Already-
309
+ claimed matching Squire task/reply input is staged under
310
+ `<agent dir>/squire/owners/<encoded session id>/inbound/` before acknowledgment,
311
+ with an atomic per-envelope file, independent of the ownership lock. Only the
312
+ owner drains it, in provider timestamp/path order, through normal handling;
313
+ callbacks run outside the owner mutex and idle drain checks do not write lock
314
+ files. If staging is unwritable, an active ready owner can process directly.
315
+ A duplicate that cannot retain input reports the claimed path and asks for a
316
+ resend: delivery cannot be lossless without writable storage.
317
+
318
+ This safeguard covers Squire input, not ordinary mail/asks: a duplicate provider
319
+ can deliver those only in the losing process, not to the owner. Its shutdown can
320
+ also remove the shared running record, making the owner look not running to
321
+ `session_mail_ask` until its next provider announcement (such as `agent_start`).
322
+ Squire does not read or rewrite provider mailbox/running-record internals. Keep
323
+ one Pi process per session. Ownership refusals name the PID and owner file; if
324
+ stale/corrupt, verify the original owner has ended, stop affected Squire runtimes
325
+ and repair only that record. Never remove live lock tickets or force adoption.
326
+
327
+ Fresh sessions do not adopt earlier trees. At startup a fresh root inspects only
328
+ this agent directory's newest **64** registry files, and only entries using its
329
+ current session-storage directory. It reports at most **10** direct owners with
330
+ positively matched live windows and explains reopening the owning session (or
331
+ returning to its existing owner). Registry-content reads are limited to 1 MiB
332
+ per tree and one **200 ms total cooperative notice budget**, including the tmux
333
+ wait, with registry lock waits capped at the remaining budget or 20 ms. The scan
334
+ yields between trees and reports unavailable tmux, unreadable owner records or
335
+ truncated scope once. Individual filesystem operations cannot be preempted.
336
+ Directory names/mtimes are enumerated, but no recursive
337
+ session search, mailbox search or other agent-directory search occurs. Exact-id
338
+ restoration is not restricted by this notice budget.
339
+
340
+ Both `delegation_status` and `/delegates` show descendants: a root sees its tree;
341
+ a coordinator sees only its own subtree, not siblings or unrelated roots.
342
+ Hierarchy uses stable Pi session ids, not the new delegation id of a resume.
343
+ Parent/root/depth, launch/result/session paths, workspaces, current verified
344
+ window and resume history remain visible. Shared accounting is labeled separately
345
+ from semantic result state: a finished accounting entry is not proof its direct
346
+ parent received or processed the result. Registry-only rows without a complete
347
+ launch record remain unknown. Views read foreign parents' facts but never inject
348
+ grandchildren's raw results into the root conversation.
349
+
350
+ Only the **direct parent** may resume a child. Ancestor visibility grants no
351
+ resume authority; forked/cloned/copied history with a new Pi id grants none.
352
+ Return to the original parent. Identity is read from exact-id durable state,
353
+ written before launch; inherited launch environment cannot assign a new session
354
+ to the old tree. Current allowlists, depth, caps and
355
+ directory/source trust still apply to resume, including its picker. A live
356
+ same-id Squire owner and obsolete local/shared resume chains are refused.
357
+
153
358
  ## Where delegate windows open
154
359
 
155
- By default a delegate's window opens in a **companion session** named after
156
- yours with `-squire` added: from the session `work`, delegates go to
157
- `work-squire`. Your own session stays as it is, even when its name already
158
- ends in `-squire`: a session called `pi-squire` gets `pi-squire-squire`.
159
- pi-squire marks each companion session it creates with the tmux session
160
- option `@squire-companion`; a parent that runs in a marked session uses it, so
161
- a delegate that delegates does not make `work-squire-squire`.
360
+ By default a delegate's window opens in a shared **project companion session**
361
+ named `<project>-delegate-session` on your existing tmux server. The owning
362
+ root Pi session's project chooses it, not the parent tmux session or the
363
+ delegate's working directory. Launching into another repository or creating a
364
+ worktree there still uses the root's companion; resuming a delegate does too.
365
+
366
+ Nested launches inherit the original root project: A → coordinator in B →
367
+ worker in C all select A's companion. The preferred base name (for example,
368
+ `A-delegate-session`) is saved as `rootPlacement` in the existing durable
369
+ delegation identity and shared tree records, not recomputed from B/C or the
370
+ containing tmux session. Reopening the **same** coordinator session without its
371
+ launch environment restores that preference; it can launch or directly resume
372
+ its workers while the root is absent, provided target trust is already resolved.
373
+ Resume retains the original preference and still requires the direct parent.
374
+ The saved preference is not a numbered selected session or a tmux locator:
375
+ every open still applies the managed/unmarked selection rules below.
376
+
377
+ In Git, the project name is the **main checkout's folder name**. Roots in
378
+ repository subdirectories and linked worktrees therefore share the main
379
+ checkout's companion. Outside Git, use the root working directory's folder
380
+ name (a non-Git subdirectory has its own name). If Git inspection fails or
381
+ cannot locate the main checkout identity, the launch reports a notice and
382
+ uses the root working-directory basename instead.
383
+
384
+ Project names preserve ASCII letters (including case), digits, hyphens and
385
+ underscores. Runs of other characters become one hyphen, edge hyphens are
386
+ trimmed, and an empty name becomes `project`: `Client API` uses
387
+ `Client-API-delegate-session`.
388
+
389
+ Multiple roots share a companion even across parent tmux sessions. Distinct
390
+ paths with the same basename, separate clones named alike, and names that
391
+ normalize identically intentionally share it. This groups windows only:
392
+ delegation records, task mail, results, limits and close-all remain scoped to
393
+ each root.
394
+
395
+ pi-squire verifies the tmux session option `@squire-companion` before reusing
396
+ an occupied name. An unmarked session is left untouched; numbered alternatives
397
+ start at `<project>-delegate-session-2`, skipping unmarked alternatives too.
398
+ Later launches reuse an existing managed alternative even if a lower name
399
+ becomes free. Unexpected duplicate managed candidates use the first in numeric
400
+ order, without moving windows or repairing sessions. Existing legacy companions
401
+ and live windows are neither renamed nor migrated; only new launches and
402
+ resumes use project naming.
162
403
 
163
404
  - The session is created on demand, by the launch that needs it, and tmux
164
405
  removes it with its last window, so it does not outlive its delegates.
406
+ - Closing delegates (including close-all) kills only that root's tracked
407
+ windows. Manually killing the shared tmux companion session terminates
408
+ **all** its windows, including other roots' delegates.
165
409
  - `detach-on-destroy` is set to `off` (and the mark set) on that session only: if you are
166
410
  looking at it when its last delegate exits, tmux moves you to your most
167
411
  recent other session instead of detaching you. Your sessions and tmux's
@@ -180,24 +424,212 @@ To open windows in your own session, as before, set `windowPlacement` in
180
424
  ```
181
425
 
182
426
  The values are `companion` (the default when the setting is absent) and
183
- `current`. The file is read at each launch. Any other value refuses the launch
427
+ `current`. The mode is read at each launch/resume independently of the saved
428
+ project preference. Even a `current` launch saves the original root project for
429
+ later companion launches; `current` itself still opens in the invoking pane's
430
+ session. Missing or malformed `rootPlacement` in a recorded identity, or conflicting
431
+ saved/launch identities, refuses restore/admission/resume. Identity-less records
432
+ also cannot be resumed after this update, including by their original root
433
+ (see [Resuming a delegation](#resuming-a-delegation)). There is
434
+ no identity migration, directory relocation/rename tracking or companion repair.
435
+ The file is read at each launch. Any other value refuses the launch
184
436
  with an error naming the file, before the task is sent or a window opens.
185
437
 
438
+ <a id="direct-delegate-overview"></a>
439
+
440
+ ## Delegate overview
441
+
442
+ In interactive Pi, Squire shows a compact **Delegates** tree above the editor.
443
+ It lists this session's running direct delegates first, then finished delegates
444
+ whose windows are still open, newest launch first within each group. A resumed
445
+ delegate keeps one row keyed by its stable Pi session id, not by its new
446
+ delegation id or mutable title. Failed and stopped results keep their actual
447
+ outcome labels; an open window or quiet session is not evidence of success.
448
+
449
+ Rows show the recorded delegate identity in bold, the current native Pi session
450
+ title when available, and task state. No title is repeated when it matches the
451
+ identity. The recorded launch model (or result model when available) and
452
+ transcript-file age appear when space permits: model is
453
+ dropped first, then file age, before identity/state are compressed. At very
454
+ narrow widths the state glyph remains beside the truncated identity.
455
+ `file … ago` is only a last-active hint from transcript mtime, not live tool
456
+ activity. When available, a second line shows factual live activity (below).
457
+ Each coordinator row summarizes **all descendant tasks**, at every depth,
458
+ including tasks whose windows have exited. Counts are per delegation/launch id,
459
+ not per stable session: a failed task and its resumed successor both count, so
460
+ a retry cannot erase a known failure. Actual replies supply done/failed/stopped;
461
+ saved gone/closed-without-result tasks count as missing. Shared registry
462
+ finished/closed/closing flags are not successful replies or proof another
463
+ parent processed an outcome. Missing parent facts or tree coverage are labeled
464
+ unknown/partial/unavailable, never all done. Summary and activity share one
465
+ secondary line; when both do not fit, the task summary wins.
466
+
467
+ Delegates also see their ancestors vertically, root to direct parent, with the
468
+ direct parent marked `(parent)` (including `root (parent)`). A connected
469
+ **Delegates** branch contains only the current session's direct children.
470
+ There is no current-session row or empty child heading; leaves show ancestors
471
+ only. Ancestor rows and secondary lines do not consume child-entry slots.
472
+
473
+ Configure it in the existing `<agent dir>/squire/settings.json`, alongside
474
+ `windowPlacement`, worktree and nesting preferences:
475
+
476
+ ```json
477
+ {
478
+ "delegateWidgetPlacement": "above",
479
+ "delegateWidgetMaxEntries": 3
480
+ }
481
+ ```
482
+
483
+ - `delegateWidgetPlacement`: `above` (default) or `below` the editor.
484
+ - `delegateWidgetMaxEntries`: nonnegative safe integer, default **3**. Each
485
+ direct child consumes one slot. `+N more direct delegates` counts hidden
486
+ eligible direct children, not descendants. **0** hides child rows and restores
487
+ Squire's running-count footer while preserving ancestor rows/navigation;
488
+ zero children never produce an empty heading.
489
+ - These preferences apply equally to roots and coordinators. Interactive views
490
+ re-read them about once a second; invalid widget preferences warn once per
491
+ distinct error and use the defaults without changing task state.
492
+
493
+ Launch/result/close changes refresh promptly; an independent one-second observer
494
+ keeps finished-open rows, window eligibility and later native title changes
495
+ fresh even when the result poll has stopped. Only visible rows resolve titles and
496
+ file metadata and live activity; overflow counts still include every eligible child. Closed/gone-only
497
+ history without a live reattached session does not keep the observer listing
498
+ windows. A positively reopened gone coordinator may appear at its current window
499
+ with its missing-outcome history intact; reopening does not make its task running. A failed tmux listing is unknown,
500
+ not proof that a window disappeared: running tasks remain eligible, but
501
+ finished rows require a positively open matching window.
502
+
503
+ Titles come from the latest complete native `session_info` in the delegate's
504
+ known session file, including external /resume-picker renames and explicit
505
+ clears. A small owner-only disposable title mirror under
506
+ `<agent dir>/squire/session-titles/` covers sessions with no persisted title
507
+ (including before the first transcript flush). Naming tools are optional;
508
+ `/name`, `--name` and Pi's native setter work. Persisted metadata takes priority
509
+ over that mirror: an external picker rename may differ temporarily from the
510
+ child's own in-memory name. Files are read incrementally, with bounded work per
511
+ refresh; very large existing transcripts may take several refreshes to catch up.
512
+
513
+ ### Factual live activity
514
+
515
+ The secondary line follows actual Pi tool-execution events: `reading…`,
516
+ `editing…`, `writing…`, `command call…`, or `using <tool name>…`. Parallel
517
+ and nested calls are tracked by their actual call ids, not by tool name.
518
+ Active nested calls take precedence over their orchestrator. A tool start is
519
+ observed before permission interception: `command call…` means an outstanding
520
+ call, not proof that a subprocess started or permission was granted. Squire
521
+ does not inspect commands to guess intent, or display arguments, paths, tool
522
+ results, thinking or provider payloads.
523
+
524
+ Otherwise, while an assistant response streams, `response: …` shows a literal
525
+ single-line excerpt bounded to 512 UTF-8 bytes. It clears when that message
526
+ ends; a quick response may finish between refreshes and never appear. Calls
527
+ clear on matching end, run end/settlement, stop, reload and shutdown. Known
528
+ task results/close suppress activity even if a delegate's last snapshot remains.
529
+ A settled or silent coordinator can still own unfinished child tasks: activity
530
+ does not determine task completion, thinking or waiting for a human.
531
+
532
+ This best-effort feed uses bounded owner-only atomic snapshots under
533
+ `<agent dir>/squire/live-sessions/`, separate from title mirrors and durable
534
+ task/tree state. Each runtime owns a unique generation; dead/reused processes
535
+ are rejected and their own snapshots can be removed, while unknown or duplicate
536
+ live owners make activity unavailable. No inactivity timeout or stale-state UI
537
+ is introduced. The same private launch correlation distinguishes a resumed task
538
+ from its stable Pi session. Manual reopen without this correlation does not
539
+ supply task-row activity yet. Missing, corrupt or inaccessible telemetry never
540
+ blocks task launch, reply delivery or settlement, and causes no chat or model
541
+ calls. Publication requires positive Squire ownership at startup; later factual
542
+ end/settlement observation continues even if task-owner processing fails or its
543
+ record becomes unreadable, without re-enabling actions. Event publishing works
544
+ in non-TUI delegates and with the list hidden.
545
+ One child-entry slot may occupy two physical lines; there is no empty secondary
546
+ line or tool-use counter.
547
+
548
+ ### Navigation
549
+
550
+ At an **empty built-in prompt**, press **Down** to select the direct parent in a
551
+ delegate session, or the first child at root. Up/Down traverse ancestors and
552
+ eligible children in display order; only children scroll within the configured cap;
553
+ hidden-above/below counts refer to direct children. Selection follows stable
554
+ session identity through renames, reordering and resumes. Enter opens that
555
+ child's details in the **same dashboard** as /delegates, scoped to that child's
556
+ branch (including descendant and resume history, not its siblings). Press **b**
557
+ for the branch list to select and inspect its workers. Initial details Escape
558
+ returns directly to the compact list; details opened from the branch list return
559
+ to that branch list first. Branch-list Escape returns to the compact list, then
560
+ Escape again returns to the prompt; Up past the first compact row also leaves
561
+ the list. A short hint appears only while the list owns these keys.
562
+ Typing or any unrelated key leaves list navigation and passes that key through.
563
+ While a widget-opened dashboard is loading or re-listing between actions, keys
564
+ are absorbed only if the exact empty built-in prompt still owns focus: Escape
565
+ cannot abort a run and action letters cannot leak into that prompt. Once a
566
+ real dashboard, dialog, completion menu or unknown component owns input, its
567
+ keys pass through normally.
568
+ Cap zero never creates invisible selectable children; ancestors remain selectable
569
+ and /delegates still works.
570
+
571
+ Enter on an ancestor switches directly to its verified **current live tmux
572
+ window**, not to the dashboard or a historical launch window. Root and leaf
573
+ processes publish their own positive pane/window observation in the same
574
+ disposable generation snapshot used for activity. The reader checks the elected
575
+ exact-session owner, process incarnation, tmux server socket/PID/start identity,
576
+ exact pane and window id **and** name. Current window placement is read from tmux;
577
+ cwd, companion names and rootPlacement are never used to guess a root window.
578
+ Own-pane observations refresh about once a second even while quiet or cap zero.
579
+ A rename/move before the owner refreshes fails closed; a later positive update
580
+ makes the new locator available. This navigates to the window, not a specific
581
+ pane's keyboard focus.
582
+
583
+ An exact-id manual reopen can supply a new locator without launch environment,
584
+ while old task history stays unchanged. It still cannot supply launch-correlated
585
+ task activity. Missing/unreadable/ambiguous metadata, a different server, a dead
586
+ pane or no attached client reports an unavailable target. Navigation never
587
+ adopts, recreates, kills or implicitly resumes an ancestor. New/forked session ids
588
+ do not inherit ancestor authority from copied history.
589
+
590
+ Squire owns only its widget key and its existing status entries. It does not
591
+ replace your footer or editor, and cleans up observers/input listeners on
592
+ shutdown or reload. Automatic navigation deliberately passes through custom
593
+ editors, autocomplete, dialogs, permission questions and unrelated dashboards.
594
+ It relies on **Pi TUI internals**, isolated in a small compatibility adapter:
595
+ the concrete focus getter and exact built-in editor identity. Compatibility was
596
+ tested with the installed **Pi 1.4.0 bundled CLI**, including regular/fullscreen
597
+ mode changes. Unknown focus/editor/completion capabilities disable key capture
598
+ rather than guessing; /delegates remains available. This is not a generic
599
+ custom-editor integration or a supported Pi focus hook. JSON/RPC hosts retain
600
+ the running-count status rather than receiving a terminal widget.
601
+
186
602
  ## Reaching delegates
187
603
 
188
604
  Delegate windows sit out of your way, so reach them from the parent. `/delegates`
189
- (or `alt+shift+d`) opens a panel of the delegations whose window is open: running,
190
- or done with auto-exit off, newest first. Each row shows the delegate's name,
191
- its agent (the agent's `display_name` when it had one), its state and its
192
- window. The model has no tool for any of this.
605
+ (or `alt+shift+d`) opens the same dashboard used by widget Enter, at the overall
606
+ view of its descendant hierarchy, including
607
+ closed/history rows. Parents precede their children; resume history stays
608
+ visible. Each row shows stable launch identity, the available current native Pi
609
+ title, agent display label, semantic state/outcome and verified window availability.
610
+ Enter opens selected details with identities, paths, result envelope, workspace
611
+ and resume information. Historical launch windows are labeled separately from
612
+ current verified windows. Lists and details scroll within the available screen.
613
+ Widget entry uses the same screen for only its selected branch; `b` exposes that
614
+ branch's list without widening its scope. A title change never changes routing.
615
+ Metadata is refreshed when opening/reopening the screen after an action; the
616
+ compact overview remains the live activity/summary view.
617
+ Only a verified live window supports switch/peek; the model has no
618
+ tool that moves the terminal. Any ancestor can explicitly close a selected
619
+ subtree. The root also has a confirmed close-all action; shared companion names
620
+ never grant control over another root.
193
621
 
194
622
  | Key | Does |
195
623
  |---|---|
196
- | `↑` / `↓` | select a delegation; `esc` leaves the panel |
197
- | `enter` | switch your tmux client to the window |
624
+ | `↑` / `↓` | select a delegation in the list; scroll in details |
625
+ | `enter` | open selected details; from details, switch your tmux client to its verified window |
626
+ | `esc` | details → overall list → prompt; initial widget details → compact list; branch-list details → branch list → compact list |
627
+ | `b` | widget entry only: show the selected branch list, preserving its target and history |
198
628
  | `p` | peek: a snapshot of the window's pane (`capture-pane`) in an overlay; `r` refreshes, `enter` switches, `esc` returns to the list |
199
- | `x` | close the delegation as `delegation_close` does; asks first only while it is running |
200
- | `n` | new delegate: the same form `/delegate` opens with no arguments (see [First use](#first-use)) |
629
+ | `x` | explicitly close the selected subtree as `delegation_close` does; confirms running work and verified live navigation rows |
630
+ | `a` | root only: confirm closing **all root** descendant subtrees, including absent coordinators, even when viewing one branch |
631
+ | `r` | resume the selected eligible direct child, collecting a follow-up through the existing resume flow; descendants without authority are refused |
632
+ | `n` | new child of the invoking session, not of the selected remote row: the same form `/delegate` opens with no arguments (see [First use](#first-use)) |
201
633
 
202
634
  Switch acts on the most recently active tmux client attached to the parent's
203
635
  session. With none attached it refuses with a notice; peek and close still
@@ -210,6 +642,22 @@ often rebound); the notice after a switch says so.
210
642
  without the panel: with no flag or with `--switch`, it switches; `--peek` opens
211
643
  the peek overlay. Ids complete from the open delegations.
212
644
 
645
+ Dashboard switch/peek and `/delegate-open` share current-locator verification.
646
+ For the latest known launch of a stable session, a positively observed current
647
+ runtime supersedes its old launch window; a later lost/changed locator refuses
648
+ rather than falling back to an old still-open window. Exact Pi pane liveness
649
+ allows navigation even if a different active pane has exited, and peek captures
650
+ that verified Pi pane. Legacy tracked windows still require matching id and
651
+ name. Reattached process visibility does not rewrite historical gone/closed/
652
+ result facts, invent task activity, or grant resume authority. Current telemetry
653
+ is navigation evidence only: close still uses the existing shared control
654
+ runner's durable locators/fences and reports partial failures honestly. A live
655
+ reattached navigation window may remain open when it differs from the durable
656
+ control locator; confirmation warns about that distinction. Current-locator
657
+ supersession memory lasts only for this navigator runtime: after a restart,
658
+ before the first positive live observation, a legacy id+name-verified historical
659
+ window can still be offered.
660
+
213
661
  ## Working directory
214
662
 
215
663
  `--cwd <dir>` (the `delegate` tool's `cwd`) runs the delegate in an existing
@@ -310,10 +758,12 @@ is `/Users/me/.pi/agent/squire`, the matching relative override resolves to
310
758
  `/Users/me/.pi/agent/special-checkouts/feature-topic`. The Git branch remains
311
759
  `feature/topic`.
312
760
 
313
- The settings file is read only for managed creation. A missing file uses the
314
- built-in default; malformed or inaccessible settings, invalid templates and
315
- duplicate canonical repository keys refuse creation rather than silently
316
- falling back. They do not affect inherited or `cwd` launches. Placement does
761
+ Worktree placement keys are read only for managed creation. A missing file
762
+ uses the built-in default; invalid templates and duplicate canonical repository
763
+ keys refuse creation rather than silently falling back. Those creation-key
764
+ errors do not affect inherited or `cwd` launches, but malformed, inaccessible
765
+ or non-object shared settings refuse every launch/resume because tree caps
766
+ cannot be read safely. Placement does
317
767
  not choose a source checkout or base commit. Pi discovers `AGENTS.md`, skills
318
768
  and other project context from the created directory and its ancestors, so the
319
769
  chosen layout determines which instructions and skills apply. For a layout
@@ -372,9 +822,12 @@ command, and nothing acts on it.
372
822
 
373
823
  ### Project trust
374
824
 
375
- The delegate's trust flag for a `--cwd` directory is decided in the parent,
376
- before the task is sent or a window opens. A same-repository managed worktree
377
- uses the parent's current trust for that repository. The model has no trust input.
825
+ The requesting parent resolves the delegate's trust flag before task mail,
826
+ worktree creation or a window opens. Unresolved choices go to the root user,
827
+ not to an intervening model or an unattended delegate UI. A same-repository
828
+ managed worktree uses the requester's current trust for that repository.
829
+ The model has no trust input; every launch/resume still passes explicit
830
+ `--approve` or `--no-approve`.
378
831
 
379
832
  1. A directory in the parent's own repository, by Git's common directory, so
380
833
  including its linked worktrees, gets the parent's current trust, even a
@@ -392,8 +845,9 @@ uses the parent's current trust for that repository. The model has no trust inpu
392
845
  (`.pi/settings.json`, extensions, skills and the like, or an
393
846
  `.agents/skills` directory above it), the delegate starts with
394
847
  `--no-approve` and no question.
395
- 4. Otherwise the parent asks, naming the directory, the main repository when
396
- it is the key, and what each answer saves:
848
+ 4. Otherwise the root asks, naming the requesting agent chain, requester
849
+ session id, directory, the main repository when it is the key, and what each
850
+ answer saves:
397
851
  `Trust`, `Trust parent folder (<folder>)` (left out at the filesystem
398
852
  root), `Trust (this session only)`, `Do not trust` and
399
853
  `Do not trust (this session only)`. The two "this session only" answers
@@ -404,9 +858,52 @@ uses the parent's current trust for that repository. The model has no trust inpu
404
858
  the turn or tool call stops a launch that is waiting for or answering a
405
859
  question; nothing is saved, sent or opened.
406
860
 
407
- Without a UI a question cannot be asked, so the launch is refused: open Pi in
408
- that directory and answer its trust question first. A trust store that cannot
409
- be read or written also refuses the launch; nothing is granted by default.
861
+ An unresolved nested question requires the same root Pi session to be running
862
+ with dialog-capable UI (TUI or RPC), without `PI_UNATTENDED=1`. An absent or
863
+ unavailable root refuses immediately; a disconnected root fails its pending
864
+ launches, rather than waiting for the root to return. Already-resolved trust
865
+ and resource-free targets need no root connection. A root's own trust in a
866
+ different repository never supplies approval. For a top-level launch without UI,
867
+ open Pi in the trust key directory named by the refusal (the source/main checkout
868
+ for a managed worktree), answer its trust question, then delegate again.
869
+
870
+ Root questions, local and remote together, are serialized, even while the
871
+ root model is busy. Each request has a **10-minute deadline including queue
872
+ wait**. Escape, tool/turn cancellation, requester or root shutdown/reload/session
873
+ replacement, disconnection and expiry cancel that launch and dismiss its
874
+ question; canceled queued requests are skipped. An unrelated root model run's
875
+ abort does not cancel a remote request. Late decisions cannot save trust or
876
+ start the canceled work. A new launch after reopening makes a new request;
877
+ pending requests and decisions are never replayed.
878
+
879
+ Squire uses an authenticated **127.0.0.1 loopback connection**, not mail or
880
+ `session_mail_ask`, for this control. The root writes its random port and
881
+ per-runtime token to the owner-only
882
+ `<agent dir>/squire/trust/<SHA-256 of root id>.json` descriptor. Only squire
883
+ reads it; no provider files, APIs or tool grants are needed. Unreachable
884
+ endpoints fail at once (connection establishment is bounded to one second).
885
+ A fresh client-nonce/HMAC handshake verifies the root before sending any
886
+ question or secret, so a reused stale port cannot impersonate it. Root restart
887
+ changes the token, invalidating old frames.
888
+ The descriptor is transient and removed on ordinary shutdown; a crash can
889
+ leave a harmless stale descriptor that cannot approve anything. Like the rest
890
+ of squire, this is not an OS boundary against processes running as you.
891
+
892
+ Only the live **requester** saves a persistent choice in that agent directory's
893
+ Pi `trust.json`, after cancellation/deadline checks and before launch.
894
+ The root holds the queue until the requester has applied the answer or
895
+ disconnected, so queued requests re-read saved decisions. The two session-only
896
+ choices save nothing and apply only to that launch. `Do not trust` launches
897
+ untrusted; Escape cancels instead. A trust store that cannot be read or written
898
+ refuses the launch; nothing is granted by default.
899
+
900
+ Model questions still use `session_mail_ask` to the direct parent, parent by
901
+ parent. Permission prompts remain with the permission provider, with each
902
+ child's own agent policy: there is **no permission inheritance**. Its bridge
903
+ can label a requester with `PI_DELEGATE_CHAIN` (comma-separated roster names,
904
+ root downward including the current delegate); `session:launch.chain` is the
905
+ corresponding frozen string array. Routing uses separate parent/root session
906
+ ids, not chain names. Squire does not implement that permission bridge.
410
907
 
411
908
  ## Agents
412
909
 
@@ -432,18 +929,31 @@ optional, and the [first-use example](#first-use) is a complete agent file:
432
929
  | `display_name` | Shown instead of the name on the delegate card, in `/delegates` and in the window title (`<display_name>-<label>`). The agent is still called by its name. It may hold only ASCII letters, digits, `-` and `_`; any other value (a space, `#`, `.`, a non-ASCII letter, or an empty value) makes the file invalid, so the agent is disabled. |
433
930
  | `model` | One model: `provider/id` or an alias such as `sonnet`, passed to the child's `--model` unchanged, so Pi resolves it to one model. A `*` pattern makes the file invalid. Absent means the parent session's current model. |
434
931
  | `thinking` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` or `max`. Absent means the parent session's level; an unknown value is dropped with a warning and the parent's level applies. Pi clamps the level to the model. |
435
- | `tools` | The delegate's tools (see below): tool names for exactly those, or only `-name` entries for the parent session's current tools less those. Absent means the parent session's current tools; empty (`tools:` or `tools: []`) means no tools except the `delegateTools` setting. MCP tools follow the selection like any other, and a tool of the agent's own `extensions` reaches the delegate only when `tools` or `delegateTools` names it. |
436
- | `extensions` | Extension paths only this agent's delegate loads (see below). |
932
+ | `tools` | The delegate's tools (see below): tool names and `$bundle` entries for exactly those, or only `-name` entries for the parent session's current tools less those. Absent means the parent session's current tools; empty (`tools:` or `tools: []`) means no tools except the `delegateTools` setting. MCP tools follow the selection like any other. |
437
933
  | `fallback` | Models to try, in order, when `model` is usage-limited (see [Usage limits](#usage-limits)). Each entry is one model, like `model`; a `*` pattern, or a YAML list entry holding a comma, makes the file invalid. A comma-separated line (`fallback: a, b`) is two entries. |
438
- | `auto-exit` | `true` or `false`, default `true`; see [Auto-exit](#auto-exit). |
934
+ | `auto_exit` | `true` or `false`, default `true`; see [Auto-exit](#auto-exit). |
935
+ | `allow_nested_delegation` | Unreleased: `true` or a YAML list of exact child agent names; grants nested delegation below `maxDepth` (see [Nested delegation](#nested-delegation-unreleased)). Absent, false or an empty list grants none. Malformed values disable the agent; unavailable names warn. |
439
936
  | `prompt_mode` | `append` (the default) wraps the body in `<agent_instructions>` and `</agent_instructions>` lines; `replace` adds the body unwrapped. An empty body adds nothing either way. |
440
937
  | `enabled` | `false` leaves the agent out. A project file with `enabled: false` hides a global agent of the same name in that project. |
441
938
  | `locked` | `true`, or a list of `model` and `thinking`. A call to `delegate`, `/delegate` or a resume that passes a locked field does not get it: the agent's value is used (the parent's when the file sets none) and the result says which agent locked which field. Passing the agent's own value is the same as passing nothing: no notice, and its fallbacks still apply. A resume of a locked field runs on the file's value as it is now, not on what the delegate last ran on. `true` locks the fields the file sets. |
442
939
 
443
- Any other key, such as `max_turns`, `inherit_context`, `run_in_background` or
444
- a permission policy for another extension, is ignored without a warning and
445
- left for whatever else reads the file. `exclude-tools` is no longer read; a
446
- file that still has it gets a warning (use `tools` instead).
940
+ Any other key, such as `max_turns`, `inherit_context` or `run_in_background`,
941
+ is ignored without a warning and left for whatever else reads the file.
942
+ Squire observes only whether `permission:` is a map for the nested-grant warning
943
+ above; it leaves interpretation and enforcement to the permission provider.
944
+
945
+ **Breaking frontmatter change:** use `auto_exit` instead of `auto-exit`.
946
+ The old `auto-exit` and `extensions` fields are silently ignored, including
947
+ malformed values and missing extension paths. In particular, an old
948
+ `auto-exit: false` no longer keeps the delegate open: without `auto_exit: false`
949
+ or a call override, auto-exit defaults to on. Agent-specific extension loading
950
+ has been removed from both launch and resume. Normal Pi extension discovery
951
+ and [`session:launch`](#sessionlaunch) customization remain available.
952
+ The `--auto-exit` / `--no-auto-exit` flags, `/auto-exit` command and
953
+ `PI_DELEGATE_AUTO_EXIT` environment variable are unchanged.
954
+
955
+ `exclude-tools` is no longer read; a file that still has it gets a warning
956
+ (use `tools` instead).
447
957
 
448
958
  `tools` lists the delegate's tools as a comma-separated or YAML list
449
959
  (`tools: read, grep` or `tools: [read, grep]`). Each entry of a YAML list
@@ -460,38 +970,54 @@ loadable by `tool_search`, whatever its exposure. Pi filters MCP tools by
460
970
  such entry pi-squire adds the entry `mcp__`, which names no tool; an empty
461
971
  selection is `--no-tools`, which removes MCP tools too. To give a delegate an
462
972
  MCP server's tools, name them, for example `mcp__docs__*`. An extension that
463
- turns tools on at run time can still add only tools the list allows. Names
464
- are not checked against the parent's tools, since a tool from one of the
465
- agent's own `extensions` exists only in the child.
973
+ turns tools on at run time can still add only tools the list allows. Tool names
974
+ are not checked against the parent's tools, at top level or nested: normal Pi
975
+ extension discovery or `session:launch` customization can provide tools in the
976
+ child.
466
977
 
467
978
  - No `tools` field: the delegate gets the parent session's tools as they are
468
- when the launch (or resume) starts, the tools active there then, passed as
979
+ when the launch (or resume) starts: active tools for a top-level launch, or
980
+ available tools including codemode/deferred operations for a nested one, passed as
469
981
  `--tools`, plus the `delegateTools` entries. A parent limited to `read`
470
982
  gives such a delegate only `read`.
471
983
  - An empty `tools` field, `tools:` or `tools: []`: the delegate gets no tools
472
984
  except the `delegateTools` entries. With none of those either, it starts
473
985
  with `--no-tools`.
474
986
  - A `tools` list of tool names: exactly those tools, plus the
475
- `delegateTools` entries.
987
+ `delegateTools` entries. A `$name` entry stands for the tools of that
988
+ bundle in the `toolBundles` setting (see below).
476
989
  - A `tools` list of only `-name` entries (`tools: -bash, -write`): the
477
990
  parent's tools, as with no `tools` field, less those, plus the
478
991
  `delegateTools` entries. A `-name` entry may be a `*` pattern
479
992
  (`-mcp__docs__*`).
480
- - A `+name` entry, or tool names and `-name` entries in one list, make the
481
- file invalid, and the agent is disabled (see below).
993
+ - A `+name` entry, a `-$name` entry, a `$name` entry naming no bundle, or
994
+ tool names and `-name` entries in one list, make the file invalid, and the
995
+ agent is disabled (see below).
996
+
997
+ `toolBundles` in `<agent dir>/squire/settings.json` names tool lists that
998
+ several agents share:
482
999
 
483
- The tools the agent's own `extensions` register are never in the parent's
484
- tools, and pi-squire does not load those extensions in the parent to find
485
- them. Such a tool reaches the delegate only when a plain `tools` list or the
486
- `delegateTools` setting names it (or matches it with a `*` pattern); with no
487
- `tools` field, or only `-name` entries, the delegate does not get it.
1000
+ ```json
1001
+ { "toolBundles": { "readonly": ["read", "grep", "find", "ls"] } }
1002
+ ```
1003
+
1004
+ An agent then writes `tools: [$readonly, pstack_models]`, which reads as
1005
+ `read, grep, find, ls, pstack_models`; a name listed twice counts once. A
1006
+ bundle name is letters, digits, `_` or `-`; each entry is one tool name or
1007
+ `*` pattern, with no comma and no leading `+`, `-` or `$`, so bundles do not
1008
+ nest. A bundle carries tools only, not permission rules. The setting is read
1009
+ with the roster. A setting that is not an object of such lists disables every
1010
+ agent that uses a bundle, with the reason; agents without bundles are
1011
+ unaffected. (`$`, unlike `@`, may start a plain YAML value.)
488
1012
 
489
1013
  The delegation tools (`delegate`, `delegation_status`, `delegation_close`,
490
- `delegation_resume`) are never passed and never available in a delegate:
491
- pi-squire registers none of them there.
1014
+ `delegation_resume`) are removed from ordinary tool selections. An opted-in
1015
+ coordinator below `maxDepth` receives them automatically; a leaf has them
1016
+ inactive. Commands are still registered but cannot bypass admission. See the
1017
+ unreleased [nesting restrictions](#nested-delegation-unreleased).
492
1018
 
493
1019
  The delegate's result reaches the parent through the `message:*` provider's
494
- reply when its run settles, not through a tool, so a short `tools` list cannot
1020
+ reply hook when squire settles the task, not through a tool, so a short `tools` list cannot
495
1021
  stop it answering. To let a delegate message its parent mid-task, whatever its
496
1022
  `tools`, list the tools your `message:*` provider gives a session for that in
497
1023
  the `delegateTools` setting, in `<agent dir>/squire/settings.json` beside
@@ -509,14 +1035,6 @@ not a list of names, or an entry that holds a comma or starts with `+` or
509
1035
  `-`, refuses every launch with an error naming the file, before the task is sent or a window
510
1036
  opens; it does not disable agents one by one.
511
1037
 
512
- `extensions` lists extension paths only this agent's delegate loads, as a
513
- comma-separated or YAML list. Each reaches the child as a repeated
514
- `-e <absolute path>`, on top of its normal extensions, so pi-squire and the
515
- `message:*` provider still load and the result still reaches the parent. `~`
516
- and `~/` expand to the home directory and a relative path resolves against the
517
- folder holding the agent file. A path that does not exist when the roster is
518
- read leaves the agent out, with a warning.
519
-
520
1038
  A `model` or `fallback` entry that is an exact `provider/id` in Pi's registry
521
1039
  is checked as before: a model without credentials or with an active
522
1040
  usage-limit mark is skipped for the next candidate. Anything else, such as an
@@ -539,11 +1057,10 @@ the prompt exactly as every delegate was built before `prompt_mode` existed.
539
1057
 
540
1058
  An agent file that cannot be used is invalid, and its agent is disabled:
541
1059
  frontmatter that is not readable YAML or not a set of fields, an `enabled` or
542
- `auto-exit` that is not `true` or `false`, a `tools` or `extensions` that is
543
- not a list, a `tools` list with a `+name` entry or with tool names and `-name`
544
- entries mixed, a `tools` or `fallback` YAML list entry holding a comma, a
545
- `model` or `fallback` entry with a `*` pattern, or an extension path that
546
- does not exist. A disabled agent is left
1060
+ `auto_exit` that is not `true` or `false`, a `tools` value that is not a list,
1061
+ a `tools` list with a `+name` entry or with tool names and `-name`
1062
+ entries mixed, a `tools` or `fallback` YAML list entry holding a comma, or a
1063
+ `model` or `fallback` entry with a `*` pattern. A disabled agent is left
547
1064
  out of the roster and of the `delegate` tool's description, so the model never
548
1065
  picks it, and a warning names the file and the reason when the session starts
549
1066
  (and before a run, when a file turns invalid while the session runs). A call
@@ -558,13 +1075,16 @@ add one.
558
1075
  ## Configuration
559
1076
 
560
1077
  `<agent dir>/squire/` holds the optional parent guidance file and
561
- `settings.json` (`windowPlacement`, `worktreeRoot` and `delegateTools`); the
1078
+ `settings.json` (`maxDepth`, `maxConcurrentDelegates`, `maxDelegatesPerRoot`,
1079
+ `windowPlacement`, `worktreeRoot`, `delegateTools`, `toolBundles`,
1080
+ `delegateWidgetPlacement` and `delegateWidgetMaxEntries`); the
562
1081
  agents live in `<agent dir>/agents/` (see [Agents](#agents)).
563
1082
  `delegation_prompt.md` is Markdown, inserted unchanged as the
564
1083
  `squire_parent_delegation` system-prompt section before each new run, including
565
1084
  a run started by an idle delegate result. It is not a conversation or user
566
1085
  message, is not duplicated in the `delegate` tool description or guidelines,
567
- and is not sent to the child. The guidance is added only while the `delegate`
1086
+ and is not copied into the launched child's prompt. An opted-in coordinator
1087
+ reads this same file for its own parent role. The guidance is added only while the `delegate`
568
1088
  tool is active and not hidden; when delegation is inactive or hidden, the
569
1089
  section is removed and tool selection is left as it was. A missing or whitespace-only
570
1090
  file adds no section. Editing the file replaces the section at the next run;
@@ -574,12 +1094,21 @@ valid delegation working. The package ships no default delegation or approval
574
1094
  policy.
575
1095
 
576
1096
  Refresh happens before a new run, not before every model request in a tool
577
- loop, and the extension does not watch the filesystem. `pi-squire` reads no
578
- environment variable of its own. `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT` are set
579
- for the child, never read as configuration by the parent. The window starts
580
- with the tmux server's environment plus only these from the parent:
581
- `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR` (when the parent has it
582
- set; `--session-dir` outranks it), `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT`, and it always gets `PI_UNATTENDED=1`.
1097
+ loop, and the extension does not watch the filesystem. `pi-squire` uses launch variables to bootstrap a delegate; durable session
1098
+ identity takes precedence when it reopens. The window starts with the tmux
1099
+ server's environment plus `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`
1100
+ (when set; `--session-dir` outranks it), and these delegate values:
1101
+
1102
+ - `PI_DELEGATE_PARENT`: direct parent's session id;
1103
+ - `PI_DELEGATE_ROOT`: root session id;
1104
+ - `PI_DELEGATE_DEPTH`: child depth as a decimal string;
1105
+ - `PI_DELEGATE_AGENT`: this child's roster identity (not display name);
1106
+ - `PI_DELEGATE_CHAIN`: comma-separated agent names, root downward, including
1107
+ the child last, e.g. `coordinator,worker`; names label requests, session ids route them;
1108
+ - `PI_DELEGATE_AUTO_EXIT`: `1` or `0`;
1109
+ - `PI_DELEGATE_FALLBACK`: remaining model candidates, when any.
1110
+
1111
+ It always gets `PI_UNATTENDED=1`.
583
1112
  `PI_UNATTENDED=1` is a general signal, not specific to pi-squire: no human is
584
1113
  watching this session, even though it runs in a TUI. Other extensions may read
585
1114
  it to skip a dialog or confirmation instead of waiting on one, and any other
@@ -592,18 +1121,19 @@ is refused with close matches, and so is a model with no configured
592
1121
  credentials. An API key that exists only as an environment variable in the
593
1122
  parent's shell does not reach the delegate, so log in through Pi or make the
594
1123
  variable part of the tmux server's environment. `--auto-exit` and `--no-auto-exit` override the
595
- agent's `auto-exit`.
1124
+ agent's `auto_exit`.
596
1125
 
597
1126
  ## Tools
598
1127
 
599
1128
  The model gets four tools. They share the command's launch path and the
600
- session's delegation records. None of them is registered inside a delegate.
1129
+ session's delegation records. In a delegate they are active only for an
1130
+ opted-in coordinator below `maxDepth`; admission also checks every launch/resume.
601
1131
 
602
1132
  | Tool | Parameters | What it does |
603
1133
  | --- | --- | --- |
604
1134
  | `delegate` | `agent`, `task`, optional `model`, `thinking`, `label`, `cwd` and `auto_exit` (boolean) | Validates the call the way the command does, starts the delegation without opening a dialog (except an unresolved [trust question](#project-trust) for `cwd`), and returns the delegation id at once without waiting for a result. |
605
1135
  | `delegation_status` | optional `id` | Reports each delegation as running, done, closed or closed without a result, with the window, the delegate's session and the result envelope's path. |
606
- | `delegation_close` | `id` | Kills the window through tmux and records the close, so `delegation_status` reports it closed. |
1136
+ | `delegation_close` | `id` | Explicitly fences and closes a descendant subtree using current verified windows. Returns per-member outcomes and complete/partial status; retains worktrees and branches. |
607
1137
  | `delegation_resume` | `id`, `task`, optional `model`, `thinking` and `auto_exit` | Reopens an exited delegate's own session in a new window with a follow-up task, and returns the new delegation's id at once (see [Resuming a delegation](#resuming-a-delegation)). |
608
1138
 
609
1139
  The `delegate` tool's description lists the current roster, one line per agent:
@@ -707,10 +1237,63 @@ even when no delegation is recorded. The details gain a `marks` array of
707
1237
  `{ scope, source, reason, clearsAt }` and a `readingErrors` array (see
708
1238
  [Usage limits](#usage-limits)).
709
1239
 
710
- `delegation_close` kills the window and records the close even when the
711
- result has already arrived. When the window is already gone it only records
712
- the close. An unknown or already closed id returns a message and kills
713
- nothing. The delegate's session file and result envelope stay on disk.
1240
+ ## Explicit subtree close
1241
+
1242
+ `delegation_close` and the `/delegates` close actions enumerate persistent
1243
+ records and close the selected session and all its descendants, including work
1244
+ whose coordinator is offline. Any ancestor may close; unrelated trees, siblings
1245
+ outside the selected subtree and user windows remain untouched. Only the root
1246
+ has the panel's `a` close-all action. Close-all fences new root admissions too;
1247
+ neither action kills a companion tmux session wholesale.
1248
+
1249
+ Admission is fenced under the shared tree lock **before** window effects.
1250
+ Already-reserved pre-dispatch launches cannot dispatch; an already-dispatched
1251
+ create stays charged and fenced until its outcome/current locator is known.
1252
+ The explicit call makes at most three passes, with two 25 ms waits for late
1253
+ locator publication, and otherwise returns partial. It never infers no-start
1254
+ from a timeout, dead opener, finished accounting or an empty tmux listing.
1255
+ There is no automatic close-repair daemon. Retry the explicit operation after
1256
+ positive create/window evidence becomes available; unknown launches are not
1257
+ refunded or relaunched speculatively.
1258
+
1259
+ Only a window matching the **current recorded id and name** is killed, even
1260
+ if its program has exited or its tmux session moved. A same-id manually reopened
1261
+ coordinator can have a newer shared locator than its parent's launch history;
1262
+ close uses that locator. An already absent verified window records a close.
1263
+ Individual tmux or persistence errors do not erase successful members. A failed
1264
+ initial registry read/fence refuses before any kill; a post-kill publication
1265
+ failure reports the side effect and uncertainty, not successful completion.
1266
+
1267
+ The tool's `details` and `structuredContent` match its output schema:
1268
+ `{id, windowId?, windowName?, closed, complete, alreadyClosed, killed, members,
1269
+ error?}`. Each member has `{id, sessionId, windowId?, windowName?, outcome,
1270
+ closed, killed, error?}`; outcome is `killed`, `absent`, `already-closed`,
1271
+ `not-started`, `pending` or `error`. Aggregate `closed/complete` is true only
1272
+ when every captured member is resolved; partial returns `isError:true` while
1273
+ retaining structured details. Repeat close visits unresolved descendants even
1274
+ if the selected coordinator alone was already closed. Unknown/out-of-scope
1275
+ ids and obsolete resumed records refuse without window effects.
1276
+
1277
+ Confirmed shared closes are control/accounting facts, not fabricated worker
1278
+ results. The sole direct-parent owner imports them into its own durable
1279
+ child-task inbox as canceled/missing work, then processes them in a model run
1280
+ before finishing upstream. An offline coordinator does this on same-id reopen;
1281
+ the ancestor never writes its semantic state or emits raw grandchild reports.
1282
+ A stopped root retains its previous replay hold on reopen. Fresh live close
1283
+ outcomes wake normally, like other new outcomes. Real replies arriving before,
1284
+ during or after close are retained and correlated normally, not replaced by a
1285
+ success or cancellation envelope.
1286
+
1287
+ Once the explicit operation is complete, a legitimate direct-parent resume can
1288
+ supersede that session's completed fence as a **new lifetime-counted delegation**.
1289
+ It still passes current authority/owner, chain, depth, grants, caps and trust
1290
+ checks. Failed pre-dispatch resumes leave the old fence effective.
1291
+ Closed descendants remain canceled; resuming a coordinator does not revive them.
1292
+ An unresolved active close cannot be bypassed by resume.
1293
+
1294
+ Quit, crash, reload, session switch and auto-exit do **not** cascade. Close never
1295
+ removes worktrees, branches, session files or result envelopes. Existing narrow
1296
+ rollback of pristine resources on a known failed launch is unchanged.
714
1297
 
715
1298
  ## Usage limits
716
1299
 
@@ -812,17 +1395,20 @@ mark, it changes nothing and lists the active marks. No tool clears marks.
812
1395
 
813
1396
  The parent resolves each delegation's auto-exit from the call
814
1397
  (`auto_exit`, or `--auto-exit` / `--no-auto-exit`), then the agent's
815
- `auto-exit`, then on, and passes it to the child as `PI_DELEGATE_AUTO_EXIT`
1398
+ `auto_exit`, then on, and passes it to the child as `PI_DELEGATE_AUTO_EXIT`
816
1399
  (`1` or `0`). Inside the delegate:
817
1400
 
818
1401
  - While it is on, a run that settles as a normal completion shuts the delegate
819
1402
  down (`ctx.shutdown()`) on the next event-loop turn, after the settle
820
1403
  reply has been sent, so its tmux window closes. The session file stays, and
821
1404
  `pi --session <path>` reopens it (see [Reopening a delegate's session](#reopening-a-delegates-session)).
822
- - A failed run, such as one ending on a usage limit or another provider
823
- error, exits like a normal completion; the parent still gets the failed
1405
+ - A failed final run, such as one ending on a usage limit or another provider
1406
+ error after fallbacks are exhausted, exits like a normal completion; the parent still gets the failed
824
1407
  reply and the session file stays. Only a run the user stopped never exits
825
1408
  the delegate.
1409
+ - A coordinator cannot exit until its child tasks finish and their outcomes
1410
+ are processed in a subsequently settled run. An intermediate failure sends
1411
+ no final task reply and keeps it available for child results.
826
1412
  - The user taking over turns it off for the rest of the session, with a
827
1413
  notice: typed or RPC input, or a run the user stopped (Esc).
828
1414
  - `/auto-exit` inside the delegate turns it on again for the next normal
@@ -830,7 +1416,8 @@ The parent resolves each delegation's auto-exit from the call
830
1416
  - The footer shows that this is a delegate, its agent, and whether auto-exit
831
1417
  is on: `👦 worker delegate · auto-exit on`, or
832
1418
  `👦 worker delegate · auto-exit off`, with 👦 or 👧
833
- picked at random per session.
1419
+ picked at random per session. Its status key is `delegate-self`; its own
1420
+ running-child count uses `delegate`, so the footers do not overwrite each other.
834
1421
 
835
1422
  There is no `subagent_done` tool: auto-exit off only means the window stays.
836
1423
 
@@ -881,10 +1468,12 @@ Delegate session: <session file or id>
881
1468
  5 non-blank lines with heading marks, backticks and bold stripped;
882
1469
  expanded (Ctrl+O), it shows the result as Markdown, the quoted task, then
883
1470
  the ids and files, with the home directory shown as `~`.
884
- - Stopping a delegate's run (Esc) sends no result: the provider holds the
1471
+ - Stopping a delegate's run (Esc) sends no result: pi-squire holds the
885
1472
  task, so the delegation stays running while the user steers it. The next
886
- run that completes sends the result, `done`, and its body opens with a note
887
- that the user took over partway.
1473
+ run that completes successfully sends the result, `done`, and its body opens
1474
+ with a note that the user took over partway. Failed runs in between do not
1475
+ answer a held task. Asks are different: the mail provider still owns their
1476
+ replies, including `stopped` replies, and does not hold them.
888
1477
  - A `failed` reply from a delegate that recorded a usage-limit mark (see
889
1478
  [Usage limits](#usage-limits)) gets one more line right after `Status:`,
890
1479
  for example `Usage limit: the delegate hit a usage limit on provider
@@ -915,15 +1504,26 @@ Delegate session: <session file or id>
915
1504
  current tool calls, before the next model call. A follow-up would wait
916
1505
  until the parent had answered, by which time it had often read the
917
1506
  envelope itself, and the late result cost a second, redundant turn.
918
- - The footer counts the running delegations (`👨 N delegates running`, with 👨 or 👩
919
- picked at random per session), clears one entry per result or close and
920
- hides itself at zero.
1507
+ - The [direct-delegate overview](#direct-delegate-overview) replaces Squire’s
1508
+ running-count entry in interactive Pi. With its cap set to zero, or in a
1509
+ non-TUI host, the footer counts running delegations (`👨 N delegates running`,
1510
+ with 👨 or 👩 picked at random per session), clears one entry per result or
1511
+ close and hides itself at zero. The own-delegate badge stays separate.
921
1512
  - Each result and each close is recorded in the parent session, and the
922
1513
  records are rebuilt synchronously at session start. A resume still
923
1514
  recognises replies, including one that arrived while the parent was
924
1515
  closed, and still reports a recorded close as closed. A result is shown
925
- once and never again.
926
- - Plain replies, and every request, are left to the provider.
1516
+ once for ordinary transport retries. Unprocessed coordinator outcomes may
1517
+ be replayed on reopening. A root retains outcomes until processing too: an
1518
+ already-displayed result wakes a recovery turn without displaying the final
1519
+ result again, and pending content is supplied to the next model request.
1520
+ An explicit root user stop is persisted: reopening does not start that work
1521
+ again automatically. Previously held outcomes wait for a later input or a
1522
+ genuinely fresh outcome. New live results still steer/start a turn, and new
1523
+ gone outcomes keep their normal wake delivery; they clear the held replay
1524
+ state. Coordinator held-task semantics stay unchanged.
1525
+ - Plain replies, and requests not taken over as this delegate's task, are left
1526
+ to the provider. Squire never answers those ordinary requests.
927
1527
 
928
1528
  While any delegation is running (no result, no close), the parent polls every
929
1529
  5 seconds; the timer does not keep the process alive and stops when nothing is
@@ -935,7 +1535,8 @@ gone. A delegation whose window is gone and still has no result after that
935
1535
  scan is recorded as closed without a result, a record separate from
936
1536
  `delegation_close`'s, and the parent gets one `[delegate]` message naming the
937
1537
  delegation, with the delegate session path when one is found, sent as a
938
- `followUp` with `triggerTurn`. A tick declares nothing when nobody answers
1538
+ `followUp` with `triggerTurn` (a coordinator uses `steer` to process the missing
1539
+ outcome before its final report). A tick declares nothing when nobody answers
939
1540
  `message:scan` or when tmux reports an error. A reply that arrives later is
940
1541
  still delivered and makes the delegation done. A resumed session polls again
941
1542
  for every delegation it restores as running.
@@ -952,6 +1553,13 @@ absolute path opens that file directly.
952
1553
 
953
1554
  ## Resuming a delegation
954
1555
 
1556
+ **Breaking resume change:** delegations recorded without the required
1557
+ original-root placement identity cannot be resumed after this update. This
1558
+ includes pre-nesting and identity-less records, even when their original root
1559
+ reopens them. The tool, command and picker refuse before trust resolution, mail,
1560
+ killing an exited window, reserving a launch or spawning a new window. There is
1561
+ no legacy identity synthesis or migration.
1562
+
955
1563
  An exited delegate can continue its own session with a follow-up task:
956
1564
 
957
1565
  ```text
@@ -987,8 +1595,10 @@ and it resumes as the command does.
987
1595
  opened would reach the next resume as a second task. The reopened delegate
988
1596
  receives it once its mailbox starts. If the send fails, the new window is
989
1597
  killed and nothing is recorded.
990
- - A chain resumes from its newest delegation; resuming an older one is refused,
991
- naming the newest.
1598
+ - Only its direct parent may resume it, preserving the original root/depth.
1599
+ The current parent grant, depth/cap/trust checks are reapplied.
1600
+ A chain resumes from its newest local/shared delegation; older records and
1601
+ copied fork/clone history are refused, naming the owner or newest record.
992
1602
  - It is refused, before anything is sent or opened, unless: the delegation is a
993
1603
  started one (not a workspace attempt); it is not `running` (mail a running
994
1604
  delegate instead) and tmux can be asked; no window with its id and name still
@@ -997,7 +1607,8 @@ and it resumes as the command does.
997
1607
  file's header still exists; its agent is still in the roster and not disabled
998
1608
  (a disabled agent's refusal names its file and the reason); and no other
999
1609
  resume of it is under way. A delegate whose window was killed mid-turn can be
1000
- resumed.
1610
+ resumed after a completed close; an unresolved active subtree close refuses
1611
+ resume until positively resolved by an explicit retry.
1001
1612
  - A removed working directory, a removed managed worktree included, makes a
1002
1613
  delegation not resumable: Pi runs a reopened session in its stored directory.
1003
1614
  The window opens in that directory, with project trust decided again for it as
@@ -1014,7 +1625,7 @@ and it resumes as the command does.
1014
1625
  field the agent locks is the exception: it runs on the
1015
1626
  agent file's current `model` (then its fallbacks, even when the delegate had
1016
1627
  moved to a fallback) or `thinking`, a value passed for it is not used, with
1017
- a notice, and passing the file's own value is the same as passing nothing. The agent's prompt, tools and extensions are those of the
1628
+ a notice, and passing the file's own value is the same as passing nothing. The agent's prompt and tools are those of the
1018
1629
  roster now, and an agent without a `tools` field gets the parent's tools as
1019
1630
  they are at the resume.
1020
1631
  - `session:launch` is emitted as for any launch, with the same payload.
@@ -1031,7 +1642,8 @@ that block.
1031
1642
 
1032
1643
  ### `session:launch`
1033
1644
 
1034
- `pi-squire` provides this hook. It emits `{ args, env, agent, cwd, toolCallId }`
1645
+ `pi-squire` provides this hook. It emits
1646
+ `{ args, env, agent, cwd, toolCallId, parent, root, depth, chain }`
1035
1647
  just before it opens the delegate's window: for a launch, after the task has
1036
1648
  been sent; for a resume, before it is sent, since a resume mails the task once
1037
1649
  the window is open (see [Resuming a delegation](#resuming-a-delegation)).
@@ -1043,10 +1655,25 @@ the window is open (see [Resuming a delegation](#resuming-a-delegation)).
1043
1655
  | `agent` | the agent's name, as in `<agent dir>/agents/<name>.md` or `<cwd>/.pi/agents/<name>.md` (never its `display_name`); read it, do not change it |
1044
1656
  | `cwd` | the directory the window opens in, the parent's or the resolved `--cwd`; read it, do not change it |
1045
1657
  | `toolCallId` | the ID of the `delegate` or `delegation_resume` tool call that started the launch; absent when started from the `/delegate` or `/delegate-resume` command; read it, do not change it |
1658
+ | `parent` | Read-only direct parent session id. |
1659
+ | `root` | Read-only root session id; retained on resume. |
1660
+ | `depth` | Read-only numeric child depth, root = 0; retained on resume. |
1661
+ | `chain` | Read-only frozen array of roster agent names ordered root downward, including the current child last; no session ids. |
1046
1662
 
1047
1663
  Listeners may only append to `args` and add keys to `env`; there is no veto,
1048
- and `pi-squire` passes both to tmux unchanged. Do all work synchronously: the
1049
- emitter reads the payload the moment `emit` returns.
1664
+ and top-level launches pass both to tmux unchanged. For nested launches and
1665
+ resumes, after all listeners return, pi-squire resolves the final tool selection
1666
+ and appends an authoritative exact-name `--tools` registration allowlist bounded
1667
+ by the direct parent. This limits which tool names the child can register and
1668
+ reach, including names provided by hook-added extensions, late registrations
1669
+ and MCP. It does not
1670
+ limit extension code or implementations registered under allowed names;
1671
+ unrelated arguments/environment keys survive. For nested launches and resumes,
1672
+ final customized arguments that Pi cannot parse refuse the launch before the
1673
+ window opens.
1674
+ An empty final set also receives `--no-tools` and an empty allowlist so earlier
1675
+ selection flags cannot restore tools. Do all work synchronously: the emitter
1676
+ reads the payload the moment `emit` returns.
1050
1677
 
1051
1678
  `cwd`, the trust flag (`--approve` or `--no-approve`), `--session-dir` and the
1052
1679
  session choice (`--session-id` for a new delegate, `--session` for a [resumed](#resuming-a-delegation)
@@ -1063,6 +1690,121 @@ pi.events.on("session:launch", (payload) => {
1063
1690
  payload.args.push("--auto");
1064
1691
  payload.env.GATE_MODE = "auto";
1065
1692
  payload.env.GATE_ROOT = payload.cwd;
1693
+ payload.env.GATE_CHAIN = payload.chain.join(",");
1694
+ payload.env.GATE_PARENT = payload.parent;
1695
+ assert.equal(payload.chain.at(-1), payload.agent);
1696
+ assert.ok(Number.isSafeInteger(payload.depth));
1697
+ assert.ok(Object.isFrozen(payload.chain));
1698
+ });
1699
+ ```
1700
+
1701
+ ### `squire:identity`
1702
+
1703
+ `pi-squire` emits this synchronous, read-only notification from its identity
1704
+ restore handler in `session_start`, before its later asynchronous startup work.
1705
+ The payload is `{ sessionId, status, identity? }` for the active exact session id:
1706
+
1707
+ - `status: "delegate"`: its saved identity passed Squire's validation and was
1708
+ successfully persisted again. `identity` contains only
1709
+ `{ parent, root, agent, chain, depth }`, with the same meanings as
1710
+ `session:launch`.
1711
+ - `status: "root"`: no identity record exists for that exact id. This is absence
1712
+ of Squire delegate identity, not proof that the session is an original root.
1713
+ - `status: "invalid"`: reading, validating or persisting its identity failed.
1714
+ No prior identity or raw error is included; Squire's existing restore error
1715
+ and launch/completion refusal remain in effect.
1716
+
1717
+ The payload, identity projection and copied `chain` array are frozen. Internal
1718
+ placement and auto-exit state are not exposed. Environment variables, display
1719
+ labels and copied transcript entries are not identity authority.
1720
+
1721
+ Pi 1.3 and 1.4 recreate the extension runtime and dispatch `session_start`
1722
+ after new, fork/clone and session switch/import; reload also dispatches it.
1723
+ Each restore reads only the destination's exact id, so a new/forked session
1724
+ without its own record reports `root`, not the source's delegate identity.
1725
+ This event does not adopt sessions or migrate records.
1726
+
1727
+ Register listeners before Squire loads and do all observation synchronously.
1728
+ The bus does not replay notifications, await asynchronous listeners, acknowledge
1729
+ listener success or veto later startup; listener exceptions are reported by Pi.
1730
+ Consumers own resetting their state at session shutdown/replacement, matching
1731
+ `sessionId` to their current context, and handling missing or invalid
1732
+ notifications. A consumer that must initialize before another extension's
1733
+ `session_start` must load its listener before Squire, and Squire before that
1734
+ extension. This is an observation seam, not an authorization gate or a provider
1735
+ integration.
1736
+
1737
+ ```js squire:identity
1738
+ pi.events.on("squire:identity", (snapshot) => {
1739
+ assert.ok(Object.isFrozen(snapshot));
1740
+ assert.equal(typeof snapshot.sessionId, "string");
1741
+ assert.ok(["root", "delegate", "invalid"].includes(snapshot.status));
1742
+ if (snapshot.status === "delegate") {
1743
+ assert.ok(Object.isFrozen(snapshot.identity));
1744
+ assert.ok(Object.isFrozen(snapshot.identity.chain));
1745
+ assert.equal(typeof snapshot.identity.root, "string");
1746
+ assert.equal("rootPlacement" in snapshot.identity, false);
1747
+ } else {
1748
+ assert.equal("identity" in snapshot, false);
1749
+ }
1750
+ });
1751
+ ```
1752
+
1753
+ ### Process-local completion holds (v1)
1754
+
1755
+ Squire composes external work with its existing completion predicate through a
1756
+ provider-neutral data table at `globalThis[Symbol.for("pi.completion-holds.v1")]`:
1757
+
1758
+ ```ts
1759
+ {
1760
+ version: 1,
1761
+ hosts: Map<sessionId, Map<hostName, runtimeToken>>,
1762
+ holds: Map<sessionId, Map<holdId, { provider: string, ready: boolean }>>
1763
+ }
1764
+ ```
1765
+
1766
+ An active Squire runtime registers `"pi-squire"` with a fresh string runtime token
1767
+ only during its exact session's `session_start`, after process ownership is
1768
+ acquired. A losing duplicate does not advertise support. Every
1769
+ `session_shutdown`, including reload, removes only that runtime's matching host
1770
+ token. Stale shutdown cannot remove a replacement's registration.
1771
+
1772
+ Providers must check for a well-formed, nonempty host map for the current exact
1773
+ session before promising a hold. Register a unique hold id with `ready:false`
1774
+ atomically before launching the work; refund only that provisional entry if
1775
+ launch fails. Keep accepted entries across reload, including while the provider
1776
+ is unavailable. Set `ready:true` only after the terminal result has been observed
1777
+ and successfully processed; the provider owns that decision and its normal-run
1778
+ commit, before Squire reads readiness at `agent_settled`. Squire neither observes
1779
+ external output nor decides whether an operation succeeded.
1780
+
1781
+ All current-session holds must be well formed and ready before Squire sends a
1782
+ new final task reply, marks its tree entry finished, or auto-exits. An absent
1783
+ table or no holds preserves normal completion. A malformed table/version or
1784
+ malformed current-session host/hold entry blocks completion; Squire does not
1785
+ overwrite it to repair storage. Other sessions' holds do not block this session.
1786
+ An absent host does not invalidate already-ready entries; it refuses promises
1787
+ of new holds. Providers clear only their own holds at non-reload teardown;
1788
+ Squire never removes provider entries.
1789
+
1790
+ This is trusted in-process extension data, not a model-callable release API,
1791
+ cross-process registry, durable reopen block, or authorization boundary. There
1792
+ are no callbacks in the table, fake child tasks, polling/continuation loops, or
1793
+ automatic model calls to keep a session alive. Ordinary shutdown cancels the
1794
+ provider's work; reopening does not recover these holds. Already-frozen reply
1795
+ outboxes retain their existing retry semantics.
1796
+
1797
+ This executable observer loads after Squire and checks only the host contract:
1798
+
1799
+ ```js completion-holds:v1
1800
+ pi.on("session_start", (_event, ctx) => {
1801
+ const table = globalThis[Symbol.for("pi.completion-holds.v1")];
1802
+ assert.equal(table.version, 1);
1803
+ assert.ok(table.hosts instanceof Map);
1804
+ assert.ok(table.holds instanceof Map);
1805
+ const hosts = table.hosts.get(ctx.sessionManager.getSessionId());
1806
+ assert.ok(hosts instanceof Map);
1807
+ assert.equal(typeof hosts.get("pi-squire"), "string");
1066
1808
  });
1067
1809
  ```
1068
1810
 
@@ -1075,6 +1817,66 @@ and `pi-squire` refuses with a message naming `message:*`. The envelope's `id`
1075
1817
  is kept in the delegation's record, so an answer can be matched to it. The
1076
1818
  provider's own README carries the contract.
1077
1819
 
1820
+ ### `message:reply`
1821
+
1822
+ After the final run settles (including fallback continuations and the durable
1823
+ child-first processing barrier), squire emits
1824
+ `{ to, inReplyTo, body, status }`: the direct parent's address, the owed request
1825
+ ids in batches of at most 50, the exact result text, and `done` or `failed`.
1826
+ The provider must synchronously set `envelope` or `error`; neither means no
1827
+ provider. Squire keeps the task owed until the write succeeds and completion
1828
+ is recorded. The provider's README owns the transport contract.
1829
+
1830
+ Task ownership and a frozen final-answer outbox live in
1831
+ `<agent dir>/squire/task-replies/<session id>.json`, independently of Pi's
1832
+ buffered transcript. Atomic file replacement preserves pending answers over
1833
+ reload, restart, squire resume and manual reopening of the same session with
1834
+ the same agent directory. Identity and auto-exit state live separately in
1835
+ `<agent dir>/squire/identity/<encoded session id>.json`; manual reopening
1836
+ restores parent/root/depth/agent/chain, the original `rootPlacement` companion
1837
+ preference and the saved auto-exit setting. This internal placement scalar adds
1838
+ no launch environment variable or `session:launch` field. The
1839
+ launch-time remaining fallback list is not restored by a manual reopen;
1840
+ squire resume still supplies its current fallback candidates.
1841
+
1842
+ Direct-child facts and pending result/gone outcomes live in
1843
+ `<agent dir>/squire/child-tasks/<encoded parent session id>.json`, independently
1844
+ of transcript buffering. Starts and terminal outcomes are saved locally before
1845
+ result injection. Restoring these facts precedes Squire task handling. If mail
1846
+ claims before that restoration (including delayed startup or a duplicate
1847
+ process), matching Squire input is staged durably for the sole owner.
1848
+ Each model request includes pending outcomes; only those observed by a request
1849
+ and followed by a non-stopped settlement are acknowledged. The task reply gate
1850
+ requires no unfinished children and no unprocessed outcomes. Storage failures, including incomplete transcript/child-identity restoration,
1851
+ block final completion/auto-exit and new launch/resume admission in roots and
1852
+ coordinators. Read-only admission checks the restored store health; a write
1853
+ preflight persists unchanged task facts before mail, windows, worktree creation
1854
+ or killing an exited resume window. Repair unhealthy storage and reload before
1855
+ launching again. An incomplete-restore latch also refuses incidental saves, so
1856
+ partial in-memory records cannot replace known pending obligations. These local
1857
+ task records remain the result-processing authority;
1858
+ the shared [tree registry](#tree-wide-launch-budgets) indexes their durable
1859
+ lifecycle facts without copying task bodies or processing outcomes.
1860
+
1861
+ A failed write produces a visible `delegate: could not handle the task reply`
1862
+ error and blocks auto-exit. A frozen answer is retried on the next settled
1863
+ run, or on reload/reopen (starting on the next event-loop turn); the next
1864
+ run's text does not replace that answer. An async startup handler between
1865
+ squire and mail can delay provider initialization past that turn, so a failed
1866
+ startup attempt gets two additional reply attempts, 100 ms apart, on an
1867
+ unref'd timer. After those attempts, any failure is visible and no ongoing
1868
+ retry timer remains. Fix the provider or storage error and reload/reopen to
1869
+ retry without a model call. A successful startup retry does not itself close
1870
+ the window.
1871
+
1872
+ Mail does not deduplicate replies. A crash after the envelope write but before
1873
+ completion recording can cause squire to resend the saved outbox. This is
1874
+ at-least-once transport, not atomic exactly-once delivery. The parent matches
1875
+ every correlated task id, records each as answered, and suppresses further
1876
+ final results for those ids even when a retry has a new envelope id. One
1877
+ envelope answering several owed tasks is shown once, using its last pending
1878
+ delegation for the displayed task quote.
1879
+
1078
1880
  ### `message:inbound`
1079
1881
 
1080
1882
  `pi-squire` consumes this hook for answers: a reply whose `in_reply_to` names a
@@ -1087,8 +1889,10 @@ to the provider.
1087
1889
  Inside a delegate, `pi-squire` also takes over the task: a `request` whose
1088
1890
  `from` is the parent's address. It sets `handled` and sends the task as a user
1089
1891
  prompt labelled `[delegate]`, so the run starts the way a typed prompt does.
1090
- The provider still arms the automatic answer for a request a listener took
1091
- over. Every other message to the delegate is left to the provider. `pi-squire` reads `from` and requires
1892
+ Squire durably records its parent and request id before starting the task;
1893
+ it owns the answer through `message:reply`. The provider arms no automatic
1894
+ request answer. Every other message to the delegate is left to the provider.
1895
+ `pi-squire` reads `from` and requires
1092
1896
  it to be the sender's address, which for a delegate is its session id; a reply
1093
1897
  from any other sender is left to the provider, even when it answers a recorded
1094
1898
  request. The provider's README carries the contract.
@@ -1114,7 +1918,12 @@ contract.
1114
1918
 
1115
1919
  ## Testing limits
1116
1920
 
1117
- The automated tests drive the extension through a fake Pi session. They check
1921
+ The automated tests drive the extension through fake Pi lifecycles. The
1922
+ reusable `test/mail-harness.ts` runs multiple independent extension instances
1923
+ with the real, exactly pinned pi-session-mail 0.6.0 over an isolated shared
1924
+ mail root. Protocol tests cover correlation, asks, stops, fallback, pending
1925
+ answers, retries and duplicate delivery after the write/completion crash
1926
+ window. These are not real model runs or real process-crash tests. They check
1118
1927
  the registered tool definitions, the structured prompt options a simulated run
1119
1928
  preparation receives, file handling, visibility gating and the existing launch
1120
1929
  and result paths. They do not prove actual Pi event scheduling, final provider
@@ -1137,12 +1946,13 @@ version and is listed under "Breaking" in [CHANGELOG.md](CHANGELOG.md).
1137
1946
 
1138
1947
  Other extensions may rely on:
1139
1948
 
1140
- - the `session:launch` payload `{ args, env, agent, cwd, toolCallId }`, emitted
1949
+ - the `session:launch` payload
1950
+ `{ args, env, agent, cwd, toolCallId, parent, root, depth, chain }`, emitted
1141
1951
  synchronously before the window opens (after the task is sent for a launch,
1142
1952
  before it is sent for a resume), with
1143
1953
  `cwd`, the trust flag and `--session-dir` chosen by `pi-squire` (see
1144
1954
  [Hooks](#hooks));
1145
- - the `message:send`, `message:inbound` and `message:scan` hooks, consumed as
1955
+ - the `message:send`, `message:reply`, `message:inbound` and `message:scan` hooks, consumed as
1146
1956
  their sections document;
1147
1957
  - the `delegate`, `delegation_status`, `delegation_close` and `delegation_resume` tool names,
1148
1958
  parameters, the recorded `workspace` shape and the `workspaceSnapshot`
@@ -1156,21 +1966,24 @@ Other extensions may rely on:
1156
1966
  - the agent file's frontmatter keys and its locations,
1157
1967
  `<agent dir>/agents/<name>.md` and, in a trusted project,
1158
1968
  `<cwd>/.pi/agents/<name>.md`;
1159
- - the `delegateTools`, `windowPlacement` and `worktreeRoot` settings in
1969
+ - the `maxDepth`, `maxConcurrentDelegates`, `maxDelegatesPerRoot`, `delegateTools`, `toolBundles`,
1970
+ `windowPlacement`, `worktreeRoot`, `delegateWidgetPlacement` and
1971
+ `delegateWidgetMaxEntries` settings in
1160
1972
  `<agent dir>/squire/settings.json`;
1161
1973
  - the optional `<agent dir>/squire/delegation_prompt.md` file and the
1162
1974
  `squire_parent_delegation` system-prompt section;
1163
- - the child environment variables `PI_DELEGATE_PARENT`,
1164
- `PI_DELEGATE_AUTO_EXIT` and `PI_UNATTENDED=1`.
1975
+ - the child environment variables `PI_DELEGATE_PARENT`, `PI_DELEGATE_ROOT`,
1976
+ `PI_DELEGATE_DEPTH`, `PI_DELEGATE_AGENT`, `PI_DELEGATE_CHAIN`,
1977
+ `PI_DELEGATE_AUTO_EXIT`, `PI_DELEGATE_FALLBACK` and `PI_UNATTENDED=1`.
1165
1978
 
1166
1979
  A `message:*` provider must:
1167
1980
 
1168
- - implement `message:send`, `message:inbound` and `message:scan` as documented
1169
- under [Hooks](#hooks);
1981
+ - implement `message:send`, `message:reply`, `message:inbound` and `message:scan`
1982
+ as documented under [Hooks](#hooks);
1170
1983
  - set a reply's `from` to the sender's address, so the delegate's reply can be
1171
1984
  matched to its task;
1172
- - reply on its own when the delegate's run settles, since that reply is what
1173
- becomes the result.
1985
+ - write explicit request replies through `message:reply`, without automatically
1986
+ answering requests at settle; asks remain separately owned by the provider.
1174
1987
 
1175
1988
  ## Contributing
1176
1989