@gotgenes/pi-subagents 11.1.0 → 11.3.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,25 @@ 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.3.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.2.0...pi-subagents-v11.3.0) (2026-05-29)
9
+
10
+
11
+ ### Features
12
+
13
+ * **pi-subagents:** add WorktreeIsolation collaborator ([ee7ab73](https://github.com/gotgenes/pi-packages/commit/ee7ab73a53f8643b5887856c33d53786a5a5a9cc))
14
+
15
+ ## [11.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.1.0...pi-subagents-v11.2.0) (2026-05-28)
16
+
17
+
18
+ ### Features
19
+
20
+ * add Agent.resume() with internal observer lifecycle ([6cffb47](https://github.com/gotgenes/pi-packages/commit/6cffb47079e385b0ccd12e358c12357291be2ef0))
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * release abort-signal listener when worktree setup fails ([ce2cac6](https://github.com/gotgenes/pi-packages/commit/ce2cac6788ffc90316f759e40e4df29576a70128))
26
+
8
27
  ## [11.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.0.1...pi-subagents-v11.1.0) (2026-05-28)
9
28
 
10
29
 
@@ -112,7 +112,7 @@ classDiagram
112
112
  +toolUses: number
113
113
  +lifetimeUsage: LifetimeUsage
114
114
  +execution?: ExecutionState
115
- +worktreeState?: WorktreeState
115
+ +worktree?: WorktreeIsolation
116
116
  +notification?: NotificationState
117
117
  +markRunning()
118
118
  +markCompleted()
@@ -121,12 +121,13 @@ classDiagram
121
121
  +markError()
122
122
  +markStopped()
123
123
  +resetForResume()
124
+ +run()
125
+ +resume(prompt, signal)
124
126
  +abort(): boolean
125
127
  +queueSteer(message)
126
128
  +flushPendingSteers(session)
127
- +setupWorktree(worktrees, isolation)
128
- +completeRun(result, worktrees)
129
- +failRun(err, worktrees)
129
+ +completeRun(result)
130
+ +failRun(err)
130
131
  +wireSignal(signal, onAbort)
131
132
  +attachObserver(unsub)
132
133
  +releaseListeners()
@@ -136,7 +137,7 @@ classDiagram
136
137
  class AgentManager {
137
138
  +spawn(snapshot, type, prompt, config)
138
139
  +spawnAndWait(snapshot, type, prompt, config)
139
- +resume(id, snapshot, exec)
140
+ +resume(id, prompt, signal)
140
141
  +getRecord(id): Agent
141
142
  +listAgents(): Agent[]
142
143
  +abort(id)
@@ -275,7 +276,7 @@ src/
275
276
  │ ├── execution-state.ts session/output phase state
276
277
  │ ├── permission-bridge.ts optional bridge to pi-permission-system registry
277
278
  │ ├── worktree.ts git worktree isolation
278
- │ ├── worktree-state.ts worktree phase state
279
+ │ ├── worktree-isolation.ts worktree lifecycle collaborator
279
280
  │ └── usage.ts token usage tracking
280
281
 
281
282
  ├── observation/ progress tracking and notification
@@ -488,13 +489,13 @@ This is achieved across three phases: Phase 14 (strip policy), Phase 16 (invert
488
489
  | Metric | Value |
489
490
  | -------------------------- | --------------------------------- |
490
491
  | Health score | 78/100 (B) |
491
- | Total LOC | 8,382 (56 files) |
492
+ | Total LOC | 7,778 (57 files) |
492
493
  | Dead code | 0 files, 0 exports |
493
494
  | Maintainability index | 90.8 (good) |
494
495
  | Avg cyclomatic complexity | 1.4 |
495
496
  | P90 cyclomatic complexity | 2 |
496
497
  | Production duplication | 11 lines (1 internal clone group) |
497
- | Test duplication | 38 clone groups, 634 lines |
498
+ | Test duplication | 42 clone groups, 661 lines |
498
499
  | Fallow refactoring targets | 0 |
499
500
 
500
501
  ### Dependency bag inventory
@@ -685,185 +686,252 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
685
686
  [#239]: https://github.com/gotgenes/pi-packages/issues/239
686
687
  [#242]: https://github.com/gotgenes/pi-packages/issues/242
687
688
 
688
- ## Improvement roadmap (Phase 15domain model evolution)
689
+ ## Improvement roadmap (Phase 16agent collaborator architecture)
689
690
 
690
- Phase 15 evolves `Agent` from a passive state machine into an object that **owns its entire execution lifecycle**.
691
+ Phase 16 gives Agent proper collaborators so it can do its work without accumulating raw materials.
691
692
 
692
- Steps 1–2 (complete) moved per-agent behavior from `AgentManager` onto `Agent`: abort, steer buffering, worktree setup, and run lifecycle methods (`completeRun`, `failRun`).
693
- However, Agent still cannot *run itself*.
694
- `AgentManager.startAgent()` orchestrates the entire execution: calling the runner, handling session creation, wiring observers, and cleaning up worktrees.
695
- The manager reaches into Agent 10 times across `spawn()` + `startAgent()` writing to `notification`, `execution`, and `promise` after construction, passing its own `worktrees` and `runner` as method arguments, and threading `onSessionCreated` callbacks through three layers.
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.
696
697
 
697
- The remaining steps address this by making **Agent born complete**: constructed with all dependencies and configuration, owning its entire execution lifecycle.
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.
698
703
 
699
- ### Architecture target
704
+ ### Target architecture
700
705
 
701
- Agent receives three concerns at construction:
706
+ Agent receives three collaborators at construction, each ready to go:
702
707
 
703
- | Concern | Fields | Lifetime |
704
- | ----------- | ----------------------------------------------------------------------------- | ------------------------- |
705
- | Identity | id, type, description, invocation | Immutable |
706
- | Run config | snapshot, prompt, model, isolation, maxTurns, thinking, signal, parentSession | Immutable per-run |
707
- | Shared deps | runner, worktrees | Shared service references |
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` |
708
713
 
709
- `Agent.run()` encapsulates the full execution lifecycle:
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.
710
717
 
711
- 1. Set up worktree internally (knows its own isolation mode, has worktrees).
712
- 2. Call `this.runner.run()` (has the runner).
713
- 3. Handle session creation internally: set `execution`, flush pending steers, attach record-observer.
714
- 4. Notify lifecycle observer (started, session created, completed, compacted).
715
- 5. Clean up worktree on completion or error.
716
- 6. Transition status.
718
+ `AgentInit` shrinks from ~20 to ~10 fields:
717
719
 
718
- `AgentManager` becomes a collection manager + observer wiring:
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`)
719
724
 
720
- - Creates complete Agent objects, stores them in the map.
721
- - Decides when to run (immediate or queue) and calls `agent.run()`.
722
- - Provides high-level actions: abort, list, cleanup.
723
- - Does *not* own the runner, worktrees, or any run-orchestration logic.
725
+ Agent's `run()` becomes coordination, not assembly:
724
726
 
725
- The queue stores agent IDs, not `SpawnArgs`.
726
- When capacity opens, the manager looks up the agent and calls `agent.run()` — the agent already has everything.
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
+ ```
727
740
 
728
- The `onSessionCreated` callback that currently threads through `AgentSpawnConfig` → `startAgent` → `RunOptions` → runner disappears.
729
- Agent handles session creation internally during `run()` and notifies external observers via the lifecycle observer pattern.
741
+ ### What we can commit to
730
742
 
731
- The synchronous-throw contract for worktree failure (introduced in Step 2's hoist) is replaced by a uniform async error surface.
732
- Worktree failures inside `agent.run()` propagate through the promise.
733
- For background agents, errors surface via `get_subagent_result` and appear in `/agents`.
734
- For foreground agents, `spawnAndWait` awaits the promise naturally.
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.
735
747
 
736
- The scheduling concern (queue, concurrency counter, drain) is tangled into `AgentManager` alongside collection management and run orchestration.
737
- `notifyConcurrencyChanged()` is a scheduling method exposed as a public API so settings can poke the queue — a cross-concern leak.
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"`).
738
752
 
739
- ### Findings summary
753
+ 3. **AgentLifecycleObserver is already a well-designed collaborator.**
754
+ No changes needed — Agent tells it about lifecycle events.
740
755
 
741
- | Finding | Category | Impact | Risk | Priority |
742
- | ------------------------------------------------------------------ | ------------ | ------ | ---- | -------- |
743
- | ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~ | B: Oversized | 5 | 3 | ✅ |
744
- | Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | 5 | 3 | 15 |
745
- | Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 | 12 |
746
- | ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | ✅ |
747
- | ~~`onSessionCreated` callback flows through 3 layers~~ | C: Callbacks | 3 | 2 | subsumed |
748
- | `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | 2 | 1 | 8 |
749
- | `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | 2 | 1 | 6 |
756
+ 4. **AgentInit must shrink dramatically.**
757
+ ~20 optional fields ~10, with clear grouping: identity + collaborators + wiring.
750
758
 
751
- ### Step 1: Evolve AgentRecord into Agent with behavior — [#227] ✅ Complete
759
+ ### Resolved investigations
752
760
 
753
- Rename `AgentRecord` `Agent` (or wrap it).
754
- Move per-agent behavior from `AgentManager` into the agent:
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`).
755
762
 
756
- 1. `Agent.abort()` absorbs status-check + controller.abort + markStopped.
757
- 2. `Agent.queueSteer(message)` / `Agent.flushPendingSteers(session)` — moves pending steers from manager map to per-agent array.
758
- 3. `Agent.setupWorktree(worktrees, isolation)` — moves worktree creation into the agent.
763
+ #### 1. `AgentSession` SDK interface resolved
759
764
 
760
- - Target: `src/lifecycle/agent-record.ts` `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
761
- - Smell: B (anemic domain model) + C (manager reaching into records)
762
- - Outcome: `AgentManager` delegates via Tell-Don't-Ask; per-agent state lives on the agent
765
+ AgentSession provides everything Agent needs for direct session interaction:
763
766
 
764
- ### Step 2: Convert startAgent to async/await — [#228] ✅ Complete
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()` |
765
776
 
766
- Converted `startAgent` to `async` with `try/catch` and dissolved `RunHandle` into `Agent` methods.
767
- `spawn()` assigns `record.promise = this.startAgent(...)` instead of calling `startAgent()` synchronously.
768
- `Agent` gained run lifecycle methods: `completeRun`, `failRun`, `wireSignal`, `attachObserver`, `releaseListeners`, `setOnRunFinished`.
769
- Worktree setup was hoisted to callers (`spawn`, `drainQueue`) to preserve the synchronous-throw contract.
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.
770
780
 
771
- - Depends on: #227
772
- - Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent.ts`
773
- - Smell: C (raw promise callbacks)
774
- - Outcome: zero `.then()`/`.catch()` in `agent-manager.ts`; `RunHandle` deleted; Agent owns run lifecycle
781
+ #### 2. Session factory boundary — resolved
775
782
 
776
- ### Step 3: Push exec/registry relay deps to runner construction — [#231] ✅
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.
777
785
 
778
- `exec` and `registry` moved from `AgentManager` to `ConcreteAgentRunner` via a new `RunnerDeps` interface.
779
- `RunContext` shrunk from 4 to 2 per-call fields (`cwd`, `parentSession`).
780
- `AgentManagerOptions` shrunk from 7 to 5 fields.
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
+ ```
781
818
 
782
- - Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent-runner.ts`, `src/index.ts`
783
- - Smell: C (relay-only dependencies)
784
- - Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields; runner is self-contained
819
+ This uses `session.subscribe()`, `session.steer()`, and `session.abort()` directly.
820
+ No runner involvement needed.
785
821
 
786
- ### Step 4: Agent born complete — Agent.run() absorbs startAgent — [#229] ✅
822
+ #### 4. Response collection — Agent's job, simplified
787
823
 
788
- Agent receives `runner`, `worktrees`, and a lifecycle observer at construction.
789
- Agent creates its own `AbortController` and `NotificationState` from `parentSession.toolCallId` no external writes.
790
- `Agent.run()` encapsulates the entire execution lifecycle: worktree setup, runner invocation, session-creation handling, observer wiring, worktree cleanup, and status transitions.
791
- `startAgent` is deleted from `AgentManager`.
792
- The `onSessionCreated` callback is removed from `AgentSpawnConfig` — replaced by `AgentLifecycleObserver` passed at construction.
793
- `SpawnArgs` is deleted — Agent has its config from construction.
794
- The queue is simplified from `{ id, args }[]` to `string[]` (agent IDs only).
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.
795
827
 
796
- `AgentManager.spawn()` becomes: create complete Agent, put in map, call `agent.run()` or queue the agent ID.
828
+ #### 5. Permission bridge factory-internal
797
829
 
798
- - Depends on: #228, #231
799
- - Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`, `src/tools/background-spawner.ts`, `src/tools/foreground-runner.ts`
800
- - Smell: C (manager orchestrates 10 external touches on Agent) + C (callback flowing through 3 layers)
801
- - Outcome: Agent owns its entire execution lifecycle; `startAgent`, `SpawnArgs`, `onSessionCreated` callback deleted; zero post-construction writes from `AgentManager`
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.
802
835
 
803
- ### Step 5: Extract ConcurrencyQueue from AgentManager — [#230]
836
+ ### Steps
804
837
 
805
- Extract `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into a `ConcurrencyQueue` class.
806
- The queue stores agent IDs — not `SpawnArgs`.
807
- Drain calls `agent.run()` directly — no worktree setup, no args threading.
808
- `SettingsManager` talks to the queue directly — `notifyConcurrencyChanged()` is eliminated from `AgentManager`.
838
+ #### Step 1: Extract `WorktreeIsolation` collaborator [#256]
809
839
 
810
- - Depends on: #229
811
- - Target: new `src/lifecycle/concurrency-queue.ts`, `src/lifecycle/agent-manager.ts`, `src/index.ts`
812
- - Smell: A (tangled concerns) + C (cross-concern leak via `notifyConcurrencyChanged`)
813
- - Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable; queue interface is trivial (agent has everything)
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.
814
844
 
815
- ### Step 6: Agent.resume() with internal observer lifecycle — [#232]
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
816
848
 
817
- Agent has the runner from construction.
818
- `Agent.resume(prompt, signal)` manages its own observer subscription lifecycle using the same internal wiring as `run()`.
819
- `AgentManager.resume()` becomes a one-liner delegation to `agent.resume(prompt, signal)` — no manual `subscribeRecordObserver` / try-finally.
849
+ #### Step 2: Extract `ChildSessionFactory` from runner — [#257]
820
850
 
821
- - Depends on: #229
822
- - Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
823
- - Smell: A (duplicated observer subscribe/unsubscribe pattern)
824
- - Outcome: `AgentManager.resume()` is a 4-line delegation; observer lifecycle is Agent-internal
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.
856
+
857
+ ```typescript
858
+ interface ChildSessionFactory {
859
+ create(cwd?: string): Promise<ChildSessionResult>;
860
+ }
861
+
862
+ interface ChildSessionResult {
863
+ session: AgentSession;
864
+ outputFile?: string;
865
+ cleanup: () => void;
866
+ }
867
+ ```
868
+
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)
872
+
873
+ #### Step 3: Agent owns session lifecycle — run + resume via factory — [#258]
874
+
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.
880
+
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
885
+
886
+ #### Step 4: Dissolve runner concept — [#259]
887
+
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).
892
+
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
825
897
 
826
898
  ### Step dependency diagram
827
899
 
828
900
  ```mermaid
829
901
  flowchart LR
830
- S1["Step 1<br/>Agent with behavior"]
831
- S2["Step 2<br/>async startAgent"]
832
- S3["Step 3<br/>runner self-contained"]
833
- S4["Step 4<br/>Agent.run()"]
834
- S5["Step 5<br/>ConcurrencyQueue"]
835
- S6["Step 6<br/>Agent.resume()"]
836
-
837
- S1 --> S2
838
- S2 --> S4
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"]
906
+
907
+ S1 --> S3
908
+ S2 --> S3
839
909
  S3 --> S4
840
- S4 --> S5
841
- S4 --> S6
842
910
  ```
843
911
 
844
912
  ### Tracks
845
913
 
846
- 1. **Track A — Foundation** (Step 3): Runner becomes self-contained.
847
- No dependencies on other Phase 15 steps; can start immediately.
848
- 2. **Track B — Agent lifecycle** (Steps 4, 6): Agent born complete, owns run + resume.
849
- Step 4 depends on Track A + Step 2.
850
- Step 6 depends on Step 4.
851
- 3. **Track C — Scheduling** (Step 5): ConcurrencyQueue extraction.
852
- Depends on Step 4 (queue drains via `agent.run()`).
914
+ 1. **Track A — Foundation** (Steps 1, 2): Extract collaborators.
915
+ 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.
853
918
 
854
- ## Improvement roadmap (Phase 16 — invert dependencies)
919
+ ### Relationship to the original Phase 16 plan
855
920
 
856
- Phase 16 completes the architectural inversion by removing the outbound permission bridge and the `extensions: false` / `isolated` concepts.
857
- It depends on Phase 15's lifecycle observer (#229) as the replacement mechanism.
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.
858
924
 
859
- Phase 16 is scoped but not yet broken into steps.
860
- Key changes:
925
+ ### Fallow health snapshot (2026-05-28)
861
926
 
862
- 1. Remove `permission-bridge.ts` — the outbound coupling to pi-permission-system.
863
- 2. Emit child session lifecycle events via the observer — pi-permission-system and other consumers listen for these events instead of being called.
864
- 3. Remove `extensions: false`all child sessions load all extensions.
865
- 4. Dissolve or redefine `isolated` — without extension control and tool filtering, the concept either disappears or becomes purely about prompt composition (no skill preloading, no parent context inheritance).
866
- 5. Update pi-permission-system to listen for child session events instead of being registered by the bridge.
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 |
867
935
 
868
936
  ## Improvement roadmap (Phase 17 — extract UI)
869
937
 
@@ -877,25 +945,25 @@ Phases 1–5, 7–14 are complete.
877
945
  Phase 6 (UI extraction to a separate package) is deferred.
878
946
  Detailed records are preserved in per-phase history files:
879
947
 
880
- | Phase | Title | Status | History |
881
- | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
882
- | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
883
- | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
884
- | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
885
- | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
886
- | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
887
- | 6 | Extract UI to separate package | Deferred → Phase 17 | — |
888
- | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
889
- | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
890
- | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
891
- | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
892
- | 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
893
- | 12 | Complexity reduction and test fixture extraction | Complete | [phase-12-complexity-test-fixtures.md](history/phase-12-complexity-test-fixtures.md) |
894
- | 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
895
- | 14 | Strip policy from core | Complete | [phase-14-strip-policy.md](history/phase-14-strip-policy.md) |
896
- | 15 | Domain model evolution | Planned | |
897
- | 16 | Invert dependencies | Planned | — |
898
- | 17 | Extract UI to separate package | Planned | — |
948
+ | Phase | Title | Status | History |
949
+ | ----- | --------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------ |
950
+ | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
951
+ | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
952
+ | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
953
+ | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
954
+ | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
955
+ | 6 | Extract UI to separate package | Deferred → Phase 17 | — |
956
+ | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
957
+ | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
958
+ | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
959
+ | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
960
+ | 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
961
+ | 12 | Complexity reduction and test fixture extraction | Complete | [phase-12-complexity-test-fixtures.md](history/phase-12-complexity-test-fixtures.md) |
962
+ | 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
963
+ | 14 | Strip policy from core | Complete | [phase-14-strip-policy.md](history/phase-14-strip-policy.md) |
964
+ | 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
965
+ | 16 | Agent collaborator architecture | Planned | — |
966
+ | 17 | Extract UI to separate package | Planned | — |
899
967
 
900
968
  ### Structural refactoring issues
901
969
 
@@ -915,6 +983,7 @@ Detailed records are preserved in per-phase history files:
915
983
  | Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
916
984
  | Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
917
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 |
918
987
 
919
988
  The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
920
989
 
@@ -947,9 +1016,8 @@ The upstream test suite is run periodically as a regression canary for the agent
947
1016
  [#217]: https://github.com/gotgenes/pi-packages/issues/217
948
1017
  [#218]: https://github.com/gotgenes/pi-packages/issues/218
949
1018
  [#219]: https://github.com/gotgenes/pi-packages/issues/219
950
- [#227]: https://github.com/gotgenes/pi-packages/issues/227
951
- [#228]: https://github.com/gotgenes/pi-packages/issues/228
952
- [#229]: https://github.com/gotgenes/pi-packages/issues/229
953
- [#230]: https://github.com/gotgenes/pi-packages/issues/230
954
1019
  [#231]: https://github.com/gotgenes/pi-packages/issues/231
955
- [#232]: https://github.com/gotgenes/pi-packages/issues/232
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