@gotgenes/pi-subagents 10.2.0 → 10.2.1

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,19 @@ 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
+ ## [10.2.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v10.2.0...pi-subagents-v10.2.1) (2026-05-27)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * **pi-subagents:** renumber Phase 15 steps to match execution order ([598bb65](https://github.com/gotgenes/pi-packages/commit/598bb653ac8b63756e8c00dfcf19d3167e2dbc37))
14
+ * **pi-subagents:** revise Phase 15 roadmap for Agent-born-complete vision ([e04583e](https://github.com/gotgenes/pi-packages/commit/e04583e75bfc1314674a6f3181762a26733fb830))
15
+ * plan push exec/registry relay deps to runner construction ([#231](https://github.com/gotgenes/pi-packages/issues/231)) ([646b4d5](https://github.com/gotgenes/pi-packages/commit/646b4d5085e0f7d36a397b43b3b46e0537c3141f))
16
+ * **retro:** add planning stage notes for issue [#231](https://github.com/gotgenes/pi-packages/issues/231) ([dc0daee](https://github.com/gotgenes/pi-packages/commit/dc0daee634c17cf2a40336e27f551bfa2ce0e249))
17
+ * **retro:** add retro notes for issue [#228](https://github.com/gotgenes/pi-packages/issues/228) ([d5b563b](https://github.com/gotgenes/pi-packages/commit/d5b563b6484cbd6a89cd7e9e87ebd431aed128fc))
18
+ * **retro:** add TDD stage notes for issue [#231](https://github.com/gotgenes/pi-packages/issues/231) ([28094ae](https://github.com/gotgenes/pi-packages/commit/28094ae812141ea1c93a22be50ed29d31b7a979a))
19
+ * update architecture for runner self-contained ([#231](https://github.com/gotgenes/pi-packages/issues/231)) ([80dd339](https://github.com/gotgenes/pi-packages/commit/80dd339d7dee9b312b52af2b74756c5748619a49))
20
+
8
21
  ## [10.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v10.1.0...pi-subagents-v10.2.0) (2026-05-27)
9
22
 
10
23
 
@@ -599,23 +599,21 @@ export interface ParentSessionInfo {
599
599
 
600
600
  `AgentSpawnConfig` now carries `parentSession?: ParentSessionInfo` instead of three flat optional fields.
601
601
 
602
- #### RunOptions (12 fields → extract RunContext) — done ([#169][169])
602
+ #### RunOptions (12 fields → extract RunContext) — done ([#169][169]), updated by [#231]
603
603
 
604
- The `RunOptions` bag mixes execution parameters with context information.
605
- `RunContext` was extracted and nested as `RunOptions.context`:
604
+ `RunContext` was extracted and nested as `RunOptions.context` in #169.
605
+ Issue #231 moved the two static dependencies (`exec`, `registry`) to `RunnerDeps` on `ConcreteAgentRunner`, leaving `RunContext` with only per-call fields:
606
606
 
607
607
  ```typescript
608
- /** Parent execution context — where/who is running. */
608
+ /** Per-call execution context — fields that vary per spawn. */
609
609
  export interface RunContext {
610
- exec: ShellExec;
611
- registry: AgentConfigLookup;
612
610
  cwd?: string;
613
611
  parentSession?: ParentSessionInfo;
614
612
  }
615
613
  ```
616
614
 
617
615
  The remaining `RunOptions` fields (`model`, `maxTurns`, `signal`, `isolated`, `thinkingLevel`, `defaultMaxTurns`, `graceTurns`, `onSessionCreated`) are genuine execution parameters.
618
- `RunOptions` now has 9 fields: 1 nested `context: RunContext` plus 8 flat execution fields.
616
+ `RunOptions` now has 9 fields: 1 nested `context: RunContext` (2 per-call fields) plus 8 flat execution fields.
619
617
 
620
618
  #### SessionConfig (11 fields → extract ToolFilterConfig) — done ([#168][168])
621
619
 
@@ -686,25 +684,66 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
686
684
 
687
685
  ## Improvement roadmap (Phase 15 — domain model evolution)
688
686
 
689
- Phase 15 addresses the anemic domain model in the lifecycle layer.
690
- `AgentRecord` is a data bag — identity, status transitions, and stats — but no behavior.
691
- `AgentManager` reaches into records 37 times, doing work that belongs on the agent.
692
- Per-agent state (pending steers, abort logic, run lifecycle) was scattered across the manager, `RunHandle`, and a manager-level Map.
693
- `RunHandle` has been dissolved into `Agent` methods see Step 2.
687
+ Phase 15 evolves `Agent` from a passive state machine into an object that **owns its entire execution lifecycle**.
688
+
689
+ Steps 1–2 (complete) moved per-agent behavior from `AgentManager` onto `Agent`: abort, steer buffering, worktree setup, and run lifecycle methods (`completeRun`, `failRun`).
690
+ However, Agent still cannot *run itself*.
691
+ `AgentManager.startAgent()` orchestrates the entire execution: calling the runner, handling session creation, wiring observers, and cleaning up worktrees.
692
+ 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
+
694
+ The remaining steps address this by making **Agent born complete**: constructed with all dependencies and configuration, owning its entire execution lifecycle.
695
+
696
+ ### Architecture target
697
+
698
+ Agent receives three concerns at construction:
699
+
700
+ | Concern | Fields | Lifetime |
701
+ | ----------- | ----------------------------------------------------------------------------- | ------------------------- |
702
+ | Identity | id, type, description, invocation | Immutable |
703
+ | Run config | snapshot, prompt, model, isolation, maxTurns, thinking, signal, parentSession | Immutable per-run |
704
+ | Shared deps | runner, worktrees | Shared service references |
705
+
706
+ `Agent.run()` encapsulates the full execution lifecycle:
707
+
708
+ 1. Set up worktree internally (knows its own isolation mode, has worktrees).
709
+ 2. Call `this.runner.run()` (has the runner).
710
+ 3. Handle session creation internally: set `execution`, flush pending steers, attach record-observer.
711
+ 4. Notify lifecycle observer (started, session created, completed, compacted).
712
+ 5. Clean up worktree on completion or error.
713
+ 6. Transition status.
714
+
715
+ `AgentManager` becomes a collection manager + concurrency controller:
716
+
717
+ - Creates complete Agent objects, stores them in the map.
718
+ - Decides when to run (immediate or queue) and calls `agent.run()`.
719
+ - Provides high-level actions: abort, list, cleanup.
720
+ - Does *not* own the runner, worktrees, or any run-orchestration logic.
721
+
722
+ The queue stores agent IDs, not `SpawnArgs`.
723
+ When capacity opens, the manager looks up the agent and calls `agent.run()` — the agent already has everything.
724
+
725
+ The `onSessionCreated` callback that currently threads through `AgentSpawnConfig` → `startAgent` → `RunOptions` → runner disappears.
726
+ Agent handles session creation internally during `run()` and notifies external observers via the lifecycle observer pattern.
727
+
728
+ The synchronous-throw contract for worktree failure (introduced in Step 2's hoist) is replaced by a uniform async error surface.
729
+ Worktree failures inside `agent.run()` propagate through the promise.
730
+ For background agents, errors surface via `get_subagent_result` and appear in `/agents`.
731
+ For foreground agents, `spawnAndWait` awaits the promise naturally.
694
732
 
695
733
  The scheduling concern (queue, concurrency counter, drain) is tangled into `AgentManager` alongside collection management and run orchestration.
696
734
  `notifyConcurrencyChanged()` is a scheduling method exposed as a public API so settings can poke the queue — a cross-concern leak.
697
735
 
698
736
  ### Findings summary
699
737
 
700
- | Finding | Category | Impact | Risk | Priority |
701
- | ----------------------------------------------------------------- | ------------ | ------ | ---- | -------- |
702
- | `AgentRecord` is anemic — no behavior, manager reaches in 37× | B: Oversized | 5 | 3 | 15 |
703
- | Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 | 12 |
704
- | ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | |
705
- | `onSessionCreated` callback flows through 3 layers | C: Callbacks | 3 | 2 | 10 |
706
- | `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | 2 | 1 | 8 |
707
- | `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | 2 | 1 | 6 |
738
+ | Finding | Category | Impact | Risk | Priority |
739
+ | ------------------------------------------------------------------ | ------------ | ------ | ---- | -------- |
740
+ | ~~`AgentRecord` is anemic — no behavior, manager reaches in 37×~~ | B: Oversized | 5 | 3 | |
741
+ | Agent cannot run itself manager orchestrates 10 external touches | C: Coupling | 5 | 3 | 15 |
742
+ | Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | 4 | 2 | 12 |
743
+ | ~~`startAgent` uses `.then()`/`.catch()` instead of async/await~~ | C: Callbacks | 3 | 2 | |
744
+ | ~~`onSessionCreated` callback flows through 3 layers~~ | C: Callbacks | 3 | 2 | subsumed |
745
+ | `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | 2 | 1 | 8 |
746
+ | `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | 2 | 1 | 6 |
708
747
 
709
748
  ### Step 1: Evolve AgentRecord into Agent with behavior — [#227] ✅ Complete
710
749
 
@@ -731,43 +770,54 @@ Worktree setup was hoisted to callers (`spawn`, `drainQueue`) to preserve the sy
731
770
  - Smell: C (raw promise callbacks)
732
771
  - Outcome: zero `.then()`/`.catch()` in `agent-manager.ts`; `RunHandle` deleted; Agent owns run lifecycle
733
772
 
734
- ### Step 3: Replace onSessionCreated callback with observer method — [#229]
773
+ ### Step 3: Push exec/registry relay deps to runner construction — [#231]
735
774
 
736
- Add `onSessionCreated(agent, session)` to `AgentManagerObserver`.
737
- Remove the `onSessionCreated` callback from `AgentSpawnConfig`.
738
- Tool-layer code subscribes via the observer pattern instead of passing callbacks through the spawn config.
775
+ `exec` and `registry` moved from `AgentManager` to `ConcreteAgentRunner` via a new `RunnerDeps` interface.
776
+ `RunContext` shrunk from 4 to 2 per-call fields (`cwd`, `parentSession`).
777
+ `AgentManagerOptions` shrunk from 7 to 5 fields.
739
778
 
740
- - Target: `src/lifecycle/agent-manager.ts`, `src/tools/background-spawner.ts`, `src/tools/foreground-runner.ts`
741
- - Smell: C (callback flowing through 3 layers)
742
- - Outcome: `AgentSpawnConfig` loses one callback field; session notification uses the observer pattern
779
+ - Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent-runner.ts`, `src/index.ts`
780
+ - Smell: C (relay-only dependencies)
781
+ - Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields; runner is self-contained
782
+
783
+ ### Step 4: Agent born complete — Agent.run() absorbs startAgent — [#229]
784
+
785
+ Agent receives `runner`, `worktrees`, and a lifecycle observer at construction.
786
+ Agent creates its own `NotificationState` from `parentSession.toolCallId` — no external write.
787
+ `Agent.run()` encapsulates the entire execution lifecycle: worktree setup, runner invocation, session-creation handling, observer wiring, worktree cleanup, and status transitions.
788
+ `startAgent` is deleted from `AgentManager`.
789
+ The `onSessionCreated` callback is removed from `AgentSpawnConfig` — Agent handles session creation internally and notifies via the lifecycle observer.
790
+ `SpawnArgs` is deleted — Agent has its config from construction.
791
+
792
+ `AgentManager.spawn()` becomes: create complete Agent, put in map, call `agent.run()` or queue the agent ID.
793
+
794
+ - Depends on: #228, #231
795
+ - Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`, `src/tools/background-spawner.ts`, `src/tools/foreground-runner.ts`
796
+ - Smell: C (manager orchestrates 10 external touches on Agent) + C (callback flowing through 3 layers)
797
+ - Outcome: Agent owns its entire execution lifecycle; `startAgent`, `SpawnArgs`, `onSessionCreated` callback deleted; zero post-construction writes from `AgentManager`
743
798
 
744
- ### Step 4: Extract ConcurrencyQueue from AgentManager — [#230]
799
+ ### Step 5: Extract ConcurrencyQueue from AgentManager — [#230]
745
800
 
746
801
  Extract `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into a `ConcurrencyQueue` class.
802
+ The queue stores agent IDs — not `SpawnArgs`.
803
+ Drain calls `agent.run()` directly — no worktree setup, no args threading.
747
804
  `SettingsManager` talks to the queue directly — `notifyConcurrencyChanged()` is eliminated from `AgentManager`.
748
805
 
806
+ - Depends on: #229
749
807
  - Target: new `src/lifecycle/concurrency-queue.ts`, `src/lifecycle/agent-manager.ts`, `src/index.ts`
750
808
  - Smell: A (tangled concerns) + C (cross-concern leak via `notifyConcurrencyChanged`)
751
- - Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable
752
-
753
- ### Step 5: Push exec/registry relay deps to runner construction — [#231]
754
-
755
- `AgentManager` receives `exec` and `registry` in its constructor but only relays them to `runner.run()` via `context`.
756
- Move them to `ConcreteAgentRunner` construction.
757
-
758
- - Target: `src/lifecycle/agent-manager.ts`, `src/lifecycle/agent-runner.ts`, `src/index.ts`
759
- - Smell: C (relay-only dependencies)
760
- - Outcome: `AgentManager` loses 2 fields; `AgentManagerOptions` shrinks from 7 to 5 fields
809
+ - Outcome: `AgentManager` loses 3 fields, 3 methods (~40 lines); scheduling is independently testable; queue interface is trivial (agent has everything)
761
810
 
762
- ### Step 6: Unify resume() with Agent run lifecycle methods — [#232]
811
+ ### Step 6: Agent.resume() with internal observer lifecycle — [#232]
763
812
 
764
- After #228 dissolved `RunHandle` into Agent methods (`completeRun`, `failRun`, `releaseListeners`), `resume()` on `AgentManager` becomes a short delegation to `agent.resume(runner, prompt, signal)`.
765
- The agent manages its own observer subscription lifecycle using the same methods that `startAgent` uses.
813
+ Agent has the runner from construction.
814
+ `Agent.resume(prompt, signal)` manages its own observer subscription lifecycle using the same internal wiring as `run()`.
815
+ `AgentManager.resume()` becomes a one-liner delegation to `agent.resume(prompt, signal)` — no manual `subscribeRecordObserver` / try-finally.
766
816
 
767
- - Depends on: #227, #228
768
- - Target: `src/lifecycle/agent-manager.ts`
817
+ - Depends on: #229
818
+ - Target: `src/lifecycle/agent.ts`, `src/lifecycle/agent-manager.ts`
769
819
  - Smell: A (duplicated observer subscribe/unsubscribe pattern)
770
- - Outcome: no manual `subscribeRecordObserver` / try-finally in the manager
820
+ - Outcome: `AgentManager.resume()` is a 4-line delegation; observer lifecycle is Agent-internal
771
821
 
772
822
  ### Step dependency diagram
773
823
 
@@ -775,28 +825,32 @@ The agent manages its own observer subscription lifecycle using the same methods
775
825
  flowchart LR
776
826
  S1["Step 1<br/>Agent with behavior"]
777
827
  S2["Step 2<br/>async startAgent"]
778
- S3["Step 3<br/>onSessionCreated observer"]
779
- S4["Step 4<br/>ConcurrencyQueue"]
780
- S5["Step 5<br/>relay deps"]
781
- S6["Step 6<br/>resume unification"]
828
+ S3["Step 3<br/>runner self-contained"]
829
+ S4["Step 4<br/>Agent.run()"]
830
+ S5["Step 5<br/>ConcurrencyQueue"]
831
+ S6["Step 6<br/>Agent.resume()"]
782
832
 
783
833
  S1 --> S2
784
- S1 --> S6
785
- S2 --> S6
786
- S3 ~~~ S4
787
- S4 ~~~ S5
834
+ S2 --> S4
835
+ S3 --> S4
836
+ S4 --> S5
837
+ S4 --> S6
788
838
  ```
789
839
 
790
840
  ### Tracks
791
841
 
792
- 1. **Track A — Domain model** (Steps 1, 2, 6): Agent with behavior, async runs, resume unification.
793
- Sequential each depends on the previous.
794
- 2. **Track B — Decoupling** (Steps 3, 4, 5): independent, can proceed in parallel with Track A.
842
+ 1. **Track A — Foundation** (Step 3): Runner becomes self-contained.
843
+ No dependencies on other Phase 15 steps; can start immediately.
844
+ 2. **Track B — Agent lifecycle** (Steps 4, 6): Agent born complete, owns run + resume.
845
+ Step 4 depends on Track A + Step 2.
846
+ Step 6 depends on Step 4.
847
+ 3. **Track C — Scheduling** (Step 5): ConcurrencyQueue extraction.
848
+ Depends on Step 4 (queue drains via `agent.run()`).
795
849
 
796
850
  ## Improvement roadmap (Phase 16 — invert dependencies)
797
851
 
798
852
  Phase 16 completes the architectural inversion by removing the outbound permission bridge and the `extensions: false` / `isolated` concepts.
799
- It depends on Phase 15's observer pattern (#229) as the replacement mechanism.
853
+ It depends on Phase 15's lifecycle observer (#229) as the replacement mechanism.
800
854
 
801
855
  Phase 16 is scoped but not yet broken into steps.
802
856
  Key changes:
@@ -856,7 +910,7 @@ Detailed records are preserved in per-phase history files:
856
910
  | Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
857
911
  | Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
858
912
  | Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
859
- | Phase 15 | #227, #228, #229, #230, #231, #232 | Agent domain model, async startAgent, onSessionCreated observer, ConcurrencyQueue, relay deps, resume unification |
913
+ | Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
860
914
 
861
915
  The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
862
916
 
@@ -0,0 +1,245 @@
1
+ ---
2
+ issue: 231
3
+ issue_title: "Push exec/registry relay deps to runner construction (Phase 15, Step 3)"
4
+ ---
5
+
6
+ # Push exec/registry relay deps to runner construction
7
+
8
+ ## Problem Statement
9
+
10
+ `AgentManager` receives `exec` and `registry` in its constructor but never uses them directly.
11
+ They are stored as fields solely to relay them into `runner.run()` via the `RunContext` parameter.
12
+ This makes `AgentManager` wider than necessary and prevents the runner from being self-contained — a prerequisite for #229 (Agent.run() absorbs startAgent).
13
+
14
+ ## Goals
15
+
16
+ - Move `exec` and `registry` from `AgentManager` construction to `ConcreteAgentRunner` construction.
17
+ - Remove `exec` and `registry` from `AgentManagerOptions` (7 → 5 fields).
18
+ - Remove `exec` and `registry` from `RunContext` (4 → 2 fields).
19
+ - Group runner-owned dependencies in a `RunnerDeps` interface: `{ io, exec, registry }`.
20
+ - Replace `runAgent()`'s `io: RunnerIO` parameter with `deps: RunnerDeps`.
21
+
22
+ ## Non-Goals
23
+
24
+ - Dissolving `RunContext` entirely — it shrinks to `{ cwd?, parentSession? }`, which is still a coherent per-call grouping.
25
+ Issue #229 will likely dissolve it when `Agent.run()` calls the runner directly.
26
+ - Changing the `AgentRunner` interface's `run()` signature — callers continue to pass `RunOptions` with `context: RunContext`.
27
+ `ConcreteAgentRunner` merges its stored deps before calling `runAgent()`.
28
+ - Touching `resume()` or `resumeAgent()` — they don't use `exec` or `registry`.
29
+
30
+ ## Background
31
+
32
+ Issue #169 extracted `RunContext` from `RunOptions` to group the 4 parent-context fields: `exec`, `registry`, `cwd`, `parentSession`.
33
+ The doc comment describes them as "parent environment and identity" fields.
34
+ However, 2 of the 4 fields (`exec`, `registry`) are static — identical across every `run()` call — while the other 2 (`cwd`, `parentSession`) vary per spawn.
35
+ The static pair are relay-only dependencies on `AgentManager`: stored at construction, never read, only forwarded.
36
+
37
+ From the code-design skill, this is a **parameter relay** smell: intermediaries (`AgentManager`) carry fields they never use, only to thread them to the endpoint (`runAgent`).
38
+ The fix: put them on the object the endpoint owns — the runner.
39
+
40
+ ### Key references
41
+
42
+ - `src/lifecycle/agent-manager.ts` — stores `exec` and `registry`, relays them at lines 193–194.
43
+ - `src/lifecycle/agent-runner.ts` — `RunContext` interface (line 125), `ConcreteAgentRunner` class (line 189), `runAgent()` free function (line 236).
44
+ - `src/index.ts` — constructs both `ConcreteAgentRunner` and `AgentManager` (lines 148–157).
45
+ - Phase 15 roadmap in `docs/architecture/architecture.md` § Step 3.
46
+
47
+ ## Design Overview
48
+
49
+ ### RunnerDeps — grouping runner-owned dependencies
50
+
51
+ A new `RunnerDeps` interface groups the three dependencies that the runner owns:
52
+
53
+ ```typescript
54
+ export interface RunnerDeps {
55
+ io: RunnerIO;
56
+ exec: ShellExec;
57
+ registry: AgentConfigLookup;
58
+ }
59
+ ```
60
+
61
+ `ConcreteAgentRunner` takes `RunnerDeps` at construction:
62
+
63
+ ```typescript
64
+ export class ConcreteAgentRunner implements AgentRunner {
65
+ constructor(private readonly deps: RunnerDeps) {}
66
+
67
+ run(snapshot, type, prompt, options) {
68
+ return runAgent(snapshot, type, prompt, options, this.deps);
69
+ }
70
+ }
71
+ ```
72
+
73
+ `runAgent()` changes its last parameter from `io: RunnerIO` to `deps: RunnerDeps`:
74
+
75
+ ```typescript
76
+ export async function runAgent(
77
+ snapshot: ParentSnapshot,
78
+ type: SubagentType,
79
+ prompt: string,
80
+ options: RunOptions,
81
+ deps: RunnerDeps,
82
+ ): Promise<RunResult> {
83
+ const effectiveCwd = options.context?.cwd ?? snapshot.cwd;
84
+ const env = await deps.io.detectEnv(deps.exec, effectiveCwd);
85
+ // ...
86
+ const cfg = assembleSessionConfig(type, ..., deps.registry, deps.io.assemblerIO);
87
+ // ...
88
+ }
89
+ ```
90
+
91
+ ### RunContext shrinks
92
+
93
+ `RunContext` loses `exec` and `registry`:
94
+
95
+ ```typescript
96
+ export interface RunContext {
97
+ /** Override working directory (e.g. for worktree isolation). */
98
+ cwd?: string;
99
+ /** Parent session identity (file path + session ID). */
100
+ parentSession?: ParentSessionInfo;
101
+ }
102
+ ```
103
+
104
+ The `AgentRunner.run()` interface is unchanged — callers still pass `RunOptions` with `context: RunContext`.
105
+ `ConcreteAgentRunner.run()` reads `exec` and `registry` from its own `deps` instead of from `options.context`.
106
+
107
+ ### AgentManager loses 2 fields
108
+
109
+ `AgentManagerOptions` removes `exec` and `registry`.
110
+ `AgentManager` removes the corresponding private fields and the `this.exec` / `this.registry` relay in `startAgent()`.
111
+ The `context` object constructed in `startAgent()` shrinks from 4 fields to 2:
112
+
113
+ ```typescript
114
+ context: {
115
+ cwd: record.worktreeState?.path,
116
+ parentSession: options.parentSession,
117
+ },
118
+ ```
119
+
120
+ ### Wiring in index.ts
121
+
122
+ ```typescript
123
+ const runner = new ConcreteAgentRunner({
124
+ io: runnerIO,
125
+ exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
126
+ registry,
127
+ });
128
+
129
+ const manager = new AgentManager({
130
+ runner,
131
+ worktrees: new GitWorktreeManager(process.cwd()),
132
+ observer,
133
+ getMaxConcurrent: () => settings.maxConcurrent,
134
+ getRunConfig: () => settings,
135
+ });
136
+ ```
137
+
138
+ ## Module-Level Changes
139
+
140
+ ### `src/lifecycle/agent-runner.ts`
141
+
142
+ 1. Add `RunnerDeps` interface (exported): `{ io: RunnerIO; exec: ShellExec; registry: AgentConfigLookup }`.
143
+ 2. Remove `exec` and `registry` from `RunContext`.
144
+ Update doc comment to reflect the 2 remaining per-call fields.
145
+ 3. Update `ConcreteAgentRunner` constructor: accept `RunnerDeps` instead of `RunnerIO`.
146
+ 4. Update `ConcreteAgentRunner.run()`: pass `this.deps` to `runAgent()`.
147
+ 5. Update `runAgent()`: change last parameter from `io: RunnerIO` to `deps: RunnerDeps`.
148
+ Replace `io.` references with `deps.io.`, `options.context.exec` with `deps.exec`, `options.context.registry` with `deps.registry`.
149
+
150
+ ### `src/lifecycle/agent-manager.ts`
151
+
152
+ 1. Remove `exec: ShellExec` and `registry: AgentTypeRegistry` from `AgentManagerOptions`.
153
+ 2. Remove `private readonly exec` and `private readonly registry` fields from `AgentManager`.
154
+ 3. Remove assignment of `this.exec` and `this.registry` in the constructor.
155
+ 4. Remove `exec: this.exec` and `registry: this.registry` from the `context` object in `startAgent()`.
156
+ 5. Remove `ShellExec` and `AgentTypeRegistry` imports (verify no other references first).
157
+
158
+ ### `src/index.ts`
159
+
160
+ 1. Move `exec` and `registry` from the `AgentManager` constructor call to `ConcreteAgentRunner`:
161
+ `new ConcreteAgentRunner({ io: runnerIO, exec: ..., registry })`.
162
+ 2. Remove `exec` and `registry` from the `AgentManager({...})` constructor argument.
163
+
164
+ ### `test/lifecycle/agent-runner.test.ts`
165
+
166
+ 1. Update all `runAgent(..., io)` calls to `runAgent(..., { io, exec, registry: mockAgentLookup })`.
167
+ 2. Remove `exec` and `registry` from `context:` objects in `RunOptions`.
168
+ `context: { exec, registry: mockAgentLookup }` → `context: {}` or `{}`.
169
+
170
+ ### `test/lifecycle/agent-runner-extension-tools.test.ts`
171
+
172
+ 1. Same pattern as `agent-runner.test.ts`: update `runAgent(..., io)` last param and strip `exec`/`registry` from `context:`.
173
+
174
+ ### `test/lifecycle/concrete-agent-runner.test.ts`
175
+
176
+ 1. Update `new ConcreteAgentRunner(io)` → `new ConcreteAgentRunner({ io, exec: vi.fn(), registry })`.
177
+ 2. Remove `exec` and `registry` from the `context:` in `runner.run()` call options.
178
+
179
+ ### `test/lifecycle/agent-manager.test.ts`
180
+
181
+ 1. Remove `exec: vi.fn()` and `registry: testRegistry` from `createManager()` factory.
182
+ 2. Remove the `testRegistry` construction and `AgentTypeRegistry` import if no other references exist.
183
+
184
+ ### `test/helpers/runner-io.ts`
185
+
186
+ 1. No structural changes needed — `createRunnerIO()` returns the `RunnerIO` shape, which is unchanged.
187
+ However, add a `createRunnerDeps()` convenience factory that bundles `{ io: createRunnerIO(), exec: vi.fn(), registry: createAgentLookup() }` for runner test files.
188
+
189
+ ### `docs/architecture/architecture.md`
190
+
191
+ 1. Update the `RunContext` code block in § "RunOptions (12 fields → extract RunContext)" to show only `cwd` and `parentSession`.
192
+ 2. Update the field-count description (4 → 2 per-call fields).
193
+ 3. Mark Step 3 as complete in the Phase 15 roadmap.
194
+
195
+ ## Test Impact Analysis
196
+
197
+ 1. No new test surfaces are needed — this is a pure mechanical refactoring (moving constructor parameters).
198
+ The existing runner and manager test suites fully cover the behavior.
199
+ 2. No existing tests become redundant — all tests exercise the same interactions, just with deps flowing through a different path.
200
+ 3. Existing `agent-manager.test.ts` tests remain as-is in coverage scope.
201
+ They verify `AgentManager` behavior (spawning, queueing, abort, etc.) independent of runner deps.
202
+ 4. Existing `agent-runner.test.ts` and `concrete-agent-runner.test.ts` tests remain.
203
+ They verify `runAgent()` and `ConcreteAgentRunner` behavior.
204
+ Call-site patterns change but assertions stay the same.
205
+
206
+ ## TDD Order
207
+
208
+ 1. **Add `RunnerDeps` interface and update `runAgent()` parameter** — define `RunnerDeps`, change `runAgent()`'s last param from `io` to `deps`, update internal references.
209
+ Update `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts` call sites.
210
+ Commit: `refactor: add RunnerDeps and update runAgent parameter (#231)`
211
+
212
+ 2. **Update `ConcreteAgentRunner` to accept `RunnerDeps`** — change constructor from `RunnerIO` to `RunnerDeps`, update `.run()` to pass `this.deps`.
213
+ Update `concrete-agent-runner.test.ts`.
214
+ Add `createRunnerDeps()` helper to `test/helpers/runner-io.ts`.
215
+ Commit: `refactor: ConcreteAgentRunner accepts RunnerDeps (#231)`
216
+
217
+ 3. **Remove `exec` and `registry` from `RunContext`** — shrink the interface to 2 fields, update doc comment.
218
+ Strip `exec`/`registry` from `context:` in all runner test call sites.
219
+ Run `pnpm run check` to verify no stale references.
220
+ Commit: `refactor: remove exec and registry from RunContext (#231)`
221
+
222
+ 4. **Remove `exec` and `registry` from `AgentManager`** — remove from `AgentManagerOptions`, remove class fields, remove relay in `startAgent()`, clean up imports.
223
+ Update `agent-manager.test.ts` factory.
224
+ Commit: `refactor: remove relay deps from AgentManager (#231)`
225
+
226
+ 5. **Update wiring in `index.ts`** — move `exec` and `registry` from `AgentManager` construction to `ConcreteAgentRunner` construction.
227
+ Commit: `refactor: wire exec and registry to ConcreteAgentRunner (#231)`
228
+
229
+ 6. **Update architecture docs** — update `RunContext` description and field counts, mark Step 3 complete.
230
+ Commit: `docs: update architecture for runner self-contained (#231)`
231
+
232
+ ## Risks and Mitigations
233
+
234
+ 1. **Test churn** — ~20 `runAgent()` call sites change their last parameter pattern.
235
+ Mitigation: mechanical find-and-replace; assertions stay identical.
236
+ 2. **Step ordering** — Steps 3 and 4 both remove `exec`/`registry` from different types.
237
+ If done in the wrong order, intermediate commits may not type-check.
238
+ Mitigation: Step 1–2 add the new path (`deps`), Step 3 removes from `RunContext` (runner side), Step 4 removes from `AgentManager` (manager side), Step 5 wires them together.
239
+ Each commit is independently valid.
240
+ 3. **Import cleanup** — removing `exec`/`registry` from `AgentManager` may leave unused imports (`ShellExec`, `AgentTypeRegistry`).
241
+ Mitigation: grep for other usages before removing; `pnpm run check` catches unused imports.
242
+
243
+ ## Open Questions
244
+
245
+ - None — the issue scope is narrow and the design is straightforward.
@@ -40,3 +40,41 @@ Test count: 986 → 1005.
40
40
  - Pre-completion reviewer returned WARN for stale `AgentRecord` and `run-handle.ts` references in `architecture.md` class diagram and layout listing.
41
41
  These were pre-existing staleness from #227's rename that wasn't fully propagated to Mermaid diagrams.
42
42
  Fixed by amending the docs commit.
43
+
44
+ ## Stage: Final Retrospective (2026-05-27T21:46:00Z)
45
+
46
+ ### Session summary
47
+
48
+ Completed all stages (plan, TDD, ship, retro) in a single session.
49
+ Dissolved `RunHandle` into 6 Agent methods, converted `startAgent` to async/await, released as `pi-subagents-v10.2.0`.
50
+ Test delta: 986 → 1005 (+19).
51
+
52
+ ### Observations
53
+
54
+ #### What went well
55
+
56
+ - The user's two redirecting questions during planning ("What's the change that makes this easier?"
57
+ and "Tell me more about RunHandle — is there something that should replace it?") transformed a mechanical "move the class" plan into a "dissolve the abstraction" plan.
58
+ The dissolve approach is architecturally superior and sets up #232 (resume unification) for free.
59
+ - The lift-and-shift decomposition (introduce Agent methods alongside `RunHandle`, then swap and delete) produced 5 independently-green commits.
60
+ The riskiest commit (step 3: delete `RunHandle`, -96/+6 lines) was trivially safe because step 2 had already proven the replacement methods.
61
+
62
+ #### What caused friction (agent side)
63
+
64
+ - `premature-convergence` — The agent planned around the issue's proposed `Agent.createRunHandle()` factory without questioning whether `RunHandle` should exist as a separate class.
65
+ The user had to ask two redirecting questions to push the analysis deeper.
66
+ Impact: plan was rewritten before commit (no wasted implementation), but the user spent two turns guiding analysis the agent should have done proactively.
67
+ - `missing-context` — Plan step 1 (narrow `Promise<string>` to `Promise<void>`) listed only `agent-manager.test.ts` for updates but missed 3 additional test files (`make-agent.test.ts`, `service-adapter.test.ts`, `get-result-tool.test.ts`) that construct `Promise<string>` values.
68
+ The testing skill says "grep for all test files" for type changes — this was not applied during planning.
69
+ Impact: caught by `pnpm run check` in the same step, no rework.
70
+
71
+ #### What caused friction (user side)
72
+
73
+ - No friction observed.
74
+ The user's questioning style (asking "what does it do?
75
+ who needs it?"
76
+ rather than prescribing the solution) was collaborative and effective.
77
+
78
+ ### Changes made
79
+
80
+ 1. `.pi/prompts/plan-issue.md` — added relocation-dissolution heuristic: when an issue proposes moving a class, list callers and fields touched to check if it should be dissolved into the owner instead.
@@ -0,0 +1,40 @@
1
+ ---
2
+ issue: 231
3
+ issue_title: "Push exec/registry relay deps to runner construction (Phase 15, Step 3)"
4
+ ---
5
+
6
+ # Retro: #231 — Push exec/registry relay deps to runner construction
7
+
8
+ ## Stage: Planning (2026-05-27T21:53:10Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 6-step TDD plan to move `exec` and `registry` from `AgentManager` to `ConcreteAgentRunner` via a new `RunnerDeps` interface.
13
+ The plan keeps `RunContext` (shrunk to 2 per-call fields) rather than dissolving it — #229 will likely dissolve it when `Agent.run()` calls the runner directly.
14
+
15
+ ### Observations
16
+
17
+ - Confirmed `exec` and `registry` are pure relay deps on `AgentManager` — stored at construction, used only at lines 193–194 to forward into `runner.run()`.
18
+ - Chose `RunnerDeps` bag over separate positional params on `ConcreteAgentRunner` and `runAgent()` — groups all three runner-owned deps (`io`, `exec`, `registry`) in one interface, and `runAgent()` stays at 5 parameters.
19
+ - `AgentManagerOptions.registry` uses the concrete `AgentTypeRegistry` class; `RunContext.registry` uses the narrow `AgentConfigLookup` interface.
20
+ The new `RunnerDeps.registry` uses `AgentConfigLookup` (ISP).
21
+ - Test churn is moderate (~20 `runAgent()` call sites change last param pattern) but mechanical — assertions stay identical.
22
+ - Added a `createRunnerDeps()` test helper to `runner-io.ts` to reduce per-file boilerplate in runner tests.
23
+
24
+ ## Stage: Implementation — TDD (2026-05-27T22:05:32Z)
25
+
26
+ ### Session summary
27
+
28
+ Implemented the 6-step plan in 4 commits (steps 3–5 merged).
29
+ All 1005 tests pass; no test count change.
30
+ Pre-completion reviewer returned PASS.
31
+
32
+ ### Observations
33
+
34
+ - Plan steps 3, 4, and 5 could not be separate commits: removing `exec`/`registry` from `RunContext` (step 3) immediately caused TypeScript excess-property errors in `AgentManager` (step 4) and `index.ts` (step 5).
35
+ Merged all three into one commit.
36
+ The testing skill’s rule “when a TDD step changes an interface that has a single call site, the step must include updating that call site” applies.
37
+ - Shrinking `RunContext` to all-optional fields made pre-existing `as never` casts in `test/helpers/manager-stubs.test.ts` unnecessary (eslint `no-unnecessary-type-assertion`).
38
+ Fixed as a lint cleanup in the doc commit.
39
+ - The `sed`-based bulk replacement for `runAgent(..., io)` → `runAgent(..., { io, exec, registry: mockAgentLookup })` missed one multi-line call site (the `rejects.toThrow` test wrapping the call in `expect()`).
40
+ Caught immediately by the test run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "10.2.0",
3
+ "version": "10.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ import { AgentTypeRegistry } from "#src/config/agent-types";
24
24
  import { loadCustomAgents } from "#src/config/custom-agents";
25
25
  import { SessionLifecycleHandler, ToolStartHandler } from "#src/handlers/index";
26
26
  import { AgentManager, type AgentManagerObserver } from "#src/lifecycle/agent-manager";
27
- import { ConcreteAgentRunner, type RunnerIO } from "#src/lifecycle/agent-runner";
27
+ import { ConcreteAgentRunner, type RunnerDeps } from "#src/lifecycle/agent-runner";
28
28
  import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
29
29
  import { GitWorktreeManager } from "#src/lifecycle/worktree";
30
30
  import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
@@ -132,25 +132,27 @@ export default function (pi: ExtensionAPI) {
132
132
  },
133
133
  };
134
134
 
135
- const runnerIO: RunnerIO = {
136
- detectEnv,
137
- getAgentDir,
138
- createResourceLoader: (opts) => new DefaultResourceLoader(opts),
139
- deriveSessionDir: deriveSubagentSessionDir,
140
- createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
141
- createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
142
- createSession: (opts) => createAgentSession(opts as any),
143
- assemblerIO: {
144
- preloadSkills,
145
- buildAgentPrompt,
135
+ const runnerDeps: RunnerDeps = {
136
+ io: {
137
+ detectEnv,
138
+ getAgentDir,
139
+ createResourceLoader: (opts) => new DefaultResourceLoader(opts),
140
+ deriveSessionDir: deriveSubagentSessionDir,
141
+ createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
142
+ createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
143
+ createSession: (opts) => createAgentSession(opts as any),
144
+ assemblerIO: {
145
+ preloadSkills,
146
+ buildAgentPrompt,
147
+ },
146
148
  },
149
+ exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
150
+ registry,
147
151
  };
148
152
 
149
153
  const manager = new AgentManager({
150
- runner: new ConcreteAgentRunner(runnerIO),
154
+ runner: new ConcreteAgentRunner(runnerDeps),
151
155
  worktrees: new GitWorktreeManager(process.cwd()),
152
- exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
153
- registry,
154
156
  observer,
155
157
  getMaxConcurrent: () => settings.maxConcurrent,
156
158
  getRunConfig: () => settings,
@@ -9,7 +9,6 @@
9
9
  import { randomUUID } from "node:crypto";
10
10
  import type { Model } from "@earendil-works/pi-ai";
11
11
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
- import { AgentTypeRegistry } from "#src/config/agent-types";
13
12
  import { debugLog } from "#src/debug";
14
13
  import { Agent } from "#src/lifecycle/agent";
15
14
  import type { AgentRunner } from "#src/lifecycle/agent-runner";
@@ -19,7 +18,7 @@ import type { WorktreeManager } from "#src/lifecycle/worktree";
19
18
  import { NotificationState } from "#src/observation/notification-state";
20
19
  import { subscribeAgentObserver } from "#src/observation/record-observer";
21
20
  import type { RunConfig } from "#src/runtime";
22
- import type { AgentInvocation, IsolationMode, ShellExec, SubagentType, ThinkingLevel } from "#src/types";
21
+ import type { AgentInvocation, IsolationMode, SubagentType, ThinkingLevel } from "#src/types";
23
22
 
24
23
  export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };
25
24
 
@@ -38,8 +37,6 @@ const DEFAULT_MAX_CONCURRENT = 4;
38
37
  export interface AgentManagerOptions {
39
38
  runner: AgentRunner;
40
39
  worktrees: WorktreeManager;
41
- exec: ShellExec;
42
- registry: AgentTypeRegistry;
43
40
  /** Injected getter for the concurrency limit - owned by SettingsManager. */
44
41
  getMaxConcurrent?: () => number;
45
42
  getRunConfig?: () => RunConfig;
@@ -94,8 +91,6 @@ export class AgentManager {
94
91
  private readonly observer?: AgentManagerObserver;
95
92
  private readonly runner: AgentRunner;
96
93
  private readonly worktrees: WorktreeManager;
97
- private readonly exec: ShellExec;
98
- private readonly registry: AgentTypeRegistry;
99
94
  private readonly _getMaxConcurrent: () => number;
100
95
  private getRunConfig?: () => RunConfig;
101
96
 
@@ -106,8 +101,6 @@ export class AgentManager {
106
101
  constructor(options: AgentManagerOptions) {
107
102
  this.runner = options.runner;
108
103
  this.worktrees = options.worktrees;
109
- this.exec = options.exec;
110
- this.registry = options.registry;
111
104
  this.observer = options.observer;
112
105
  this.getRunConfig = options.getRunConfig;
113
106
  this._getMaxConcurrent = options.getMaxConcurrent ?? (() => DEFAULT_MAX_CONCURRENT);
@@ -190,8 +183,6 @@ export class AgentManager {
190
183
  try {
191
184
  const result = await this.runner.run(snapshot, type, prompt, {
192
185
  context: {
193
- exec: this.exec,
194
- registry: this.registry,
195
186
  cwd: record.worktreeState?.path,
196
187
  parentSession: options.parentSession,
197
188
  },
@@ -114,19 +114,27 @@ export interface SessionFactoryIO {
114
114
  */
115
115
  export type RunnerIO = EnvironmentIO & SessionFactoryIO;
116
116
 
117
+ /**
118
+ * Dependencies owned by the runner — injected at construction time.
119
+ *
120
+ * Groups the IO boundary with the two static domain deps (exec, registry)
121
+ * that every run() call needs but that do not vary per call.
122
+ */
123
+ export interface RunnerDeps {
124
+ io: RunnerIO;
125
+ exec: ShellExec;
126
+ registry: AgentConfigLookup;
127
+ }
128
+
117
129
  // ── Public interfaces ─────────────────────────────────────────────────────────
118
130
 
119
131
  /**
120
- * Parent execution context - where/who is running.
132
+ * Per-call execution context fields that vary per spawn.
121
133
  *
122
- * Groups the four fields that describe the parent environment and identity,
123
- * separating them from the per-call execution parameters in RunOptions.
134
+ * Static dependencies (exec, registry) live on RunnerDeps; this interface
135
+ * carries only the two per-call fields that AgentManager supplies at spawn time.
124
136
  */
125
137
  export interface RunContext {
126
- /** Shell-exec callback for detectEnv - injected from pi.exec(). */
127
- exec: ShellExec;
128
- /** Agent config lookup - provides resolveAgentConfig and getToolNamesForType. */
129
- registry: AgentConfigLookup;
130
138
  /** Override working directory (e.g. for worktree isolation). */
131
139
  cwd?: string;
132
140
  /** Parent session identity (file path + session ID). */
@@ -182,15 +190,16 @@ export interface AgentRunner {
182
190
  }
183
191
 
184
192
  /**
185
- * Concrete AgentRunner backed by a RunnerIO boundary.
193
+ * Concrete AgentRunner backed by RunnerDeps.
186
194
  *
187
- * Captures io at construction time so AgentManager remains IO-unaware.
195
+ * Captures IO, exec, and registry at construction time so AgentManager
196
+ * remains unaware of runner-internal dependencies.
188
197
  */
189
198
  export class ConcreteAgentRunner implements AgentRunner {
190
- constructor(private readonly io: RunnerIO) {}
199
+ constructor(private readonly deps: RunnerDeps) {}
191
200
 
192
201
  run(snapshot: ParentSnapshot, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult> {
193
- return runAgent(snapshot, type, prompt, options, this.io);
202
+ return runAgent(snapshot, type, prompt, options, this.deps);
194
203
  }
195
204
 
196
205
  resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string> {
@@ -253,11 +262,11 @@ export async function runAgent(
253
262
  type: SubagentType,
254
263
  prompt: string,
255
264
  options: RunOptions,
256
- io: RunnerIO,
265
+ deps: RunnerDeps,
257
266
  ): Promise<RunResult> {
258
267
  // Resolve working directory upfront - needed for detectEnv before assembly.
259
268
  const effectiveCwd = options.context.cwd ?? snapshot.cwd;
260
- const env = await io.detectEnv(options.context.exec, effectiveCwd);
269
+ const env = await deps.io.detectEnv(deps.exec, effectiveCwd);
261
270
 
262
271
  // Assemble session configuration (synchronous, no SDK objects).
263
272
  const cfg = assembleSessionConfig(
@@ -275,11 +284,11 @@ export async function runAgent(
275
284
  thinkingLevel: options.thinkingLevel,
276
285
  },
277
286
  env,
278
- options.context.registry,
279
- io.assemblerIO,
287
+ deps.registry,
288
+ deps.io.assemblerIO,
280
289
  );
281
290
 
282
- const agentDir = io.getAgentDir();
291
+ const agentDir = deps.io.getAgentDir();
283
292
 
284
293
  // Load extensions/skills: true → load; false → don't.
285
294
  // Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md - upstream's
@@ -287,7 +296,7 @@ export async function runAgent(
287
296
  // would defeat prompt_mode: replace and isolated: true. Parent context, if
288
297
  // wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
289
298
  // is embedded in systemPromptOverride) or inherit_context (conversation).
290
- const loader = io.createResourceLoader({
299
+ const loader = deps.io.createResourceLoader({
291
300
  cwd: cfg.effectiveCwd,
292
301
  agentDir,
293
302
  noExtensions: !cfg.extensions,
@@ -303,15 +312,15 @@ export async function runAgent(
303
312
  // Create a persisted SessionManager so transcripts are written in Pi's
304
313
  // official JSONL format. Falls back to a temp directory when the parent
305
314
  // session is not persisted (e.g. headless/API mode).
306
- const sessionDir = io.deriveSessionDir(options.context.parentSession?.parentSessionFile, cfg.effectiveCwd);
307
- const sessionManager = io.createSessionManager(cfg.effectiveCwd, sessionDir);
315
+ const sessionDir = deps.io.deriveSessionDir(options.context.parentSession?.parentSessionFile, cfg.effectiveCwd);
316
+ const sessionManager = deps.io.createSessionManager(cfg.effectiveCwd, sessionDir);
308
317
  sessionManager.newSession({ parentSession: options.context.parentSession?.parentSessionId });
309
318
 
310
- const { session } = await io.createSession({
319
+ const { session } = await deps.io.createSession({
311
320
  cwd: cfg.effectiveCwd,
312
321
  agentDir,
313
322
  sessionManager,
314
- settingsManager: io.createSettingsManager(cfg.effectiveCwd, agentDir),
323
+ settingsManager: deps.io.createSettingsManager(cfg.effectiveCwd, agentDir),
315
324
  modelRegistry: snapshot.modelRegistry,
316
325
  model: cfg.model,
317
326
  tools: cfg.toolNames,