@gotgenes/pi-subagents 11.3.0 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [11.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.4.0...pi-subagents-v11.5.0) (2026-05-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * add WorkspaceProvider registration seam to subagents service ([51a9970](https://github.com/gotgenes/pi-packages/commit/51a99701db214c11f08251e9ed5549d01c4d5839))
14
+ * consult workspace provider for child cwd and disposal ([32eeffc](https://github.com/gotgenes/pi-packages/commit/32eeffc1cc31bc7e403c25cdd116e2b351be4527))
15
+
16
+ ## [11.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.3.0...pi-subagents-v11.4.0) (2026-05-29)
17
+
18
+
19
+ ### Features
20
+
21
+ * add child-execution lifecycle event publisher ([4d27c13](https://github.com/gotgenes/pi-packages/commit/4d27c130b4782b7fffb9b61a37e151f8500c55ea))
22
+ * emit child-execution lifecycle events and retire permission-bridge ([c8daee4](https://github.com/gotgenes/pi-packages/commit/c8daee4bcf21f6720d9dbc164282fb6a04e552b1))
23
+
8
24
  ## [11.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.2.0...pi-subagents-v11.3.0) (2026-05-29)
9
25
 
10
26
 
@@ -274,7 +274,8 @@ src/
274
274
  │ ├── concurrency-queue.ts background agent scheduling with configurable concurrency limit
275
275
  │ ├── parent-snapshot.ts immutable spawn-time parent state
276
276
  │ ├── execution-state.ts session/output phase state
277
- │ ├── permission-bridge.ts optional bridge to pi-permission-system registry
277
+ │ ├── child-lifecycle.ts child-execution lifecycle event publisher
278
+ │ ├── workspace.ts workspace provider seam (generative extension surface)
278
279
  │ ├── worktree.ts git worktree isolation
279
280
  │ ├── worktree-isolation.ts worktree lifecycle collaborator
280
281
  │ └── usage.ts token usage tracking
@@ -352,15 +353,18 @@ They declare this package as an optional peer dependency and use dynamic import
352
353
  - `AgentManager` — spawn, abort, resume, collection management, observer wiring.
353
354
  - `ConcurrencyQueue` — background agent scheduling with configurable concurrency limit.
354
355
  - `agent-runner` — session creation, turn loop, extension binding.
355
- - `permission-bridge` — optional cross-extension bridge to `@gotgenes/pi-permission-system`; registers each child session with `SubagentSessionRegistry` before `bindExtensions()` so the permission system detects in-process children deterministically.
356
- Scheduled for removal in Phase 16 replaced by lifecycle events that consumers listen for.
356
+ - `child-lifecycle` — publishes the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) on `pi.events`.
357
+ Reactive consumers subscribe: `@gotgenes/pi-permission-system` registers each child session on `session-created` and unregisters it on `disposed`.
358
+ This replaced the former outbound `permission-bridge` (#261, ADR 0002) — the core no longer looks up a named consumer.
359
+ - `workspace` — the single generative seam (#262, ADR 0002): a registered `WorkspaceProvider` supplies a child's cwd plus bracketed `dispose()` at run-start.
360
+ With no provider, children run in the parent cwd (default unchanged); the git worktree strategy moves behind this seam in #263.
357
361
  - `session-config` — pure configuration assembler (extracted from `agent-runner`).
358
362
  - `SubagentRuntime` — session-scoped state bag with methods.
359
363
  - `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
360
364
  - `record-observer` — session-event observer that updates record statistics without callback threading.
361
365
  - Agent type registry — default agents, custom `.md` file loading.
362
366
  - Prompt assembly, context extraction, skills, environment.
363
- - Worktree isolation.
367
+ - Worktree isolation — moving to `@gotgenes/pi-subagents-worktrees` via the workspace provider seam in Phase 16 (ADR 0002).
364
368
  - Token usage tracking.
365
369
  - Session directory derivation and persisted `SessionManager` for subagent transcripts.
366
370
  - Settings persistence.
@@ -449,9 +453,34 @@ These are fire-and-forget broadcast events — no request IDs, no reply channels
449
453
 
450
454
  ## Target architecture
451
455
 
452
- The long-term architectural direction is to make pi-subagents a **minimal core** with inverted dependencies.
453
- Today, pi-subagents reaches outward to pi-permission-system via a bridge module and owns tool/extension filtering logic that duplicates permission-system responsibilities.
454
- The target state eliminates this overlap and flips the dependency direction.
456
+ The long-term architectural direction is to make pi-subagents a **minimal orchestrator** with inverted dependencies.
457
+ The core spawns a child session derived from the parent, runs the turn loop, tracks and streams and collects the result, gates concurrency, supports resume, and **publishes its lifecycle**.
458
+ Everything else permissions, worktree/workspace isolation, UI, telemetry — is an extension that attaches through one of two surfaces and never reaches into the core.
459
+
460
+ The rationale and the full reasoning chain that led here are recorded in [`docs/decisions/0002-extensions-on-a-minimal-core.md`](../decisions/0002-extensions-on-a-minimal-core.md).
461
+
462
+ ### Two extension surfaces
463
+
464
+ Extensions attach through exactly two surfaces, distinguished by the direction of information flow.
465
+
466
+ 1. **Lifecycle events (observational) — unlimited.**
467
+ The core emits awaited, ordered events for the child-execution lifecycle (`spawning`, `session-created` pre-`bindExtensions`, `completed`, `disposed`).
468
+ Any number of extensions subscribe; handlers return nothing.
469
+ Reactive concerns live here: permission detection, telemetry, UI, notifications.
470
+ Adding a reactive concern never modifies the core.
471
+ 2. **Provider seams (generative) — rationed.**
472
+ The rare concern that must *inject* a value the core consumes synchronously registers a provider the core consults.
473
+ Today there is exactly one: the **workspace provider** (returns the child's working directory plus bracketed setup/teardown).
474
+ A provider seam is the only place the core is "open," so the list is kept as small as possible.
475
+
476
+ The discriminator when deciding how a concern attaches:
477
+
478
+ - It only needs to **know** what happened → subscribe to a lifecycle event (observational, unlimited).
479
+ - It must **return a value the core consumes** → register a provider (generative, rationed).
480
+
481
+ The governing rule — **no vacant hooks**: the architecture must *admit* a seam without *shipping* it until a concrete consumer exists.
482
+ A provider seam with no consumer is a speculative abstraction that taxes every reader and that `fallow` flags as dead.
483
+ Latent extensibility is the deliverable; a vacant hook is not.
455
484
 
456
485
  ### Core responsibilities (keep)
457
486
 
@@ -460,27 +489,34 @@ The target state eliminates this overlap and flips the dependency direction.
460
489
  - **Session lifecycle** — create child sessions, bind extensions, run conversation loop, track results.
461
490
  - **Concurrency management** — queue, abort, resume, max concurrency.
462
491
  - **Recursion guard** — remove pi-subagents' own three tools from child sessions (prevent infinite nesting).
463
- - **Lifecycle events** emit events on `pi.events` when child sessions are created, completed, etc.
492
+ With `isolated` removed, children always load the parent's resources, so the guard becomes unconditional rather than gated on `cfg.extensions`.
493
+ This is the core defending its own invariant, keyed off its own tool names — not policy.
494
+ - **Lifecycle events** — emit awaited, ordered events when child sessions spawn, are created, complete, and are disposed.
495
+ - **Workspace provider seam** — accept a registered `WorkspaceProvider` and consult it for the child's cwd; default to the parent's cwd when none is registered.
464
496
  - **Service API** — publish `SubagentsService` via `Symbol.for()` for cross-extension access.
465
497
 
466
498
  ### Responsibilities to remove
467
499
 
468
500
  - **Tool policy** (`disallowed_tools`) — access control belongs in pi-permission-system's `permission:` frontmatter.
469
501
  - **Extension filtering** (`extensions: string[]` allowlist) — tool visibility is pi-permission-system's job.
470
- - **Permission bridge** (`permission-bridge.ts`) — outbound coupling to pi-permission-system.
471
- Replaced by lifecycle events that pi-permission-system listens for.
472
- - **Extension lifecycle control** (`extensions: false`, `isolated`) extensions provide behavioral layers (permissions, formatting, context management) that benefit all agents.
473
- Blanket-disabling them is a blunt instrument with no clear use case; tool restrictions belong in the permission system.
502
+ - **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) — environment policy, not core.
503
+ Git worktrees are one *strategy* for choosing the child's working directory; containers, throwaway tmpdirs, and remote sandboxes are others.
504
+ These move to `@gotgenes/pi-subagents-worktrees`, the first consumer of the workspace provider seam.
505
+ - **Extension lifecycle control** (`extensions: false`, `isolated`, `noSkills`) deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
506
+ Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and is left as a *latent* (un-built) provider seam, added only if a real consumer needs it.
474
507
 
475
508
  ### Composition model
476
509
 
477
- In the target state, pi-subagents publishes events and other packages hook in:
510
+ In the target state, pi-subagents publishes events and a provider seam; other packages hook in:
478
511
 
479
- - **pi-permission-system** listens for child session lifecycle events, applies per-agent policy (allow/ask/deny), gates tool calls at runtime.
512
+ - **pi-permission-system** (observational) subscribes to child-session lifecycle events, detects subagent execution context in the child, and gates tool calls at runtime.
513
+ - **pi-subagents-worktrees** (generative) registers a `WorkspaceProvider` that prepares a git worktree at run-start and tears it down after, supplying the child's cwd.
480
514
  - **pi-subagents-ui** (future) subscribes to the service API, renders the widget, conversation viewer, and `/agents` menu.
481
- - **Any future extension** (OTel, auditing, cost tracking) hooks into the same events without pi-subagents knowing.
515
+ - **Any future extension** (OTel, auditing, cost tracking) subscribes to the same events without pi-subagents knowing.
482
516
 
483
- This is achieved across three phases: Phase 14 (strip policy), Phase 16 (invert dependencies), and Phase 17 (extract UI).
517
+ Composition test: install neither extension, only permissions, only workspaces, or both the core is byte-for-byte identical in all four cases, and the two extensions never reference each other.
518
+
519
+ This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert dependencies — extensions on a minimal core), and Phase 17 (extract UI).
484
520
 
485
521
  ## Current structural analysis
486
522
 
@@ -686,252 +722,109 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
686
722
  [#239]: https://github.com/gotgenes/pi-packages/issues/239
687
723
  [#242]: https://github.com/gotgenes/pi-packages/issues/242
688
724
 
689
- ## Improvement roadmap (Phase 16 — agent collaborator architecture)
690
-
691
- Phase 16 gives Agent proper collaborators so it can do its work without accumulating raw materials.
692
-
693
- Phase 15 established the principle: Agent owns its lifecycle, not a manager.
694
- But in practice, Agent received 9 raw config fields and a shared generic runner, then assembled the runner call itself.
695
- The runner (`ConcreteAgentRunner`) is a stateless service — one instance shared across all agents — so every per-agent concern (snapshot, prompt, model, maxTurns, etc.) had to live on Agent as private fields.
696
- The result: `AgentInit` has ~20 optional fields, and Agent stores ~87 `this._` references.
697
-
698
- The deeper issue: the "runner" conflates two concerns.
699
- Session *creation* (platform plumbing — resource loaders, extension binding, tool filtering, env detection) is genuinely separate from session *interaction* (prompt, steer, abort, resume).
700
- Pi's own `Agent` class (in `packages/agent/`) already handles the interaction — it owns the transcript, runs the turn loop, executes tools, manages steering queues.
701
- Our extension's novel value is **child session orchestration within a parent session**: creating child sessions with config derived from the parent, managing concurrency, wiring lifecycle across sessions, and enabling resume.
702
- We should leverage the Pi session for interaction and focus on what's novel.
703
-
704
- ### Target architecture
705
-
706
- Agent receives three collaborators at construction, each ready to go:
707
-
708
- | Collaborator | Absorbs | Agent tells it |
709
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
710
- | Session factory | runner + snapshot + prompt + model + maxTurns + isolated + thinkingLevel + parentSession + getRunConfig (9 fields) | "create me a configured child session" |
711
- | WorktreeIsolation | worktrees + isolation + worktreeState (3 fields) | `setup()`, `cleanup(description)` |
712
- | AgentLifecycleObserver | (already exists, 0 new fields) | `onStarted`, `onSessionCreated`, `onRunFinished`, `onCompacted` |
713
-
714
- After the session factory creates a session, Agent owns it directly — prompt, steer, abort, resume are Agent's verbs, not a collaborator's.
715
- The shared `ConcreteAgentRunner` becomes a factory that produces per-agent session factories.
716
- The "runner" concept dissolves.
717
-
718
- `AgentInit` shrinks from ~20 to ~10 fields:
719
-
720
- - 4 identity (`id`, `type`, `description`, `invocation`)
721
- - 2 status (`status`, `startedAt` — for tests/restore)
722
- - 3 collaborators (`sessionFactory`, `worktree`, `observer`)
723
- - 1 wiring (`signal`)
724
-
725
- Agent's `run()` becomes coordination, not assembly:
726
-
727
- ```text
728
- mark running → notify observer → wire signal
729
- → tell worktree to setup
730
- → tell session factory to create session
731
- → own the session: flush steers, subscribe observers, prompt, track turns
732
- → on completion: tell worktree to cleanup, transition status, notify observer
733
- ```
734
-
735
- Agent's `resume()` is trivially Agent's work — it already has the session:
736
-
737
- ```text
738
- reset status → re-subscribe observer → prompt the existing session → transition status
739
- ```
740
-
741
- ### What we can commit to
742
-
743
- 1. **The runner is not a collaborator — it's Agent's core behavior conflated with a session factory.**
744
- The shared `ConcreteAgentRunner` becomes a factory.
745
- Each agent receives a per-agent session factory with config already bound.
746
- Once the session exists, Agent interacts with it directly.
747
-
748
- 2. **WorktreeIsolation is a genuine collaborator.**
749
- Created by the factory (AgentManager) only when `isolation === "worktree"`.
750
- Agent tells it `setup()` and `cleanup()` instead of managing worktree internals.
751
- The null check (`this.worktree?.setup()`) replaces the mode check (`this._isolation !== "worktree"`).
725
+ ## Phase 15 (complete)
752
726
 
753
- 3. **AgentLifecycleObserver is already a well-designed collaborator.**
754
- No changes needed — Agent tells it about lifecycle events.
727
+ Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that owns its entire execution lifecycle.
728
+ Before Phase 15, `AgentManager` orchestrated everything: calling the runner, handling session creation, wiring observers, and cleaning up worktrees reaching into Agent 10+ times across `spawn()` and `startAgent()`.
729
+ After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
730
+ All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
731
+ See [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) for details.
755
732
 
756
- 4. **AgentInit must shrink dramatically.**
757
- ~20 optional fields → ~10, with clear grouping: identity + collaborators + wiring.
733
+ ## Improvement roadmap (Phase 16 — invert dependencies: extensions on a minimal core)
758
734
 
759
- ### Resolved investigations
735
+ Phase 16 reclaims its original intent — invert the core's outbound dependencies — and extends it: worktree isolation joins permissions as an *extension* on a minimal core, leaving pi-subagents a pure child-session orchestrator.
736
+ The decision and the full reasoning chain are recorded in [ADR 0002](../decisions/0002-extensions-on-a-minimal-core.md); the two-surface extension model is described under [Target architecture](#target-architecture).
760
737
 
761
- All five investigations have been resolved by examining Pi's `AgentSession` SDK interface (source: `@earendil-works/pi-coding-agent` + Pi's `packages/agent/src/agent.ts`).
738
+ ### Abandoned exploration: agent collaborator architecture
762
739
 
763
- #### 1. `AgentSession` SDK interfaceresolved
740
+ An earlier Phase 16 plan ("agent collaborator architecture") proposed giving `Agent` three collaboratorsa session factory, a `WorktreeIsolation`, and a lifecycle observer — and dissolving the runner.
741
+ That framing was abandoned.
742
+ Pulling on a single late-bound `create(cwd?)` parameter on the planned `ChildSessionFactory` exposed deeper problems:
764
743
 
765
- AgentSession provides everything Agent needs for direct session interaction:
744
+ - `WorktreeIsolation.setup()` is a two-phase `construct-then-setup()` that violates "Construct complete" (principle 8) — the worktree is only *ready* at dequeue.
745
+ - The worktree and the child session share one lifespan, so they are one run-scoped resource, not sibling collaborators that `Agent` must sequence; the `cwd` parameter only existed because the worktree was split out and `Agent` relayed its output back in.
746
+ - Worktrees are not intrinsic to subagents — they are one *workspace strategy* and belong outside the core, exactly as Phase 14 evicted tool/extension policy.
766
747
 
767
- | What Agent needs | AgentSession provides |
768
- | ------------------------- | ------------------------------------------------------------------------------------------ |
769
- | Prompt (initial + resume) | `session.prompt(text)` — works for both; calling it again on an existing session IS resume |
770
- | Steer | `session.steer(text)` |
771
- | Abort | `session.abort()` — async, waits for idle |
772
- | Subscribe to events | `session.subscribe(listener)` — turn_end, message_end, tool_execution_end, compaction_end |
773
- | Read messages | `session.messages` |
774
- | Get session file | `session.sessionManager.getSessionFile()` |
775
- | Dispose | `session.dispose()` |
776
-
777
- Key finding: `session.prompt(text)` handles both initial run and resume — our current `resumeAgent()` already just calls this.
778
- The core Pi `Agent` (accessible via `session.agent`) owns the transcript, turn loop, tool execution, and steering/follow-up queues.
779
- Our Agent should call `session.prompt()` directly and subscribe to events for turn-limit enforcement.
780
-
781
- #### 2. Session factory boundary — resolved
782
-
783
- The factory encapsulates everything *before* Agent starts using the session.
784
- The seam is clean: factory produces a ready-to-use `AgentSession`, Agent operates it.
785
-
786
- ```text
787
- Factory creates (platform plumbing):
788
- detect env → assemble config → create resource loader → reload
789
- → create session manager → new session
790
- → createAgentSession() → bindExtensions() → filter tools (recursion guard)
791
- → register with permission bridge
792
- → return { session, outputFile, cleanup }
793
-
794
- Agent takes over (novel orchestration):
795
- → subscribe for turn tracking (maxTurns + graceTurns)
796
- → session.prompt(text)
797
- → collect response from session.messages
798
- → session.steer() / session.abort() for turn limits
799
- → call cleanup() when done
800
- ```
801
-
802
- Factory input: per-agent config (snapshot, prompt, model, maxTurns, isolated, thinkingLevel, parentSession) bound at construction, plus per-call `cwd` from worktree.
803
- Factory output: `{ session: AgentSession, outputFile?: string, cleanup: () => void }`.
804
-
805
- #### 3. Turn-limit enforcement — Agent's job via session subscription
806
-
807
- Agent subscribes to session events and enforces turn limits — this is novel orchestration that Pi's Agent doesn't provide:
808
-
809
- ```typescript
810
- session.subscribe((event) => {
811
- if (event.type === "turn_end") {
812
- turnCount++;
813
- if (turnCount >= maxTurns) session.steer("wrap up");
814
- if (turnCount >= maxTurns + graceTurns) session.abort();
815
- }
816
- });
817
- ```
818
-
819
- This uses `session.subscribe()`, `session.steer()`, and `session.abort()` directly.
820
- No runner involvement needed.
821
-
822
- #### 4. Response collection — Agent's job, simplified
823
-
824
- Agent collects the response directly from `session.messages` after `prompt()` completes.
825
- The existing `getLastAssistantText()` helper (which reads `session.messages`) already works as a fallback.
826
- The streaming `collectResponseText()` subscriber can move onto Agent for real-time text collection during the run.
827
-
828
- #### 5. Permission bridge — factory-internal
829
-
830
- The bridge calls (`registerChildSession` / `unregisterChildSession`) bracket `bindExtensions()` inside the factory.
831
- Since the factory owns `createAgentSession()` and `bindExtensions()`, both bridge calls become factory-internal.
832
- The factory returns a `cleanup()` function that Agent calls on completion; `cleanup()` handles `unregisterChildSession()` along with any other teardown.
833
- Agent never sees or imports the permission bridge.
834
- This naturally resolves the original Phase 16 dependency-inversion concern.
748
+ Issue #256 (`WorktreeIsolation` as a collaborator) shipped under the abandoned plan and is now superseded by #263; issue #257 (`ChildSessionFactory` extraction) was parked.
749
+ The structural win the collaborator plan chased — a born-complete child execution and the dissolution of the runner — is recovered by a cleaner route once the workspace seam exists (Step 5).
835
750
 
836
751
  ### Steps
837
752
 
838
- #### Step 1: Extract `WorktreeIsolation` collaborator — [#256]
753
+ #### Step 1: Child-execution lifecycle events; retire permission-bridge — [#261] ✅ Delivered
839
754
 
840
- Create a collaborator that owns the worktree lifecycle: setup, path access, and cleanup.
841
- Agent receives `worktree?: WorktreeIsolation` instead of `_worktrees` + `_isolation` + managing `worktreeState` internally.
842
- The null check (`this.worktree?.setup()`) replaces the mode check (`this._isolation !== "worktree"`).
843
- AgentManager creates the collaborator only when `isolation === "worktree"` and passes it to Agent ready to go.
755
+ Emit ordered child-execution events (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) carrying child identity (session directory, agent name, parent session id).
756
+ Migrate `@gotgenes/pi-permission-system` to subscribe to `session-created`/`disposed` for registration instead of being looked up by the core; delete `permission-bridge.ts`.
844
757
 
845
- - Target: new `src/lifecycle/worktree-isolation.ts`, `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
846
- - Smell: C (Ask-Don't-Tell Agent checks `_isolation !== "worktree"` and orchestrates `_worktrees.create()` + `worktreeState.performCleanup()` instead of telling a collaborator)
847
- - Outcome: Agent loses `_worktrees`, `_isolation` fields + `setupWorktree()` method; `completeRun()`/`failRun()` simplify from 4-line null-check blocks to `this.worktree?.cleanup()`; AgentInit loses 2 fields
758
+ - Cross-package: pi-subagents (emit + remove bridge) and pi-permission-system (subscribe).
759
+ - Investigation (resolved): `pi.events` is a Node `EventEmitter`, so `emit()` dispatches listeners synchronously on the same call stack — a synchronous subscriber completes before `emit()` returns.
760
+ Emitting `session-created` immediately before `bindExtensions()` therefore guarantees the registry entry lands pre-bind, with no new SDK hook.
761
+ The synchronous-handler constraint is encoded as a real-bus test in pi-permission-system.
762
+ - Outcome: the core stops reaching out to a named consumer; permission detection rides events.
763
+ - Deferred: removing the now-caller-less `registerSubagentSession`/`unregisterSubagentSession` from `PermissionsService` → #267; registry-detected resume ("executing now" → "exists" semantics) → #265.
848
764
 
849
- #### Step 2: Extract `ChildSessionFactory` from runner — [#257]
765
+ #### Step 2: Define the `WorkspaceProvider` seam — [#262] ✅ Delivered
850
766
 
851
- Define the factory interface and extract session creation logic from `runAgent()` into a factory class.
852
- The factory is per-agent: constructed by AgentManager with config (snapshot, prompt, model, maxTurns, isolated, thinkingLevel, parentSession, getRunConfig) already bound.
853
- The shared `ConcreteAgentRunner` gains a `createFactory(config)` method that produces per-agent factories.
854
- `runAgent()` delegates to the factory internally during this step (lift-and-shift Agent is not changed yet).
855
- Permission bridge calls (`registerChildSession` / `unregisterChildSession`) move inside the factory.
767
+ Added the `WorkspaceProvider` / `Workspace` interfaces (`src/lifecycle/workspace.ts`) and `SubagentsService.registerWorkspaceProvider` (single provider, throws on duplicate, returns an unregister disposer).
768
+ Only `WorkspaceProvider` is named-re-exported from `service.ts`; `Workspace` and the context types resolve via inference when a consumer assigns to `WorkspaceProvider` (the worktrees package adds named re-exports in #263 when it imports them by name).
769
+ At run-start `Agent.run()` consults the registered provider (provider-first precedence) for the child's cwd and a disposal handle; with no provider it falls back to the legacy worktree collaborator, and with neither the child runs in the parent's cwd.
770
+ On completion the core calls `Workspace.dispose({ status, description })` and appends the returned `resultAddendum` verbatimthe provider owns the wording.
856
771
 
857
- ```typescript
858
- interface ChildSessionFactory {
859
- create(cwd?: string): Promise<ChildSessionResult>;
860
- }
772
+ - The seam is additive and non-breaking: the existing `isolation: "worktree"` path is untouched (its eviction is Step 3).
773
+ - Land alongside its first consumer (Step 3) to avoid a vacant hook — the "no vacant hooks" rule.
774
+ Within #262 the seam is exercised only by test fakes; do not cut a release containing the seam without `@gotgenes/pi-subagents-worktrees`.
775
+ - Outcome: a single generative seam; the core no longer knows what an "isolation strategy" is.
861
776
 
862
- interface ChildSessionResult {
863
- session: AgentSession;
864
- outputFile?: string;
865
- cleanup: () => void;
866
- }
867
- ```
777
+ #### Step 3: Extract worktrees to `@gotgenes/pi-subagents-worktrees` — [#263]
778
+
779
+ New package implementing `WorkspaceProvider`: prepares a git worktree at run-start (born complete), tears it down after (saving the branch), and owns the "changes saved to branch" result.
780
+ Remove `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, and the `isolation: "worktree"` mode from the core; drop `isolation` from the spawn API and `SubagentsService`.
868
781
 
869
- - Target: new `src/lifecycle/child-session-factory.ts`, `src/lifecycle/agent-runner.ts`
870
- - Smell: B (conflated concerns — `runAgent()` mixes session creation with session interaction)
871
- - Outcome: session creation is independently testable; `permission-bridge.ts` imports move from runner to factory; factory interface is narrow (one method)
782
+ - Supersedes #256.
783
+ New package registered in `release-please-config.json`; peer-depends on `@gotgenes/pi-subagents`.
784
+ - Outcome: git leaves the core; worktree users install one package, everyone else pays nothing.
872
785
 
873
- #### Step 3: Agent owns session lifecycle run + resume via factory — [#258]
786
+ #### Step 4: Remove `isolated` / `extensions: false` / `noSkills` — [#264]
874
787
 
875
- The central step: Agent's `run()` calls `this.factory.create()` to get a session, then interacts with it directly.
876
- Agent absorbs turn-limit enforcement (subscribe to `turn_end`, steer/abort on limits), response collection (read `session.messages` after prompt), and abort forwarding (wire parent signal to `session.abort()`).
877
- Agent's `resume()` calls `session.prompt()` directly — the session already exists from the initial run.
878
- `AgentInit` shrinks: loses `_runner`, `_snapshot`, `_prompt`, `_model`, `_maxTurns`, `_isolated`, `_thinkingLevel`, `_parentSession`, `_getRunConfig` (9 fields); gains `factory` (1 field).
879
- Combined with Step 1, AgentInit goes from ~20 to ~10 fields.
788
+ Children always load the parent's extensions and skills; the recursion guard becomes unconditional.
789
+ Deny-at-use (the in-child permission layer) covers tool restriction; prevent-load is left as a latent provider seam (not shipped).
880
790
 
881
- - Depends on: Step 1 (worktree is a collaborator), Step 2 (factory exists)
882
- - Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`, `src/tools/foreground-runner.ts`, `src/tools/background-spawner.ts`
883
- - Smell: C (Agent assembles 9 raw fields into a runner call instead of telling a collaborator) + B (runner conflates creation and interaction)
884
- - Outcome: Agent owns session interaction; `run()` is coordination not assembly; `resume()` is trivially `session.prompt()`; AgentInit has ~10 fields
791
+ - Depends on: Step 1 (deny-at-use over events).
792
+ - Outcome: the `isolated`/`extensions`/`noSkills` axis is gone; one fewer conditional in the guard.
885
793
 
886
- #### Step 4: Dissolve runner concept — [#259]
794
+ #### Step 5: Born-complete child execution; dissolve the runner — [#265]
887
795
 
888
- Delete `AgentRunner` interface, `ConcreteAgentRunner` class, `runAgent()` function, `resumeAgent()` function.
889
- The shared service that creates per-agent factories gets a clean interface (e.g., `SessionFactoryProvider`).
890
- Clean up dead types: `RunOptions`, `RunResult`, `ResumeOptions` — replaced by the factory interface and direct session interaction.
891
- Retain `getAgentConversation()` (used by conversation viewer) and `normalizeMaxTurns()` (used by spawn-config).
796
+ With the cwd resolved through the provider seam (not relayed by `Agent`), child-session creation produces a born-complete execution (`{ session, outputFile?, dispose() }`).
797
+ `Agent` owns session interaction directly (prompt, steer, abort, resume, turn limits, response collection); `runAgent` / `resumeAgent` / `ConcreteAgentRunner` / `RunOptions` / `RunResult` dissolve.
798
+ Retain `getAgentConversation()` and `normalizeMaxTurns()`.
892
799
 
893
- - Depends on: Step 3
894
- - Target: `src/lifecycle/agent-runner.ts`, `src/lifecycle/agent.ts`, `src/index.ts`
895
- - Smell: A (dead code after runner dissolution)
896
- - Outcome: `agent-runner.ts` shrinks from 467 to ~50 lines (retained helpers only) or is deleted with helpers relocated; the "runner" concept is gone from the architecture
800
+ - Depends on: Steps 2–4.
801
+ - Outcome: the "runner" concept is gone; `Agent.run()` is coordination, not assembly — the structural goal of the abandoned collaborator plan, reached cleanly.
897
802
 
898
803
  ### Step dependency diagram
899
804
 
900
805
  ```mermaid
901
806
  flowchart LR
902
- S1["Step 1<br/>WorktreeIsolation"]
903
- S2["Step 2<br/>ChildSessionFactory"]
904
- S3["Step 3<br/>Agent owns session"]
905
- S4["Step 4<br/>Dissolve runner"]
807
+ S1["Step 1<br/>Lifecycle events<br/>(retire bridge)"]
808
+ S2["Step 2<br/>WorkspaceProvider seam"]
809
+ S3["Step 3<br/>Extract worktrees pkg"]
810
+ S4["Step 4<br/>Remove isolated"]
811
+ S5["Step 5<br/>Born-complete execution"]
906
812
 
907
- S1 --> S3
908
813
  S2 --> S3
909
- S3 --> S4
814
+ S1 --> S4
815
+ S2 --> S5
816
+ S3 --> S5
817
+ S4 --> S5
910
818
  ```
911
819
 
912
820
  ### Tracks
913
821
 
914
- 1. **Track A — Foundation** (Steps 1, 2): Extract collaborators.
822
+ 1. **Track A — Inversion seams** (Steps 1, 2): lifecycle events and the workspace seam.
915
823
  Independent of each other — can proceed in parallel.
916
- 2. **Track B — Integration** (Steps 3, 4): Agent uses collaborators, runner dissolves.
917
- Sequential; depends on Track A completing.
918
-
919
- ### Relationship to the original Phase 16 plan
920
-
921
- The original Phase 16 ("invert dependencies") targeted permission-bridge removal, `extensions: false` removal, and `isolated` dissolution.
922
- The permission-bridge concern is resolved by Step 2 — the factory handles registration internally, and Agent never imports the bridge.
923
- The `extensions`/`isolated` concerns are secondary and may move to a later phase once the collaborator architecture is in place.
924
-
925
- ### Fallow health snapshot (2026-05-28)
926
-
927
- | Metric | Value |
928
- | ---------------------- | ------------------------------------------------------------------- |
929
- | Health score | 78/100 (B) — deductions: hotspots -10, unit size -10, coupling -2.5 |
930
- | Dead code | 0 files, 0 exports |
931
- | Production duplication | 11 lines (1 internal clone in `agent-config-editor.ts`) |
932
- | Test duplication | 42 clone groups, 661 lines (3.1%) |
933
- | Hotspot #1 | `index.ts` — 70.0, accelerating (128 commits) |
934
- | Refactoring targets | 0 |
824
+ 2. **Track B — Eviction** (Steps 3, 4): worktrees and `isolated` leave the core.
825
+ Step 3 depends on Step 2.
826
+ 3. **Track C — Consolidation** (Step 5): dissolve the runner around the new seam.
827
+ Depends on Tracks A and B.
935
828
 
936
829
  ## Improvement roadmap (Phase 17 — extract UI)
937
830
 
@@ -962,28 +855,29 @@ Detailed records are preserved in per-phase history files:
962
855
  | 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
963
856
  | 14 | Strip policy from core | Complete | [phase-14-strip-policy.md](history/phase-14-strip-policy.md) |
964
857
  | 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
965
- | 16 | Agent collaborator architecture | Planned | |
858
+ | 16 | Invert dependencies (extensions on a minimal core) | Planned | [ADR 0002](../decisions/0002-extensions-on-a-minimal-core.md) |
966
859
  | 17 | Extract UI to separate package | Planned | — |
967
860
 
968
861
  ### Structural refactoring issues
969
862
 
970
- | Phase | Issue | Summary |
971
- | ------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
972
- | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
973
- | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
974
- | Interface polish | #66, #77 | SDK types, projectAgentsDir |
975
- | Features | #61 | JSONL session transcripts |
976
- | AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
977
- | Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
978
- | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
979
- | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
980
- | Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
981
- | Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
982
- | Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
983
- | Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
984
- | Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
985
- | Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
986
- | Phase 16 | #256, #257, #258, #259 | WorktreeIsolation, ChildSessionFactory, Agent owns session, dissolve runner |
863
+ | Phase | Issue | Summary |
864
+ | -------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
865
+ | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
866
+ | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
867
+ | Interface polish | #66, #77 | SDK types, projectAgentsDir |
868
+ | Features | #61 | JSONL session transcripts |
869
+ | AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
870
+ | Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
871
+ | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
872
+ | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
873
+ | Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
874
+ | Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
875
+ | Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
876
+ | Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
877
+ | Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
878
+ | Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
879
+ | Phase 16 | #261, #262, #263, #264, #265 | Lifecycle events (retire permission-bridge), WorkspaceProvider seam, extract worktrees package, remove isolated, born-complete execution / dissolve runner |
880
+ | Phase 16 (abandoned) | #256 (superseded), #257 (parked) | Agent collaborator architecture — replaced by the inversion approach above (ADR 0002) |
987
881
 
988
882
  The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
989
883
 
@@ -1016,8 +910,14 @@ The upstream test suite is run periodically as a regression canary for the agent
1016
910
  [#217]: https://github.com/gotgenes/pi-packages/issues/217
1017
911
  [#218]: https://github.com/gotgenes/pi-packages/issues/218
1018
912
  [#219]: https://github.com/gotgenes/pi-packages/issues/219
913
+ [#227]: https://github.com/gotgenes/pi-packages/issues/227
914
+ [#228]: https://github.com/gotgenes/pi-packages/issues/228
915
+ [#229]: https://github.com/gotgenes/pi-packages/issues/229
916
+ [#230]: https://github.com/gotgenes/pi-packages/issues/230
1019
917
  [#231]: https://github.com/gotgenes/pi-packages/issues/231
1020
- [#256]: https://github.com/gotgenes/pi-packages/issues/256
1021
- [#257]: https://github.com/gotgenes/pi-packages/issues/257
1022
- [#258]: https://github.com/gotgenes/pi-packages/issues/258
1023
- [#259]: https://github.com/gotgenes/pi-packages/issues/259
918
+ [#232]: https://github.com/gotgenes/pi-packages/issues/232
919
+ [#261]: https://github.com/gotgenes/pi-packages/issues/261
920
+ [#262]: https://github.com/gotgenes/pi-packages/issues/262
921
+ [#263]: https://github.com/gotgenes/pi-packages/issues/263
922
+ [#264]: https://github.com/gotgenes/pi-packages/issues/264
923
+ [#265]: https://github.com/gotgenes/pi-packages/issues/265