@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 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, worktree setup) lives on the agent
47
- * rather than on AgentManager — each agent manages its own lifecycle concerns.
43
+ * Behavior (abort, steer buffering) lives on the agent rather than on
44
+ * AgentManager — each agent manages its own lifecycle concerns.
48
45
  *
49
- * Worktree isolation is delegated to an optional WorktreeIsolation collaborator
50
- * (set at construction when isolation is requested); its presence IS the mode.
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/worktree/run lifecycle)"]
59
+ Agent["Agent<br/>(status, behavior: abort/steer/run lifecycle)"]
60
60
  ParentSnapshot["ParentSnapshot<br/>(frozen parent state)"]
61
- Worktree["worktree<br/>(git isolation)"]
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, worktree)
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 moves behind this seam in #263.
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 — moving to `@gotgenes/pi-subagents-worktrees` via the workspace provider seam in Phase 16 (ADR 0002).
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`, `isolation`.
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
- These move to `@gotgenes/pi-subagents-worktrees`, the first consumer of the workspace provider seam.
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` (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.
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
- Remove `worktree.ts`, `worktree-isolation.ts`, `GitWorktreeManager`, and the `isolation: "worktree"` mode from the core; drop `isolation` from the spawn API and `SubagentsService`.
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`; peer-depends on `@gotgenes/pi-subagents`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "11.6.0",
3
+ "version": "12.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -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, IsolationMode, ThinkingLevel } from "#src/types";
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, IsolationMode, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
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
  }
@@ -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, worktree setup) lives on the agent
12
- * rather than on AgentManager — each agent manages its own lifecycle concerns.
11
+ * Behavior (abort, steer buffering) lives on the agent rather than on
12
+ * AgentManager — each agent manages its own lifecycle concerns.
13
13
  *
14
- * Worktree isolation is delegated to an optional WorktreeIsolation collaborator
15
- * (set at construction when isolation is requested); its presence IS the mode.
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: worktree setup, runner invocation,
219
- * session-creation handling, observer wiring, worktree cleanup, and
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
- // Provider-first: a registered workspace provider supplies the cwd and
240
- // owns teardown; otherwise fall back to the legacy worktree collaborator.
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, worktree cleanup, status transition, execution update, notify observer. */
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 worktree cleanup, notify observer. */
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
  }
@@ -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` gets `Workspace` and the context types via inference.
18
- // The worktrees package (#263) adds named re-exports when it imports them.
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. */
@@ -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
- - Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).`,
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, IsolationMode, SubagentType, ThinkingLevel } from "#src/types";
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
- }