@proletariat/cli 0.3.13 → 0.3.15
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/README.md +15 -8
- package/dist/commands/agent/staff/add.d.ts +1 -0
- package/dist/commands/agent/staff/add.js +5 -0
- package/dist/commands/work/revise.js +8 -14
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +7 -0
- package/dist/commands/work/start.d.ts +1 -0
- package/dist/commands/work/start.js +19 -33
- package/dist/lib/agents/commands.d.ts +5 -1
- package/dist/lib/agents/commands.js +48 -21
- package/dist/lib/agents/index.d.ts +5 -1
- package/dist/lib/agents/index.js +165 -107
- package/dist/lib/database/index.d.ts +4 -2
- package/dist/lib/database/index.js +29 -14
- package/dist/lib/execution/context.d.ts +24 -0
- package/dist/lib/execution/context.js +55 -0
- package/dist/lib/execution/devcontainer.d.ts +3 -0
- package/dist/lib/execution/devcontainer.js +38 -22
- package/dist/lib/execution/runners.js +9 -67
- package/dist/lib/execution/spawner.js +5 -18
- package/dist/lib/execution/types.d.ts +1 -0
- package/dist/lib/pmo/storage/base.js +56 -5
- package/oclif.manifest.json +4072 -4054
- package/package.json +2 -2
package/dist/lib/agents/index.js
CHANGED
|
@@ -74,29 +74,45 @@ export async function promptAgentNames(existingAgents = []) {
|
|
|
74
74
|
return agentNames.trim().split(/\s+/).filter(Boolean);
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* Get the remote URL for a git repository
|
|
78
|
+
*/
|
|
79
|
+
function getRemoteUrl(repoPath) {
|
|
80
|
+
try {
|
|
81
|
+
const url = execSync('git remote get-url origin', { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
82
|
+
return url || null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create agent repos (worktree or clone mode)
|
|
90
|
+
* - worktree mode: Uses git worktree add (shared .git, requires parent repo mounts in container)
|
|
91
|
+
* - clone mode: Uses git clone (independent repo, no path translation needed)
|
|
78
92
|
*/
|
|
79
93
|
export async function createAgentWorktrees(workspacePath, agents, hqPath, options) {
|
|
94
|
+
const mountMode = options?.mountMode || 'worktree'; // Default to worktree for real-time file sync
|
|
95
|
+
const modeLabel = mountMode === 'worktree' ? 'worktree' : 'clone';
|
|
80
96
|
if (hqPath) {
|
|
81
|
-
// HQ mode - create worktrees for all repos in repos/ directory
|
|
97
|
+
// HQ mode - create worktrees/clones for all repos in repos/ directory
|
|
82
98
|
const reposDir = path.join(hqPath, 'repos');
|
|
83
99
|
// Get repositories from database instead of JSON config
|
|
84
100
|
const repos = getWorkspaceRepositories(hqPath);
|
|
85
101
|
if (repos.length > 0) {
|
|
86
|
-
// Create worktrees for each agent across all repositories
|
|
102
|
+
// Create worktrees/clones for each agent across all repositories
|
|
87
103
|
for (const agent of agents) {
|
|
88
104
|
const agentDir = path.join(workspacePath, agent);
|
|
89
|
-
console.log(chalk.blue(`Creating agent: ${agent}...`));
|
|
105
|
+
console.log(chalk.blue(`Creating agent: ${agent} (${modeLabel} mode)...`));
|
|
90
106
|
try {
|
|
91
107
|
// Create agent directory
|
|
92
108
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
93
|
-
// Track which repos successfully
|
|
94
|
-
const
|
|
95
|
-
// Create
|
|
109
|
+
// Track which repos were successfully created
|
|
110
|
+
const createdRepos = [];
|
|
111
|
+
// Create repos for all repositories
|
|
96
112
|
for (const repo of repos) {
|
|
97
113
|
const sourceRepo = path.join(reposDir, repo.name);
|
|
98
|
-
//
|
|
99
|
-
const
|
|
114
|
+
// Target directory is just the repo name (the agent name is already in the parent path)
|
|
115
|
+
const targetDir = path.join(agentDir, repo.name);
|
|
100
116
|
if (fs.existsSync(sourceRepo)) {
|
|
101
117
|
// Check if repo is empty (no commits)
|
|
102
118
|
let isEmptyRepo = false;
|
|
@@ -110,85 +126,109 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
|
|
|
110
126
|
console.log(chalk.yellow(` Skipping ${repo.name} (empty repository with no commits)`));
|
|
111
127
|
continue;
|
|
112
128
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
if (mountMode === 'clone') {
|
|
130
|
+
// CLONE MODE: Create independent git clone
|
|
131
|
+
console.log(styles.muted(` Cloning ${repo.name}...`));
|
|
132
|
+
// Get remote URL from source repo
|
|
133
|
+
const remoteUrl = getRemoteUrl(sourceRepo);
|
|
134
|
+
if (!remoteUrl) {
|
|
135
|
+
console.log(chalk.yellow(` Skipping ${repo.name} (no remote URL configured)`));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
// Clone the repository
|
|
140
|
+
execSync(`git clone "${remoteUrl}" "${targetDir}"`, {
|
|
141
|
+
stdio: 'inherit'
|
|
142
|
+
});
|
|
143
|
+
createdRepos.push(repo.name);
|
|
144
|
+
}
|
|
145
|
+
catch (cloneError) {
|
|
146
|
+
console.log(chalk.yellow(` Warning: Clone failed for ${repo.name}: ${cloneError}`));
|
|
147
|
+
}
|
|
129
148
|
}
|
|
130
|
-
|
|
131
|
-
//
|
|
149
|
+
else {
|
|
150
|
+
// WORKTREE MODE: Create git worktree (original behavior)
|
|
151
|
+
console.log(styles.muted(` Creating worktree for ${repo.name}...`));
|
|
152
|
+
// Fetch latest from origin to ensure we have up-to-date main
|
|
132
153
|
try {
|
|
133
|
-
execSync(
|
|
134
|
-
|
|
154
|
+
execSync(`git fetch origin main`, {
|
|
155
|
+
cwd: sourceRepo,
|
|
156
|
+
stdio: 'pipe'
|
|
157
|
+
});
|
|
135
158
|
}
|
|
136
159
|
catch {
|
|
137
|
-
//
|
|
138
|
-
|
|
160
|
+
// Ignore fetch errors (might be offline)
|
|
161
|
+
console.log(chalk.yellow(` Warning: Could not fetch origin/main, using local state`));
|
|
139
162
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
// Determine the base ref to use (origin/main, main, or HEAD)
|
|
164
|
+
let baseRef = 'origin/main';
|
|
165
|
+
try {
|
|
166
|
+
execSync(`git rev-parse ${baseRef}`, { cwd: sourceRepo, stdio: 'pipe' });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// origin/main doesn't exist, try local main
|
|
170
|
+
try {
|
|
171
|
+
execSync('git rev-parse main', { cwd: sourceRepo, stdio: 'pipe' });
|
|
172
|
+
baseRef = 'main';
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// No main branch, use HEAD
|
|
176
|
+
baseRef = 'HEAD';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Create git worktree for the agent
|
|
180
|
+
const branchName = `agent-${agent}`;
|
|
153
181
|
try {
|
|
154
|
-
|
|
155
|
-
execSync(`git worktree add "${worktreeDir}" ${branchName}`, {
|
|
182
|
+
execSync(`git worktree add "${targetDir}" -b ${branchName} ${baseRef}`, {
|
|
156
183
|
cwd: sourceRepo,
|
|
157
184
|
stdio: 'inherit'
|
|
158
185
|
});
|
|
159
|
-
|
|
186
|
+
createdRepos.push(repo.name);
|
|
160
187
|
}
|
|
161
188
|
catch {
|
|
162
|
-
//
|
|
189
|
+
// Branch might already exist, try to use it or clean up
|
|
190
|
+
console.log(chalk.yellow(` Branch ${branchName} already exists, attempting to reuse or clean up...`));
|
|
163
191
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
stdio: 'pipe'
|
|
167
|
-
});
|
|
168
|
-
execSync(`git worktree add "${worktreeDir}" -b ${branchName} ${baseRef}`, {
|
|
192
|
+
// Try without creating a new branch (use existing)
|
|
193
|
+
execSync(`git worktree add "${targetDir}" ${branchName}`, {
|
|
169
194
|
cwd: sourceRepo,
|
|
170
195
|
stdio: 'inherit'
|
|
171
196
|
});
|
|
172
|
-
|
|
197
|
+
createdRepos.push(repo.name);
|
|
173
198
|
}
|
|
174
|
-
catch
|
|
175
|
-
|
|
199
|
+
catch {
|
|
200
|
+
// If that fails too, clean up the orphaned branch and try again
|
|
201
|
+
try {
|
|
202
|
+
execSync(`git branch -D ${branchName}`, {
|
|
203
|
+
cwd: sourceRepo,
|
|
204
|
+
stdio: 'pipe'
|
|
205
|
+
});
|
|
206
|
+
execSync(`git worktree add "${targetDir}" -b ${branchName} ${baseRef}`, {
|
|
207
|
+
cwd: sourceRepo,
|
|
208
|
+
stdio: 'inherit'
|
|
209
|
+
});
|
|
210
|
+
createdRepos.push(repo.name);
|
|
211
|
+
}
|
|
212
|
+
catch (finalError) {
|
|
213
|
+
throw new Error(`Failed to create worktree after cleanup: ${finalError}`);
|
|
214
|
+
}
|
|
176
215
|
}
|
|
177
216
|
}
|
|
178
217
|
}
|
|
179
218
|
}
|
|
180
219
|
}
|
|
181
|
-
// Create devcontainer config for sandboxed execution (only for repos
|
|
220
|
+
// Create devcontainer config for sandboxed execution (only for repos that were created)
|
|
182
221
|
// Note: Agent metadata is stored in SQLite (agents table), not in config files
|
|
183
|
-
if (!options?.skipDevcontainer &&
|
|
222
|
+
if (!options?.skipDevcontainer && createdRepos.length > 0) {
|
|
184
223
|
console.log(styles.muted(` Creating devcontainer config...`));
|
|
185
224
|
createDevcontainerConfig({
|
|
186
225
|
agentName: agent,
|
|
187
226
|
agentDir,
|
|
188
|
-
repoWorktrees:
|
|
227
|
+
repoWorktrees: mountMode === 'worktree' ? createdRepos : undefined, // Only pass repos for worktree mode
|
|
228
|
+
mountMode,
|
|
189
229
|
});
|
|
190
230
|
}
|
|
191
|
-
console.log(chalk.green(`✅ Agent ${agent} created with ${
|
|
231
|
+
console.log(chalk.green(`✅ Agent ${agent} created with ${createdRepos.length} ${modeLabel}(s)`));
|
|
192
232
|
}
|
|
193
233
|
catch (error) {
|
|
194
234
|
console.log(chalk.red(`Failed to create agent ${agent}: ${error}`));
|
|
@@ -211,9 +251,9 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
|
|
|
211
251
|
const repoName = path.basename(sourceRepo);
|
|
212
252
|
for (const agent of agents) {
|
|
213
253
|
const agentDir = path.join(workspacePath, agent);
|
|
214
|
-
//
|
|
215
|
-
const
|
|
216
|
-
console.log(chalk.blue(`Creating agent: ${agent}...`));
|
|
254
|
+
// Target directory is just the repo name (the agent name is already in the parent path)
|
|
255
|
+
const targetDir = path.join(agentDir, repoName);
|
|
256
|
+
console.log(chalk.blue(`Creating agent: ${agent} (${modeLabel} mode)...`));
|
|
217
257
|
try {
|
|
218
258
|
// Check if repo is empty (no commits)
|
|
219
259
|
let isEmptyRepo = false;
|
|
@@ -229,65 +269,82 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
|
|
|
229
269
|
}
|
|
230
270
|
// Create agent directory
|
|
231
271
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
272
|
+
if (mountMode === 'clone') {
|
|
273
|
+
// CLONE MODE: Create independent git clone
|
|
274
|
+
console.log(styles.muted(` Cloning ${repoName}...`));
|
|
275
|
+
// Get remote URL from source repo
|
|
276
|
+
const remoteUrl = getRemoteUrl(sourceRepo);
|
|
277
|
+
if (!remoteUrl) {
|
|
278
|
+
console.log(chalk.yellow(` Skipping ${repoName} (no remote URL configured)`));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// Clone the repository
|
|
282
|
+
execSync(`git clone "${remoteUrl}" "${targetDir}"`, {
|
|
283
|
+
stdio: 'inherit'
|
|
237
284
|
});
|
|
238
285
|
}
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
// Determine the base ref to use (origin/main, main, or HEAD)
|
|
244
|
-
let baseRef = 'origin/main';
|
|
245
|
-
try {
|
|
246
|
-
execSync(`git rev-parse ${baseRef}`, { cwd: sourceRepo, stdio: 'pipe' });
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// origin/main doesn't exist, try local main
|
|
286
|
+
else {
|
|
287
|
+
// WORKTREE MODE: Create git worktree (original behavior)
|
|
288
|
+
// Fetch latest from origin to ensure we have up-to-date main
|
|
250
289
|
try {
|
|
251
|
-
execSync(
|
|
252
|
-
|
|
290
|
+
execSync(`git fetch origin main`, {
|
|
291
|
+
cwd: sourceRepo,
|
|
292
|
+
stdio: 'pipe'
|
|
293
|
+
});
|
|
253
294
|
}
|
|
254
295
|
catch {
|
|
255
|
-
//
|
|
256
|
-
|
|
296
|
+
// Ignore fetch errors (might be offline)
|
|
297
|
+
console.log(chalk.yellow(` Warning: Could not fetch origin/main, using local state`));
|
|
257
298
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const branchName = `agent-${agent}`;
|
|
261
|
-
try {
|
|
262
|
-
execSync(`git worktree add "${worktreeDir}" -b ${branchName} ${baseRef}`, {
|
|
263
|
-
cwd: sourceRepo,
|
|
264
|
-
stdio: 'inherit'
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
// Branch might already exist, try to use it or clean up
|
|
269
|
-
console.log(chalk.yellow(` Branch ${branchName} already exists, attempting to reuse or clean up...`));
|
|
299
|
+
// Determine the base ref to use (origin/main, main, or HEAD)
|
|
300
|
+
let baseRef = 'origin/main';
|
|
270
301
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
302
|
+
execSync(`git rev-parse ${baseRef}`, { cwd: sourceRepo, stdio: 'pipe' });
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// origin/main doesn't exist, try local main
|
|
306
|
+
try {
|
|
307
|
+
execSync('git rev-parse main', { cwd: sourceRepo, stdio: 'pipe' });
|
|
308
|
+
baseRef = 'main';
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// No main branch, use HEAD
|
|
312
|
+
baseRef = 'HEAD';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Create git worktree for the agent
|
|
316
|
+
const branchName = `agent-${agent}`;
|
|
317
|
+
try {
|
|
318
|
+
execSync(`git worktree add "${targetDir}" -b ${branchName} ${baseRef}`, {
|
|
273
319
|
cwd: sourceRepo,
|
|
274
320
|
stdio: 'inherit'
|
|
275
321
|
});
|
|
276
322
|
}
|
|
277
323
|
catch {
|
|
278
|
-
//
|
|
324
|
+
// Branch might already exist, try to use it or clean up
|
|
325
|
+
console.log(chalk.yellow(` Branch ${branchName} already exists, attempting to reuse or clean up...`));
|
|
279
326
|
try {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
stdio: 'pipe'
|
|
283
|
-
});
|
|
284
|
-
execSync(`git worktree add "${worktreeDir}" -b ${branchName} ${baseRef}`, {
|
|
327
|
+
// Try without creating a new branch (use existing)
|
|
328
|
+
execSync(`git worktree add "${targetDir}" ${branchName}`, {
|
|
285
329
|
cwd: sourceRepo,
|
|
286
330
|
stdio: 'inherit'
|
|
287
331
|
});
|
|
288
332
|
}
|
|
289
|
-
catch
|
|
290
|
-
|
|
333
|
+
catch {
|
|
334
|
+
// If that fails too, clean up the orphaned branch and try again
|
|
335
|
+
try {
|
|
336
|
+
execSync(`git branch -D ${branchName}`, {
|
|
337
|
+
cwd: sourceRepo,
|
|
338
|
+
stdio: 'pipe'
|
|
339
|
+
});
|
|
340
|
+
execSync(`git worktree add "${targetDir}" -b ${branchName} ${baseRef}`, {
|
|
341
|
+
cwd: sourceRepo,
|
|
342
|
+
stdio: 'inherit'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (finalError) {
|
|
346
|
+
throw new Error(`Failed to create worktree after cleanup: ${finalError}`);
|
|
347
|
+
}
|
|
291
348
|
}
|
|
292
349
|
}
|
|
293
350
|
}
|
|
@@ -298,10 +355,11 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
|
|
|
298
355
|
createDevcontainerConfig({
|
|
299
356
|
agentName: agent,
|
|
300
357
|
agentDir,
|
|
301
|
-
repoWorktrees: [repoName],
|
|
358
|
+
repoWorktrees: mountMode === 'worktree' ? [repoName] : undefined, // Only pass repos for worktree mode
|
|
359
|
+
mountMode,
|
|
302
360
|
});
|
|
303
361
|
}
|
|
304
|
-
console.log(chalk.green(`✅ Agent ${agent} created with
|
|
362
|
+
console.log(chalk.green(`✅ Agent ${agent} created with ${modeLabel}`));
|
|
305
363
|
}
|
|
306
364
|
catch (error) {
|
|
307
365
|
console.log(chalk.red(`Failed to create agent ${agent}: ${error}`));
|
|
@@ -17,6 +17,7 @@ export interface Repository {
|
|
|
17
17
|
}
|
|
18
18
|
export type AgentType = 'persistent' | 'ephemeral';
|
|
19
19
|
export type AgentStatus = 'active' | 'cleaned';
|
|
20
|
+
export type MountMode = 'worktree' | 'clone';
|
|
20
21
|
export interface Agent {
|
|
21
22
|
name: string;
|
|
22
23
|
type: AgentType;
|
|
@@ -24,6 +25,7 @@ export interface Agent {
|
|
|
24
25
|
base_name: string | null;
|
|
25
26
|
theme_id: string | null;
|
|
26
27
|
worktree_path: string | null;
|
|
28
|
+
mount_mode: MountMode;
|
|
27
29
|
created_at: string;
|
|
28
30
|
cleaned_at: string | null;
|
|
29
31
|
}
|
|
@@ -91,11 +93,11 @@ export declare function addRepositoriesToDatabase(workspacePath: string, repos:
|
|
|
91
93
|
/**
|
|
92
94
|
* Add agents to database (case-insensitive uniqueness)
|
|
93
95
|
*/
|
|
94
|
-
export declare function addAgentsToDatabase(workspacePath: string, agentNames: string[], themeId?: string): void;
|
|
96
|
+
export declare function addAgentsToDatabase(workspacePath: string, agentNames: string[], themeId?: string, mountMode?: MountMode): void;
|
|
95
97
|
/**
|
|
96
98
|
* Add an ephemeral agent to the database
|
|
97
99
|
*/
|
|
98
|
-
export declare function addEphemeralAgentToDatabase(workspacePath: string, agentName: string, baseName: string, themeId?: string): Agent;
|
|
100
|
+
export declare function addEphemeralAgentToDatabase(workspacePath: string, agentName: string, baseName: string, themeId?: string, mountMode?: MountMode): Agent;
|
|
99
101
|
/**
|
|
100
102
|
* Get all ephemeral agent names from the database
|
|
101
103
|
*/
|
|
@@ -51,6 +51,7 @@ CREATE TABLE IF NOT EXISTS agents (
|
|
|
51
51
|
base_name TEXT,
|
|
52
52
|
theme_id TEXT,
|
|
53
53
|
worktree_path TEXT,
|
|
54
|
+
mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone')),
|
|
54
55
|
created_at TEXT NOT NULL,
|
|
55
56
|
cleaned_at TEXT,
|
|
56
57
|
FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE SET NULL
|
|
@@ -183,6 +184,17 @@ export function openWorkspaceDatabase(workspacePath) {
|
|
|
183
184
|
catch {
|
|
184
185
|
// Ignore migration errors - table might not exist yet
|
|
185
186
|
}
|
|
187
|
+
// Migration: add mount_mode column to agents table (TKT-686)
|
|
188
|
+
try {
|
|
189
|
+
const agentsTableInfo = db.prepare("PRAGMA table_info(agents)").all();
|
|
190
|
+
if (!agentsTableInfo.some(col => col.name === 'mount_mode')) {
|
|
191
|
+
// Add mount_mode column with default 'worktree' for existing agents (backward compat)
|
|
192
|
+
db.exec("ALTER TABLE agents ADD COLUMN mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone'))");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Ignore migration errors - table might not exist yet or column already exists
|
|
197
|
+
}
|
|
186
198
|
return db;
|
|
187
199
|
}
|
|
188
200
|
/**
|
|
@@ -307,13 +319,13 @@ export function addRepositoriesToDatabase(workspacePath, repos) {
|
|
|
307
319
|
/**
|
|
308
320
|
* Add agents to database (case-insensitive uniqueness)
|
|
309
321
|
*/
|
|
310
|
-
export function addAgentsToDatabase(workspacePath, agentNames, themeId) {
|
|
322
|
+
export function addAgentsToDatabase(workspacePath, agentNames, themeId, mountMode = 'worktree') {
|
|
311
323
|
const db = openWorkspaceDatabase(workspacePath);
|
|
312
324
|
// Check for existing agents (case-insensitive)
|
|
313
325
|
const checkExisting = db.prepare('SELECT name FROM agents WHERE LOWER(name) = LOWER(?)');
|
|
314
326
|
const insertAgent = db.prepare(`
|
|
315
|
-
INSERT OR REPLACE INTO agents (name, type, base_name, theme_id, worktree_path, created_at)
|
|
316
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
327
|
+
INSERT OR REPLACE INTO agents (name, type, base_name, theme_id, worktree_path, mount_mode, created_at)
|
|
328
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
317
329
|
`);
|
|
318
330
|
const insertWorktree = db.prepare(`
|
|
319
331
|
INSERT OR REPLACE INTO agent_worktrees (agent_name, repo_name, worktree_path, branch, created_at)
|
|
@@ -339,7 +351,7 @@ export function addAgentsToDatabase(workspacePath, agentNames, themeId) {
|
|
|
339
351
|
? `agents/${persistentDir}/${agentName}`
|
|
340
352
|
: agentName;
|
|
341
353
|
// Add agent (persistent type for manually added agents)
|
|
342
|
-
insertAgent.run(agentName, 'persistent', null, effectiveThemeId || null, agentWorktreePath, now);
|
|
354
|
+
insertAgent.run(agentName, 'persistent', null, effectiveThemeId || null, agentWorktreePath, mountMode, now);
|
|
343
355
|
// Add worktrees for all repos
|
|
344
356
|
for (const repo of repos) {
|
|
345
357
|
const worktreePath = workspace.type === 'hq'
|
|
@@ -355,14 +367,14 @@ export function addAgentsToDatabase(workspacePath, agentNames, themeId) {
|
|
|
355
367
|
/**
|
|
356
368
|
* Add an ephemeral agent to the database
|
|
357
369
|
*/
|
|
358
|
-
export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId) {
|
|
370
|
+
export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
|
|
359
371
|
const db = openWorkspaceDatabase(workspacePath);
|
|
360
372
|
const now = new Date().toISOString();
|
|
361
373
|
const worktreePath = `agents/temp/${agentName}`;
|
|
362
374
|
db.prepare(`
|
|
363
|
-
INSERT INTO agents (name, type, status, base_name, theme_id, worktree_path, created_at)
|
|
364
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
365
|
-
`).run(agentName, 'ephemeral', 'active', baseName, themeId || null, worktreePath, now);
|
|
375
|
+
INSERT INTO agents (name, type, status, base_name, theme_id, worktree_path, mount_mode, created_at)
|
|
376
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
377
|
+
`).run(agentName, 'ephemeral', 'active', baseName, themeId || null, worktreePath, mountMode, now);
|
|
366
378
|
const agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentName);
|
|
367
379
|
db.close();
|
|
368
380
|
return {
|
|
@@ -372,6 +384,7 @@ export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName,
|
|
|
372
384
|
base_name: agent.base_name,
|
|
373
385
|
theme_id: agent.theme_id,
|
|
374
386
|
worktree_path: agent.worktree_path,
|
|
387
|
+
mount_mode: (agent.mount_mode || 'clone'),
|
|
375
388
|
created_at: agent.created_at,
|
|
376
389
|
cleaned_at: agent.cleaned_at,
|
|
377
390
|
};
|
|
@@ -411,6 +424,7 @@ export function getWorkspaceAgents(workspacePath, includeCleanedUp = false) {
|
|
|
411
424
|
base_name: row.base_name,
|
|
412
425
|
theme_id: row.theme_id,
|
|
413
426
|
worktree_path: row.worktree_path,
|
|
427
|
+
mount_mode: (row.mount_mode || 'worktree'),
|
|
414
428
|
created_at: row.created_at,
|
|
415
429
|
cleaned_at: row.cleaned_at,
|
|
416
430
|
}));
|
|
@@ -445,6 +459,7 @@ export function getAgentByPath(workspacePath, absolutePath) {
|
|
|
445
459
|
base_name: row.base_name,
|
|
446
460
|
theme_id: row.theme_id,
|
|
447
461
|
worktree_path: row.worktree_path,
|
|
462
|
+
mount_mode: (row.mount_mode || 'worktree'),
|
|
448
463
|
created_at: row.created_at,
|
|
449
464
|
cleaned_at: row.cleaned_at,
|
|
450
465
|
};
|
|
@@ -527,10 +542,10 @@ export function discoverAgentsOnDisk(workspacePath) {
|
|
|
527
542
|
`).run(worktreePath, entry.name);
|
|
528
543
|
}
|
|
529
544
|
else {
|
|
530
|
-
// Register new agent
|
|
545
|
+
// Register new agent - discovered agents default to 'worktree' mode (legacy behavior)
|
|
531
546
|
db.prepare(`
|
|
532
|
-
INSERT INTO agents (name, type, status, worktree_path, created_at)
|
|
533
|
-
VALUES (?, 'persistent', 'active', ?, ?)
|
|
547
|
+
INSERT INTO agents (name, type, status, worktree_path, mount_mode, created_at)
|
|
548
|
+
VALUES (?, 'persistent', 'active', ?, 'worktree', ?)
|
|
534
549
|
`).run(entry.name, worktreePath, now);
|
|
535
550
|
}
|
|
536
551
|
result.discovered.push({ name: entry.name, type: 'persistent', path: worktreePath });
|
|
@@ -559,10 +574,10 @@ export function discoverAgentsOnDisk(workspacePath) {
|
|
|
559
574
|
`).run(worktreePath, entry.name);
|
|
560
575
|
}
|
|
561
576
|
else {
|
|
562
|
-
// Register new agent
|
|
577
|
+
// Register new agent - discovered agents default to 'worktree' mode (legacy behavior)
|
|
563
578
|
db.prepare(`
|
|
564
|
-
INSERT INTO agents (name, type, status, worktree_path, created_at)
|
|
565
|
-
VALUES (?, 'ephemeral', 'active', ?, ?)
|
|
579
|
+
INSERT INTO agents (name, type, status, worktree_path, mount_mode, created_at)
|
|
580
|
+
VALUES (?, 'ephemeral', 'active', ?, 'worktree', ?)
|
|
566
581
|
`).run(entry.name, worktreePath, now);
|
|
567
582
|
}
|
|
568
583
|
result.discovered.push({ name: entry.name, type: 'ephemeral', path: worktreePath });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Context Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared helpers for building ExecutionContext and detecting repository worktrees.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Detect git repository worktrees within an agent directory.
|
|
8
|
+
*
|
|
9
|
+
* Scans the agent directory for subdirectories that contain a .git file or folder,
|
|
10
|
+
* indicating they are git repositories (either worktrees or clones).
|
|
11
|
+
*
|
|
12
|
+
* @param agentDir - The agent's working directory
|
|
13
|
+
* @returns Array of repository directory names found within the agent directory
|
|
14
|
+
*/
|
|
15
|
+
export declare function detectRepoWorktrees(agentDir: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Determine the worktree path for an agent based on detected repositories.
|
|
18
|
+
*
|
|
19
|
+
* @param agentDir - The agent's working directory
|
|
20
|
+
* @param repoWorktrees - Array of repository names (from detectRepoWorktrees)
|
|
21
|
+
* @param fallbackPath - Path to use if no worktrees found (defaults to process.cwd())
|
|
22
|
+
* @returns The resolved worktree path
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveWorktreePath(agentDir: string, repoWorktrees: string[], fallbackPath?: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Context Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared helpers for building ExecutionContext and detecting repository worktrees.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
/**
|
|
9
|
+
* Detect git repository worktrees within an agent directory.
|
|
10
|
+
*
|
|
11
|
+
* Scans the agent directory for subdirectories that contain a .git file or folder,
|
|
12
|
+
* indicating they are git repositories (either worktrees or clones).
|
|
13
|
+
*
|
|
14
|
+
* @param agentDir - The agent's working directory
|
|
15
|
+
* @returns Array of repository directory names found within the agent directory
|
|
16
|
+
*/
|
|
17
|
+
export function detectRepoWorktrees(agentDir) {
|
|
18
|
+
if (!fs.existsSync(agentDir)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const agentContents = fs.readdirSync(agentDir);
|
|
22
|
+
return agentContents.filter(item => {
|
|
23
|
+
const itemPath = path.join(agentDir, item);
|
|
24
|
+
const gitPath = path.join(itemPath, '.git');
|
|
25
|
+
try {
|
|
26
|
+
return fs.statSync(itemPath).isDirectory() && fs.existsSync(gitPath);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Handle permission errors or race conditions
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Determine the worktree path for an agent based on detected repositories.
|
|
36
|
+
*
|
|
37
|
+
* @param agentDir - The agent's working directory
|
|
38
|
+
* @param repoWorktrees - Array of repository names (from detectRepoWorktrees)
|
|
39
|
+
* @param fallbackPath - Path to use if no worktrees found (defaults to process.cwd())
|
|
40
|
+
* @returns The resolved worktree path
|
|
41
|
+
*/
|
|
42
|
+
export function resolveWorktreePath(agentDir, repoWorktrees, fallbackPath) {
|
|
43
|
+
if (repoWorktrees.length === 1) {
|
|
44
|
+
// Single repo - open directly in the repo worktree
|
|
45
|
+
return path.join(agentDir, repoWorktrees[0]);
|
|
46
|
+
}
|
|
47
|
+
else if (repoWorktrees.length > 1) {
|
|
48
|
+
// Multiple repos - use agent directory, let user navigate between them
|
|
49
|
+
return agentDir;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// No git worktrees found - use fallback
|
|
53
|
+
return fallbackPath ?? process.cwd();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Uses a custom Dockerfile with network firewall for security sandboxing.
|
|
6
6
|
*/
|
|
7
7
|
import { ExecutionConfig } from './types.js';
|
|
8
|
+
export type MountMode = 'worktree' | 'clone';
|
|
8
9
|
export interface DevcontainerOptions {
|
|
9
10
|
agentName: string;
|
|
10
11
|
agentDir: string;
|
|
@@ -14,6 +15,8 @@ export interface DevcontainerOptions {
|
|
|
14
15
|
timezone?: string;
|
|
15
16
|
/** prlt channel: "npm", "npm:dev", "gh", "gh:dev", "mount", or version like "npm:1.2.3" */
|
|
16
17
|
prltChannel?: string;
|
|
18
|
+
/** Mount mode: 'worktree' needs parent repo mounts + git wrapper, 'clone' is self-contained */
|
|
19
|
+
mountMode?: MountMode;
|
|
17
20
|
}
|
|
18
21
|
export interface DevcontainerJson {
|
|
19
22
|
name: string;
|