@nklisch/pi-enhanced 0.4.3 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/node_modules/@nklisch/pi-astral-pocket/README.md +127 -93
- package/node_modules/@nklisch/pi-astral-pocket/package.json +2 -2
- package/node_modules/@nklisch/pi-astral-pocket/src/distiller.ts +196 -183
- package/node_modules/@nklisch/pi-astral-pocket/src/guidance.ts +26 -62
- package/node_modules/@nklisch/pi-astral-pocket/src/index.ts +8 -5
- package/node_modules/@nklisch/pi-astral-pocket/src/sessions.ts +10 -3
- package/node_modules/@nklisch/pi-astral-pocket/src/store.ts +439 -321
- package/node_modules/@nklisch/pi-astral-pocket/src/tools.ts +172 -66
- package/node_modules/@nklisch/pi-clearance/docs/CONFIGURATION.md +3 -3
- package/node_modules/@nklisch/pi-clearance/docs/DEVELOPER_GUIDE.md +11 -12
- package/node_modules/@nklisch/pi-clearance/docs/PACK_AUTHORING.md +5 -5
- package/node_modules/@nklisch/pi-clearance/docs/PRINCIPLES.md +1 -2
- package/node_modules/@nklisch/pi-clearance/docs/REFERENCE_PATTERNS.md +13 -19
- package/node_modules/@nklisch/pi-clearance/docs/SPEC.md +4 -4
- package/node_modules/@nklisch/pi-clearance/docs/USER_GUIDE.md +2 -3
- package/node_modules/@nklisch/pi-clearance/docs/VISION.md +1 -1
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +2 -3
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +105 -886
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0002-extensions-on-a-minimal-core.md +44 -78
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0003-publish-bundled-type-declarations.md +28 -57
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +49 -261
- package/node_modules/@nklisch/pi-plugins/package.json +2 -2
- package/package.json +2 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/client-server-opportunities.md +0 -127
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-1-api-boundary.md +0 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-10-structural-decomposition.md +0 -141
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-11-closure-to-class.md +0 -100
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-12-complexity-test-fixtures.md +0 -55
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-13-remaining-smells.md +0 -88
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-14-strip-policy.md +0 -49
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-15-domain-model-evolution.md +0 -73
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-16-invert-dependencies.md +0 -144
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-17-core-consolidation.md +0 -214
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-18-reconsider-ui.md +0 -166
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-19-implement-ui-decisions.md +0 -282
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-2-remove-scheduling.md +0 -9
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +0 -11
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-4-implement-service.md +0 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-5-decompose-index.md +0 -42
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-7-encapsulation.md +0 -173
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-8-testability.md +0 -103
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/history/phase-9-observation-ctx.md +0 -122
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0001-deferred-patches.md +0 -80
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Phase 15: Domain model evolution
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that **owns its entire execution lifecycle**.
|
|
6
|
-
Before Phase 15, `AgentManager` orchestrated everything: calling the runner, handling session creation, wiring observers, and cleaning up worktrees — reaching into Agent 10+ times across `spawn()` and `startAgent()`.
|
|
7
|
-
After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
|
|
8
|
-
|
|
9
|
-
All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
|
|
10
|
-
|
|
11
|
-
## Key changes
|
|
12
|
-
|
|
13
|
-
- `AgentRecord` renamed to `Agent` with full behavioral surface.
|
|
14
|
-
- `Agent.run()` encapsulates the entire execution lifecycle: worktree setup, runner invocation, session-creation handling, observer wiring, worktree cleanup, and status transitions.
|
|
15
|
-
- `Agent.resume()` manages its own observer subscription lifecycle.
|
|
16
|
-
- `startAgent` deleted from `AgentManager` — replaced by `agent.run()`.
|
|
17
|
-
- `ConcurrencyQueue` extracted from `AgentManager` — scheduling is independently testable.
|
|
18
|
-
- `SpawnArgs` deleted — the queue stores agent IDs, not config objects.
|
|
19
|
-
- `onSessionCreated` callback replaced by `AgentLifecycleObserver` passed at construction.
|
|
20
|
-
- `exec` and `registry` relay-only dependencies moved from `AgentManager` to `ConcreteAgentRunner`.
|
|
21
|
-
- `AgentManagerOptions` shrunk from 7 to 5 fields.
|
|
22
|
-
|
|
23
|
-
## Steps
|
|
24
|
-
|
|
25
|
-
### Step 1: Evolve AgentRecord into Agent with behavior — [#227]
|
|
26
|
-
|
|
27
|
-
Renamed `AgentRecord` → `Agent`.
|
|
28
|
-
Moved per-agent behavior from `AgentManager` into the agent: `abort()`, `queueSteer()` / `flushPendingSteers()`, `setupWorktree()`.
|
|
29
|
-
|
|
30
|
-
### Step 2: Convert startAgent to async/await — [#228]
|
|
31
|
-
|
|
32
|
-
Converted `startAgent` to `async` with `try/catch` and dissolved `RunHandle` into `Agent` methods.
|
|
33
|
-
Agent gained run lifecycle methods: `completeRun`, `failRun`, `wireSignal`, `attachObserver`, `releaseListeners`.
|
|
34
|
-
|
|
35
|
-
### Step 3: Push exec/registry relay deps to runner construction — [#231]
|
|
36
|
-
|
|
37
|
-
`exec` and `registry` moved from `AgentManager` to `ConcreteAgentRunner` via `RunnerDeps`.
|
|
38
|
-
`RunContext` shrunk from 4 to 2 per-call fields.
|
|
39
|
-
|
|
40
|
-
### Step 4: Agent born complete — Agent.run() absorbs startAgent — [#229]
|
|
41
|
-
|
|
42
|
-
Agent receives `runner`, `worktrees`, and a lifecycle observer at construction.
|
|
43
|
-
`Agent.run()` encapsulates the entire execution lifecycle.
|
|
44
|
-
`startAgent`, `SpawnArgs`, `onSessionCreated` callback deleted.
|
|
45
|
-
|
|
46
|
-
### Step 5: Extract ConcurrencyQueue from AgentManager — [#230]
|
|
47
|
-
|
|
48
|
-
Extracted `queue[]`, `runningBackground`, `_getMaxConcurrent`, `drainQueue()`, `finalizeBackgroundRun()` into `ConcurrencyQueue`.
|
|
49
|
-
`AgentManager` lost 3 fields and 3 methods (~40 lines).
|
|
50
|
-
|
|
51
|
-
### Step 6: Agent.resume() with internal observer lifecycle — [#232]
|
|
52
|
-
|
|
53
|
-
`Agent.resume(prompt, signal)` manages its own observer subscription lifecycle.
|
|
54
|
-
`AgentManager.resume()` became a one-liner delegation.
|
|
55
|
-
|
|
56
|
-
## Findings summary
|
|
57
|
-
|
|
58
|
-
| Finding | Category | Status |
|
|
59
|
-
| ------------------------------------------------------------------ | ------------ | --------------------- |
|
|
60
|
-
| `AgentRecord` anemic — no behavior, manager reaches in 37× | B: Oversized | ✅ Resolved |
|
|
61
|
-
| Agent cannot run itself — manager orchestrates 10 external touches | C: Coupling | ✅ Resolved |
|
|
62
|
-
| Scheduling tangled into `AgentManager` (3 fields, 3 methods) | A: Coupling | ✅ Resolved |
|
|
63
|
-
| `startAgent` uses `.then()`/`.catch()` instead of async/await | C: Callbacks | ✅ Resolved |
|
|
64
|
-
| `onSessionCreated` callback flows through 3 layers | C: Callbacks | ✅ Subsumed by Step 4 |
|
|
65
|
-
| `resume()` duplicates observer subscribe/unsubscribe pattern | A: Redundant | ✅ Resolved |
|
|
66
|
-
| `exec`/`registry` relay-only deps on `AgentManager` | C: Coupling | ✅ Resolved |
|
|
67
|
-
|
|
68
|
-
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
69
|
-
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
70
|
-
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
71
|
-
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
72
|
-
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
73
|
-
[#232]: https://github.com/gotgenes/pi-packages/issues/232
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# Phase 16: Invert dependencies — extensions on a minimal core
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
Phase 16 reclaimed its original intent — invert the core's outbound dependencies — and extended it: worktree isolation joined permissions as an *extension* on a minimal core, leaving pi-subagents a pure child-session orchestrator.
|
|
6
|
-
The decision and the full reasoning chain are recorded in [ADR-0002]; the two-surface extension model is described under [Target architecture](../architecture.md#target-architecture).
|
|
7
|
-
|
|
8
|
-
All five steps are closed: [#261], [#262], [#263], [#264], [#265].
|
|
9
|
-
|
|
10
|
-
## Two extension surfaces
|
|
11
|
-
|
|
12
|
-
Extensions attach through exactly two surfaces, distinguished by the direction of information flow.
|
|
13
|
-
|
|
14
|
-
1. **Lifecycle events (observational) — unlimited.**
|
|
15
|
-
The core emits awaited, ordered events for the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`).
|
|
16
|
-
Any number of extensions subscribe; handlers return nothing.
|
|
17
|
-
Reactive concerns live here: permission detection, telemetry, UI, notifications.
|
|
18
|
-
Adding a reactive concern never modifies the core.
|
|
19
|
-
2. **Provider seams (generative) — rationed.**
|
|
20
|
-
The rare concern that must *inject* a value the core consumes synchronously registers a provider the core consults.
|
|
21
|
-
Today there is exactly one: the **workspace provider** (returns the child's working directory plus bracketed setup/teardown).
|
|
22
|
-
A provider seam is the only place the core is "open," so the list is kept as small as possible.
|
|
23
|
-
|
|
24
|
-
The discriminator when deciding how a concern attaches:
|
|
25
|
-
|
|
26
|
-
- It only needs to **know** what happened → subscribe to a lifecycle event (observational, unlimited).
|
|
27
|
-
- It must **return a value the core consumes** → register a provider (generative, rationed).
|
|
28
|
-
|
|
29
|
-
The governing rule — **no vacant hooks**: the architecture must *admit* a seam without *shipping* it until a concrete consumer exists.
|
|
30
|
-
A provider seam with no consumer is a speculative abstraction that taxes every reader and that `fallow` flags as dead.
|
|
31
|
-
Latent extensibility is the deliverable; a vacant hook is not.
|
|
32
|
-
|
|
33
|
-
## Abandoned exploration: agent collaborator architecture
|
|
34
|
-
|
|
35
|
-
An earlier Phase 16 plan ("agent collaborator architecture") proposed giving `Agent` three collaborators — a session factory, a `WorktreeIsolation`, and a lifecycle observer — and dissolving the runner.
|
|
36
|
-
That framing was abandoned.
|
|
37
|
-
Pulling on a single late-bound `create(cwd?)` parameter on the planned `ChildSessionFactory` exposed deeper problems:
|
|
38
|
-
|
|
39
|
-
- `WorktreeIsolation.setup()` is a two-phase `construct-then-setup()` that violates "Construct complete" (principle 8) — the worktree is only *ready* at dequeue.
|
|
40
|
-
- The worktree and the child session share one lifespan, so they are one run-scoped resource, not sibling collaborators that `Agent` must sequence; the `cwd` parameter only existed because the worktree was split out and `Agent` relayed its output back in.
|
|
41
|
-
- Worktrees are not intrinsic to subagents — they are one *workspace strategy* and belong outside the core, exactly as Phase 14 evicted tool/extension policy.
|
|
42
|
-
|
|
43
|
-
Issue #256 (`WorktreeIsolation` as a collaborator) shipped under the abandoned plan and was superseded by #263; issue #257 (`ChildSessionFactory` extraction) was parked.
|
|
44
|
-
Issues #258 (Agent owns session lifecycle via factory) and #259 (dissolve runner concept) belonged to the same abandoned plan — both depended on #256/#257 and were closed as not planned; their structural goals were recovered by Step 5 (#265) via a cleaner route.
|
|
45
|
-
The structural win the collaborator plan chased — a born-complete child execution and the dissolution of the runner — is recovered once the workspace seam exists (Step 5).
|
|
46
|
-
|
|
47
|
-
## Steps
|
|
48
|
-
|
|
49
|
-
### Step 1: Child-execution lifecycle events; retire permission-bridge — [#261]
|
|
50
|
-
|
|
51
|
-
Emit ordered child-execution events (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) carrying child identity (session directory, agent name, parent session id).
|
|
52
|
-
Migrated `@gotgenes/pi-permission-system` to subscribe to `session-created`/`disposed` for registration instead of being looked up by the core; deleted `permission-bridge.ts`.
|
|
53
|
-
|
|
54
|
-
- Cross-package: pi-subagents (emit + remove bridge) and pi-permission-system (subscribe).
|
|
55
|
-
- Investigation (resolved): `pi.events` is a Node `EventEmitter`, so `emit()` dispatches listeners synchronously on the same call stack — a synchronous subscriber completes before `emit()` returns.
|
|
56
|
-
Emitting `session-created` immediately before `bindExtensions()` therefore guarantees the registry entry lands pre-bind, with no new SDK hook.
|
|
57
|
-
The synchronous-handler constraint is encoded as a real-bus test in pi-permission-system.
|
|
58
|
-
- Outcome: the core stops reaching out to a named consumer; permission detection rides events.
|
|
59
|
-
- Deferred: removing the now-caller-less `registerSubagentSession`/`unregisterSubagentSession` from `PermissionsService` → #267; registry-detected resume ("executing now" → "exists" semantics) → #265.
|
|
60
|
-
|
|
61
|
-
### Step 2: Define the `WorkspaceProvider` seam — [#262]
|
|
62
|
-
|
|
63
|
-
Added the `WorkspaceProvider` / `Workspace` interfaces (`src/lifecycle/workspace.ts`) and `SubagentsService.registerWorkspaceProvider` (single provider, throws on duplicate, returns an unregister disposer).
|
|
64
|
-
All five workspace types are named-re-exported from `service.ts`: `WorkspaceProvider`, `Workspace`, `WorkspacePrepareContext`, `WorkspaceDisposeOutcome`, and `WorkspaceDisposeResult` (added in #272).
|
|
65
|
-
At run-start `Agent.run()` consults the registered provider for the child's cwd and a disposal handle; with no provider the child runs in the parent's cwd.
|
|
66
|
-
On completion the core calls `Workspace.dispose({ status, description })` and appends the returned `resultAddendum` verbatim — the provider owns the wording.
|
|
67
|
-
|
|
68
|
-
- The seam is additive and non-breaking.
|
|
69
|
-
- Landed alongside its first consumer (Step 3) to avoid a vacant hook — the "no vacant hooks" rule.
|
|
70
|
-
- Outcome: a single generative seam; the core no longer knows what an "isolation strategy" is.
|
|
71
|
-
|
|
72
|
-
### Step 3: Extract worktrees to `@gotgenes/pi-subagents-worktrees` — [#263]
|
|
73
|
-
|
|
74
|
-
New package implementing `WorkspaceProvider`: prepares a git worktree at run-start (born complete), tears it down after (saving the branch), and owns the "changes saved to branch" result.
|
|
75
|
-
Worktree isolation is opt-in per agent type via the package's own `worktreeAgents` config; creation failure for an opted-in agent throws (strict, no silent fallback).
|
|
76
|
-
Removed `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, and the `isolation: "worktree"` mode from the core; dropped `isolation` from the spawn API and `SubagentsService`, and `worktreeResult` from `SubagentRecord`.
|
|
77
|
-
|
|
78
|
-
- Supersedes #256.
|
|
79
|
-
New package registered in `release-please-config.json` and `.pi/settings.json` (after pi-subagents); consumes the published `@gotgenes/pi-subagents` from the registry (`linkWorkspacePackages: false`).
|
|
80
|
-
- Outcome: git leaves the core; worktree users install one package, everyone else pays nothing.
|
|
81
|
-
|
|
82
|
-
### Step 4: Remove `isolated` / `extensions: false` / `noSkills` — [#264]
|
|
83
|
-
|
|
84
|
-
Children always load the parent's extensions and skills; the recursion guard is now unconditional.
|
|
85
|
-
Deny-at-use (the in-child permission layer) covers tool restriction; prevent-load is left as a latent provider seam (not shipped).
|
|
86
|
-
The `skills` curation axis collapsed symmetrically with `extensions`: `AgentConfig.skills`, the skill-preload path (`skill-loader.ts`, `safe-fs.ts`, `preloadSkills`, `PromptExtras`), `SessionConfig.{extensions,noSkills,extras}`, and the `isolated:` / `extensions:` / `skills:` custom-agent frontmatter keys are all gone.
|
|
87
|
-
|
|
88
|
-
- Depended on: Step 1 (deny-at-use over events).
|
|
89
|
-
- Outcome: the `isolated`/`extensions`/`noSkills`/`skills` axis is gone; the guard is unconditional.
|
|
90
|
-
|
|
91
|
-
### Step 5: Born-complete child execution; dissolve the runner — [#265]
|
|
92
|
-
|
|
93
|
-
`createSubagentSession()` is an assembly factory that returns a born-complete `SubagentSession` (session created, extensions bound, recursion guard applied).
|
|
94
|
-
`SubagentSession` owns turn driving (`runTurnLoop`/`resumeTurnLoop`), steering, and disposal.
|
|
95
|
-
`Agent.run()` is coordination, not assembly; `runAgent` / `resumeAgent` / `ConcreteAgentRunner` / `AgentRunner` / `RunOptions` / `RunResult` / `ExecutionState` dissolved.
|
|
96
|
-
`getAgentConversation()` relocated to `session/conversation.ts`; `normalizeMaxTurns()` to `lifecycle/turn-limits.ts`.
|
|
97
|
-
`disposed` now fires at true session disposal (cleanup), so resume executions are registry-detected (closing the gap deferred from #261).
|
|
98
|
-
|
|
99
|
-
- Depends on: Steps 2–4.
|
|
100
|
-
- Outcome: the "runner" concept is gone; `Agent.run()` is coordination, not assembly — the structural goal of the abandoned collaborator plan, reached cleanly.
|
|
101
|
-
|
|
102
|
-
## Step dependency diagram
|
|
103
|
-
|
|
104
|
-
```mermaid
|
|
105
|
-
flowchart LR
|
|
106
|
-
S1["Step 1<br/>Lifecycle events<br/>(retire bridge)"]
|
|
107
|
-
S2["Step 2<br/>WorkspaceProvider seam"]
|
|
108
|
-
S3["Step 3<br/>Extract worktrees pkg"]
|
|
109
|
-
S4["Step 4<br/>Remove isolated"]
|
|
110
|
-
S5["Step 5<br/>Born-complete execution"]
|
|
111
|
-
|
|
112
|
-
S2 --> S3
|
|
113
|
-
S1 --> S4
|
|
114
|
-
S2 --> S5
|
|
115
|
-
S3 --> S5
|
|
116
|
-
S4 --> S5
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## Tracks
|
|
120
|
-
|
|
121
|
-
1. **Track A — Inversion seams** (Steps 1, 2): lifecycle events and the workspace seam.
|
|
122
|
-
Independent of each other — proceeded in parallel.
|
|
123
|
-
2. **Track B — Eviction** (Steps 3, 4): worktrees and `isolated` leave the core.
|
|
124
|
-
Step 3 depends on Step 2.
|
|
125
|
-
3. **Track C — Consolidation** (Step 5): dissolve the runner around the new seam.
|
|
126
|
-
Depends on Tracks A and B.
|
|
127
|
-
|
|
128
|
-
## Composition model
|
|
129
|
-
|
|
130
|
-
In the post-Phase-16 state, pi-subagents publishes events and a provider seam; other packages hook in:
|
|
131
|
-
|
|
132
|
-
- **pi-permission-system** (observational) subscribes to child-session lifecycle events, detects subagent execution context in the child, and gates tool calls at runtime.
|
|
133
|
-
- **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.
|
|
134
|
-
- **pi-subagents-ui** (future, Phase 17) subscribes to the service API, renders the widget, conversation viewer, and `/agents` menu.
|
|
135
|
-
- **Any future extension** (OTel, auditing, cost tracking) subscribes to the same events without pi-subagents knowing.
|
|
136
|
-
|
|
137
|
-
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.
|
|
138
|
-
|
|
139
|
-
[#261]: https://github.com/gotgenes/pi-packages/issues/261
|
|
140
|
-
[#262]: https://github.com/gotgenes/pi-packages/issues/262
|
|
141
|
-
[#263]: https://github.com/gotgenes/pi-packages/issues/263
|
|
142
|
-
[#264]: https://github.com/gotgenes/pi-packages/issues/264
|
|
143
|
-
[#265]: https://github.com/gotgenes/pi-packages/issues/265
|
|
144
|
-
[ADR-0002]: ../../decisions/0002-extensions-on-a-minimal-core.md
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# Phase 17: Core consolidation
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
|
-
Phase 17 consolidated the core's remaining structural debt before the UI reconsideration (Phase 18).
|
|
6
|
-
The findings came from the standard discovery pass — fallow suite, entry-point trace, design-review checklist, and test-constructibility audit — run after Phase 16 landed.
|
|
7
|
-
|
|
8
|
-
All nine steps are closed: [#381], [#373], [#374], [#375], [#376], [#377], [#378], [#379], [#380].
|
|
9
|
-
[#412] unified the overlapping session-mock builders identified during Step 7.
|
|
10
|
-
[#415] migrated `pi-subagents-worktrees` to the shared settings helper after the Step 9 published release.
|
|
11
|
-
|
|
12
|
-
## Context
|
|
13
|
-
|
|
14
|
-
Phase 17 is the consolidation slice of the [first-principles refinement](../architecture.md#first-principles-refinement-and-the-deeper-target), not the full domain split.
|
|
15
|
-
It lands the first cut of the lifecycle-state domain (Step 2's `SubagentState`) plus the wiring, queue, and duplication cleanups.
|
|
16
|
-
The fuller four-domain split — metrics as a projection, result delivery as its own domain, the hook/broadcast reclassification, and the push/pull (DIP) inversion — is recorded in the refinement and sequenced into later phases.
|
|
17
|
-
|
|
18
|
-
## Findings summary
|
|
19
|
-
|
|
20
|
-
Health metrics at the start of Phase 17 (fallow, package-wide including tests):
|
|
21
|
-
|
|
22
|
-
| Metric | Phase 16 baseline | Phase 17 start |
|
|
23
|
-
| -------------------------- | ------------------------------ | ------------------------------------------------------------------ |
|
|
24
|
-
| Health score | 78/100 (B) | 78/100 (B) |
|
|
25
|
-
| Source LOC | 7,778 (57 files) | 8,356 (61 files, landed Phase 17 Step 5) |
|
|
26
|
-
| Dead code | 0 files, 0 exports | 0 files, 0 exports |
|
|
27
|
-
| Maintainability index | 90.8 (good) | 90.8 (good) |
|
|
28
|
-
| Avg / P90 cyclomatic | 1.4 / 2 | 1.4 / 2 |
|
|
29
|
-
| Production duplication | 11 lines (1 internal group) | 11 lines (1 internal group; cross-package pair resolved in Step 9) |
|
|
30
|
-
| Test duplication | 42 groups, 661 lines | 44 groups, ~750 lines |
|
|
31
|
-
| Fallow refactoring targets | 0 | 0 |
|
|
32
|
-
| Top churn hotspot | `index.ts` 65.0 ▲ accelerating | `index.ts` 31.3 ▼ cooling |
|
|
33
|
-
|
|
34
|
-
The syntactic metrics are healthy and stable — the remaining debt is structural, mostly invisible to fallow, and concentrated in three places:
|
|
35
|
-
|
|
36
|
-
1. **`Subagent` construction duality.**
|
|
37
|
-
`SubagentInit` carries ~20 fields, nearly all optional with "required for run(), optional for tests" semantics, and `run()` compensates with runtime throws ("not configured for execution").
|
|
38
|
-
This violates principle 8 (construct complete): the class is simultaneously a passive record (tests build display-only snapshots) and an executor (production wires factory, observer, run config, workspace provider).
|
|
39
|
-
The symptoms are in the tests: external writes `record.promise = …` (manager, queue callback, four test files) and `record.notification = new NotificationState(…)` (seven test sites) are output-argument smells on fields the object should own.
|
|
40
|
-
This duality is the two most visible of four domains fused into `Subagent`; Phase 17 resolves it (Step 2) and defers the remaining split (metrics, result delivery) to a later phase per the [first-principles refinement](../architecture.md#first-principles-refinement-and-the-deeper-target).
|
|
41
|
-
2. **Wiring debt in `index.ts`.**
|
|
42
|
-
Two forward references (settings → queue, queue → manager) are replicated with an `eslint-disable prefer-const` dance in `test/lifecycle/subagent-manager.test.ts`; the queue's start callback (`record.promise = record.run()` after a status check) is duplicated verbatim between `index.ts` and the test helper.
|
|
43
|
-
A ~70-line inline `SubagentManagerObserver` literal mixes three concerns (event emission, `appendEntry` persistence, notification dispatch).
|
|
44
|
-
`runtime.widget` is assigned post-construction behind five relay-only delegation methods on `SubagentRuntime`.
|
|
45
|
-
3. **Duplication.**
|
|
46
|
-
A 23-line cross-package production clone (`settings.ts:198-211` ↔ `pi-subagents-worktrees/src/config.ts:51-73`: the layered global/project settings-file loader) and 44 test clone groups (~750 lines), with clone families concentrated in `test/lifecycle/` and `test/ui/`.
|
|
47
|
-
|
|
48
|
-
Deferred findings (scored below the priority cut, tracked here rather than as steps): the `resolveModel` error-as-string union return (callers branch on `typeof resolved === "string"`), the file-top SDK `eslint-disable` headers in 14 files (re-audit when the Pi SDK exports improve), missing unit tests for `observation/renderer.ts` (the top CRAP-risk file), and the 11-line internal clone in `ui/agent-config-editor.ts` (folds into the Phase 18 UI extraction).
|
|
49
|
-
|
|
50
|
-
## Steps
|
|
51
|
-
|
|
52
|
-
Priority = Impact × (6 − Risk).
|
|
53
|
-
|
|
54
|
-
| Step | Title | Category | Impact | Risk | Priority |
|
|
55
|
-
| ---- | ------------------------------------------------------------------------------------ | -------- | ------ | ---- | -------- |
|
|
56
|
-
| 1 | Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter | A/C | 4 | 2 | 16 |
|
|
57
|
-
| 2 | Extract `SubagentState`; make `Subagent` execution deps mandatory | B/D | 4 | 3 | 12 |
|
|
58
|
-
| 3 | Encapsulate run start and notification attachment on Subagent | C | 3 | 2 | 12 |
|
|
59
|
-
| 4 | Extract run-listener and workspace-bracket collaborators from Subagent | B/C | 3 | 2 | 12 |
|
|
60
|
-
| 5 | Extract the manager observer from index.ts into a class | B/E | 3 | 2 | 12 |
|
|
61
|
-
| 6 | Split widget delegation out of SubagentRuntime | C | 3 | 3 | 9 |
|
|
62
|
-
| 7 | Consolidate lifecycle test fixtures | D | 3 | 1 | 15 |
|
|
63
|
-
| 8 | Consolidate UI and tools test fixtures | D | 2 | 1 | 10 |
|
|
64
|
-
| 9 | Resolve the cross-package settings-loader duplication | A | 2 | 2 | 8 |
|
|
65
|
-
|
|
66
|
-
### Step 1 — Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter ([#381])
|
|
67
|
-
|
|
68
|
-
- Targets: `src/lifecycle/concurrency-queue.ts` (→ `concurrency-limiter.ts`), `src/lifecycle/subagent-manager.ts`, `src/index.ts`, `test/lifecycle/concurrency-queue.test.ts`, `test/lifecycle/subagent-manager.test.ts`.
|
|
69
|
-
- Smell: Category C (forward references: the queue's ID-registry design forces a start callback that reaches back into the manager, duplicated between `index.ts` and the test helper) and Category A (dual counting: the queue's `running` counter is fed by `markStarted`/`markFinished` relays in the manager's observer, mirroring state the agents already carry).
|
|
70
|
-
- Change: replace the ID-registry queue with a `ConcurrencyLimiter` that schedules thunks FIFO against a dynamic `getLimit()` — the injected limiter knows nothing about agents, IDs, or the manager.
|
|
71
|
-
Spawn gates background runs with `limiter.schedule(() => record.start())` — `start()` owns the abort-while-queued status guard and stores the promise internally; foreground and `bypassQueue` runs invoke `record.start()` directly.
|
|
72
|
-
The settings `onMaxConcurrentChanged` hook wires to `limiter.recheck()` in `index.ts`; `dispose()` calls `limiter.clear()` to drop pending thunks.
|
|
73
|
-
- Outcome: dependency direction is strictly manager → limiter (no callback back-edge; the `prefer-const` eslint-disable in the test helper is deleted); the observer's two queue relays are gone; every spawned agent has a `promise` at spawn, collapsing `waitForAll`'s `while (true)` drain loop and its eslint-disable.
|
|
74
|
-
|
|
75
|
-
### Step 2 — Extract `SubagentState`; make `Subagent` execution deps mandatory ([#373])
|
|
76
|
-
|
|
77
|
-
- Targets: `src/lifecycle/subagent.ts` (state fields, transition/accumulation methods, constructor, `run()` guards), `src/lifecycle/subagent-manager.ts` (`spawn`), `test/helpers/make-subagent.ts`, `test/lifecycle/subagent.test.ts`, `test/observation/record-observer.test.ts`.
|
|
78
|
-
- Smell: Category B (god interface — ~20 fields) and Category D (constructibility: "optional for tests" fields with compensating runtime throws).
|
|
79
|
-
The record/executor duality is the two most visible of the four conflated domains (see the first-principles refinement in [architecture.md](../architecture.md#first-principles-refinement-and-the-deeper-target)).
|
|
80
|
-
- Change: extract the passive-record state — status, result, error, timestamps, and the stats (toolUses, lifetimeUsage, compactionCount) — into a `SubagentState` value object that owns the transition and accumulation methods.
|
|
81
|
-
`Subagent` holds one privately; its existing getters and `markX`/`incrementX`/`addUsage` methods become one-line delegations, so the ~40 read sites and the mutation callers are unchanged.
|
|
82
|
-
This is not reach-through: `SubagentState` is a private owned value, not a foreign collaborator (contrast [#277], which removed reach-through to the raw SDK session).
|
|
83
|
-
With the readable state extracted, the remaining execution inputs (snapshot, prompt, model, maxTurns, thinkingLevel, parentSession, signal, createSubagentSession, observer, getRunConfig, getWorkspaceProvider, baseCwd) collapse into a single **mandatory** `SubagentExecution` collaborator: production always supplies it (the one `spawn()` site), the passive-record construction moves entirely into `make-subagent.ts`, and `run()`'s two "not configured" throws vanish by construction.
|
|
84
|
-
- Outcome: state-machine and observer tests target `SubagentState` directly (no stub execution); `Subagent` is construct-complete with no optional execution fields and no runtime throws (grep-verifiable: no "not configured for execution" in `subagent.ts`); the record-vs-executor duality is resolved, not type-encoded.
|
|
85
|
-
- Scope boundary: stats stay on `SubagentState` for now.
|
|
86
|
-
Hoisting **metrics** into a projection over the child session's event stream and extracting **result delivery** (`notification`/`resultConsumed`) into its own domain are the remaining two of the four domains, deferred to a later phase per the refinement.
|
|
87
|
-
- Landed: `SubagentState` (`src/lifecycle/subagent-state.ts`) owns status/result/error/timestamps/stats and the transition/accumulation methods; `Subagent` delegates getters and `markX`/`incrementX`/`addUsage` to it.
|
|
88
|
-
`subscribeSubagentObserver` targets `SubagentState`, so observer and state-machine tests no longer stub execution.
|
|
89
|
-
`SubagentExecution` is a mandatory constructor collaborator (production wires it in the single `spawn()` site; passive records build via `make-subagent.ts`), and the two `run()` throws are gone.
|
|
90
|
-
|
|
91
|
-
### Step 3 — Encapsulate run start and notification attachment on Subagent ([#374])
|
|
92
|
-
|
|
93
|
-
- Targets: `src/lifecycle/subagent.ts`, `src/lifecycle/subagent-manager.ts`, `test/tools/get-result-tool.test.ts`, `test/lifecycle/subagent-manager.test.ts`, `test/service/service-adapter.test.ts`, `test/observation/notification.test.ts`, `test/helpers/make-subagent.test.ts`, `test/lifecycle/subagent.test.ts`.
|
|
94
|
-
- Smell: Category C — output arguments: external writes to `record.promise` (2 production sites in `subagent-manager.ts`, 4 test sites) and `record.notification` (7 test sites; the production path was resolved in Step 2 — the constructor creates `notification` from `execution.parentSession?.toolCallId`, so Step 3's remaining work is making the field read-only and updating tests to supply it via `parentSession`).
|
|
95
|
-
- Change: add `Subagent.start()` that runs and stores its own promise (plus an awaitable accessor for `spawnAndWait`/`waitForAll`); make `promise` and `notification` externally read-only (private `_promise`/`_notification` fields backed by public getters); the abort-while-queued status guard folds into `start()`, removing the inline check from the limiter callback; tests use `createTestSubagent({ toolCallId })` or spawn with `parentSession.toolCallId` instead of post-construction assignment.
|
|
96
|
-
- Outcome: zero external writes to `Subagent` fields outside its own methods (grep-verifiable: `\.promise =` and `\.notification =` appear only inside `subagent.ts`); 6 new unit tests for `start()` behaviour; test count +6 (975 → 981).
|
|
97
|
-
- Landed: `Subagent.start()` (immediate path) and `Subagent.scheduleVia(schedule)` (queued path) own the promise and the shared `guardedRun()` status guard; `SubagentManager.spawn()` calls one or the other; `TestSubagentOptions.toolCallId` wires notification state via the constructor path.
|
|
98
|
-
- Correction (post-merge): the first cut used `void this.limiter.schedule(() => record.start())`, which left a queued agent's `promise` unset until its slot opened — silently regressing Step 1's "every spawned agent has a `promise` at spawn" invariant.
|
|
99
|
-
Fixed by inverting control: `scheduleVia` captures the limiter promise eagerly inside the agent (no external `.promise =` write), restoring the invariant.
|
|
100
|
-
Lesson: a step's acceptance criteria must include the cross-step invariants it could regress, not only its own grep-verifiable outcome.
|
|
101
|
-
|
|
102
|
-
### Step 4 — Extract run-listener and workspace-bracket collaborators from Subagent ([#375])
|
|
103
|
-
|
|
104
|
-
- Targets: `src/lifecycle/subagent.ts` (455 LOC after Step 2 extracted SubagentState — still the largest source file).
|
|
105
|
-
- Smell: Category B (oversized class; per-run listener fields declared mid-class) and Category C (state owns its mutations: workspace dispose logic appears in `run()`'s catch, `completeRun`, and `failRun`).
|
|
106
|
-
- Change: extract a `RunListeners` object owning the observer-unsubscribe and signal-detach handles (`wireSignal`/`attachObserver`/`release`), and a `WorkspaceBracket` collaborator owning prepare/dispose-with-addendum, centralising the dispose logic.
|
|
107
|
-
- Outcome: `subagent.ts` ≤ 450 LOC; workspace disposal logic in exactly one place; listener handles no longer raw nullable fields.
|
|
108
|
-
- Landed: `RunListeners` (`src/lifecycle/run-listeners.ts`) owns the signal-detach and observer-unsub handles with a single `release()` call; `WorkspaceBracket` (`src/lifecycle/workspace-bracket.ts`) owns prepare-at-run-start and dispose-with-addendum — `completeRun` and `failRun` call `workspaceBracket.dispose(outcome)` and receive the addendum string (or `""`) without reaching through to the workspace object directly.
|
|
109
|
-
`Subagent.wireSignal`, `attachObserver`, and `releaseListeners` are removed.
|
|
110
|
-
`subagent.ts`: 488 → 448 LOC.
|
|
111
|
-
Test count: 982 → 994 (+12: 7 RunListeners + 13 WorkspaceBracket − 8 redundant Subagent listener tests).
|
|
112
|
-
|
|
113
|
-
### Step 5 — Extract the manager observer from index.ts into a class ([#376])
|
|
114
|
-
|
|
115
|
-
- Targets: `src/index.ts` (inline `SubagentManagerObserver` literal, ~70 lines), new module under `src/observation/`.
|
|
116
|
-
- Smell: Category B/E — `index.ts` is the dominant churn hotspot (31.3, 91 commits); the literal mixes event emission, record persistence (`appendEntry`), and notification dispatch; principle 9 (state and behavior belong in classes, not closure-captured literals).
|
|
117
|
-
- Change: extract a class (e.g. `SubagentEventsObserver`) constructed with narrow deps (`emit`, `appendEntry`, the `NotificationSystem`).
|
|
118
|
-
- Outcome: `index.ts` < 170 lines; the observer's three concerns unit-tested directly without booting the extension.
|
|
119
|
-
- Landed: `src/observation/subagent-events-observer.ts` (new, 97 LOC); `index.ts` 226 → 177 lines; 60 → 61 source files; 994 → 1009 tests (+15 covering all four observer methods).
|
|
120
|
-
|
|
121
|
-
### Step 6 — Split widget delegation out of SubagentRuntime ([#377])
|
|
122
|
-
|
|
123
|
-
- Targets: `src/runtime.ts`, `src/tools/agent-tool.ts` (`AgentToolRuntime`), `src/handlers/tool-start.ts` (`ToolStartRuntime`), `src/observation/notification.ts` (`NotificationManager` constructor), `src/ui/agent-widget.ts`, `src/index.ts`.
|
|
124
|
-
- Smell: Category C — relay-only dependency (five delegation methods that only forward to `widget`) and a post-construction `runtime.widget =` write violating principle 8.
|
|
125
|
-
- Constraint discovered in planning: "pass the handle directly to the consumers" is infeasible for `NotificationManager`, which is a _transitive dependency_ of the widget (`NotificationManager → widget → manager → observer → NotificationManager`).
|
|
126
|
-
The `runtime.widget` lazy field exists to break exactly this cycle; removing it forces the one late seam to move, and the operator's principles (no setters, instantiate ready-to-work) rule out relocating it to a setter or forward-ref `let`.
|
|
127
|
-
- Change (tidy-first): first dissolve the cycle by giving `AgentWidget.update()` self-seeding of `finishedTurnAge` for finished agents it observes via `listAgents()`, then drop the `markFinished`/`updateWidget` callbacks from `NotificationManager` (it keeps `agentActivity.delete` + nudge scheduling).
|
|
128
|
-
With the cycle gone, the widget is constructed as a `const` after the manager and injected directly into `AgentTool` and `ToolStartHandler`; the `widget` field, the five relay methods, and the post-construction write delete cleanly, and `AgentToolRuntime` narrows to its context-query slice.
|
|
129
|
-
- Outcome: `SubagentRuntime` has zero widget knowledge; no post-construction field writes in `index.ts`; the construction graph is a cycle-free DAG (`notifications → observer → manager → widget → {AgentTool, ToolStartHandler}`).
|
|
130
|
-
- Landed: `AgentWidget.seedFinishedAgents` owns completion detection; `NotificationManager` has no widget dependency (2-arg constructor); `AgentToolWidget` (in `agent-tool.ts`) and `ToolStartWidget` (in `tool-start.ts`) are narrow per-consumer interfaces the real widget satisfies structurally; tool fixtures stub a `widget` separate from the narrowed `runtime` mock.
|
|
131
|
-
Behavior-preserving: the widget timer runs through every background completion, so self-seeding lands ≤80ms later within the same turn (linger is turn-based).
|
|
132
|
-
Test count: 1009 → 1005 (+3 widget self-seed tests, −7 removed relay/field tests).
|
|
133
|
-
|
|
134
|
-
### Step 7 — Consolidate lifecycle test fixtures ([#378])
|
|
135
|
-
|
|
136
|
-
- Targets: `test/lifecycle/subagent-manager.test.ts`, `test/lifecycle/subagent-session.test.ts`, `test/lifecycle/create-subagent-session.test.ts`, `test/lifecycle/create-subagent-session-extension-tools.test.ts` (deleted), `test/helpers/subagent-session-io.ts`.
|
|
137
|
-
- Smell: Category D — at planning time fallow reported four clone families across the lifecycle tests (the issue's "five" predated Steps 1–6, which renamed the queue and dissolved the `subagent.test.ts`/`concurrency-limiter.test.ts` families).
|
|
138
|
-
- Change: extract the repeated spawn/run/factory arrangements into shared helpers, migrating incrementally (lift-and-shift, never a single-step rewrite of a large test file).
|
|
139
|
-
- Outcome: package test duplication below 600 lines; lifecycle clone families reduced, with the residual deliberately left as the visible test subject (see Landed).
|
|
140
|
-
- Landed: added a shared `createFactorySession` mock-session builder (`test/helpers/subagent-session-io.ts`); grouped the `createSubagentSession` arrange into describe-scoped `beforeEach` hooks with the `createSubagentSession(...)` act kept explicit per test (AAA); extracted a `programTurns` arrange helper for the turn-limit tests; consolidated the manager spawn/queued-pair arrangements; and folded `create-subagent-session-extension-tools.test.ts` (4 post-bind guard tests) into `create-subagent-session.test.ts`, deleting the file and its duplicated scaffolding.
|
|
141
|
-
Package test duplication: 669 → 512 lines; test files 64 → 63; test count 1005 → 1010 (+5 `createFactorySession` self-tests).
|
|
142
|
-
Two lifecycle clone families remain (`create-subagent-session.test.ts` ~29 lines, `subagent-manager.test.ts` ~23 lines): both are the repeated `await createSubagentSession(...)` / `spawn(...)` **act** with test-specific arrange, intentionally not extracted because hiding the system-under-test behind a helper is the wrong abstraction for test code (Sandi Metz: "duplication is far cheaper than the wrong abstraction").
|
|
143
|
-
Lesson: the original "families ≤ 1" target was a weak signal for _test_ code — an early act-wrapping helper that hit the metric was reverted in favour of the AAA structure above; the metric was relaxed deliberately.
|
|
144
|
-
The three overlapping session-mock builders this surfaced were resolved in [#412] by targeted reuse: `createFactorySession` now spreads the shared `createMockSession` core (inheriting the `messages`/`subscribe`/`emit`/`steer`/`dispose`/`sessionManager` base) and layers only the factory facet on top.
|
|
145
|
-
`createSubagentSessionStub` was left as-is — it already composes `createMockSession` as its wrapped `.session`, so its overlap is intrinsic delegation glue rather than duplication.
|
|
146
|
-
|
|
147
|
-
### Step 8 — Consolidate UI and tools test fixtures ([#379])
|
|
148
|
-
|
|
149
|
-
- Targets: `test/ui/agent-creation-wizard.test.ts`, `test/ui/agent-config-editor.test.ts`, `test/ui/ui-observer.test.ts`, `test/tools/foreground-runner.test.ts`, `test/tools/background-spawner.test.ts`, `test/session/session-config.test.ts`, and `test/service/service-adapter.test.ts` (a seventh non-lifecycle family fallow reports; added to scope in planning).
|
|
150
|
-
- Smell: Category D — remaining clone families outside the lifecycle tree.
|
|
151
|
-
- Change: extract per-file repeated arrangements into local helpers or `test/helpers/` where shared across files.
|
|
152
|
-
- Outcome: each named family's extractable arrange consolidated; resulting fallow figures reported (the roadmap's original "44 → ≤ 25 groups; ≤ 0.6%" target predated Steps 1–7 and used a different baseline, so acceptance was "consolidate and measure," not a binding number).
|
|
153
|
-
- Landed: promoted the cross-file `ResolvedSpawnConfig` builder to `test/helpers/make-spawn-config.ts` (`createResolvedSpawnConfig`, flat options that derive the mirrored `agentInvocation`/`presentation.detailBase` regions) with a 5-test companion; consolidated the remaining six families with file-local, value-returning arrange builders and `beforeEach` setup (`spawnAndWaitRegistering`, `ui-observer` session/tracker `beforeEach` with `vi.fn()` onUpdate counters, `generateUI`/`manualUI`/`withInputs`, `disabledConfig`, `exploreConfig`, `createManagerStub`).
|
|
154
|
-
Package test duplication: 512 → 355 lines; clone groups 32 → 24; duplication 2.49% → 1.73%; test files 63 → 64; test count 1010 → 1015 (+5 `createResolvedSpawnConfig` self-tests).
|
|
155
|
-
Three multi-group families remain: the two lifecycle residuals from Step 7 (`create-subagent-session.test.ts`, `subagent-manager.test.ts`) and `agent-config-editor.test.ts`, whose residual clones are the repeated `await editor.showAgentDetail(...)` **act** plus its `setupDetail` arrange and `ui.select.mock.calls` menu assertion — left inline because wrapping the system-under-test is the wrong abstraction for test code (Step 7 lesson; Sandi Metz: "duplication is far cheaper than the wrong abstraction").
|
|
156
|
-
|
|
157
|
-
### Step 9 — Resolve the cross-package settings-loader duplication ([#380])
|
|
158
|
-
|
|
159
|
-
- Targets: `src/settings.ts:198-211`, `packages/pi-subagents-worktrees/src/config.ts:51-73`.
|
|
160
|
-
- Smell: Category A — 23-line production clone: the layered global/project JSON read-sanitize-warn-merge loader.
|
|
161
|
-
- Decision: **extract** — `loadLayeredSettings<T>` added to `src/layered-settings.ts` and published via the `@gotgenes/pi-subagents/settings` dedicated subpath export (`dist/settings.d.ts`); `loadSettings` in `settings.ts` delegates to it internally.
|
|
162
|
-
Option 2 (fallow suppression) was rejected; the public-API cost is accepted as the right long-term tradeoff for a shared convention in the `@gotgenes/pi-*` family.
|
|
163
|
-
Worktrees migration (swap `config.ts`'s inlined loader for the shared helper) is deferred to [#415] — worktrees resolves pi-subagents from the registry, so it must wait for a published release carrying the helper.
|
|
164
|
-
- Outcome: `pnpm fallow:dupes --skip-local` no longer reports the `settings.ts` ↔ `config.ts` pair.
|
|
165
|
-
The parametrised helper's token sequence diverged sufficiently that the contiguous identical run dropped below the reporting threshold even before the worktrees migration lands.
|
|
166
|
-
Definitive semantic elimination completed when worktrees adopted the helper in [#415].
|
|
167
|
-
|
|
168
|
-
## Step dependency diagram
|
|
169
|
-
|
|
170
|
-
```mermaid
|
|
171
|
-
flowchart TB
|
|
172
|
-
S1["Step 1 (#381)<br/>ConcurrencyLimiter replacement"]
|
|
173
|
-
S2["Step 2 (#373)<br/>SubagentState extraction"]
|
|
174
|
-
S3["Step 3 (#374)<br/>Encapsulate start + notification"]
|
|
175
|
-
S4["Step 4 (#375)<br/>Run collaborators extraction"]
|
|
176
|
-
S5["Step 5 (#376)<br/>Observer class from index.ts"]
|
|
177
|
-
S6["Step 6 (#377)<br/>Widget handle out of runtime"]
|
|
178
|
-
S7["Step 7 (#378)<br/>Lifecycle test fixtures"]
|
|
179
|
-
S8["Step 8 (#379)<br/>UI/tools test fixtures"]
|
|
180
|
-
S9["Step 9 (#380)<br/>Settings-loader duplication"]
|
|
181
|
-
|
|
182
|
-
S1 --> S3
|
|
183
|
-
S2 --> S3
|
|
184
|
-
S3 --> S4
|
|
185
|
-
S4 --> S7
|
|
186
|
-
S5 --> S6
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Steps 8 and 9 have no dependencies and can run at any point.
|
|
190
|
-
|
|
191
|
-
## Tracks
|
|
192
|
-
|
|
193
|
-
| Track | Steps | Theme |
|
|
194
|
-
| ----------------------------- | ------------- | ---------------------------------------------------------------------------------- |
|
|
195
|
-
| A — Subagent constructibility | 2 → 3 → 4 → 7 | Construct complete; encapsulate run state; then consolidate the tests that churned |
|
|
196
|
-
| B — Wiring debt | 1, 5 → 6 | Shrink index.ts; eliminate forward references and relay delegation |
|
|
197
|
-
| C — Test hygiene | 8 | Clone families outside the lifecycle tree |
|
|
198
|
-
| D — Duplication policy | 9 | Cross-package clone decision |
|
|
199
|
-
|
|
200
|
-
Tracks A and B intersect only at Step 3 (which needs Step 1's queue relocation); otherwise they proceed in parallel.
|
|
201
|
-
Tracks C and D are fully independent.
|
|
202
|
-
|
|
203
|
-
[#277]: https://github.com/gotgenes/pi-packages/issues/277
|
|
204
|
-
[#373]: https://github.com/gotgenes/pi-packages/issues/373
|
|
205
|
-
[#374]: https://github.com/gotgenes/pi-packages/issues/374
|
|
206
|
-
[#375]: https://github.com/gotgenes/pi-packages/issues/375
|
|
207
|
-
[#376]: https://github.com/gotgenes/pi-packages/issues/376
|
|
208
|
-
[#377]: https://github.com/gotgenes/pi-packages/issues/377
|
|
209
|
-
[#378]: https://github.com/gotgenes/pi-packages/issues/378
|
|
210
|
-
[#379]: https://github.com/gotgenes/pi-packages/issues/379
|
|
211
|
-
[#380]: https://github.com/gotgenes/pi-packages/issues/380
|
|
212
|
-
[#381]: https://github.com/gotgenes/pi-packages/issues/381
|
|
213
|
-
[#412]: https://github.com/gotgenes/pi-packages/issues/412
|
|
214
|
-
[#415]: https://github.com/gotgenes/pi-packages/issues/415
|