@proletariat/cli 0.3.45 → 0.3.46
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/dist/commands/config/index.js +39 -1
- package/dist/commands/linear/auth.d.ts +14 -0
- package/dist/commands/linear/auth.js +211 -0
- package/dist/commands/linear/import.d.ts +21 -0
- package/dist/commands/linear/import.js +260 -0
- package/dist/commands/linear/status.d.ts +11 -0
- package/dist/commands/linear/status.js +88 -0
- package/dist/commands/linear/sync.d.ts +15 -0
- package/dist/commands/linear/sync.js +233 -0
- package/dist/commands/orchestrator/attach.d.ts +9 -1
- package/dist/commands/orchestrator/attach.js +67 -13
- package/dist/commands/orchestrator/index.js +22 -7
- package/dist/commands/ticket/link/duplicates.d.ts +15 -0
- package/dist/commands/ticket/link/duplicates.js +95 -0
- package/dist/commands/ticket/link/index.js +14 -0
- package/dist/commands/ticket/link/relates.d.ts +15 -0
- package/dist/commands/ticket/link/relates.js +95 -0
- package/dist/commands/work/revise.js +4 -3
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +75 -19
- package/dist/lib/execution/config.d.ts +15 -0
- package/dist/lib/execution/config.js +54 -0
- package/dist/lib/execution/devcontainer.d.ts +6 -3
- package/dist/lib/execution/devcontainer.js +39 -12
- package/dist/lib/execution/runners.d.ts +28 -32
- package/dist/lib/execution/runners.js +345 -275
- package/dist/lib/execution/spawner.js +62 -5
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/execution/types.js +3 -0
- package/dist/lib/external-issues/adapters.d.ts +26 -0
- package/dist/lib/external-issues/adapters.js +251 -0
- package/dist/lib/external-issues/index.d.ts +10 -0
- package/dist/lib/external-issues/index.js +14 -0
- package/dist/lib/external-issues/mapper.d.ts +21 -0
- package/dist/lib/external-issues/mapper.js +86 -0
- package/dist/lib/external-issues/types.d.ts +144 -0
- package/dist/lib/external-issues/types.js +26 -0
- package/dist/lib/external-issues/validation.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +219 -0
- package/dist/lib/linear/client.d.ts +55 -0
- package/dist/lib/linear/client.js +254 -0
- package/dist/lib/linear/config.d.ts +37 -0
- package/dist/lib/linear/config.js +100 -0
- package/dist/lib/linear/index.d.ts +11 -0
- package/dist/lib/linear/index.js +10 -0
- package/dist/lib/linear/mapper.d.ts +67 -0
- package/dist/lib/linear/mapper.js +219 -0
- package/dist/lib/linear/sync.d.ts +37 -0
- package/dist/lib/linear/sync.js +89 -0
- package/dist/lib/linear/types.d.ts +139 -0
- package/dist/lib/linear/types.js +34 -0
- package/dist/lib/mcp/helpers.d.ts +8 -0
- package/dist/lib/mcp/helpers.js +10 -0
- package/dist/lib/mcp/tools/board.js +63 -11
- package/dist/lib/pmo/schema.d.ts +2 -0
- package/dist/lib/pmo/schema.js +20 -0
- package/dist/lib/pmo/storage/base.js +92 -13
- package/dist/lib/pmo/storage/dependencies.js +15 -0
- package/dist/lib/prompt-json.d.ts +4 -0
- package/oclif.manifest.json +2867 -2380
- package/package.json +2 -1
|
@@ -10,6 +10,7 @@ import * as path from 'node:path';
|
|
|
10
10
|
import * as os from 'node:os';
|
|
11
11
|
import { DEFAULT_EXECUTION_CONFIG, } from './types.js';
|
|
12
12
|
import { getSetTitleCommands } from '../terminal.js';
|
|
13
|
+
import { readDevcontainerJson } from './devcontainer.js';
|
|
13
14
|
// =============================================================================
|
|
14
15
|
// Terminal Title Helpers
|
|
15
16
|
// =============================================================================
|
|
@@ -87,49 +88,6 @@ export function configureITermTmuxWindowMode(mode) {
|
|
|
87
88
|
configureITermTmuxPreferences(mode);
|
|
88
89
|
}
|
|
89
90
|
// =============================================================================
|
|
90
|
-
// Background Mode Cleanup Helpers (TKT-988)
|
|
91
|
-
// =============================================================================
|
|
92
|
-
/**
|
|
93
|
-
* Build the tmux script that runs inside the container.
|
|
94
|
-
* In background mode: kills PID 1 (sleep infinity) after Claude exits to stop/remove container.
|
|
95
|
-
* In terminal/foreground mode: drops into exec bash for user inspection.
|
|
96
|
-
*/
|
|
97
|
-
export function buildTmuxScript(sessionName, claudeCmd, displayMode) {
|
|
98
|
-
if (displayMode === 'background') {
|
|
99
|
-
return `#!/bin/bash
|
|
100
|
-
export TERM=xterm-256color
|
|
101
|
-
export COLORTERM=truecolor
|
|
102
|
-
unset CI
|
|
103
|
-
unset CLAUDECODE
|
|
104
|
-
echo "🚀 Starting: ${sessionName}"
|
|
105
|
-
echo ""
|
|
106
|
-
${claudeCmd}
|
|
107
|
-
echo ""
|
|
108
|
-
echo "✅ Agent work complete. Cleaning up container..."
|
|
109
|
-
kill 1
|
|
110
|
-
`;
|
|
111
|
-
}
|
|
112
|
-
return `#!/bin/bash
|
|
113
|
-
export TERM=xterm-256color
|
|
114
|
-
export COLORTERM=truecolor
|
|
115
|
-
unset CI
|
|
116
|
-
unset CLAUDECODE
|
|
117
|
-
echo "🚀 Starting: ${sessionName}"
|
|
118
|
-
echo ""
|
|
119
|
-
${claudeCmd}
|
|
120
|
-
echo ""
|
|
121
|
-
echo "✅ Agent work complete. Press Enter to close or run more commands."
|
|
122
|
-
exec bash
|
|
123
|
-
`;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Get the auto-remove flags for docker run based on display mode.
|
|
127
|
-
* Background mode containers get --rm so Docker removes them when they stop.
|
|
128
|
-
*/
|
|
129
|
-
export function getDockerAutoRemoveFlags(displayMode) {
|
|
130
|
-
return displayMode === 'background' ? ['--rm'] : [];
|
|
131
|
-
}
|
|
132
|
-
// =============================================================================
|
|
133
91
|
// Docker Credential Helpers
|
|
134
92
|
// =============================================================================
|
|
135
93
|
const CLAUDE_CREDENTIALS_VOLUME = 'claude-credentials';
|
|
@@ -190,150 +148,116 @@ export function getDockerCredentialInfo() {
|
|
|
190
148
|
return null;
|
|
191
149
|
}
|
|
192
150
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
function
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Executor Commands
|
|
153
|
+
// =============================================================================
|
|
154
|
+
export function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
197
155
|
switch (executor) {
|
|
198
156
|
case 'claude-code':
|
|
199
|
-
|
|
157
|
+
if (skipPermissions) {
|
|
158
|
+
// Skip permissions - agent runs autonomously without prompting
|
|
159
|
+
// Note: NO -p flag - we want interactive mode for streaming output in terminal
|
|
160
|
+
// --permission-mode bypassPermissions: skips the "trust this folder" dialog
|
|
161
|
+
// --dangerously-skip-permissions: skips tool permission checks
|
|
162
|
+
// --effort high: skips the effort level prompt (TKT-1134)
|
|
163
|
+
return { cmd: 'claude', args: ['--permission-mode', 'bypassPermissions', '--dangerously-skip-permissions', '--effort', 'high', prompt] };
|
|
164
|
+
}
|
|
165
|
+
// Manual mode - will prompt for each action (still interactive, no -p)
|
|
166
|
+
return { cmd: 'claude', args: [prompt] };
|
|
200
167
|
case 'codex':
|
|
201
|
-
return 'codex';
|
|
168
|
+
return { cmd: 'codex', args: ['--prompt', prompt] };
|
|
202
169
|
case 'aider':
|
|
203
|
-
return 'aider';
|
|
170
|
+
return { cmd: 'aider', args: ['--message', prompt] };
|
|
204
171
|
case 'custom':
|
|
205
|
-
|
|
172
|
+
// Custom executor should be configured
|
|
173
|
+
return { cmd: 'echo', args: ['Custom executor not configured'] };
|
|
206
174
|
default:
|
|
207
|
-
|
|
175
|
+
if (skipPermissions) {
|
|
176
|
+
// Note: NO -p flag - we want interactive mode for streaming output
|
|
177
|
+
// --effort high: skips the effort level prompt (TKT-1134)
|
|
178
|
+
return { cmd: 'claude', args: ['--permission-mode', 'bypassPermissions', '--dangerously-skip-permissions', '--effort', 'high', prompt] };
|
|
179
|
+
}
|
|
180
|
+
return { cmd: 'claude', args: [prompt] };
|
|
208
181
|
}
|
|
209
182
|
}
|
|
210
183
|
/**
|
|
211
|
-
*
|
|
184
|
+
* Check if an executor is Claude Code.
|
|
185
|
+
* Used to gate Claude-specific flags and configuration.
|
|
212
186
|
*/
|
|
213
|
-
function
|
|
187
|
+
export function isClaudeExecutor(executor) {
|
|
188
|
+
return executor === 'claude-code';
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get the display name for an executor type.
|
|
192
|
+
*/
|
|
193
|
+
export function getExecutorDisplayName(executor) {
|
|
214
194
|
switch (executor) {
|
|
215
|
-
case 'claude-code':
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
case '
|
|
219
|
-
|
|
220
|
-
' Then authenticate: set OPENAI_API_KEY or run codex --login';
|
|
221
|
-
case 'aider':
|
|
222
|
-
return 'Install aider: pip install aider-chat\n' +
|
|
223
|
-
' Then authenticate: set OPENAI_API_KEY or ANTHROPIC_API_KEY';
|
|
224
|
-
case 'custom':
|
|
225
|
-
return 'Configure a custom executor command in your execution settings.';
|
|
226
|
-
default:
|
|
227
|
-
return 'Install the required executor and ensure it is on your PATH.';
|
|
195
|
+
case 'claude-code': return 'Claude Code';
|
|
196
|
+
case 'codex': return 'Codex';
|
|
197
|
+
case 'aider': return 'Aider';
|
|
198
|
+
case 'custom': return 'Custom';
|
|
199
|
+
default: return 'Claude Code';
|
|
228
200
|
}
|
|
229
201
|
}
|
|
230
202
|
/**
|
|
231
|
-
*
|
|
232
|
-
|
|
233
|
-
|
|
203
|
+
* Get the npm package name for an executor (for container installation).
|
|
204
|
+
*/
|
|
205
|
+
export function getExecutorPackage(executor) {
|
|
206
|
+
switch (executor) {
|
|
207
|
+
case 'claude-code': return '@anthropic-ai/claude-code';
|
|
208
|
+
case 'codex': return '@openai/codex';
|
|
209
|
+
case 'aider': return null; // aider is Python-based, installed via pip
|
|
210
|
+
case 'custom': return null;
|
|
211
|
+
default: return '@anthropic-ai/claude-code';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check executor binary availability on host.
|
|
234
216
|
*/
|
|
235
217
|
export function checkExecutorOnHost(executor) {
|
|
236
|
-
const
|
|
218
|
+
const { cmd } = getExecutorCommand(executor, 'preflight');
|
|
237
219
|
try {
|
|
238
|
-
execSync(
|
|
239
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
240
|
-
timeout: 10000,
|
|
241
|
-
});
|
|
220
|
+
execSync(`command -v ${cmd}`, { stdio: 'pipe' });
|
|
242
221
|
return { ok: true };
|
|
243
222
|
}
|
|
244
223
|
catch {
|
|
224
|
+
const pkg = getExecutorPackage(executor);
|
|
225
|
+
const installHint = pkg ? `Install it with: npm install -g ${pkg}` : 'Install and configure the executor binary.';
|
|
245
226
|
return {
|
|
246
227
|
ok: false,
|
|
247
|
-
error:
|
|
248
|
-
`Remediation:\n ${getExecutorRemediationHint(executor)}`,
|
|
228
|
+
error: `${getExecutorDisplayName(executor)} CLI not found on host (missing "${cmd}"). ${installHint}`,
|
|
249
229
|
};
|
|
250
230
|
}
|
|
251
231
|
}
|
|
252
232
|
/**
|
|
253
|
-
* Check
|
|
254
|
-
* Returns a PreflightResult with ok=true if the binary is found,
|
|
255
|
-
* or ok=false with a descriptive error and remediation hint.
|
|
233
|
+
* Check executor binary availability inside a container.
|
|
256
234
|
*/
|
|
257
235
|
export function checkExecutorInContainer(executor, containerId) {
|
|
258
|
-
const
|
|
236
|
+
const { cmd } = getExecutorCommand(executor, 'preflight');
|
|
259
237
|
try {
|
|
260
|
-
execSync(`docker exec ${containerId}
|
|
261
|
-
stdio: 'pipe',
|
|
262
|
-
timeout: 10000,
|
|
263
|
-
});
|
|
238
|
+
execSync(`docker exec ${containerId} sh -lc 'command -v ${cmd}'`, { stdio: 'pipe' });
|
|
264
239
|
return { ok: true };
|
|
265
240
|
}
|
|
266
241
|
catch {
|
|
242
|
+
const pkg = getExecutorPackage(executor);
|
|
243
|
+
const installHint = pkg ? `Container image is missing ${pkg}.` : `Container image is missing "${cmd}".`;
|
|
267
244
|
return {
|
|
268
245
|
ok: false,
|
|
269
|
-
error:
|
|
270
|
-
`Remediation:\n Ensure "${binary}" is installed in the devcontainer image.\n ` +
|
|
271
|
-
getExecutorRemediationHint(executor),
|
|
246
|
+
error: `${getExecutorDisplayName(executor)} CLI not found in container (missing "${cmd}"). ${installHint}`,
|
|
272
247
|
};
|
|
273
248
|
}
|
|
274
249
|
}
|
|
275
250
|
/**
|
|
276
|
-
* Run preflight checks for
|
|
277
|
-
* Validates that the executor binary is available before spawning.
|
|
278
|
-
*
|
|
279
|
-
* Checks performed per environment:
|
|
280
|
-
* - host: Verify binary on PATH
|
|
281
|
-
* - devcontainer: Verify binary inside container (if container running)
|
|
282
|
-
* - docker: Verify binary on host (used in docker run command)
|
|
283
|
-
* - vm: Verify binary on host (will be checked on remote separately)
|
|
251
|
+
* Run executor preflight checks for the target environment.
|
|
284
252
|
*/
|
|
285
|
-
export function runExecutorPreflight(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return checkExecutorOnHost(executor);
|
|
289
|
-
case 'devcontainer':
|
|
290
|
-
// For devcontainer, check inside the container if it's already running
|
|
291
|
-
if (containerId) {
|
|
292
|
-
return checkExecutorInContainer(executor, containerId);
|
|
293
|
-
}
|
|
294
|
-
// Container not yet running - will be checked after container start
|
|
295
|
-
return { ok: true };
|
|
296
|
-
case 'docker':
|
|
297
|
-
// Docker runner builds the command on host, executor runs inside container
|
|
298
|
-
// Can't check until container is created, so skip for now
|
|
299
|
-
return { ok: true };
|
|
300
|
-
case 'vm':
|
|
301
|
-
// VM executor runs remotely - can't check from host
|
|
302
|
-
// Could add SSH-based check in the future
|
|
303
|
-
return { ok: true };
|
|
304
|
-
default:
|
|
305
|
-
return { ok: true };
|
|
253
|
+
export function runExecutorPreflight(environment, executor, options) {
|
|
254
|
+
if (environment === 'host') {
|
|
255
|
+
return checkExecutorOnHost(executor);
|
|
306
256
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// Executor Commands
|
|
310
|
-
// =============================================================================
|
|
311
|
-
export function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
312
|
-
switch (executor) {
|
|
313
|
-
case 'claude-code':
|
|
314
|
-
if (skipPermissions) {
|
|
315
|
-
// Skip permissions - agent runs autonomously without prompting
|
|
316
|
-
// Note: NO -p flag - we want interactive mode for streaming output in terminal
|
|
317
|
-
// --permission-mode bypassPermissions: skips the "trust this folder" dialog
|
|
318
|
-
// --dangerously-skip-permissions: skips tool permission checks
|
|
319
|
-
return { cmd: 'claude', args: ['--permission-mode', 'bypassPermissions', '--dangerously-skip-permissions', prompt] };
|
|
320
|
-
}
|
|
321
|
-
// Manual mode - will prompt for each action (still interactive, no -p)
|
|
322
|
-
return { cmd: 'claude', args: [prompt] };
|
|
323
|
-
case 'codex':
|
|
324
|
-
return { cmd: 'codex', args: ['--prompt', prompt] };
|
|
325
|
-
case 'aider':
|
|
326
|
-
return { cmd: 'aider', args: ['--message', prompt] };
|
|
327
|
-
case 'custom':
|
|
328
|
-
// Custom executor should be configured
|
|
329
|
-
return { cmd: 'echo', args: ['Custom executor not configured'] };
|
|
330
|
-
default:
|
|
331
|
-
if (skipPermissions) {
|
|
332
|
-
// Note: NO -p flag - we want interactive mode for streaming output
|
|
333
|
-
return { cmd: 'claude', args: ['--permission-mode', 'bypassPermissions', '--dangerously-skip-permissions', prompt] };
|
|
334
|
-
}
|
|
335
|
-
return { cmd: 'claude', args: [prompt] };
|
|
257
|
+
if (environment === 'devcontainer' && options?.containerId) {
|
|
258
|
+
return checkExecutorInContainer(executor, options.containerId);
|
|
336
259
|
}
|
|
260
|
+
return { ok: true };
|
|
337
261
|
}
|
|
338
262
|
function buildPrompt(context) {
|
|
339
263
|
let prompt = '';
|
|
@@ -452,7 +376,7 @@ export async function runHost(context, executor, config, displayMode = 'terminal
|
|
|
452
376
|
const prompt = buildPrompt(context);
|
|
453
377
|
// Terminal - use sandboxed setting
|
|
454
378
|
const skipPermissions = !config.sandboxed;
|
|
455
|
-
const { cmd } = getExecutorCommand(executor, prompt, skipPermissions);
|
|
379
|
+
const { cmd, args } = getExecutorCommand(executor, prompt, skipPermissions);
|
|
456
380
|
// Write command to temp script to avoid shell escaping issues
|
|
457
381
|
// Use HQ .proletariat/scripts if available, otherwise fallback to home dir
|
|
458
382
|
const baseDir = context.hqPath
|
|
@@ -464,23 +388,36 @@ export async function runHost(context, executor, config, displayMode = 'terminal
|
|
|
464
388
|
const promptPath = path.join(baseDir, `prompt-${context.ticketId}-${timestamp}.txt`);
|
|
465
389
|
// Write prompt to separate file to avoid any shell escaping issues
|
|
466
390
|
fs.writeFileSync(promptPath, prompt, { mode: 0o644 });
|
|
467
|
-
// Build
|
|
468
|
-
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
391
|
+
// Build the executor command using getExecutorCommand() output
|
|
392
|
+
// For Claude Code, we also support outputMode and additional flags
|
|
393
|
+
// For non-Claude executors, we use the command as-is from getExecutorCommand()
|
|
394
|
+
let executorInvocation;
|
|
395
|
+
if (isClaudeExecutor(executor)) {
|
|
396
|
+
// Build flags based on config - Claude-specific flags
|
|
397
|
+
const permissionsFlag = skipPermissions ? '--dangerously-skip-permissions ' : '';
|
|
398
|
+
// outputMode: 'print' adds -p flag (final result only), 'interactive' shows streaming UI
|
|
399
|
+
const printFlag = config.outputMode === 'print' ? '-p ' : '';
|
|
400
|
+
// --effort high: skips the effort level prompt for automated agents (TKT-1134)
|
|
401
|
+
const effortFlag = skipPermissions ? '--effort high ' : '';
|
|
402
|
+
executorInvocation = `${cmd} ${permissionsFlag}${effortFlag}${printFlag}"$(cat "$PROMPT_PATH")"`;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// Non-Claude executors: build command from getExecutorCommand() args
|
|
406
|
+
// Replace the prompt in args with a file read to avoid shell escaping
|
|
407
|
+
const argsWithFile = args.map(a => a === prompt ? '"$(cat "$PROMPT_PATH")"' : `"${a}"`);
|
|
408
|
+
executorInvocation = `${cmd} ${argsWithFile.join(' ')}`;
|
|
409
|
+
}
|
|
410
|
+
// Build script that runs executor and keeps shell open after completion
|
|
472
411
|
const setTitleCmds = getSetTitleCommands(windowTitle);
|
|
473
412
|
const scriptContent = `#!/bin/bash
|
|
474
413
|
# Auto-generated script for ticket ${context.ticketId}
|
|
475
|
-
unset CI
|
|
476
|
-
unset CLAUDECODE
|
|
477
414
|
SCRIPT_PATH="${scriptPath}"
|
|
478
415
|
PROMPT_PATH="${promptPath}"
|
|
479
416
|
${setTitleCmds}
|
|
480
417
|
echo "🚀 Starting: ${sessionName}"
|
|
481
418
|
echo ""
|
|
482
419
|
cd "${context.worktreePath}"
|
|
483
|
-
${
|
|
420
|
+
${executorInvocation}
|
|
484
421
|
|
|
485
422
|
# Clean up script and prompt files
|
|
486
423
|
rm -f "$SCRIPT_PATH" "$PROMPT_PATH"
|
|
@@ -805,6 +742,24 @@ export function isDevcontainerCliInstalled() {
|
|
|
805
742
|
// =============================================================================
|
|
806
743
|
// Docker Container Management (Raw Docker, no devcontainer CLI)
|
|
807
744
|
// =============================================================================
|
|
745
|
+
/**
|
|
746
|
+
* Get the host's installed prlt CLI version.
|
|
747
|
+
* Returns the semver version string (e.g., "0.3.35") or null if not available.
|
|
748
|
+
* Used to ensure containers run the same prlt version as the host (TKT-1029).
|
|
749
|
+
*/
|
|
750
|
+
function getHostPrltVersion() {
|
|
751
|
+
try {
|
|
752
|
+
const output = execSync('prlt --version', {
|
|
753
|
+
encoding: 'utf-8',
|
|
754
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
755
|
+
}).trim();
|
|
756
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
757
|
+
return match ? match[1] : null;
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
808
763
|
/**
|
|
809
764
|
* Get the container name for an agent.
|
|
810
765
|
* Format: prlt-agent-{agentName}
|
|
@@ -900,7 +855,7 @@ function imageExists(imageName) {
|
|
|
900
855
|
* Create and start a Docker container for an agent.
|
|
901
856
|
* Uses raw Docker commands instead of devcontainer CLI.
|
|
902
857
|
*/
|
|
903
|
-
function createDockerContainer(context, containerName, imageName, config,
|
|
858
|
+
function createDockerContainer(context, containerName, imageName, config, executor = 'claude-code', prltInfo) {
|
|
904
859
|
// Build mount flags
|
|
905
860
|
// KEY: Use a named Docker volume for Claude credentials - this is how devcontainer.json
|
|
906
861
|
// was handling it. The volume persists across containers, so login once = logged in everywhere.
|
|
@@ -923,10 +878,14 @@ function createDockerContainer(context, containerName, imageName, config, displa
|
|
|
923
878
|
// These mounts make those paths accessible inside the container at /hq/repos/{repoName}
|
|
924
879
|
...(context.repoWorktrees || []).map(repoName => `-v "${context.hqPath}/repos/${repoName}:/hq/repos/${repoName}:cached"`),
|
|
925
880
|
// Claude credentials - shared named volume (login once, all containers share)
|
|
926
|
-
|
|
881
|
+
// Only needed for Claude Code executor
|
|
882
|
+
...(isClaudeExecutor(executor) ? [`-v "claude-credentials:/home/node/.claude"`] : []),
|
|
927
883
|
];
|
|
928
884
|
// Build environment flags
|
|
929
885
|
const hasWorktrees = context.repoWorktrees && context.repoWorktrees.length > 0;
|
|
886
|
+
const firewallAllowlistDomains = [...new Set((config.firewall?.allowlistDomains || [])
|
|
887
|
+
.map(domain => domain.trim().toLowerCase())
|
|
888
|
+
.filter(domain => /^[a-z0-9.-]+$/.test(domain)))];
|
|
930
889
|
const envVars = [
|
|
931
890
|
`-e DEVCONTAINER=true`,
|
|
932
891
|
`-e PRLT_HQ_PATH=/hq`,
|
|
@@ -938,10 +897,16 @@ function createDockerContainer(context, containerName, imageName, config, displa
|
|
|
938
897
|
...(context.useApiKey && process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
|
|
939
898
|
...(process.env.GITHUB_TOKEN ? [`-e GITHUB_TOKEN="${process.env.GITHUB_TOKEN}"`] : []),
|
|
940
899
|
...(process.env.GH_TOKEN ? [`-e GH_TOKEN="${process.env.GH_TOKEN}"`] : []),
|
|
900
|
+
...(firewallAllowlistDomains.length > 0 ? [`-e PRLT_EXTRA_ALLOWLIST_DOMAINS="${firewallAllowlistDomains.join(',')}"`] : []),
|
|
941
901
|
// NOTE: Do NOT pass CLAUDE_CODE_OAUTH_TOKEN - it overrides credentials file
|
|
942
902
|
// and setup-token generates invalid tokens. Use "prlt agent auth" instead.
|
|
943
903
|
// Set mount mode to worktree if we have repo worktrees - triggers git wrapper setup
|
|
944
904
|
...(hasWorktrees ? [`-e PRLT_MOUNT_MODE=worktree`] : []),
|
|
905
|
+
// Pass prlt version info for setup-prlt.sh to verify/update at container start (TKT-1029)
|
|
906
|
+
...(prltInfo ? [
|
|
907
|
+
`-e PRLT_REGISTRY="${prltInfo.registry}"`,
|
|
908
|
+
`-e PRLT_VERSION="${prltInfo.version}"`,
|
|
909
|
+
] : []),
|
|
945
910
|
];
|
|
946
911
|
// Resource limits
|
|
947
912
|
const resourceFlags = [
|
|
@@ -954,16 +919,12 @@ function createDockerContainer(context, containerName, imageName, config, displa
|
|
|
954
919
|
'--cap-add=NET_RAW', // For firewall setup
|
|
955
920
|
// Note: After firewall is set up, the container is network-restricted
|
|
956
921
|
];
|
|
957
|
-
// Auto-remove container on stop for background mode (R5)
|
|
958
|
-
// Background containers should be cleaned up after work completes — nobody will attach to inspect
|
|
959
|
-
const autoRemoveFlags = getDockerAutoRemoveFlags(displayMode);
|
|
960
922
|
try {
|
|
961
923
|
const createCmd = [
|
|
962
924
|
'docker run -d',
|
|
963
925
|
`--name ${containerName}`,
|
|
964
926
|
'--user node',
|
|
965
927
|
'-w /workspace',
|
|
966
|
-
...autoRemoveFlags,
|
|
967
928
|
...mounts,
|
|
968
929
|
...envVars,
|
|
969
930
|
...resourceFlags,
|
|
@@ -985,8 +946,9 @@ function createDockerContainer(context, containerName, imageName, config, displa
|
|
|
985
946
|
* This includes firewall initialization, prlt setup, and Claude settings.
|
|
986
947
|
* @param containerId - Docker container ID
|
|
987
948
|
* @param sandboxed - Whether running in safe mode (true) or danger mode (false)
|
|
949
|
+
* @param executor - Which executor is being used (determines Claude-specific setup)
|
|
988
950
|
*/
|
|
989
|
-
function runContainerSetup(containerId, sandboxed = true) {
|
|
951
|
+
function runContainerSetup(containerId, sandboxed = true, executor = 'claude-code') {
|
|
990
952
|
try {
|
|
991
953
|
// Run firewall init (requires sudo since we're running as node user)
|
|
992
954
|
execSync(`docker exec ${containerId} sudo /usr/local/bin/init-firewall.sh`, { stdio: 'pipe' });
|
|
@@ -1009,67 +971,109 @@ function runContainerSetup(containerId, sandboxed = true) {
|
|
|
1009
971
|
// Non-fatal - pnpm may not be installed in all containers
|
|
1010
972
|
}
|
|
1011
973
|
// Copy Claude settings file (.claude.json) from host to container
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
974
|
+
// Only needed for Claude Code executor - other executors have their own config
|
|
975
|
+
if (isClaudeExecutor(executor)) {
|
|
976
|
+
// This is needed for Claude Code to recognize settings and bypass prompts
|
|
977
|
+
// Note: Auth tokens are in the claude-credentials volume at /home/node/.claude/.credentials.json
|
|
978
|
+
// But settings (.claude.json) need to be at /home/node/.claude.json (outside the .claude dir)
|
|
979
|
+
try {
|
|
980
|
+
const hostClaudeJson = path.join(os.homedir(), '.claude.json');
|
|
981
|
+
let settings = {};
|
|
982
|
+
if (fs.existsSync(hostClaudeJson)) {
|
|
983
|
+
// Read host file content as base
|
|
984
|
+
const content = fs.readFileSync(hostClaudeJson, 'utf-8');
|
|
985
|
+
try {
|
|
986
|
+
settings = JSON.parse(content);
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
console.debug('[runners:docker] Failed to parse host .claude.json, using empty settings');
|
|
990
|
+
}
|
|
1023
991
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
992
|
+
// Only set bypassPermissionsModeAccepted when user chose danger mode (!sandboxed)
|
|
993
|
+
// This doesn't modify the host file - only the container copy
|
|
994
|
+
if (!sandboxed) {
|
|
995
|
+
settings.bypassPermissionsModeAccepted = true;
|
|
1026
996
|
}
|
|
997
|
+
// Skip first-run onboarding (theme picker, tips, etc.) for automated agents
|
|
998
|
+
// These flags indicate Claude Code has been run before
|
|
999
|
+
settings.numStartups = settings.numStartups || 1;
|
|
1000
|
+
settings.hasCompletedOnboarding = true;
|
|
1001
|
+
settings.theme = settings.theme || 'dark';
|
|
1002
|
+
// Ensure tipsHistory exists to prevent tip prompts
|
|
1003
|
+
if (!settings.tipsHistory || typeof settings.tipsHistory !== 'object') {
|
|
1004
|
+
settings.tipsHistory = {};
|
|
1005
|
+
}
|
|
1006
|
+
const tips = settings.tipsHistory;
|
|
1007
|
+
tips['new-user-warmup'] = tips['new-user-warmup'] || 1;
|
|
1008
|
+
// Dismiss the effort level callout so agents aren't prompted (TKT-1134)
|
|
1009
|
+
settings.effortCalloutDismissed = true;
|
|
1010
|
+
// Pre-accept the "trust this folder" dialog for /workspace (TKT-1134)
|
|
1011
|
+
// Claude Code stores trust per-project under projects[path].hasTrustDialogAccepted
|
|
1012
|
+
// Without this, agents get stuck on the workspace safety prompt
|
|
1013
|
+
if (!settings.projects || typeof settings.projects !== 'object') {
|
|
1014
|
+
settings.projects = {};
|
|
1015
|
+
}
|
|
1016
|
+
const projects = settings.projects;
|
|
1017
|
+
// Accept trust for /workspace and root / to cover all container working directories
|
|
1018
|
+
for (const projectPath of ['/workspace', '/']) {
|
|
1019
|
+
if (!projects[projectPath]) {
|
|
1020
|
+
projects[projectPath] = {};
|
|
1021
|
+
}
|
|
1022
|
+
projects[projectPath].hasTrustDialogAccepted = true;
|
|
1023
|
+
projects[projectPath].hasCompletedProjectOnboarding = true;
|
|
1024
|
+
}
|
|
1025
|
+
// Pipe settings via stdin to avoid ARG_MAX limits with large .claude.json files
|
|
1026
|
+
const settingsJson = JSON.stringify(settings);
|
|
1027
|
+
// Write to container at /home/node/.claude.json using stdin piping
|
|
1028
|
+
execSync(`docker exec -i ${containerId} bash -c 'cat > /home/node/.claude.json'`, { input: settingsJson, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1029
|
+
console.debug(`[runners:docker] Copied .claude.json settings to container (bypassPermissionsModeAccepted=${!sandboxed})`);
|
|
1030
|
+
// Write ~/.claude/settings.json to skip the dangerous mode permission prompt (TKT-1134)
|
|
1031
|
+
// This prevents Claude Code from prompting about permission mode on first run
|
|
1032
|
+
const claudeSettings = JSON.stringify({ skipDangerousModePermissionPrompt: true });
|
|
1033
|
+
execSync(`docker exec -i ${containerId} bash -c 'mkdir -p /home/node/.claude && cat > /home/node/.claude/settings.json'`, { input: claudeSettings, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1034
|
+
console.debug(`[runners:docker] Wrote ~/.claude/settings.json to container`);
|
|
1027
1035
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
settings.bypassPermissionsModeAccepted = true;
|
|
1032
|
-
}
|
|
1033
|
-
// Skip first-run onboarding (theme picker, tips, etc.) for automated agents
|
|
1034
|
-
// These flags indicate Claude Code has been run before
|
|
1035
|
-
settings.numStartups = settings.numStartups || 1;
|
|
1036
|
-
settings.hasCompletedOnboarding = true;
|
|
1037
|
-
settings.theme = settings.theme || 'dark';
|
|
1038
|
-
// Ensure tipsHistory exists to prevent tip prompts
|
|
1039
|
-
if (!settings.tipsHistory || typeof settings.tipsHistory !== 'object') {
|
|
1040
|
-
settings.tipsHistory = {};
|
|
1036
|
+
catch (error) {
|
|
1037
|
+
console.debug('[runners:docker] Failed to copy Claude settings to container:', error);
|
|
1038
|
+
// Non-fatal - Claude will just prompt for settings
|
|
1041
1039
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
// Write to container at /home/node/.claude.json using stdin piping
|
|
1047
|
-
execSync(`docker exec -i ${containerId} bash -c 'cat > /home/node/.claude.json'`, { input: settingsJson, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1048
|
-
console.debug(`[runners:docker] Copied .claude.json settings to container (bypassPermissionsModeAccepted=${!sandboxed})`);
|
|
1040
|
+
// NOTE: Auth credentials come from the claude-credentials volume.
|
|
1041
|
+
// Run "prlt agent auth" to set up authentication (one-time).
|
|
1042
|
+
// Do NOT sync CLAUDE_CODE_OAUTH_TOKEN env var - it causes issues
|
|
1043
|
+
// (setup-token generates invalid tokens, and env var overrides valid credentials file).
|
|
1049
1044
|
}
|
|
1050
|
-
|
|
1051
|
-
console.debug(
|
|
1052
|
-
// Non-fatal - Claude will just prompt for settings
|
|
1045
|
+
else {
|
|
1046
|
+
console.debug(`[runners:docker] Skipping .claude.json settings injection for ${executor} executor`);
|
|
1053
1047
|
}
|
|
1054
|
-
// NOTE: Auth credentials come from the claude-credentials volume.
|
|
1055
|
-
// Run "prlt agent auth" to set up authentication (one-time).
|
|
1056
|
-
// Do NOT sync CLAUDE_CODE_OAUTH_TOKEN env var - it causes issues
|
|
1057
|
-
// (setup-token generates invalid tokens, and env var overrides valid credentials file).
|
|
1058
1048
|
return true;
|
|
1059
1049
|
}
|
|
1060
1050
|
/**
|
|
1061
1051
|
* Ensure a Docker container is running for the agent.
|
|
1052
|
+
* Reuses running containers to preserve in-progress work (TKT-1028).
|
|
1053
|
+
* Only destroys and recreates stopped containers.
|
|
1062
1054
|
* Builds image and creates container if needed.
|
|
1063
1055
|
* Returns the container ID if successful, null otherwise.
|
|
1064
1056
|
*/
|
|
1065
|
-
function ensureDockerContainer(context, config,
|
|
1057
|
+
function ensureDockerContainer(context, config, executor = 'claude-code') {
|
|
1066
1058
|
const containerName = getContainerName(context.agentName);
|
|
1067
1059
|
const imageName = getImageName(context.agentName);
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1060
|
+
// TKT-1028: Reuse running containers instead of destroying them.
|
|
1061
|
+
// This preserves in-progress tmux sessions and avoids killing running agents.
|
|
1062
|
+
// Only destroy stopped containers (which have stale mounts anyway).
|
|
1071
1063
|
if (containerExists(containerName)) {
|
|
1072
|
-
|
|
1064
|
+
if (isContainerRunning(containerName)) {
|
|
1065
|
+
// Container is running - reuse it to preserve any in-progress work.
|
|
1066
|
+
// Note: runContainerSetup is skipped for reused containers since they
|
|
1067
|
+
// were already set up when first created. GitHub token and credentials
|
|
1068
|
+
// are refreshed by the caller (runDevcontainer).
|
|
1069
|
+
const containerId = getContainerId(containerName);
|
|
1070
|
+
if (containerId) {
|
|
1071
|
+
console.debug(`[runners:docker] Reusing running container ${containerName} (${containerId}), skipping setup`);
|
|
1072
|
+
return containerId;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// Container exists but is stopped - remove and recreate for fresh mounts
|
|
1076
|
+
console.debug(`[runners:docker] Removing stopped container ${containerName} to create fresh one`);
|
|
1073
1077
|
try {
|
|
1074
1078
|
execSync(`docker rm -f ${containerName}`, { stdio: 'pipe' });
|
|
1075
1079
|
}
|
|
@@ -1077,21 +1081,50 @@ function ensureDockerContainer(context, config, displayMode = 'terminal') {
|
|
|
1077
1081
|
// Ignore removal errors
|
|
1078
1082
|
}
|
|
1079
1083
|
}
|
|
1080
|
-
// Build image
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1084
|
+
// Build image with version-aware cache busting (TKT-1029)
|
|
1085
|
+
// Read build args from devcontainer.json instead of hardcoding
|
|
1086
|
+
const devcontainerJson = readDevcontainerJson(context.agentDir);
|
|
1087
|
+
const buildArgs = {
|
|
1088
|
+
TZ: devcontainerJson?.build?.args?.TZ || 'America/Los_Angeles',
|
|
1089
|
+
PRLT_REGISTRY: devcontainerJson?.build?.args?.PRLT_REGISTRY || 'npm',
|
|
1090
|
+
};
|
|
1091
|
+
// Resolve the specific prlt version to install (TKT-1029)
|
|
1092
|
+
// When the configured version is a tag like "latest", resolve it to the host's
|
|
1093
|
+
// actual prlt version. This serves two purposes:
|
|
1094
|
+
// 1. Ensures the container runs the same version as the host
|
|
1095
|
+
// 2. Enables Docker layer cache busting when the host version changes
|
|
1096
|
+
// (Docker caches "latest" as a static string, so the layer never rebuilds)
|
|
1097
|
+
const configuredVersion = devcontainerJson?.build?.args?.PRLT_VERSION || 'latest';
|
|
1098
|
+
const isTagVersion = ['latest', 'dev', 'next'].includes(configuredVersion);
|
|
1099
|
+
const hostPrltVersion = isTagVersion ? getHostPrltVersion() : null;
|
|
1100
|
+
if (hostPrltVersion) {
|
|
1101
|
+
buildArgs.PRLT_VERSION = hostPrltVersion;
|
|
1102
|
+
console.debug(`[runners:docker] Using host prlt version ${hostPrltVersion} for image build`);
|
|
1091
1103
|
}
|
|
1104
|
+
else {
|
|
1105
|
+
buildArgs.PRLT_VERSION = configuredVersion;
|
|
1106
|
+
}
|
|
1107
|
+
// Always run docker build - Docker layer caching makes this efficient when
|
|
1108
|
+
// nothing has changed. When PRLT_VERSION changes (e.g., "0.3.29" -> "0.3.35"),
|
|
1109
|
+
// the changed build arg invalidates the cache from that layer forward,
|
|
1110
|
+
// ensuring the new version gets installed.
|
|
1111
|
+
console.debug(`[runners:docker] Building image ${imageName} (PRLT_VERSION=${buildArgs.PRLT_VERSION})`);
|
|
1112
|
+
if (!buildDockerImage(context.agentDir, imageName, buildArgs)) {
|
|
1113
|
+
if (!imageExists(imageName)) {
|
|
1114
|
+
return null; // No image at all, can't proceed
|
|
1115
|
+
}
|
|
1116
|
+
// Build failed but old image exists - continue with setup-prlt.sh as fallback
|
|
1117
|
+
console.debug(`[runners:docker] Build failed but existing image found, continuing with runtime update`);
|
|
1118
|
+
}
|
|
1119
|
+
// Pass resolved prlt version info to the container environment (TKT-1029)
|
|
1120
|
+
// This allows setup-prlt.sh to verify/update prlt without querying npm registry
|
|
1121
|
+
const prltInfo = {
|
|
1122
|
+
registry: buildArgs.PRLT_REGISTRY,
|
|
1123
|
+
version: buildArgs.PRLT_VERSION,
|
|
1124
|
+
};
|
|
1092
1125
|
// Create and start container
|
|
1093
1126
|
console.debug(`[runners:docker] Creating container ${containerName}`);
|
|
1094
|
-
if (!createDockerContainer(context, containerName, imageName, config,
|
|
1127
|
+
if (!createDockerContainer(context, containerName, imageName, config, executor, prltInfo)) {
|
|
1095
1128
|
return null;
|
|
1096
1129
|
}
|
|
1097
1130
|
const containerId = getContainerId(containerName);
|
|
@@ -1100,8 +1133,9 @@ function ensureDockerContainer(context, config, displayMode = 'terminal') {
|
|
|
1100
1133
|
}
|
|
1101
1134
|
// Run post-start setup (firewall, prlt, Claude settings)
|
|
1102
1135
|
// Pass sandboxed config to determine whether to set bypassPermissionsModeAccepted
|
|
1103
|
-
|
|
1104
|
-
|
|
1136
|
+
// Pass executor to skip Claude-specific setup for non-Claude executors
|
|
1137
|
+
console.debug(`[runners:docker] Running container setup (sandboxed=${config.sandboxed}, executor=${executor})`);
|
|
1138
|
+
if (!runContainerSetup(containerId, config.sandboxed, executor)) {
|
|
1105
1139
|
console.debug(`[runners:docker] Setup failed, but continuing...`);
|
|
1106
1140
|
// Don't fail completely - setup might partially work
|
|
1107
1141
|
}
|
|
@@ -1185,42 +1219,42 @@ function writePromptFile(context) {
|
|
|
1185
1219
|
* Uses docker exec for direct container access.
|
|
1186
1220
|
* Uses a prompt file to avoid shell escaping issues.
|
|
1187
1221
|
*/
|
|
1188
|
-
function buildDevcontainerCommand(context, executor, promptFile, containerId, outputMode = 'interactive', sandboxed = true, displayMode = 'terminal') {
|
|
1189
|
-
// Get base command (just 'claude' for claude-code)
|
|
1190
|
-
let baseCmd;
|
|
1191
|
-
switch (executor) {
|
|
1192
|
-
case 'claude-code':
|
|
1193
|
-
baseCmd = 'claude';
|
|
1194
|
-
break;
|
|
1195
|
-
case 'codex':
|
|
1196
|
-
baseCmd = 'codex';
|
|
1197
|
-
break;
|
|
1198
|
-
case 'aider':
|
|
1199
|
-
baseCmd = 'aider';
|
|
1200
|
-
break;
|
|
1201
|
-
default:
|
|
1202
|
-
baseCmd = 'claude';
|
|
1203
|
-
}
|
|
1222
|
+
export function buildDevcontainerCommand(context, executor, promptFile, containerId, outputMode = 'interactive', sandboxed = true, displayMode = 'terminal') {
|
|
1204
1223
|
// Calculate the relative path from agentDir to worktreePath for cd
|
|
1205
1224
|
const relativePath = path.relative(context.agentDir, context.worktreePath);
|
|
1206
1225
|
const cdCmd = relativePath ? `cd /workspace/${relativePath} && ` : '';
|
|
1207
|
-
// Build
|
|
1208
|
-
//
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1226
|
+
// Build executor command using the centralized getExecutorCommand()
|
|
1227
|
+
// This ensures all runners use consistent executor invocation
|
|
1228
|
+
let executorCmd;
|
|
1229
|
+
if (isClaudeExecutor(executor)) {
|
|
1230
|
+
// Claude-specific flags based on output mode and sandboxed setting
|
|
1231
|
+
// - interactive: No -p flag, shows streaming UI (watch Claude work in real-time)
|
|
1232
|
+
// - print: Uses -p flag, outputs final result only (better for logs/automation)
|
|
1233
|
+
const printFlag = outputMode === 'print' ? '-p ' : '';
|
|
1234
|
+
// sandboxed=true means safe mode (no --dangerously-skip-permissions)
|
|
1235
|
+
// sandboxed=false means danger mode (use --dangerously-skip-permissions)
|
|
1236
|
+
// --permission-mode bypassPermissions: skips the "trust this folder" dialog
|
|
1237
|
+
const bypassTrustFlag = '--permission-mode bypassPermissions ';
|
|
1238
|
+
const permissionsFlag = !sandboxed ? '--dangerously-skip-permissions ' : '';
|
|
1239
|
+
// --effort high: skips the effort level prompt for automated agents (TKT-1134)
|
|
1240
|
+
const effortFlag = '--effort high ';
|
|
1241
|
+
executorCmd = `claude ${bypassTrustFlag}${permissionsFlag}${effortFlag}${printFlag}"$(cat ${promptFile})"`;
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
// Non-Claude executors: use getExecutorCommand() to get correct command and args
|
|
1245
|
+
const { cmd, args } = getExecutorCommand(executor, `PLACEHOLDER`, false);
|
|
1246
|
+
// Replace the placeholder prompt with a file read for shell safety
|
|
1247
|
+
const argsStr = args.map(a => a === 'PLACEHOLDER' ? `"$(cat ${promptFile})"` : a).join(' ');
|
|
1248
|
+
executorCmd = `${cmd} ${argsStr}`;
|
|
1249
|
+
}
|
|
1250
|
+
// Build the full command with cd, executor invocation, and cleanup
|
|
1251
|
+
const fullCmd = `${cdCmd}${executorCmd} && rm -f ${promptFile}`;
|
|
1218
1252
|
// Use docker exec for running commands in the container
|
|
1219
1253
|
// Use -it flags only for terminal/foreground modes where a TTY is available
|
|
1220
1254
|
// Background mode runs without a TTY, so -it flags would cause "not a TTY" error
|
|
1221
1255
|
const ttyFlags = displayMode === 'background' ? '' : '-it ';
|
|
1222
|
-
// Direct mode - run
|
|
1223
|
-
return `docker exec ${ttyFlags}${containerId} bash -c '${
|
|
1256
|
+
// Direct mode - run executor directly (tmux setup is handled by runDevcontainerInTmux)
|
|
1257
|
+
return `docker exec ${ttyFlags}${containerId} bash -c '${fullCmd}'`;
|
|
1224
1258
|
}
|
|
1225
1259
|
/**
|
|
1226
1260
|
* Run command inside a Docker container.
|
|
@@ -1265,25 +1299,20 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
1265
1299
|
}
|
|
1266
1300
|
}
|
|
1267
1301
|
// Copy Claude credentials into agent directory so container can access them
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1302
|
+
// Only needed for Claude Code executor
|
|
1303
|
+
if (isClaudeExecutor(executor)) {
|
|
1304
|
+
// This was the original working approach - credentials at /workspace/.claude.json
|
|
1305
|
+
copyClaudeCredentials(context.agentDir);
|
|
1306
|
+
}
|
|
1270
1307
|
// Start or reuse container using raw Docker commands
|
|
1271
1308
|
// No devcontainer CLI required!
|
|
1272
|
-
const containerId = ensureDockerContainer(context, config,
|
|
1309
|
+
const containerId = ensureDockerContainer(context, config, executor);
|
|
1273
1310
|
if (!containerId) {
|
|
1274
1311
|
return {
|
|
1275
1312
|
success: false,
|
|
1276
1313
|
error: 'Failed to start Docker container. Check Docker logs for details.',
|
|
1277
1314
|
};
|
|
1278
1315
|
}
|
|
1279
|
-
// Executor preflight check (TKT-1082): verify executor binary is available inside container
|
|
1280
|
-
const preflight = checkExecutorInContainer(executor, containerId);
|
|
1281
|
-
if (!preflight.ok) {
|
|
1282
|
-
return {
|
|
1283
|
-
success: false,
|
|
1284
|
-
error: preflight.error,
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
1316
|
// Write prompt to file in worktree (accessible by container)
|
|
1288
1317
|
const { hostPath: promptHostPath, containerPath: promptFile } = writePromptFile(context);
|
|
1289
1318
|
// Inject fresh GitHub token into container (containers may be reused with stale/empty tokens)
|
|
@@ -1628,10 +1657,21 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
|
|
|
1628
1657
|
// Extract the claude command from the devcontainer command
|
|
1629
1658
|
const cmdMatch = devcontainerCmd.match(/bash -c '(.+)'$/);
|
|
1630
1659
|
const claudeCmd = cmdMatch ? cmdMatch[1] : devcontainerCmd;
|
|
1631
|
-
// Create a script inside the container that runs claude
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
|
|
1660
|
+
// Create a script inside the container that runs claude and keeps shell open
|
|
1661
|
+
// TERM must be set for Claude's TUI to render properly
|
|
1662
|
+
// Unset CI to prevent Claude from detecting CI environment which suppresses TUI output
|
|
1663
|
+
// Note: We keep DEVCONTAINER set so prlt workspace detection works correctly
|
|
1664
|
+
const tmuxScript = `#!/bin/bash
|
|
1665
|
+
export TERM=xterm-256color
|
|
1666
|
+
export COLORTERM=truecolor
|
|
1667
|
+
unset CI
|
|
1668
|
+
echo "🚀 Starting: ${sessionName}"
|
|
1669
|
+
echo ""
|
|
1670
|
+
${claudeCmd}
|
|
1671
|
+
echo ""
|
|
1672
|
+
echo "✅ Agent work complete. Press Enter to close or run more commands."
|
|
1673
|
+
exec bash
|
|
1674
|
+
`;
|
|
1635
1675
|
const scriptPath = `/tmp/prlt-${sessionName}.sh`;
|
|
1636
1676
|
// Write script and start tmux session inside container
|
|
1637
1677
|
// IMPORTANT: We create the session with bash first, then send keys to run the script.
|
|
@@ -1655,6 +1695,22 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
|
|
|
1655
1695
|
error: `Failed to write script to container: ${error instanceof Error ? error.message : error}`,
|
|
1656
1696
|
};
|
|
1657
1697
|
}
|
|
1698
|
+
// TKT-1028: If a tmux session with the same name already exists (e.g., same
|
|
1699
|
+
// ticket+action spawned again in a reused container), kill the old session first.
|
|
1700
|
+
try {
|
|
1701
|
+
execSync(`docker exec ${actualContainerId} tmux has-session -t "${sessionName}" 2>&1`, { stdio: 'pipe' });
|
|
1702
|
+
// Session exists - kill it before creating a new one
|
|
1703
|
+
console.debug(`[runners:tmux] Killing existing tmux session "${sessionName}" in container`);
|
|
1704
|
+
try {
|
|
1705
|
+
execSync(`docker exec ${actualContainerId} tmux kill-session -t "${sessionName}"`, { stdio: 'pipe' });
|
|
1706
|
+
}
|
|
1707
|
+
catch {
|
|
1708
|
+
// Ignore kill errors
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
catch {
|
|
1712
|
+
// Session doesn't exist - that's the normal case
|
|
1713
|
+
}
|
|
1658
1714
|
// Step 2: Create tmux session running the script directly
|
|
1659
1715
|
// Pass the script as the session command (like host runner does) instead of using send-keys.
|
|
1660
1716
|
// The send-keys approach had a race condition where keys could be lost if bash hadn't
|
|
@@ -1901,8 +1957,6 @@ exec $SHELL
|
|
|
1901
1957
|
export async function runDocker(context, executor, config) {
|
|
1902
1958
|
const prompt = buildPrompt(context);
|
|
1903
1959
|
const containerName = `work-${context.ticketId}-${Date.now()}`;
|
|
1904
|
-
// Get the correct executor command (claude, codex, aider, etc.)
|
|
1905
|
-
const { cmd, args } = getExecutorCommand(executor, prompt, !config.sandboxed);
|
|
1906
1960
|
try {
|
|
1907
1961
|
// Check if docker is available
|
|
1908
1962
|
execSync('which docker', { stdio: 'pipe' });
|
|
@@ -1927,10 +1981,19 @@ export async function runDocker(context, executor, config) {
|
|
|
1927
1981
|
if (config.docker.cpus) {
|
|
1928
1982
|
dockerCmd += ` --cpus ${config.docker.cpus}`;
|
|
1929
1983
|
}
|
|
1930
|
-
// Build executor command
|
|
1931
|
-
const
|
|
1984
|
+
// Build executor command using getExecutorCommand() for correct invocation
|
|
1985
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
1986
|
+
const { cmd, args } = getExecutorCommand(executor, escapedPrompt, !config.sandboxed);
|
|
1987
|
+
// For Claude Code in Docker, use --print for non-interactive output
|
|
1988
|
+
// Non-Claude executors use their native command format from getExecutorCommand()
|
|
1932
1989
|
dockerCmd += ` ${config.docker.image}`;
|
|
1933
|
-
|
|
1990
|
+
if (isClaudeExecutor(executor)) {
|
|
1991
|
+
dockerCmd += ` ${cmd} --print '${escapedPrompt}'`;
|
|
1992
|
+
}
|
|
1993
|
+
else {
|
|
1994
|
+
const argsStr = args.map(a => a === escapedPrompt ? `'${escapedPrompt}'` : a).join(' ');
|
|
1995
|
+
dockerCmd += ` ${cmd} ${argsStr}`;
|
|
1996
|
+
}
|
|
1934
1997
|
const containerId = execSync(dockerCmd, { encoding: 'utf-8' }).trim();
|
|
1935
1998
|
return {
|
|
1936
1999
|
success: true,
|
|
@@ -1959,8 +2022,6 @@ export async function runVm(context, executor, config, host) {
|
|
|
1959
2022
|
const user = config.vm.user;
|
|
1960
2023
|
const keyPath = config.vm.keyPath;
|
|
1961
2024
|
const remoteWorkspace = `/workspace/${context.agentName}`;
|
|
1962
|
-
// Get the correct executor command (claude, codex, aider, etc.)
|
|
1963
|
-
const { cmd, args } = getExecutorCommand(executor, prompt, !config.sandboxed);
|
|
1964
2025
|
try {
|
|
1965
2026
|
// Build SSH options
|
|
1966
2027
|
let sshOpts = '';
|
|
@@ -1982,9 +2043,18 @@ export async function runVm(context, executor, config, host) {
|
|
|
1982
2043
|
const gitPullCmd = `cd ${remoteWorkspace} && git fetch && git checkout ${context.branch}`;
|
|
1983
2044
|
execSync(`ssh ${sshOpts} ${user}@${targetHost} "${gitPullCmd}"`, { stdio: 'pipe' });
|
|
1984
2045
|
}
|
|
1985
|
-
// Execute on remote using
|
|
1986
|
-
const
|
|
1987
|
-
const
|
|
2046
|
+
// Execute on remote using executor-appropriate command
|
|
2047
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
2048
|
+
const { cmd: executorCmd, args: executorArgs } = getExecutorCommand(executor, escapedPrompt, !config.sandboxed);
|
|
2049
|
+
// Build the remote command based on executor type
|
|
2050
|
+
let remoteCmd;
|
|
2051
|
+
if (isClaudeExecutor(executor)) {
|
|
2052
|
+
remoteCmd = `cd ${remoteWorkspace} && ${executorCmd} --print '${escapedPrompt}'`;
|
|
2053
|
+
}
|
|
2054
|
+
else {
|
|
2055
|
+
const argsStr = executorArgs.map(a => a === escapedPrompt ? `'${escapedPrompt}'` : a).join(' ');
|
|
2056
|
+
remoteCmd = `cd ${remoteWorkspace} && ${executorCmd} ${argsStr}`;
|
|
2057
|
+
}
|
|
1988
2058
|
const sshCmd = `ssh ${sshOpts} ${user}@${targetHost} "nohup ${remoteCmd} > /tmp/work-${context.ticketId}.log 2>&1 &"`;
|
|
1989
2059
|
execSync(sshCmd, { stdio: 'pipe' });
|
|
1990
2060
|
return {
|