@gotgenes/pi-subagents 13.2.0 → 13.2.2
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 +14 -0
- package/README.md +21 -10
- package/dist/public.d.ts +34 -35
- package/docs/architecture/architecture.md +58 -148
- package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
- package/docs/decisions/0003-publish-bundled-type-declarations.md +3 -1
- package/docs/plans/0051-update-adr-0001-hard-fork.md +8 -6
- package/docs/plans/0257-extract-child-session-factory.md +3 -1
- package/docs/plans/0262-add-workspace-provider-seam.md +41 -39
- package/docs/plans/0264-remove-extension-lifecycle-control.md +100 -98
- package/docs/plans/0265-born-complete-subagent-session.md +5 -2
- package/docs/plans/0270-type-consumable-public-surface.md +3 -1
- package/docs/plans/0280-rename-agent-to-subagent.md +197 -0
- package/docs/retro/0051-update-adr-0001-hard-fork.md +4 -2
- package/docs/retro/0257-extract-child-session-factory.md +3 -1
- package/docs/retro/0262-add-workspace-provider-seam.md +3 -1
- package/docs/retro/0264-remove-extension-lifecycle-control.md +3 -1
- package/docs/retro/0270-type-consumable-public-surface.md +4 -2
- package/docs/retro/0277-encapsulate-agent-session.md +41 -0
- package/docs/retro/0280-rename-agent-to-subagent.md +96 -0
- package/package.json +1 -1
- package/src/index.ts +9 -9
- package/src/lifecycle/create-subagent-session.ts +1 -1
- package/src/lifecycle/{agent-manager.ts → subagent-manager.ts} +27 -27
- package/src/lifecycle/subagent-session.ts +2 -2
- package/src/lifecycle/{agent.ts → subagent.ts} +28 -28
- package/src/lifecycle/turn-limits.ts +1 -1
- package/src/lifecycle/workspace.ts +2 -2
- package/src/observation/notification.ts +9 -9
- package/src/observation/record-observer.ts +9 -9
- package/src/runtime.ts +1 -1
- package/src/service/service-adapter.ts +10 -10
- package/src/service/service.ts +5 -9
- package/src/tools/agent-tool.ts +5 -5
- package/src/tools/background-spawner.ts +3 -3
- package/src/tools/foreground-runner.ts +5 -5
- package/src/tools/get-result-tool.ts +2 -2
- package/src/tools/steer-tool.ts +2 -2
- package/src/types.ts +1 -1
- package/src/ui/agent-creation-wizard.ts +2 -2
- package/src/ui/agent-menu.ts +5 -5
- package/src/ui/agent-widget.ts +2 -2
- package/src/ui/conversation-viewer.ts +3 -3
|
@@ -0,0 +1,144 @@
|
|
|
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
|
|
@@ -27,7 +27,7 @@ A `tsc --traceResolution` of a sibling consuming the package surfaced two compou
|
|
|
27
27
|
The public entry's type closure is deeply entangled: `WorkspaceProvider` (in `lifecycle/workspace.ts`) reaches `AgentStatus` in the 510-line `lifecycle/agent.ts`, plus `SubagentType`/`AgentInvocation` from `types.ts` (which itself re-exports the `Agent` class).
|
|
28
28
|
A shallow alias-free entry is therefore not achievable without a substantial source restructure.
|
|
29
29
|
|
|
30
|
-
This collides with the ship-source model (ADR
|
|
30
|
+
This collides with the ship-source model ([ADR-0002]): every package ships raw `.ts` executed directly by Pi, with no build step.
|
|
31
31
|
|
|
32
32
|
## Decision
|
|
33
33
|
|
|
@@ -67,3 +67,5 @@ It is deliberately narrow: it produces type declarations only and changes nothin
|
|
|
67
67
|
- The `types` condition points at a build-time artifact; an in-repo workspace-linked consumer that imported the package would need `dist/public.d.ts` present.
|
|
68
68
|
This is acceptable because no in-repo package imports the surface yet; #263 consumes the built artifact from the published tarball.
|
|
69
69
|
- Sequencing: #270 must be published (its release-please PR merged) before #263 edits `pi-subagents` core, so #263's changes do not batch into the same `pi-subagents` release.
|
|
70
|
+
|
|
71
|
+
[ADR-0002]: ./0002-extensions-on-a-minimal-core.md
|
|
@@ -3,14 +3,14 @@ issue: 51
|
|
|
3
3
|
issue_title: "docs: update ADR 0001 to reflect hard-fork decision"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Update ADR
|
|
6
|
+
# Update ADR-0001 to reflect hard-fork decision
|
|
7
7
|
|
|
8
8
|
## Problem Statement
|
|
9
9
|
|
|
10
|
-
ADR
|
|
10
|
+
[ADR-0001] was written when the fork was a thin-patch layer over `tintinweb/pi-subagents`.
|
|
11
11
|
The new architecture document (`docs/architecture/architecture.md`) commits to a hard fork with material scope reduction — scheduling removal, a `SubagentsAPI` boundary, `index.ts` decomposition, and more.
|
|
12
12
|
|
|
13
|
-
Several claims in ADR
|
|
13
|
+
Several claims in [ADR-0001] are now outdated:
|
|
14
14
|
|
|
15
15
|
1. The status is "accepted" but the decision has been superseded by the architecture doc.
|
|
16
16
|
2. The Upstream PRs section states "the fork's divergence reduces to package naming and tooling," which is no longer true.
|
|
@@ -18,7 +18,7 @@ Several claims in ADR 0001 are now outdated:
|
|
|
18
18
|
|
|
19
19
|
## Goals
|
|
20
20
|
|
|
21
|
-
- Add a supersession note to ADR
|
|
21
|
+
- Add a supersession note to [ADR-0001] pointing to `docs/architecture/architecture.md`.
|
|
22
22
|
- Update the "Upstream PRs are open" subsection so the "divergence reduces to…" claim reflects reality.
|
|
23
23
|
- Update the Consequences → Operational section to note intentional divergence per the architecture document.
|
|
24
24
|
- Preserve all existing rationale — no information is removed.
|
|
@@ -31,7 +31,7 @@ Several claims in ADR 0001 are now outdated:
|
|
|
31
31
|
|
|
32
32
|
## Background
|
|
33
33
|
|
|
34
|
-
ADR
|
|
34
|
+
[ADR-0001] has YAML frontmatter (`status: accepted`, `date: 2026-05-11`) and follows a standard ADR structure: Status, Context, Decision, Consequences.
|
|
35
35
|
|
|
36
36
|
The architecture document (`docs/architecture/architecture.md`) describes a six-phase plan that materially diverges from upstream: scheduling removal, ad-hoc RPC replacement, group-join and output-file removal, a typed `SubagentsAPI` boundary, and `index.ts` decomposition.
|
|
37
37
|
|
|
@@ -60,7 +60,7 @@ No tests are affected — this is a docs-only change.
|
|
|
60
60
|
|
|
61
61
|
## TDD Order
|
|
62
62
|
|
|
63
|
-
1. Update ADR
|
|
63
|
+
1. Update [ADR-0001] with all four edits described above.
|
|
64
64
|
Commit: `docs: update ADR 0001 to reflect hard-fork decision (#51)`
|
|
65
65
|
|
|
66
66
|
## Risks and Mitigations
|
|
@@ -73,3 +73,5 @@ No tests are affected — this is a docs-only change.
|
|
|
73
73
|
## Open Questions
|
|
74
74
|
|
|
75
75
|
None — the issue's acceptance criteria are unambiguous.
|
|
76
|
+
|
|
77
|
+
[ADR-0001]: ../decisions/0001-deferred-patches.md
|
|
@@ -6,7 +6,7 @@ issue_title: "Extract ChildSessionFactory from runner"
|
|
|
6
6
|
# Extract ChildSessionFactory from runner
|
|
7
7
|
|
|
8
8
|
> Superseded — issue #257 was closed `not_planned`.
|
|
9
|
-
> Planning this extraction exposed that worktree isolation does not belong in the core; see [ADR
|
|
9
|
+
> Planning this extraction exposed that worktree isolation does not belong in the core; see [ADR-0002] and the reclaimed Phase 16 roadmap in [`docs/architecture/architecture.md`](../architecture/architecture.md).
|
|
10
10
|
> The structural goal is recovered by #265.
|
|
11
11
|
> This plan is retained for historical context only.
|
|
12
12
|
|
|
@@ -281,3 +281,5 @@ After all steps: `pnpm run check`, `pnpm run lint`, `pnpm -r run test`, `pnpm fa
|
|
|
281
281
|
Deferred to Step 4, when the runner dissolves and the natural home for these creation contracts is the factory module.
|
|
282
282
|
- Whether `ConcreteAgentRunner.createFactory()` lands in Step 3 (when `AgentManager` consumes it) exactly as the issue describes.
|
|
283
283
|
Deferred to Step 3 per the Design Overview rationale.
|
|
284
|
+
|
|
285
|
+
[ADR-0002]: ../decisions/0002-extensions-on-a-minimal-core.md
|
|
@@ -7,50 +7,50 @@ issue_title: "Add WorkspaceProvider extension seam"
|
|
|
7
7
|
|
|
8
8
|
## Problem Statement
|
|
9
9
|
|
|
10
|
-
Phase 16, Step 2 of ADR
|
|
11
|
-
The core needs only a working directory and a disposal hook for a child run; the default
|
|
10
|
+
Phase 16, Step 2 of [ADR-0002].
|
|
11
|
+
The core needs only a working directory and a disposal hook for a child run; the default - the parent's cwd, with no setup or teardown - is always correct.
|
|
12
12
|
"Where does a child run, and what brackets the run?"
|
|
13
13
|
is a *strategy* (git worktree, container, tmpdir, remote sandbox), not core behavior.
|
|
14
|
-
ADR
|
|
15
|
-
This issue adds that seam
|
|
14
|
+
[ADR-0002] classifies this as the single *generative* extension surface: a concern that must return a value the core consumes synchronously attaches through a rationed provider seam, not an observational event.
|
|
15
|
+
This issue adds that seam -`WorkspaceProvider` / `Workspace` plus `SubagentsService.registerWorkspaceProvider` - without the core gaining any knowledge of what an "isolation strategy" is.
|
|
16
16
|
|
|
17
17
|
## Goals
|
|
18
18
|
|
|
19
19
|
- Define the `WorkspaceProvider` and `Workspace` interfaces in the core, with zero git or worktree knowledge.
|
|
20
|
-
- Add `SubagentsService.registerWorkspaceProvider(provider): () => void`
|
|
21
|
-
- At run-start, consult the registered provider for the child's cwd and a disposal handle; with no provider, the child runs in `baseCwd` (parent cwd
|
|
20
|
+
- Add `SubagentsService.registerWorkspaceProvider(provider): () => void` - a single-provider seam (chaining is out of scope) that throws if a provider is already registered and returns an unregister disposer.
|
|
21
|
+
- At run-start, consult the registered provider for the child's cwd and a disposal handle; with no provider, the child runs in `baseCwd` (parent cwd - default behavior unchanged).
|
|
22
22
|
- Call `dispose()` after the run and append the returned `resultAddendum` to the child's result.
|
|
23
|
-
- This change is **additive and non-breaking**
|
|
23
|
+
- This change is **additive and non-breaking** - the existing `isolation: "worktree"` path is left intact (its eviction is #263).
|
|
24
24
|
|
|
25
25
|
## Non-Goals
|
|
26
26
|
|
|
27
|
-
- Removing `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, or the `isolation: "worktree"` spawn mode
|
|
28
|
-
- Removing `isolated` / `extensions: false` / `noSkills`
|
|
29
|
-
- Born-complete child execution / dissolving the runner
|
|
30
|
-
- Multiple/chained providers
|
|
31
|
-
- Shipping a concrete provider implementation
|
|
27
|
+
- Removing `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, or the `isolation: "worktree"` spawn mode - deferred to #263.
|
|
28
|
+
- Removing `isolated` / `extensions: false` / `noSkills` - deferred to #264.
|
|
29
|
+
- Born-complete child execution / dissolving the runner - deferred to #265.
|
|
30
|
+
- Multiple/chained providers - out of scope per the issue; one provider only.
|
|
31
|
+
- Shipping a concrete provider implementation - the worktrees package (#263) is the seam's first real consumer.
|
|
32
32
|
Within this issue the seam is exercised only by test fakes; see Risks for the "no vacant hooks" release-coordination constraint.
|
|
33
33
|
|
|
34
34
|
## Background
|
|
35
35
|
|
|
36
36
|
Relevant existing modules:
|
|
37
37
|
|
|
38
|
-
- `src/lifecycle/agent.ts`
|
|
38
|
+
- `src/lifecycle/agent.ts` - `Agent.run()` calls `this.worktree?.setup()` at run-start to obtain a cwd, threads it into `runner.run({ context: { cwd } })`, and on completion calls `this.worktree?.cleanup(description)`, appending a "Changes saved to branch ..." addendum.
|
|
39
39
|
This is exactly the prepare/dispose shape the seam generalizes.
|
|
40
|
-
- `src/lifecycle/worktree-isolation.ts`
|
|
40
|
+
- `src/lifecycle/worktree-isolation.ts` - `WorktreeIsolation` is the current run-scoped collaborator: `setup()` returns a path, `cleanup(description)` returns a `WorktreeCleanupResult`.
|
|
41
41
|
The seam is its abstraction; #263 will reimplement it as a `WorkspaceProvider` in a separate package.
|
|
42
|
-
- `src/lifecycle/agent-manager.ts`
|
|
42
|
+
- `src/lifecycle/agent-manager.ts` - constructs each `Agent`, owns the injected `WorktreeManager`, and threads `getRunConfig` as a getter.
|
|
43
43
|
The same getter pattern is reused for the workspace provider.
|
|
44
|
-
- `src/service/service.ts`
|
|
44
|
+
- `src/service/service.ts` - the package's public API surface (`package.json` `exports` points at `./src/service.ts`).
|
|
45
45
|
`SubagentsService`, `SpawnOptions`, and `SubagentRecord` all live here; the seam types are re-exported here so the worktrees package can implement them.
|
|
46
|
-
- `src/service/service-adapter.ts`
|
|
47
|
-
- `src/lifecycle/child-lifecycle.ts`
|
|
46
|
+
- `src/service/service-adapter.ts` - `SubagentsServiceAdapter implements SubagentsService`, wrapping the `AgentManagerLike` narrow interface.
|
|
47
|
+
- `src/lifecycle/child-lifecycle.ts` - the *observational* lifecycle events from #261 (`spawning`, `session-created`, `completed`, `disposed`).
|
|
48
48
|
The provider seam is orthogonal: events tell consumers what happened; the provider returns a value the core consumes.
|
|
49
49
|
|
|
50
50
|
AGENTS.md constraints that apply:
|
|
51
51
|
|
|
52
|
-
- Pi SDK imports stay out of library modules
|
|
53
|
-
- Do not read `process.cwd()` inside library functions
|
|
52
|
+
- Pi SDK imports stay out of library modules - the seam interfaces and `AgentManager` accept the provider as a parameter; `index.ts` (the SDK edge) supplies `baseCwd: process.cwd()`.
|
|
53
|
+
- Do not read `process.cwd()` inside library functions - `baseCwd` is injected into `AgentManager` from `index.ts`.
|
|
54
54
|
- When adding a public API pattern, follow the established convention: the repo's registration/subscription convention is an unsubscribe **function** (`() => void`, as in `SubscribableSession.subscribe` and `pi.events.on`), not a `Symbol.dispose` `Disposable`.
|
|
55
55
|
The seam therefore returns `() => void`; this is a deliberate divergence from the issue's literal `Disposable` to match the codebase convention.
|
|
56
56
|
|
|
@@ -97,10 +97,10 @@ export interface WorkspaceProvider {
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Note the addendum-formatting boundary: the core appends `resultAddendum` *verbatim*.
|
|
100
|
-
The provider owns its own separator and wording (the worktrees package owns the "Changes saved to branch
|
|
100
|
+
The provider owns its own separator and wording (the worktrees package owns the "Changes saved to branch ..." string in #263).
|
|
101
101
|
The core never formats branch text.
|
|
102
102
|
|
|
103
|
-
### Registration
|
|
103
|
+
### Registration - single provider, throw on duplicate
|
|
104
104
|
|
|
105
105
|
`AgentManager` holds an optional provider and exposes registration:
|
|
106
106
|
|
|
@@ -130,7 +130,7 @@ The disposer clears the slot only if the same provider is still active, so a sta
|
|
|
130
130
|
Provider-first precedence: when a provider supplies a workspace, the core routes cwd and dispose through it and skips the legacy worktree collaborator; with no provider it falls back to the existing worktree path; with neither it runs in `baseCwd` (cwd undefined → SDK uses the parent cwd).
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
|
-
// run()
|
|
133
|
+
// run() - replacing the worktree?.setup() block
|
|
134
134
|
let cwd: string | undefined;
|
|
135
135
|
try {
|
|
136
136
|
const provider = this._getWorkspaceProvider?.();
|
|
@@ -152,7 +152,7 @@ try {
|
|
|
152
152
|
this.observer?.onRunFinished?.(this);
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
//
|
|
155
|
+
// ... runner.run({ context: { cwd, parentSession }, ... })
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
On completion (`completeRun`) the core computes the final status, then disposes:
|
|
@@ -165,7 +165,7 @@ if (this._workspace) {
|
|
|
165
165
|
if (out?.resultAddendum) finalResult += out.resultAddendum;
|
|
166
166
|
} else {
|
|
167
167
|
const wt = this.worktree?.cleanup(this.description);
|
|
168
|
-
if (wt?.hasChanges && wt.branch) finalResult += `\n\n---\nChanges saved to branch \`${wt.branch}
|
|
168
|
+
if (wt?.hasChanges && wt.branch) finalResult += `\n\n---\nChanges saved to branch \`${wt.branch}\`...`;
|
|
169
169
|
}
|
|
170
170
|
```
|
|
171
171
|
|
|
@@ -197,7 +197,7 @@ Provider-first precedence means the two never silently conflict, and #263 collap
|
|
|
197
197
|
| `src/lifecycle/agent-manager.ts` | `AgentManagerOptions` gains required `baseCwd: string`. New `workspaceProvider` field, `registerWorkspaceProvider()` (throw on dup, unregister disposer). `spawn()` passes `baseCwd` and the `getWorkspaceProvider` getter into each `Agent`. |
|
|
198
198
|
| `src/service/service.ts` | Re-export the five seam types and `AgentStatus`. Add `registerWorkspaceProvider(provider: WorkspaceProvider): () => void` to the `SubagentsService` interface. |
|
|
199
199
|
| `src/service/service-adapter.ts` | `AgentManagerLike` gains `registerWorkspaceProvider(provider): () => void`. `SubagentsServiceAdapter` implements the method, delegating to the manager. |
|
|
200
|
-
| `src/index.ts` | Pass `baseCwd: process.cwd()` to the `new AgentManager({
|
|
200
|
+
| `src/index.ts` | Pass `baseCwd: process.cwd()` to the `new AgentManager({...})` construction (alongside the existing `GitWorktreeManager(process.cwd())`). |
|
|
201
201
|
| `docs/architecture/architecture.md` | Mark Phase 16 Step 2 (#262) as landed in the roadmap; note the seam exists and `workspace.ts` is added to the lifecycle domain listing. |
|
|
202
202
|
|
|
203
203
|
No exports are removed or renamed, so no `src/`/`test/` removed-symbol grep is required.
|
|
@@ -206,8 +206,8 @@ No file in Module-Level Changes is also claimed as unchanged in Non-Goals (the w
|
|
|
206
206
|
### Grep checklist before finalizing
|
|
207
207
|
|
|
208
208
|
- Objects typed as `SubagentsService` in tests: `test/service/service.test.ts` casts `{ spawn: () => "id" } as unknown as SubagentsService`, so adding an interface method does **not** break it (verified).
|
|
209
|
-
- `new AgentManager(` call sites: `src/index.ts` (one) and `test/lifecycle/agent-manager.test.ts` `createManager` (one)
|
|
210
|
-
- `AgentManagerLike` mocks in `test/service/service-adapter.test.ts` (`defaultManager`, inline `spawn:` stubs)
|
|
209
|
+
- `new AgentManager(` call sites: `src/index.ts` (one) and `test/lifecycle/agent-manager.test.ts` `createManager` (one) - both updated for required `baseCwd` in the same step.
|
|
210
|
+
- `AgentManagerLike` mocks in `test/service/service-adapter.test.ts` (`defaultManager`, inline `spawn:` stubs) - add `registerWorkspaceProvider` stub in the same step.
|
|
211
211
|
|
|
212
212
|
## Test Impact Analysis
|
|
213
213
|
|
|
@@ -216,36 +216,36 @@ This is an additive seam, so the work is dominated by *new* tests; little existi
|
|
|
216
216
|
1. New unit tests the seam enables: provider registration (throw-on-duplicate, disposer-unregisters), run-start consultation (cwd from `prepare`, `resultAddendum` appended on dispose), `prepare` returns undefined → `baseCwd`, `prepare` rejects → `markError`, and adapter delegation.
|
|
217
217
|
These were impossible before because there was no provider abstraction to substitute.
|
|
218
218
|
2. Redundant existing tests: none.
|
|
219
|
-
The seam does not subsume worktree tests
|
|
220
|
-
3. Existing tests that must stay as-is: all `worktree.test.ts`, `worktree-isolation.test.ts`, and the AgentManager worktree-isolation tests (`calls worktrees.create` / `cleanup`)
|
|
219
|
+
The seam does not subsume worktree tests - they exercise the legacy path, which is preserved.
|
|
220
|
+
3. Existing tests that must stay as-is: all `worktree.test.ts`, `worktree-isolation.test.ts`, and the AgentManager worktree-isolation tests (`calls worktrees.create` / `cleanup`) - they genuinely exercise the fallback path that remains in #262.
|
|
221
221
|
The Agent no-provider tests assert unchanged worktree behavior.
|
|
222
222
|
|
|
223
223
|
## TDD Order
|
|
224
224
|
|
|
225
|
-
1. **Seam types + registration surface**
|
|
225
|
+
1. **Seam types + registration surface** - `feat`.
|
|
226
226
|
New `src/lifecycle/workspace.ts`; re-export seam types + `AgentStatus` from `service.ts`; add `registerWorkspaceProvider` to `SubagentsService`, `AgentManagerLike`, and `SubagentsServiceAdapter` (delegating); add required `baseCwd` + provider field + `registerWorkspaceProvider` (throw on dup, disposer) to `AgentManager`; update `index.ts` and the `createManager` test factory for `baseCwd`.
|
|
227
227
|
Tests: `agent-manager.test.ts` registration (throws on second register; disposer clears only the active provider; getter returns the registered provider) and `service-adapter.test.ts` delegation.
|
|
228
|
-
This whole surface lands in one commit because the `SubagentsService` interface method forces the adapter to implement it and the required `baseCwd` forces both construction sites
|
|
228
|
+
This whole surface lands in one commit because the `SubagentsService` interface method forces the adapter to implement it and the required `baseCwd` forces both construction sites - splitting would not type-check.
|
|
229
229
|
Suggested message: `feat: add WorkspaceProvider registration seam to subagents service`.
|
|
230
230
|
Run `pnpm run check` immediately after (shared-interface change).
|
|
231
231
|
|
|
232
|
-
2. **Run-start consumption + dispose**
|
|
232
|
+
2. **Run-start consumption + dispose** - `feat`.
|
|
233
233
|
`Agent`: `AgentInit` gains `baseCwd`/`getWorkspaceProvider`; new private fields; `run()` provider-first prepare; `completeRun`/`failRun` dispose + verbatim `resultAddendum`.
|
|
234
234
|
`AgentManager.spawn` passes `baseCwd` and the `getWorkspaceProvider` getter (sole extra construction site, folded in).
|
|
235
|
-
Tests: `agent.test.ts`
|
|
235
|
+
Tests: `agent.test.ts` - provider `prepare` supplies cwd to the runner; `dispose` `resultAddendum` appended to the result; `prepare` undefined → cwd falls back to `baseCwd`; `prepare` rejects → `markError` + `onRunFinished`; no-provider path still uses the worktree collaborator (regression guard).
|
|
236
236
|
Suggested message: `feat: consult workspace provider for child cwd and disposal`.
|
|
237
237
|
Run `pnpm run check` after (AgentInit change).
|
|
238
238
|
|
|
239
|
-
3. **Architecture doc update**
|
|
239
|
+
3. **Architecture doc update** - `docs`.
|
|
240
240
|
Mark Phase 16 Step 2 (#262) landed in the roadmap; add `workspace.ts` to the lifecycle domain listing; cross-link the seam.
|
|
241
241
|
Suggested message: `docs: record WorkspaceProvider seam in phase 16 roadmap`.
|
|
242
242
|
|
|
243
243
|
## Risks and Mitigations
|
|
244
244
|
|
|
245
245
|
- **Vacant hook (the headline risk).**
|
|
246
|
-
ADR
|
|
246
|
+
[ADR-0002]’s “no vacant hooks” rule says a provider seam with no consumer is a speculative abstraction that `fallow` flags as dead.
|
|
247
247
|
Within #262 the seam is exercised only by test fakes.
|
|
248
|
-
Mitigation: land #262 **alongside** #263 (its first real consumer, `@gotgenes/pi-subagents-worktrees`)
|
|
248
|
+
Mitigation: land #262 **alongside** #263 (its first real consumer, `@gotgenes/pi-subagents-worktrees`) - do not cut a release that contains the seam without the worktrees package.
|
|
249
249
|
Track this as a release-coordination constraint; the architecture roadmap already pairs Steps 2 and 3.
|
|
250
250
|
- **Dual cwd path confusion.**
|
|
251
251
|
Provider-first precedence keeps worktree and provider from silently conflicting; the branch is documented and removed in #263.
|
|
@@ -257,6 +257,8 @@ This is an additive seam, so the work is dominated by *new* tests; little existi
|
|
|
257
257
|
## Open Questions
|
|
258
258
|
|
|
259
259
|
- Should `baseCwd` eventually come from the parent `SessionContext.cwd` rather than `process.cwd()`?
|
|
260
|
-
Deferred
|
|
260
|
+
Deferred -`process.cwd()` preserves current worktree behavior; revisit during the born-complete work (#265).
|
|
261
261
|
- Should the `disposed` lifecycle event (#261) and `Workspace.dispose` be reconciled into one teardown notion?
|
|
262
|
-
Deferred
|
|
262
|
+
Deferred - they serve different surfaces (observational vs generative); revisit if #265 dissolves the runner.
|
|
263
|
+
|
|
264
|
+
[ADR-0002]: ../decisions/0002-extensions-on-a-minimal-core.md
|