@gotgenes/pi-subagents 11.6.0 → 12.0.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 +13 -0
- package/dist/public.d.ts +4 -12
- package/docs/architecture/architecture.md +14 -16
- package/docs/retro/0270-type-consumable-public-surface.md +50 -0
- package/package.json +1 -1
- package/src/config/custom-agents.ts +0 -1
- package/src/config/invocation-config.ts +1 -4
- package/src/index.ts +0 -2
- package/src/lifecycle/agent-manager.ts +1 -14
- package/src/lifecycle/agent.ts +10 -24
- package/src/service/service-adapter.ts +0 -3
- package/src/service/service.ts +3 -4
- package/src/tools/agent-tool.ts +1 -7
- package/src/tools/background-spawner.ts +0 -1
- package/src/tools/foreground-runner.ts +0 -1
- package/src/tools/spawn-config.ts +1 -5
- package/src/types.ts +0 -6
- package/src/ui/agent-config-editor.ts +0 -1
- package/src/ui/agent-creation-wizard.ts +0 -1
- package/src/ui/display.ts +0 -1
- package/src/lifecycle/worktree-isolation.ts +0 -59
- package/src/lifecycle/worktree.ts +0 -194
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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
|
+
## [12.0.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.6.0...pi-subagents-v12.0.0) (2026-05-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* SubagentRecord no longer carries worktreeResult, and the core no longer creates git worktrees. Worktree isolation moves to @gotgenes/pi-subagents-worktrees.
|
|
14
|
+
* the Agent tool no longer accepts isolation: "worktree", and SubagentsService.SpawnOptions no longer has an isolation field. Install @gotgenes/pi-subagents-worktrees and list the agent in worktreeAgents instead.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* drop the isolation spawn axis from the subagents API ([2ff8970](https://github.com/gotgenes/pi-packages/commit/2ff897059feec67a49af7e3f54e0e4828faa2521))
|
|
19
|
+
* remove git worktree isolation from the subagents core ([2e81044](https://github.com/gotgenes/pi-packages/commit/2e81044221562c528d1fb296f356f60d81af0661))
|
|
20
|
+
|
|
8
21
|
## [11.6.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v11.5.0...pi-subagents-v11.6.0) (2026-05-29)
|
|
9
22
|
|
|
10
23
|
|
package/dist/public.d.ts
CHANGED
|
@@ -20,8 +20,6 @@ type LifetimeUsage = {
|
|
|
20
20
|
|
|
21
21
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
22
22
|
type SubagentType = string;
|
|
23
|
-
/** Isolation mode for agent execution. */
|
|
24
|
-
type IsolationMode = "worktree";
|
|
25
23
|
interface AgentInvocation {
|
|
26
24
|
/** Short display name, e.g. "haiku" — only set when different from parent. */
|
|
27
25
|
modelName?: string;
|
|
@@ -30,7 +28,6 @@ interface AgentInvocation {
|
|
|
30
28
|
isolated?: boolean;
|
|
31
29
|
inheritContext?: boolean;
|
|
32
30
|
runInBackground?: boolean;
|
|
33
|
-
isolation?: IsolationMode;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
/**
|
|
@@ -43,11 +40,11 @@ interface AgentInvocation {
|
|
|
43
40
|
* Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
|
|
44
41
|
* accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
|
|
45
42
|
*
|
|
46
|
-
* Behavior (abort, steer buffering
|
|
47
|
-
*
|
|
43
|
+
* Behavior (abort, steer buffering) lives on the agent rather than on
|
|
44
|
+
* AgentManager — each agent manages its own lifecycle concerns.
|
|
48
45
|
*
|
|
49
|
-
*
|
|
50
|
-
* (
|
|
46
|
+
* The child's working directory is supplied by a registered WorkspaceProvider
|
|
47
|
+
* (the workspace seam); with no provider the child runs in the parent cwd.
|
|
51
48
|
*
|
|
52
49
|
* Phase-specific collaborators (execution, notification) are attached
|
|
53
50
|
* after construction as lifecycle information becomes available.
|
|
@@ -121,10 +118,6 @@ interface SubagentRecord {
|
|
|
121
118
|
completedAt?: number;
|
|
122
119
|
lifetimeUsage: LifetimeUsage;
|
|
123
120
|
compactionCount: number;
|
|
124
|
-
worktreeResult?: {
|
|
125
|
-
hasChanges: boolean;
|
|
126
|
-
branch?: string;
|
|
127
|
-
};
|
|
128
121
|
}
|
|
129
122
|
/** Options for spawning an agent via the service. */
|
|
130
123
|
interface SpawnOptions {
|
|
@@ -136,7 +129,6 @@ interface SpawnOptions {
|
|
|
136
129
|
inheritContext?: boolean;
|
|
137
130
|
foreground?: boolean;
|
|
138
131
|
bypassQueue?: boolean;
|
|
139
|
-
isolation?: "worktree";
|
|
140
132
|
}
|
|
141
133
|
/** The public service contract for cross-extension subagent access. */
|
|
142
134
|
interface SubagentsService {
|
|
@@ -56,9 +56,9 @@ flowchart TB
|
|
|
56
56
|
AgentManager["AgentManager<br/>(spawn, abort, collection)"]
|
|
57
57
|
ConcurrencyQueue["ConcurrencyQueue<br/>(scheduling, drain)"]
|
|
58
58
|
AgentRunner["agent-runner<br/>(session, turns, results)"]
|
|
59
|
-
Agent["Agent<br/>(status, behavior: abort/steer/
|
|
59
|
+
Agent["Agent<br/>(status, behavior: abort/steer/run lifecycle)"]
|
|
60
60
|
ParentSnapshot["ParentSnapshot<br/>(frozen parent state)"]
|
|
61
|
-
|
|
61
|
+
Workspace["workspace<br/>(provider seam: child cwd + teardown)"]
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
subgraph observation["Observation domain"]
|
|
@@ -112,7 +112,6 @@ classDiagram
|
|
|
112
112
|
+toolUses: number
|
|
113
113
|
+lifetimeUsage: LifetimeUsage
|
|
114
114
|
+execution?: ExecutionState
|
|
115
|
-
+worktree?: WorktreeIsolation
|
|
116
115
|
+notification?: NotificationState
|
|
117
116
|
+markRunning()
|
|
118
117
|
+markCompleted()
|
|
@@ -270,14 +269,12 @@ src/
|
|
|
270
269
|
├── lifecycle/ agent execution and state tracking
|
|
271
270
|
│ ├── agent-manager.ts collection manager + observer wiring
|
|
272
271
|
│ ├── agent-runner.ts session creation, turn loop, tool filtering
|
|
273
|
-
│ ├── agent.ts owns full execution lifecycle (run, abort, steer,
|
|
272
|
+
│ ├── agent.ts owns full execution lifecycle (run, abort, steer, workspace)
|
|
274
273
|
│ ├── concurrency-queue.ts background agent scheduling with configurable concurrency limit
|
|
275
274
|
│ ├── parent-snapshot.ts immutable spawn-time parent state
|
|
276
275
|
│ ├── execution-state.ts session/output phase state
|
|
277
276
|
│ ├── child-lifecycle.ts child-execution lifecycle event publisher
|
|
278
277
|
│ ├── workspace.ts workspace provider seam (generative extension surface)
|
|
279
|
-
│ ├── worktree.ts git worktree isolation
|
|
280
|
-
│ ├── worktree-isolation.ts worktree lifecycle collaborator
|
|
281
278
|
│ └── usage.ts token usage tracking
|
|
282
279
|
│
|
|
283
280
|
├── observation/ progress tracking and notification
|
|
@@ -357,14 +354,14 @@ They declare this package as an optional peer dependency and use dynamic import
|
|
|
357
354
|
Reactive consumers subscribe: `@gotgenes/pi-permission-system` registers each child session on `session-created` and unregisters it on `disposed`.
|
|
358
355
|
This replaced the former outbound `permission-bridge` (#261, ADR 0002) — the core no longer looks up a named consumer.
|
|
359
356
|
- `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
|
|
357
|
+
With no provider, children run in the parent cwd (default unchanged); the git worktree strategy lives behind this seam in `@gotgenes/pi-subagents-worktrees` (#263, the seam's first consumer).
|
|
361
358
|
- `session-config` — pure configuration assembler (extracted from `agent-runner`).
|
|
362
359
|
- `SubagentRuntime` — session-scoped state bag with methods.
|
|
363
360
|
- `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
|
|
364
361
|
- `record-observer` — session-event observer that updates record statistics without callback threading.
|
|
365
362
|
- Agent type registry — default agents, custom `.md` file loading.
|
|
366
363
|
- Prompt assembly, context extraction, skills, environment.
|
|
367
|
-
- Worktree isolation —
|
|
364
|
+
- Worktree isolation — evicted to `@gotgenes/pi-subagents-worktrees` via the workspace provider seam in Phase 16 (#263, ADR 0002); `git` no longer appears in the core.
|
|
368
365
|
- Token usage tracking.
|
|
369
366
|
- Session directory derivation and persisted `SessionManager` for subagent transcripts.
|
|
370
367
|
- Settings persistence.
|
|
@@ -418,7 +415,7 @@ Key types:
|
|
|
418
415
|
|
|
419
416
|
- `SubagentsService` — `spawn`, `getRecord`, `listAgents`, `abort`, `steer`, `waitForAll`, `hasRunning`.
|
|
420
417
|
- `SubagentRecord` — serializable agent snapshot (no live session objects).
|
|
421
|
-
- `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue
|
|
418
|
+
- `SpawnOptions` — `description`, `model`, `maxTurns`, `thinkingLevel`, `isolated`, `inheritContext`, `foreground`, `bypassQueue`.
|
|
422
419
|
- `SUBAGENT_EVENTS` — channel constants for `pi.events` subscriptions.
|
|
423
420
|
|
|
424
421
|
### Accessor pattern
|
|
@@ -501,7 +498,7 @@ Latent extensibility is the deliverable; a vacant hook is not.
|
|
|
501
498
|
- **Extension filtering** (`extensions: string[]` allowlist) — tool visibility is pi-permission-system's job.
|
|
502
499
|
- **Worktree isolation** (`worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, the `isolation: "worktree"` spawn mode) — environment policy, not core.
|
|
503
500
|
Git worktrees are one *strategy* for choosing the child's working directory; containers, throwaway tmpdirs, and remote sandboxes are others.
|
|
504
|
-
|
|
501
|
+
Evicted to `@gotgenes/pi-subagents-worktrees` (#263), the first consumer of the workspace provider seam.
|
|
505
502
|
- **Extension lifecycle control** (`extensions: false`, `isolated`, `noSkills`) — deny-at-use (the in-child permission layer blocking disallowed tool calls) covers what `isolated` pretended to do for tools.
|
|
506
503
|
Prevent-load (refusing to bind an extension because of load-time side effects, cost, or true sandboxing) is genuinely generative and is left as a *latent* (un-built) provider seam, added only if a real consumer needs it.
|
|
507
504
|
|
|
@@ -608,7 +605,6 @@ interface SpawnExecution {
|
|
|
608
605
|
inheritContext: boolean;
|
|
609
606
|
runInBackground: boolean;
|
|
610
607
|
isolated: boolean;
|
|
611
|
-
isolation: IsolationMode | undefined;
|
|
612
608
|
agentInvocation: AgentInvocation;
|
|
613
609
|
}
|
|
614
610
|
|
|
@@ -765,8 +761,8 @@ Migrate `@gotgenes/pi-permission-system` to subscribe to `session-created`/`disp
|
|
|
765
761
|
#### Step 2: Define the `WorkspaceProvider` seam — [#262] ✅ Delivered
|
|
766
762
|
|
|
767
763
|
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` (
|
|
769
|
-
At run-start `Agent.run()` consults the registered provider
|
|
764
|
+
Only `WorkspaceProvider` is named-re-exported from `service.ts`; `Workspace` and the context types resolve via inference when a consumer assigns to `WorkspaceProvider` (named re-exports of the collaborator types are tracked in #272).
|
|
765
|
+
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 (the legacy worktree-collaborator fallback was removed when worktrees left the core in #263).
|
|
770
766
|
On completion the core calls `Workspace.dispose({ status, description })` and appends the returned `resultAddendum` verbatim — the provider owns the wording.
|
|
771
767
|
|
|
772
768
|
- The seam is additive and non-breaking: the existing `isolation: "worktree"` path is untouched (its eviction is Step 3).
|
|
@@ -774,13 +770,15 @@ On completion the core calls `Workspace.dispose({ status, description })` and ap
|
|
|
774
770
|
Within #262 the seam is exercised only by test fakes; do not cut a release containing the seam without `@gotgenes/pi-subagents-worktrees`.
|
|
775
771
|
- Outcome: a single generative seam; the core no longer knows what an "isolation strategy" is.
|
|
776
772
|
|
|
777
|
-
#### Step 3: Extract worktrees to `@gotgenes/pi-subagents-worktrees` — [#263]
|
|
773
|
+
#### Step 3: Extract worktrees to `@gotgenes/pi-subagents-worktrees` — [#263] ✅ Delivered
|
|
778
774
|
|
|
779
775
|
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.
|
|
780
|
-
|
|
776
|
+
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).
|
|
777
|
+
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`.
|
|
781
778
|
|
|
782
779
|
- Supersedes #256.
|
|
783
|
-
New package registered in `release-please-config.json
|
|
780
|
+
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`), since `exports.types` resolves to the shipped declaration bundle.
|
|
781
|
+
Only `WorkspaceProvider` is importable by name from 11.6.0; the collaborator types are recovered via inference until #272 adds named re-exports.
|
|
784
782
|
- Outcome: git leaves the core; worktree users install one package, everyone else pays nothing.
|
|
785
783
|
|
|
786
784
|
#### Step 4: Remove `isolated` / `extensions: false` / `noSkills` — [#264]
|
|
@@ -54,3 +54,53 @@ Root `pnpm run check`, root `pnpm run lint`, and `verify:public-types` all pass.
|
|
|
54
54
|
- No `src/`/`test/` `.ts` files were touched, so the vitest suite and `tsc` were unaffected (confirmed via root check).
|
|
55
55
|
- Pre-completion reviewer: WARN — no findings attributable to this session.
|
|
56
56
|
Reviewer warnings: (1) the `package-pi-subagents` skill lacked a build-step note — addressed in commit `2ff5a375`; (2) `pnpm fallow dead-code` exits non-zero on a pre-existing finding in `packages/pi-subagents-worktrees/package.json` from the #263 scaffold (commit `9a7dcfc5`), out of scope for #270 and left for #263.
|
|
57
|
+
|
|
58
|
+
## Stage: Final Retrospective (2026-05-29T21:00:00Z)
|
|
59
|
+
|
|
60
|
+
### Session summary
|
|
61
|
+
|
|
62
|
+
Shipped #270 end-to-end across planning, build, and ship stages: diagnosed the cross-package type-resolution failure empirically, built a `rollup-plugin-dts` declaration bundle plus a pack-based verification harness, and published `@gotgenes/pi-subagents@11.6.0` (tag `pi-subagents-v11.6.0`).
|
|
63
|
+
Two CI failures during the ship stage — a pre-existing `pnpm fallow dead-code` gate and lockfile drift — required two extra fix commits before CI went green.
|
|
64
|
+
|
|
65
|
+
### Observations
|
|
66
|
+
|
|
67
|
+
#### What went well
|
|
68
|
+
|
|
69
|
+
- Empirical-first diagnosis: `tsc --traceResolution` in planning pinned the exact two-part failure (consumer `paths` collision + the publisher's `imports`-field extensionless-`.ts` miss) and directly justified the chosen `.d.ts`-emit approach over the alias-free restructure.
|
|
70
|
+
- The flagged primary risk evaporated: `rollup-plugin-dts` resolved `#src/*` out of the box via the package `tsconfig` paths, producing a clean 178-line `dist/public.d.ts` with no resolver plugin.
|
|
71
|
+
- Novel, reusable pattern: `scripts/verify-public-types.sh` proves a ship-source package is externally type-consumable via `pnpm pack` → throwaway-consumer → `tsc`, with no publish round-trip.
|
|
72
|
+
Worth promoting if other packages grow public surfaces.
|
|
73
|
+
- Disciplined `ask_user` use on the genuinely ambiguous decisions (approach, bundler, artifact handling, scope), with strong user steering — the `tsup`-is-unmaintained redirect to `rollup-plugin-dts`, the "no workspace trickery / use released versions" directive, and the #263 chicken-and-egg catch that correctly narrowed scope.
|
|
74
|
+
|
|
75
|
+
#### What caused friction (agent side)
|
|
76
|
+
|
|
77
|
+
- `missing-context` — Pushed to `main` with a pre-existing `pnpm fallow dead-code` failure (unused `@earendil-works/pi-coding-agent` devDependency in `packages/pi-subagents-worktrees/package.json`, from the #263 scaffold).
|
|
78
|
+
The pre-completion reviewer reported it as `FAIL` but labelled it out-of-scope, and I accepted that framing and pushed.
|
|
79
|
+
The CI `Fallow dead-code gate` runs `if: github.ref == 'refs/heads/main'` — a hard gate that fires on every `main` push regardless of who introduced the failure — so CI failed (run `26659647270`).
|
|
80
|
+
Impact: 2 fix commits (`7e7afadd`, `10e74f2f`) and 2 extra CI cycles (~10 min).
|
|
81
|
+
The ship pre-push step runs only `pnpm run lint`, never `pnpm fallow dead-code`.
|
|
82
|
+
- `missing-context` — Removed the devDependency and committed/pushed `package.json` (`7e7afadd`) without the updated `pnpm-lock.yaml`.
|
|
83
|
+
CI's `pnpm install --frozen-lockfile` failed with `ERR_PNPM_OUTDATED_LOCKFILE` (run `26659851716`).
|
|
84
|
+
Impact: 1 extra commit (`10e74f2f`) and 1 extra CI cycle.
|
|
85
|
+
Self-identified from the CI log.
|
|
86
|
+
- `rabbit-hole` (minor) — While debugging the harness's `ERR_PNPM_IGNORED_BUILDS`, the `pnpm ... | tail; echo $?` idiom reported `tail`'s exit code, masking pnpm's real failure; took ~4 tool calls before tracing with `bash -x`.
|
|
87
|
+
Impact: added friction, no rework.
|
|
88
|
+
|
|
89
|
+
#### What caused friction (user side)
|
|
90
|
+
|
|
91
|
+
- The "pi-subagents-* extensions should use the released, npm-installed version, no workspace trickery" directive arrived mid-planning, after initial exploration.
|
|
92
|
+
Surfacing the consumption-model constraint at kickoff would have framed the scope question earlier.
|
|
93
|
+
Opportunity, not criticism — the same exchange produced the high-value chicken-and-egg catch (the registry version with the fix cannot exist until #270 publishes) that correctly deferred the worktrees flip to #263.
|
|
94
|
+
- A brief "there is no ADR 0003" → "My mistake" exchange; no rework.
|
|
95
|
+
|
|
96
|
+
### Diagnostic details
|
|
97
|
+
|
|
98
|
+
- Model-performance correlation — the lone subagent dispatch (`pre-completion-reviewer`) ran on `anthropic/claude-sonnet-4-6`, appropriate for judgment-heavy review.
|
|
99
|
+
The dead-code-gate framing miss was a protocol-scope issue (pre-existing vs blocking), not a model-capability mismatch.
|
|
100
|
+
- Escalation-delay — no error sequence exceeded 5 consecutive tool calls; the harness `ERR_PNPM_IGNORED_BUILDS` resolved in ~4.
|
|
101
|
+
- Feedback-loop gap — build-stage verification ran incrementally after each step (good); the gap was at ship: the pre-push check omits the `main`-only gates (`pnpm fallow dead-code`) and lockfile validation that CI enforces, so a locally-clean `pnpm run lint` still failed CI twice.
|
|
102
|
+
|
|
103
|
+
### Changes made
|
|
104
|
+
|
|
105
|
+
1. `.pi/prompts/ship-issue.md` — renamed Step 2 to "Pre-push checks" and added `pnpm fallow dead-code` alongside `pnpm run lint`, with a one-line note that the gate is `main`-only and blocks pushes regardless of who introduced the failure.
|
|
106
|
+
2. `AGENTS.md` (§ Code Style pnpm rules) — added a rule to run `pnpm install` and commit the updated `pnpm-lock.yaml` in the same commit when a `package.json` dependency changes, since CI installs with `--frozen-lockfile`.
|
package/package.json
CHANGED
|
@@ -68,7 +68,6 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
68
68
|
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
69
69
|
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
70
70
|
isolated: fm.isolated != null ? fm.isolated === true : undefined,
|
|
71
|
-
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
72
71
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
73
72
|
source,
|
|
74
73
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentConfig,
|
|
1
|
+
import type { AgentConfig, ThinkingLevel } from "#src/types";
|
|
2
2
|
|
|
3
3
|
interface AgentInvocationParams {
|
|
4
4
|
model?: string;
|
|
@@ -7,7 +7,6 @@ interface AgentInvocationParams {
|
|
|
7
7
|
run_in_background?: boolean;
|
|
8
8
|
inherit_context?: boolean;
|
|
9
9
|
isolated?: boolean;
|
|
10
|
-
isolation?: IsolationMode;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function resolveAgentInvocationConfig(
|
|
@@ -21,7 +20,6 @@ export function resolveAgentInvocationConfig(
|
|
|
21
20
|
inheritContext: boolean;
|
|
22
21
|
runInBackground: boolean;
|
|
23
22
|
isolated: boolean;
|
|
24
|
-
isolation?: IsolationMode;
|
|
25
23
|
} {
|
|
26
24
|
return {
|
|
27
25
|
modelInput: agentConfig?.model ?? params.model,
|
|
@@ -31,6 +29,5 @@ export function resolveAgentInvocationConfig(
|
|
|
31
29
|
inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
32
30
|
runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
33
31
|
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
34
|
-
isolation: agentConfig?.isolation ?? params.isolation,
|
|
35
32
|
};
|
|
36
33
|
}
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import { ConcreteAgentRunner, type RunnerDeps } from "#src/lifecycle/agent-runne
|
|
|
28
28
|
import { createChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
|
|
29
29
|
import { ConcurrencyQueue } from "#src/lifecycle/concurrency-queue";
|
|
30
30
|
import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
31
|
-
import { GitWorktreeManager } from "#src/lifecycle/worktree";
|
|
32
31
|
import { buildEventData, type NotificationDetails, NotificationManager } from "#src/observation/notification";
|
|
33
32
|
import { createNotificationRenderer } from "#src/observation/renderer";
|
|
34
33
|
import { createSubagentRuntime } from "#src/runtime";
|
|
@@ -166,7 +165,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
166
165
|
|
|
167
166
|
const manager = new AgentManager({
|
|
168
167
|
runner: new ConcreteAgentRunner(runnerDeps),
|
|
169
|
-
worktrees: new GitWorktreeManager(process.cwd()),
|
|
170
168
|
baseCwd: process.cwd(),
|
|
171
169
|
observer,
|
|
172
170
|
queue,
|
|
@@ -14,11 +14,9 @@ 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
16
|
import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
17
|
-
import type { WorktreeManager } from "#src/lifecycle/worktree";
|
|
18
|
-
import { WorktreeIsolation } from "#src/lifecycle/worktree-isolation";
|
|
19
17
|
|
|
20
18
|
import type { RunConfig } from "#src/runtime";
|
|
21
|
-
import type { AgentInvocation, CompactionInfo,
|
|
19
|
+
import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
|
|
22
20
|
|
|
23
21
|
/** Observer interface for agent lifecycle notifications. */
|
|
24
22
|
export interface AgentManagerObserver {
|
|
@@ -31,7 +29,6 @@ export interface AgentManagerObserver {
|
|
|
31
29
|
|
|
32
30
|
export interface AgentManagerOptions {
|
|
33
31
|
runner: AgentRunner;
|
|
34
|
-
worktrees: WorktreeManager;
|
|
35
32
|
/** Concurrency queue — owns scheduling, limit checks, and drain logic. */
|
|
36
33
|
queue: ConcurrencyQueue;
|
|
37
34
|
/** Base working directory handed to a workspace provider (the parent cwd). */
|
|
@@ -54,8 +51,6 @@ export interface AgentSpawnConfig {
|
|
|
54
51
|
* callers (e.g. cross-extension RPC) that must not be deferred by the queue.
|
|
55
52
|
*/
|
|
56
53
|
bypassQueue?: boolean;
|
|
57
|
-
/** Isolation mode - "worktree" creates a temp git worktree for the agent. */
|
|
58
|
-
isolation?: IsolationMode;
|
|
59
54
|
/** Resolved invocation snapshot captured for UI display. */
|
|
60
55
|
invocation?: AgentInvocation;
|
|
61
56
|
/** Parent abort signal - when aborted, the subagent is also stopped. */
|
|
@@ -71,7 +66,6 @@ export class AgentManager {
|
|
|
71
66
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
72
67
|
private readonly observer?: AgentManagerObserver;
|
|
73
68
|
private readonly runner: AgentRunner;
|
|
74
|
-
private readonly worktrees: WorktreeManager;
|
|
75
69
|
private readonly queue: ConcurrencyQueue;
|
|
76
70
|
private readonly baseCwd: string;
|
|
77
71
|
private getRunConfig?: () => RunConfig;
|
|
@@ -84,7 +78,6 @@ export class AgentManager {
|
|
|
84
78
|
|
|
85
79
|
constructor(options: AgentManagerOptions) {
|
|
86
80
|
this.runner = options.runner;
|
|
87
|
-
this.worktrees = options.worktrees;
|
|
88
81
|
this.queue = options.queue;
|
|
89
82
|
this.baseCwd = options.baseCwd;
|
|
90
83
|
this.observer = options.observer;
|
|
@@ -162,10 +155,6 @@ export class AgentManager {
|
|
|
162
155
|
signal: options.signal,
|
|
163
156
|
// Shared deps
|
|
164
157
|
runner: this.runner,
|
|
165
|
-
worktree:
|
|
166
|
-
options.isolation === "worktree"
|
|
167
|
-
? new WorktreeIsolation(this.worktrees, id)
|
|
168
|
-
: undefined,
|
|
169
158
|
observer: this.buildObserver(options),
|
|
170
159
|
getRunConfig: this.getRunConfig,
|
|
171
160
|
baseCwd: this.baseCwd,
|
|
@@ -322,7 +311,5 @@ export class AgentManager {
|
|
|
322
311
|
record.session?.dispose();
|
|
323
312
|
}
|
|
324
313
|
this.agents.clear();
|
|
325
|
-
// Prune any orphaned git worktrees (crash recovery)
|
|
326
|
-
try { this.worktrees.prune(); } catch (err) { debugLog("pruneWorktrees on dispose", err); }
|
|
327
314
|
}
|
|
328
315
|
}
|
package/src/lifecycle/agent.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* Stats (toolUses, lifetimeUsage, compactionCount) are owned by the class and
|
|
9
9
|
* accumulated via mutation methods (incrementToolUses, addUsage, incrementCompactions).
|
|
10
10
|
*
|
|
11
|
-
* Behavior (abort, steer buffering
|
|
12
|
-
*
|
|
11
|
+
* Behavior (abort, steer buffering) lives on the agent rather than on
|
|
12
|
+
* AgentManager — each agent manages its own lifecycle concerns.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* (
|
|
14
|
+
* The child's working directory is supplied by a registered WorkspaceProvider
|
|
15
|
+
* (the workspace seam); with no provider the child runs in the parent cwd.
|
|
16
16
|
*
|
|
17
17
|
* Phase-specific collaborators (execution, notification) are attached
|
|
18
18
|
* after construction as lifecycle information becomes available.
|
|
@@ -27,7 +27,6 @@ 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
29
|
import type { Workspace, WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
30
|
-
import type { WorktreeIsolation } from "#src/lifecycle/worktree-isolation";
|
|
31
30
|
import { NotificationState } from "#src/observation/notification-state";
|
|
32
31
|
import { subscribeAgentObserver } from "#src/observation/record-observer";
|
|
33
32
|
import type { RunConfig } from "#src/runtime";
|
|
@@ -70,7 +69,6 @@ export interface AgentInit {
|
|
|
70
69
|
|
|
71
70
|
// Shared deps (required for run(), optional for tests)
|
|
72
71
|
runner?: AgentRunner;
|
|
73
|
-
worktree?: WorktreeIsolation;
|
|
74
72
|
observer?: AgentLifecycleObserver;
|
|
75
73
|
getRunConfig?: () => RunConfig;
|
|
76
74
|
/** Resolves the registered workspace provider (if any) at run-start. */
|
|
@@ -130,8 +128,6 @@ export class Agent {
|
|
|
130
128
|
|
|
131
129
|
// Shared deps — optional (required for run())
|
|
132
130
|
private readonly _runner?: AgentRunner;
|
|
133
|
-
/** Worktree isolation collaborator — present only when isolation: "worktree". */
|
|
134
|
-
readonly worktree?: WorktreeIsolation;
|
|
135
131
|
readonly observer?: AgentLifecycleObserver;
|
|
136
132
|
private readonly _getRunConfig?: () => RunConfig;
|
|
137
133
|
private readonly _getWorkspaceProvider?: () => WorkspaceProvider | undefined;
|
|
@@ -192,7 +188,6 @@ export class Agent {
|
|
|
192
188
|
|
|
193
189
|
// Shared deps
|
|
194
190
|
this._runner = init.runner;
|
|
195
|
-
this.worktree = init.worktree;
|
|
196
191
|
this.observer = init.observer;
|
|
197
192
|
this._getRunConfig = init.getRunConfig;
|
|
198
193
|
this._getWorkspaceProvider = init.getWorkspaceProvider;
|
|
@@ -215,8 +210,8 @@ export class Agent {
|
|
|
215
210
|
}
|
|
216
211
|
|
|
217
212
|
/**
|
|
218
|
-
* Execute the full agent lifecycle:
|
|
219
|
-
* session-creation handling, observer wiring,
|
|
213
|
+
* Execute the full agent lifecycle: workspace preparation, runner invocation,
|
|
214
|
+
* session-creation handling, observer wiring, workspace disposal, and
|
|
220
215
|
* status transitions.
|
|
221
216
|
*
|
|
222
217
|
* Requires runner and snapshot to be set at construction.
|
|
@@ -236,8 +231,8 @@ export class Agent {
|
|
|
236
231
|
|
|
237
232
|
let cwd: string | undefined;
|
|
238
233
|
try {
|
|
239
|
-
//
|
|
240
|
-
//
|
|
234
|
+
// A registered workspace provider supplies the child's cwd and owns its
|
|
235
|
+
// teardown; with no provider the child runs in the parent cwd.
|
|
241
236
|
const provider = this._getWorkspaceProvider?.();
|
|
242
237
|
if (provider) {
|
|
243
238
|
this._workspace = await provider.prepare({
|
|
@@ -247,9 +242,6 @@ export class Agent {
|
|
|
247
242
|
invocation: this.invocation,
|
|
248
243
|
});
|
|
249
244
|
cwd = this._workspace?.cwd;
|
|
250
|
-
} else {
|
|
251
|
-
this.worktree?.setup();
|
|
252
|
-
cwd = this.worktree?.path;
|
|
253
245
|
}
|
|
254
246
|
} catch (err) {
|
|
255
247
|
this.markError(err);
|
|
@@ -463,7 +455,7 @@ export class Agent {
|
|
|
463
455
|
this._detachFn = undefined;
|
|
464
456
|
}
|
|
465
457
|
|
|
466
|
-
/** Complete a run: release listeners,
|
|
458
|
+
/** Complete a run: release listeners, dispose the workspace, status transition, execution update, notify observer. */
|
|
467
459
|
completeRun(result: RunResult): void {
|
|
468
460
|
this.releaseListeners();
|
|
469
461
|
|
|
@@ -476,11 +468,6 @@ export class Agent {
|
|
|
476
468
|
: "completed";
|
|
477
469
|
const disposeResult = this._workspace.dispose({ status: finalStatus, description: this.description });
|
|
478
470
|
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
|
-
}
|
|
484
471
|
}
|
|
485
472
|
|
|
486
473
|
if (result.aborted) this.markAborted(finalResult);
|
|
@@ -495,14 +482,13 @@ export class Agent {
|
|
|
495
482
|
this.observer?.onRunFinished?.(this);
|
|
496
483
|
}
|
|
497
484
|
|
|
498
|
-
/** Fail a run: mark error, release listeners, best-effort
|
|
485
|
+
/** Fail a run: mark error, release listeners, best-effort workspace dispose, notify observer. */
|
|
499
486
|
failRun(err: unknown): void {
|
|
500
487
|
this.markError(err);
|
|
501
488
|
this.releaseListeners();
|
|
502
489
|
|
|
503
490
|
try {
|
|
504
491
|
if (this._workspace) this._workspace.dispose({ status: "error", description: this.description });
|
|
505
|
-
else this.worktree?.cleanup(this.description);
|
|
506
492
|
} catch (cleanupErr) { debugLog("workspace dispose on agent error", cleanupErr); }
|
|
507
493
|
|
|
508
494
|
this.observer?.onRunFinished?.(this);
|
|
@@ -69,7 +69,6 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
69
69
|
isolated: options?.isolated,
|
|
70
70
|
inheritContext: options?.inheritContext,
|
|
71
71
|
bypassQueue: options?.bypassQueue,
|
|
72
|
-
isolation: options?.isolation,
|
|
73
72
|
isBackground,
|
|
74
73
|
});
|
|
75
74
|
}
|
|
@@ -134,8 +133,6 @@ export function toSubagentRecord(record: Agent): SubagentRecord {
|
|
|
134
133
|
if (record.result !== undefined) out.result = record.result;
|
|
135
134
|
if (record.error !== undefined) out.error = record.error;
|
|
136
135
|
if (record.completedAt !== undefined) out.completedAt = record.completedAt;
|
|
137
|
-
const worktreeResult = record.worktree?.cleanupResult;
|
|
138
|
-
if (worktreeResult !== undefined) out.worktreeResult = worktreeResult;
|
|
139
136
|
|
|
140
137
|
return out;
|
|
141
138
|
}
|
package/src/service/service.ts
CHANGED
|
@@ -14,8 +14,9 @@ import type { WorkspaceProvider } from "#src/lifecycle/workspace";
|
|
|
14
14
|
|
|
15
15
|
// Generative extension seam (ADR 0002, Phase 16 Step 2). Only the provider
|
|
16
16
|
// entry-point type is re-exported here; a consumer assigning to
|
|
17
|
-
// `WorkspaceProvider`
|
|
18
|
-
//
|
|
17
|
+
// `WorkspaceProvider` recovers `Workspace` and the context types via inference
|
|
18
|
+
// (e.g. `Parameters<WorkspaceProvider["prepare"]>[0]`). Named re-exports of
|
|
19
|
+
// those collaborator types are tracked in #272.
|
|
19
20
|
export type { LifetimeUsage, WorkspaceProvider };
|
|
20
21
|
|
|
21
22
|
export type SubagentStatus =
|
|
@@ -40,7 +41,6 @@ export interface SubagentRecord {
|
|
|
40
41
|
completedAt?: number;
|
|
41
42
|
lifetimeUsage: LifetimeUsage;
|
|
42
43
|
compactionCount: number;
|
|
43
|
-
worktreeResult?: { hasChanges: boolean; branch?: string };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Options for spawning an agent via the service. */
|
|
@@ -53,7 +53,6 @@ export interface SpawnOptions {
|
|
|
53
53
|
inheritContext?: boolean;
|
|
54
54
|
foreground?: boolean;
|
|
55
55
|
bypassQueue?: boolean;
|
|
56
|
-
isolation?: "worktree";
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
/** The public service contract for cross-extension subagent access. */
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -179,7 +179,7 @@ Guidelines:
|
|
|
179
179
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
180
180
|
- Use thinking to control extended thinking level.
|
|
181
181
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
182
|
-
|
|
182
|
+
`,
|
|
183
183
|
parameters: Type.Object({
|
|
184
184
|
prompt: Type.String({
|
|
185
185
|
description: "The task for the agent to perform.",
|
|
@@ -231,12 +231,6 @@ Guidelines:
|
|
|
231
231
|
"If true, fork parent conversation into the agent. Default: false (fresh context).",
|
|
232
232
|
}),
|
|
233
233
|
),
|
|
234
|
-
isolation: Type.Optional(
|
|
235
|
-
Type.Literal("worktree", {
|
|
236
|
-
description:
|
|
237
|
-
'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
238
|
-
}),
|
|
239
|
-
),
|
|
240
234
|
}),
|
|
241
235
|
|
|
242
236
|
// ---- Custom rendering: Claude Code style ----
|
|
@@ -52,7 +52,6 @@ export function spawnBackground(
|
|
|
52
52
|
inheritContext: execution.inheritContext,
|
|
53
53
|
thinkingLevel: execution.thinking,
|
|
54
54
|
isBackground: true,
|
|
55
|
-
isolation: execution.isolation,
|
|
56
55
|
invocation: execution.agentInvocation,
|
|
57
56
|
observer: {
|
|
58
57
|
onSessionCreated: (_agent, session) => {
|
|
@@ -105,7 +105,6 @@ export async function runForeground(
|
|
|
105
105
|
isolated: execution.isolated,
|
|
106
106
|
inheritContext: execution.inheritContext,
|
|
107
107
|
thinkingLevel: execution.thinking,
|
|
108
|
-
isolation: execution.isolation,
|
|
109
108
|
invocation: execution.agentInvocation,
|
|
110
109
|
signal,
|
|
111
110
|
parentSession: params.parentSession,
|
|
@@ -12,7 +12,7 @@ import type { AgentTypeRegistry } from "#src/config/agent-types";
|
|
|
12
12
|
import { resolveAgentInvocationConfig } from "#src/config/invocation-config";
|
|
13
13
|
import { normalizeMaxTurns } from "#src/lifecycle/agent-runner";
|
|
14
14
|
import { resolveInvocationModel } from "#src/session/model-resolver";
|
|
15
|
-
import type { AgentInvocation,
|
|
15
|
+
import type { AgentInvocation, SubagentType, ThinkingLevel } from "#src/types";
|
|
16
16
|
import {
|
|
17
17
|
type AgentDetails,
|
|
18
18
|
buildInvocationTags,
|
|
@@ -44,7 +44,6 @@ export interface SpawnExecution {
|
|
|
44
44
|
inheritContext: boolean;
|
|
45
45
|
runInBackground: boolean;
|
|
46
46
|
isolated: boolean;
|
|
47
|
-
isolation: IsolationMode | undefined;
|
|
48
47
|
agentInvocation: AgentInvocation;
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -104,7 +103,6 @@ export function resolveSpawnConfig(
|
|
|
104
103
|
const inheritContext = resolvedConfig.inheritContext;
|
|
105
104
|
const runInBackground = resolvedConfig.runInBackground;
|
|
106
105
|
const isolated = resolvedConfig.isolated;
|
|
107
|
-
const isolation = resolvedConfig.isolation;
|
|
108
106
|
|
|
109
107
|
// Compute display model name (only shown when different from parent)
|
|
110
108
|
const parentModelId = modelInfo.parentModel?.id;
|
|
@@ -125,7 +123,6 @@ export function resolveSpawnConfig(
|
|
|
125
123
|
isolated,
|
|
126
124
|
inheritContext,
|
|
127
125
|
runInBackground,
|
|
128
|
-
isolation,
|
|
129
126
|
};
|
|
130
127
|
|
|
131
128
|
const modeLabel = getPromptModeLabel(subagentType, registry);
|
|
@@ -151,7 +148,6 @@ export function resolveSpawnConfig(
|
|
|
151
148
|
inheritContext,
|
|
152
149
|
runInBackground,
|
|
153
150
|
isolated,
|
|
154
|
-
isolation,
|
|
155
151
|
agentInvocation,
|
|
156
152
|
},
|
|
157
153
|
presentation: { modelName, agentTags, detailBase },
|
package/src/types.ts
CHANGED
|
@@ -21,9 +21,6 @@ export interface SubscribableSession {
|
|
|
21
21
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
22
22
|
export type SubagentType = string;
|
|
23
23
|
|
|
24
|
-
/** Isolation mode for agent execution. */
|
|
25
|
-
export type IsolationMode = "worktree";
|
|
26
|
-
|
|
27
24
|
/** UI display and agent listing — name, display name, description, prompt mode. */
|
|
28
25
|
export interface AgentIdentity {
|
|
29
26
|
name: string;
|
|
@@ -55,8 +52,6 @@ export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
|
55
52
|
runInBackground?: boolean;
|
|
56
53
|
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
57
54
|
isolated?: boolean;
|
|
58
|
-
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
59
|
-
isolation?: IsolationMode;
|
|
60
55
|
/** true = this is an embedded default agent (informational) */
|
|
61
56
|
isDefault?: boolean;
|
|
62
57
|
/** false = agent is hidden from the registry */
|
|
@@ -73,7 +68,6 @@ export interface AgentInvocation {
|
|
|
73
68
|
isolated?: boolean;
|
|
74
69
|
inheritContext?: boolean;
|
|
75
70
|
runInBackground?: boolean;
|
|
76
|
-
isolation?: IsolationMode;
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
/**
|
|
@@ -54,7 +54,6 @@ export function buildEjectContent(cfg: AgentConfig): string {
|
|
|
54
54
|
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
55
55
|
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
56
56
|
if (cfg.isolated) fmFields.push("isolated: true");
|
|
57
|
-
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
58
57
|
return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -109,7 +109,6 @@ skills: <true (inherit all), false (none), or comma-separated skill names to pre
|
|
|
109
109
|
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
110
110
|
run_in_background: <true to run in background by default. Default: false>
|
|
111
111
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
112
|
-
isolation: <"worktree" to run in isolated git worktree. Omit for normal>
|
|
113
112
|
---
|
|
114
113
|
|
|
115
114
|
<system prompt body — instructions for the agent>
|
package/src/ui/display.ts
CHANGED
|
@@ -136,7 +136,6 @@ export function buildInvocationTags(
|
|
|
136
136
|
if (!invocation) return { tags };
|
|
137
137
|
if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
|
|
138
138
|
if (invocation.isolated) tags.push("isolated");
|
|
139
|
-
if (invocation.isolation === "worktree") tags.push("worktree");
|
|
140
139
|
if (invocation.inheritContext) tags.push("inherit context");
|
|
141
140
|
if (invocation.runInBackground) tags.push("background");
|
|
142
141
|
if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* worktree-isolation.ts — WorktreeIsolation: collaborator that owns the
|
|
3
|
-
* git-worktree lifecycle for an isolated agent.
|
|
4
|
-
*
|
|
5
|
-
* Constructed by AgentManager only when isolation === "worktree", bound to a
|
|
6
|
-
* WorktreeManager and the agent id. Agent tells it `setup()` and
|
|
7
|
-
* `cleanup(description)` instead of managing worktree internals itself.
|
|
8
|
-
*
|
|
9
|
-
* The presence/absence of this collaborator IS the isolation mode: Agent calls
|
|
10
|
-
* `this.worktree?.setup()` rather than checking an isolation string.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { WorktreeCleanupResult, WorktreeInfo, WorktreeManager } from "#src/lifecycle/worktree";
|
|
14
|
-
|
|
15
|
-
export class WorktreeIsolation {
|
|
16
|
-
private _info?: WorktreeInfo;
|
|
17
|
-
private _cleanupResult?: WorktreeCleanupResult;
|
|
18
|
-
|
|
19
|
-
constructor(
|
|
20
|
-
private readonly worktrees: WorktreeManager,
|
|
21
|
-
private readonly agentId: string,
|
|
22
|
-
) {}
|
|
23
|
-
|
|
24
|
-
/** Absolute worktree path — undefined before setup(). */
|
|
25
|
-
get path(): string | undefined {
|
|
26
|
-
return this._info?.path;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Cleanup outcome — undefined until cleanup() runs. */
|
|
30
|
-
get cleanupResult(): WorktreeCleanupResult | undefined {
|
|
31
|
-
return this._cleanupResult;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Create the git worktree and store its info.
|
|
36
|
-
* Throws on failure (strict isolation — no silent fallback).
|
|
37
|
-
*/
|
|
38
|
-
setup(): void {
|
|
39
|
-
const wt = this.worktrees.create(this.agentId);
|
|
40
|
-
if (!wt) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
43
|
-
"Initialize git and commit at least once, or omit `isolation`.",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
this._info = wt;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Perform worktree cleanup and record the result.
|
|
51
|
-
* No-op returning { hasChanges: false } if setup never ran.
|
|
52
|
-
*/
|
|
53
|
-
cleanup(description: string): WorktreeCleanupResult {
|
|
54
|
-
if (!this._info) return { hasChanges: false };
|
|
55
|
-
const result = this.worktrees.cleanup(this._info, description);
|
|
56
|
-
this._cleanupResult = result;
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* worktree.ts — Git worktree isolation for agents.
|
|
3
|
-
*
|
|
4
|
-
* Creates a temporary git worktree so the agent works on an isolated copy of the repo.
|
|
5
|
-
* On completion, if no changes were made, the worktree is cleaned up.
|
|
6
|
-
* If changes exist, a branch is created and returned in the result.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execFileSync } from "node:child_process";
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { debugLog } from "#src/debug";
|
|
15
|
-
|
|
16
|
-
export interface WorktreeInfo {
|
|
17
|
-
/** Absolute path to the worktree directory. */
|
|
18
|
-
path: string;
|
|
19
|
-
/** Branch name created for this worktree (if changes exist). */
|
|
20
|
-
branch: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface WorktreeCleanupResult {
|
|
24
|
-
/** Whether changes were found in the worktree. */
|
|
25
|
-
hasChanges: boolean;
|
|
26
|
-
/** Branch name if changes were committed. */
|
|
27
|
-
branch?: string;
|
|
28
|
-
/** Worktree path if it was kept. */
|
|
29
|
-
path?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Create a temporary git worktree for an agent.
|
|
34
|
-
* Returns the worktree path, or undefined if not in a git repo.
|
|
35
|
-
*/
|
|
36
|
-
export function createWorktree(cwd: string, agentId: string): WorktreeInfo | undefined {
|
|
37
|
-
// Verify we're in a git repo with at least one commit (HEAD must exist)
|
|
38
|
-
try {
|
|
39
|
-
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
40
|
-
execFileSync("git", ["rev-parse", "HEAD"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
41
|
-
} catch (err) {
|
|
42
|
-
debugLog("createWorktree git rev-parse", err);
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const branch = `pi-agent-${agentId}`;
|
|
47
|
-
const suffix = randomUUID().slice(0, 8);
|
|
48
|
-
const worktreePath = join(tmpdir(), `pi-agent-${agentId}-${suffix}`);
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
// Create detached worktree at HEAD
|
|
52
|
-
execFileSync("git", ["worktree", "add", "--detach", worktreePath, "HEAD"], {
|
|
53
|
-
cwd,
|
|
54
|
-
stdio: "pipe",
|
|
55
|
-
timeout: 30000,
|
|
56
|
-
});
|
|
57
|
-
return { path: worktreePath, branch };
|
|
58
|
-
} catch (err) {
|
|
59
|
-
debugLog("git worktree add", err);
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Clean up a worktree after agent completion.
|
|
66
|
-
* - If no changes: remove worktree entirely.
|
|
67
|
-
* - If changes exist: create a branch, commit changes, return branch info.
|
|
68
|
-
*/
|
|
69
|
-
export function cleanupWorktree(
|
|
70
|
-
cwd: string,
|
|
71
|
-
worktree: WorktreeInfo,
|
|
72
|
-
agentDescription: string,
|
|
73
|
-
): WorktreeCleanupResult {
|
|
74
|
-
if (!existsSync(worktree.path)) {
|
|
75
|
-
return { hasChanges: false };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
// Check for uncommitted changes in the worktree
|
|
80
|
-
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
81
|
-
cwd: worktree.path,
|
|
82
|
-
stdio: "pipe",
|
|
83
|
-
timeout: 10000,
|
|
84
|
-
}).toString().trim();
|
|
85
|
-
|
|
86
|
-
if (!status) {
|
|
87
|
-
// No changes — remove worktree
|
|
88
|
-
removeWorktree(cwd, worktree.path);
|
|
89
|
-
return { hasChanges: false };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Changes exist — stage, commit, and create a branch
|
|
93
|
-
execFileSync("git", ["add", "-A"], { cwd: worktree.path, stdio: "pipe", timeout: 10000 });
|
|
94
|
-
// Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
|
|
95
|
-
const safeDesc = agentDescription.slice(0, 200);
|
|
96
|
-
const commitMsg = `pi-agent: ${safeDesc}`;
|
|
97
|
-
execFileSync("git", ["commit", "-m", commitMsg], {
|
|
98
|
-
cwd: worktree.path,
|
|
99
|
-
stdio: "pipe",
|
|
100
|
-
timeout: 10000,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Create a branch pointing to the worktree's HEAD.
|
|
104
|
-
// If the branch already exists, append a suffix to avoid overwriting previous work.
|
|
105
|
-
let branchName = worktree.branch;
|
|
106
|
-
try {
|
|
107
|
-
execFileSync("git", ["branch", branchName], {
|
|
108
|
-
cwd: worktree.path,
|
|
109
|
-
stdio: "pipe",
|
|
110
|
-
timeout: 5000,
|
|
111
|
-
});
|
|
112
|
-
} catch (err) {
|
|
113
|
-
debugLog("git branch", err);
|
|
114
|
-
branchName = `${worktree.branch}-${Date.now()}`;
|
|
115
|
-
execFileSync("git", ["branch", branchName], {
|
|
116
|
-
cwd: worktree.path,
|
|
117
|
-
stdio: "pipe",
|
|
118
|
-
timeout: 5000,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
// Update branch name in worktree info for the caller
|
|
122
|
-
worktree.branch = branchName;
|
|
123
|
-
|
|
124
|
-
// Remove the worktree (branch persists in main repo)
|
|
125
|
-
removeWorktree(cwd, worktree.path);
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
hasChanges: true,
|
|
129
|
-
branch: worktree.branch,
|
|
130
|
-
path: worktree.path,
|
|
131
|
-
};
|
|
132
|
-
} catch (err) {
|
|
133
|
-
debugLog("cleanupWorktree", err);
|
|
134
|
-
try { removeWorktree(cwd, worktree.path); } catch (removeErr) { debugLog("removeWorktree on cleanup error", removeErr); }
|
|
135
|
-
return { hasChanges: false };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Force-remove a worktree.
|
|
141
|
-
*/
|
|
142
|
-
function removeWorktree(cwd: string, worktreePath: string): void {
|
|
143
|
-
try {
|
|
144
|
-
execFileSync("git", ["worktree", "remove", "--force", worktreePath], {
|
|
145
|
-
cwd,
|
|
146
|
-
stdio: "pipe",
|
|
147
|
-
timeout: 10000,
|
|
148
|
-
});
|
|
149
|
-
} catch (err) {
|
|
150
|
-
debugLog("git worktree remove", err);
|
|
151
|
-
try {
|
|
152
|
-
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
153
|
-
} catch (pruneErr) { debugLog("git worktree prune", pruneErr); }
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Prune any orphaned worktrees (crash recovery).
|
|
159
|
-
*/
|
|
160
|
-
export function pruneWorktrees(cwd: string): void {
|
|
161
|
-
try {
|
|
162
|
-
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
163
|
-
} catch (err) { debugLog("pruneWorktrees", err); }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Interface for managing git worktrees relative to a fixed repository root.
|
|
168
|
-
* Callers do not thread `cwd` per call — it is captured at construction time.
|
|
169
|
-
*/
|
|
170
|
-
export interface WorktreeManager {
|
|
171
|
-
create(id: string): WorktreeInfo | undefined;
|
|
172
|
-
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult;
|
|
173
|
-
prune(): void;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Concrete implementation of WorktreeManager backed by the free functions in this module.
|
|
178
|
-
* Captures `cwd` (the repository root) at construction and delegates each method.
|
|
179
|
-
*/
|
|
180
|
-
export class GitWorktreeManager implements WorktreeManager {
|
|
181
|
-
constructor(private readonly cwd: string) {}
|
|
182
|
-
|
|
183
|
-
create(id: string): WorktreeInfo | undefined {
|
|
184
|
-
return createWorktree(this.cwd, id);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult {
|
|
188
|
-
return cleanupWorktree(this.cwd, wt, description);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
prune(): void {
|
|
192
|
-
pruneWorktrees(this.cwd);
|
|
193
|
-
}
|
|
194
|
-
}
|