@gotgenes/pi-subagents 11.5.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 +20 -0
- package/dist/public.d.ts +170 -0
- package/docs/architecture/architecture.md +14 -16
- package/docs/decisions/0003-publish-bundled-type-declarations.md +69 -0
- package/docs/plans/0270-type-consumable-public-surface.md +202 -0
- package/docs/retro/0262-add-workspace-provider-seam.md +43 -0
- package/docs/retro/0270-type-consumable-public-surface.md +106 -0
- package/package.json +18 -2
- 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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 270
|
|
3
|
+
issue_title: "Make @gotgenes/pi-subagents type-consumable by sibling workspace packages"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #270 — Make @gotgenes/pi-subagents type-consumable by sibling workspace packages
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-29T00:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Diagnosed the consumability failure empirically with `tsc --traceResolution` and planned a `.d.ts`-emit fix.
|
|
13
|
+
The plan adds a `rollup-plugin-dts` build that bundles `src/service/service.ts` into a self-contained `dist/public.d.ts`, wires conditional `exports` (`types` → the bundle, `default` → the real source), generates the artifact at `prepack` time, ships it via a `files` allowlist, and proves external consumability with a `pnpm pack` → throwaway-consumer → `tsc` harness.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- Root cause is two compounding failures: the stale `exports["."]` path (`./src/service.ts` does not exist) and, once fixed, an unresolvable `#src/*` cascade.
|
|
18
|
+
The trace showed the consumer's own `paths` (`#src/*` → `./src/*`) intercept first (both packages define `#src/*`), and the publisher's `imports`-field fallback cannot resolve the extensionless `.ts` target.
|
|
19
|
+
- The public type closure is entangled: `WorkspaceProvider` → `AgentStatus` (in the 510-LOC `agent.ts`) → `types.ts` (which re-exports the `Agent` class).
|
|
20
|
+
This made the alias-free-entry alternative (Option 2) a substantial source restructure, so it was rejected.
|
|
21
|
+
- Decisions taken via `ask_user`:
|
|
22
|
+
1. Approach — emit a bundled `.d.ts` (the repo's first build step), over alias-free restructure or type re-declaration.
|
|
23
|
+
2. Bundler — `rollup-plugin-dts` (purpose-built for flattening declarations; no JS bundle, which suits ship-source), over `tsdown`/`api-extractor`.
|
|
24
|
+
3. Artifact — not committed; generated at `prepack` and shipped in the tarball, consumed via the package interface.
|
|
25
|
+
4. Scope — tight: packaging + a `pnpm pack`-based verification harness in #270; defer the `pi-subagents-worktrees` registry-consumption flip (drop `workspace:*`, `link-workspace-packages: false`, wire the real import) to #263.
|
|
26
|
+
- Scope was deliberately narrowed after a chicken-and-egg surfaced: the registry version carrying the fix does not exist until #270 publishes, so the meaningful consumer flip belongs to #263.
|
|
27
|
+
- Sequencing constraint for #263 (captured in the plan): publish #270 first — merge its release-please PR so `pi-subagents` publishes — *before* resuming #263, otherwise #263's `pi-subagents` core edits batch into the same release.
|
|
28
|
+
The current `#263` scaffold commits on `main` touch only the unregistered `pi-subagents-worktrees` component, so they do not batch into `pi-subagents` and #270 ships cleanly.
|
|
29
|
+
- Primary feasibility risk flagged: whether `rollup-plugin-dts` resolves `#src/*` while rolling up the type graph.
|
|
30
|
+
Build Step 1 is the explicit checkpoint (emit + assert the output is alias-free and exports the expected symbols).
|
|
31
|
+
- `dist/` is gitignored and already excluded by eslint/biome; the new wrinkle is that a `files` allowlist is required so the gitignored `dist/public.d.ts` is included in the npm tarball — validate `pnpm pack --dry-run` parity so no currently-shipped file is dropped.
|
|
32
|
+
|
|
33
|
+
## Stage: Implementation — Build (2026-05-29T00:00:00Z)
|
|
34
|
+
|
|
35
|
+
### Session summary
|
|
36
|
+
|
|
37
|
+
Executed all four build-order steps: added `rollup` + `rollup-plugin-dts` and a `build:types` script that bundles `src/service/service.ts` into a self-contained `dist/public.d.ts`; wired conditional `exports` (`types` + `default`, fixing the stale path) with a `prepack` hook and a `files` allowlist; added a `pnpm pack` → throwaway-consumer → `tsc` verification harness (`scripts/verify-public-types.sh`) plus a CI step; and recorded ADR 0003.
|
|
38
|
+
A fifth commit documented the new build step in the `package-pi-subagents` skill (reviewer WARN).
|
|
39
|
+
Root `pnpm run check`, root `pnpm run lint`, and `verify:public-types` all pass.
|
|
40
|
+
|
|
41
|
+
### Observations
|
|
42
|
+
|
|
43
|
+
- The primary feasibility risk (`rollup-plugin-dts` resolving `#src/*`) resolved cleanly out of the box: driving it with the package `tsconfig` (which carries the `#src/*` paths) produced a 178-line `dist/public.d.ts` with zero `#src/` residue and only `ThinkingLevel` kept external from `@earendil-works/pi-ai`.
|
|
44
|
+
No alias/path resolver plugin was needed.
|
|
45
|
+
- Harness deviation (fixed in the same step): `pnpm add` in the isolated (`--ignore-workspace`) throwaway consumer exited non-zero with `ERR_PNPM_IGNORED_BUILDS` because it does not inherit the workspace `allowBuilds` approvals (`@google/genai`, `protobufjs`).
|
|
46
|
+
Fixed by adding `--ignore-scripts` — a type-check needs no dependency build scripts.
|
|
47
|
+
Worth remembering for any future packaged-consumer harness.
|
|
48
|
+
- A subtle gotcha while debugging: `pnpm ... | tail; echo $?` reports `tail`'s exit, not pnpm's, which masked the real failure.
|
|
49
|
+
Use `set -o pipefail` or check the command directly.
|
|
50
|
+
- `files` allowlist parity was validated with a before/after `pnpm pack --dry-run` diff: nothing dropped, only `dist/public.d.ts` added.
|
|
51
|
+
The allowlist reproduces the current contents (`src`, `docs`, `vitest.config.ts`, `AGENTS.md`, `CHANGELOG.md`, `.prettierignore`) plus `dist`.
|
|
52
|
+
Did not take the opportunity to slim the tarball (docs/test-config still ship) — that would be a separate deliberate change.
|
|
53
|
+
- Runtime `default` → `./src/service/service.ts` is safe because that module's only internal imports are `import type`, which erase; no runtime `#src/*` resolution occurs.
|
|
54
|
+
- No `src/`/`test/` `.ts` files were touched, so the vitest suite and `tsc` were unaffected (confirmed via root check).
|
|
55
|
+
- Pre-completion reviewer: WARN — no findings attributable to this session.
|
|
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,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotgenes/pi-subagents",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".":
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/public.d.ts",
|
|
8
|
+
"default": "./src/service/service.ts"
|
|
9
|
+
}
|
|
7
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"dist",
|
|
14
|
+
"docs",
|
|
15
|
+
"vitest.config.ts",
|
|
16
|
+
"AGENTS.md",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
".prettierignore"
|
|
19
|
+
],
|
|
8
20
|
"imports": {
|
|
9
21
|
"#src/*": "./src/*",
|
|
10
22
|
"#test/*": "./test/*"
|
|
@@ -51,6 +63,8 @@
|
|
|
51
63
|
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
52
64
|
"@earendil-works/pi-tui": "0.75.4",
|
|
53
65
|
"@types/node": "^22.15.3",
|
|
66
|
+
"rollup": "^4.60.4",
|
|
67
|
+
"rollup-plugin-dts": "^6.4.1",
|
|
54
68
|
"rumdl": "^0.1.93",
|
|
55
69
|
"typescript": "^6.0.3",
|
|
56
70
|
"vitest": "^4.1.5"
|
|
@@ -64,6 +78,8 @@
|
|
|
64
78
|
},
|
|
65
79
|
"scripts": {
|
|
66
80
|
"check": "tsc --noEmit",
|
|
81
|
+
"build:types": "rollup -c rollup.dts.config.mjs",
|
|
82
|
+
"verify:public-types": "bash scripts/verify-public-types.sh",
|
|
67
83
|
"test": "vitest run",
|
|
68
84
|
"test:watch": "vitest",
|
|
69
85
|
"lint:md": "rumdl check *.md docs/**/*.md",
|
|
@@ -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
|
-
}
|