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