@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.
@@ -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;
@@ -339,6 +339,27 @@ export function seedBuiltinPhaseTemplates(db) {
339
339
  insertTemplate.run(template.id, template.name, template.description, JSON.stringify(template.phases), now);
340
340
  }
341
341
  }
342
+ /**
343
+ * Rule for agents about using the globally installed prlt command.
344
+ * This prevents agents from wasting time trying to build from source when working in the prlt repo.
345
+ */
346
+ const PRLT_USAGE_RULE = `
347
+ **IMPORTANT RULES**:
348
+
349
+ 1. **Use globally installed prlt**: Always use the \`prlt\` command directly
350
+ 2. **Never use local dev builds**: Do NOT use \`./apps/cli/bin/dev.js\` or \`./apps/cli/bin/run.js\`
351
+ 3. **Why**: You may be working inside the prlt source code. The local builds require dependencies and may not work correctly. The globally installed version is already configured and ready to use.
352
+
353
+ Example (correct):
354
+ \`\`\`bash
355
+ prlt ticket edit TKT-123 --add-ac "Test passes"
356
+ \`\`\`
357
+
358
+ Example (WRONG - do not do this):
359
+ \`\`\`bash
360
+ ./apps/cli/bin/run.js ticket edit TKT-123 ...
361
+ \`\`\`
362
+ `.trim();
342
363
  /**
343
364
  * Seed built-in work actions.
344
365
  */
@@ -348,7 +369,13 @@ export function seedBuiltinActions(db) {
348
369
  id: 'groom',
349
370
  name: 'Groom',
350
371
  description: 'Flesh out ticket with requirements and acceptance criteria',
351
- prompt: `Analyze this ticket and improve its definition:
372
+ prompt: `${PRLT_USAGE_RULE}
373
+
374
+ ---
375
+
376
+ # Action: Groom
377
+
378
+ Analyze this ticket and improve its definition:
352
379
  - Add detailed requirements if missing or vague
353
380
  - Add clear, testable acceptance criteria
354
381
  - Break down into subtasks if the work is complex
@@ -422,7 +449,13 @@ After updating, output a brief summary of your grooming changes.`,
422
449
  id: 'implement',
423
450
  name: 'Implement',
424
451
  description: 'Write code to implement the ticket requirements',
425
- prompt: `Implement this ticket according to its requirements and acceptance criteria:
452
+ prompt: `${PRLT_USAGE_RULE}
453
+
454
+ ---
455
+
456
+ # Action: Implement
457
+
458
+ Implement this ticket according to its requirements and acceptance criteria:
426
459
  - Follow the acceptance criteria exactly
427
460
  - Write clean, well-tested code
428
461
  - Update documentation if the changes affect it
@@ -469,7 +502,13 @@ When complete, the ticket should be ready for code review.`,
469
502
  id: 'continue',
470
503
  name: 'Continue',
471
504
  description: 'Continue working from where you left off',
472
- prompt: `Continue working on this ticket from where you left off.
505
+ prompt: `${PRLT_USAGE_RULE}
506
+
507
+ ---
508
+
509
+ # Action: Continue
510
+
511
+ Continue working on this ticket from where you left off.
473
512
  - Review existing commits and changes to understand current state
474
513
  - Check what subtasks remain incomplete
475
514
  - Complete the remaining work
@@ -508,7 +547,13 @@ git add -A && prlt commit "your change" && git push
508
547
  id: 'test',
509
548
  name: 'Write Tests',
510
549
  description: 'Add comprehensive tests for the implementation',
511
- prompt: `Write comprehensive tests for this ticket's implementation:
550
+ prompt: `${PRLT_USAGE_RULE}
551
+
552
+ ---
553
+
554
+ # Action: Write Tests
555
+
556
+ Write comprehensive tests for this ticket's implementation:
512
557
  - Add unit tests for core functionality
513
558
  - Add integration tests where appropriate
514
559
  - Cover edge cases and error handling
@@ -567,7 +612,13 @@ No commits are needed for code review.`,
567
612
  id: 'revise',
568
613
  name: 'Revise',
569
614
  description: 'Address PR feedback and review comments',
570
- prompt: `Address the feedback on this ticket's pull request:
615
+ prompt: `${PRLT_USAGE_RULE}
616
+
617
+ ---
618
+
619
+ # Action: Revise
620
+
621
+ Address the feedback on this ticket's pull request:
571
622
  - Review all comments and requested changes carefully
572
623
  - Make the necessary code changes to address each point
573
624
  - Respond to questions with explanations