@nklisch/pi-enhanced 0.4.4 → 0.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +1 -1
  3. package/node_modules/@nklisch/pi-astral-pocket/README.md +127 -93
  4. package/node_modules/@nklisch/pi-astral-pocket/package.json +2 -2
  5. package/node_modules/@nklisch/pi-astral-pocket/src/distiller.ts +196 -183
  6. package/node_modules/@nklisch/pi-astral-pocket/src/guidance.ts +26 -62
  7. package/node_modules/@nklisch/pi-astral-pocket/src/index.ts +8 -5
  8. package/node_modules/@nklisch/pi-astral-pocket/src/sessions.ts +10 -3
  9. package/node_modules/@nklisch/pi-astral-pocket/src/store.ts +439 -321
  10. package/node_modules/@nklisch/pi-astral-pocket/src/tools.ts +172 -66
  11. package/node_modules/@nklisch/pi-clearance/docs/CONFIGURATION.md +3 -3
  12. package/node_modules/@nklisch/pi-clearance/docs/DEVELOPER_GUIDE.md +11 -12
  13. package/node_modules/@nklisch/pi-clearance/docs/PACK_AUTHORING.md +5 -5
  14. package/node_modules/@nklisch/pi-clearance/docs/PRINCIPLES.md +1 -2
  15. package/node_modules/@nklisch/pi-clearance/docs/REFERENCE_PATTERNS.md +13 -19
  16. package/node_modules/@nklisch/pi-clearance/docs/SPEC.md +4 -4
  17. package/node_modules/@nklisch/pi-clearance/docs/USER_GUIDE.md +2 -3
  18. package/node_modules/@nklisch/pi-clearance/docs/VISION.md +1 -1
  19. package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
  20. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +2 -2
  21. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +2 -3
  22. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +105 -886
  23. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0002-extensions-on-a-minimal-core.md +44 -78
  24. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0003-publish-bundled-type-declarations.md +28 -57
  25. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +49 -261
  26. package/package.json +1 -1
  27. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/client-server-opportunities.md +0 -127
  28. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-1-api-boundary.md +0 -8
  29. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-10-structural-decomposition.md +0 -141
  30. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-11-closure-to-class.md +0 -100
  31. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-12-complexity-test-fixtures.md +0 -55
  32. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-13-remaining-smells.md +0 -88
  33. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-14-strip-policy.md +0 -49
  34. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-15-domain-model-evolution.md +0 -73
  35. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-16-invert-dependencies.md +0 -144
  36. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-17-core-consolidation.md +0 -214
  37. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-18-reconsider-ui.md +0 -166
  38. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-19-implement-ui-decisions.md +0 -282
  39. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-2-remove-scheduling.md +0 -9
  40. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +0 -11
  41. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-4-implement-service.md +0 -8
  42. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-5-decompose-index.md +0 -42
  43. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-7-encapsulation.md +0 -173
  44. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-8-testability.md +0 -103
  45. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-9-observation-ctx.md +0 -122
  46. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0001-deferred-patches.md +0 -80
@@ -1,6 +1,6 @@
1
1
  # Architecture
2
2
 
3
- This document describes the architecture of the pi-subagents fork: a focused, composable core with a stable API boundary that other extensions can build on.
3
+ This document describes the architecture of `@nklisch/pi-subagents`: a focused, composable core with a stable API boundary that other extensions can build on. A subagent is a child Pi session. Joined delivery returns its settled result through the calling tool; detached delivery returns an identity while the child continues running.
4
4
 
5
5
  ## Design principles
6
6
 
@@ -10,10 +10,10 @@ This document describes the architecture of the pi-subagents fork: a focused, co
10
10
  3. **Typed API boundary** — this package exports a `SubagentsService` interface and `Symbol.for()` accessors (`publishSubagentsService` / `getSubagentsService`).
11
11
  Consumers declare this package as an optional peer dependency and use dynamic import for compile-time types.
12
12
  The runtime bridge is `Symbol.for("@nklisch/pi-subagents:service")` on `globalThis` — no separate API package.
13
- 4. **No time-based scheduling** — cron-style timed dispatch (upstream's `schedule.ts` subsystem) is removed from the core (#52).
13
+ 4. **No time-based scheduling** — cron-style timed dispatch stays outside the core.
14
14
  Timed dispatch is a separate concern that any extension can implement by calling `launch()` on the published API.
15
15
  The max-concurrent admission gate is not scheduling in this sense — concurrency management stays in core.
16
- 5. **UI is an in-core, substitutable consumer** — [ADR-0004](../decisions/0004-reconsider-ui-direction.md) records the per-component decision: the widget shows detached agents only, the bespoke conversation viewer is replaced by native session navigation, the `/agents` command is dissolved into focused surfaces, and the surviving UI stays in the core as a reactive consumer (not extracted to a separate package).
16
+ 5. **UI is an in-core, substitutable consumer** — [ADR-0004](../decisions/0004-reconsider-ui-direction.md) records the per-component decision: the widget shows detached agents only, native session navigation provides read-only transcripts, settings have a focused command, and the UI stays in the core as a reactive consumer (not extracted to a separate package).
17
17
  Extraction remains an available future option because the composition invariant holds — the core is byte-for-byte identical with or without a given UI consumer.
18
18
  6. **Snapshot identity; share the canonical runtime** — mutable parent identity and prompt state are frozen into `ParentSnapshot` at spawn. The parent `ModelRuntime` is deliberately shared so child sessions preserve runtime provider and authentication registrations; no live session context is captured.
19
19
  7. **Subscribe, don't thread** — observation of agent progress uses direct session-event subscription, not callback parameters threaded through multiple layers.
@@ -21,7 +21,7 @@ This document describes the architecture of the pi-subagents fork: a focused, co
21
21
  If state isn't available yet, the object that needs it doesn't exist yet.
22
22
  No post-construction field writes from external code — if an object can't be instantiated ready-to-go, the prep work hasn't been done and the right dependencies haven't been identified.
23
23
  9. **State owns its mutations** — mutable state lives in a class whose methods enforce valid transitions and invariants.
24
- Free functions that mutate module-scoped variables, closure-captured bags-of-functions, and external writes to shared interfaces are replaced by classes that encapsulate the state they manage.
24
+ Classes encapsulate the state they manage rather than exposing module-scoped variables or mutable shared interfaces to external writers.
25
25
  10. **Open for extension, closed for modification** — pi-subagents is a minimal core that publishes events and a service API.
26
26
  Other packages (pi-permission-system, a future UI extension, hypothetical OTel integration) hook into these events to add permissions, rendering, or telemetry.
27
27
  Pi-subagents has zero knowledge of its consumers — dependency arrows point inward, never outward.
@@ -104,107 +104,18 @@ flowchart TB
104
104
 
105
105
  ```mermaid
106
106
  classDiagram
107
- class Subagent {
108
- +id: string
109
- +type: SubagentType
110
- +description: string
111
- -state: SubagentState
112
- -execution: SubagentExecution
113
- +status: SubagentStatus
114
- +result?: string
115
- +error?: string
116
- +toolUses: number
117
- +lifetimeUsage: LifetimeUsage
118
- +subagentSession?: SubagentSession
119
- +toolCallId?: string
120
- +markRunning() delegates
121
- +launch()
122
- +resume(prompt, mode, signal)
123
- +requestStop(reason): boolean
124
- +steer(message): Promise~SteerOutcome~
125
- +settlement: Promise~void~
126
- +isSessionReady(): boolean
127
- +getConversation(): string | undefined
128
- +getContextPercent(): number | null
129
- +subscribeToUpdates(fn): unsub | undefined
130
- +messages: readonly unknown[]
131
- +effectiveThinkingLevel: ThinkingLevel
132
- -executionInFlight: boolean
133
- -resumeReserved: boolean
134
- +completeRun(result)
135
- +failRun(err)
136
- +disposeSession(): Promise~void~
137
- }
138
-
139
- class SubagentState {
140
- +status: SubagentStatus
141
- +result?: string
142
- +error?: string
143
- +startedAt: number
144
- +completedAt?: number
145
- +toolUses: number
146
- +lifetimeUsage: LifetimeUsage
147
- +compactionCount: number
148
- +markRunning() ... markStopped()
149
- +resetForResume()
150
- +incrementToolUses()
151
- +addUsage(delta)
152
- +incrementCompactions()
153
- }
154
-
155
- class SubagentExecution {
156
- +createSubagentSession(params)
157
- +snapshot: ParentSnapshot
158
- +prompt: string
159
- +baseCwd: string
160
- +observer?: SubagentLifecycleObserver
161
- +getRunConfig?()
162
- +getWorkspaceProvider?()
163
- +model?, maxTurns?, thinkingLevel?
164
- +parentSession?, signal?
165
- }
166
-
167
- class SubagentManager {
168
- +spawn(snapshot, type, prompt, config)
169
- +launch(snapshot, type, prompt, config)
170
- +resume(id, prompt, mode, signal)
171
- +stop(id, timeout): StopOutcome
172
- +getRecord(id): Subagent
173
- +listAgents(): Subagent[]
174
- }
175
-
176
- class AgentTypeRegistry {
177
- +resolveType(type): string
178
- +resolveAgentConfig(type): AgentConfig
179
- +reload()
180
- +getToolNamesForType(type): string[]
181
- }
182
-
183
- class ParentSnapshot {
184
- +cwd: string
185
- +systemPrompt: string
186
- +model: unknown
187
- +modelRegistry: unknown
188
- +modelRuntime?: unknown
189
- +parentContext?: string
190
- }
191
-
192
- class SubagentsService {
193
- +launch(type, prompt, options?): Promise~LaunchDelivery~
194
- +resume(id, prompt, options?): Promise~ResumeDelivery~
195
- +stop(id, timeout?): Promise~StopDelivery~
196
- +steer(id, message): Promise~SteerDelivery~
197
- +list(options?): SubagentRecord[]
198
- +getResult(id): ResultDelivery
199
- +getRecord(id): SubagentRecord
200
- +waitForAll()
201
- +hasRunning(): boolean
202
- }
107
+ class Subagent
108
+ class SubagentState
109
+ class SubagentExecution
110
+ class SubagentManager
111
+ class AgentTypeRegistry
112
+ class ParentSnapshot
113
+ class SubagentsService
203
114
 
204
115
  SubagentManager --> Subagent : creates/manages
205
- Subagent --> SubagentState : owns (private)
206
- Subagent --> SubagentExecution : runs via (mandatory)
207
- SubagentManager --> ParentSnapshot : receives at spawn
116
+ Subagent --> SubagentState : owns transitions and metrics
117
+ Subagent --> SubagentExecution : owns run coordination
118
+ SubagentExecution --> ParentSnapshot : frozen identity and prompt inputs
208
119
  SubagentsService --> SubagentManager : wraps via adapter
209
120
  SubagentManager --> AgentTypeRegistry : resolves types
210
121
  ```
@@ -236,7 +147,7 @@ Resume admission is a record-owned lease. A genuinely running or already-reserve
236
147
 
237
148
  Terminal outcomes also carry orthogonal consumption state. Joined delivery, `get_subagent_result`, and a queued detached completion notification mark the outcome consumed. Records remain available for the parent session; only their heavy live child sessions are released after the configured consumed or unconsumed retention window. Released records keep their result and persisted transcript pointer but cannot resume. Release detaches the session synchronously before awaiting teardown, so it cannot race a new resume.
238
149
 
239
- Completion notifications are held while the parent agent run is active and flushed on `agent_settled`, rechecking consumption before enqueueing a follow-up. A blocking `get_subagent_result` request claims direct delivery before awaiting, so the same terminal outcome cannot also enqueue a completion follow-up. Child session creation shares the parent model runtime and leaves extension-tool registration open; a denylist removes disallowed built-ins and recursive orchestration tools across registry refreshes. Child teardown mirrors Pi's managed root lifecycle: it emits and awaits `session_shutdown` before `AgentSession.dispose()` revokes extension contexts, then publishes the child `disposed` event.
150
+ Completion notifications are held while the parent agent run is active and flushed on `agent_settled`, rechecking consumption before enqueueing a follow-up. `get_subagent_result` is a bounded, nonblocking read: active runs return status; terminal results are marked consumed so a later completion nudge does not duplicate delivery. Child session creation shares the parent model runtime and leaves extension-tool registration open; a denylist removes disallowed built-ins and recursive orchestration tools across registry refreshes. Child teardown mirrors Pi's managed root lifecycle: it emits and awaits `session_shutdown` before `AgentSession.dispose()` revokes extension contexts, then publishes the child `disposed` event.
240
151
 
241
152
  ## Execution flow
242
153
 
@@ -275,9 +186,7 @@ sequenceDiagram
275
186
 
276
187
  ## Module organization
277
188
 
278
- The extension has 67 source files organized into six domains plus entry-point wiring.
279
- All eight domains have directories: `config/`, `session/`, `lifecycle/`, `observation/`, `service/`, `tools/`, `ui/`, and `handlers/`.
280
- Issue #164 moved the 26 previously flat root-level files into five new domain directories, reducing the root to 5 files + 8 directories.
189
+ The source separates domain logic from entry-point wiring in these directories: `config/`, `session/`, `lifecycle/`, `observation/`, `service/`, `tools/`, `ui/`, and `handlers/`.
281
190
 
282
191
  ### Current layout
283
192
 
@@ -389,7 +298,7 @@ content and labels the degraded state.
389
298
  Record statistics (tool uses, token usage, compaction counts) and live activity (active tools, response text, turn counts) are updated by `record-observer.ts`, which subscribes directly to session events.
390
299
  This is the single per-child session subscription — all run state lives on the `Subagent` record.
391
300
 
392
- The widget maintains a bounded reactive read model of active detached records and the small terminal linger set, populated by `SubagentManagerObserver` lifecycle callbacks. While active it refreshes that model at 500 ms; each refresh reads only those retained records and requests the normal Pi widget render, rather than cloning or sorting the manager's full terminal history. When only terminal linger records remain, it renders the completion state once and leaves the static widget registered without an interval until turn aging or clear removes it. The `/subagents:sessions` navigator reads messages via `Subagent.agentMessages`, falls back to the retained JSONL source after release, and subscribes to both session and record-release updates — no direct `AgentSession` reference (#277).
301
+ The widget maintains a bounded reactive read model of active detached records and the small terminal linger set, populated by `SubagentManagerObserver` lifecycle callbacks. While active it refreshes that model at 500 ms; each refresh reads only those retained records and requests the normal Pi widget render, rather than cloning or sorting the manager's full terminal history. When only terminal linger records remain, it renders the completion state once and leaves the static widget registered without an interval until turn aging or clear removes it. The `/subagents:sessions` navigator reads messages via `Subagent.agentMessages`, falls back to the retained JSONL source after release, and subscribes to both session and record-release updates — no direct `AgentSession` reference.
393
302
 
394
303
  ## Cross-extension architecture
395
304
 
@@ -402,9 +311,7 @@ flowchart TD
402
311
  ui_int["Internal UI: widget, session-navigator,<br/>subagents-settings"]
403
312
  end
404
313
 
405
- core -- "Symbol.for on globalThis" --> sched["scheduling extension<br/>(hypothetical)"]
406
- core -- "Symbol.for on globalThis" --> subui["pi-subagents-ui<br/>(deferred)"]
407
- core -- "Symbol.for on globalThis" --> future["any future extension"]
314
+ core -- "Symbol.for on globalThis" --> consumer["cross-extension service consumer"]
408
315
  ```
409
316
 
410
317
  Consumers call `getSubagentsService()?.launch(...)` at runtime.
@@ -418,35 +325,35 @@ They declare this package as an optional peer dependency and use dynamic import
418
325
  - `createSubagentSession` — assembly factory: session creation and extension binding; returns a born-complete `SubagentSession`.
419
326
  - `SubagentSession` — the born-complete child session: drives turn loops, exposes Pi's idle boundary, steers, and performs idempotent asynchronous teardown (`session_shutdown` → `AgentSession.dispose()` → child `disposed`).
420
327
  - `child-lifecycle` — publishes the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) on `pi.events`.
421
- Reactive consumers subscribe: `@gotgenes/pi-permission-system` registers each child session on `session-created` and unregisters it on `disposed`.
422
- This replaced the former outbound `permission-bridge` (#261, [ADR-0002]) — the core no longer looks up a named consumer.
423
- - `workspace` — the single generative seam (#262, [ADR-0002]): a registered `WorkspaceProvider` supplies a child's cwd plus bracketed `dispose()` at run-start.
424
- With no provider, children run in the parent cwd (default unchanged); the git worktree strategy lives behind this seam in `@gotgenes/pi-subagents-worktrees` (#263, the seam's first consumer).
328
+ A permission consumer can register each child session on `session-created` and unregister it on `disposed`.
329
+ The core does not look up a named consumer; see [ADR-0002].
330
+ - `workspace` — the workspace provider seam ([ADR-0002]): a registered `WorkspaceProvider` supplies a child's cwd plus bracketed `dispose()` at run-start.
331
+ With no provider, children run in the parent cwd; a companion package can supply a Git worktree strategy behind this seam.
425
332
  - `session-config` — pure configuration assembler (called by `createSubagentSession`).
426
333
  - `SubagentRuntime` — session-scoped state bag with methods.
427
334
  - `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
428
335
  - `record-observer` — session-event observer that updates record statistics without callback threading.
429
336
  - Agent type registry — default agents, custom `.md` file loading.
430
337
  - Prompt assembly, context extraction, skills, environment.
431
- - Worktree isolation evicted to `@gotgenes/pi-subagents-worktrees` via the workspace provider seam in Phase 16 (#263, [ADR-0002]); `git` no longer appears in the core.
338
+ - Worktree isolation is an external workspace-provider strategy, not a core responsibility ([ADR-0002]).
432
339
  - Token usage tracking.
433
340
  - Session directory derivation and persisted `SessionManager` for subagent transcripts.
434
341
  - Settings persistence.
435
- - Internal UI (widget, `/subagents:sessions` session navigator, `/subagents:settings` command) the conversation viewer and `/agents` menu were removed in Phase 19 (Steps 5–6, [#442], [#441]) per [ADR-0004].
342
+ - Internal UI (widget, `/subagents:sessions` session navigator, `/subagents:settings` command) is a substitutable consumer ([ADR-0004]).
436
343
 
437
- ### What the core dropped
344
+ ### What stays outside the core
438
345
 
439
- - **Scheduling** (`schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`) removed (#52).
440
- - **Ad-hoc RPC** (`cross-extension-rpc.ts`) replaced by the typed `SubagentsService` published via `Symbol.for()` (#49).
441
- - **Group join** (`group-join.ts`) removed (#49).
442
- - **Output file** (`output-file.ts`) replaced by `session-dir.ts` + `SessionManager.create()` (#61).
443
- - **Callback threading** — the three-layer `on*` callback chain was replaced by direct session-event subscriptions (#100).
444
- - **Live `ctx` capture** — replaced by `ParentSnapshot`, an immutable data object captured once at spawn time (#99).
346
+ Time-based scheduling, event RPC, batch/group identity, hard process termination,
347
+ and agent-definition editors are not core responsibilities. Permission policy and
348
+ workspace strategies belong to companion extensions. The core retains its own
349
+ parent-only tool guard and supported tool-denylist behavior; these are not a
350
+ second permission system.
445
351
 
446
352
  ## SubagentsService
447
353
 
448
354
  The `SubagentsService` interface, accessor functions, and serializable types are exported from `@nklisch/pi-subagents` via the root export.
449
- No separate API package is needed.
355
+ No separate API package is needed. The service adapter resolves caller-supplied
356
+ model strings against the active registry before launch.
450
357
 
451
358
  Consumers declare this package as an optional peer dependency:
452
359
 
@@ -485,774 +392,86 @@ Key types:
485
392
  - `LaunchOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `inheritContext`, `mode`, `timeoutSeconds`, and `signal`.
486
393
  - `SUBAGENT_EVENTS` — channel constants for `pi.events` subscriptions.
487
394
 
488
- ### Accessor pattern
489
-
490
- ```typescript
491
- const SERVICE_KEY = Symbol.for("@nklisch/pi-subagents:service");
492
-
493
- export function publishSubagentsService(service: SubagentsService): void {
494
- (globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
495
- }
496
-
497
- export function getSubagentsService(): SubagentsService | undefined {
498
- return (globalThis as Record<symbol, unknown>)[SERVICE_KEY] as
499
- | SubagentsService
500
- | undefined;
501
- }
502
- ```
503
-
504
- If Pi gains a native service registry ([earendil-works/pi#4207]), these accessors can be updated to delegate to `pi.registerService()` / `pi.getService()` internally while keeping the same consumer API.
505
-
506
395
  ### Lifecycle events
507
396
 
508
397
  The core emits events on `pi.events` that any extension can observe:
509
398
 
510
- | Channel | Payload | When |
511
- | --------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------- |
512
- | `subagents:started` | `{ id, type, description, runId, mode }` | Agent begins running |
513
- | `subagents:completed` | `{ id, type, runId, mode, status, terminalReason, result?, error?, toolUses, activeRuntimeMs, tokens? }` | Agent reaches a terminal state |
514
- | `subagents:resumed` | same as `completed` (`buildEventData` shape) | A retained session's resumed turn finishes |
515
- | `subagents:failed` | same as `completed` (`buildEventData` shape) | Agent ends in `error` or `stopped` |
516
- | `subagents:compacted` | `{ id, type, description, reason, tokensBefore, compactionCount }` | Child session compacts |
517
- | `subagents:created` | `{ id, type, description, runId, mode }` | Agent record created (pre-admission) |
518
- | `subagents:steered` | `{ id, message }` | Steering message delivered to a running agent |
519
-
520
- These are fire-and-forget broadcast events — no request IDs, no reply channels.
521
-
522
- ## Target architecture
523
-
524
- The long-term architectural direction is to make pi-subagents a **minimal orchestrator** with inverted dependencies.
525
- The core spawns a child session derived from the parent, runs the turn loop, tracks and streams and collects the result, gates concurrency, supports resume, and **publishes its lifecycle**.
526
- Everything else — permissions, worktree/workspace isolation, UI, telemetry — is an extension that attaches through one of two surfaces and never reaches into the core.
527
-
528
- The rationale and the full reasoning chain that led here are recorded in [`docs/decisions/0002-extensions-on-a-minimal-core.md`](../decisions/0002-extensions-on-a-minimal-core.md).
529
-
530
- A separate, longer-horizon note — [`client-server-opportunities.md`](./client-server-opportunities.md) — records what Pi's eventual client-server split (Mario Zechner's session-sync unification) would unlock for pi-subagents: viewing live subagent sessions, viewing suspended ones, and operators interacting with a subagent through an editor.
531
- That architecture is not on the near-term roadmap; the note captures the opportunity so it is on record.
532
-
533
- ### Two extension surfaces
534
-
535
- Extensions attach through exactly two surfaces, distinguished by the direction of information flow.
536
-
537
- 1. **Lifecycle events (observational) — unlimited.**
538
- The core emits awaited, ordered events for the child-execution lifecycle (`spawning`, `session-created` pre-`bindExtensions`, `completed`, `disposed`).
539
- Any number of extensions subscribe; handlers return nothing.
540
- Reactive concerns live here: permission detection, telemetry, UI, notifications.
541
- Adding a reactive concern never modifies the core.
542
- 2. **Provider seams (generative) — rationed.**
543
- The rare concern that must _inject_ a value the core consumes synchronously registers a provider the core consults.
544
- Today there is exactly one: the **workspace provider** (returns the child's working directory plus bracketed setup/teardown).
545
- A provider seam is the only place the core is "open," so the list is kept as small as possible.
546
-
547
- The discriminator when deciding how a concern attaches:
548
-
549
- - It only needs to **know** what happened → subscribe to a lifecycle event (observational, unlimited).
550
- - It must **return a value the core consumes** → register a provider (generative, rationed).
551
-
552
- The governing rule — **no vacant hooks**: the architecture must _admit_ a seam without _shipping_ it until a concrete consumer exists.
553
- A provider seam with no consumer is a speculative abstraction that taxes every reader and that `fallow` flags as dead.
554
- Latent extensibility is the deliverable; a vacant hook is not.
555
-
556
- The [first-principles refinement](#first-principles-refinement-and-the-deeper-target) below sharpens this two-surface split.
557
- The awaited, behavior-affecting lifecycle events (notably `session-created` before `bindExtensions`) are _hooks_ — the child's own extension surface applied recursively, generative because the core waits on the handler before deciding what to do next.
558
- The observational surface then carries only fire-and-forget broadcasts of immutable snapshots, which no consumer can use to change the core.
559
-
560
- ### Core responsibilities (keep)
561
-
562
- - **Agent definitions** — name, model, thinking, system prompt, tools list.
563
- - **Prompt composition** — system prompt assembly.
564
- - **Session lifecycle** — create child sessions, bind extensions, run conversation loop, track results.
565
- - **Concurrency management** — queue, abort, resume, max concurrency.
566
- - **Recursion guard** — remove pi-subagents' own three tools from child sessions (prevent infinite nesting).
567
- With `isolated` removed (#264), children always load the parent's resources, so the guard is unconditional rather than gated on `cfg.extensions`.
568
- This is the core defending its own invariant, keyed off its own tool names — not policy.
569
- - **Lifecycle events** — emit awaited, ordered events when child sessions spawn, are created, complete, and are disposed.
570
- - **Workspace provider seam** — accept a registered `WorkspaceProvider` and consult it for the child's cwd; default to the parent's cwd when none is registered.
571
- - **Service API** — publish `SubagentsService` via `Symbol.for()` for cross-extension access.
572
-
573
- ### Responsibilities to remove
574
-
575
- - **Tool policy** (`disallowed_tools`) — access control belongs in pi-permission-system's `permission:` frontmatter.
576
- - **Extension filtering** (`extensions: string[]` allowlist) — tool visibility is pi-permission-system's job.
577
- - **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) — environment policy, not core.
578
- Git worktrees are one _strategy_ for choosing the child's working directory; containers, throwaway tmpdirs, and remote sandboxes are others.
579
- Evicted to `@gotgenes/pi-subagents-worktrees` (#263), the first consumer of the workspace provider seam.
580
- - **Extension lifecycle control** (`extensions: false`, `isolated`, `noSkills`) — removed in #264.
581
- Deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
582
- Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and is left as a _latent_ (un-built) provider seam, added only if a real consumer needs it.
583
-
584
- ### Composition model
585
-
586
- In the target state, pi-subagents publishes events and a provider seam; other packages hook in:
587
-
588
- - **pi-permission-system** (observational) subscribes to child-session lifecycle events, detects subagent execution context in the child, and gates tool calls at runtime.
589
- - **pi-subagents-worktrees** (generative) registers a `WorkspaceProvider` that prepares a git worktree at run-start and tears it down after, supplying the child's cwd.
590
- - **pi-subagents-ui** (future, under reconsideration — see the [first-principles refinement](#first-principles-refinement-and-the-deeper-target)) subscribes to the broadcast and the query/behavior interfaces; the conversation viewer and `/agents` menu were removed in Phase 19 per [ADR-0004]; the surviving UI (widget, session navigator, settings command) stays in-core.
591
- - **Any future extension** (OTel, auditing, cost tracking) subscribes to the same events without pi-subagents knowing.
592
-
593
- Composition test: install neither extension, only permissions, only workspaces, or both — the core is byte-for-byte identical in all four cases, and the two extensions never reference each other.
594
-
595
- This is achieved across phases: Phase 14 (strip policy), Phase 16 (invert dependencies — extensions on a minimal core), and Phase 18 (reconsider UI).
596
-
597
- ### First-principles refinement and the deeper target
598
-
599
- The two-surface model above is correct but coarse.
600
- Pushing it against our own principles — construct complete, state owns its mutations, tell-don't-ask, dependency inversion — surfaces sharper boundaries that the current code draws through the middle of classes.
601
- This subsection records the deeper target; the steps that realize it are sequenced in later phases.
602
-
603
- #### `Subagent` is four conflated domains
604
-
605
- The construction duality that motivates Phase 17 — a class that is simultaneously a passive record and an executor — is only the two most visible of four domains fused into one class.
606
- Pulling each apart by asking "who changes this, how often, and who needs to know" surfaces:
607
-
608
- 1. **Lifecycle state** — status, result, error, timestamps.
609
- Owned by the subagent; transitions are rare and meaningful; the right outward shape is an immutable snapshot announced on change.
610
- 2. **Metrics** — tool uses, token usage, compaction count.
611
- These are not lifecycle state; they are a projection aggregated over the child session's event stream.
612
- `record-observer` already computes them — its only error is writing the aggregate back onto the subagent.
613
- 3. **The hook surface** — the points where an extension alters or augments the child before and around its run.
614
- This is the child session's own extension binding (see below), not data on the subagent.
615
- 4. **Result delivery** — whether the parent has consumed the result, when to nudge, how the result reaches the caller.
616
- The homeless `notification.resultConsumed` field belongs to this domain, not to execution.
617
-
618
- The ~20 optional constructor fields and the runtime `run()` throws are the pressure these four domains exert on one class.
619
- Separating them is what makes the Phase 17 steps fall out rather than fight back.
620
-
621
- #### The subagent is a recursive Pi
622
-
623
- A subagent is a child Pi session: created with `createAgentSession`, then `bindExtensions`.
624
- Its extension surface is therefore Pi's extension surface applied recursively — not a bespoke event bus.
625
- What the current doc calls "awaited, ordered lifecycle events" are not observations; they are **hooks**, structurally identical to Pi's own (`session_start`, `tool_execution_start`).
626
- The tell is the awaiting: the core waits for the handler because the handler's completion changes what the core does next — an extension registers before the child binds.
627
- A handler that can change subsequent behavior is generative, not observational, whatever we name the channel.
628
-
629
- This splits the current "lifecycle events" surface cleanly in two:
630
-
631
- 1. **Broadcast** (observational, fire-and-forget) — "this happened; react if you want; you cannot change anything."
632
- Carries immutable snapshots for telemetry, notification, and any renderer.
633
- No consumer holds a live `Subagent`.
634
- 2. **Hooks** (generative, awaited, ordered) — the recursive Pi extension surface where workspace, permissions, and future concerns attach to the child.
635
- The `WorkspaceProvider` is one _typed_ hook; the general form is "be an extension of the child session."
636
-
637
- The "no vacant hooks" rule still governs the generative side: admit the surface, ship a hook only when a real consumer exists.
638
-
639
- #### Reactive versus discrete (not internal versus external)
640
-
641
- The axis that decides push versus pull is whether a need is reactive or discrete — never whether the consumer is in-package or out.
642
-
643
- - **Reactive** (ambient state that changes underneath you) → subscribe to the broadcast; be told.
644
- The state-owner announces; the consumer maintains its own read-model; nobody pulls.
645
- - **Discrete** (a one-shot question: current value, full transcript) → pull a query.
646
- `get_subagent_result`, `query_subagent_session`, opening a transcript, and the external `SubagentsService.getRecord` are queries by nature and stay pull, in-package or not.
647
-
648
- Behavior is a third interface: **tell by id, with outcomes**.
649
- `steer` and `abort` own their own rules — a non-running agent rejects a steer from inside `steer`, not via a caller's status pre-check — so coordinators never ask-then-tell.
650
-
651
- #### Consequences
399
+ | Channel | Meaning |
400
+ | --- | --- |
401
+ | `subagents:started` | Agent begins running. |
402
+ | `subagents:completed` | Agent reaches a terminal state. |
403
+ | `subagents:resumed` | A retained session's resumed turn finishes. |
404
+ | `subagents:failed` | Agent ends in error or stopped state. |
405
+ | `subagents:compacted` | Child session compacts. |
406
+ | `subagents:created` | Agent record exists before admission. |
407
+ | `subagents:steered` | A steering message reaches a running agent. |
652
408
 
653
- Two consequences fall straight out, and both cut scope.
409
+ The event names are defined in `src/service/service.ts`; the lifecycle observer
410
+ and child-lifecycle publisher own payload construction. Consumers should use
411
+ those code-owned contracts rather than a separately maintained payload schema.
654
412
 
655
- 1. **The activity/metrics push tier is provisional.**
656
- Its only reactive consumer is the inherited widget.
657
- Treated from first principles, metrics are accumulated by an observer, exposed as a discrete query, and folded into the completion snapshot — so the high-frequency stream may not need to exist at all.
658
- We do not contort the core's event design to feed an inherited consumer.
659
- 2. **Phase 18 is "reconsider the UI," not "extract the UI."**
660
- The widget and `/agents` menu predate the fork; they are consumers to be judged on our principles, not requirements to preserve.
661
- If a UI survives, it survives as a reactive consumer of the broadcast and a caller of the query/behavior interfaces — built on our terms, possibly smaller, possibly removed.
662
-
663
- #### Sibling packages follow the same discipline
664
-
665
- `@gotgenes/pi-permission-system` is one of these hooks, and it is subject to the same scrutiny.
666
- Its boundaries deserve the same first-principles treatment: surface its conflated domains, distinguish what it observes from what it injects, and prefer being told over asking.
667
- The recursion principle means a consumer's internal design is not exempt because it lives in another package — the same axes (reactive versus discrete, hook versus broadcast, construct complete) apply across the seam.
668
-
669
- #### How we find these boundaries
670
-
671
- The boundaries above were not deduced top-down; they were surfaced by friction.
672
- Each place the target got _harder_ to test marked a domain seam drawn through the middle of a class.
673
- That method — testability friction as a boundary probe, with its limits — is recorded in the `improvement-discovery` skill so it outlives this phase.
674
-
675
- ## Current structural analysis
676
-
677
- ### Health metrics
678
-
679
- | Metric | Value |
680
- | -------------------------- | --------------------------- |
681
- | Health score | 78/100 (B), end of Phase 19 |
682
- | Total LOC | 7,068 (57 files) |
683
- | Dead code | 0 files, 0 exports |
684
- | Maintainability index | 91.0 (good) |
685
- | Avg cyclomatic complexity | 1.4 |
686
- | P90 cyclomatic complexity | 2 |
687
- | Production duplication | 0 lines |
688
- | Test duplication | 9 clone groups, 81 lines |
689
- | Fallow refactoring targets | 0 |
690
-
691
- ### Dependency bag inventory
692
-
693
- These interfaces carry hidden dependencies that obscure true coupling.
694
- Bags with 10+ fields are the highest priority for decomposition.
695
-
696
- | Interface | Fields | Consumers | Severity |
697
- | ----------------------------- | ------------------------------------------------------------ | ------------------------------------------------- | --------- |
698
- | `ResolvedSpawnConfig` | 3 nested | spawn-config, agent-tool | ✓ done |
699
- | `AgentSpawnConfig` | execution options + parent identity | subagent-manager (internal) | ✓ done |
700
- | `CreateSubagentSessionParams` | 6 (snapshot, type, cwd, parentSession, model, thinkingLevel) | create-subagent-session | ✓ done |
701
- | `TurnLoopOptions` | 4 (maxTurns, defaultMaxTurns, graceTurns, signal) | subagent-session | ✓ done |
702
- | `SessionConfig` | 6 (flat fields; extensions/noSkills/extras removed in #264) | session-config (output of assembler) | ✓ done |
703
- | `NotificationDetails` | 10 | notification | Low (DTO) |
704
- | `ResourceLoaderOptions` | 10 | create-subagent-session (SDK bridge) | Low (SDK) |
705
- | `SubagentSessionIO` | split → `EnvironmentIO` (3) + `SessionFactoryIO` (5+1) | create-subagent-session | ✓ done |
706
- | `CreateSessionOptions` | 9 | create-subagent-session (SDK bridge) | Low (SDK) |
707
- | `AgentToolDeps` | 8 | agent-tool | ✓ done |
708
- | `SubagentInit` | 5 (id, type, description, invocation, execution, state) | subagent (one production site) | ✓ done |
709
- | `SubagentExecution` | 12 (4 mandatory: factory, snapshot, prompt, baseCwd) | subagent (mandatory collaborator) | ✓ done |
710
-
711
- ### Complexity hotspots
712
-
713
- Functions with cyclomatic complexity ≥ 21 (critical threshold):
714
-
715
- No functions remain above the critical threshold — all hotspots resolved in Phase 12. 6 functions remain at HIGH severity (CRAP ≥ 65); 13 at moderate.
716
-
717
- ### Churn hotspots
718
-
719
- Files with highest commit frequency × complexity:
720
-
721
- | Score | File | Commits | Trend |
722
- | ----- | ----------------------------- | ------- | -------------- |
723
- | 27.1 | `index.ts` | 109 | ▼ cooling |
724
- | 9.6 | `tools/agent-tool.ts` | 56 | ▼ cooling |
725
- | 8.8 | `ui/agent-widget.ts` | 22 | ▼ cooling |
726
- | 7.3 | `lifecycle/subagent.ts` | 22 | ▼ cooling |
727
- | 6.6 | `service/service-adapter.ts` | 15 | ▲ accelerating |
728
- | 6.3 | `config/custom-agents.ts` | 13 | ▼ cooling |
729
-
730
- `index.ts` remains the top churn hotspot but has cooled after the Phase 19 terminal cut removed its four `/agents`-wiring blocks; `service-adapter.ts` is now the sole accelerating file.
731
-
732
- ### Production duplication
733
-
734
- The prior clone group between the execution and formatting paths was resolved in #172.
735
- The definition-management clone groups were removed with the `/agents` terminal cut in Phase 19 ([#441]); the supporting file-writer module was removed with them. Production duplication remains 0 lines.
736
-
737
- ### Session encapsulation debt (Law of Demeter) — resolved by [#277] ✔️
738
-
739
- All consumer reach-throughs to the raw SDK `AgentSession` via `Subagent.session` have been eliminated.
740
- `Subagent.session` is removed; `SubagentSession.session` is marked `@internal` (lifecycle use only).
741
- The intent-revealing replacements added by [#277]:
742
-
743
- | Reach-through | Sites | Replacement |
744
- | ---------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------- |
745
- | Steer buffer-or-deliver (was duplicated) | `service-adapter.ts`, `steer-tool.ts` | `Subagent.steer(message)` |
746
- | Conversation viewing | `session-navigation.ts`, `session-navigator.ts` | `Subagent.agentMessages` / transcript sources |
747
- | Session-readiness guard | `agent-tool.ts`, `subagent-manager.ts` | `Subagent.isSessionReady()` |
748
- | Context-window stats | `steer-tool.ts`, `get-result-tool.ts`, `notification.ts` | `Subagent.getContextPercent()` |
749
- | Live updates (subscription) | `session-navigator.ts` | `Subagent.subscribeToUpdates(fn)` |
750
- | Session disposal | `subagent-manager.ts` | `SubagentSession.dispose()` — resolved by [#265] |
751
-
752
- ### Proposed bag decompositions
753
-
754
- #### ResolvedSpawnConfig (15 fields → 3 value objects)
755
-
756
- This bag mixes three concerns: who the agent is, how it should run, and how it should be displayed.
757
- Each consumer uses a different subset.
758
-
759
- ```typescript
760
- /** Who this agent is — type resolution result. */
761
- interface SpawnIdentity {
762
- subagentType: string;
763
- rawType: SubagentType;
764
- fellBack: boolean;
765
- displayName: string;
766
- }
767
-
768
- /** How the agent should run — execution parameters. */
769
- interface SpawnExecution {
770
- prompt: string;
771
- description: string;
772
- model: Model<any> | undefined;
773
- effectiveMaxTurns: number | undefined;
774
- thinking: ThinkingLevel | undefined;
775
- inheritContext: boolean;
776
- mode: SubagentMode;
777
- timeoutSeconds?: number;
778
- agentInvocation: AgentInvocation;
779
- }
780
-
781
- /** How the agent is presented — display metadata. */
782
- interface SpawnPresentation {
783
- modelName: string | undefined;
784
- agentTags: string[];
785
- detailBase: Pick<AgentDetails, ...>;
786
- }
787
- ```
788
-
789
- `agent-tool` consumes all three to build the internal `AgentSpawnConfig` and the result text.
790
- The joined/detached mode lives in `SpawnExecution`; no separate foreground/background runner exists.
791
- After decomposition, each consumer declares its real dependencies explicitly.
792
-
793
- #### AgentSpawnConfig — ParentSessionInfo extracted (done, [#166][166])
794
-
795
- The `parentSessionFile`, `parentSessionId`, and `toolCallId` fields were grouped into `ParentSessionInfo`:
796
-
797
- ```typescript
798
- /** Parent session identity — always travel together from the tool boundary. */
799
- export interface ParentSessionInfo {
800
- parentSessionFile?: string;
801
- parentSessionId?: string;
802
- toolCallId?: string;
803
- }
804
- ```
805
-
806
- `AgentSpawnConfig` now carries `parentSession?: ParentSessionInfo` instead of three flat optional fields.
807
-
808
- #### RunOptions (12 fields → extract RunContext) — done ([#169][169]), updated by [#231]
809
-
810
- `RunContext` was extracted and nested as `RunOptions.context` in #169.
811
- Issue #231 moved the two static dependencies (`exec`, `registry`) to `RunnerDeps` on `ConcreteAgentRunner`, leaving `RunContext` with only per-call fields:
812
-
813
- ```typescript
814
- /** Per-call execution context — fields that vary per spawn. */
815
- export interface RunContext {
816
- cwd?: string;
817
- parentSession?: ParentSessionInfo;
818
- }
819
- ```
820
-
821
- The current execution seam is `SubagentExecution` in `lifecycle/subagent.ts`; it carries the immutable parent snapshot, joined/detached mode, runtime deadline, session factory, and lifecycle collaborators directly. The old runner-specific `RunOptions` bag is no longer part of the source tree.
822
-
823
- #### SessionConfig (11 fields → extract ToolFilterConfig) — done ([#168][168])
824
-
825
- The tool-filtering cluster (`toolNames`, `disallowedSet`, `extensions`) was extracted into `ToolFilterConfig` and nested as `SessionConfig.toolFilter`.
826
- `filterActiveTools` now accepts a single `ToolFilterConfig` argument instead of three positional parameters.
827
- `SessionConfig` reduced from 10 to 8 top-level fields.
828
-
829
- #### Session factory IO (9 methods → 2 focused interfaces) — done ([#167][167])
830
-
831
- The IO boundary was split into two focused interfaces:
832
-
833
- ```typescript
834
- /** Environment discovery — detect runtime context and resolve directories. */
835
- export interface EnvironmentIO {
836
- detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
837
- getAgentDir: () => string;
838
- deriveSessionDir: (
839
- parentSessionFile: string | undefined,
840
- effectiveCwd: string,
841
- ) => string;
842
- }
843
-
844
- /** Session factory — create SDK objects for a child agent session. */
845
- export interface SessionFactoryIO {
846
- createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
847
- createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
848
- createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
849
- createSession: (
850
- opts: CreateSessionOptions,
851
- ) => Promise<{ session: AgentSession }>;
852
- assemblerIO: AssemblerIO;
853
- }
854
- ```
855
-
856
- The factory consumes the `EnvironmentIO & SessionFactoryIO` boundary directly; no runner implementation or compatibility alias remains.
857
-
858
- ## Phase 11 (complete)
859
-
860
- Phase 11 converted all closure factories to classes, eliminating adapter closure density in `index.ts`.
861
- Four layers: SessionContext typing → runtime query methods → interface alignment → class conversions → index.ts simplification.
862
- See [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) for details.
863
-
864
- ## Phase 12 (complete)
865
-
866
- Phase 12 decomposed the three remaining high-complexity UI functions and extracted shared test fixtures.
867
- All four steps are closed: [#205], [#206], [#207], [#208].
868
-
869
- ## Phase 13 (complete)
870
-
871
- Phase 13 addressed remaining closure factories, the last fallow refactoring target, oversized methods, production duplication, SDK boundary coupling, and test clone families.
872
- All six steps are closed: [#214], [#215], [#216], [#217], [#218], [#219].
873
- See [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) for details.
874
-
875
- ## Phase 14 (complete)
876
-
877
- Phase 14 removed tool and extension policy enforcement from pi-subagents, eliminating overlap with pi-permission-system.
878
- All four steps are closed: [#237], [#238], [#239], [#242].
879
- See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
880
-
881
- [#237]: https://github.com/gotgenes/pi-packages/issues/237
882
- [#238]: https://github.com/gotgenes/pi-packages/issues/238
883
- [#239]: https://github.com/gotgenes/pi-packages/issues/239
884
- [#242]: https://github.com/gotgenes/pi-packages/issues/242
885
-
886
- ## Phase 15 (complete)
887
-
888
- Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that owns its entire execution lifecycle.
889
- Before Phase 15, `AgentManager` orchestrated everything: calling the runner, handling session creation, wiring observers, and cleaning up worktrees — reaching into Agent 10+ times across `spawn()` and `startAgent()`.
890
- After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
891
- All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
892
- See [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) for details.
893
-
894
- ## Phase 16 (complete)
895
-
896
- Phase 16 inverted the core's outbound dependencies: worktree isolation joined permissions as an _extension_ on a minimal core, leaving pi-subagents a pure child-session orchestrator.
897
- The core now attaches extensions through exactly two surfaces — observational lifecycle events (unlimited) and rationed generative provider seams (today only the workspace provider) — and has zero knowledge of its consumers.
898
- The "runner" concept is gone: `createSubagentSession()` returns a born-complete `SubagentSession` that owns turn driving, steering, and disposal, and `Subagent.run()` is coordination, not assembly.
899
- The decision and the full reasoning chain are recorded in [ADR-0002]; the two-surface extension model is described under [Target architecture](#target-architecture).
900
- All five steps are closed: [#261], [#262], [#263], [#264], [#265].
901
- The earlier "agent collaborator architecture" framing (#256 superseded, #257 parked, #258 and #259 closed not-planned) was abandoned; its structural win was reached cleanly via the workspace seam.
902
- See [phase-16-invert-dependencies.md](history/phase-16-invert-dependencies.md) for details.
903
-
904
- ## Phase 17 (complete)
905
-
906
- Phase 17 consolidated the core's remaining structural debt before the UI reconsideration (Phase 18).
907
- The `Subagent` record/executor duality was resolved by extracting `SubagentState` (status, result, timestamps, metrics) into a private owned value object and making `SubagentExecution` a mandatory constructor collaborator — eliminating ~20 optional fields and the two "not configured for execution" runtime throws.
908
- The concurrency queue was replaced with a thunk-based `ConcurrencyLimiter` that knows nothing about agents or IDs.
909
- Run collaborators (`RunListeners`, `WorkspaceBracket`) were extracted from the 455-LOC `Subagent` class.
910
- The inline `SubagentManagerObserver` literal was promoted to `SubagentEventsObserver`, making its three concerns (event emission, record persistence, notification dispatch) unit-testable in isolation.
911
- Widget delegation and the post-construction `runtime.widget =` write were removed from `SubagentRuntime` by dissolving the notification→widget cycle via `AgentWidget.seedFinishedAgents`.
912
- Lifecycle, UI/tools, and cross-package test fixture clones were consolidated.
913
- `loadLayeredSettings<T>` was extracted to `src/layered-settings.ts` and published via the `@gotgenes/pi-subagents/settings` subpath export, eliminating the 23-line production clone with `pi-subagents-worktrees`.
914
- All nine steps are closed: [#381], [#373], [#374], [#375], [#376], [#377], [#378], [#379], [#380].
915
- [#412] unified the overlapping session-mock builders identified during Step 7.
916
- [#415] migrated `pi-subagents-worktrees` to `loadLayeredSettings` after the Step 9 published release.
917
- See [phase-17-core-consolidation.md](history/phase-17-core-consolidation.md) for the full findings, step outcomes, dependency diagram, and tracks.
918
-
919
- ## Phase 18 (complete)
920
-
921
- Phase 18 disentangled the activity tier from the core and recorded a first-principles decision about the UI's direction.
922
- Steps 1–5 (the spine) consolidated all run state onto `SubagentState`, deleted `AgentActivityTracker` and `ui-observer` (−145 LOC), and made the widget a pure reactive consumer of lifecycle events with no inbound calls from core spawn tools.
923
- Step 6 reconciled the public event contract (breaking: removed the vacant `SUBAGENT_EVENTS.ACTIVITY` channel; added `FAILED`, `COMPACTED`, `CREATED`, `STEERED`).
924
- Step 7 consolidated residual test clone families, dropping from 24 to 14 clone groups (below the <15 target).
925
- Step 8 captured the per-component UI decisions in [ADR-0004]: the widget shrinks to background agents only; the bespoke `ConversationViewer` is replaced by native session navigation (a read-only transcript via `parseSessionEntries`, per the Step 1 spike — see [ADR-0004]'s addendum); the `/agents` command is dissolved (remove the create wizard and agent-types editor, extract settings to a focused command); the surviving UI stays in-core as a substitutable reactive consumer.
926
- Source LOC decreased from 7,751 (62 files) to 7,650 (61 files); tests grew from 1,031 to 1,047.
927
- All eight steps are closed: [#420], [#421], [#422], [#423], [#424], [#425], [#426], [#427].
928
- See [phase-18-reconsider-ui.md](history/phase-18-reconsider-ui.md) for the full findings, step outcomes, dependency diagram, and tracks.
929
-
930
- ## Phase 19 (complete)
931
-
932
- Phase 19 implemented the per-component UI decisions recorded in [ADR-0004]: it shrank the widget to background-only agents, replaced the bespoke conversation viewer with native session navigation dual-sourced by liveness (live record or persisted-file snapshot) and rendered through Pi's own per-entry TUI components, extracted subagent settings to a focused `/subagents-settings` command, and dissolved the monolithic `/agents` menu in a two-commit terminal cut (hub first, then the orphaned agent-definition-management subtree).
933
- Production duplication dropped to 0 lines; test clone groups dropped from 16 to 9 (target: ≤ 10); source shrank from 7,650 LOC (61 files) to 7,068 LOC (57 files).
934
- All nine steps are closed: [#446], [#447], [#444], [#445], [#462], [#463], [#442], [#441], [#443].
935
- A follow-on issue, [#470] (README staleness after the terminal cut), was filed and closed independently.
936
- See [phase-19-implement-ui-decisions.md](history/phase-19-implement-ui-decisions.md) for the full findings, step outcomes, dependency diagram, and tracks.
937
-
938
- ## Phase 20 improvement roadmap
939
-
940
- > Planning note: this roadmap records the pre-execution-control baseline. The
941
- > joined/detached execution-control work below supersedes any obsolete runner,
942
- > unbounded-result, and foreground/background vocabulary in these notes.
943
-
944
- Phase 20 realizes the last un-extracted domain from the [first-principles refinement](#first-principles-refinement-and-the-deeper-target) — **result delivery** — and clears the residual boundary and complexity debt discovery surfaced around it.
945
-
946
- Discovery findings (fallow + entry-point trace + test-constructibility audit, 2026-07-03):
947
-
948
- 1. `NotificationState` (`toolCallId`, `resultConsumed`) still lives on `Subagent`; `get-result-tool` reaches through `record.notification?.markConsumed()` twice, always paired with `notifications.cancelNudge(id)` — the doc's own "homeless field" (result-delivery domain) plus a scattered two-step reset.
949
- 2. Both `steer-tool` and `service-adapter` pre-check `status !== "running"` before calling `record.steer()` — ask-then-tell, contradicting the target's "tell by id, with outcomes" rule.
950
- 3. Five file-level eslint-disable headers (`agent-tool` disables 6 rules; `spawn-config` and `agent-widget` 4 each; `model-resolver` 2; `index` 1) and `model: unknown`/`Model<any>`/`any` threading through 8 files mark the SDK type boundary as the largest remaining `any` surface.
951
- 4. Three src functions carry HIGH CRAP scores (notification renderer arrow 79.4, `service-adapter.spawn` 71.3, `get-result-tool.execute` 63.6 — resolved by Step 2); `subagents-settings.handle` (13 cyclomatic, 24 cognitive) is three copy-pasted select→input→validate→apply branches; `service-adapter.ts` is the sole accelerating churn file.
952
- 5. `createTestSubagent` is the most complex function in the workspace (19 cyclomatic, 25 cognitive) because `SubagentStateInit` accepts only transition fields, forcing mutation loops to seed metrics — a Category D "shared factory complexity" signal pointing at the production init surface.
953
- 6. Test duplication sits at 9 in-package clone groups (81 lines), at the ≤ 10 target but with two consolidatable clone families.
954
-
955
- No finding scores ≥ 20 on the priority scale (Impact × (6 − Risk)); the phase is a should-fix band (10–15) consolidation.
956
- Directory organization is healthy (seven domain directories, six root files) — no reorg this phase.
957
-
958
- ### Health metrics (Phase 20)
959
-
960
- | Metric | Phase 19 (end) | Phase 20 target |
961
- | ------------------------------------------------ | ---------------- | ---------------------- |
962
- | Health score | 78/100 (B) | ≥ 78 (B) |
963
- | Source LOC | 7,068 (57 files) | ~7,050 (no net growth) |
964
- | `record.notification?.` reach-throughs | 4 sites | 0 |
965
- | Steer status pre-checks outside `Subagent.steer` | 2 sites | 0 |
966
- | src functions with CRAP ≥ 60 | 3 | 0 |
967
- | File-level eslint-disable headers | 5 | ≤ 2 |
968
- | `createTestSubagent` cyclomatic | 19 | ≤ 8 |
969
- | Test clone groups (in-package) | 9 (81 lines) | ≤ 5 (≤ 40 lines) |
970
-
971
- ### Steps
972
-
973
- #### ✅ Step 1 — Extract result delivery from `Subagent` ([#535])
974
-
975
- Result delivery evolved twice. `notification-state.ts` was first dissolved and `Subagent.toolCallId` moved to execution identity. Consumption now lives as domain state on `SubagentState`, where retention and every push/pull delivery path can consult it. `NotificationManager` owns only delivery timing: it withholds records during a parent run and rechecks consumption at `agent_settled`, eliminating the uncancellable follow-up race rather than relying on a timer window.
976
-
977
- `Release: batch "result-delivery"`
978
-
979
- #### ✅ Step 2 — Decompose `get-result-tool.execute` ([#536])
980
-
981
- Smell: Category B (oversized function) — 61 lines, 15 cyclomatic, CRAP 63.6; mixes wait/consume policy, stats formatting, and output assembly.
982
- Target files:
983
-
984
- - `src/tools/get-result-tool.ts` — extract a pure report formatter (status line, stats parts, body selection) alongside the existing `result-renderer.ts` pattern; consume via the Step 1 delivery interface.
985
- - `test/tools/get-result-tool.test.ts` — unit-test the pure formatter directly.
986
-
987
- Outcome: `execute` ≤ 30 lines with cyclomatic < 10; off the fallow high-complexity list.
988
-
989
- Landed: bounded result formatting was extracted while the earlier result-delivery design was active. The later execution-control clean break removed the standalone report module and its unbounded wait path; `GetResultTool` now owns a nonblocking, bounded status/result projection.
990
- `get-result-tool.execute` remains a focused lookup-and-render boundary.
991
-
992
- `Release: batch "result-delivery"`
993
-
994
- #### ✅ Step 3 — `Subagent.steer` returns an outcome ([#537])
995
-
996
- Smell: Category C (ask-then-tell) — coordinators pre-check status before telling.
997
- Target files:
998
-
999
- - `src/lifecycle/subagent.ts` — `steer` owns the non-running rejection and returns a discriminated outcome (`delivered` / `buffered` / `rejected` with the observed status).
1000
- - `src/tools/steer-tool.ts`, `src/service/service-adapter.ts` — drop the status pre-checks and switch on the outcome; the adapter maps the outcome to the public `SubagentsService.steer` boolean, so the published contract is unchanged.
1001
-
1002
- Outcome: zero steer status pre-checks outside `Subagent.steer`; `steer-tool.execute` cyclomatic drops below 10.
1003
-
1004
- Landed: `Subagent.steer` returns a discriminated `SteerOutcome` (`delivered` / `buffered` / `rejected` with the observed status) and owns the non-running rejection as its first guard; `SteerOutcome` is exported from `subagent.ts` and re-exported via `types.ts`.
1005
- `SteerTool.execute` and `SubagentsServiceAdapter.steer` dropped their `status !== "running"` pre-checks and switch on the outcome — the adapter maps `outcome.kind !== "rejected"` to the unchanged public boolean, and the tool's delivered-path stats moved into a private `renderDelivered` helper.
1006
- Zero steer status pre-checks remain outside `Subagent.steer`.
1007
-
1008
- `Release: independent`
1009
-
1010
- #### ✅ Step 4 — Type the model boundary ([#538])
1011
-
1012
- Smell: Category C (platform type threading) — `ModelRegistry.find/getAll/getAvailable` return `any`, forcing `any`/`unknown` model threading through `model-resolver`, `spawn-config`, `service-adapter`, and `parent-snapshot`.
1013
- Target files:
1014
-
1015
- - `src/session/model-resolver.ts` — type the registry against `Model<any>` from `@earendil-works/pi-ai` (already imported elsewhere); remove the file-level eslint-disable; extract the fuzzy-scoring loop as a named helper if `resolveModel` (17 cyclomatic, 60 lines) still trips the threshold.
1016
- - `src/service/service-adapter.ts` — type the resolved model in `spawn` (16 cyclomatic, CRAP 71.3, sole accelerating churn file) and extract the model-resolution branch.
1017
- - `src/tools/spawn-config.ts` — shrink the 4-rule file-level disable to line-level or remove it.
1018
-
1019
- Outcome: `model-resolver.ts` file-level eslint-disable removed; `service-adapter.spawn` off the HIGH CRAP list; `any` model returns eliminated from the resolver.
1020
-
1021
- Landed: `ModelRegistry.find/getAll/getAvailable` and `resolveModel`'s return are typed against `Model<any>` from `@earendil-works/pi-ai`; the file-level eslint-disable headers on `model-resolver.ts` (2 rules) and `spawn-config.ts` (4 rules) are both removed — running disable-header tally 5 → 3 (`agent-tool` 6, `agent-widget` 4, `index` 1 remain, all Step 5 scope).
1022
- `resolveModel`'s fuzzy-scoring loop was extracted to a private `findBestFuzzyMatch` helper, dropping `resolveModel` off the complexity list entirely.
1023
- `service-adapter.spawn`'s model-resolution branch was extracted to a private `resolveModelOption`, dropping `spawn` from 16 cyclomatic / CRAP 71.3 (HIGH) to 13 cyclomatic / CRAP 49.5 (moderate) — off the HIGH-CRAP list; running HIGH-CRAP tally 2 → 1 remaining (the notification renderer arrow, untouched — Step 7 scope).
1024
- `resolveInvocationModel` gained a `registry: ModelRegistry | undefined` guard (typed error instead of a crash when a model override is requested with no registry present); the residual `unknown` thread through `ParentSnapshot.model` / `SessionContext.model` is a separate SDK-boundary gap, deferred.
1025
-
1026
- `Release: independent`
1027
-
1028
- #### Step 5 — Narrow `tui`/`theme` render interfaces ([#539])
1029
-
1030
- Smell: Category C/D (platform type threading; wide `any` params in render callbacks).
1031
- Target files:
1032
-
1033
- - `src/ui/agent-widget.ts` — replace `tui: any` with a lean local interface (`terminal.columns`, `requestRender()`); shrink the 4-rule file-level disable.
1034
- - `src/tools/agent-tool.ts` — type `renderCall`/`renderResult` params (`theme`, `result`) with lean local interfaces; shrink the 6-rule file-level disable to the genuinely SDK-gapped lines.
1035
- Some disables are irreducible SDK export gaps; the goal is line-level precision, not zero.
1036
-
1037
- Outcome: file-level eslint-disable headers 5 → ≤ 2; remaining suppressions are line-level with named rules.
1038
-
1039
- `Release: independent`
1040
-
1041
- #### Step 6 — Table-driven settings handler ([#540])
1042
-
1043
- Smell: Category B (function duplication inside one function) — `subagents-settings.handle` (13 cyclomatic, 24 cognitive, 52 lines) repeats the select→input→parse→validate→apply→notify flow three times.
1044
- Target files:
1045
-
1046
- - `src/ui/subagents-settings.ts` — describe each numeric setting as a descriptor (label, prompt, minimum, validation message, apply method) and drive one loop over the table.
1047
- - `test/ui/subagents-settings.test.ts` — assert per-descriptor behavior.
1048
-
1049
- Outcome: `handle` cyclomatic ≤ 6 and cognitive ≤ 10; off the fallow high-complexity list.
1050
-
1051
- `Release: independent`
1052
-
1053
- #### Step 7 — Decompose the notification renderer ([#541])
1054
-
1055
- Smell: Category B/D (oversized arrow, untested complexity) — the renderer arrow in `src/observation/renderer.ts` is fallow's top triage concern (17 cyclomatic, CRAP 79.4).
1056
- Target files:
1057
-
1058
- - `src/observation/renderer.ts` — extract pure line-assembly helpers (status→icon/label selection, stats-parts assembly, preview truncation) that are unit-testable without `Text` or a theme; the arrow becomes a thin wrapper.
1059
- - `test/observation/renderer.test.ts` — test the pure helpers directly.
1060
-
1061
- Soft ordering: land after Step 1 so the notification-domain files settle first.
1062
-
1063
- Outcome: renderer arrow cyclomatic < 10; `renderer.ts` off the top of the fallow triage list.
1064
-
1065
- `Release: independent`
1066
-
1067
- #### Step 8 — Full-value `SubagentStateInit` ([#542])
1068
-
1069
- Smell: Category D (shared factory complexity → narrow/complete the production init surface) — `createTestSubagent` (19 cyclomatic, 25 cognitive) seeds metrics via mutation loops because `SubagentStateInit` accepts only transition fields.
1070
- Target files:
1071
-
1072
- - `src/lifecycle/subagent-state.ts` — extend `SubagentStateInit` to optionally seed the full value (toolUses, lifetimeUsage, compactionCount, turnCount, activeTools, responseText); a value object is legitimately constructible at any point in its value space.
1073
- - `test/helpers/make-subagent.ts` — collapse the mutation loops into direct init.
1074
-
1075
- Outcome: `createTestSubagent` cyclomatic ≤ 8; off the fallow complexity list; no production behavior change.
1076
-
1077
- `Release: independent`
1078
-
1079
- #### Step 9 — Consolidate remaining test clone families ([#543])
1080
-
1081
- Smell: Category D (test duplication) — two clone families (`spawn-config.test.ts`: 2 groups / 21 lines; `subagent-manager.test.ts`: 2 groups / 15 lines) plus the `session-config.test.ts` pair (16 lines).
1082
- Target files: the three test files and `test/helpers/` as needed.
1083
-
1084
- Runs last — Steps 1–3 and 8 rewrite portions of these suites, so consolidating first would churn twice.
1085
-
1086
- Outcome: in-package clone groups 9 → ≤ 5; duplicated lines 81 → ≤ 40.
1087
-
1088
- `Release: independent`
1089
-
1090
- ### Step dependencies
413
+ These are fire-and-forget broadcast events no request IDs, no reply channels.
1091
414
 
1092
- ```mermaid
1093
- flowchart LR
1094
- S1["✅ Step 1 (#535)<br/>Result delivery off Subagent"] --> S2["✅ Step 2 (#536)<br/>Decompose get-result-tool"]
1095
- S1 -.soft.-> S7["Step 7 (#541)<br/>Decompose notification renderer"]
1096
- S3["✅ Step 3 (#537)<br/>Steer returns an outcome"]
1097
- S4["✅ Step 4 (#538)<br/>Type the model boundary"]
1098
- S5["Step 5 (#539)<br/>Narrow tui/theme interfaces"]
1099
- S6["Step 6 (#540)<br/>Table-driven settings handler"]
1100
- S8["Step 8 (#542)<br/>Full-value SubagentStateInit"] --> S9["Step 9 (#543)<br/>Consolidate test clones"]
1101
- S2 --> S9
1102
- S3 --> S9
1103
- ```
415
+ ## Extension composition
416
+
417
+ The core owns child-session orchestration. Companion extensions attach through
418
+ lifecycle observation or a concrete provider seam, rather than reaching into
419
+ `SubagentManager` or making the core look up a named consumer.
420
+
421
+ ### Observation and providers
422
+
423
+ - Child-execution lifecycle publication exposes spawning, pre-bind session
424
+ creation, completion, and disposal to consumers. Session creation is published
425
+ before extension binding so a companion can identify the child at that boundary.
426
+ - Agent lifecycle broadcasts report immutable event data. They are notifications,
427
+ not prompt or result replacement APIs.
428
+ - A workspace provider supplies the child's working directory and bracketed
429
+ cleanup. With no provider, the child runs in the parent's working directory.
430
+ - Ordered lifecycle interceptors can replace prompts or candidate results, abort a
431
+ turn, or request bounded same-session continuation. Registration order,
432
+ cancellation, failure, and disposal follow [ADR-0005].
433
+
434
+ The governing rule is **no vacant hooks**: admit a seam in the design, but ship it
435
+ only when a concrete consumer needs it. Permissions and workspace strategies
436
+ compose independently; the core depends on neither implementation.
437
+
438
+ ### State, queries, and behavior
439
+
440
+ Run state has one owner. Observers accumulate statistics and activity from the
441
+ child session; tools and UI read the resulting state rather than counting again.
442
+ Reactive UI keeps its own bounded read model. Discrete questions—status, results,
443
+ and transcript queries—remain bounded pull operations. Steering and stop requests
444
+ are behaviors whose owner checks whether the transition is valid, not a sequence
445
+ of caller-side status checks followed by external mutation.
446
+
447
+ Mutable parent identity and prompt inputs are frozen before admission. The
448
+ canonical parent model/authentication runtime is shared deliberately, preserving
449
+ runtime registrations without retaining a live session context. Cleanup is owned
450
+ at resource boundaries, and a queued run has an awaitable settlement even before
451
+ its concurrency slot opens.
452
+
453
+ ### Engineering boundaries
454
+
455
+ The entry point constructs collaborators and binds Pi APIs. Domain modules receive
456
+ explicit dependencies; environment discovery and session-factory I/O are separate
457
+ boundaries so tests can drive real behavior without module-mocking choreography.
458
+ Rendering helpers consume values, while Pi-specific components own terminal
459
+ integration. Shared test setup may be extracted, but the operation under test
460
+ stays visible rather than hidden merely to reduce duplication metrics.
461
+
462
+ The root service and `./settings` are public package exports. Type declarations
463
+ are generated and bundled independently of the shipped TypeScript runtime source;
464
+ [ADR-0003] owns that packaging decision. Public interfaces and durable user data
465
+ carry compatibility obligations; internal structures remain code-owned.
466
+
467
+ ## Fork ownership
468
+
469
+ `@nklisch/pi-subagents` is maintained in the `nklisch/pi-extensions` monorepo.
470
+ [Fork maintenance](../FORK-MAINTENANCE.md) owns provenance, release policy, and the
471
+ upstream relationship. [Comparison with upstream](../comparison-with-upstream.md)
472
+ explains the product boundary; upstream ideas are not automatic parity work.
1104
473
 
1105
- ### Parallel tracks
1106
-
1107
- - **Track A — Result delivery:** Steps 1 → 2, then 7 (soft).
1108
- - **Track B — Tell-don't-ask:** Step 3.
1109
- - **Track C — SDK boundary:** Steps 4, 5 (independent of each other).
1110
- - **Track D — UI polish:** Step 6.
1111
- - **Track E — Test health:** Step 8, then 9 (9 also waits on Tracks A/B test churn).
1112
-
1113
- Tracks A–D can proceed in parallel; only Step 9 serializes behind the rest.
1114
-
1115
- ### Release batches
1116
-
1117
- - **Batch "result-delivery":** Steps 1, 2 (ship together; tail = Step 2).
1118
- - Independently releasable: Steps 3, 4, 5, 6, 7, 8, 9.
1119
-
1120
- Every step lands as a `refactor:`/`test:` commit — hidden changelog types that cut no release on their own; the work auto-batches into the next unhidden release.
1121
-
1122
- ## Refactoring history
1123
-
1124
- Phases 1–5, 7–19 are complete.
1125
- Phase 6 (UI extraction to a separate package) was deferred to Phase 18; its intent was resolved by [ADR-0004] (Phase 18 Step 8).
1126
- Detailed records are preserved in per-phase history files:
1127
-
1128
- | Phase | Title | Status | History |
1129
- | ----- | --------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------ |
1130
- | 1 | Export SubagentsService API boundary | Complete | [phase-1-api-boundary.md](history/phase-1-api-boundary.md) |
1131
- | 2 | Remove scheduling subsystem | Complete | [phase-2-remove-scheduling.md](history/phase-2-remove-scheduling.md) |
1132
- | 3 | Remove group-join, RPC; replace output-file | Complete | [phase-3-remove-rpc-groupjoin.md](history/phase-3-remove-rpc-groupjoin.md) |
1133
- | 4 | Implement and publish SubagentsService | Complete | [phase-4-implement-service.md](history/phase-4-implement-service.md) |
1134
- | 5 | Decompose index.ts | Complete | [phase-5-decompose-index.md](history/phase-5-decompose-index.md) |
1135
- | 6 | Extract UI to separate package | Superseded by ADR-0004 | — |
1136
- | 7 | Encapsulation and dependency narrowing | Complete | [phase-7-encapsulation.md](history/phase-7-encapsulation.md) |
1137
- | 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
1138
- | 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
1139
- | 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
1140
- | 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
1141
- | 12 | Complexity reduction and test fixture extraction | Complete | [phase-12-complexity-test-fixtures.md](history/phase-12-complexity-test-fixtures.md) |
1142
- | 13 | Remaining structural smells | Complete | [phase-13-remaining-smells.md](history/phase-13-remaining-smells.md) |
1143
- | 14 | Strip policy from core | Complete | [phase-14-strip-policy.md](history/phase-14-strip-policy.md) |
1144
- | 15 | Domain model evolution | Complete | [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) |
1145
- | 16 | Invert dependencies (extensions on a minimal core) | Complete | [phase-16-invert-dependencies.md](history/phase-16-invert-dependencies.md) |
1146
- | 17 | Core consolidation | Complete | [phase-17-core-consolidation.md](history/phase-17-core-consolidation.md) |
1147
- | 18 | Reconsider UI (first principles) | Complete | [phase-18-reconsider-ui.md](history/phase-18-reconsider-ui.md) |
1148
- | 19 | Implement ADR-0004 UI decisions | Complete | [phase-19-implement-ui-decisions.md](history/phase-19-implement-ui-decisions.md) |
1149
-
1150
- ### Structural refactoring issues
1151
-
1152
- | Phase | Issue | Summary |
1153
- | -------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1154
- | Foundation | #69, #71, #76, #80 | SubagentRuntime, pure assembler, cwd injection, config consolidation |
1155
- | Core decomposition | #84, #72, #87, #70 | WorktreeManager, AgentManager DI, runtime methods, handler extraction |
1156
- | Interface polish | #66, #77 | SDK types, projectAgentsDir |
1157
- | Features | #61 | JSONL session transcripts |
1158
- | AgentManager | #98, #99, #100, #102 | Record state machine, ParentSnapshot, session-event observation, test factory |
1159
- | Encapsulation | #108, #109, #110, #111, #112, #113, #114, #115, #116, #118 | Registry, settings, activity tracker, record lifecycle, observer, spawn options, deps narrowing, tool split, type housekeeping |
1160
- | Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
1161
- | Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
1162
- | Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
1163
- | Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
1164
- | Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
1165
- | Phase 13 | #214, #215, #216, #217, #218, #219 | Closure-to-class, buildParentContext, startAgent decomp, overwrite guard, settings SDK, test duplication |
1166
- | Phase 14 | #237, #238, #239, #242 | Remove disallowed_tools, remove extensions filtering, collapse filterActiveTools, rename Agent to subagent |
1167
- | Phase 15 | #227, #228, #231, #229, #230, #232 | Agent domain model, async startAgent, runner self-contained, Agent.run(), ConcurrencyQueue, Agent.resume() |
1168
- | Phase 16 | #261, #262, #263, #264, #265 | Lifecycle events (retire permission-bridge), WorkspaceProvider seam, extract worktrees package, remove isolated, born-complete execution / dissolve runner |
1169
- | Phase 16 (abandoned) | #256 (superseded), #257 (parked), #258, #259 (not planned) | Agent collaborator architecture — replaced by the inversion approach above ([ADR-0002]) |
1170
- | Phase 17 | #381, #373, #374, #375, #376, #377, #378, #379, #380 | ConcurrencyLimiter, SubagentState, run-start encapsulation, run collaborators, events observer, widget decoupling, lifecycle test fixtures, UI/tools test fixtures, settings-loader extraction |
1171
- | Phase 17 (follow-on) | #412, #415 | Session-mock builder unification, worktrees settings-helper migration |
1172
- | Phase 18 | #420, #421, #422, #423, #424, #425, #426, #427 | Fold metrics onto record, migrate readers, delete activity tier, widget self-drives, drop widget from tool, reconcile event contract, consolidate test clones, UI-direction ADR |
1173
- | Phase 19 | #446, #447, #444, #445, #462, #463, #442, #441, #443 | ADR-0004 spike, settings command, background widget, native session nav slice, TUI renderer, file-snapshot source, dissolve /agents + viewer, remove definition mgmt, consolidate test clones |
1174
- | Phase 19 (follow-on) | #470 | README refresh for the removed /agents command surface |
1175
-
1176
- The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
1177
-
1178
- ## Relationship with upstream
1179
-
1180
- This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is a hard fork of [tintinweb/pi-subagents].
1181
- The decomposition diverges materially from upstream's direction.
1182
-
1183
- The three upstream PRs (#71, #72, #73) remain open.
1184
- If they land, upstream gains the peer-dep fix and the two RepOne patches.
1185
- This fork continues independently regardless.
1186
-
1187
- Upstream fixes and ideas are cherry-picked when they align with this fork's scope.
1188
- The upstream test suite is run periodically as a regression canary for the session assembly core.
1189
-
1190
- [earendil-works/pi#4207]: https://github.com/earendil-works/pi/issues/4207
1191
- [gotgenes/pi-packages]: https://github.com/gotgenes/pi-packages
1192
- [tintinweb/pi-subagents]: https://github.com/tintinweb/pi-subagents
1193
- [166]: https://github.com/gotgenes/pi-packages/issues/166
1194
- [167]: https://github.com/gotgenes/pi-packages/issues/167
1195
- [168]: https://github.com/gotgenes/pi-packages/issues/168
1196
- [169]: https://github.com/gotgenes/pi-packages/issues/169
1197
- [#205]: https://github.com/gotgenes/pi-packages/issues/205
1198
- [#206]: https://github.com/gotgenes/pi-packages/issues/206
1199
- [#207]: https://github.com/gotgenes/pi-packages/issues/207
1200
- [#208]: https://github.com/gotgenes/pi-packages/issues/208
1201
- [#214]: https://github.com/gotgenes/pi-packages/issues/214
1202
- [#215]: https://github.com/gotgenes/pi-packages/issues/215
1203
- [#216]: https://github.com/gotgenes/pi-packages/issues/216
1204
- [#217]: https://github.com/gotgenes/pi-packages/issues/217
1205
- [#218]: https://github.com/gotgenes/pi-packages/issues/218
1206
- [#219]: https://github.com/gotgenes/pi-packages/issues/219
1207
- [#227]: https://github.com/gotgenes/pi-packages/issues/227
1208
- [#228]: https://github.com/gotgenes/pi-packages/issues/228
1209
- [#229]: https://github.com/gotgenes/pi-packages/issues/229
1210
- [#230]: https://github.com/gotgenes/pi-packages/issues/230
1211
- [#231]: https://github.com/gotgenes/pi-packages/issues/231
1212
- [#232]: https://github.com/gotgenes/pi-packages/issues/232
1213
- [#261]: https://github.com/gotgenes/pi-packages/issues/261
1214
- [#262]: https://github.com/gotgenes/pi-packages/issues/262
1215
- [#263]: https://github.com/gotgenes/pi-packages/issues/263
1216
- [#264]: https://github.com/gotgenes/pi-packages/issues/264
1217
- [#265]: https://github.com/gotgenes/pi-packages/issues/265
1218
- [#277]: https://github.com/gotgenes/pi-packages/issues/277
1219
- [#373]: https://github.com/gotgenes/pi-packages/issues/373
1220
- [#374]: https://github.com/gotgenes/pi-packages/issues/374
1221
- [#375]: https://github.com/gotgenes/pi-packages/issues/375
1222
- [#376]: https://github.com/gotgenes/pi-packages/issues/376
1223
- [#377]: https://github.com/gotgenes/pi-packages/issues/377
1224
- [#378]: https://github.com/gotgenes/pi-packages/issues/378
1225
- [#379]: https://github.com/gotgenes/pi-packages/issues/379
1226
- [#380]: https://github.com/gotgenes/pi-packages/issues/380
1227
- [#381]: https://github.com/gotgenes/pi-packages/issues/381
1228
- [#412]: https://github.com/gotgenes/pi-packages/issues/412
1229
- [#415]: https://github.com/gotgenes/pi-packages/issues/415
1230
- [#420]: https://github.com/gotgenes/pi-packages/issues/420
1231
- [#421]: https://github.com/gotgenes/pi-packages/issues/421
1232
- [#422]: https://github.com/gotgenes/pi-packages/issues/422
1233
- [#423]: https://github.com/gotgenes/pi-packages/issues/423
1234
- [#424]: https://github.com/gotgenes/pi-packages/issues/424
1235
- [#425]: https://github.com/gotgenes/pi-packages/issues/425
1236
- [#426]: https://github.com/gotgenes/pi-packages/issues/426
1237
- [#427]: https://github.com/gotgenes/pi-packages/issues/427
1238
- [#441]: https://github.com/gotgenes/pi-packages/issues/441
1239
- [#442]: https://github.com/gotgenes/pi-packages/issues/442
1240
- [#443]: https://github.com/gotgenes/pi-packages/issues/443
1241
- [#444]: https://github.com/gotgenes/pi-packages/issues/444
1242
- [#445]: https://github.com/gotgenes/pi-packages/issues/445
1243
- [#446]: https://github.com/gotgenes/pi-packages/issues/446
1244
- [#447]: https://github.com/gotgenes/pi-packages/issues/447
1245
- [#462]: https://github.com/gotgenes/pi-packages/issues/462
1246
- [#463]: https://github.com/gotgenes/pi-packages/issues/463
1247
- [#470]: https://github.com/gotgenes/pi-packages/issues/470
1248
- [#535]: https://github.com/gotgenes/pi-packages/issues/535
1249
- [#536]: https://github.com/gotgenes/pi-packages/issues/536
1250
- [#537]: https://github.com/gotgenes/pi-packages/issues/537
1251
- [#538]: https://github.com/gotgenes/pi-packages/issues/538
1252
- [#539]: https://github.com/gotgenes/pi-packages/issues/539
1253
- [#540]: https://github.com/gotgenes/pi-packages/issues/540
1254
- [#541]: https://github.com/gotgenes/pi-packages/issues/541
1255
- [#542]: https://github.com/gotgenes/pi-packages/issues/542
1256
- [#543]: https://github.com/gotgenes/pi-packages/issues/543
1257
474
  [ADR-0002]: ../decisions/0002-extensions-on-a-minimal-core.md
475
+ [ADR-0003]: ../decisions/0003-publish-bundled-type-declarations.md
1258
476
  [ADR-0004]: ../decisions/0004-reconsider-ui-direction.md
477
+ [ADR-0005]: ../decisions/0005-ordered-lifecycle-interceptors.md