@gotgenes/pi-subagents 11.4.0 → 11.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 +8 -0
- package/docs/architecture/architecture.md +28 -3
- package/docs/plans/0262-add-workspace-provider-seam.md +262 -0
- package/docs/retro/0262-add-workspace-provider-seam.md +44 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lifecycle/agent-manager.ts +30 -0
- package/src/lifecycle/agent.ts +44 -7
- package/src/lifecycle/workspace.ts +47 -0
- package/src/service/service-adapter.ts +6 -0
- package/src/service/service.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [11.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.4.0...pi-subagents-v11.5.0) (2026-05-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add WorkspaceProvider registration seam to subagents service ([51a9970](https://github.com/gotgenes/pi-packages/commit/51a99701db214c11f08251e9ed5549d01c4d5839))
|
|
14
|
+
* consult workspace provider for child cwd and disposal ([32eeffc](https://github.com/gotgenes/pi-packages/commit/32eeffc1cc31bc7e403c25cdd116e2b351be4527))
|
|
15
|
+
|
|
8
16
|
## [11.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.3.0...pi-subagents-v11.4.0) (2026-05-29)
|
|
9
17
|
|
|
10
18
|
|
|
@@ -275,6 +275,7 @@ src/
|
|
|
275
275
|
│ ├── parent-snapshot.ts immutable spawn-time parent state
|
|
276
276
|
│ ├── execution-state.ts session/output phase state
|
|
277
277
|
│ ├── child-lifecycle.ts child-execution lifecycle event publisher
|
|
278
|
+
│ ├── workspace.ts workspace provider seam (generative extension surface)
|
|
278
279
|
│ ├── worktree.ts git worktree isolation
|
|
279
280
|
│ ├── worktree-isolation.ts worktree lifecycle collaborator
|
|
280
281
|
│ └── usage.ts token usage tracking
|
|
@@ -355,6 +356,8 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
355
356
|
- `child-lifecycle` — publishes the child-execution lifecycle (`spawning`, `session-created` before `bindExtensions()`, `completed`, `disposed`) on `pi.events`.
|
|
356
357
|
Reactive consumers subscribe: `@gotgenes/pi-permission-system` registers each child session on `session-created` and unregisters it on `disposed`.
|
|
357
358
|
This replaced the former outbound `permission-bridge` (#261, ADR 0002) — the core no longer looks up a named consumer.
|
|
359
|
+
- `workspace` — the single generative seam (#262, ADR 0002): a registered `WorkspaceProvider` supplies a child's cwd plus bracketed `dispose()` at run-start.
|
|
360
|
+
With no provider, children run in the parent cwd (default unchanged); the git worktree strategy moves behind this seam in #263.
|
|
358
361
|
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
359
362
|
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
360
363
|
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
@@ -719,6 +722,14 @@ See [phase-14-strip-policy.md](history/phase-14-strip-policy.md) for details.
|
|
|
719
722
|
[#239]: https://github.com/gotgenes/pi-packages/issues/239
|
|
720
723
|
[#242]: https://github.com/gotgenes/pi-packages/issues/242
|
|
721
724
|
|
|
725
|
+
## Phase 15 (complete)
|
|
726
|
+
|
|
727
|
+
Phase 15 evolved `Agent` from a passive state machine (`AgentRecord`) into an object that owns its entire execution lifecycle.
|
|
728
|
+
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()`.
|
|
729
|
+
After Phase 15, Agent is born complete with all dependencies and configuration, owns `run()` and `resume()`, and manages its own observer and worktree lifecycle.
|
|
730
|
+
All six steps are closed: [#227], [#228], [#231], [#229], [#230], [#232].
|
|
731
|
+
See [phase-15-domain-model-evolution.md](history/phase-15-domain-model-evolution.md) for details.
|
|
732
|
+
|
|
722
733
|
## Improvement roadmap (Phase 16 — invert dependencies: extensions on a minimal core)
|
|
723
734
|
|
|
724
735
|
Phase 16 reclaims its original intent — invert the core's outbound dependencies — and extends it: worktree isolation joins permissions as an *extension* on a minimal core, leaving pi-subagents a pure child-session orchestrator.
|
|
@@ -751,12 +762,16 @@ Migrate `@gotgenes/pi-permission-system` to subscribe to `session-created`/`disp
|
|
|
751
762
|
- Outcome: the core stops reaching out to a named consumer; permission detection rides events.
|
|
752
763
|
- Deferred: removing the now-caller-less `registerSubagentSession`/`unregisterSubagentSession` from `PermissionsService` → #267; registry-detected resume ("executing now" → "exists" semantics) → #265.
|
|
753
764
|
|
|
754
|
-
#### Step 2: Define the `WorkspaceProvider` seam — [#262]
|
|
765
|
+
#### Step 2: Define the `WorkspaceProvider` seam — [#262] ✅ Delivered
|
|
755
766
|
|
|
756
|
-
|
|
757
|
-
|
|
767
|
+
Added the `WorkspaceProvider` / `Workspace` interfaces (`src/lifecycle/workspace.ts`) and `SubagentsService.registerWorkspaceProvider` (single provider, throws on duplicate, returns an unregister disposer).
|
|
768
|
+
Only `WorkspaceProvider` is named-re-exported from `service.ts`; `Workspace` and the context types resolve via inference when a consumer assigns to `WorkspaceProvider` (the worktrees package adds named re-exports in #263 when it imports them by name).
|
|
769
|
+
At run-start `Agent.run()` consults the registered provider (provider-first precedence) for the child's cwd and a disposal handle; with no provider it falls back to the legacy worktree collaborator, and with neither the child runs in the parent's cwd.
|
|
770
|
+
On completion the core calls `Workspace.dispose({ status, description })` and appends the returned `resultAddendum` verbatim — the provider owns the wording.
|
|
758
771
|
|
|
772
|
+
- The seam is additive and non-breaking: the existing `isolation: "worktree"` path is untouched (its eviction is Step 3).
|
|
759
773
|
- Land alongside its first consumer (Step 3) to avoid a vacant hook — the "no vacant hooks" rule.
|
|
774
|
+
Within #262 the seam is exercised only by test fakes; do not cut a release containing the seam without `@gotgenes/pi-subagents-worktrees`.
|
|
760
775
|
- Outcome: a single generative seam; the core no longer knows what an "isolation strategy" is.
|
|
761
776
|
|
|
762
777
|
#### Step 3: Extract worktrees to `@gotgenes/pi-subagents-worktrees` — [#263]
|
|
@@ -895,4 +910,14 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
895
910
|
[#217]: https://github.com/gotgenes/pi-packages/issues/217
|
|
896
911
|
[#218]: https://github.com/gotgenes/pi-packages/issues/218
|
|
897
912
|
[#219]: https://github.com/gotgenes/pi-packages/issues/219
|
|
913
|
+
[#227]: https://github.com/gotgenes/pi-packages/issues/227
|
|
914
|
+
[#228]: https://github.com/gotgenes/pi-packages/issues/228
|
|
915
|
+
[#229]: https://github.com/gotgenes/pi-packages/issues/229
|
|
916
|
+
[#230]: https://github.com/gotgenes/pi-packages/issues/230
|
|
898
917
|
[#231]: https://github.com/gotgenes/pi-packages/issues/231
|
|
918
|
+
[#232]: https://github.com/gotgenes/pi-packages/issues/232
|
|
919
|
+
[#261]: https://github.com/gotgenes/pi-packages/issues/261
|
|
920
|
+
[#262]: https://github.com/gotgenes/pi-packages/issues/262
|
|
921
|
+
[#263]: https://github.com/gotgenes/pi-packages/issues/263
|
|
922
|
+
[#264]: https://github.com/gotgenes/pi-packages/issues/264
|
|
923
|
+
[#265]: https://github.com/gotgenes/pi-packages/issues/265
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 262
|
|
3
|
+
issue_title: "Add WorkspaceProvider extension seam"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add the WorkspaceProvider extension seam
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Phase 16, Step 2 of ADR 0002 (`packages/pi-subagents/docs/decisions/0002-extensions-on-a-minimal-core.md`).
|
|
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
|
+
"Where does a child run, and what brackets the run?"
|
|
13
|
+
is a *strategy* (git worktree, container, tmpdir, remote sandbox), not core behavior.
|
|
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
|
+
|
|
17
|
+
## Goals
|
|
18
|
+
|
|
19
|
+
- Define the `WorkspaceProvider` and `Workspace` interfaces in the core, with zero git or worktree knowledge.
|
|
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
|
+
- Call `dispose()` after the run and append the returned `resultAddendum` to the child's result.
|
|
23
|
+
- This change is **additive and non-breaking** — the existing `isolation: "worktree"` path is left intact (its eviction is #263).
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
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
|
+
Within this issue the seam is exercised only by test fakes; see Risks for the "no vacant hooks" release-coordination constraint.
|
|
33
|
+
|
|
34
|
+
## Background
|
|
35
|
+
|
|
36
|
+
Relevant existing modules:
|
|
37
|
+
|
|
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
|
+
This is exactly the prepare/dispose shape the seam generalizes.
|
|
40
|
+
- `src/lifecycle/worktree-isolation.ts` — `WorktreeIsolation` is the current run-scoped collaborator: `setup()` returns a path, `cleanup(description)` returns a `WorktreeCleanupResult`.
|
|
41
|
+
The seam is its abstraction; #263 will reimplement it as a `WorkspaceProvider` in a separate package.
|
|
42
|
+
- `src/lifecycle/agent-manager.ts` — constructs each `Agent`, owns the injected `WorktreeManager`, and threads `getRunConfig` as a getter.
|
|
43
|
+
The same getter pattern is reused for the workspace provider.
|
|
44
|
+
- `src/service/service.ts` — the package's public API surface (`package.json` `exports` points at `./src/service.ts`).
|
|
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` — `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
|
+
The provider seam is orthogonal: events tell consumers what happened; the provider returns a value the core consumes.
|
|
49
|
+
|
|
50
|
+
AGENTS.md constraints that apply:
|
|
51
|
+
|
|
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
|
+
- 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
|
+
The seam therefore returns `() => void`; this is a deliberate divergence from the issue's literal `Disposable` to match the codebase convention.
|
|
56
|
+
|
|
57
|
+
## Design Overview
|
|
58
|
+
|
|
59
|
+
### Seam interfaces
|
|
60
|
+
|
|
61
|
+
Defined in a new core module `src/lifecycle/workspace.ts` (sibling to `child-lifecycle.ts`), re-exported from `service.ts` for public consumers.
|
|
62
|
+
The `status` field reuses the core `AgentStatus` union (from `agent.ts`), re-exported publicly so the worktrees package can name it.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import type { AgentStatus } from "#src/lifecycle/agent";
|
|
66
|
+
import type { AgentInvocation, SubagentType } from "#src/types";
|
|
67
|
+
|
|
68
|
+
/** Context the core hands a provider when a child run starts. */
|
|
69
|
+
export interface WorkspacePrepareContext {
|
|
70
|
+
agentId: string;
|
|
71
|
+
agentType: SubagentType;
|
|
72
|
+
baseCwd: string;
|
|
73
|
+
invocation?: AgentInvocation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Outcome the core reports to a workspace when the run ends. */
|
|
77
|
+
export interface WorkspaceDisposeOutcome {
|
|
78
|
+
status: AgentStatus;
|
|
79
|
+
description: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What dispose may hand back for the core to fold into the child result. */
|
|
83
|
+
export interface WorkspaceDisposeResult {
|
|
84
|
+
resultAddendum?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A prepared working directory plus its bracketed teardown. Born complete. */
|
|
88
|
+
export interface Workspace {
|
|
89
|
+
readonly cwd: string; // the directory already exists
|
|
90
|
+
dispose(outcome: WorkspaceDisposeOutcome): WorkspaceDisposeResult | void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The single generative seam: supplies a child's workspace. */
|
|
94
|
+
export interface WorkspaceProvider {
|
|
95
|
+
prepare(ctx: WorkspacePrepareContext): Promise<Workspace | undefined>;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
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 …" string in #263).
|
|
101
|
+
The core never formats branch text.
|
|
102
|
+
|
|
103
|
+
### Registration — single provider, throw on duplicate
|
|
104
|
+
|
|
105
|
+
`AgentManager` holds an optional provider and exposes registration:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
private workspaceProvider?: WorkspaceProvider;
|
|
109
|
+
|
|
110
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void {
|
|
111
|
+
if (this.workspaceProvider) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"A WorkspaceProvider is already registered; only one is supported.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
this.workspaceProvider = provider;
|
|
117
|
+
return () => {
|
|
118
|
+
if (this.workspaceProvider === provider) this.workspaceProvider = undefined;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The throw surfaces a misconfiguration loudly (two workspace extensions installed at once).
|
|
124
|
+
The disposer clears the slot only if the same provider is still active, so a stale disposer cannot evict a later registration.
|
|
125
|
+
`SubagentsServiceAdapter.registerWorkspaceProvider` delegates straight through; `AgentManagerLike` gains the method.
|
|
126
|
+
|
|
127
|
+
### Run-start consultation (Tell-Don't-Ask call site)
|
|
128
|
+
|
|
129
|
+
`Agent.run()` consults the provider at the point where it currently calls `worktree?.setup()`.
|
|
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
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// run() — replacing the worktree?.setup() block
|
|
134
|
+
let cwd: string | undefined;
|
|
135
|
+
try {
|
|
136
|
+
const provider = this._getWorkspaceProvider?.();
|
|
137
|
+
if (provider) {
|
|
138
|
+
this._workspace = await provider.prepare({
|
|
139
|
+
agentId: this.id,
|
|
140
|
+
agentType: this.type,
|
|
141
|
+
baseCwd: this._baseCwd,
|
|
142
|
+
invocation: this.invocation,
|
|
143
|
+
});
|
|
144
|
+
cwd = this._workspace?.cwd;
|
|
145
|
+
} else {
|
|
146
|
+
this.worktree?.setup();
|
|
147
|
+
cwd = this.worktree?.path;
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
this.markError(err);
|
|
151
|
+
this.releaseListeners();
|
|
152
|
+
this.observer?.onRunFinished?.(this);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// … runner.run({ context: { cwd, parentSession }, … })
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
On completion (`completeRun`) the core computes the final status, then disposes:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const finalStatus: AgentStatus =
|
|
162
|
+
result.aborted ? "aborted" : result.steered ? "steered" : "completed";
|
|
163
|
+
if (this._workspace) {
|
|
164
|
+
const out = this._workspace.dispose({ status: finalStatus, description: this.description });
|
|
165
|
+
if (out?.resultAddendum) finalResult += out.resultAddendum;
|
|
166
|
+
} else {
|
|
167
|
+
const wt = this.worktree?.cleanup(this.description);
|
|
168
|
+
if (wt?.hasChanges && wt.branch) finalResult += `\n\n---\nChanges saved to branch \`${wt.branch}\`…`;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`failRun` mirrors this in a `try/catch`, disposing with `status: "error"` and discarding any addendum (matching the existing error-path behavior, which does not append branch text).
|
|
173
|
+
|
|
174
|
+
The provider getter is injected into each `Agent` by `AgentManager.spawn` (`getWorkspaceProvider: () => this.workspaceProvider`), exactly like `getRunConfig`.
|
|
175
|
+
`baseCwd` is injected into `AgentManager` from `index.ts` and threaded to each `Agent`.
|
|
176
|
+
|
|
177
|
+
### Why the worktree path stays (scope decision A)
|
|
178
|
+
|
|
179
|
+
Per the clarification, #262 is the additive seam only; the legacy `isolation: "worktree"` orchestration is untouched and removed in #263.
|
|
180
|
+
A genuinely separate strategy could register a provider today and get correct cwd + dispose behavior; worktree spawns keep working unchanged.
|
|
181
|
+
Provider-first precedence means the two never silently conflict, and #263 collapses the branch by deleting the worktree arm.
|
|
182
|
+
|
|
183
|
+
### Edge cases
|
|
184
|
+
|
|
185
|
+
- `prepare()` resolves `undefined` → `cwd` is undefined → runner uses `baseCwd` (parent cwd).
|
|
186
|
+
No dispose call (no workspace).
|
|
187
|
+
- `prepare()` rejects → `markError`, release listeners, notify observer, return (same shape as a worktree `setup()` failure today).
|
|
188
|
+
- `dispose()` returns `void` or no `resultAddendum` → result unchanged.
|
|
189
|
+
- Duplicate `registerWorkspaceProvider` → throws synchronously.
|
|
190
|
+
|
|
191
|
+
## Module-Level Changes
|
|
192
|
+
|
|
193
|
+
| File | Change |
|
|
194
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| `src/lifecycle/workspace.ts` | **New.** Defines `WorkspaceProvider`, `Workspace`, `WorkspacePrepareContext`, `WorkspaceDisposeOutcome`, `WorkspaceDisposeResult`. No behavior. |
|
|
196
|
+
| `src/lifecycle/agent.ts` | `AgentInit` gains optional `baseCwd?: string` and `getWorkspaceProvider?: () => WorkspaceProvider \| undefined`. New private fields `_baseCwd`, `_getWorkspaceProvider`, `_workspace?: Workspace`. `run()` provider-first prepare; `completeRun`/`failRun` dispose + verbatim `resultAddendum`. Export `AgentStatus` is already public. |
|
|
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
|
+
| `src/service/service.ts` | Re-export the five seam types and `AgentStatus`. Add `registerWorkspaceProvider(provider: WorkspaceProvider): () => void` to the `SubagentsService` interface. |
|
|
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({…})` construction (alongside the existing `GitWorktreeManager(process.cwd())`). |
|
|
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
|
+
|
|
203
|
+
No exports are removed or renamed, so no `src/`/`test/` removed-symbol grep is required.
|
|
204
|
+
No file in Module-Level Changes is also claimed as unchanged in Non-Goals (the worktree *modules* are non-goals; `agent.ts` touches the worktree *call path* additively, which is consistent).
|
|
205
|
+
|
|
206
|
+
### Grep checklist before finalizing
|
|
207
|
+
|
|
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) — 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
|
+
|
|
212
|
+
## Test Impact Analysis
|
|
213
|
+
|
|
214
|
+
This is an additive seam, so the work is dominated by *new* tests; little existing coverage is affected.
|
|
215
|
+
|
|
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
|
+
These were impossible before because there was no provider abstraction to substitute.
|
|
218
|
+
2. Redundant existing tests: none.
|
|
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
|
+
The Agent no-provider tests assert unchanged worktree behavior.
|
|
222
|
+
|
|
223
|
+
## TDD Order
|
|
224
|
+
|
|
225
|
+
1. **Seam types + registration surface** — `feat`.
|
|
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
|
+
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 — splitting would not type-check.
|
|
229
|
+
Suggested message: `feat: add WorkspaceProvider registration seam to subagents service`.
|
|
230
|
+
Run `pnpm run check` immediately after (shared-interface change).
|
|
231
|
+
|
|
232
|
+
2. **Run-start consumption + dispose** — `feat`.
|
|
233
|
+
`Agent`: `AgentInit` gains `baseCwd`/`getWorkspaceProvider`; new private fields; `run()` provider-first prepare; `completeRun`/`failRun` dispose + verbatim `resultAddendum`.
|
|
234
|
+
`AgentManager.spawn` passes `baseCwd` and the `getWorkspaceProvider` getter (sole extra construction site, folded in).
|
|
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
|
+
Suggested message: `feat: consult workspace provider for child cwd and disposal`.
|
|
237
|
+
Run `pnpm run check` after (AgentInit change).
|
|
238
|
+
|
|
239
|
+
3. **Architecture doc update** — `docs`.
|
|
240
|
+
Mark Phase 16 Step 2 (#262) landed in the roadmap; add `workspace.ts` to the lifecycle domain listing; cross-link the seam.
|
|
241
|
+
Suggested message: `docs: record WorkspaceProvider seam in phase 16 roadmap`.
|
|
242
|
+
|
|
243
|
+
## Risks and Mitigations
|
|
244
|
+
|
|
245
|
+
- **Vacant hook (the headline risk).**
|
|
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
|
+
Within #262 the seam is exercised only by test fakes.
|
|
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
|
+
Track this as a release-coordination constraint; the architecture roadmap already pairs Steps 2 and 3.
|
|
250
|
+
- **Dual cwd path confusion.**
|
|
251
|
+
Provider-first precedence keeps worktree and provider from silently conflicting; the branch is documented and removed in #263.
|
|
252
|
+
- **`baseCwd` source.**
|
|
253
|
+
Injecting `process.cwd()` from `index.ts` matches the existing `GitWorktreeManager(process.cwd())` construction; no new global-state read enters a library module.
|
|
254
|
+
- **Status timing in dispose.**
|
|
255
|
+
The final status is computed before the status-transition methods mutate, so `dispose`'s outcome reflects the true terminal status.
|
|
256
|
+
|
|
257
|
+
## Open Questions
|
|
258
|
+
|
|
259
|
+
- Should `baseCwd` eventually come from the parent `SessionContext.cwd` rather than `process.cwd()`?
|
|
260
|
+
Deferred — `process.cwd()` preserves current worktree behavior; revisit during the born-complete work (#265).
|
|
261
|
+
- Should the `disposed` lifecycle event (#261) and `Workspace.dispose` be reconciled into one teardown notion?
|
|
262
|
+
Deferred — they serve different surfaces (observational vs generative); revisit if #265 dissolves the runner.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 262
|
|
3
|
+
issue_title: "Add WorkspaceProvider extension seam"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #262 — Add WorkspaceProvider extension seam
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-29T14:51:15Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a numbered implementation plan for the Phase 16, Step 2 `WorkspaceProvider` seam (ADR 0002).
|
|
13
|
+
The plan adds the seam additively — `WorkspaceProvider` / `Workspace` interfaces, `SubagentsService.registerWorkspaceProvider`, run-start consultation, and `dispose` with a verbatim `resultAddendum` — while leaving the existing `isolation: "worktree"` path untouched for #263 to evict.
|
|
14
|
+
Three TDD steps (two `feat`, one `docs`).
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- Two ambiguous choices were surfaced via `ask_user` and resolved: **scope = additive seam only** (Option A — leave the legacy worktree path; #263 evicts it), and **duplicate registration = throw** (loud misconfiguration surface, disposer clears only the active provider).
|
|
19
|
+
- The package's public surface is `./src/service.ts` (per `package.json` `exports`), so the seam types are defined in a new core `src/lifecycle/workspace.ts` and re-exported from `service.ts` — avoiding a `service ↔ lifecycle` import cycle while still exposing them to the worktrees consumer.
|
|
20
|
+
- Diverged from the issue's literal `Disposable` return type: the repo convention for unsubscribe/unregister is a plain `() => void` (matching `SubscribableSession.subscribe` and `pi.events.on`); no `Symbol.dispose` usage exists anywhere in the codebase.
|
|
21
|
+
- Provider-first precedence was chosen so the new seam and the legacy worktree collaborator never silently conflict during the transient dual-path window (#263 collapses the branch).
|
|
22
|
+
- Headline risk is the ADR "no vacant hooks" rule: within #262 the seam is exercised only by test fakes, so it must land **alongside** #263 (`@gotgenes/pi-subagents-worktrees`) and not ship in a release on its own.
|
|
23
|
+
- Step 1 bundles the entire registration surface (types, `SubagentsService` method, adapter impl, `AgentManagerLike`, required `baseCwd`) into one commit because the interface method forces the adapter and the required field forces both construction sites — splitting would not type-check.
|
|
24
|
+
- Verified `test/service/service.test.ts` casts its mock `as unknown as SubagentsService`, so adding an interface method does not break it; flagged the `createManager` and `AgentManagerLike` mock updates for the `baseCwd` and registration additions.
|
|
25
|
+
|
|
26
|
+
## Stage: Implementation — TDD (2026-05-29T15:09:49Z)
|
|
27
|
+
|
|
28
|
+
### Session summary
|
|
29
|
+
|
|
30
|
+
Implemented the `WorkspaceProvider` seam across three TDD cycles (two `feat`, one `docs`): the registration surface (`AgentManager.registerWorkspaceProvider` + service/adapter delegation + `workspace.ts` types), run-start consumption in `Agent.run()` with provider-first precedence and `dispose`/`resultAddendum`, and an architecture-doc update.
|
|
31
|
+
Test count went from 1049 to 1061 (+12 new tests; +6 in `agent.test.ts`, +4 registration in `agent-manager.test.ts`, +1 adapter delegation, plus existing-helper additions).
|
|
32
|
+
All deterministic gates green: `check`, `lint`, `test`, and `fallow dead-code` (run from repo root).
|
|
33
|
+
|
|
34
|
+
### Observations
|
|
35
|
+
|
|
36
|
+
- Deviation from plan (Module-Level Changes): the plan said `service.ts` would re-export "the five seam types and `AgentStatus`", but `fallow dead-code` flagged those five re-exports as unused (no consumer until #263), and AGENTS.md forbids speculative re-exports.
|
|
37
|
+
Resolved by re-exporting only `WorkspaceProvider` — a consumer assigning to it gets `Workspace` and the context types via inference; #263 adds named re-exports when it imports them.
|
|
38
|
+
This is the concrete manifestation of the plan's headline "vacant hook" risk surfacing in the dead-code gate.
|
|
39
|
+
- Lint surprise: `WorkspaceDisposeResult | void` tripped eslint `no-invalid-void-type`.
|
|
40
|
+
Changed the `dispose` return type to `WorkspaceDisposeResult | undefined` (equivalent — a side-effecting `dispose` that falls off the end returns `undefined`); minor divergence from the issue's literal `| void`.
|
|
41
|
+
- Three test mock factories implement `AgentManagerLike` in `service-adapter.test.ts` (`createMockManager`, `defaultManager`, `createTestManager`) — all three needed the new `registerWorkspaceProvider` stub; `tsc` caught the third after the first two were updated.
|
|
42
|
+
- Used `git commit --fixup` + `--autosquash` rebase twice (unpushed history) to fold the fallow trim into the Step 1 `feat` commit and the reviewer's doc-wording fix into the Step 3 `docs` commit, keeping each commit self-consistent.
|
|
43
|
+
- Pre-completion reviewer: WARN — all blocking checks pass; one non-blocking doc finding (architecture.md overstated that `Workspace` is re-exported).
|
|
44
|
+
Addressed before finishing.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -167,6 +167,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
167
167
|
const manager = new AgentManager({
|
|
168
168
|
runner: new ConcreteAgentRunner(runnerDeps),
|
|
169
169
|
worktrees: new GitWorktreeManager(process.cwd()),
|
|
170
|
+
baseCwd: process.cwd(),
|
|
170
171
|
observer,
|
|
171
172
|
queue,
|
|
172
173
|
getRunConfig: () => settings,
|
|
@@ -13,6 +13,7 @@ import { Agent, type AgentLifecycleObserver } from "#src/lifecycle/agent";
|
|
|
13
13
|
import type { AgentRunner } from "#src/lifecycle/agent-runner";
|
|
14
14
|
import type { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
|
|
15
15
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
16
|
+
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
16
17
|
import type { WorktreeManager } from "#src/lifecycle/worktree";
|
|
17
18
|
import { WorktreeIsolation } from "#src/lifecycle/worktree-isolation";
|
|
18
19
|
|
|
@@ -33,6 +34,8 @@ export interface AgentManagerOptions {
|
|
|
33
34
|
worktrees: WorktreeManager;
|
|
34
35
|
/** Concurrency queue — owns scheduling, limit checks, and drain logic. */
|
|
35
36
|
queue: ConcurrencyQueue;
|
|
37
|
+
/** Base working directory handed to a workspace provider (the parent cwd). */
|
|
38
|
+
baseCwd: string;
|
|
36
39
|
getRunConfig?: () => RunConfig;
|
|
37
40
|
observer?: AgentManagerObserver;
|
|
38
41
|
}
|
|
@@ -70,12 +73,20 @@ export class AgentManager {
|
|
|
70
73
|
private readonly runner: AgentRunner;
|
|
71
74
|
private readonly worktrees: WorktreeManager;
|
|
72
75
|
private readonly queue: ConcurrencyQueue;
|
|
76
|
+
private readonly baseCwd: string;
|
|
73
77
|
private getRunConfig?: () => RunConfig;
|
|
78
|
+
private _workspaceProvider?: WorkspaceProvider;
|
|
79
|
+
|
|
80
|
+
/** The registered workspace provider, or undefined when none is registered. */
|
|
81
|
+
get workspaceProvider(): WorkspaceProvider | undefined {
|
|
82
|
+
return this._workspaceProvider;
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
constructor(options: AgentManagerOptions) {
|
|
76
86
|
this.runner = options.runner;
|
|
77
87
|
this.worktrees = options.worktrees;
|
|
78
88
|
this.queue = options.queue;
|
|
89
|
+
this.baseCwd = options.baseCwd;
|
|
79
90
|
this.observer = options.observer;
|
|
80
91
|
this.getRunConfig = options.getRunConfig;
|
|
81
92
|
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
@@ -83,6 +94,23 @@ export class AgentManager {
|
|
|
83
94
|
this.cleanupInterval.unref();
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Register the single workspace provider. Throws if one is already
|
|
99
|
+
* registered (chaining is out of scope — see ADR 0002). Returns a disposer
|
|
100
|
+
* that clears the slot only if this provider is still the active one.
|
|
101
|
+
*/
|
|
102
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void {
|
|
103
|
+
if (this._workspaceProvider) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"A WorkspaceProvider is already registered; only one is supported.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
this._workspaceProvider = provider;
|
|
109
|
+
return () => {
|
|
110
|
+
if (this._workspaceProvider === provider) this._workspaceProvider = undefined;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
/** Compose a per-agent lifecycle observer from manager and spawn-config concerns. */
|
|
87
115
|
private buildObserver(options: AgentSpawnConfig): AgentLifecycleObserver {
|
|
88
116
|
return {
|
|
@@ -140,6 +168,8 @@ export class AgentManager {
|
|
|
140
168
|
: undefined,
|
|
141
169
|
observer: this.buildObserver(options),
|
|
142
170
|
getRunConfig: this.getRunConfig,
|
|
171
|
+
baseCwd: this.baseCwd,
|
|
172
|
+
getWorkspaceProvider: () => this._workspaceProvider,
|
|
143
173
|
});
|
|
144
174
|
this.agents.set(id, record);
|
|
145
175
|
|
package/src/lifecycle/agent.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { ExecutionState } from "#src/lifecycle/execution-state";
|
|
|
26
26
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
27
27
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
28
28
|
import { addUsage } from "#src/lifecycle/usage";
|
|
29
|
+
import type { Workspace, WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
29
30
|
import type { WorktreeIsolation } from "#src/lifecycle/worktree-isolation";
|
|
30
31
|
import { NotificationState } from "#src/observation/notification-state";
|
|
31
32
|
import { subscribeAgentObserver } from "#src/observation/record-observer";
|
|
@@ -72,6 +73,10 @@ export interface AgentInit {
|
|
|
72
73
|
worktree?: WorktreeIsolation;
|
|
73
74
|
observer?: AgentLifecycleObserver;
|
|
74
75
|
getRunConfig?: () => RunConfig;
|
|
76
|
+
/** Resolves the registered workspace provider (if any) at run-start. */
|
|
77
|
+
getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
78
|
+
/** Parent working directory handed to a workspace provider's prepare(). */
|
|
79
|
+
baseCwd?: string;
|
|
75
80
|
|
|
76
81
|
// Run config (required for run(), optional for tests)
|
|
77
82
|
snapshot?: ParentSnapshot;
|
|
@@ -129,6 +134,10 @@ export class Agent {
|
|
|
129
134
|
readonly worktree?: WorktreeIsolation;
|
|
130
135
|
readonly observer?: AgentLifecycleObserver;
|
|
131
136
|
private readonly _getRunConfig?: () => RunConfig;
|
|
137
|
+
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
138
|
+
private readonly _baseCwd: string;
|
|
139
|
+
/** Workspace prepared at run-start by a provider — undefined when none is registered. */
|
|
140
|
+
private _workspace?: Workspace;
|
|
132
141
|
|
|
133
142
|
// Run config — optional (required for run())
|
|
134
143
|
private readonly _snapshot?: ParentSnapshot;
|
|
@@ -186,6 +195,8 @@ export class Agent {
|
|
|
186
195
|
this.worktree = init.worktree;
|
|
187
196
|
this.observer = init.observer;
|
|
188
197
|
this._getRunConfig = init.getRunConfig;
|
|
198
|
+
this._getWorkspaceProvider = init.getWorkspaceProvider;
|
|
199
|
+
this._baseCwd = init.baseCwd ?? "";
|
|
189
200
|
|
|
190
201
|
// Run config
|
|
191
202
|
this._snapshot = init.snapshot;
|
|
@@ -223,8 +234,23 @@ export class Agent {
|
|
|
223
234
|
this.observer?.onStarted?.(this);
|
|
224
235
|
this.wireSignal(this._signal, () => this.abort());
|
|
225
236
|
|
|
237
|
+
let cwd: string | undefined;
|
|
226
238
|
try {
|
|
227
|
-
|
|
239
|
+
// Provider-first: a registered workspace provider supplies the cwd and
|
|
240
|
+
// owns teardown; otherwise fall back to the legacy worktree collaborator.
|
|
241
|
+
const provider = this._getWorkspaceProvider?.();
|
|
242
|
+
if (provider) {
|
|
243
|
+
this._workspace = await provider.prepare({
|
|
244
|
+
agentId: this.id,
|
|
245
|
+
agentType: this.type,
|
|
246
|
+
baseCwd: this._baseCwd,
|
|
247
|
+
invocation: this.invocation,
|
|
248
|
+
});
|
|
249
|
+
cwd = this._workspace?.cwd;
|
|
250
|
+
} else {
|
|
251
|
+
this.worktree?.setup();
|
|
252
|
+
cwd = this.worktree?.path;
|
|
253
|
+
}
|
|
228
254
|
} catch (err) {
|
|
229
255
|
this.markError(err);
|
|
230
256
|
this.releaseListeners();
|
|
@@ -236,7 +262,7 @@ export class Agent {
|
|
|
236
262
|
try {
|
|
237
263
|
const result = await this._runner.run(this._snapshot, this.type, this._prompt, {
|
|
238
264
|
context: {
|
|
239
|
-
cwd
|
|
265
|
+
cwd,
|
|
240
266
|
parentSession: this._parentSession,
|
|
241
267
|
},
|
|
242
268
|
model: this._model,
|
|
@@ -442,9 +468,19 @@ export class Agent {
|
|
|
442
468
|
this.releaseListeners();
|
|
443
469
|
|
|
444
470
|
let finalResult = result.responseText;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
471
|
+
if (this._workspace) {
|
|
472
|
+
const finalStatus: AgentStatus = result.aborted
|
|
473
|
+
? "aborted"
|
|
474
|
+
: result.steered
|
|
475
|
+
? "steered"
|
|
476
|
+
: "completed";
|
|
477
|
+
const disposeResult = this._workspace.dispose({ status: finalStatus, description: this.description });
|
|
478
|
+
if (disposeResult?.resultAddendum) finalResult += disposeResult.resultAddendum;
|
|
479
|
+
} else {
|
|
480
|
+
const wtResult = this.worktree?.cleanup(this.description);
|
|
481
|
+
if (wtResult?.hasChanges && wtResult.branch) {
|
|
482
|
+
finalResult += `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
|
|
483
|
+
}
|
|
448
484
|
}
|
|
449
485
|
|
|
450
486
|
if (result.aborted) this.markAborted(finalResult);
|
|
@@ -465,8 +501,9 @@ export class Agent {
|
|
|
465
501
|
this.releaseListeners();
|
|
466
502
|
|
|
467
503
|
try {
|
|
468
|
-
this.
|
|
469
|
-
|
|
504
|
+
if (this._workspace) this._workspace.dispose({ status: "error", description: this.description });
|
|
505
|
+
else this.worktree?.cleanup(this.description);
|
|
506
|
+
} catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
|
|
470
507
|
|
|
471
508
|
this.observer?.onRunFinished?.(this);
|
|
472
509
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace.ts — The single generative extension seam (ADR 0002, Phase 16 Step 2).
|
|
3
|
+
*
|
|
4
|
+
* "Where does a child run, and what brackets the run?" is a strategy (git
|
|
5
|
+
* worktree, container, tmpdir, remote sandbox), not core behavior. The core
|
|
6
|
+
* needs only a working directory plus a disposal hook; the default — the
|
|
7
|
+
* parent's cwd, with no setup/teardown — is always correct.
|
|
8
|
+
*
|
|
9
|
+
* Unlike the observational lifecycle events in child-lifecycle.ts, this is a
|
|
10
|
+
* *generative* seam: a registered provider returns a value the core consumes
|
|
11
|
+
* synchronously at run-start. The core has no knowledge of git or worktrees.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AgentStatus } from "#src/lifecycle/agent";
|
|
15
|
+
import type { AgentInvocation, SubagentType } from "#src/types";
|
|
16
|
+
|
|
17
|
+
/** Context the core hands a provider when a child run starts. */
|
|
18
|
+
export interface WorkspacePrepareContext {
|
|
19
|
+
agentId: string;
|
|
20
|
+
agentType: SubagentType;
|
|
21
|
+
baseCwd: string;
|
|
22
|
+
invocation?: AgentInvocation;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Outcome the core reports to a workspace when the run ends. */
|
|
26
|
+
export interface WorkspaceDisposeOutcome {
|
|
27
|
+
status: AgentStatus;
|
|
28
|
+
description: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What dispose may hand back for the core to fold into the child result. */
|
|
32
|
+
export interface WorkspaceDisposeResult {
|
|
33
|
+
/** Appended verbatim to the child's result text — the provider owns the wording. */
|
|
34
|
+
resultAddendum?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A prepared working directory plus its bracketed teardown. Born complete. */
|
|
38
|
+
export interface Workspace {
|
|
39
|
+
/** The working directory — already exists when the workspace is handed back. */
|
|
40
|
+
readonly cwd: string;
|
|
41
|
+
dispose(outcome: WorkspaceDisposeOutcome): WorkspaceDisposeResult | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The single generative seam: supplies a child's workspace. */
|
|
45
|
+
export interface WorkspaceProvider {
|
|
46
|
+
prepare(ctx: WorkspacePrepareContext): Promise<Workspace | undefined>;
|
|
47
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
9
|
+
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
9
10
|
import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
|
|
10
11
|
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
11
12
|
import type { Agent, SessionContext } from "#src/types";
|
|
@@ -18,6 +19,7 @@ export interface AgentManagerLike {
|
|
|
18
19
|
abort(id: string): boolean;
|
|
19
20
|
waitForAll(): Promise<void>;
|
|
20
21
|
hasRunning(): boolean;
|
|
22
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -107,6 +109,10 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
107
109
|
hasRunning(): boolean {
|
|
108
110
|
return this.manager.hasRunning();
|
|
109
111
|
}
|
|
112
|
+
|
|
113
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void {
|
|
114
|
+
return this.manager.registerWorkspaceProvider(provider);
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|
package/src/service/service.ts
CHANGED
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { LifetimeUsage } from "#src/lifecycle/usage";
|
|
13
|
+
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// Generative extension seam (ADR 0002, Phase 16 Step 2). Only the provider
|
|
16
|
+
// entry-point type is re-exported here; a consumer assigning to
|
|
17
|
+
// `WorkspaceProvider` gets `Workspace` and the context types via inference.
|
|
18
|
+
// The worktrees package (#263) adds named re-exports when it imports them.
|
|
19
|
+
export type { LifetimeUsage, WorkspaceProvider };
|
|
15
20
|
|
|
16
21
|
export type SubagentStatus =
|
|
17
22
|
| "queued"
|
|
@@ -73,6 +78,13 @@ export interface SubagentsService {
|
|
|
73
78
|
|
|
74
79
|
/** Whether any agents are running or queued. */
|
|
75
80
|
hasRunning(): boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register the single workspace provider that supplies a child's working
|
|
84
|
+
* directory plus bracketed setup/teardown. Throws if one is already
|
|
85
|
+
* registered. Returns a disposer that unregisters the provider.
|
|
86
|
+
*/
|
|
87
|
+
registerWorkspaceProvider(provider: WorkspaceProvider): () => void;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/** Event channel constants for pi.events subscriptions. */
|