Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 70 additions & 11 deletions apps/cli/src/commands/agent/shell.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Args } from '@oclif/core';
import { Args, Flags } from '@oclif/core';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { execSync, spawn } from 'node:child_process';
Expand All @@ -19,6 +19,10 @@ import {
shouldOutputJson,
} from '../../lib/prompt-json.js';
import { trackChildProcess } from '../../lib/signal-handler.js';
import {
buildShellExports,
parseExecutorEnv,
} from '../../lib/execution/executor-overrides.js';

export default class Shell extends PMOCommand {
static description = 'Open an interactive shell in an agent workspace';
Expand All @@ -37,6 +41,15 @@ export default class Shell extends PMOCommand {

static flags = {
...pmoBaseFlags,
'executor-env': Flags.string({
description:
'Set env var on Claude (KEY=VALUE). Repeatable. ' +
'e.g. --executor-env CLAUDE_CONFIG_DIR=$HOME/.claude-work to switch accounts.',
multiple: true,
}),
'executor-bin': Flags.string({
description: 'Override the claude binary path (e.g. wrapper script). Defaults to "claude".',
}),
};

protected getPMOOptions() {
Expand All @@ -49,6 +62,15 @@ export default class Shell extends PMOCommand {
// Check if JSON output mode is active
const jsonMode = shouldOutputJson(flags);

// PRLT-1369: parse --executor-env / --executor-bin once up front
let executorEnv: Record<string, string> | undefined
try {
executorEnv = parseExecutorEnv(flags['executor-env'] as string[] | undefined)
} catch (err) {
this.error(err instanceof Error ? err.message : String(err))
}
const executorBin = (flags['executor-bin'] as string | undefined) || 'claude'

// Error handling config
const errorConfig = { jsonMode, commandName: 'agent shell', flags };

Expand Down Expand Up @@ -172,9 +194,9 @@ export default class Shell extends PMOCommand {
const dangerMode = permissionMode === 'danger';

if (environment === 'devcontainer') {
await this.openDevcontainerShell(workspaceInfo.path, agentDir, agentName!, displayMode, dangerMode);
await this.openDevcontainerShell(workspaceInfo.path, agentDir, agentName!, displayMode, dangerMode, executorEnv, executorBin);
} else {
await this.openHostShell(workspaceInfo.path, agentDir, agentName!, displayMode, dangerMode);
await this.openHostShell(workspaceInfo.path, agentDir, agentName!, displayMode, dangerMode, executorEnv, executorBin);
}
return;
}
Expand Down Expand Up @@ -245,7 +267,15 @@ export default class Shell extends PMOCommand {
}
}

private async openDevcontainerShell(hqPath: string, agentDir: string, agentName: string, displayMode: 'terminal' | 'foreground', dangerMode: boolean): Promise<void> {
private async openDevcontainerShell(
hqPath: string,
agentDir: string,
agentName: string,
displayMode: 'terminal' | 'foreground',
dangerMode: boolean,
executorEnv?: Record<string, string>,
executorBin: string = 'claude',
): Promise<void> {
// Check Docker is running
if (!isDockerRunning()) {
this.error('Docker is not running. Please start Docker Desktop and try again.');
Expand Down Expand Up @@ -281,8 +311,18 @@ export default class Shell extends PMOCommand {
this.log(colors.success(`✓ Container running: ${containerId}`));
this.log('');

// PRLT-1369: pass --executor-env values into the container as -e KEY=VALUE flags.
const envFlags: string[] = []
if (executorEnv) {
for (const [k, v] of Object.entries(executorEnv)) {
envFlags.push('-e', `${k}=${v}`)
}
}

// The command to run inside the container
const claudeArgs = dangerMode ? ['exec', '-it', '-w', '/workspace', containerId!, 'claude', '--dangerously-skip-permissions'] : ['exec', '-it', '-w', '/workspace', containerId!, 'claude'];
const claudeArgs = dangerMode
? ['exec', '-it', '-w', '/workspace', ...envFlags, containerId!, executorBin, '--dangerously-skip-permissions']
: ['exec', '-it', '-w', '/workspace', ...envFlags, containerId!, executorBin];

if (displayMode === 'foreground') {
// Run Claude directly in current terminal
Expand Down Expand Up @@ -318,9 +358,15 @@ export default class Shell extends PMOCommand {
const scriptPath = path.join(baseDir, `shell-${agentName}-${Date.now()}.sh`);

// Launch claude inside the container
// PRLT-1369: -e KEY=VALUE flags forward env vars; executorBin overrides the binary
const envFlagStr = executorEnv
? ' ' + Object.entries(executorEnv)
.map(([k, v]) => `-e ${k}='${v.replace(/'/g, "'\\''")}'`)
.join(' ')
: ''
const claudeCmd = dangerMode
? `docker exec -it -w /workspace ${containerId} claude --dangerously-skip-permissions`
: `docker exec -it -w /workspace ${containerId} claude`;
? `docker exec -it -w /workspace${envFlagStr} ${containerId} ${executorBin} --dangerously-skip-permissions`
: `docker exec -it -w /workspace${envFlagStr} ${containerId} ${executorBin}`;

const scriptContent = `#!/bin/bash
# Shell for agent ${agentName}
Expand Down Expand Up @@ -351,15 +397,25 @@ exec bash
}
}

private async openHostShell(hqPath: string, agentDir: string, agentName: string, displayMode: 'terminal' | 'foreground', dangerMode: boolean): Promise<void> {
private async openHostShell(
hqPath: string,
agentDir: string,
agentName: string,
displayMode: 'terminal' | 'foreground',
dangerMode: boolean,
executorEnv?: Record<string, string>,
executorBin: string = 'claude',
): Promise<void> {
if (displayMode === 'foreground') {
this.log(colors.text(`Starting Claude Code in ${agentDir}...`));
this.log('');

const claudeArgs = dangerMode ? ['--dangerously-skip-permissions'] : [];
const child = spawn('claude', claudeArgs, {
const child = spawn(executorBin, claudeArgs, {
stdio: 'inherit',
cwd: agentDir,
// PRLT-1369: --executor-env merges into process env (e.g. CLAUDE_CONFIG_DIR)
env: executorEnv ? { ...process.env, ...executorEnv } : process.env,
});

trackChildProcess(child);
Expand All @@ -386,7 +442,10 @@ exec bash
fs.mkdirSync(baseDir, { recursive: true });
const scriptPath = path.join(baseDir, `shell-${agentName}-${Date.now()}.sh`);

const claudeCmd = dangerMode ? 'claude --dangerously-skip-permissions' : 'claude';
const claudeCmd = dangerMode ? `${executorBin} --dangerously-skip-permissions` : executorBin;
// PRLT-1369: emit `export KEY='VALUE'` lines for user-supplied env (e.g. CLAUDE_CONFIG_DIR)
const envExports = buildShellExports(executorEnv);
const envBlock = envExports ? `\n${envExports}\n` : '';

const scriptContent = `#!/bin/bash
# Shell for agent ${agentName}
Expand All @@ -395,7 +454,7 @@ echo "======================================"
echo "Agent Shell: ${agentName} (host)"
echo "======================================"
echo ""

${envBlock}
cd "${agentDir}"

# Run Claude Code
Expand Down
22 changes: 22 additions & 0 deletions apps/cli/src/commands/claude/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
hasShellPreference,
} from '../../lib/execution/config.js'
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js'
import { readExecutorOverridesFromFlags } from '../../lib/execution/executor-overrides.js'

// Catch-all devcontainer image for directories without .devcontainer
const CATCHALL_DEVCONTAINER_IMAGE = 'ghcr.io/chrismcdermut/proletariat-claude:latest'
Expand Down Expand Up @@ -101,6 +102,15 @@ export default class Claude extends PromptCommand {
prompt: Flags.string({
description: 'Initial task/prompt for Claude',
}),
'executor-env': Flags.string({
description:
'Set env var on Claude (KEY=VALUE). Repeatable. ' +
'e.g. --executor-env CLAUDE_CONFIG_DIR=$HOME/.claude-work to switch accounts.',
multiple: true,
}),
'executor-bin': Flags.string({
description: 'Override the claude binary path (e.g. wrapper script). Defaults to "claude".',
}),
directory: Flags.string({
description: 'Directory to run in (default: cwd)',
}),
Expand Down Expand Up @@ -151,6 +161,8 @@ export default class Claude extends PromptCommand {
environment?: string
'display-mode'?: string
prompt?: string
'executor-env'?: string[]
'executor-bin'?: string
json: boolean
},
jsonMode: boolean
Expand Down Expand Up @@ -396,6 +408,7 @@ export default class Claude extends PromptCommand {

// Build minimal execution context for yolo mode
// Use devcontainerConfigDir for worktreePath so runner finds .devcontainer there
const yoloOverrides = readExecutorOverridesFromFlags(flags)
const context: ExecutionContext = {
ticketId: 'ADHOC',
ticketTitle: `Ad-hoc: ${slug}`,
Expand All @@ -406,6 +419,9 @@ export default class Claude extends PromptCommand {
actionName: slug,
actionPrompt: flags.prompt,
modifiesCode: false, // Don't try to create branches
// PRLT-1369: switch Claude accounts / use wrapper binary
executorEnv: yoloOverrides?.env,
executorBin: yoloOverrides?.bin,
}

// Load execution config (use defaults for yolo mode)
Expand Down Expand Up @@ -507,6 +523,8 @@ export default class Claude extends PromptCommand {
prompt?: string
project?: string
title?: string
'executor-env'?: string[]
'executor-bin'?: string
json: boolean
},
jsonMode: boolean
Expand Down Expand Up @@ -850,6 +868,7 @@ export default class Claude extends PromptCommand {
this.log(styles.success(` Agent: ${agentName}`))

// Build execution context
const trackedOverrides = readExecutorOverridesFromFlags(flags)
const context: ExecutionContext = {
ticketId: ticket.id,
ticketTitle: ticket.title,
Expand All @@ -864,6 +883,9 @@ export default class Claude extends PromptCommand {
actionName: 'Ad-hoc',
actionPrompt: flags.prompt,
modifiesCode: false, // Don't manage branches for adhoc
// PRLT-1369: switch Claude accounts / use wrapper binary
executorEnv: trackedOverrides?.env,
executorBin: trackedOverrides?.bin,
}

// Create execution record
Expand Down
14 changes: 14 additions & 0 deletions apps/cli/src/commands/orchestrator/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
runOrchestratorInDocker,
} from '../../lib/execution/runners.js'
import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js'
import { readExecutorOverridesFromFlags } from '../../lib/execution/executor-overrides.js'
import { ExecutionStorage } from '../../lib/execution/storage.js'
import {
createMirrorExecution,
Expand Down Expand Up @@ -238,6 +239,15 @@ export default class OrchestratorStart extends RuntimeCommand {
description: 'Executor type',
options: ['claude-code', 'codex', 'custom'],
}),
'executor-env': Flags.string({
description:
'Set env var on the orchestrator executor (KEY=VALUE). Repeatable. ' +
'e.g. --executor-env CLAUDE_CONFIG_DIR=$HOME/.claude-work',
multiple: true,
}),
'executor-bin': Flags.string({
description: 'Override executor binary path. Defaults to claude/codex.',
}),
'skip-permissions': Flags.boolean({
description: 'Skip permission checks (shorthand for --permission-mode danger)',
default: false,
Expand Down Expand Up @@ -574,6 +584,7 @@ export default class OrchestratorStart extends RuntimeCommand {
// Build execution context
// Use ticketId='prlt', actionName='orchestrator', agentName=orchestratorName
// so buildSessionName produces 'prlt-orchestrator-{name}'
const orchExecutorOverrides = readExecutorOverridesFromFlags(flags)
const context: ExecutionContext = {
ticketId: 'prlt',
ticketTitle: 'Orchestrator',
Expand All @@ -588,6 +599,9 @@ export default class OrchestratorStart extends RuntimeCommand {
isOrchestrator: true,
hqName,
executionEnvironment: environment,
// PRLT-1369: pin orchestrator session to a specific Claude account / binary
executorEnv: orchExecutorOverrides?.env,
executorBin: orchExecutorOverrides?.bin,
}

// Build execution config
Expand Down
22 changes: 22 additions & 0 deletions apps/cli/src/commands/qa/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
hasShellPreference,
} from '../../lib/execution/config.js'
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js'
import { readExecutorOverridesFromFlags } from '../../lib/execution/executor-overrides.js'

// Catch-all devcontainer image for directories without .devcontainer
const CATCHALL_DEVCONTAINER_IMAGE = 'ghcr.io/chrismcdermut/proletariat-claude:latest'
Expand Down Expand Up @@ -86,6 +87,15 @@ export default class QA extends PromptCommand {
prompt: Flags.string({
description: 'Additional instructions to append to the QA prompt',
}),
'executor-env': Flags.string({
description:
'Set env var on Claude (KEY=VALUE). Repeatable. ' +
'e.g. --executor-env CLAUDE_CONFIG_DIR=$HOME/.claude-work to switch accounts.',
multiple: true,
}),
'executor-bin': Flags.string({
description: 'Override the claude binary path (e.g. wrapper script). Defaults to "claude".',
}),
}

async run(): Promise<void> {
Expand Down Expand Up @@ -243,6 +253,8 @@ Clean up your tmux session when done.`,
'display-mode'?: string
'permission-mode'?: string
prompt?: string
'executor-env'?: string[]
'executor-bin'?: string
json: boolean
},
jsonMode: boolean
Expand Down Expand Up @@ -376,6 +388,7 @@ Clean up your tmux session when done.`,
}

// Build execution context
const qaHQOverrides = readExecutorOverridesFromFlags(flags)
const context: ExecutionContext = {
ticketId: ticket.id,
ticketTitle: ticket.title,
Expand All @@ -391,6 +404,9 @@ Clean up your tmux session when done.`,
actionPrompt,
actionEndPrompt: actionData.endPrompt,
modifiesCode: false,
// PRLT-1369: switch Claude accounts / use wrapper binary
executorEnv: qaHQOverrides?.env,
executorBin: qaHQOverrides?.bin,
}

// Create execution record
Expand Down Expand Up @@ -488,6 +504,8 @@ Clean up your tmux session when done.`,
'display-mode'?: string
'permission-mode'?: string
prompt?: string
'executor-env'?: string[]
'executor-bin'?: string
json: boolean
},
jsonMode: boolean
Expand Down Expand Up @@ -553,6 +571,7 @@ Clean up your tmux session when done.`,
}

// Build execution context
const qaYoloOverrides = readExecutorOverridesFromFlags(flags)
const context: ExecutionContext = {
ticketId: 'QA',
ticketTitle: 'Exploratory QA Session',
Expand All @@ -565,6 +584,9 @@ Clean up your tmux session when done.`,
actionPrompt,
actionEndPrompt: actionData.endPrompt,
modifiesCode: false,
// PRLT-1369: switch Claude accounts / use wrapper binary
executorEnv: qaYoloOverrides?.env,
executorBin: qaYoloOverrides?.bin,
}

// Load execution config
Expand Down
Loading
Loading