@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
|
@@ -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
|
-
}
|