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