@gotgenes/pi-subagents 11.2.0 → 11.4.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.
@@ -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
@@ -0,0 +1,98 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-05-29
4
+ ---
5
+
6
+ # 0002 — Workspaces and permissions are extensions on a minimal core
7
+
8
+ ## Status
9
+
10
+ Accepted.
11
+ Supersedes the "agent collaborator architecture" framing of Phase 16 (an abandoned exploration) and the work shipped under it: issue #256 (`WorktreeIsolation` as an `Agent` collaborator) and issue #257 (`ChildSessionFactory` extraction, parked at planning).
12
+ Reclaims Phase 16's original intent — "invert dependencies" — and extends it to evict worktree isolation from the core.
13
+
14
+ ## Context
15
+
16
+ The core question that triggered this decision: a single-method `ChildSessionFactory` with a `create(cwd?)` method (planned for #257) looked like it wanted to be a function, and the `cwd` parameter was late-bound.
17
+ Pulling that thread exposed progressively more rudimentary issues.
18
+
19
+ 1. `cwd` is late-bound because `WorktreeIsolation.setup()` is called lazily inside `Agent.run()`, after construction — a two-phase `construct-then-setup()` that violates design principle 8 ("Construct complete").
20
+ 2. The worktree is *ready* only at dequeue (a concurrency slot is held and `git worktree add` has run).
21
+ "Construct when ready" therefore means constructing the worktree at run-start, not at spawn — which dissolves the lazy `setup()` and makes `cwd` knowable at construction.
22
+ 3. The worktree and the child session share one lifespan: both are born at run-start and torn down at completion (the worktree's cleanup saves a branch; the session is disposed).
23
+ Resources with one lifetime are one resource, not sibling collaborators that `Agent` must sequence.
24
+ The `create(cwd?)` parameter only existed because we split one run-scoped resource (the worktree) out and made `Agent` relay its output back in.
25
+ 4. Worktrees are not intrinsic to what makes subagents useful.
26
+ The maintainer never uses them (WIP-of-1, trunk-based, CI/CD).
27
+ Git worktree isolation is one *strategy* for answering "where does this child run, and what brackets the run?"
28
+ — a container, a throwaway tmpdir, or a remote sandbox are others.
29
+ The core needs only *a working directory and a disposal hook*; the default (the parent's cwd, no setup/teardown) is always correct.
30
+ 5. This mirrors Phase 14, which evicted tool/extension *policy* (`disallowed_tools`, `extensions` filtering) to `@gotgenes/pi-permission-system`.
31
+ Worktrees are *environment* policy; they belong outside the core for the same reason.
32
+
33
+ Permissions and workspaces are orthogonal concerns that must compose as independent extensions on the core, never knowing about each other.
34
+
35
+ ## Decision
36
+
37
+ pi-subagents is a minimal orchestrator: it spawns a child session derived from the parent, runs the turn loop, tracks and streams and collects the result, gates concurrency, supports resume, and **publishes its lifecycle**.
38
+ Everything else attaches through exactly two extension surfaces, distinguished by the direction of information flow.
39
+
40
+ ### Two extension surfaces
41
+
42
+ 1. **Lifecycle events (observational) — unlimited.**
43
+ The core emits awaited, ordered events for the child-execution lifecycle (`spawning`, `session-created` pre-`bindExtensions`, `completed`, `disposed`).
44
+ Any number of extensions subscribe; handlers return nothing.
45
+ Reactive concerns live here: permission detection, telemetry, UI, notifications.
46
+ Adding a reactive concern never modifies the core.
47
+
48
+ 2. **Provider seams (generative) — rationed.**
49
+ The rare concern that must *inject* a value the core consumes synchronously registers a provider the core consults.
50
+ Today there is exactly one: the **workspace provider** (it returns the child's working directory plus bracketed setup/teardown).
51
+ A provider seam is the only place the core is "open," so the list is kept as small as possible.
52
+
53
+ ### The discriminator
54
+
55
+ When deciding how a concern attaches:
56
+
57
+ - It only needs to **know** what happened → subscribe to a lifecycle event (observational, unlimited).
58
+ - It must **return a value the core consumes** → register a provider (generative, rationed).
59
+
60
+ Permissions are observational: the core does not enforce policy; it publishes the child's identity at the pre-bind instant so the permission extension (loaded in the child) can detect "am I a subagent?"
61
+ and gate tool calls at runtime.
62
+ Workspaces are generative: the core cannot default the cwd away when an isolation strategy is requested, so the provider hands it back.
63
+
64
+ ### The governing rule: no vacant hooks
65
+
66
+ The architecture must *admit* a seam without *shipping* it until a concrete consumer exists.
67
+ A provider seam with no consumer is not extensibility — it is a speculative abstraction that taxes every reader, and `fallow` flags it as dead.
68
+ Latent extensibility (the design can host the seam additively) is the deliverable; a vacant hook is not.
69
+
70
+ ### What leaves the core
71
+
72
+ - **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) → a new package, `@gotgenes/pi-subagents-worktrees`, that implements the workspace provider and owns the git plumbing and the "saved to branch" result.
73
+ - **`permission-bridge.ts`** → retired.
74
+ The core stops reaching *out* to `Symbol.for("@gotgenes/pi-permission-system:service")` and instead *emits* lifecycle events the permission system subscribes to.
75
+ - **`isolated` / `extensions: false` / `noSkills`** → removed.
76
+ Deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
77
+ Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and cannot be reduced to observation, so it is left as a *latent* (un-built) provider seam, added only if a real consumer needs it.
78
+
79
+ ### What stays in the core (not policy)
80
+
81
+ - The **recursion guard** (stripping the core's own `subagent` / `get_subagent_result` / `steer_subagent` tools from children).
82
+ It defends the core's own invariant — a subagent must not recursively spawn — keyed off the core's own tool names.
83
+ With `isolated` gone, children always load the parent's resources, so the guard becomes unconditional rather than gated on `cfg.extensions`.
84
+
85
+ ### Composition test
86
+
87
+ Install neither extension, only permissions, only workspaces, or both: the core is byte-for-byte identical in all four cases, and the two extensions never reference each other.
88
+ Permissions depend only on the core's events; workspaces depend only on the core's provider seam; the core depends on neither.
89
+
90
+ ## Consequences
91
+
92
+ - The "agent collaborator architecture" Phase 16 (give `Agent` a worktree collaborator + a session factory) is abandoned.
93
+ #256 is superseded (worktree was placed in the wrong layer); #257 is parked (it polished a subsystem slated for eviction).
94
+ - A new package `@gotgenes/pi-subagents-worktrees` is introduced; the core spawn API drops `isolation` and `isolated`.
95
+ - `permission-bridge.ts` is removed; `@gotgenes/pi-permission-system` migrates from a published-service lookup to lifecycle-event subscription, which requires the core to emit an awaited, ordered `session-created` event before `bindExtensions()`.
96
+ Confirming Pi's event model supports awaited pre-bind emission is the first investigation of the reclaimed phase.
97
+ - Once the cwd is resolved through the provider seam rather than relayed by `Agent`, child-session creation can construct a born-complete execution and the "runner" concept dissolves — recovering the structural goal of the abandoned collaborator steps by a cleaner route.
98
+ - The reclaimed Phase 16 roadmap and step issues live in [`docs/architecture/architecture.md`](../architecture/architecture.md).
@@ -0,0 +1,256 @@
1
+ ---
2
+ issue: 256
3
+ issue_title: "Extract WorktreeIsolation collaborator"
4
+ ---
5
+
6
+ # Extract WorktreeIsolation collaborator
7
+
8
+ ## Problem Statement
9
+
10
+ `Agent` currently holds three separate worktree-related members — `_worktrees` (a shared `WorktreeManager`), `_isolation` (the `IsolationMode`), and `worktreeState` (a `WorktreeState` phase object) — and orchestrates the worktree internals itself.
11
+ It checks `this._isolation !== "worktree"`, calls `this._worktrees.create()`, constructs the `WorktreeState`, and drives `worktreeState.performCleanup(this._worktrees, ...)` from both `completeRun()` and `failRun()`.
12
+ This is Ask-Don't-Tell: `Agent` asks its collaborators for raw materials and does the worktree work itself rather than telling a single collaborator to handle its own lifecycle.
13
+
14
+ This is Phase 16, Step 1 of the agent-collaborator architecture (`docs/architecture/architecture.md`).
15
+
16
+ ## Goals
17
+
18
+ - Introduce a `WorktreeIsolation` collaborator that owns the entire worktree lifecycle: `setup()`, `path` access, and `cleanup(description)`.
19
+ - `AgentManager` constructs the collaborator only when `isolation === "worktree"` and passes it to `Agent` ready to go.
20
+ - Replace `Agent`'s mode check (`this._isolation !== "worktree"`) with a null check (`this.worktree?.setup()`).
21
+ - Fold the existing `WorktreeState` value object into `WorktreeIsolation` (delete `worktree-state.ts`), matching the architecture's target table which lists `WorktreeIsolation` as absorbing `worktrees` + `isolation` + `worktreeState`.
22
+ - Shrink `Agent`: remove `_worktrees`, `_isolation`, `worktreeState`, and `setupWorktree()`; add a single `worktree?: WorktreeIsolation` collaborator.
23
+
24
+ This change is **not** breaking to any published API — `WorktreeManager`, `WorktreeState`, and `AgentInit` are all internal to the package.
25
+
26
+ ## Non-Goals
27
+
28
+ - No changes to the runner, session creation, or `ChildSessionFactory` — that is Step 2 (#257).
29
+ - No changes to `Agent.run()`'s session-interaction logic, turn-limit enforcement, or response collection — that is Step 3 (#258).
30
+ - No changes to the low-level git plumbing in `worktree.ts` (`createWorktree`, `cleanupWorktree`, `pruneWorktrees`, `GitWorktreeManager`) — those free functions and the `WorktreeManager` interface stay as-is.
31
+ - No change to the `worktreeResult` shape exposed by `service-adapter.ts` — only the access path changes.
32
+
33
+ ## Background
34
+
35
+ Relevant modules:
36
+
37
+ - `src/lifecycle/agent.ts` — the `Agent` class.
38
+ Holds `_worktrees: WorktreeManager`, `_isolation: IsolationMode`, `worktreeState?: WorktreeState`; defines `setupWorktree()`; reads `this.worktreeState?.path` for the runner `cwd`; drives cleanup in `completeRun()` / `failRun()`.
39
+ - `src/lifecycle/agent-manager.ts` — `AgentManager` holds the shared `WorktreeManager` (`this.worktrees`), passes `worktrees` + `isolation` into each `Agent` via `AgentInit`, and calls `this.worktrees.prune()` on `dispose()`.
40
+ - `src/lifecycle/worktree.ts` — `WorktreeManager` interface + `GitWorktreeManager` impl + free functions.
41
+ `WorktreeManager.cleanup(wt, description)` mutates `wt.branch` in place (in `cleanupWorktree`), so the object passed must carry a writable `branch`.
42
+ - `src/lifecycle/worktree-state.ts` — `WorktreeState`: holds `path`/`branch`, tracks `cleanupResult`, exposes `performCleanup(worktrees, description)`.
43
+ Re-exports `WorktreeCleanupResult` and `WorktreeInfo` (no external consumer imports those two from this path — verified by grep).
44
+ - `src/service/service-adapter.ts:131` — reads `record.worktreeState?.cleanupResult` to populate `worktreeResult`.
45
+ - `src/index.ts:167` — constructs `new GitWorktreeManager(process.cwd())` and passes it to `AgentManager`.
46
+
47
+ AGENTS.md constraints that apply:
48
+
49
+ - This package targets ES2024; Biome (not Prettier) formats.
50
+ - Tests use `vi.hoisted(...)` patterns; the full vitest suite must pass before publishing.
51
+ - When a barrel/module gains exports, verify a consumer imports them — fallow flags speculative re-exports.
52
+ Here we are removing a module, not adding one, so the risk is dangling imports rather than dead exports.
53
+
54
+ ## Design Overview
55
+
56
+ ### Decision model
57
+
58
+ `AgentManager` owns the shared `WorktreeManager` (one instance, repo-root-bound).
59
+ Per spawn, when `isolation === "worktree"`, it constructs a per-agent `WorktreeIsolation` bound to that `WorktreeManager` and the agent id, and hands it to `Agent`.
60
+ When isolation is not requested, no collaborator is created and `Agent.worktree` is `undefined`.
61
+
62
+ `Agent` no longer knows the isolation mode or the `WorktreeManager`.
63
+ The presence/absence of the collaborator *is* the mode: `this.worktree?.setup()` and `this.worktree?.cleanup(...)`.
64
+
65
+ ### WorktreeIsolation shape
66
+
67
+ ```typescript
68
+ // src/lifecycle/worktree-isolation.ts
69
+ import type { WorktreeCleanupResult, WorktreeInfo, WorktreeManager } from "#src/lifecycle/worktree";
70
+
71
+ export class WorktreeIsolation {
72
+ private _info?: WorktreeInfo;
73
+ private _cleanupResult?: WorktreeCleanupResult;
74
+
75
+ constructor(
76
+ private readonly worktrees: WorktreeManager,
77
+ private readonly agentId: string,
78
+ ) {}
79
+
80
+ /** Absolute worktree path — undefined before setup(). */
81
+ get path(): string | undefined {
82
+ return this._info?.path;
83
+ }
84
+
85
+ /** Cleanup outcome — undefined until cleanup() runs. */
86
+ get cleanupResult(): WorktreeCleanupResult | undefined {
87
+ return this._cleanupResult;
88
+ }
89
+
90
+ /**
91
+ * Create the git worktree and store its info.
92
+ * Throws on failure (strict isolation — no silent fallback).
93
+ */
94
+ setup(): void {
95
+ const wt = this.worktrees.create(this.agentId);
96
+ if (!wt) {
97
+ throw new Error(
98
+ 'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
99
+ "Initialize git and commit at least once, or omit `isolation`.",
100
+ );
101
+ }
102
+ this._info = wt;
103
+ }
104
+
105
+ /** Perform cleanup and record the result. No-op ({ hasChanges: false }) if setup never ran. */
106
+ cleanup(description: string): WorktreeCleanupResult {
107
+ if (!this._info) return { hasChanges: false };
108
+ const result = this.worktrees.cleanup(this._info, description);
109
+ this._cleanupResult = result;
110
+ return result;
111
+ }
112
+ }
113
+ ```
114
+
115
+ Notes:
116
+
117
+ - `_info` is a mutable `WorktreeInfo`, so `WorktreeManager.cleanup` mutating `branch` in place keeps working (the same behavior `WorktreeState` relied on today).
118
+ - The `missing worktrees dependency` error from `setupWorktree()` disappears: the collaborator is only ever created with a `WorktreeManager`, so that defensive branch is structurally impossible.
119
+ - `cleanup()` returns `{ hasChanges: false }` when `setup()` never ran, so `Agent`'s `completeRun()`/`failRun()` can call it unconditionally via the optional-chain without a separate guard.
120
+
121
+ ### Agent call sites (Tell-Don't-Ask)
122
+
123
+ `Agent.run()` setup:
124
+
125
+ ```typescript
126
+ try {
127
+ this.worktree?.setup(); // was: this.setupWorktree() with internal mode check
128
+ } catch (err) {
129
+ this.markError(err);
130
+ this.releaseListeners();
131
+ this.observer?.onRunFinished?.(this);
132
+ return;
133
+ }
134
+ // ...
135
+ cwd: this.worktree?.path, // was: this.worktreeState?.path
136
+ ```
137
+
138
+ `Agent.completeRun()`:
139
+
140
+ ```typescript
141
+ let finalResult = result.responseText;
142
+ const wtResult = this.worktree?.cleanup(this.description);
143
+ if (wtResult?.hasChanges && wtResult.branch) {
144
+ finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
145
+ }
146
+ ```
147
+
148
+ `Agent.failRun()`:
149
+
150
+ ```typescript
151
+ try {
152
+ this.worktree?.cleanup(this.description);
153
+ } catch (cleanupErr) {
154
+ debugLog("cleanupWorktree on agent error", cleanupErr);
155
+ }
156
+ ```
157
+
158
+ `AgentManager.spawn()`:
159
+
160
+ ```typescript
161
+ const worktree = options.isolation === "worktree"
162
+ ? new WorktreeIsolation(this.worktrees, id)
163
+ : undefined;
164
+ const record = new Agent({ /* ... */, worktree /* was: worktrees + isolation */ });
165
+ ```
166
+
167
+ The reach-through `agent.worktreeState.cleanupResult` in `service-adapter.ts` becomes `agent.worktree?.cleanupResult` — the collaborator owns the result, so this is a single-hop access, not a reach-through into a phase object.
168
+
169
+ ### Edge cases
170
+
171
+ - Isolation not requested → `worktree` is `undefined` → `setup()`/`cleanup()` are skipped via optional chaining; behavior identical to today's `_isolation !== "worktree"` early-return.
172
+ - `create()` returns `undefined` (not a git repo) → `setup()` throws; `Agent.run()` catches, marks error, releases listeners, fires `onRunFinished`.
173
+ The existing AgentManager regression test (worktree fails loud, no silent fallback) is preserved.
174
+ - Cleanup throws in `failRun()` → caught and logged best-effort, identical to today.
175
+
176
+ ## Module-Level Changes
177
+
178
+ - New: `src/lifecycle/worktree-isolation.ts` — the `WorktreeIsolation` class (shape above).
179
+ - Changed: `src/lifecycle/agent.ts`
180
+ - Remove imports of `WorktreeManager` (type) and `WorktreeState`; add import of `WorktreeIsolation`.
181
+ - `AgentInit`: remove `worktrees?: WorktreeManager` and `isolation?: IsolationMode`; add `worktree?: WorktreeIsolation`.
182
+ (`IsolationMode` may remain imported if still referenced elsewhere in the file; grep confirms it is only used for the removed field — remove the now-unused import.)
183
+ - Remove instance fields `_worktrees`, `_isolation`, `worktreeState`; add `worktree?: WorktreeIsolation`.
184
+ - Remove the `setupWorktree()` method.
185
+ - Constructor: replace the `_worktrees`/`_isolation` assignments with `this.worktree = init.worktree`.
186
+ - `run()`: `this.worktree?.setup()`; `cwd: this.worktree?.path`.
187
+ - `completeRun()` / `failRun()`: replace the 4-line `worktreeState && _worktrees` blocks with `this.worktree?.cleanup(this.description)`.
188
+ - Update the file header doc comment (lists `worktreeState` as a phase-specific collaborator).
189
+ - Changed: `src/lifecycle/agent-manager.ts`
190
+ - Import `WorktreeIsolation`.
191
+ - `spawn()`: construct the per-agent `WorktreeIsolation` when `options.isolation === "worktree"`; pass `worktree` to `Agent` instead of `worktrees` + `isolation`.
192
+ - Keep `this.worktrees` field, `AgentManagerOptions.worktrees`, and the `dispose()` → `this.worktrees.prune()` call unchanged.
193
+ - Changed: `src/service/service-adapter.ts`
194
+ - `record.worktreeState?.cleanupResult` → `record.worktree?.cleanupResult`.
195
+ - Removed: `src/lifecycle/worktree-state.ts` (folded into `WorktreeIsolation`).
196
+ - Doc updates (`docs/architecture/architecture.md`):
197
+ - Class diagram (line ~115): `+worktreeState?: WorktreeState` → `+worktree?: WorktreeIsolation`; remove the `+setupWorktree(...)` method line.
198
+ - Layout listing (lines ~279–280): replace `worktree-state.ts worktree phase state` with `worktree-isolation.ts worktree lifecycle collaborator`.
199
+ - Doc update (`.pi/skills/package-pi-subagents/SKILL.md`): Lifecycle domain row — replace `worktree-state.ts` with `worktree-isolation.ts` (module count stays 9).
200
+
201
+ Symbols removed and their consumers (grepped across `src/` and `test/`):
202
+
203
+ - `WorktreeState` (class): `src/lifecycle/agent.ts` (removed in this plan), `test/lifecycle/agent.test.ts`, `test/service/service-adapter.test.ts`, `test/lifecycle/worktree-state.test.ts` — all updated/removed below.
204
+ - `Agent.setupWorktree()`: only `test/lifecycle/agent.test.ts` — removed below.
205
+ - `Agent.worktreeState`: `service-adapter.ts` + several tests — all migrated to `worktree`.
206
+ - The `WorktreeCleanupResult`/`WorktreeInfo` re-exports from `worktree-state.ts`: no external importer (verified) — safe to drop.
207
+
208
+ ## Test Impact Analysis
209
+
210
+ 1. New unit tests enabled by the extraction: `WorktreeIsolation` is now independently testable without an `Agent` — `worktree-isolation.test.ts` covers `setup()` (success stores path; failure throws), `cleanup()` (delegates to `worktrees.cleanup` with stored info + description, records `cleanupResult`; no-op before setup), and `path`/`cleanupResult` getters.
211
+ These absorb the existing `worktree-state.test.ts` coverage (constructor, `recordCleanup`, `performCleanup`) at the same granularity.
212
+ 2. Existing tests that become redundant / simplified: `test/lifecycle/worktree-state.test.ts` is removed (its behavior is covered by the new collaborator tests).
213
+ The `Agent — setupWorktree` describe block in `agent.test.ts` is removed (the method is gone); its intent migrates to the `WorktreeIsolation` unit tests plus the existing `Agent.run() — worktree` integration tests.
214
+ 3. Existing tests that must stay (genuinely exercise the layer):
215
+ `test/lifecycle/worktree.test.ts` (git plumbing + `GitWorktreeManager`) is untouched.
216
+ `Agent.run() — worktree` integration tests stay but switch their assertions from `agent.worktreeState` to `agent.worktree` and construct the agent with a `WorktreeIsolation`.
217
+ `agent-manager.test.ts` worktree tests stay but assert via `record.worktree?.path` / `record.worktree?.cleanupResult`.
218
+
219
+ ## TDD Order
220
+
221
+ 1. Add `WorktreeIsolation` with unit tests — new module, no consumers yet.
222
+ Surface: `test/lifecycle/worktree-isolation.test.ts`.
223
+ Covers: `setup()` success/failure, `cleanup()` delegation + result recording + pre-setup no-op, `path`/`cleanupResult` getters (migrating `worktree-state.test.ts` coverage).
224
+ Commit: `test: add WorktreeIsolation collaborator tests` then `feat(pi-subagents): add WorktreeIsolation collaborator`. (May be a single `feat` commit with the test if preferred — the module is self-contained.)
225
+ 2. Wire `WorktreeIsolation` into `Agent` and `AgentManager`; drop the old fields.
226
+ This is one commit because TypeScript will not accept `AgentInit` losing `worktrees`/`isolation` while call sites still pass them.
227
+ Changes: `agent.ts` (remove `_worktrees`/`_isolation`/`worktreeState`/`setupWorktree`, add `worktree`, update `run`/`completeRun`/`failRun`), `agent-manager.ts` (construct collaborator in `spawn`), `service-adapter.ts` (`record.worktree?.cleanupResult`), and their tests (`agent.test.ts` helpers `createRunnableAgent`/`createAgentWithWorktrees` + worktree describe blocks; remove the `setupWorktree` block; `agent-manager.test.ts` worktree assertions; `service-adapter.test.ts` setup).
228
+ Commit: `refactor(pi-subagents): Agent delegates worktree lifecycle to WorktreeIsolation`.
229
+ 3. Delete the now-orphaned `WorktreeState`.
230
+ Remove `src/lifecycle/worktree-state.ts` and `test/lifecycle/worktree-state.test.ts`; remove any remaining `WorktreeState` imports.
231
+ Run `pnpm fallow dead-code` to confirm no dangling exports.
232
+ Commit: `refactor(pi-subagents): remove WorktreeState, folded into WorktreeIsolation`.
233
+ 4. Update architecture doc + package skill.
234
+ `docs/architecture/architecture.md` class diagram + layout listing; `SKILL.md` Lifecycle domain row.
235
+ Commit: `docs(pi-subagents): reflect WorktreeIsolation extraction in architecture`.
236
+
237
+ After all steps: `pnpm run check`, `pnpm run lint`, `pnpm -r run test`, `pnpm fallow dead-code`.
238
+
239
+ ## Risks and Mitigations
240
+
241
+ - Risk: `WorktreeManager.cleanup` mutates `branch` in place; folding `WorktreeState` could lose that behavior.
242
+ Mitigation: `WorktreeIsolation` stores a mutable `WorktreeInfo` (`_info`) and passes it directly to `cleanup`, preserving the in-place mutation.
243
+ - Risk: a hidden consumer imports `WorktreeCleanupResult`/`WorktreeInfo` from `worktree-state.ts`.
244
+ Mitigation: grep confirms all consumers import those types from `worktree.ts`; the deletion step re-runs the grep and `pnpm run check`.
245
+ - Risk: the combined Step 2 commit touches several test files at once.
246
+ Mitigation: the changes are mechanical and localized to worktree-specific helpers/describe blocks; the type checker pinpoints every call site.
247
+ The bulk of `agent.test.ts` is untouched.
248
+ - Risk: AgentManager's `dispose()` prune path relies on `this.worktrees`.
249
+ Mitigation: `AgentManager` keeps ownership of the shared `WorktreeManager`; only per-agent collaborator construction is added.
250
+
251
+ ## Open Questions
252
+
253
+ - Whether `setup()` should return the path (as `setupWorktree()` did) for symmetry.
254
+ Deferred: no caller needs the return value once `Agent` reads `this.worktree?.path`; keep `setup(): void` until a consumer needs otherwise.
255
+ - Whether `WorktreeIsolation` should later absorb the parent `cwd`/repo-root concern from `GitWorktreeManager`.
256
+ Deferred to the broader Phase 16 collaborator work; out of scope for Step 1.