@proletariat/cli 0.3.14 → 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.
@@ -74,29 +74,45 @@ export async function promptAgentNames(existingAgents = []) {
74
74
  return agentNames.trim().split(/\s+/).filter(Boolean);
75
75
  }
76
76
  /**
77
- * Create agent worktrees (shared between HQ and workspace-only modes)
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 had worktrees created
94
- const createdWorktrees = [];
95
- // Create worktrees for all repositories
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
- // Worktree directory is just the repo name (the agent name is already in the parent path)
99
- const worktreeDir = path.join(agentDir, repo.name);
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
- console.log(styles.muted(` Creating worktree for ${repo.name}...`));
114
- // Fetch latest from origin to ensure we have up-to-date main
115
- try {
116
- execSync(`git fetch origin main`, {
117
- cwd: sourceRepo,
118
- stdio: 'pipe'
119
- });
120
- }
121
- catch {
122
- // Ignore fetch errors (might be offline)
123
- console.log(chalk.yellow(` Warning: Could not fetch origin/main, using local state`));
124
- }
125
- // Determine the base ref to use (origin/main, main, or HEAD)
126
- let baseRef = 'origin/main';
127
- try {
128
- execSync(`git rev-parse ${baseRef}`, { cwd: sourceRepo, stdio: 'pipe' });
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
- catch {
131
- // origin/main doesn't exist, try local main
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('git rev-parse main', { cwd: sourceRepo, stdio: 'pipe' });
134
- baseRef = 'main';
154
+ execSync(`git fetch origin main`, {
155
+ cwd: sourceRepo,
156
+ stdio: 'pipe'
157
+ });
135
158
  }
136
159
  catch {
137
- // No main branch, use HEAD
138
- baseRef = 'HEAD';
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
- // Create git worktree for the agent
142
- const branchName = `agent-${agent}`;
143
- try {
144
- execSync(`git worktree add "${worktreeDir}" -b ${branchName} ${baseRef}`, {
145
- cwd: sourceRepo,
146
- stdio: 'inherit'
147
- });
148
- createdWorktrees.push(repo.name);
149
- }
150
- catch {
151
- // Branch might already exist, try to use it or clean up
152
- console.log(chalk.yellow(` Branch ${branchName} already exists, attempting to reuse or clean up...`));
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
- // Try without creating a new branch (use existing)
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
- createdWorktrees.push(repo.name);
186
+ createdRepos.push(repo.name);
160
187
  }
161
188
  catch {
162
- // If that fails too, clean up the orphaned branch and try again
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
- execSync(`git branch -D ${branchName}`, {
165
- cwd: sourceRepo,
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
- createdWorktrees.push(repo.name);
197
+ createdRepos.push(repo.name);
173
198
  }
174
- catch (finalError) {
175
- throw new Error(`Failed to create worktree after cleanup: ${finalError}`);
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 with worktrees)
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 && createdWorktrees.length > 0) {
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: createdWorktrees,
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 ${createdWorktrees.length} worktree(s)`));
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
- // Worktree directory is just the repo name (the agent name is already in the parent path)
215
- const worktreeDir = path.join(agentDir, repoName);
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
- // Fetch latest from origin to ensure we have up-to-date main
233
- try {
234
- execSync(`git fetch origin main`, {
235
- cwd: sourceRepo,
236
- stdio: 'pipe'
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
- catch {
240
- // Ignore fetch errors (might be offline)
241
- console.log(chalk.yellow(` Warning: Could not fetch origin/main, using local state`));
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('git rev-parse main', { cwd: sourceRepo, stdio: 'pipe' });
252
- baseRef = 'main';
290
+ execSync(`git fetch origin main`, {
291
+ cwd: sourceRepo,
292
+ stdio: 'pipe'
293
+ });
253
294
  }
254
295
  catch {
255
- // No main branch, use HEAD
256
- baseRef = 'HEAD';
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
- // Create git worktree for the agent
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
- // Try without creating a new branch (use existing)
272
- execSync(`git worktree add "${worktreeDir}" ${branchName}`, {
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
- // If that fails too, clean up the orphaned branch and try again
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
- execSync(`git branch -D ${branchName}`, {
281
- cwd: sourceRepo,
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 (finalError) {
290
- throw new Error(`Failed to create worktree after cleanup: ${finalError}`);
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 worktree`));
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;