@gotgenes/pi-subagents 11.1.0 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/docs/architecture/architecture.md +229 -161
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +180 -0
- package/docs/plans/0256-extract-worktree-isolation.md +256 -0
- package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +109 -0
- package/docs/retro/0256-extract-worktree-isolation.md +45 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +10 -25
- package/src/lifecycle/agent.ts +52 -45
- package/src/lifecycle/worktree-isolation.ts +59 -0
- package/src/service/service-adapter.ts +1 -1
- package/src/lifecycle/worktree-state.ts +0 -45
|
@@ -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,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 232
|
|
3
|
+
issue_title: "Agent.resume() with internal observer lifecycle (Phase 15, Step 6)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent.resume() with internal observer lifecycle
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
After #229 (`Agent.run()` absorbs `startAgent`), the agent owns its entire run lifecycle but `AgentManager.resume()` still duplicates the observer subscribe/use/release pattern that `run()` handles internally.
|
|
11
|
+
The manager manually calls `subscribeAgentObserver`, wraps `runner.resume()` in a try/catch/finally, marks completion/error, and unsubscribes — the same acquire → use → release resource shape `Agent.run()` already encapsulates.
|
|
12
|
+
This is the last "manager reaches into Agent" duplication in the Phase 15 roadmap (priority 8, smell A: redundant pattern).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Add `Agent.resume(prompt, signal?)` that owns its observer subscription lifecycle, mirroring `run()`'s internal wiring.
|
|
17
|
+
- Reduce `AgentManager.resume()` to a guard-plus-delegation method (no `subscribeAgentObserver`, no try/finally).
|
|
18
|
+
- Preserve the existing public contract of `AgentManager.resume()` exactly: same signature, same `Agent | undefined` return, same behavior when the record or session is missing.
|
|
19
|
+
- Keep the change non-breaking (`feat:`, not `feat!:`).
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- No change to `runner.resume()` / `resumeAgent()` in `agent-runner.ts`.
|
|
24
|
+
- No change to the abort semantics of resume — the parent `signal` continues to flow straight through to `runner.resume({ signal })` (resume does not route through the agent's `abortController`, matching today's behavior).
|
|
25
|
+
- No queue interaction on resume — resume is not subject to the concurrency queue, so `onStarted`/`onRunFinished` are not fired (unchanged from today).
|
|
26
|
+
- No full rewrite of the stale `AgentManager`/`Agent` class diagram in `architecture.md` — that diagram already diverged in #229 (missing `run()`, stale `setupWorktree`/`completeRun`/`setOnRunFinished` signatures); a comprehensive diagram refresh is out of scope here.
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
Relevant modules (all under `packages/pi-subagents/src/`):
|
|
31
|
+
|
|
32
|
+
- `lifecycle/agent.ts` — the `Agent` class.
|
|
33
|
+
Already owns the per-run listener state (`_unsub`, `_detachFn`), the `attachObserver(unsub)` / `releaseListeners()` pair, `resetForResume(startedAt)` (which calls `releaseListeners()`), and `markCompleted` / `markError`.
|
|
34
|
+
Holds `_runner` and `observer` (an `AgentLifecycleObserver`) from construction (#229).
|
|
35
|
+
`Agent.run()` is the template to follow: it wires the observer via `attachObserver(subscribeAgentObserver(session, this, { onCompact: (r, info) => this.observer?.onCompacted?.(r, info) }))`.
|
|
36
|
+
- `lifecycle/agent-manager.ts` — `AgentManager.resume()` currently does the manual subscribe/try-finally dance and imports `subscribeAgentObserver` solely for that.
|
|
37
|
+
- `observation/record-observer.ts` — `subscribeAgentObserver(session, record, options)` returns an unsubscribe function; observes `tool_execution_end`, `message_end`, `compaction_end`.
|
|
38
|
+
- `lifecycle/agent-runner.ts` — `AgentRunner.resume(session, prompt, options?)` returns `Promise<string>` (the response text).
|
|
39
|
+
|
|
40
|
+
Constraint from AGENTS.md / `package-pi-subagents` skill: pi-subagents is a narrow core; this is a pure internal refactor (Tell-Don't-Ask, "state owns its mutations") with no policy or API-surface change.
|
|
41
|
+
|
|
42
|
+
### Observer routing equivalence
|
|
43
|
+
|
|
44
|
+
The manager's old resume wired compaction to the `AgentManagerObserver`:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
subscribeAgentObserver(session, record, {
|
|
48
|
+
onCompact: (r, info) => this.observer?.onAgentCompacted(r, info),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`Agent.resume()` instead routes through the per-agent `AgentLifecycleObserver` (`this.observer?.onCompacted?.`), exactly as `run()` does.
|
|
53
|
+
That lifecycle observer is built by `AgentManager.buildObserver()`, whose `onCompacted` forwards to `this.observer?.onAgentCompacted(agent, info)`.
|
|
54
|
+
Net routing is identical — compaction events still reach the manager-level `AgentManagerObserver.onAgentCompacted`.
|
|
55
|
+
|
|
56
|
+
## Design Overview
|
|
57
|
+
|
|
58
|
+
### `Agent.resume()`
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
async resume(prompt: string, signal?: AbortSignal): Promise<void> {
|
|
62
|
+
if (!this._runner) {
|
|
63
|
+
throw new Error("Agent not configured for execution — missing runner");
|
|
64
|
+
}
|
|
65
|
+
const session = this.session;
|
|
66
|
+
if (!session) {
|
|
67
|
+
throw new Error("Agent not configured for resume — missing session");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.resetForResume(Date.now()); // sets running, clears result/error, releases stale listeners
|
|
71
|
+
this.attachObserver(subscribeAgentObserver(session, this, {
|
|
72
|
+
onCompact: (r, info) => this.observer?.onCompacted?.(r, info),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const responseText = await this._runner.resume(session, prompt, { signal });
|
|
77
|
+
this.markCompleted(responseText);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.markError(err);
|
|
80
|
+
} finally {
|
|
81
|
+
this.releaseListeners();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Decision model:
|
|
87
|
+
|
|
88
|
+
- `resetForResume()` already calls `releaseListeners()`, so any leftover handle from a prior run/resume is cleared before the new subscription is attached.
|
|
89
|
+
- The new subscription handle is stored via `attachObserver()` (reusing the `_unsub` slot shared with `run()`), and released in `finally` via `releaseListeners()`.
|
|
90
|
+
- Errors are captured (`markError`) rather than rethrown — `resume()` resolves like `run()`.
|
|
91
|
+
- The two guards (missing runner, missing session) mirror `run()`'s guard style.
|
|
92
|
+
They are defensive: the manager guards `agent?.session` before delegating, so the session guard is unreachable in normal flow but protects the invariant for direct `Agent.resume()` callers/tests.
|
|
93
|
+
|
|
94
|
+
### `AgentManager.resume()` (delegation)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
async resume(id: string, prompt: string, signal?: AbortSignal): Promise<Agent | undefined> {
|
|
98
|
+
const agent = this.agents.get(id);
|
|
99
|
+
if (!agent?.session) return undefined;
|
|
100
|
+
await agent.resume(prompt, signal);
|
|
101
|
+
return agent;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Edge cases preserved:
|
|
106
|
+
|
|
107
|
+
- Missing record → `undefined` (no throw).
|
|
108
|
+
- Record present but no session → `undefined` (no throw).
|
|
109
|
+
- Session present → delegate, return the agent.
|
|
110
|
+
|
|
111
|
+
After this change `agent-manager.ts` no longer references `subscribeAgentObserver` — that import must be removed.
|
|
112
|
+
`this.runner` is still used by `spawn()` (passed to the `Agent` constructor), so the `runner` field stays.
|
|
113
|
+
|
|
114
|
+
## Module-Level Changes
|
|
115
|
+
|
|
116
|
+
- `src/lifecycle/agent.ts`
|
|
117
|
+
- Add the public async method `resume(prompt: string, signal?: AbortSignal): Promise<void>` (placed near `run()` per the stepdown rule).
|
|
118
|
+
- No new imports — `subscribeAgentObserver` is already imported for `run()`.
|
|
119
|
+
- `src/lifecycle/agent-manager.ts`
|
|
120
|
+
- Replace the body of `resume()` with the guard-plus-delegation form above.
|
|
121
|
+
- Remove the now-unused `import { subscribeAgentObserver } from "#src/observation/record-observer";`.
|
|
122
|
+
- No other methods change.
|
|
123
|
+
- `src/lifecycle/agent-runner.ts` — unchanged.
|
|
124
|
+
- `src/observation/record-observer.ts` — unchanged.
|
|
125
|
+
- `docs/architecture/architecture.md` — light doc touch:
|
|
126
|
+
- In the class diagram, update `AgentManager.resume(id, snapshot, exec)` → `resume(id, prompt, signal)` and add `Agent.resume(prompt, signal)` (and, while there, `Agent.run()`, which #229 omitted).
|
|
127
|
+
- Mark Step 6 in the Phase 15 roadmap table/section as complete (`✅`).
|
|
128
|
+
- Note: the class diagram has pre-existing staleness from #229; this touch only corrects the resume-related entries, not the whole diagram.
|
|
129
|
+
|
|
130
|
+
Symbol-removal check: the only removed symbol is the `subscribeAgentObserver` import in `agent-manager.ts`.
|
|
131
|
+
`grep` confirms `subscribeAgentObserver` is still imported and used in `agent.ts` and defined in `record-observer.ts`, so the export stays live.
|
|
132
|
+
|
|
133
|
+
No file in Module-Level Changes is claimed unchanged in Non-Goals (the Non-Goals list `agent-runner.ts` and `record-observer.ts`, which are genuinely untouched).
|
|
134
|
+
|
|
135
|
+
## Test Impact Analysis
|
|
136
|
+
|
|
137
|
+
This is an extraction/relocation of behavior from the manager into the agent.
|
|
138
|
+
|
|
139
|
+
1. New unit tests enabled — `Agent.resume()` can now be tested directly on `Agent` (file `test/lifecycle/agent.test.ts`), which was previously impossible because resume logic lived only in the manager.
|
|
140
|
+
New direct coverage:
|
|
141
|
+
- `resume()` transitions to `completed` and sets `result` from the runner's response text.
|
|
142
|
+
- `resume()` transitions to `error` (and does not throw) when `runner.resume()` rejects.
|
|
143
|
+
- `resume()` subscribes the record-observer to the session (usage/compaction events accumulate on the agent) and releases the subscription in `finally` (handle cleared after completion and after error).
|
|
144
|
+
- `resume()` throws on missing runner / missing session (guard symmetry with `run()`).
|
|
145
|
+
- Compaction during resume forwards through `this.observer?.onCompacted?.`.
|
|
146
|
+
|
|
147
|
+
2. Existing tests that become redundant — none should be deleted.
|
|
148
|
+
The two manager-level resume tests in `test/lifecycle/agent-manager.test.ts` (`resume() also accumulates usage and increments compactions on the same record` and `calls injected runner.resume when resuming an agent`) now exercise the delegation + observer-forwarding integration rather than the inlined logic.
|
|
149
|
+
They stay as integration coverage of `AgentManager.resume()` → `Agent.resume()` and the `onCompacted` → `onAgentCompacted` routing.
|
|
150
|
+
`test/helpers/make-deps.test.ts` (calls `manager.resume(...)`) stays.
|
|
151
|
+
|
|
152
|
+
3. Existing tests that must stay as-is — the manager-level resume tests above genuinely exercise the manager's guard + delegation seam and the observer routing through `buildObserver`, which the agent-level tests do not cover.
|
|
153
|
+
|
|
154
|
+
## TDD Order
|
|
155
|
+
|
|
156
|
+
1. `test/lifecycle/agent.test.ts` — add `Agent.resume()` happy-path + error + guard tests, then implement `Agent.resume()` in `agent.ts`.
|
|
157
|
+
Covers: completed/result on success, error (no throw) on rejection, observer subscribe + `releaseListeners()` in `finally`, compaction forwarding via `onCompacted`, and the missing-runner / missing-session guards.
|
|
158
|
+
At this point both the new `Agent.resume()` and the old `AgentManager.resume()` body coexist (lift-and-shift: introduce the new method alongside the old logic).
|
|
159
|
+
Commit: `feat: add Agent.resume() with internal observer lifecycle`
|
|
160
|
+
2. `test/lifecycle/agent-manager.test.ts` — keep the existing resume tests green, then collapse `AgentManager.resume()` to the guard-plus-delegation form and remove the unused `subscribeAgentObserver` import in the same commit.
|
|
161
|
+
Removing the import and rewriting the body must land together — the type checker flags the unused import immediately, and the existing manager-level resume tests verify the delegation still satisfies the same contract.
|
|
162
|
+
Commit: `refactor: delegate AgentManager.resume() to Agent.resume()`
|
|
163
|
+
3. `docs/architecture/architecture.md` — update the class diagram resume entries (and add `Agent.run()`/`Agent.resume()`), mark Step 6 complete.
|
|
164
|
+
Commit: `docs: mark Phase 15 Step 6 (Agent.resume) complete`
|
|
165
|
+
|
|
166
|
+
## Risks and Mitigations
|
|
167
|
+
|
|
168
|
+
- Risk: observer routing diverges (compaction events stop reaching `onAgentCompacted`).
|
|
169
|
+
Mitigation: the existing manager-level test `resume() also accumulates usage and increments compactions on the same record` asserts `compactionCount` after resume; it stays green only if routing is preserved.
|
|
170
|
+
- Risk: listener leak if `releaseListeners()` is missed on the error path.
|
|
171
|
+
Mitigation: `releaseListeners()` is in `finally`; a dedicated agent-level test asserts the unsub handle is released after both success and error.
|
|
172
|
+
- Risk: behavior change in abort handling if resume is rerouted through `abortController`.
|
|
173
|
+
Mitigation: explicitly keep `signal` flowing straight to `runner.resume({ signal })` (Non-Goal), identical to today.
|
|
174
|
+
- Risk: removing the `subscribeAgentObserver` import while another caller still needs it.
|
|
175
|
+
Mitigation: `grep` confirms `agent.ts` is the only other importer and `record-observer.ts` still exports it.
|
|
176
|
+
|
|
177
|
+
## Open Questions
|
|
178
|
+
|
|
179
|
+
- Whether to later refresh the full `AgentManager`/`Agent` class diagram in `architecture.md` (stale since #229).
|
|
180
|
+
Deferred — out of scope for this issue; a focused follow-up can resync the whole diagram.
|
|
@@ -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.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 232
|
|
3
|
+
issue_title: "Agent.resume() with internal observer lifecycle (Phase 15, Step 6)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #232 — Agent.resume() with internal observer lifecycle (Phase 15, Step 6)
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-28T18:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 3-step plan to move the observer subscribe/use/release pattern out of `AgentManager.resume()` into a new `Agent.resume(prompt, signal?)`, mirroring the `run()` wiring added in #229.
|
|
13
|
+
This is the last "manager reaches into Agent" duplication in the Phase 15 roadmap (Step 6, priority 8).
|
|
14
|
+
Confirmed the prerequisite #229 is closed and `Agent` already holds `_runner`, `observer`, `attachObserver`/`releaseListeners`, and `resetForResume`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- Non-breaking (`feat:`) — `AgentManager.resume()` keeps its signature and `Agent | undefined` contract; `Agent.resume()` is additive.
|
|
19
|
+
No `ask_user` needed; the issue's proposed change is concrete and unambiguous.
|
|
20
|
+
- Observer routing equivalence verified: old code wired `onCompact` → `AgentManagerObserver.onAgentCompacted`; new code routes through the per-agent `AgentLifecycleObserver.onCompacted`, which `buildObserver()` forwards to `onAgentCompacted`.
|
|
21
|
+
Net routing identical.
|
|
22
|
+
- Abort semantics intentionally preserved — `signal` flows straight to `runner.resume({ signal })`, not through the agent's `abortController` (resume differs from `run()` here; flagged as a Non-Goal to avoid accidental behavior change).
|
|
23
|
+
- Removing the `subscribeAgentObserver` import from `agent-manager.ts` must land in the same commit as the body rewrite (type checker flags the unused import). `grep` confirmed `agent.ts` remains the importer and `record-observer.ts` keeps the export live.
|
|
24
|
+
- Discovered the `architecture.md` class diagram is stale from #229 (missing `Agent.run()`, stale `setupWorktree`/`completeRun`/`setOnRunFinished` signatures, old `resume(id, snapshot, exec)`).
|
|
25
|
+
Scoped only a light touch (resume-related entries + Step 6 ✅); full diagram refresh deferred as a follow-up.
|
|
26
|
+
- Lift-and-shift TDD order: step 1 introduces `Agent.resume()` alongside the old manager logic; step 2 collapses the manager method and removes the import together.
|
|
27
|
+
Existing manager-level resume tests act as the integration safety net and stay.
|
|
28
|
+
|
|
29
|
+
## Stage: Implementation — TDD (2026-05-28T19:00:00Z)
|
|
30
|
+
|
|
31
|
+
### Session summary
|
|
32
|
+
|
|
33
|
+
Completed all 3 TDD steps in 3 commits plus a bonus `fix:` commit, totalling 4 new commits.
|
|
34
|
+
`Agent.resume()` added with full observer lifecycle, `AgentManager.resume()` collapsed to guard-plus-delegation, `subscribeAgentObserver` import removed from `agent-manager.ts`, and `architecture.md` updated.
|
|
35
|
+
Test count: 1042 → 1053 (+11).
|
|
36
|
+
|
|
37
|
+
### Observations
|
|
38
|
+
|
|
39
|
+
- **Bonus fix found mid-session:** A user question revealed a listener leak introduced in #229 — `Agent.run()` called `wireSignal()` before `setupWorktree()`, but the worktree-failure catch block returned without `releaseListeners()`, leaving the parent `AbortSignal` holding a reference to the errored agent.
|
|
40
|
+
Fixed TDD-style: failing test first (`"releases the parent-signal listener when worktree setup fails"` in `agent.test.ts`), then one-line fix adding `this.releaseListeners()` to the catch block in `run()`.
|
|
41
|
+
Committed as a separate `fix:` commit with a body attributing the regression to #229.
|
|
42
|
+
- **Pre-completion reviewer: WARN** — one non-blocking finding: the Phase 15 findings-summary table in `architecture.md` didn't mark the resolved rows (consistent pre-existing pattern from #229–#231).
|
|
43
|
+
Fixed by adding strikethrough + ✅ to all four resolved finding rows (#229 "Agent cannot run itself", #230 "Scheduling", #231 "exec/registry", #232 "resume()") in an additional `docs:` commit.
|
|
44
|
+
All other reviewer checks passed (Mermaid diagrams validated with `mmdc`, fallow clean, code design clean).
|
|
45
|
+
- **Reviewer warning resolved:** The findings table gap was pre-existing across four issues; closing it in this commit makes the table accurate going into Phase 16.
|
|
46
|
+
|
|
47
|
+
## Stage: Final Retrospective (2026-05-28T20:31:35Z)
|
|
48
|
+
|
|
49
|
+
### Session summary
|
|
50
|
+
|
|
51
|
+
Planned, implemented (3 TDD steps), fixed a latent #229 bug surfaced by a user question, shipped, and released `pi-subagents-v11.2.0` in a single continuous session.
|
|
52
|
+
Test count: 1042 → 1053 (+11).
|
|
53
|
+
The dominant friction was capturing the `pre-completion-reviewer`'s verdict: foreground subagent dispatch surfaced only the completion banner, not the report body, forcing several retrieval attempts and a near-miss where shipping began before a clean verdict existed.
|
|
54
|
+
|
|
55
|
+
### Observations
|
|
56
|
+
|
|
57
|
+
#### What went well
|
|
58
|
+
|
|
59
|
+
- **User-prompted latent-bug discovery, fixed TDD-style.**
|
|
60
|
+
The user's question "did we introduce a bug in a prior issue?"
|
|
61
|
+
led to finding the `Agent.run()` abort-signal listener leak (regression from #229: `wireSignal()` ran before `setupWorktree()`, and the worktree-failure catch returned without `releaseListeners()`).
|
|
62
|
+
Fixed red→green: failing test `"releases the parent-signal listener when worktree setup fails"` first, then a one-line `releaseListeners()` addition.
|
|
63
|
+
The `fix:` commit body attributes the regression to #229 so release-please categorizes it correctly.
|
|
64
|
+
- **Lift-and-shift plan executed without backtracking.**
|
|
65
|
+
Step 1 introduced `Agent.resume()` alongside the old manager logic; step 2 collapsed the manager method and removed the `subscribeAgentObserver` import together (type checker would reject splitting them).
|
|
66
|
+
Every commit stayed green.
|
|
67
|
+
- **Incremental verification.** `pnpm run check` + targeted `vitest run` after each TDD step; full suite, lint, and `pnpm fallow dead-code` (from repo root) after the last step.
|
|
68
|
+
|
|
69
|
+
#### What caused friction (agent side)
|
|
70
|
+
|
|
71
|
+
- `other` (tooling) — Foreground `pre-completion-reviewer` dispatch returned only the completion banner (`Agent completed in Xs, N tool uses`), not the report body.
|
|
72
|
+
Two foreground dispatches yielded a truncated line and an empty body; `get_subagent_result` reported the foreground agent was "cleaned up"; `read_session` omits tool-result bodies.
|
|
73
|
+
Only a background dispatch retrieved via `get_subagent_result(wait: true, verbose: true)` surfaced the full PASS/WARN report.
|
|
74
|
+
Impact: ~5 wasted retrieval/re-dispatch tool calls and one long thrashing reviewer run (232 tool uses, with repeated `fatal: bad revision` git lookups) before a clean verdict.
|
|
75
|
+
- `instruction-violation` (user-caught) — The `pre-completion` skill says "proceed to Summarize only after the reviewer returns PASS or WARN," but I began `/ship-issue` (pushed, started `ci_watch`) without ever cleanly capturing a verdict.
|
|
76
|
+
The user interrupted: "we should have verified our fix … can we try dispatching pre-completion again?"
|
|
77
|
+
Impact: aborted `ci_watch`, re-dispatched the review, then re-shipped — no incorrect release, but a redundant push/CI cycle.
|
|
78
|
+
Root cause is shared with the tooling friction above: because the verdict was never captured, the gate silently passed.
|
|
79
|
+
|
|
80
|
+
#### What caused friction (user side)
|
|
81
|
+
|
|
82
|
+
- The user's prior-issue-bug question was high-value strategic redirection — it surfaced a real defect the `pre-completion-reviewer` itself examined (`completeRun`/`failRun`/`abort`) but did not flag.
|
|
83
|
+
Opportunity: the reviewer's code-design lens could check resource-cleanup symmetry across all early-return paths, not just the happy/`failRun` paths.
|
|
84
|
+
- The user caught the "shipped before verifying" gap that should have been the agent's own gate.
|
|
85
|
+
Framed as opportunity: a reliable verdict-capture step removes the need for this manual oversight.
|
|
86
|
+
|
|
87
|
+
### Diagnostic details
|
|
88
|
+
|
|
89
|
+
- **Model-performance correlation** — The `pre-completion-reviewer` ran on `claude-sonnet-4-6`; appropriate for judgment-heavy review (code design, acceptance criteria, Mermaid validation).
|
|
90
|
+
No mismatch.
|
|
91
|
+
Note: the first (truncated) run used 232 tool calls vs 26 for the clean run — the long run thrashed on failed `git rev-parse` lookups of abbreviated SHAs.
|
|
92
|
+
- **Escalation-delay tracking** — The verdict-capture rabbit hole ran >5 consecutive tool calls (foreground re-dispatch → `get_subagent_result` → `read_session` → background dispatch) before the background+verbose approach worked.
|
|
93
|
+
Switching to background dispatch after the first truncation would have resolved it immediately.
|
|
94
|
+
- **Feedback-loop gap analysis** — No gap: verification ran incrementally after each TDD step, and `fallow` ran from the repo root (not a package subdir), matching CI.
|
|
95
|
+
|
|
96
|
+
### Changes made
|
|
97
|
+
|
|
98
|
+
1. `.pi/skills/pre-completion/SKILL.md` — added a Step 3 guard (P2, safety net): a missing `Overall: PASS|WARN|FAIL` line is treated as "report not captured" and triggers a re-dispatch; do not proceed to "Summarize" on a banner-only result.
|
|
99
|
+
2. `.pi/agents/pre-completion-reviewer.md` — reviewer-side durable fix: (a) the final message must be the report block ending with `### Overall`, never a trailing tool call; (b) thrash guard — use the dispatcher-provided base tag and modified-files list, do not retry `git rev-parse` on abbreviated SHAs.
|
|
100
|
+
3. Proposal P1 (background dispatch + verbose retrieval) was presented but **not** adopted; with the reviewer's output contract fixed, foreground dispatch should return the report directly.
|
|
101
|
+
Recorded as a fallback if banner-only foreground results recur.
|
|
102
|
+
|
|
103
|
+
### Root-cause follow-up: reviewer verdict-capture failure
|
|
104
|
+
|
|
105
|
+
After the initial retro commit we examined *why* foreground dispatches returned only a banner.
|
|
106
|
+
Ruled out the #229 abort-signal leak: it only fires on `isolation: "worktree"` setup failure (never exercised by the reviewer dispatches, which used no worktree), and a leaked listener cannot truncate a healthy agent's output — wrong code path and wrong symptom.
|
|
107
|
+
The `/reload` after the fix is a confounder (it clears in-session state) but does not implicate the leak itself.
|
|
108
|
+
Best explanation (≈70% confidence): the reviewer ended long, thrashing runs (232 tool calls, repeated `fatal: bad revision` lookups) *on tool activity rather than a final report*, so foreground returned the last text it saw.
|
|
109
|
+
Note: the running extension loads `../packages/pi-subagents` from this working tree (per `.pi/settings.json`), so source edits take effect after `/reload` — an earlier claim that the session ran an installed build was wrong.
|