@gotgenes/pi-subagents 16.3.1 → 16.5.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 +17 -0
- package/README.md +47 -0
- package/dist/public.d.ts +7 -4
- package/dist/settings.d.ts +82 -0
- package/docs/architecture/architecture.md +193 -207
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/plans/0378-consolidate-lifecycle-test-fixtures.md +243 -0
- package/docs/plans/0379-consolidate-ui-tools-test-fixtures.md +335 -0
- package/docs/plans/0380-extract-layered-settings-loader.md +223 -0
- package/docs/plans/0412-unify-session-mock-builders.md +146 -0
- package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +228 -0
- package/docs/retro/0378-consolidate-lifecycle-test-fixtures.md +100 -0
- package/docs/retro/0379-consolidate-ui-tools-test-fixtures.md +52 -0
- package/docs/retro/0380-extract-layered-settings-loader.md +97 -0
- package/docs/retro/0412-unify-session-mock-builders.md +80 -0
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +45 -0
- package/package.json +5 -1
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/subagent-state.ts +49 -4
- package/src/lifecycle/subagent.ts +4 -0
- package/src/observation/record-observer.ts +25 -1
- package/src/settings.ts +10 -23
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 412
|
|
3
|
+
issue_title: "Unify the three overlapping session-mock builders in pi-subagents tests"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Unify the overlapping session-mock builders (targeted reuse)
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`test/helpers/` carries three session-mock builders whose shapes overlap on four fields — `messages`, `subscribe`, `dispose`, `steer`:
|
|
11
|
+
|
|
12
|
+
- `createMockSession` (`mock-session.ts`) — a subscribable event bus (`messages`, `subscribe`, `emit`, `dispose`, `steer`, `sessionManager`).
|
|
13
|
+
- `createSubagentSessionStub` (`mock-session.ts`) — a born-complete `SubagentSession` wrapper (turn-loop spies plus `messages`/`subscribe`/`dispose`/`steer` delegation).
|
|
14
|
+
- `createFactorySession` (`subagent-session-io.ts`) — the `createSubagentSession`-factory session (`bindExtensions`, `setActiveToolsByName`, `getActiveToolNames`, plus `messages`/`subscribe`/`prompt`/`abort`/`steer`/`dispose`).
|
|
15
|
+
|
|
16
|
+
Issue [#412] asks whether to unify the three behind one configurable builder.
|
|
17
|
+
The issue itself flags the risk — "trading three small honest stubs for one over-parameterized factory" — and quotes Sandi Metz: "duplication is far cheaper than the wrong abstraction."
|
|
18
|
+
|
|
19
|
+
## Goals
|
|
20
|
+
|
|
21
|
+
- Remove the one genuine independent redeclaration of the four shared base fields, which lives in `createFactorySession`.
|
|
22
|
+
- Make `createMockSession` the explicit shared core, and have `createFactorySession` layer its factory facet on top of that core (working event bus as the core default).
|
|
23
|
+
- Keep the change test-only and behavior-preserving for production code.
|
|
24
|
+
|
|
25
|
+
This change is **not breaking** — it touches only `test/helpers/` and a doc note; no published surface, default, or runtime behavior changes.
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- The full composable-factory design from the issue's "Proposed change" (one `createSessionMock()` with opt-in `withTurnLoop()` / `withBindFacet()` extensions).
|
|
30
|
+
Rejected at the `Decide` gate (operator chose targeted reuse) because it forces a multi-facet parameterized factory the issue itself warns against.
|
|
31
|
+
- Changing `createSubagentSessionStub`.
|
|
32
|
+
It already **composes** a `createMockSession` internally and delegates `steer`/`dispose`/`subscribe`/`messages` to it, so its overlap is intrinsic delegation glue, not duplication.
|
|
33
|
+
- Renaming any builder or changing any builder's public signature.
|
|
34
|
+
- Touching production `src/` modules.
|
|
35
|
+
|
|
36
|
+
## Background
|
|
37
|
+
|
|
38
|
+
Relevant modules:
|
|
39
|
+
|
|
40
|
+
- `test/helpers/mock-session.ts` — defines `createMockSession` (the event-bus core), `createSubagentSessionStub` (the `SubagentSession` wrapper, which calls `createMockSession()` as its default `session`), plus the `toAgentSession`/`toSubagentSession` casts.
|
|
41
|
+
- `test/helpers/subagent-session-io.ts` — defines `createFactorySession` (and the IO/lookup/lifecycle stubs).
|
|
42
|
+
`createFactorySession` independently redeclares `messages: []` and an **inert** `subscribe: vi.fn(() => () => {})`, plus its own `steer`/`dispose`, then adds the factory facet.
|
|
43
|
+
- `src/lifecycle/create-subagent-session.ts` — production consumer of the raw factory session: calls `getActiveToolNames()`, `setActiveToolsByName()`, `bindExtensions()`, and `dispose()`.
|
|
44
|
+
- `src/lifecycle/subagent-session.ts` — wraps the raw session and calls `session.subscribe(...)`, `session.prompt(...)`, `session.abort(...)`, `session.steer(...)`, `session.dispose()`, `session.messages`.
|
|
45
|
+
|
|
46
|
+
Structural reading (per the `code-design` "structural reasons before extracting duplication" heuristic): the three builders sit on two axes — AgentSession-vs-`SubagentSession` (type) and event-bus-vs-factory (facet).
|
|
47
|
+
`createMockSession` and `createFactorySession` are both AgentSession stubs; `createSubagentSessionStub` is the `SubagentSession` wrapper.
|
|
48
|
+
The only honest, non-delegation duplication is `createFactorySession`'s independent base, so that is the entire target.
|
|
49
|
+
|
|
50
|
+
AGENTS.md constraint: this is the `@gotgenes/pi-subagents` package; run package-scoped scripts via `pnpm --filter @gotgenes/pi-subagents exec vitest run` and type-check with `pnpm run check`.
|
|
51
|
+
|
|
52
|
+
## Design Overview
|
|
53
|
+
|
|
54
|
+
`createMockSession` is already the core shape, with the working event bus the operator chose as the default.
|
|
55
|
+
`createFactorySession` is rebuilt to spread that core and add only the factory facet:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
export function createFactorySession(options: FactorySessionOptions = {}) {
|
|
59
|
+
const before = options.toolsBeforeBind ?? ["read"];
|
|
60
|
+
const after = options.toolsAfterBind ?? before;
|
|
61
|
+
let bound = false;
|
|
62
|
+
return {
|
|
63
|
+
...createMockSession(), // working event bus + messages/steer/dispose/sessionManager core
|
|
64
|
+
prompt: vi.fn().mockResolvedValue(undefined),
|
|
65
|
+
abort: vi.fn(),
|
|
66
|
+
getActiveToolNames: vi.fn(() => (bound ? after : before)),
|
|
67
|
+
setActiveToolsByName: vi.fn(),
|
|
68
|
+
bindExtensions: vi.fn(async () => {
|
|
69
|
+
bound = true;
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Mock-typing is preserved through the spread.
|
|
76
|
+
`createMockSession()` returns `MockSession & Record<string, unknown>`; the explicit facet properties added in the literal intersect with the `Record`'s `unknown` index signature, and `unknown & Mock<...>` narrows to `Mock<...>`.
|
|
77
|
+
Verified with a throwaway `tsc --noEmit` probe: `session.setActiveToolsByName.mock.calls[0][0]` type-checks after the spread.
|
|
78
|
+
|
|
79
|
+
Facet semantics are unchanged: `getActiveToolNames` closes over a `bound` flag flipped by `bindExtensions`, returning `before` until bind and `after` after.
|
|
80
|
+
|
|
81
|
+
Behavioral delta (intentional, per the operator's "working bus is the core default" choice): `createFactorySession`'s `subscribe` changes from inert (`() => () => {}`) to the core's real bus, and the session gains `emit` and `sessionManager`.
|
|
82
|
+
No factory test emits events, and production `subagent-session.ts` only registers a subscriber (never emits during these tests), so the change is inert in practice — the disposer now truly unregisters instead of being a no-op.
|
|
83
|
+
|
|
84
|
+
Edge cases:
|
|
85
|
+
|
|
86
|
+
- `create-subagent-session.test.ts:194` asserts `expect(session.dispose).toHaveBeenCalledOnce()` — `dispose` remains a `vi.fn()` spy supplied by the core, so the assertion holds.
|
|
87
|
+
- The factory session is returned through `io.createSession.mockResolvedValue({ session })`, not assigned as a fresh literal to a typed slot, so the extra `emit`/`sessionManager` fields raise no excess-property error.
|
|
88
|
+
|
|
89
|
+
## Module-Level Changes
|
|
90
|
+
|
|
91
|
+
- `test/helpers/subagent-session-io.ts` — rewrite `createFactorySession` to spread `...createMockSession()` and add the factory facet; import `createMockSession` from `#test/helpers/mock-session`.
|
|
92
|
+
Update its docstring to state it layers the factory facet on the shared `createMockSession` core.
|
|
93
|
+
- `test/helpers/subagent-session-io.test.ts` — add one self-test asserting `createFactorySession` exposes the core's working event bus (subscribe a fn, `emit`, assert receipt).
|
|
94
|
+
Existing `createFactorySession` self-tests remain unchanged.
|
|
95
|
+
- `test/helpers/mock-session.ts` — docstring touch-up only: note that `createMockSession` is the shared session-mock core that `createFactorySession` builds on (no code change).
|
|
96
|
+
- `docs/architecture/architecture.md` — update the Phase 17 Step 7 "Landed" closing sentence (currently "The three overlapping session-mock builders this surfaced are tracked separately [#412].") to record the resolution: targeted reuse — `createFactorySession` now layers on the `createMockSession` core; `createSubagentSessionStub`'s overlap is intrinsic delegation and was left as-is.
|
|
97
|
+
|
|
98
|
+
Grep confirmation (no other consumers break): `createFactorySession` is referenced only in `create-subagent-session.test.ts` and its own self-test/source; `.pi/skills/package-*/SKILL.md` mention none of the three builders by name; the `[#412]:` reference-link definition already exists at the bottom of `architecture.md`.
|
|
99
|
+
|
|
100
|
+
## Test Impact Analysis
|
|
101
|
+
|
|
102
|
+
1. **New tests enabled** — one self-test for `createFactorySession`'s newly-inherited working event bus (subscribe/emit), previously impossible because its `subscribe` was inert.
|
|
103
|
+
2. **Tests made redundant** — none.
|
|
104
|
+
The existing `createFactorySession` self-tests exercise the factory facet (the eight methods, the before/after bind flip), which is unchanged and still its own concern.
|
|
105
|
+
3. **Tests that must stay as-is** — the `createMockSession` event-bus self-tests in `mock-session.test.ts` (they pin the core the factory now reuses); the `createFactorySession` facet self-tests; `create-subagent-session.test.ts` lifecycle tests (they exercise the production consumer of the factory session).
|
|
106
|
+
|
|
107
|
+
## Invariants at risk
|
|
108
|
+
|
|
109
|
+
The change touches the lifecycle test fixtures refactored in Phase 17 Step 7 ([#378]).
|
|
110
|
+
That step's documented `Outcome`/`Landed` invariants and the tests that pin them:
|
|
111
|
+
|
|
112
|
+
- "AAA structure: `createSubagentSession(...)` act kept explicit per test" — pinned by `create-subagent-session.test.ts` (the act calls remain in each `it`; this plan does not touch them).
|
|
113
|
+
- "`createFactorySession` flips `getActiveToolNames` from before-bind to after-bind" — pinned by the existing `subagent-session-io.test.ts` facet self-tests, which stay green through the rewrite.
|
|
114
|
+
|
|
115
|
+
No earlier outcome is regressed: the rewrite preserves every facet assertion and only adds the event-bus surface.
|
|
116
|
+
|
|
117
|
+
## TDD Order
|
|
118
|
+
|
|
119
|
+
1. **`refactor(test):` fold `createFactorySession` onto the shared core** (#412).
|
|
120
|
+
- Red: add `it("exposes the core's working event bus (subscribe/emit)")` to the `createFactorySession` describe in `subagent-session-io.test.ts` — fails to compile/pass because the current return has no `emit` and an inert `subscribe`.
|
|
121
|
+
- Green: rewrite `createFactorySession` to `{ ...createMockSession(), <factory facet> }`; import `createMockSession`; refresh the docstrings in both helper files.
|
|
122
|
+
- Verify: `pnpm --filter @gotgenes/pi-subagents exec vitest run test/helpers/subagent-session-io.test.ts test/helpers/mock-session.test.ts test/lifecycle/create-subagent-session.test.ts`, then `pnpm run check` (spread-typing) and the full package suite.
|
|
123
|
+
- Commit: `refactor(test): build createFactorySession on the shared createMockSession core (#412)`.
|
|
124
|
+
2. **`docs:` record the resolution in the architecture doc** (#412).
|
|
125
|
+
- Update the Phase 17 Step 7 closing sentence in `docs/architecture/architecture.md` to describe the targeted-reuse outcome (no roadmap-table or metric-row change required; rerun `pnpm fallow:dupes` only to report, not to gate).
|
|
126
|
+
- Commit: `docs: record session-mock builder unification outcome (#412)`.
|
|
127
|
+
|
|
128
|
+
Both steps are test/doc-only with single, isolated surfaces, so no lift-and-shift staging is needed.
|
|
129
|
+
|
|
130
|
+
## Risks and Mitigations
|
|
131
|
+
|
|
132
|
+
- **Risk:** the spread erases `Mock<...>` typing on the facet methods, breaking `.mock.calls` assertions.
|
|
133
|
+
**Mitigation:** verified with a throwaway `tsc --noEmit` probe that `setActiveToolsByName.mock.calls[0][0]` type-checks after the spread (intersection narrows `unknown & Mock<...>` to `Mock<...>`); Step 1 reruns `pnpm run check`.
|
|
134
|
+
- **Risk:** the inert→working `subscribe` change alters a lifecycle test's behavior.
|
|
135
|
+
**Mitigation:** no factory test emits events; production only registers a subscriber during these tests; `session.dispose` remains a spy.
|
|
136
|
+
Step 1's verify command runs the lifecycle suite.
|
|
137
|
+
- **Risk:** the extra `emit`/`sessionManager` fields trip excess-property checks at a consumer.
|
|
138
|
+
**Mitigation:** the factory session flows through `mockResolvedValue({ session })`, not a fresh typed literal, so excess-property checking does not apply.
|
|
139
|
+
|
|
140
|
+
## Open Questions
|
|
141
|
+
|
|
142
|
+
None.
|
|
143
|
+
The operator confirmed targeted reuse with a working-bus core default at the `Decide` gate.
|
|
144
|
+
|
|
145
|
+
[#378]: https://github.com/gotgenes/pi-packages/issues/378
|
|
146
|
+
[#412]: https://github.com/gotgenes/pi-packages/issues/412
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 420
|
|
3
|
+
issue_title: "pi-subagents: fold subagent run metrics and live activity onto the core record"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fold subagent run metrics and live activity onto the core record
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The run-metric domain is split across two parallel observers, each subscribed to every child session.
|
|
11
|
+
`record-observer` accumulates `SubagentState` (tool uses, lifetime usage, compaction count); `ui-observer` accumulates a separate `AgentActivityTracker` (active tools, response text, turn count).
|
|
12
|
+
`turnCount` is a genuine run metric that lives only in the UI tracker, so `notification.ts` and the foreground result text reach into the tracker to recover it (Phase 18 Finding 4).
|
|
13
|
+
Consolidating the live-activity fields onto the single owned run-state value object (`SubagentState`) removes the duplication and is the first move in disentangling the UI's activity tier from the core.
|
|
14
|
+
|
|
15
|
+
This is Phase 18 Step 1 of the architecture roadmap (`docs/architecture/architecture.md`).
|
|
16
|
+
It is a **pure addition, tidy-first**: `AgentActivityTracker` and `ui-observer` still exist, both observers still run, and nothing reads the new getters yet.
|
|
17
|
+
The reader migration is Step 2 ([#421]) and the tracker deletion is Step 3 ([#422]).
|
|
18
|
+
|
|
19
|
+
## Goals
|
|
20
|
+
|
|
21
|
+
- Extend `SubagentState` (`src/lifecycle/subagent-state.ts`) with the live-activity fields — `turnCount`, `activeTools`, `responseText` — and their transition methods, behind read-only getters, owning all their own mutations (no field written from outside).
|
|
22
|
+
- Have `subscribeSubagentObserver` (`src/observation/record-observer.ts`) handle `turn_end`, `tool_execution_start`, `message_start`, and `message_update` (text_delta) alongside the events it already handles, mutating the same `SubagentState`.
|
|
23
|
+
- Add read-only `turnCount` / `maxTurns` / `activeTools` / `responseText` getters to `Subagent` (`src/lifecycle/subagent.ts`); the first three plus `responseText` delegate to the owned state, `maxTurns` delegates to `this.execution.maxTurns` (the effective max-turns already wired at spawn).
|
|
24
|
+
- Preserve the `AgentActivityTracker` semantics the readers depend on so Step 2 is a clean swap: `turnCount` starts at 1 and increments on each `turn_end`; `activeTools` is a `ReadonlyMap<string, string>` keyed `name_seq` to disambiguate concurrent same-name tools; `responseText` resets at each `message_start` and appends each text delta.
|
|
25
|
+
|
|
26
|
+
This change is **not breaking**.
|
|
27
|
+
The published service surface (`src/service/service.ts`) exposes `SubagentRecord`/`SubagentStatus`, not `SubagentState` or the `Subagent` constructor; the change only adds getters and value-object fields.
|
|
28
|
+
No observable behavior changes — both observers still run and no consumer reads the new getters.
|
|
29
|
+
|
|
30
|
+
## Non-Goals
|
|
31
|
+
|
|
32
|
+
- Migrating any reader (`widget-renderer`, `conversation-viewer`, `agent-menu`, `foreground-runner`, `notification`) off `AgentActivityTracker` — Step 2 ([#421]).
|
|
33
|
+
- Deleting `AgentActivityTracker` / `ui-observer` or dropping `SubagentRuntime.agentActivity` — Step 3 ([#422]).
|
|
34
|
+
- Removing the second (`ui-observer`) session subscription — Step 3.
|
|
35
|
+
- Adding a `session` reference to `SubagentState` — the tracker's `_session`/`setSession` exists only for the UI's polling reads and is migrated/removed in Steps 2–3, not folded here.
|
|
36
|
+
- The deeper "metrics as a pure observer projection" target — deliberately deferred per the architecture doc's first-principles refinement; folding onto the record (consistent with Phase 17's `SubagentState`) avoids inventing an asynchronous-observation seam.
|
|
37
|
+
- Resetting the new live-activity fields in `resetForResume` — see Design Overview; left unchanged to preserve tracker parity.
|
|
38
|
+
|
|
39
|
+
## Background
|
|
40
|
+
|
|
41
|
+
Relevant modules and how they relate:
|
|
42
|
+
|
|
43
|
+
- `src/lifecycle/subagent-state.ts` — the value object under change.
|
|
44
|
+
Already owns status/result/error/timestamps and stats (`toolUses`, `lifetimeUsage`, `compactionCount`) behind getters, mutated only via transition/accumulation methods (`markRunning`, `incrementToolUses`, `addUsage`, `incrementCompactions`).
|
|
45
|
+
Pure value object — imports only `LifetimeUsage`/`addUsage` from `usage.ts`; no Pi SDK imports (keep it that way).
|
|
46
|
+
- `src/observation/record-observer.ts` — `subscribeSubagentObserver(session, state, { onCompact })` subscribes once and currently handles `tool_execution_end`, `message_end` (assistant), and `compaction_end`.
|
|
47
|
+
- `src/ui/agent-activity-tracker.ts` — the parallel `AgentActivityTracker` whose live fields (`_activeTools`, `_toolKeySeq`, `_responseText`, `_turnCount` starting at 1) and transition methods (`onToolStart`, `onToolDone`, `onMessageStart`, `onMessageUpdate`, `onTurnEnd`) are the behavior being folded onto `SubagentState`.
|
|
48
|
+
It also carries `_session`/`setSession` and `maxTurns` — `maxTurns` is folded (via `execution`), `_session` is not.
|
|
49
|
+
- `src/ui/ui-observer.ts` — `subscribeUIObserver` maps `tool_execution_start`/`tool_execution_end`/`message_start`/`message_update`/`turn_end` onto the tracker.
|
|
50
|
+
The event-shape reference for the new `record-observer` branches (`event.toolName`; `event.assistantMessageEvent.type === "text_delta"` → `event.assistantMessageEvent.delta`).
|
|
51
|
+
- `src/lifecycle/subagent.ts` — holds one private `SubagentState`; its getters and mutation methods are one-line delegations.
|
|
52
|
+
`run()` and `resume()` both attach `subscribeSubagentObserver(session, this.state, …)`.
|
|
53
|
+
`this.execution.maxTurns` is the effective max-turns (both spawners pass `execution.effectiveMaxTurns` as `options.maxTurns`, threaded into `SubagentExecution.maxTurns` by `SubagentManager.spawn`).
|
|
54
|
+
- `src/tools/spawn-config.ts` — `effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? settings.defaultMaxTurns)`.
|
|
55
|
+
`AgentActivityTracker` is constructed with `effectiveMaxTurns`; `SubagentExecution.maxTurns` carries the same value, so the new `maxTurns` getter matches the tracker.
|
|
56
|
+
|
|
57
|
+
AGENTS.md / code-design constraints that apply:
|
|
58
|
+
|
|
59
|
+
- Keep Pi SDK imports out of `SubagentState` — the new fields and methods stay SDK-free.
|
|
60
|
+
- The new `record-observer` branches consume SDK event payloads via the existing lean structural reads (`event.toolName`, `event.assistantMessageEvent`) — no new SDK type imports needed.
|
|
61
|
+
- `SubagentState` and `Subagent` are reached through the `types.ts`/internal barrels; adding getters does not change any export.
|
|
62
|
+
|
|
63
|
+
## Design Overview
|
|
64
|
+
|
|
65
|
+
### `SubagentState` additions
|
|
66
|
+
|
|
67
|
+
Three new private fields with read-only getters and five transition methods, mirroring the `AgentActivityTracker` semantics exactly so the Step 2 swap is byte-equivalent:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Live activity — accumulated via transition methods, readable via getters
|
|
71
|
+
private _turnCount = 1;
|
|
72
|
+
get turnCount(): number { return this._turnCount; }
|
|
73
|
+
|
|
74
|
+
private _activeTools = new Map<string, string>();
|
|
75
|
+
get activeTools(): ReadonlyMap<string, string> { return this._activeTools; }
|
|
76
|
+
|
|
77
|
+
private _toolKeySeq = 0;
|
|
78
|
+
|
|
79
|
+
private _responseText = "";
|
|
80
|
+
get responseText(): string { return this._responseText; }
|
|
81
|
+
|
|
82
|
+
/** Record a turn boundary. Called by record-observer on turn_end. */
|
|
83
|
+
incrementTurnCount(): void { this._turnCount++; }
|
|
84
|
+
|
|
85
|
+
/** Record a tool starting. Called by record-observer on tool_execution_start. */
|
|
86
|
+
addActiveTool(toolName: string): void {
|
|
87
|
+
this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Remove one active tool by name (first match). Called by record-observer on tool_execution_end. */
|
|
91
|
+
removeActiveTool(toolName: string): void {
|
|
92
|
+
for (const [key, name] of this._activeTools) {
|
|
93
|
+
if (name === toolName) { this._activeTools.delete(key); break; }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reset the current response text. Called by record-observer on message_start. */
|
|
98
|
+
resetResponseText(): void { this._responseText = ""; }
|
|
99
|
+
|
|
100
|
+
/** Append a text delta to the current response text. Called by record-observer on message_update. */
|
|
101
|
+
appendResponseText(delta: string): void { this._responseText += delta; }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `_toolKeySeq` counter has no getter — it is internal disambiguation state, exactly as in the tracker.
|
|
105
|
+
`turnCount` initializes to `1` (not `0`) to match the tracker; the readers (`notification.ts` `turnCount ?? 0`, `result-renderer.ts` `turnCount > 0`) already assume the at-least-1 invariant once an agent exists.
|
|
106
|
+
|
|
107
|
+
`resetForResume` is left **unchanged**.
|
|
108
|
+
The tracker is not reset on resume today (the resume path in `agent-tool.ts` does not reconstruct it), so the record-observer accumulating onto the surviving `SubagentState` across a resume preserves parity (turn count continues, response text carries the last message, active tools are empty after a completed run).
|
|
109
|
+
Touching `resetForResume` here would be a behavior change, contradicting the pure-addition contract.
|
|
110
|
+
Flagged in Risks for Step 2 to revisit if a reader demands a reset.
|
|
111
|
+
|
|
112
|
+
### `record-observer` additions
|
|
113
|
+
|
|
114
|
+
Three new branches alongside the existing ones, reading the same SDK payload shapes `ui-observer` reads:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
if (event.type === "tool_execution_start") state.addActiveTool(event.toolName);
|
|
118
|
+
if (event.type === "turn_end") state.incrementTurnCount();
|
|
119
|
+
if (event.type === "message_start") state.resetResponseText();
|
|
120
|
+
if (
|
|
121
|
+
event.type === "message_update" &&
|
|
122
|
+
event.assistantMessageEvent.type === "text_delta"
|
|
123
|
+
) {
|
|
124
|
+
state.appendResponseText(event.assistantMessageEvent.delta);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The existing `tool_execution_end` branch already fires `state.incrementToolUses()`; it gains a paired `state.removeActiveTool(event.toolName)` so the active-tool map drains symmetrically (the tracker did this in `onToolDone`).
|
|
129
|
+
The observer takes no new options and emits no callbacks for these branches — the live fields are read-only state, polled by consumers later.
|
|
130
|
+
|
|
131
|
+
### `Subagent` getters
|
|
132
|
+
|
|
133
|
+
Four one-line getters added next to the existing `toolUses`/`lifetimeUsage`/`compactionCount` delegations:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
get turnCount(): number { return this.state.turnCount; }
|
|
137
|
+
get activeTools(): ReadonlyMap<string, string> { return this.state.activeTools; }
|
|
138
|
+
get responseText(): string { return this.state.responseText; }
|
|
139
|
+
get maxTurns(): number | undefined { return this.execution.maxTurns; }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`maxTurns` is the one getter that does not delegate to `state` — max-turns is execution config, not accumulated run state, and `this.execution.maxTurns` already holds the effective value (`effectiveMaxTurns`) the tracker was constructed with.
|
|
143
|
+
|
|
144
|
+
### Extracted-module interaction check (record-observer ↔ SubagentState)
|
|
145
|
+
|
|
146
|
+
The observer already targets `SubagentState` directly (Phase 17) and carries no `Subagent` dependency.
|
|
147
|
+
The new branches are Tell-Don't-Ask: the observer tells the state to mutate (`addActiveTool`, `incrementTurnCount`, …) and never reads-then-writes a field on the state.
|
|
148
|
+
No output arguments, no reach-through, no reverse search — the same shape as the existing three branches.
|
|
149
|
+
|
|
150
|
+
## Module-Level Changes
|
|
151
|
+
|
|
152
|
+
- `src/lifecycle/subagent-state.ts` — add `_turnCount`/`_activeTools`/`_toolKeySeq`/`_responseText` fields, their getters (`turnCount`, `activeTools`, `responseText`), and five transition methods (`incrementTurnCount`, `addActiveTool`, `removeActiveTool`, `resetResponseText`, `appendResponseText`).
|
|
153
|
+
Update the file header doc comment to note the live-activity fields.
|
|
154
|
+
- `src/observation/record-observer.ts` — add `tool_execution_start`, `turn_end`, `message_start`, `message_update` (text_delta) branches; add `removeActiveTool` to the existing `tool_execution_end` branch.
|
|
155
|
+
Update the JSDoc "Handles:" list.
|
|
156
|
+
- `src/lifecycle/subagent.ts` — add `turnCount`, `activeTools`, `responseText`, `maxTurns` getters.
|
|
157
|
+
- `src/lifecycle/subagent-state.ts` is referenced in `.pi/skills/package-pi-subagents/SKILL.md` and `docs/architecture/architecture.md` only as a value object / domain entry; no symbol is removed or renamed, so no skill/doc prose update is required.
|
|
158
|
+
The architecture doc's Phase 18 Step 1 entry already describes this work — no edit needed.
|
|
159
|
+
|
|
160
|
+
No existing export is removed or renamed; no call site changes.
|
|
161
|
+
The `AgentActivityTracker` / `ui-observer` modules are untouched (they are deleted in Step 3).
|
|
162
|
+
|
|
163
|
+
## Test Impact Analysis
|
|
164
|
+
|
|
165
|
+
This is an additive change, so the analysis is mostly about new coverage:
|
|
166
|
+
|
|
167
|
+
1. **New unit tests enabled.**
|
|
168
|
+
`subagent-state.test.ts` gains direct tests for the new fields/methods (turn count starts at 1 and increments; active-tool add/remove with concurrent same-name handling; response-text reset/append) — testable on a bare `SubagentState` with no session or executor.
|
|
169
|
+
`record-observer.test.ts` gains tests that emitting `tool_execution_start`/`turn_end`/`message_start`/`message_update`/`tool_execution_end` drives the new state fields, using the existing `createMockSession` `emit` harness.
|
|
170
|
+
2. **Tests that become redundant.**
|
|
171
|
+
None yet — `ui-observer.test.ts` and the tracker tests still pin live behavior until Step 3 deletes those modules.
|
|
172
|
+
The new tests overlap conceptually with `ui-observer.test.ts` but exercise a different module (`record-observer` → `SubagentState`), so both stay until the tracker is removed.
|
|
173
|
+
3. **Tests that must stay as-is.**
|
|
174
|
+
All existing `record-observer.test.ts` cases (tool uses, lifetime usage, compaction) genuinely exercise the observer→state path being extended and must keep passing unchanged.
|
|
175
|
+
`subagent.test.ts` getter/delegation tests stay; new getter tests are added beside them.
|
|
176
|
+
|
|
177
|
+
## Invariants at risk
|
|
178
|
+
|
|
179
|
+
Phase 17 Step 1 ([#373]) established that `SubagentState` owns all its own mutations (no field written from outside) and that `record-observer` targets `SubagentState` directly with no `Subagent` dependency.
|
|
180
|
+
|
|
181
|
+
- **Invariant: the value object owns every mutation** — pinned by the encapsulation tests in `subagent-state.test.ts` (fields are read-only getters; mutation only via methods).
|
|
182
|
+
The new fields follow the same private-field/getter/transition-method shape; new tests assert mutation flows only through the new methods.
|
|
183
|
+
- **Invariant: `record-observer` carries no `Subagent` dependency** — pinned by `record-observer.test.ts` constructing a bare `SubagentState` (never a `Subagent`).
|
|
184
|
+
The new branches keep that property (they call `state.*` only); the existing test setup (`makeState()` returns `new SubagentState(...)`) already guards it.
|
|
185
|
+
|
|
186
|
+
No earlier phase step's `Outcome:` is regressed — this step only adds to the record both prior steps consolidated onto.
|
|
187
|
+
|
|
188
|
+
## TDD Order
|
|
189
|
+
|
|
190
|
+
1. **`SubagentState` live-activity fields and transition methods.**
|
|
191
|
+
Surface: `test/lifecycle/subagent-state.test.ts`.
|
|
192
|
+
Red→green: new `describe` blocks for turn count (defaults to 1, `incrementTurnCount` increments), active tools (`addActiveTool` adds with unique keys for concurrent same-name tools, `removeActiveTool` removes first match and is a no-op when absent, getter is read-only), and response text (defaults to `""`, `resetResponseText` clears, `appendResponseText` concatenates).
|
|
193
|
+
Implement the fields/getters/methods in `src/lifecycle/subagent-state.ts`.
|
|
194
|
+
Commit: `feat: add live-activity fields to SubagentState (#420)`.
|
|
195
|
+
2. **`record-observer` populates the live-activity fields.**
|
|
196
|
+
Surface: `test/observation/record-observer.test.ts`.
|
|
197
|
+
Red→green: new tests that `tool_execution_start` adds an active tool, paired `tool_execution_end` removes it (extending the existing tool-uses test or a sibling case), `turn_end` increments `turnCount`, `message_start` resets `responseText`, and `message_update` text_delta appends; assert non-text_delta `message_update` is ignored.
|
|
198
|
+
Implement the new branches in `src/observation/record-observer.ts` and the `removeActiveTool` call in the existing `tool_execution_end` branch.
|
|
199
|
+
Commit: `feat: accumulate live activity in record-observer (#420)`.
|
|
200
|
+
3. **`Subagent` exposes the live-activity getters.**
|
|
201
|
+
Surface: `test/lifecycle/subagent.test.ts`.
|
|
202
|
+
Red→green: tests that `turnCount`/`activeTools`/`responseText` delegate to the owned state (seed via a supplied `SubagentState` or by driving the attached observer), and `maxTurns` returns `execution.maxTurns`.
|
|
203
|
+
Implement the four getters in `src/lifecycle/subagent.ts`.
|
|
204
|
+
Run `pnpm run check` after this commit — the getters are the public-facing surface this step adds.
|
|
205
|
+
Commit: `feat: expose live-activity getters on Subagent (#420)`.
|
|
206
|
+
|
|
207
|
+
Each step is independently green: the field exists before the observer writes it, and the observer writes it before `Subagent` exposes it.
|
|
208
|
+
No existing test breaks at any step (pure addition, no removed/renamed symbol).
|
|
209
|
+
|
|
210
|
+
## Risks and Mitigations
|
|
211
|
+
|
|
212
|
+
- **Risk: the folded fields drift from `AgentActivityTracker` semantics, making Step 2's swap not behavior-preserving.**
|
|
213
|
+
Mitigation: the transition methods are copied field-for-field (turn count starts at 1, `name_seq` keying, first-match removal, reset-then-append response text); Step 2's reader-migration tests will compare record getters against the tracker before deletion.
|
|
214
|
+
- **Risk: resume parity — the surviving `SubagentState` accumulates turn count across a resume while a fresh tracker would not.**
|
|
215
|
+
Mitigation: the tracker is *not* reset on resume today either (it is not reconstructed), so the record matches.
|
|
216
|
+
`resetForResume` is left unchanged and the decision is documented; Step 2 revisits if a reader needs a per-resume reset.
|
|
217
|
+
- **Risk: a second observer now writes the same conceptual fields (record-observer + ui-observer), briefly duplicating active-tool/turn-count bookkeeping.**
|
|
218
|
+
Mitigation: this is the intended tidy-first overlap; the duplicate (`ui-observer`) is removed in Step 3 ([#422]).
|
|
219
|
+
No consumer reads the record's copy yet, so the duplication is inert.
|
|
220
|
+
|
|
221
|
+
## Open Questions
|
|
222
|
+
|
|
223
|
+
- Whether `resetForResume` should clear the live-activity fields is deferred to Step 2 ([#421]), where the actual reader behavior on resume is observable.
|
|
224
|
+
Resolving it now would be speculative and would break the pure-addition contract.
|
|
225
|
+
|
|
226
|
+
[#421]: https://github.com/gotgenes/pi-packages/issues/421
|
|
227
|
+
[#422]: https://github.com/gotgenes/pi-packages/issues/422
|
|
228
|
+
[#373]: https://github.com/gotgenes/pi-packages/issues/373
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 378
|
|
3
|
+
issue_title: "Consolidate lifecycle test fixtures"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #378 — Consolidate lifecycle test fixtures
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-15T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned Phase 17 Step 7 — consolidating the lifecycle test clone families into shared/file-local helpers.
|
|
13
|
+
Produced `docs/plans/0378-consolidate-lifecycle-test-fixtures.md` with a six-step lift-and-shift TDD order and committed it.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The issue body is stale relative to `main`: it cites five families across six files (including `concurrency-queue.test.ts` and a 766-LOC `subagent-manager.test.ts`).
|
|
18
|
+
Measuring with `fallow dupes -r packages/pi-subagents` against today's `main` shows **four** lifecycle families — Steps 1–6 already removed the queue (`concurrency-queue.test.ts` → `concurrency-limiter.test.ts`) and the `subagent.test.ts`/`concurrency-limiter.test.ts` families.
|
|
19
|
+
The plan is written against the measured current state, not the issue snapshot.
|
|
20
|
+
- Design call: promote to `test/helpers/` only the genuinely cross-file duplication (the `createSubagentSession`-test mock-session builder, shared by `create-subagent-session.test.ts` and `create-subagent-session-extension-tools.test.ts` — `createFactorySession`).
|
|
21
|
+
The manager and `subagent-session` families are intra-file (fallow recommends same-file extraction), so they get file-local helpers.
|
|
22
|
+
Force-promoting intra-file families to `test/helpers/` would manufacture cross-file coupling that does not exist.
|
|
23
|
+
- Resisted extracting the `io.createSession.mockResolvedValue(...)` + `createSubagentSession(...)` invoke pair into a helper — two lines with per-test varying overrides; wrapping the system-under-test call would be procedure-splitting, not design improvement.
|
|
24
|
+
- Invariants at risk flagged: Step 1/Step 3's "every spawned agent has a `promise` at spawn" (pinned by the queued-promise test) and Step 3's "zero external `.promise`/`.notification` writes outside `subagent.ts`" (grep-verifiable).
|
|
25
|
+
`arrangeQueuedPair()` must return the queued id; Step 4 folds in a re-grep.
|
|
26
|
+
- Baseline: package test duplication 669 lines / 3.3% across 20 files; the four lifecycle families total ~122 lines, so Step 7 alone should land below the 600-line goal (~547).
|
|
27
|
+
Flagged as an Open Question pending the Step 6 `fallow dupes` measurement.
|
|
28
|
+
- Not breaking — test-only, no `src/`, public-surface, or behavior change.
|
|
29
|
+
|
|
30
|
+
## Stage: Implementation — TDD (2026-06-15T23:10:00Z)
|
|
31
|
+
|
|
32
|
+
### Session summary
|
|
33
|
+
|
|
34
|
+
Executed the lifecycle test fixture consolidation across 8 commits.
|
|
35
|
+
Added a shared `createFactorySession` builder, migrated the four lifecycle clone families, and (on operator steer) folded `create-subagent-session-extension-tools.test.ts` into `create-subagent-session.test.ts`, deleting the file.
|
|
36
|
+
Package test duplication dropped 669 → 512 lines (under the 600 goal); test count 1005 → 1010 (+5 `createFactorySession` self-tests); test files 64 → 63.
|
|
37
|
+
Pre-completion reviewer: PASS.
|
|
38
|
+
|
|
39
|
+
### Observations
|
|
40
|
+
|
|
41
|
+
- Plan premise was wrong on one point: extracting `createFactorySession` alone did **not** collapse the create-subagent-session families — the dominant clone was the arrange-act invoke block, not the builder.
|
|
42
|
+
I first extracted a `runCreate`/`runCreateWith` act-helper to hit the metric, then the operator flagged it: mixing arrange + act hides the system under test, and arranges should be grouped by `describe`.
|
|
43
|
+
Reworked to AAA — describe-scoped `beforeEach` for arrange, `createSubagentSession(...)` act kept explicit per test.
|
|
44
|
+
- The operator relaxed the roadmap's "lifecycle families ≤ 1" Outcome.
|
|
45
|
+
Two families remain by design (the repeated act with test-specific arrange); documented as intentional in `architecture.md` Step 7.
|
|
46
|
+
Lesson recorded: a clone-count metric is a weak signal for *test* code — AAA structure beats it, and chasing the metric produced the wrong abstraction before it was caught.
|
|
47
|
+
- `programTurns(session, listeners, turns)` is a legitimate arrange helper for the turn-limit tests (turn count is the meaningful input, not a discriminator flag); removed the restated-boundary comments per `code-design` (names/args over comments).
|
|
48
|
+
- Folding the extension-tools tests was safe because the recursion guard reads only the session mock's `getActiveToolNames`/`setActiveToolsByName`; the agent config and `type` don't affect those assertions (mocked `io.createSession` ignores `cfg.toolNames`).
|
|
49
|
+
- Surfaced three overlapping session-mock builders (`createMockSession`, `createSubagentSessionStub`, `createFactorySession`); filed [#412] as a follow-up rather than expanding #378 scope.
|
|
50
|
+
- Cross-step invariants verified intact: queued-agent "promise at spawn" ([#374]) test preserved through the `arrangeQueuedPair` extraction; zero external `.promise`/`.notification` writes in `test/lifecycle/`.
|
|
51
|
+
|
|
52
|
+
## Stage: Final Retrospective (2026-06-15T23:45:00Z)
|
|
53
|
+
|
|
54
|
+
### Session summary
|
|
55
|
+
|
|
56
|
+
Shipped Phase 17 Step 7 across plan → TDD → ship in one continuous session: 11 `test:`/`docs:` commits, package test duplication 669 → 512 lines, test files 64 → 63, tests 1005 → 1010.
|
|
57
|
+
The dominant arc was a metric-driven detour — an act-wrapping helper extracted to satisfy the roadmap's "families ≤ 1" target, caught by the operator and reworked into an AAA structure (describe-scoped arrange, visible act).
|
|
58
|
+
CI passed, issue closed, no release (test/docs commits only); follow-up [#412] filed.
|
|
59
|
+
|
|
60
|
+
### Observations
|
|
61
|
+
|
|
62
|
+
#### What went well
|
|
63
|
+
|
|
64
|
+
- The "no sacred cows" invitation → structured `ask_user` multi-select produced a real improvement: deleting the redundant `create-subagent-session-extension-tools.test.ts` entirely (#1) and filing [#412], rather than accepting the status quo.
|
|
65
|
+
Self-critique under explicit invitation surfaced a whole-file removal the plan had not considered.
|
|
66
|
+
- Verification was incremental, not end-only: `pnpm run check` plus a per-file `vitest run` plus a `fallow dupes` re-measurement ran between every TDD step, so each consolidation's effect on the clone count was visible immediately.
|
|
67
|
+
- Cross-step invariant discipline held: the queued-agent "promise at spawn" ([#374]) test survived the `arrangeQueuedPair` extraction, and the grep for external `.promise`/`.notification` writes stayed at zero.
|
|
68
|
+
- The release-timing gate correctly fired on the phased roadmap (Phase 17 Step 7 of 9) and surfaced the batch-vs-now choice instead of releasing silently.
|
|
69
|
+
|
|
70
|
+
#### What caused friction (agent side)
|
|
71
|
+
|
|
72
|
+
- `wrong-abstraction` — extracted a `runCreate`/`runCreateWith` act-helper that bundled arrange + the `createSubagentSession(...)` act, hiding the system under test, purely to drive the clone count to the roadmap's "families ≤ 1" target.
|
|
73
|
+
User-caught.
|
|
74
|
+
Impact: one commit (`96acc6c7`) committed then undone via `git reset --soft HEAD~1` and reworked into the AAA structure (`14375b07`); ~1 commit of rework, caught before ship.
|
|
75
|
+
- `missing-context` — the plan asserted `createFactorySession` was "the larger half of each clone" without decomposing the clone; the post-Step-3 `fallow dupes` measurement showed the arrange-act invoke block was the dominant half, which is what drove the wrong-abstraction detour.
|
|
76
|
+
Impact: a wrong planning premise that fed the rework above.
|
|
77
|
+
A `fallow dupes --trace <file:line>` per-line decomposition during planning would have shown the split.
|
|
78
|
+
- `other` (tool misuse) — first invoked `set_session_name` as a shell command (`command not found`), then self-corrected to the tool call.
|
|
79
|
+
Impact: one wasted call, no rework.
|
|
80
|
+
|
|
81
|
+
#### What caused friction (user side)
|
|
82
|
+
|
|
83
|
+
- The operator's redirect on arrange/act mixing landed after the act-helper was already committed.
|
|
84
|
+
The natural self-checkpoint was earlier: when the Step-3 measurement contradicted the plan's "builder is the larger half" premise, that contradiction was itself a deviation worth surfacing via `ask_user` ("wrap the act to hit the metric, or accept the residual for better structure?") rather than pushing forward to the helper.
|
|
85
|
+
Framed as opportunity: treat a plan's quantitative premise proving wrong as a stop-and-ask trigger, not a cue to escalate the abstraction.
|
|
86
|
+
|
|
87
|
+
### Diagnostic details
|
|
88
|
+
|
|
89
|
+
- **Model-performance correlation** — one subagent dispatch (`pre-completion-reviewer`) on a judgment-heavy review task; appropriate match, no mismatch.
|
|
90
|
+
- **Escalation-delay tracking** — no rabbit-hole: the wrong-abstraction was a single committed approach redirected by the operator, not a repeated-error loop, so no 5+ consecutive same-error sequence.
|
|
91
|
+
- **Unused-tool detection** — `fallow dupes --trace <file:line>` was available and would have decomposed the clone into builder-vs-act portions during planning; it was not used, and its absence is what let the wrong premise stand.
|
|
92
|
+
- **Feedback-loop gap analysis** — no gap; checks ran incrementally after each step.
|
|
93
|
+
|
|
94
|
+
### Changes made
|
|
95
|
+
|
|
96
|
+
1. `.pi/skills/testing/SKILL.md` (§ Test organization) — added a rule to group shared arrange in a describe-scoped `beforeEach`, keep the act explicit, and not wrap the system-under-test call to eliminate a duplication-metric clone.
|
|
97
|
+
2. `.pi/prompts/tdd-plan.md` — sharpened the deviation rule: a quantitative target (LOC, clone count, complexity) that does not fall out as predicted is a stop-and-ask deviation, re-decided via `ask_user` rather than forcing the number.
|
|
98
|
+
|
|
99
|
+
[#374]: https://github.com/gotgenes/pi-packages/issues/374
|
|
100
|
+
[#412]: https://github.com/gotgenes/pi-packages/issues/412
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 379
|
|
3
|
+
issue_title: "Consolidate UI and tools test fixtures"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #379 — Consolidate UI and tools test fixtures
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-16T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 9-step lift-and-shift plan for Phase 17 Step 8 — consolidating the non-lifecycle test clone families.
|
|
13
|
+
Measured the live baseline (`fallow dupes -r packages/pi-subagents`: 32 clone groups / 512 lines / 2.49%, seven families) and characterized each family by reading all seven test files, classifying each as genuine cross-file duplication (one) versus intra-file (six).
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The only genuine cross-file duplication is the `ResolvedSpawnConfig` builder shared by `foreground-runner.test.ts` and `background-spawner.test.ts` (`dup:80ee2004`) — the one promotion to `test/helpers/` (`make-spawn-config.ts`).
|
|
18
|
+
Everything else fallow scores same-file, so it stays file-local or moves into a describe-scoped `beforeEach`, per Step 7's ([#378]) discipline.
|
|
19
|
+
- The plan's central constraint is Step 7's hard-won lesson: never wrap the system-under-test **act** in a helper to chase a duplication metric.
|
|
20
|
+
Every extracted helper returns a value (`createResolvedSpawnConfig`, `disabledConfig`, `exploreConfig`, `createManagerStub`, `spawnAndWaitRegistering`) or seeds `beforeEach`; the acts stay inline.
|
|
21
|
+
Added an "Invariants at risk" grep check to enforce this at review.
|
|
22
|
+
- Scope was genuinely ambiguous and resolved via `ask_user`: the issue body names six files, but fallow reports a seventh non-lifecycle family in `test/service/service-adapter.test.ts` (four near-identical `SubagentManagerLike` stubs).
|
|
23
|
+
Operator chose to include service-adapter (seven files total) and to **not** bind a numeric group target — acceptance is "each named family consolidated, resulting fallow numbers reported."
|
|
24
|
+
My `ask_user` prompt incorrectly claimed the architecture roadmap's Step 8 `Targets` lists service-adapter; it does not (it lists the six issue-body files).
|
|
25
|
+
The plan corrects this and notes service-adapter was added in planning.
|
|
26
|
+
- The roadmap's stated Step 8 outcome ("clone groups 44 → ≤ 25; overall duplication ≤ 0.6%") predates Steps 1–7 and does not match the current fallow metric (2.49%); flagged as an Open Question rather than treated as binding.
|
|
27
|
+
- Non-Goals call out the two residual lifecycle families (Step 7 left them as the visible act), the 11-line production clone inside `src/ui/agent-config-editor.ts` (test-only issue), and the three overlapping session-mock builders ([#412]).
|
|
28
|
+
- `ResolvedSpawnConfig` is deeply nested (`identity`/`execution`/`presentation` with mirrored scalars).
|
|
29
|
+
Designed the shared builder to take **flat** options and assemble the nested structure internally — sidesteps the `Partial<T>` deep-merge trap and encapsulates the mirroring (`agentInvocation.runInBackground`, `presentation.detailBase`) the hand-built copies duplicate.
|
|
30
|
+
|
|
31
|
+
## Stage: Implementation — TDD (2026-06-16T13:10:00Z)
|
|
32
|
+
|
|
33
|
+
### Session summary
|
|
34
|
+
|
|
35
|
+
Executed all 9 plan steps as green-to-green lift-and-shift refactors (no red phase — the existing assertions were the spec), one commit per step plus the docs commit.
|
|
36
|
+
Promoted the shared `createResolvedSpawnConfig` builder to `test/helpers/` (+5 self-tests) and consolidated the six remaining families with file-local value-returning builders / `beforeEach` setup.
|
|
37
|
+
Test count 1010 → 1015; test files 63 → 64; clone groups 32 → 24; package test duplication 512 → 355 lines (2.49% → 1.73%).
|
|
38
|
+
|
|
39
|
+
### Observations
|
|
40
|
+
|
|
41
|
+
- The `agent-config-editor.test.ts` family did not collapse as the plan loosely estimated ("4 → ideally 2"): it stayed at 4 groups / 28 lines.
|
|
42
|
+
The `disabledConfig` helper removed only the small `enabled: false` arrange; the residual clones are the repeated `await editor.showAgentDetail(...)` **act** plus its `setupDetail` arrange and `ui.select.mock.calls` menu assertion — left inline because wrapping the system-under-test is the wrong abstraction (Step 7 lesson).
|
|
43
|
+
No `ask_user` was needed: acceptance was "consolidate and measure" (no binding numeric target), and the only way to force the number would violate the plan's stated discipline.
|
|
44
|
+
- `service-adapter.test.ts` had four near-identical `SubagentManagerLike` stub factories with diverging defaults (`hasRunning` true vs false, varying `spawn` returns).
|
|
45
|
+
Consolidated into one file-local `createManagerStub()` with **unannotated** return (typed `vi.fn<SubagentManagerLike[...]>()` stubs) so callers retain `Mock<...>` methods — per the testing-skill rule that annotating the return with the production interface, or spreading `Partial<Interface>` overrides, erases mock methods.
|
|
46
|
+
Dropped the overrides parameter entirely; per-test behavior is configured on the returned object (`mgr.getRecord.mockImplementation(...)`, `mgr.hasRunning.mockReturnValue(true)`).
|
|
47
|
+
Used `sed` to swap the `defaultManager()` / `createTestManager()` call sites after deleting the factory definitions.
|
|
48
|
+
- `ui-observer` update-counting tests moved from closure `let updateCount` counters to `vi.fn()` onUpdate with `toHaveBeenCalledTimes` — removes the repeated scaffolding without hiding the `session.emit` act.
|
|
49
|
+
- Pre-completion reviewer: WARN — sole finding was this missing TDD retro entry (now written); all deterministic checks PASS, cross-step invariant ("don't wrap the act") verified upheld, 6 Mermaid blocks validated, dead-code clean.
|
|
50
|
+
|
|
51
|
+
[#378]: https://github.com/gotgenes/pi-packages/issues/378
|
|
52
|
+
[#412]: https://github.com/gotgenes/pi-packages/issues/412
|