@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.
@@ -17,6 +17,7 @@ import { parseChannel } from '../workspace-config.js';
17
17
  */
18
18
  export function generateDevcontainerJson(options, config) {
19
19
  const cfg = config || DEFAULT_EXECUTION_CONFIG;
20
+ const mountMode = options.mountMode || 'worktree'; // Default to worktree mode
20
21
  // Parse the channel to determine registry and version
21
22
  const channel = parseChannel(options.prltChannel || 'npm');
22
23
  const useMount = channel.registry === 'mount';
@@ -34,6 +35,34 @@ export function generateDevcontainerJson(options, config) {
34
35
  if (channel.registry === 'gh') {
35
36
  buildArgs.GITHUB_TOKEN = '${localEnv:GITHUB_TOKEN}';
36
37
  }
38
+ // Build mounts array - parent repo mounts only needed for worktree mode
39
+ const mounts = [
40
+ 'source=${localWorkspaceFolder},target=/workspace,type=bind',
41
+ 'source=claude-bash-history,target=/commandhistory,type=volume',
42
+ 'source=claude-credentials,target=/home/node/.claude,type=volume',
43
+ // NOTE: ~/.claude.json is COPIED (not mounted) to /workspace/.claude.json
44
+ // to avoid corruption from concurrent writes by multiple containers
45
+ // NOTE: SSH agent socket mounting doesn't work reliably on Docker Desktop for Mac
46
+ // So we use HTTPS + token approach instead. The token is fetched fresh at spawn time.
47
+ 'source=${localEnv:PRLT_HQ_PATH}/.proletariat,target=/hq/.proletariat,type=bind',
48
+ // PMO path can be anywhere (e.g., /hq/pmo or /hq/repos/myrepo/pmo)
49
+ // Use PRLT_PMO_PATH env var to mount the actual location to /hq/pmo
50
+ 'source=${localEnv:PRLT_PMO_PATH},target=/hq/pmo,type=bind',
51
+ ];
52
+ // Only add parent repo mounts for worktree mode
53
+ // Worktree .git files reference paths like /Users/.../repos/{repoName}/.git/worktrees/name
54
+ // These mounts make those paths accessible inside the container at /hq/repos/{repoName}
55
+ // Clone mode doesn't need this because each clone has its own self-contained .git directory
56
+ if (mountMode === 'worktree' && options.repoWorktrees) {
57
+ for (const repoName of options.repoWorktrees) {
58
+ mounts.push(`source=\${localEnv:PRLT_HQ_PATH}/repos/${repoName},target=/hq/repos/${repoName},type=bind`);
59
+ }
60
+ }
61
+ // If using "mount" channel, mount local prlt build from PRLT_REPO_PATH
62
+ // The setup-prlt.sh script will detect /opt/prlt and configure the wrapper
63
+ if (useMount) {
64
+ mounts.push('source=${localEnv:PRLT_REPO_PATH},target=/opt/prlt,type=bind,readonly');
65
+ }
37
66
  const devcontainerJson = {
38
67
  name: `Agent: ${options.agentName}`,
39
68
  build: {
@@ -59,26 +88,7 @@ export function generateDevcontainerJson(options, config) {
59
88
  `--cpus=${options.cpus || cfg.devcontainer.cpus}`,
60
89
  ],
61
90
  remoteUser: 'node',
62
- mounts: [
63
- 'source=${localWorkspaceFolder},target=/workspace,type=bind',
64
- 'source=claude-bash-history,target=/commandhistory,type=volume',
65
- 'source=claude-credentials,target=/home/node/.claude,type=volume',
66
- // NOTE: ~/.claude.json is COPIED (not mounted) to /workspace/.claude.json
67
- // to avoid corruption from concurrent writes by multiple containers
68
- // NOTE: SSH agent socket mounting doesn't work reliably on Docker Desktop for Mac
69
- // So we use HTTPS + token approach instead. The token is fetched fresh at spawn time.
70
- 'source=${localEnv:PRLT_HQ_PATH}/.proletariat,target=/hq/.proletariat,type=bind',
71
- // PMO path can be anywhere (e.g., /hq/pmo or /hq/repos/myrepo/pmo)
72
- // Use PRLT_PMO_PATH env var to mount the actual location to /hq/pmo
73
- 'source=${localEnv:PRLT_PMO_PATH},target=/hq/pmo,type=bind',
74
- // Mount each repo's directory so git worktrees can resolve their parent
75
- // Worktree .git files reference paths like /Users/.../repos/{repoName}/.git/worktrees/name
76
- // These mounts make those paths accessible inside the container at /hq/repos/{repoName}
77
- ...(options.repoWorktrees || []).map(repoName => `source=\${localEnv:PRLT_HQ_PATH}/repos/${repoName},target=/hq/repos/${repoName},type=bind`),
78
- // If using "mount" channel, mount local prlt build from PRLT_REPO_PATH
79
- // The setup-prlt.sh script will detect /opt/prlt and configure the wrapper
80
- ...(useMount ? ['source=${localEnv:PRLT_REPO_PATH},target=/opt/prlt,type=bind,readonly'] : []),
81
- ],
91
+ mounts,
82
92
  containerEnv: {
83
93
  DEVCONTAINER: 'true',
84
94
  ANTHROPIC_API_KEY: '${localEnv:ANTHROPIC_API_KEY}',
@@ -89,6 +99,8 @@ export function generateDevcontainerJson(options, config) {
89
99
  // Agent identity - allows agent to know its name and host path
90
100
  PRLT_AGENT_NAME: options.agentName,
91
101
  PRLT_HOST_PATH: options.agentDir,
102
+ // Mount mode - allows scripts to know if git wrapper is needed
103
+ PRLT_MOUNT_MODE: mountMode,
92
104
  // /hq/.proletariat/bin contains prlt wrapper with ESM loader for native modules
93
105
  PATH: '/hq/.proletariat/bin:/home/node/.npm-global/bin:/usr/local/bin:/usr/bin:/bin',
94
106
  },
@@ -386,8 +398,12 @@ GITWRAPPER
386
398
  echo "Git wrapper installed for worktree path translation"
387
399
  }
388
400
 
389
- # Set up git wrapper for worktree path translation
390
- setup_git_wrapper
401
+ # Set up git wrapper for worktree path translation (only needed for worktree mount mode)
402
+ if [ "\${PRLT_MOUNT_MODE:-clone}" = "worktree" ]; then
403
+ setup_git_wrapper
404
+ else
405
+ echo "Clone mode: git wrapper not needed (self-contained .git directories)"
406
+ fi
391
407
 
392
408
  # Copy Claude credentials from workspace to home (each container gets its own copy)
393
409
  if [ -f "/workspace/.claude.json" ]; then
@@ -742,10 +742,15 @@ function createDockerContainer(context, containerName, imageName, config) {
742
742
  ...(context.hqPath ? [`-v "${context.hqPath}/.proletariat:/hq/.proletariat"`] : []),
743
743
  // PMO path
744
744
  ...(context.pmoPath ? [`-v "${context.pmoPath}:/hq/pmo"`] : []),
745
+ // Mount parent repos for git worktree resolution
746
+ // Worktree .git files reference paths like /Users/.../repos/{repoName}/.git/worktrees/name
747
+ // These mounts make those paths accessible inside the container at /hq/repos/{repoName}
748
+ ...(context.repoWorktrees || []).map(repoName => `-v "${context.hqPath}/repos/${repoName}:/hq/repos/${repoName}"`),
745
749
  // Claude credentials - shared named volume (login once, all containers share)
746
750
  `-v "claude-credentials:/home/node/.claude"`,
747
751
  ];
748
752
  // Build environment flags
753
+ const hasWorktrees = context.repoWorktrees && context.repoWorktrees.length > 0;
749
754
  const envVars = [
750
755
  `-e DEVCONTAINER=true`,
751
756
  `-e PRLT_HQ_PATH=/hq`,
@@ -756,6 +761,8 @@ function createDockerContainer(context, containerName, imageName, config) {
756
761
  ...(process.env.GH_TOKEN ? [`-e GH_TOKEN="${process.env.GH_TOKEN}"`] : []),
757
762
  // NOTE: Do NOT pass CLAUDE_CODE_OAUTH_TOKEN - it overrides credentials file
758
763
  // and setup-token generates invalid tokens. Use "prlt agent auth" instead.
764
+ // Set mount mode to worktree if we have repo worktrees - triggers git wrapper setup
765
+ ...(hasWorktrees ? [`-e PRLT_MOUNT_MODE=worktree`] : []),
759
766
  ];
760
767
  // Resource limits
761
768
  const resourceFlags = [
@@ -1421,12 +1428,11 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
1421
1428
  const claudeCmd = cmdMatch ? cmdMatch[1] : devcontainerCmd;
1422
1429
  // Create a script inside the container that runs claude and keeps shell open
1423
1430
  // TERM must be set for Claude's TUI to render properly
1424
- // Unset DEVCONTAINER and CI to prevent Claude from detecting container/CI environment
1425
- // which might cause it to suppress TUI output
1431
+ // Unset CI to prevent Claude from detecting CI environment which suppresses TUI output
1432
+ // Note: We keep DEVCONTAINER set so prlt workspace detection works correctly
1426
1433
  const tmuxScript = `#!/bin/bash
1427
1434
  export TERM=xterm-256color
1428
1435
  export COLORTERM=truecolor
1429
- unset DEVCONTAINER
1430
1436
  unset CI
1431
1437
  echo "🚀 Starting: ${sessionName}"
1432
1438
  echo ""
@@ -1695,70 +1701,6 @@ exec $SHELL
1695
1701
  };
1696
1702
  }
1697
1703
  }
1698
- /**
1699
- * Legacy: Run devcontainer in host-side tmux (kept for non-container modes)
1700
- */
1701
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1702
- async function runDevcontainerInHostTmux(context, devcontainerCmd, config) {
1703
- const sessionName = config.tmux.session;
1704
- const windowName = buildTmuxWindowName(context);
1705
- try {
1706
- // Check if tmux is available on host
1707
- execSync('which tmux', { stdio: 'pipe' });
1708
- // Write command to temp script
1709
- const baseDir = context.hqPath
1710
- ? path.join(context.hqPath, '.proletariat', 'scripts')
1711
- : path.join(os.homedir(), '.proletariat', 'scripts');
1712
- fs.mkdirSync(baseDir, { recursive: true });
1713
- const scriptPath = path.join(baseDir, `exec-${context.ticketId}-${Date.now()}.sh`);
1714
- const windowTitle = buildWindowTitle(context);
1715
- const setTitleCmds = getSetTitleCommands(windowTitle);
1716
- const scriptContent = `#!/bin/bash
1717
- ${setTitleCmds}
1718
- echo "🚀 Starting ticket execution: ${context.ticketId}"
1719
- ${devcontainerCmd}
1720
- rm -f "${scriptPath}"
1721
- exec $SHELL
1722
- `;
1723
- fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
1724
- // Check if session exists
1725
- let sessionExists = false;
1726
- try {
1727
- execSync(`tmux has-session -t ${sessionName}`, { stdio: 'pipe' });
1728
- sessionExists = true;
1729
- }
1730
- catch (err) {
1731
- console.debug(`[runners:hostTmux] Session ${sessionName} does not exist:`, err);
1732
- sessionExists = false;
1733
- }
1734
- const targetPane = `${sessionName}:${windowName}`;
1735
- if (!sessionExists) {
1736
- execSync(`tmux new-session -d -s ${sessionName} -n "${windowName}"`, { stdio: 'pipe' });
1737
- }
1738
- else if (config.tmux.layout === 'window') {
1739
- // Create new window in existing session (starts with shell)
1740
- execSync(`tmux new-window -t ${sessionName} -n "${windowName}"`, { stdio: 'pipe' });
1741
- }
1742
- else {
1743
- // Split existing pane (starts with shell)
1744
- execSync(`tmux split-window -t ${sessionName} -h`, { stdio: 'pipe' });
1745
- }
1746
- // Send the script command to the shell - execute directly (not source)
1747
- // Using exec replaces the shell, ensuring proper TTY passthrough
1748
- execSync(`tmux send-keys -t "${targetPane}" 'exec ${scriptPath}' Enter`, { stdio: 'pipe' });
1749
- return {
1750
- success: true,
1751
- containerId: `devcontainer-${context.agentName}`,
1752
- sessionId: `${sessionName}:${windowName}`,
1753
- };
1754
- }
1755
- catch (error) {
1756
- return {
1757
- success: false,
1758
- error: error instanceof Error ? error.message : 'Failed to start tmux session',
1759
- };
1760
- }
1761
- }
1762
1704
  // =============================================================================
1763
1705
  // Docker Runner
1764
1706
  // =============================================================================
@@ -13,6 +13,7 @@ import { findHQRoot } from '../repos/index.js';
13
13
  import { hasDevcontainerConfig } from './devcontainer.js';
14
14
  import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
15
15
  import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from './runners.js';
16
+ import { detectRepoWorktrees, resolveWorktreePath } from './context.js';
16
17
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
17
18
  // =============================================================================
18
19
  // Git Utilities
@@ -165,24 +166,9 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
165
166
  error: `Agent directory not found at ${agentDir}`,
166
167
  };
167
168
  }
168
- // Find worktree path for agent
169
- let worktreePath = agentDir;
170
- const agentContents = fs.readdirSync(agentDir);
171
- const repoWorktrees = agentContents.filter(item => {
172
- const itemPath = path.join(agentDir, item);
173
- const gitPath = path.join(itemPath, '.git');
174
- return fs.statSync(itemPath).isDirectory() && fs.existsSync(gitPath);
175
- });
176
- if (repoWorktrees.length === 1) {
177
- worktreePath = path.join(agentDir, repoWorktrees[0]);
178
- }
179
- else if (repoWorktrees.length > 1) {
180
- worktreePath = agentDir;
181
- }
182
- else {
183
- // No git worktrees found - use current directory
184
- worktreePath = process.cwd();
185
- }
169
+ // Detect repository worktrees within agent directory
170
+ const repoWorktrees = detectRepoWorktrees(agentDir);
171
+ const worktreePath = resolveWorktreePath(agentDir, repoWorktrees);
186
172
  // Get coder name for branch naming (prompts on first use)
187
173
  const coderName = await getOrPromptCoderName(db);
188
174
  // Generate branch name
@@ -228,6 +214,7 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
228
214
  branch,
229
215
  hqPath,
230
216
  pmoPath,
217
+ repoWorktrees,
231
218
  createPR: options.createPR ?? false,
232
219
  };
233
220
  // Determine execution environment and display mode
@@ -79,6 +79,7 @@ export interface ExecutionContext {
79
79
  branch: string;
80
80
  hqPath?: string;
81
81
  pmoPath?: string;
82
+ repoWorktrees?: string[];
82
83
  createPR?: boolean;
83
84
  actionId?: string;
84
85
  actionName?: string;