@gotgenes/pi-subagents 16.4.0 → 16.6.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 +16 -0
- package/dist/public.d.ts +7 -4
- package/docs/architecture/architecture.md +184 -209
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -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/plans/0421-migrate-activity-readers-to-record-getters.md +309 -0
- package/docs/retro/0380-extract-layered-settings-loader.md +49 -0
- package/docs/retro/0412-unify-session-mock-builders.md +80 -0
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +90 -0
- package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +56 -0
- package/package.json +1 -1
- package/src/index.ts +1 -3
- package/src/lifecycle/subagent-state.ts +49 -4
- package/src/lifecycle/subagent.ts +4 -0
- package/src/observation/notification.ts +3 -12
- package/src/observation/record-observer.ts +25 -1
- package/src/observation/subagent-events-observer.ts +1 -4
- package/src/tools/foreground-runner.ts +8 -4
- package/src/tools/helpers.ts +5 -4
- package/src/ui/agent-menu.ts +0 -12
- package/src/ui/agent-widget.ts +24 -5
- package/src/ui/conversation-viewer.ts +4 -9
- package/src/ui/widget-renderer.ts +14 -22
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 421
|
|
3
|
+
issue_title: "pi-subagents: migrate activity-tracker readers to the record getters"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Migrate activity-tracker readers to the record getters
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Phase 18 Step 1 ([#420]) folded the live-activity fields (`turnCount`, `activeTools`, `responseText`, `maxTurns`) onto the single owned run-state value object and exposed them as read-only getters on `Subagent`, but nothing reads them yet.
|
|
11
|
+
Five consumers still reach into a separate UI `AgentActivityTracker` for data the record now exposes directly — a Law-of-Demeter coupling that keeps the activity tier entangled with the core.
|
|
12
|
+
This step switches each reader from the tracker to the record getters so that no reader reads activity off `AgentActivityTracker`, unblocking the tracker's deletion in Step 3 ([#422]).
|
|
13
|
+
|
|
14
|
+
This is Phase 18 Step 2 of the architecture roadmap (`docs/architecture/architecture.md`).
|
|
15
|
+
It is a reader migration: the producer plumbing that still constructs trackers, subscribes `ui-observer`, and populates the runtime activity map (`foreground-runner`, `background-spawner`, `runtime.ts`, `ui-observer.ts`) is left in place and removed in Step 3.
|
|
16
|
+
The reader-held map dependencies in `notification`, `agent-widget`, and `agent-menu` — files Step 3 does not touch — are removed here.
|
|
17
|
+
|
|
18
|
+
## Goals
|
|
19
|
+
|
|
20
|
+
- Switch `widget-renderer` to read live activity off the `Subagent` records returned by `listAgents()` rather than a parallel `activityMap`, folding the `WidgetActivity` interface into `WidgetAgent`.
|
|
21
|
+
- Drop the `activity` constructor param from `ConversationViewer`; read `activeTools` / `responseText` off `this.record`.
|
|
22
|
+
- Drop the `AgentActivityReader` dependency from `AgentsMenuHandler`.
|
|
23
|
+
- Migrate `runForeground` and `buildDetails` to read streaming state (`turnCount`, `maxTurns`, `activeTools`, `responseText`) off the `Subagent` they already hold.
|
|
24
|
+
- Migrate `buildNotificationDetails` to read `turnCount` / `maxTurns` off the record, and drop the `agentActivity` map dependency (and the now-vestigial `cleanupCompleted` method) from `NotificationManager`.
|
|
25
|
+
- Project each `Subagent` into a pure-data `WidgetAgent` snapshot inside `AgentWidget` (with a `contextPercent` field), keeping `widget-renderer` a pure function of plain data.
|
|
26
|
+
|
|
27
|
+
This change is **not breaking**.
|
|
28
|
+
The published service surface (`src/service/service.ts`) exposes `SubagentRecord` / `SubagentStatus`, neither of which changes.
|
|
29
|
+
There is one accepted observable behavior change in the widget (below), but no API, config, or default changes on upgrade without a user edit.
|
|
30
|
+
|
|
31
|
+
### Accepted behavior change
|
|
32
|
+
|
|
33
|
+
Today the widget shows **no** turn count on finished-agent lines because the activity tracker is deleted on completion (foreground deletes it after `spawnAndWait`; `NotificationManager.sendCompletion` deletes it for background agents).
|
|
34
|
+
The record's `turnCount` getter persists, so once `widget-renderer` reads off `listAgents()` records, finished lines will show `⟳N`.
|
|
35
|
+
This is **accepted** (confirmed with the operator): the deletion-on-completion behavior was a UI-state artifact, and showing the turn count consistently is the more correct outcome.
|
|
36
|
+
Preserving the old behavior would require the widget to track which finished agents were "cleaned up" — reintroducing exactly the coupling this phase removes.
|
|
37
|
+
|
|
38
|
+
## Non-Goals
|
|
39
|
+
|
|
40
|
+
- Deleting `AgentActivityTracker` / `ui-observer`, or removing the producer plumbing in `foreground-runner` / `background-spawner` / `runtime.ts` that still constructs trackers, subscribes, and populates the activity map — Step 3 ([#422]).
|
|
41
|
+
- Dropping the `widget` and `agentActivity` constructor params from `AgentTool` — Step 5 ([#424]).
|
|
42
|
+
- Making the widget self-drive its timer from lifecycle events instead of spawn-tool calls — Step 4 ([#423]).
|
|
43
|
+
- Reconciling the public event contract (`SUBAGENT_EVENTS.ACTIVITY`) — Step 6 ([#425]).
|
|
44
|
+
- Changing `SubagentState`, `record-observer`, or the getters themselves — landed in Step 1 ([#420]).
|
|
45
|
+
|
|
46
|
+
## Background
|
|
47
|
+
|
|
48
|
+
Relevant modules and how they relate:
|
|
49
|
+
|
|
50
|
+
- `src/lifecycle/subagent.ts` — exposes the four read-only getters added in Step 1: `turnCount` and `activeTools` and `responseText` delegate to the owned `SubagentState`; `maxTurns` returns `this.execution.maxTurns`.
|
|
51
|
+
Also exposes `getContextPercent(): number | null` (delegates to `subagentSession.getContextPercent()`), the live context-window utilization.
|
|
52
|
+
- `src/ui/widget-renderer.ts` — pure rendering functions.
|
|
53
|
+
`renderFinishedLine` / `renderRunningLines` take a `WidgetAgent` plus a `WidgetActivity | undefined`; `renderWidgetLines` takes an `activityMap: ReadonlyMap<string, WidgetActivity>`.
|
|
54
|
+
`renderRunningLines` computes the context percent via `activity?.session ? getSessionContextPercent(activity.session) : null` — a reach into session stats from a module documented as taking no SDK types.
|
|
55
|
+
- `src/ui/agent-widget.ts` — `AgentWidget` holds the `agentActivity` map and passes `manager.listAgents()` (Subagents, structurally accepted as `WidgetAgent`) plus the map to `renderWidgetLines`.
|
|
56
|
+
- `src/ui/conversation-viewer.ts` — `ConversationViewer` takes an `activity: AgentActivityTracker | undefined`; reads `activity.activeTools` / `activity.responseText` in `buildContentLines` for the running streaming indicator.
|
|
57
|
+
- `src/ui/agent-menu.ts` — defines `AgentActivityReader` (`get(id): AgentActivityTracker | undefined`), takes it as a constructor param, and reads `this.agentActivity.get(record.id)` in `viewAgentConversation` to pass to `ConversationViewer`.
|
|
58
|
+
- `src/tools/foreground-runner.ts` — constructs `fgState = new AgentActivityTracker(...)`, subscribes `ui-observer`, populates the activity map, and reads `fgState.turnCount` / `fgState.maxTurns` / `fgState.activeTools` / `fgState.responseText` in `streamUpdate`; passes `fgState` to `buildDetails`.
|
|
59
|
+
- `src/tools/helpers.ts` — `buildDetails(base, record, activity?, overrides?)` reads `activity?.turnCount` and `activity?.maxTurns`.
|
|
60
|
+
Two call sites: `runForeground` (passes `fgState`) and `AgentTool` resume (no activity arg).
|
|
61
|
+
- `src/observation/notification.ts` — `buildNotificationDetails(record, resultMaxLen, activity?)` reads `activity?.turnCount` / `activity?.maxTurns`.
|
|
62
|
+
`NotificationManager` holds the `agentActivity` map and deletes from it in `sendCompletion` and `cleanupCompleted`.
|
|
63
|
+
- `src/observation/subagent-events-observer.ts` — `onSubagentCompleted` calls `notifications.cleanupCompleted(record.id)` when `record.notification?.resultConsumed`, else `notifications.sendCompletion(record)`.
|
|
64
|
+
- `src/index.ts` — constructs `NotificationManager`, `AgentWidget`, and `AgentsMenuHandler`, each currently passed `runtime.agentActivity`.
|
|
65
|
+
|
|
66
|
+
AGENTS.md / code-design constraints that apply:
|
|
67
|
+
|
|
68
|
+
- `widget-renderer.ts` is a pure rendering module ("stateless: they receive data and return formatted strings.
|
|
69
|
+
No timers, no SDK types, no side effects").
|
|
70
|
+
The migration keeps it pure and **narrows** it: the precomputed `contextPercent` field lets it drop the `SessionLike` / `getSessionContextPercent` imports, removing the session reach-through.
|
|
71
|
+
- `AgentWidget` is the adapter between the live manager/records and the pure renderer; the explicit `Subagent` → `WidgetAgent` projection belongs there (DIP, narrow data boundary).
|
|
72
|
+
- `getSessionContextPercent` and `SessionLike` stay in `usage.ts` — still used by `subagent-session.ts` and `getSessionTokens`, so removing the widget-renderer import creates no dead code.
|
|
73
|
+
|
|
74
|
+
## Design Overview
|
|
75
|
+
|
|
76
|
+
### `WidgetAgent` absorbs the activity fields (widget-renderer)
|
|
77
|
+
|
|
78
|
+
`WidgetActivity` is removed; its fields move onto `WidgetAgent` as plain data, plus a precomputed context percent:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
export interface WidgetAgent {
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly type: SubagentType;
|
|
84
|
+
readonly status: string;
|
|
85
|
+
readonly description: string;
|
|
86
|
+
readonly toolUses: number;
|
|
87
|
+
readonly startedAt: number;
|
|
88
|
+
readonly completedAt?: number;
|
|
89
|
+
readonly error?: string;
|
|
90
|
+
readonly lifetimeUsage?: Readonly<LifetimeUsage>;
|
|
91
|
+
readonly compactionCount: number;
|
|
92
|
+
// Folded-in live activity (was WidgetActivity)
|
|
93
|
+
readonly turnCount: number;
|
|
94
|
+
readonly maxTurns?: number;
|
|
95
|
+
readonly activeTools: ReadonlyMap<string, string>;
|
|
96
|
+
readonly responseText: string;
|
|
97
|
+
readonly contextPercent: number | null;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`renderFinishedLine(agent, registry, theme)` and `renderRunningLines(agent, registry, spinnerFrame, theme)` drop the `WidgetActivity | undefined` parameter and read `agent.turnCount` / `agent.maxTurns` / `agent.activeTools` / `agent.responseText` directly.
|
|
102
|
+
`renderRunningLines` uses `agent.contextPercent` in place of `activity?.session ? getSessionContextPercent(activity.session) : null`.
|
|
103
|
+
`renderWidgetLines` and `buildSections` drop the `activityMap` parameter.
|
|
104
|
+
Because every `WidgetAgent` now carries activity, the old "no activity provided → omit turn count / show `thinking…`" branch is gone — `describeActivity(emptyMap, "")` already returns the `thinking…` string for an idle running agent, so the running-line text is preserved.
|
|
105
|
+
|
|
106
|
+
### `AgentWidget` projects records into snapshots
|
|
107
|
+
|
|
108
|
+
`AgentWidget` drops its `agentActivity` constructor param and field.
|
|
109
|
+
`renderWidget` maps each record through a local projection before handing the array to the pure renderer:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
private toWidgetAgent(record: Subagent): WidgetAgent {
|
|
113
|
+
return {
|
|
114
|
+
id: record.id,
|
|
115
|
+
type: record.type,
|
|
116
|
+
status: record.status,
|
|
117
|
+
description: record.description,
|
|
118
|
+
toolUses: record.toolUses,
|
|
119
|
+
startedAt: record.startedAt,
|
|
120
|
+
completedAt: record.completedAt,
|
|
121
|
+
error: record.error,
|
|
122
|
+
lifetimeUsage: record.lifetimeUsage,
|
|
123
|
+
compactionCount: record.compactionCount,
|
|
124
|
+
turnCount: record.turnCount,
|
|
125
|
+
maxTurns: record.maxTurns,
|
|
126
|
+
activeTools: record.activeTools,
|
|
127
|
+
responseText: record.responseText,
|
|
128
|
+
contextPercent: record.getContextPercent(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`renderWidget` then calls `renderWidgetLines({ agents: this.manager.listAgents().map(r => this.toWidgetAgent(r)), registry, … })` with no `activityMap`.
|
|
134
|
+
`assembleWidgetState` keeps consuming raw records via its `AgentSummary` shape (id / status / completedAt) — unchanged.
|
|
135
|
+
This makes the snapshot boundary explicit: the renderer depends only on plain data; `AgentWidget` owns the projection (Tell-Don't-Ask — it asks each record once and hands forward a value).
|
|
136
|
+
|
|
137
|
+
### `ConversationViewer` reads off the record
|
|
138
|
+
|
|
139
|
+
The `activity` constructor param and field are removed.
|
|
140
|
+
`buildContentLines` reads the streaming indicator inputs off the record it already holds:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
if (this.record.status === "running") {
|
|
144
|
+
lines.push(...formatStreamingIndicator(
|
|
145
|
+
this.record.activeTools,
|
|
146
|
+
this.record.responseText,
|
|
147
|
+
width,
|
|
148
|
+
th,
|
|
149
|
+
));
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The `&& this.activity` guard is dropped — a running record always exposes `activeTools` / `responseText` (empty / `""` when idle), and `describeActivity` renders the idle case as `thinking…`, matching the prior no-tracker fallback.
|
|
154
|
+
|
|
155
|
+
### `AgentsMenuHandler` drops the reader
|
|
156
|
+
|
|
157
|
+
The `AgentActivityReader` interface and the `agentActivity` constructor param are removed.
|
|
158
|
+
`viewAgentConversation` no longer looks up a tracker; it constructs `ConversationViewer` without an `activity` field.
|
|
159
|
+
|
|
160
|
+
### `buildDetails` / `runForeground` read off the record
|
|
161
|
+
|
|
162
|
+
`buildDetails` drops its `activity?: AgentActivityTracker` parameter; the structural `record` param gains `turnCount?: number` and `maxTurns?: number`, and the body reads `record.turnCount` / `record.maxTurns`:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
export function buildDetails(
|
|
166
|
+
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
167
|
+
record: {
|
|
168
|
+
toolUses: number; startedAt: number; completedAt?: number; status: string;
|
|
169
|
+
error?: string; id?: string; lifetimeUsage: LifetimeUsage;
|
|
170
|
+
turnCount?: number; maxTurns?: number;
|
|
171
|
+
},
|
|
172
|
+
overrides?: Partial<AgentDetails>,
|
|
173
|
+
): AgentDetails { … turnCount: record.turnCount, maxTurns: record.maxTurns … }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`Subagent` satisfies this structurally (the getters supply `turnCount` / `maxTurns`).
|
|
177
|
+
In `runForeground`, `streamUpdate` reads `recordRef?.turnCount ?? 1`, `recordRef?.activeTools ?? new Map()`, `recordRef?.responseText ?? ""`, and `execution.effectiveMaxTurns` for `maxTurns` (the runner already holds this constant; `recordRef` is undefined until `onSessionCreated`).
|
|
178
|
+
The final `buildDetails(presentation.detailBase, record, fgState, { tokens })` becomes `buildDetails(presentation.detailBase, record, { tokens })`.
|
|
179
|
+
The `fgState` tracker, its `subscribeUIObserver` subscription, and the `agentActivity.set` / `delete` calls remain (producer plumbing removed in Step 3) but are no longer read.
|
|
180
|
+
|
|
181
|
+
### `buildNotificationDetails` / `NotificationManager`
|
|
182
|
+
|
|
183
|
+
`buildNotificationDetails(record, resultMaxLen)` drops the `activity?` param and reads `record.turnCount` / `record.maxTurns`.
|
|
184
|
+
`NotificationManager` drops the `agentActivity` constructor param and field; `sendCompletion` keeps only the nudge scheduling.
|
|
185
|
+
`cleanupCompleted` only ever deleted a map entry, so it becomes vestigial and is **removed** from both `NotificationSystem` and `NotificationManager`.
|
|
186
|
+
`SubagentEventsObserver.onSubagentCompleted` updates its `resultConsumed` branch to simply `return` (skip the nudge) instead of calling `cleanupCompleted`.
|
|
187
|
+
|
|
188
|
+
### Consumer call-site check (index.ts)
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const notifications = new NotificationManager((msg, opts) => pi.sendMessage(msg, opts));
|
|
192
|
+
const widget = new AgentWidget(manager, registry);
|
|
193
|
+
const agentsMenu = new AgentsMenuHandler(manager, registry, settings, new FsAgentFileOps(), …);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
All three constructions drop the `runtime.agentActivity` argument.
|
|
197
|
+
`runtime.agentActivity` itself stays (still written by the spawn tools; removed in Step 3).
|
|
198
|
+
|
|
199
|
+
## Module-Level Changes
|
|
200
|
+
|
|
201
|
+
- `src/ui/widget-renderer.ts` — remove the `WidgetActivity` interface; fold its fields plus `contextPercent` into `WidgetAgent`; drop the `WidgetActivity | undefined` param from `renderFinishedLine` / `renderRunningLines`; drop `activityMap` from `buildSections` / `renderWidgetLines`; remove the `SessionLike` and `getSessionContextPercent` imports (keep `getLifetimeTotal`).
|
|
202
|
+
- `src/ui/agent-widget.ts` — drop the `agentActivity` constructor param and field; add the `toWidgetAgent` projection; map `listAgents()` through it in `renderWidget`; drop the `AgentActivityTracker` import.
|
|
203
|
+
- `src/ui/conversation-viewer.ts` — drop the `activity` field and `ConversationViewerOptions.activity`; read `activeTools` / `responseText` off `this.record`; drop the `AgentActivityTracker` import.
|
|
204
|
+
- `src/ui/agent-menu.ts` — remove the `AgentActivityReader` interface and the `agentActivity` constructor param; drop the tracker lookup in `viewAgentConversation`; drop the `AgentActivityTracker` import.
|
|
205
|
+
- `src/tools/helpers.ts` — `buildDetails` drops the `activity?` param and reads `record.turnCount` / `record.maxTurns` (param type gains the two optional fields); drop the `AgentActivityTracker` import.
|
|
206
|
+
- `src/tools/foreground-runner.ts` — `streamUpdate` and the final `buildDetails` call read off `recordRef` / `record`; the `fgState` construction, `subscribeUIObserver`, and map writes stay (Step 3).
|
|
207
|
+
- `src/observation/notification.ts` — `buildNotificationDetails` drops the `activity?` param and reads off the record; `NotificationManager` drops the `agentActivity` param/field and the `delete` calls; remove `cleanupCompleted` from `NotificationSystem` and the class; drop the `AgentActivityTracker` import.
|
|
208
|
+
- `src/observation/subagent-events-observer.ts` — `onSubagentCompleted` `resultConsumed` branch returns without calling `cleanupCompleted`.
|
|
209
|
+
- `src/index.ts` — drop `runtime.agentActivity` from the `NotificationManager`, `AgentWidget`, and `AgentsMenuHandler` constructions.
|
|
210
|
+
- `.pi/skills/package-pi-subagents/SKILL.md` — update the module-dependency-flow line `widget ─polls─→ AgentActivityTracker map` to reflect that the widget now polls `listAgents()` records.
|
|
211
|
+
- `docs/architecture/architecture.md` — add a `Landed:` line to the Phase 18 Step 2 entry during implementation (the entry text already describes the work).
|
|
212
|
+
|
|
213
|
+
Test files (see TDD Order for which step each lands in):
|
|
214
|
+
|
|
215
|
+
- `test/helpers/make-subagent.ts` — add `turnCount` / `activeTools` / `responseText` / `maxTurns` shorthands to `createTestSubagent` so reader tests can seed activity on records.
|
|
216
|
+
- `test/widget-renderer.test.ts` — remove `WidgetActivity` / `makeActivity`; fold activity defaults into `makeAgent` (`turnCount`, `maxTurns`, `activeTools`, `responseText`, `contextPercent`); drop the `activity` arg from render calls and `activityMap` from `renderWidgetLines` calls; update the former "omit turn count when no activity" cases to assert the turn count now renders; convert the session-stats context test to a `contextPercent` field.
|
|
217
|
+
- `test/ui/agent-widget.test.ts` — drop the `agentActivity` arg from `new AgentWidget(...)`; add a projection test asserting `renderWidget` reads activity off records.
|
|
218
|
+
- `test/conversation-viewer.test.ts` — drop `activity` from `TestViewerOptions` / `createTestViewer`; seed the running-indicator test's `activeTools` / `responseText` via `createTestSubagent`; drop the `AgentActivityTracker` import.
|
|
219
|
+
- `test/ui/agent-menu.test.ts` — drop the `agentActivity` arg from `new AgentsMenuHandler(...)`.
|
|
220
|
+
- `test/tools/helpers.test.ts` — drop the `activity` arg from `buildDetails` calls; seed `turnCount` / `maxTurns` via `createTestSubagent`; drop the `AgentActivityTracker` import.
|
|
221
|
+
- `test/tools/foreground-runner.test.ts` — adjust any `details.turnCount` assertions to the record's value (turn count `1` default matches the prior tracker default, so most cases are unchanged); the activity-map registration test stays (producer plumbing remains).
|
|
222
|
+
- `test/observation/notification.test.ts` — drop `agentActivity` from `makeArgs` and `new NotificationManager(...)`; drop the `activity` arg from `buildNotificationDetails` calls and seed `turnCount` / `maxTurns` via `createTestSubagent`; remove the two map-cleanup assertions and the `cleanupCompleted` test.
|
|
223
|
+
- `test/observation/subagent-events-observer.test.ts` — update any assertion that `cleanupCompleted` is called on `resultConsumed` to assert no nudge is sent instead.
|
|
224
|
+
|
|
225
|
+
`test/runtime.test.ts` and `test/helpers/make-deps.ts` keep their `AgentActivityTracker` map usage — the runtime map and the `AgentTool` `agentActivity` access survive this step (removed in Steps 3 / 5).
|
|
226
|
+
Completed historical plans under `docs/plans/` are point-in-time records and are not edited.
|
|
227
|
+
|
|
228
|
+
## Test Impact Analysis
|
|
229
|
+
|
|
230
|
+
1. **New unit tests enabled.**
|
|
231
|
+
`agent-widget.test.ts` can now assert the projection directly — that `renderWidget` surfaces a record's `turnCount` / `activeTools` / `contextPercent` without any external map — because activity lives on the record the manager stub returns.
|
|
232
|
+
`widget-renderer.test.ts` tests get simpler: a single `makeAgent` carries everything, removing the two-object (`WidgetAgent` + `WidgetActivity`) arrangement.
|
|
233
|
+
2. **Tests that become redundant / simplified.**
|
|
234
|
+
The `widget-renderer` "omits turn count when no activity provided" cases lose their premise (there is no activity-absent path); they convert to asserting the turn count renders.
|
|
235
|
+
The `notification` "sendCompletion cleans up activity" and "cleanupCompleted removes activity" cases lose their subject (the map is gone) and are removed.
|
|
236
|
+
`ui-observer.test.ts` and `agent-activity-tracker.test.ts` stay — they pin the producer behavior still wired until Step 3.
|
|
237
|
+
3. **Tests that must stay as-is.**
|
|
238
|
+
`foreground-runner.test.ts`'s activity-map registration test (the tracker is still constructed and registered), and `runtime.test.ts`'s `agentActivity` map presence test, genuinely exercise the producer plumbing this step does not remove.
|
|
239
|
+
|
|
240
|
+
## Invariants at risk
|
|
241
|
+
|
|
242
|
+
- **Step 1 ([#420]) outcome — `Subagent` is the single home for run state; getters available for migration.**
|
|
243
|
+
This step is the consumer of that outcome; the getters' parity with the old tracker (turn count starts at `1`, `name_seq` active-tool keying, reset-then-append response text) is what makes the swap behavior-preserving for running agents.
|
|
244
|
+
Pinned by the existing `subagent-state.test.ts` / `record-observer.test.ts` cases from Step 1 plus the reader tests migrated here.
|
|
245
|
+
- **`widget-renderer` purity — pure functions, plain data, no SDK types** (established when the renderer was split out, plan 0148).
|
|
246
|
+
The migration preserves and strengthens it: the `contextPercent` field removes the `getSessionContextPercent(session)` reach-through, so the renderer no longer touches session stats.
|
|
247
|
+
Pinned by `widget-renderer.test.ts` constructing plain `WidgetAgent` objects (no class instances).
|
|
248
|
+
- **`NotificationManager` construction graph stays a cycle-free DAG with no widget dependency** (index.ts comment).
|
|
249
|
+
Dropping the `agentActivity` map narrows its deps further; no new dependency is added.
|
|
250
|
+
|
|
251
|
+
No earlier phase step's documented `Outcome:` is regressed — the producer plumbing Step 1's tidy-first overlap left in place is untouched here.
|
|
252
|
+
|
|
253
|
+
## TDD Order
|
|
254
|
+
|
|
255
|
+
1. **Preparatory: seed activity on test records.**
|
|
256
|
+
Surface: `test/helpers/make-subagent.ts` (+ a focused assertion in `test/helpers/make-deps.test.ts` or `make-subagent` coverage if one exists).
|
|
257
|
+
Add `turnCount` / `activeTools` / `responseText` / `maxTurns` shorthands to `createTestSubagent` (drive `state.incrementTurnCount` / `addActiveTool` / `appendResponseText`; thread `maxTurns` into `makeStubExecution`).
|
|
258
|
+
Pure test-helper addition — no production change; makes the reader-migration steps easy ("make the change that makes the change easy").
|
|
259
|
+
Commit: `test: seed live-activity on createTestSubagent (#421)`.
|
|
260
|
+
2. **Migrate the widget reader.**
|
|
261
|
+
Surface: `test/widget-renderer.test.ts`, `test/ui/agent-widget.test.ts`.
|
|
262
|
+
Red→green: update `makeAgent` to carry activity, drop the `activity` arg / `activityMap`, assert finished lines now render the turn count, assert the `AgentWidget` projection.
|
|
263
|
+
Implement the `WidgetAgent` fold + `contextPercent`, the render-signature changes, and the `AgentWidget.toWidgetAgent` projection; update the `new AgentWidget(...)` call in `index.ts` and the SKILL.md flow line.
|
|
264
|
+
This step lands the accepted behavior change (finished-agent turn count), so the commit is `feat:`, not `refactor:`.
|
|
265
|
+
Run `pnpm run check` after this commit — `renderWidgetLines` is the shared signature changed.
|
|
266
|
+
Commit: `feat: read widget activity off subagent records (#421)`.
|
|
267
|
+
3. **Migrate the conversation viewer and `/agents` menu.**
|
|
268
|
+
Surface: `test/conversation-viewer.test.ts`, `test/ui/agent-menu.test.ts`.
|
|
269
|
+
Red→green: drop `activity` from the viewer factory and seed the running-indicator inputs on the record; drop the `agentActivity` arg from the menu handler construction.
|
|
270
|
+
Implement: drop `ConversationViewer.activity`; read off `this.record`; remove `AgentActivityReader` and the `viewAgentConversation` lookup; update the `new AgentsMenuHandler(...)` call in `index.ts`.
|
|
271
|
+
The viewer constructor signature and the menu constructor signature both change with their sole call sites in the same commit.
|
|
272
|
+
Commit: `refactor: read conversation-viewer activity off the record (#421)`.
|
|
273
|
+
4. **Migrate `buildDetails` and the foreground runner.**
|
|
274
|
+
Surface: `test/tools/helpers.test.ts`, `test/tools/foreground-runner.test.ts`.
|
|
275
|
+
Red→green: drop the `activity` arg from `buildDetails` calls and seed `turnCount` / `maxTurns` on the record; confirm the foreground `details` still report the record's turn count.
|
|
276
|
+
Implement: `buildDetails` param/body change (both call sites — `runForeground` and the `AgentTool` resume path — compile in the same commit), and `runForeground`'s `streamUpdate` / final `buildDetails` reads.
|
|
277
|
+
Commit: `refactor: build agent details from the record, not the tracker (#421)`.
|
|
278
|
+
5. **Migrate notifications and drop the activity map there.**
|
|
279
|
+
Surface: `test/observation/notification.test.ts`, `test/observation/subagent-events-observer.test.ts`.
|
|
280
|
+
Red→green: drop `agentActivity` from the notification fixtures and the `activity` arg from `buildNotificationDetails`; remove the map-cleanup and `cleanupCompleted` cases; assert the observer's `resultConsumed` branch sends no nudge.
|
|
281
|
+
Implement: `buildNotificationDetails` reads off the record; `NotificationManager` drops the map param/field and `cleanupCompleted`; `NotificationSystem` drops `cleanupCompleted`; `SubagentEventsObserver` returns in the `resultConsumed` branch; update the `new NotificationManager(...)` call in `index.ts`.
|
|
282
|
+
Run `pnpm run check` after this commit — `NotificationSystem` is a shared interface and `index.ts` is its construction site.
|
|
283
|
+
Commit: `refactor: build notification details from the record (#421)`.
|
|
284
|
+
|
|
285
|
+
After Step 5, no reader reads activity off `AgentActivityTracker`; the tracker, `ui-observer`, and the runtime map remain only as inert producer plumbing for Step 3 ([#422]).
|
|
286
|
+
|
|
287
|
+
## Risks and Mitigations
|
|
288
|
+
|
|
289
|
+
- **Risk: the running-line idle text changes.**
|
|
290
|
+
Once `activeTools` / `responseText` always come off the record, an idle running agent renders via `describeActivity(new Map(), "")` instead of the old no-tracker `thinking…` fallback.
|
|
291
|
+
Mitigation: `describeActivity` already returns `thinking…` for an empty map and blank text, so the output is preserved; the migrated `widget-renderer` test pins it.
|
|
292
|
+
- **Risk: transient background-tracker map leak between Steps 2 and 3.**
|
|
293
|
+
`NotificationManager` no longer deletes finished agents' trackers from the runtime map, and background agents are no longer cleaned up by anyone until the map is removed.
|
|
294
|
+
Mitigation: after this step the map is write-only and read by nobody, so the leak is inert and short-lived; Step 3 ([#422]) removes the map entirely.
|
|
295
|
+
- **Risk: `streamUpdate` runs before `recordRef` is set (foreground).**
|
|
296
|
+
The initial `streamUpdate()` and the first spinner ticks fire before `onSessionCreated` assigns `recordRef`.
|
|
297
|
+
Mitigation: read with defaults that match the tracker's initial values (`turnCount ?? 1`, empty `activeTools`, `"" `responseText, `execution.effectiveMaxTurns` for `maxTurns`).
|
|
298
|
+
- **Risk: removing `cleanupCompleted` changes the observer's consumed-result path.**
|
|
299
|
+
Mitigation: the method only deleted a map entry; the observer's intent (skip the nudge when the result was already consumed) is preserved by an early `return`, pinned by the updated observer test.
|
|
300
|
+
|
|
301
|
+
## Open Questions
|
|
302
|
+
|
|
303
|
+
- Whether the `NotificationSystem` interface should be narrowed further (e.g. to a two-method completion notifier) now that `cleanupCompleted` is gone is deferred — it is orthogonal to this step and best judged once Steps 3–5 settle the surrounding wiring.
|
|
304
|
+
|
|
305
|
+
[#420]: https://github.com/gotgenes/pi-packages/issues/420
|
|
306
|
+
[#422]: https://github.com/gotgenes/pi-packages/issues/422
|
|
307
|
+
[#423]: https://github.com/gotgenes/pi-packages/issues/423
|
|
308
|
+
[#424]: https://github.com/gotgenes/pi-packages/issues/424
|
|
309
|
+
[#425]: https://github.com/gotgenes/pi-packages/issues/425
|
|
@@ -46,3 +46,52 @@ Follow-up issue [#415] created for the worktrees migration.
|
|
|
46
46
|
- The `rollup.dts.config.mjs` array-of-configs approach worked without incident: both bundles (`dist/public.d.ts` and `dist/settings.d.ts`) are self-contained and `verify:public-types` confirmed both probes type-check against the packaged tarball.
|
|
47
47
|
- The `satisfies LayeredSettingsSource<SubagentsSettings>` annotation at the `loadSettings` call site serves double duty: validates the object literal and keeps `LayeredSettingsSource` referenced for fallow dead-code (fallow confirmed: 0 issues).
|
|
48
48
|
- Follow-up issue [#415] created before the TDD stage notes were written (operator requested it during the session); architecture doc updated with the `[#415]` reference and link definition.
|
|
49
|
+
|
|
50
|
+
## Stage: Final Retrospective (2026-06-16T18:00:00Z)
|
|
51
|
+
|
|
52
|
+
### Session summary
|
|
53
|
+
|
|
54
|
+
Shipped issue #380 end-to-end across four stages (plan → TDD → ship) in one session: extracted `loadLayeredSettings<T>` into `src/layered-settings.ts`, published it at the `@gotgenes/pi-subagents/settings` subpath, and adopted it internally in `settings.ts`.
|
|
55
|
+
Released as `pi-subagents-v16.4.0`; CI passed first try; pre-completion reviewer returned PASS.
|
|
56
|
+
Notably clean run — no rework, no failed commits, no rabbit-holes.
|
|
57
|
+
|
|
58
|
+
### Observations
|
|
59
|
+
|
|
60
|
+
#### What went well
|
|
61
|
+
|
|
62
|
+
- The two-stage `ask_user` planning gate worked as designed: it surfaced the binary (extract vs. suppress) neutrally, the operator engaged deeply (asked for explicit for/against before committing), and the answers drove the plan's Goals rather than the issue body.
|
|
63
|
+
This is the intended use of `ask_user` for an operator-authored issue framed as "weigh two options."
|
|
64
|
+
- Proactive tool-mechanism research paid off: `web_search` + `fetch_content` on the fallow docs during planning revealed that `code-duplication` suppression is **file-scoped only** (no line-level directive), which would have changed Option 2's shape had it been chosen.
|
|
65
|
+
Checking the tool's real surface before planning around it caught a latent wrong assumption.
|
|
66
|
+
- The plan's honestly-hedged "Outcome caveat" (clone might persist until the worktrees follow-up) resolved favourably — the parametrised helper diverged below fallow's threshold immediately.
|
|
67
|
+
Hedging a quantitative prediction rather than over-promising left no credibility gap when the better outcome landed.
|
|
68
|
+
- Incremental verification cadence: `pnpm run check` ran right after the interface-adjacent Step 2, vitest ran per-file in each Red/Green, and `build:types` + `verify:public-types` ran inside Step 3 — not deferred to the end.
|
|
69
|
+
No feedback-loop gaps.
|
|
70
|
+
|
|
71
|
+
#### What caused friction (agent side)
|
|
72
|
+
|
|
73
|
+
- `missing-context` (process gap) — the plan's Open Questions flagged the worktrees follow-up issue as "created at ship time," but the `ship-issue` prompt has no step for creating a deferred follow-up.
|
|
74
|
+
During shipping I went straight to close + release; the operator had to prompt ("What about the follow up issue…") to trigger #415's creation.
|
|
75
|
+
Impact: one extra user turn, no rework — but the #380 close comment references #415, so the follow-up had to exist before close, making the ordering load-bearing.
|
|
76
|
+
- `instruction-violation` (self-identified, no rework) — the new test used `String(spy.mock.calls[0]![0])`, which the `testing` skill explicitly warns against ("Assert mock calls with `expect(fn).toHaveBeenCalledWith(...)`, not `fn.mock.calls[0]![0]`").
|
|
77
|
+
ESLint's pre-commit hook auto-stripped the redundant `!`, leaving `spy.mock.calls[0][0]`; tests stayed green.
|
|
78
|
+
I mirrored the pre-existing pattern in `settings.test.ts` rather than the skill's recommended `toHaveBeenCalledWith(expect.stringMatching(...))`.
|
|
79
|
+
Impact: none (auto-fixed, matches existing file style); the rule already exists in the skill, so this is a salience note, not a gap.
|
|
80
|
+
|
|
81
|
+
#### What caused friction (user side)
|
|
82
|
+
|
|
83
|
+
- None material.
|
|
84
|
+
The operator's one mid-ship intervention (asking about the follow-up issue) was a good catch that compensated for the prompt gap above, not a correction of a mistake.
|
|
85
|
+
|
|
86
|
+
### Diagnostic details
|
|
87
|
+
|
|
88
|
+
- **Model-performance correlation** — the single subagent dispatch (`pre-completion-reviewer`) ran on `anthropic/claude-sonnet-4-6`, appropriate for judgment-heavy review; no mismatch.
|
|
89
|
+
Parent-session `model_change` entries toggled among `sonnet-4-6`, `deepseek-v4-flash`, and `opus-4-8`, but these are operator model selections, not quality-relevant task assignments.
|
|
90
|
+
- **Escalation-delay tracking** — no `rabbit-hole` friction; no error sequence exceeded one or two tool calls before resolution.
|
|
91
|
+
- **Unused-tool detection** — nothing missed; `web_search`/`fetch_content` were dispatched proactively during planning for the fallow-mechanism question.
|
|
92
|
+
- **Feedback-loop gap analysis** — verification ran incrementally (per-step `check`/vitest, in-step `verify:public-types`), not just at the end.
|
|
93
|
+
No gap.
|
|
94
|
+
|
|
95
|
+
### Changes made
|
|
96
|
+
|
|
97
|
+
1. `.pi/prompts/ship-issue.md` — added `## 4c. Create planned follow-up issues` between the stacked-release check (§4b) and the close step (§5): if the plan or retro defers work to a follow-up issue, create it with `gh issue create` before closing so the close comment can reference its number.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 412
|
|
3
|
+
issue_title: "Unify the three overlapping session-mock builders in pi-subagents tests"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #412 — Unify the three overlapping session-mock builders in pi-subagents tests
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-16T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned the unification of the three `test/helpers/` session-mock builders.
|
|
13
|
+
A structural read showed the three sit on two axes (AgentSession-vs-`SubagentSession`, event-bus-vs-factory), that `createSubagentSessionStub` already composes `createMockSession` (intrinsic delegation, not duplication), and that the only genuine independent redeclaration of the four shared base fields lives in `createFactorySession`.
|
|
14
|
+
The operator chose targeted reuse with a working-bus core default; the plan folds `createFactorySession` onto the `createMockSession` core and leaves the other two builders untouched.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- The issue is the operator's own and explicitly flags the wrong-abstraction risk (Sandi Metz quote), so the `Decide` gate used `ask_user` to choose between full composable factory (A), targeted reuse (B), and decline-and-document (C).
|
|
19
|
+
Operator picked **B** with the **working event bus as the core default**.
|
|
20
|
+
- Rejected option A (the issue's literal "Proposed change") because a multi-facet `createSessionMock()` with opt-in `withTurnLoop()`/`withBindFacet()` is the over-parameterized factory the issue itself warns against; the honest target is only `createFactorySession`'s independently-redeclared base.
|
|
21
|
+
- De-risked the key feasibility assumption with a throwaway `tsc --noEmit` probe: spreading `...createMockSession()` (which returns `MockSession & Record<string, unknown>`) preserves `Mock<...>` typing on the facet methods because `unknown & Mock<...>` narrows to `Mock<...>`.
|
|
22
|
+
- Behavioral delta is the inert→working `subscribe` plus new `emit`/`sessionManager` fields on the factory session; confirmed no factory/lifecycle test emits or asserts on the inert subscribe, and `session.dispose` stays a spy (`create-subagent-session.test.ts:194`).
|
|
23
|
+
- Plan is two commits: a `refactor(test):` cycle (one new event-bus self-test + the rewrite) and a `docs:` cycle updating the Phase 17 Step 7 note in `architecture.md` to record the resolution.
|
|
24
|
+
|
|
25
|
+
## Stage: Implementation — TDD (2026-06-16T21:20:00Z)
|
|
26
|
+
|
|
27
|
+
### Session summary
|
|
28
|
+
|
|
29
|
+
Executed the plan in two TDD cycles exactly as written: a `refactor(test):` cycle (added the working-event-bus self-test as the red, rewrote `createFactorySession` to spread `...createMockSession()` + the factory facet as the green) and a `docs:` cycle recording the targeted-reuse outcome in the `architecture.md` Phase 17 Step 7 note.
|
|
30
|
+
Test count went 1030 → 1031 (the one new `createFactorySession` event-bus self-test); full suite 1031 pass across 65 files.
|
|
31
|
+
All deterministic gates green from repo root: `check`, `lint`, `test`, and `pnpm fallow dead-code`.
|
|
32
|
+
|
|
33
|
+
### Observations
|
|
34
|
+
|
|
35
|
+
- The plan's feasibility probe held: spreading `...createMockSession()` (typed `MockSession & Record<string, unknown>`) preserved `Mock<...>` typing on the facet methods, so `setActiveToolsByName.mock.calls[0][0]` still type-checks — no annotation gymnastics needed.
|
|
36
|
+
- The inert→working `subscribe` change was inert in practice as predicted: no factory or lifecycle test emits, and `create-subagent-session.test.ts:194`'s `session.dispose` spy assertion held (the core supplies `dispose` as a `vi.fn()`).
|
|
37
|
+
- Pre-completion reviewer: **WARN** (no FAILs).
|
|
38
|
+
Reviewer warnings: (1) the `createMockSession` core docstring I added landed orphaned above `toAgentSession` rather than attached to `createMockSession` — fixed in a follow-up `refactor(test):` commit (`5999dcad`) by moving it directly above the declaration; (2) the TDD retro stage was not yet written when the reviewer ran — this entry resolves it.
|
|
39
|
+
- Deviation: one extra cleanup commit beyond the planned two (the docstring-placement fix), landed as `refactor(test):` rather than amended because the `docs:` commit already sat on top of the refactor commit and neither was pushed.
|
|
40
|
+
|
|
41
|
+
## Stage: Final Retrospective (2026-06-17T00:00:00Z)
|
|
42
|
+
|
|
43
|
+
### Session summary
|
|
44
|
+
|
|
45
|
+
Shipped issue #412 across three stages (planning, TDD, ship) in one continuous session with a single deviation: one extra `refactor(test):` cleanup commit for an orphaned docstring the pre-completion reviewer flagged.
|
|
46
|
+
The whole arc was low-friction — the `ask_user` wrong-abstraction gate in planning, the `tsc --noEmit` feasibility probe, and incremental verification all paid off, and CI/release/close ran clean.
|
|
47
|
+
No `pi-subagents` release bumped (a `refactor(test):` change), so only the unrelated `pi-github-tools-v4.1.5` doc release rode along in the release-please PR.
|
|
48
|
+
|
|
49
|
+
### Observations
|
|
50
|
+
|
|
51
|
+
#### What went well
|
|
52
|
+
|
|
53
|
+
- The planning-stage `ask_user` gate did real work: it surfaced the wrong-abstraction risk the issue itself flagged and let the operator pick targeted reuse (B) over the issue's literal composable-factory proposal (A).
|
|
54
|
+
This is the `ask_user` skill behaving exactly as the `Decide` gate intends for an operator-authored issue.
|
|
55
|
+
- The `tsc --noEmit` throwaway probe in planning (does spreading `...createMockSession()` preserve `Mock<...>` typing?) de-risked the one feasibility unknown before any commit, so the TDD green step landed on the first try with no annotation gymnastics.
|
|
56
|
+
- Verification cadence was incremental, not end-loaded: `pnpm run check` ran right after the TDD green step, the full suite after both steps, then `lint`/`fallow` before push.
|
|
57
|
+
No feedback-loop gap.
|
|
58
|
+
|
|
59
|
+
#### What caused friction (agent side)
|
|
60
|
+
|
|
61
|
+
- `missing-context` — the plan called for a docstring touch-up on `createMockSession`, but the existing `createMockSession` docstring block in `mock-session.ts` was *already orphaned* (sitting above `toAgentSession`, not its own declaration).
|
|
62
|
+
I enriched the orphaned block instead of noticing it wasn't attached to the symbol it documents.
|
|
63
|
+
Impact: one extra `refactor(test):` commit (`5999dcad`) to move the block; caught by the pre-completion reviewer, so no escaped defect — reviewer-caught, not user-caught.
|
|
64
|
+
|
|
65
|
+
#### What caused friction (user side)
|
|
66
|
+
|
|
67
|
+
- None.
|
|
68
|
+
The operator's only mid-session input was the planning `ask_user` answer (direction B, working-bus core default), which was strategic judgment, not mechanical oversight.
|
|
69
|
+
|
|
70
|
+
### Diagnostic details
|
|
71
|
+
|
|
72
|
+
- **Model-performance correlation** — the one subagent dispatch (`pre-completion-reviewer`) ran on `anthropic/claude-sonnet-4-6`, appropriate for judgment-heavy review; it found a genuine issue (the orphaned docstring), so no quality mismatch.
|
|
73
|
+
- **Escalation-delay tracking** — no `rabbit-hole` friction; no error sequence exceeded one tool call.
|
|
74
|
+
- **Unused-tool detection** — no missing-context gap that an Explore/`colgrep` dispatch would have closed; planning already used `grep`/`colgrep` and a `tsc` probe.
|
|
75
|
+
- **Feedback-loop gap analysis** — verification ran incrementally (see "What went well"); no end-loaded check.
|
|
76
|
+
|
|
77
|
+
### Changes made
|
|
78
|
+
|
|
79
|
+
1. Appended this Final Retrospective stage entry to `packages/pi-subagents/docs/retro/0412-unify-session-mock-builders.md`.
|
|
80
|
+
No `AGENTS.md` or `.pi/prompts/` changes — the single friction point was a one-off slip the pre-completion reviewer already caught (weak evidence for a new rule).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 420
|
|
3
|
+
issue_title: "pi-subagents: fold subagent run metrics and live activity onto the core record"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #420 — pi-subagents: fold subagent run metrics and live activity onto the core record
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-17T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 3-step TDD plan (`docs/plans/0420-fold-run-metrics-live-activity-onto-record.md`) for Phase 18 Step 1: fold `turnCount`, active tools, and response text onto `SubagentState`, have `record-observer` populate them, and expose read-only getters on `Subagent`.
|
|
13
|
+
The change is a pure addition / tidy-first — both observers keep running and no consumer reads the new getters until Step 2 ([#421]).
|
|
14
|
+
Operator-authored, unambiguous proposal matching the architecture roadmap, so the `ask-user` gate was skipped.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- The `maxTurns` getter is the one getter that does **not** delegate to `SubagentState` — it delegates to `this.execution.maxTurns`.
|
|
19
|
+
Verified both spawners pass `execution.effectiveMaxTurns` as `options.maxTurns` (threaded into `SubagentExecution.maxTurns` by `SubagentManager.spawn`), so the record getter returns the same value `AgentActivityTracker` was constructed with.
|
|
20
|
+
- Semantics must be copied field-for-field from `AgentActivityTracker` so Step 2's reader swap is behavior-preserving: `turnCount` starts at **1** (readers assume the at-least-1 invariant — `notification.ts` uses `?? 0`, `result-renderer.ts` gates on `> 0`); `activeTools` uses `name_seq` keying for concurrent same-name tools; `removeActiveTool` deletes the first match; `responseText` resets at `message_start` and appends each text delta.
|
|
21
|
+
- Decided to leave `resetForResume` **unchanged** (the new fields are not reset on resume).
|
|
22
|
+
Rationale: the tracker is not reconstructed/reset on resume today, so the surviving `SubagentState` accumulating across a resume preserves parity.
|
|
23
|
+
Touching it would violate the pure-addition contract; flagged in Open Questions for Step 2 to revisit against observable reader behavior.
|
|
24
|
+
- The tracker's `_session`/`setSession` is deliberately **not** folded — it exists only for UI polling reads and is migrated/removed in Steps 2–3.
|
|
25
|
+
- No symbol is removed or renamed, so no `package-pi-subagents` SKILL or architecture-doc prose update is needed; the Phase 18 Step 1 roadmap entry already describes this work.
|
|
26
|
+
|
|
27
|
+
[#421]: https://github.com/gotgenes/pi-packages/issues/421
|
|
28
|
+
|
|
29
|
+
## Stage: Implementation — TDD (2026-06-17T13:10:00Z)
|
|
30
|
+
|
|
31
|
+
### Session summary
|
|
32
|
+
|
|
33
|
+
Executed all 3 TDD steps from the plan: (1) added `turnCount`/`activeTools`/`responseText` fields plus 5 transition methods to `SubagentState`; (2) extended `record-observer` with 4 new event branches (`tool_execution_start`, `turn_end`, `message_start`, `message_update` text_delta) plus paired `removeActiveTool` on `tool_execution_end`; (3) added 4 read-only getters to `Subagent` (`turnCount`, `activeTools`, `responseText`, `maxTurns`).
|
|
34
|
+
Test count: 1031 → 1058 (+27 across 3 test files).
|
|
35
|
+
Full suite green; type check and lint clean; zero dead code.
|
|
36
|
+
|
|
37
|
+
### Observations
|
|
38
|
+
|
|
39
|
+
- The first pre-completion reviewer run returned **FAIL** due to 3 pre-existing `MD051` broken-fragment links in `docs/architecture/history/phase-17-core-consolidation.md` (fragment `#first-principles-refinement-the-deeper-target` was missing `-and-`; correct anchor is `#first-principles-refinement-and-the-deeper-target`).
|
|
40
|
+
Fixed in a separate `docs:` commit; re-run returned **WARN**.
|
|
41
|
+
- 3 Biome `useTemplate` infos in `packages/pi-permission-system/` are informational only (marked unsafe fix, `biome check --write` skips them) and do not cause a non-zero lint exit.
|
|
42
|
+
- The 3 "vacuously-passing" new observer tests (paired `tool_execution_end` removal, `message_start` reset, non-text_delta ignore) pass before the observer handles the events because the state starts in the default/empty state — they correctly verify absence-of-mutation and fully exercise the code path after the observer is implemented.
|
|
43
|
+
- WARN finding: `package-pi-subagents` SKILL.md `Observation` domain row description ("Session-event stats") is now slightly incomplete for `record-observer` (it also accumulates live-activity fields).
|
|
44
|
+
Intentionally deferred per the planning-stage decision — no symbol removed, and the description will be updated in Step 2 ([#421]) when the observer's role is fully defined after the reader migration.
|
|
45
|
+
- Pre-completion reviewer verdict: **WARN** (one non-blocking finding, deferred per retro).
|
|
46
|
+
|
|
47
|
+
## Stage: Final Retrospective (2026-06-17T17:40:39Z)
|
|
48
|
+
|
|
49
|
+
### Session summary
|
|
50
|
+
|
|
51
|
+
Shipped Phase 18 Step 1 across planning, TDD (3 red→green→commit cycles, +27 tests), and release (`pi-subagents` v16.5.0).
|
|
52
|
+
The code work was clean and rework-free; all friction was in markdown-lint surfacing late — two separate doc-lint failures (one pre-existing, one introduced) escaped the baseline and post-TDD gates and were caught only by the pre-completion reviewer and the ship-stage lint, each requiring a fix commit.
|
|
53
|
+
|
|
54
|
+
### Observations
|
|
55
|
+
|
|
56
|
+
#### What went well
|
|
57
|
+
|
|
58
|
+
- The `pre-completion-reviewer` safety net earned its keep: it caught the pre-existing `MD051` broken-fragment links in `phase-17-core-consolidation.md` (introduced earlier by `e4d92535`, unrelated to this issue) before the push, returning FAIL on the first run.
|
|
59
|
+
Without it the broken links would have shipped.
|
|
60
|
+
- TDD execution was textbook: each of the 3 steps followed red→green→commit with no rework to production code, the `maxTurns`-via-`execution` delegation decision from planning held, and the "vacuously-passing" observer tests (verified in the retro) correctly converted to real assertions once the observer was implemented.
|
|
61
|
+
|
|
62
|
+
#### What caused friction (agent side)
|
|
63
|
+
|
|
64
|
+
- `instruction-violation` (gate-caught, at ship) — wrote a second `[#421]:` link-reference definition into the retro file when appending the TDD stage entry via `Write` full-content, tripping `MD053` (duplicate definition).
|
|
65
|
+
The `markdown-conventions` skill states this rule verbatim ("Link reference definitions are file-scoped … a duplicate trips MD053").
|
|
66
|
+
Impact: one fix commit at ship time (`80d4d050`), caught by the root `pnpm run lint` pre-push gate rather than during TDD (no lint runs after stage-notes writing).
|
|
67
|
+
- `missing-context` (self-identified during ship investigation) — the TDD green-baseline lint was run package-scoped (`pnpm --filter @gotgenes/pi-subagents run lint`), which **silently passes** on `MD051` cross-file fragment failures.
|
|
68
|
+
Verified empirically: package `lint:md` (`rumdl check *.md docs/**/*.md`) returns "No issues" on the broken fragment, while root `rumdl check .` exits 1 on it — the cross-file fragment target (`../architecture.md`) is only resolved on a repo-root tree walk.
|
|
69
|
+
Impact: the pre-existing `MD051` failure slipped past the baseline gate (whose job is to catch exactly that) and surfaced mid-TDD via the reviewer, costing a fix commit (`86ed0c81`) and a reviewer FAIL/re-dispatch cycle.
|
|
70
|
+
|
|
71
|
+
#### What caused friction (user side)
|
|
72
|
+
|
|
73
|
+
- None.
|
|
74
|
+
The session ran end-to-end without user correction; the operator-authored, roadmap-aligned issue meant no clarification was needed.
|
|
75
|
+
|
|
76
|
+
### Diagnostic details
|
|
77
|
+
|
|
78
|
+
- **Model-performance correlation** — two `pre-completion-reviewer` subagent dispatches ran on the agent's configured reviewer model and performed judgment-heavy review work (acceptance criteria, design review, cross-step invariants); appropriate match.
|
|
79
|
+
No high-cost model was spent on mechanical work and no reasoning-weak model on judgment work.
|
|
80
|
+
- **Feedback-loop gap analysis** — the lint feedback loop fired at the wrong scope and the wrong time: package-scoped at baseline (missed `MD051`), and absent after retro stage-notes writing (missed `MD053` until ship).
|
|
81
|
+
`pnpm run check` and `pnpm run test` were run incrementally per step and caught everything they should; only the markdown-lint loop was misconfigured.
|
|
82
|
+
- **Escalation-delay / unused-tool** — no `rabbit-hole` friction; no lens-2 or lens-3 findings.
|
|
83
|
+
|
|
84
|
+
### Changes made
|
|
85
|
+
|
|
86
|
+
1. `.pi/prompts/tdd-plan.md` — "Verify green baseline" step 2 and "After the last TDD step" step 3 now specify `pnpm run lint` runs **from the repo root**, with a one-clause rationale that package-scoped lint silently passes on `MD051` cross-file fragments and cross-package issues (Proposal 1).
|
|
87
|
+
2. Proposal 2 (an `MD053` duplicate-link-definition reminder in the retro-writing prompts) was declined — the rule already exists verbatim in the `markdown-conventions` skill, so a prompt clause would duplicate it.
|
|
88
|
+
3. Follow-up (post-retro): `#420` completed Phase 18 Step 1 but the roadmap Steps section was not marked done until the operator noticed.
|
|
89
|
+
Added a roadmap-step-status check so this is caught going forward: a WARN bullet in `pre-completion-reviewer.md` §2c (backstop), and a proactive step in `.pi/prompts/tdd-plan.md` step 7 and `.pi/prompts/build-plan.md` "After the last step" — mark the completed roadmap step done (`✅`/`Landed:`) and update the phase status row.
|
|
90
|
+
Also marked Step 1 complete in `architecture.md` (Steps list, dependency diagram node, phase table row → "In progress").
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 421
|
|
3
|
+
issue_title: "pi-subagents: migrate activity-tracker readers to the record getters"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #421 — Migrate activity-tracker readers to the record getters
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-06-17T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 5-step TDD plan for Phase 18 Step 2 — migrating the five activity readers (`widget-renderer`, `conversation-viewer`, `agent-menu`, `foreground-runner`, `notification`) off `AgentActivityTracker` and onto the `Subagent` getters added in Step 1 ([#420]).
|
|
13
|
+
The plan files at `packages/pi-subagents/docs/plans/0421-migrate-activity-readers-to-record-getters.md`.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- **Two design forks surfaced and were resolved with the operator** via `ask_user`. (1) Finished agents currently show **no** turn count because the tracker is deleted on completion; reading off the record (which persists `turnCount`) makes finished lines show `⟳N`.
|
|
18
|
+
Operator accepted this as the more-correct behavior — so the widget step is `feat:`, not `refactor:`. (2) How `WidgetAgent` exposes context percent: chose a precomputed `contextPercent` **field** + a `Subagent` → `WidgetAgent` projection in `AgentWidget` (over a `getContextPercent()` method on the snapshot), honoring the renderer's pure-data contract and dropping its `getSessionContextPercent(session)` reach-through.
|
|
19
|
+
Operator's steer: "no sacred cows, especially in the UI — make the change that makes the change easy, then make the easy change."
|
|
20
|
+
- **Scope boundary with Step 3 ([#422]).**
|
|
21
|
+
The producer plumbing (`foreground-runner` / `background-spawner` constructing trackers, `subscribeUIObserver`, the `runtime.agentActivity` map) stays — it is removed in Step 3.
|
|
22
|
+
But the reader-held map deps in `notification`, `agent-widget`, and `agent-menu` are removed **here**, because Step 3's target list does not include those files.
|
|
23
|
+
This means `cleanupCompleted` (which only deleted a map entry) becomes vestigial and is removed from `NotificationSystem`, rippling to `SubagentEventsObserver`.
|
|
24
|
+
- **Transient map leak between Steps 2 and 3** is noted as an accepted risk: after this step the runtime map is write-only and read by nobody, so it is inert until Step 3 deletes it.
|
|
25
|
+
- **Prep step (tidy-first):** Step 1 adds `turnCount` / `activeTools` / `responseText` / `maxTurns` shorthands to `createTestSubagent` so the reader-migration tests can seed activity on records.
|
|
26
|
+
- **Living-doc updates identified:** `.pi/skills/package-pi-subagents/SKILL.md` line 56 (`widget ─polls─→ AgentActivityTracker map`) and the architecture roadmap's Phase 18 Step 2 `Landed:` line.
|
|
27
|
+
Completed historical plans under `docs/plans/` are point-in-time records and are not edited.
|
|
28
|
+
- **Shared-signature commits** flagged for `pnpm run check`: the widget step (`renderWidgetLines`) and the notification step (`NotificationSystem` + `index.ts` construction).
|
|
29
|
+
Both `buildDetails` call sites and both viewer/menu constructor call sites must land in the same commit as their signature changes.
|
|
30
|
+
|
|
31
|
+
[#420]: https://github.com/gotgenes/pi-packages/issues/420
|
|
32
|
+
[#422]: https://github.com/gotgenes/pi-packages/issues/422
|
|
33
|
+
|
|
34
|
+
## Stage: Implementation — TDD (2026-06-17T17:00:00Z)
|
|
35
|
+
|
|
36
|
+
### Session summary
|
|
37
|
+
|
|
38
|
+
All 5 TDD cycles completed in one session.
|
|
39
|
+
The five activity readers (`widget-renderer`, `conversation-viewer`, `agent-menu`, `foreground-runner`, `notification`) were migrated off `AgentActivityTracker` and onto the `Subagent` record getters added in Phase 18 Step 1.
|
|
40
|
+
Test count went from 1058 to 1066 (+8).
|
|
41
|
+
|
|
42
|
+
### Observations
|
|
43
|
+
|
|
44
|
+
- **Step 2 (widget)** introduced the `WidgetAgent` → activity-field fold and the `AgentWidget.toWidgetAgent` projection.
|
|
45
|
+
The accepted behavior change (finished agents now show `⟳N` from the persisted record) landed as `feat:` as planned.
|
|
46
|
+
`renderWidgetLines` drops `activityMap`; renderer is now a pure function of plain data with no SDK-type reach.
|
|
47
|
+
- **Step 3 (viewer + menu)** was straightforward once `ConversationViewer` dropped `activity?: AgentActivityTracker`; the streaming indicator now reads `this.record.activeTools` / `this.record.responseText` directly.
|
|
48
|
+
`AgentActivityReader` interface removed; `AgentsMenuHandler` constructor lost one parameter.
|
|
49
|
+
- **Step 4 (buildDetails + foreground runner)** required care around the pre-`onSessionCreated` phase: `streamUpdate` falls back to `recordRef?.turnCount ?? 1`, `execution.effectiveMaxTurns`, empty `Map()`, and `""` before the record reference is assigned.
|
|
50
|
+
The `AgentTool` resume call site (`buildDetails(base, record)`) was already correct and needed no change.
|
|
51
|
+
- **Step 5 (notifications)** removed `NotificationSystem.cleanupCompleted` entirely (it only deleted a map entry).
|
|
52
|
+
`SubagentEventsObserver.onSubagentCompleted` now returns early on `resultConsumed` rather than calling the removed method.
|
|
53
|
+
The `NotificationManager` constructor drops the `agentActivity: Map` argument; `index.ts` needed one arg removed.
|
|
54
|
+
- **Post-commit SKILL.md fix**: the pre-completion reviewer (WARN) flagged that `subagent-events-observer.ts` was missing from the Observation domain table; fixed in a follow-up `docs:` commit.
|
|
55
|
+
- **Pre-completion reviewer**: WARN (non-blocking).
|
|
56
|
+
Reviewer warning: SKILL.md Observation domain table listed 4 modules and omitted `subagent-events-observer.ts`; corrected before writing these notes.
|