fix(loop): make Docker sandbox opt-in and preserve progress file

- Add --sandbox flag to loop command (default: use plain claude -p)
- Append to progress.txt instead of overwriting between runs
- Display execution mode (Docker sandbox vs Claude CLI) in output
This commit is contained in:
Ralph Khreish
2026-01-14 11:06:09 +01:00
parent 3cc6174b47
commit e762e4f646
7 changed files with 52 additions and 19 deletions

View File

@@ -0,0 +1,10 @@
---
"@tm/core": patch
"@tm/cli": patch
---
Make Docker sandbox mode opt-in for loop command
- Add `--sandbox` flag to `task-master loop` (default: use plain `claude -p`)
- Preserve progress.txt between runs (append instead of overwrite)
- Display execution mode in loop startup output

View File

@@ -22,6 +22,7 @@ export interface LoopCommandOptions {
progressFile?: string; progressFile?: string;
tag?: string; tag?: string;
project?: string; project?: string;
sandbox?: boolean;
} }
export class LoopCommand extends Command { export class LoopCommand extends Command {
@@ -47,6 +48,7 @@ export class LoopCommand extends Command {
'--project <path>', '--project <path>',
'Project root directory (auto-detected if not provided)' 'Project root directory (auto-detected if not provided)'
) )
.option('--sandbox', 'Run Claude in Docker sandbox mode')
.action((options: LoopCommandOptions) => this.execute(options)); .action((options: LoopCommandOptions) => this.execute(options));
} }
@@ -80,11 +82,17 @@ export class LoopCommand extends Command {
storageType: this.tmCore.tasks.getStorageType() storageType: this.tmCore.tasks.getStorageType()
}); });
this.handleSandboxAuth(); // Only check sandbox auth when --sandbox flag is used
if (options.sandbox) {
this.handleSandboxAuth();
}
console.log(chalk.cyan('Starting Task Master Loop...')); console.log(chalk.cyan('Starting Task Master Loop...'));
console.log(chalk.dim(`Preset: ${prompt}`)); console.log(chalk.dim(`Preset: ${prompt}`));
console.log(chalk.dim(`Max iterations: ${iterations}`)); console.log(chalk.dim(`Max iterations: ${iterations}`));
console.log(
chalk.dim(`Mode: ${options.sandbox ? 'Docker sandbox' : 'Claude CLI'}`)
);
// Show next task only for default preset (other presets don't use Task Master tasks) // Show next task only for default preset (other presets don't use Task Master tasks)
if (prompt === 'default') { if (prompt === 'default') {
@@ -105,7 +113,8 @@ export class LoopCommand extends Command {
iterations, iterations,
prompt, prompt,
progressFile, progressFile,
tag: options.tag tag: options.tag,
sandbox: options.sandbox
}; };
const result = await this.tmCore.loop.run(config); const result = await this.tmCore.loop.run(config);

1
package-lock.json generated
View File

@@ -67,6 +67,7 @@
"ollama-ai-provider-v2": "^1.3.1", "ollama-ai-provider-v2": "^1.3.1",
"open": "^10.2.0", "open": "^10.2.0",
"ora": "^8.2.0", "ora": "^8.2.0",
"proper-lockfile": "^4.1.2",
"simple-git": "^3.28.0", "simple-git": "^3.28.0",
"steno": "^4.0.2", "steno": "^4.0.2",
"terminal-link": "^5.0.0", "terminal-link": "^5.0.0",

View File

@@ -117,7 +117,8 @@
"turndown": "^7.2.2", "turndown": "^7.2.2",
"undici": "^7.16.0", "undici": "^7.16.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^4.1.12" "zod": "^4.1.12",
"proper-lockfile": "^4.1.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"@anthropic-ai/claude-code": "^2.0.59", "@anthropic-ai/claude-code": "^2.0.59",

View File

@@ -188,7 +188,8 @@ export class LoopDomain {
partial.progressFile ?? partial.progressFile ??
path.join(this.projectRoot, '.taskmaster', 'progress.txt'), path.join(this.projectRoot, '.taskmaster', 'progress.txt'),
sleepSeconds: partial.sleepSeconds ?? 5, sleepSeconds: partial.sleepSeconds ?? 5,
tag: partial.tag tag: partial.tag,
sandbox: partial.sandbox ?? false
}; };
} }
} }

View File

@@ -3,7 +3,7 @@
*/ */
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'; import { appendFile, mkdir, readFile } from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { PRESETS, isPreset as checkIsPreset } from '../presets/index.js'; import { PRESETS, isPreset as checkIsPreset } from '../presets/index.js';
import type { import type {
@@ -80,7 +80,11 @@ export class LoopService {
console.log(`━━━ Iteration ${i} of ${config.iterations} ━━━`); console.log(`━━━ Iteration ${i} of ${config.iterations} ━━━`);
const prompt = await this.buildPrompt(config, i); const prompt = await this.buildPrompt(config, i);
const iteration = this.executeIteration(prompt, i); const iteration = this.executeIteration(
prompt,
i,
config.sandbox ?? false
);
iterations.push(iteration); iterations.push(iteration);
// Check for early exit conditions // Check for early exit conditions
@@ -135,9 +139,11 @@ export class LoopService {
private async initProgressFile(config: LoopConfig): Promise<void> { private async initProgressFile(config: LoopConfig): Promise<void> {
await mkdir(path.dirname(config.progressFile), { recursive: true }); await mkdir(path.dirname(config.progressFile), { recursive: true });
const tagLine = config.tag ? `# Tag: ${config.tag}\n` : ''; const tagLine = config.tag ? `# Tag: ${config.tag}\n` : '';
await writeFile( // Append to existing progress file instead of overwriting
await appendFile(
config.progressFile, config.progressFile,
`# Task Master Loop Progress `
# Task Master Loop Progress
# Started: ${new Date().toISOString()} # Started: ${new Date().toISOString()}
# Preset: ${config.prompt} # Preset: ${config.prompt}
# Max Iterations: ${config.iterations} # Max Iterations: ${config.iterations}
@@ -217,20 +223,23 @@ Loop iteration ${iteration} of ${config.iterations}${tagInfo}`;
private executeIteration( private executeIteration(
prompt: string, prompt: string,
iterationNum: number iterationNum: number,
sandbox: boolean
): LoopIteration { ): LoopIteration {
const startTime = Date.now(); const startTime = Date.now();
const result = spawnSync( // Use docker sandbox or plain claude based on config
'docker', const command = sandbox ? 'docker' : 'claude';
['sandbox', 'run', 'claude', '-p', prompt], const args = sandbox
{ ? ['sandbox', 'run', 'claude', '-p', prompt]
cwd: this.projectRoot, : ['-p', prompt, '--allowedTools', 'Edit,Write,Bash,Read,Glob,Grep'];
encoding: 'utf-8',
maxBuffer: 50 * 1024 * 1024, // 50MB buffer const result = spawnSync(command, args, {
stdio: ['inherit', 'pipe', 'pipe'] cwd: this.projectRoot,
} encoding: 'utf-8',
); maxBuffer: 50 * 1024 * 1024, // 50MB buffer
stdio: ['inherit', 'pipe', 'pipe']
});
const output = (result.stdout || '') + (result.stderr || ''); const output = (result.stdout || '') + (result.stderr || '');

View File

@@ -26,6 +26,8 @@ export interface LoopConfig {
sleepSeconds: number; sleepSeconds: number;
/** Tag context to operate on (optional) */ /** Tag context to operate on (optional) */
tag?: string; tag?: string;
/** Run Claude in Docker sandbox mode (default: false) */
sandbox?: boolean;
} }
/** /**