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;
tag?: string;
project?: string;
sandbox?: boolean;
}
export class LoopCommand extends Command {
@@ -47,6 +48,7 @@ export class LoopCommand extends Command {
'--project <path>',
'Project root directory (auto-detected if not provided)'
)
.option('--sandbox', 'Run Claude in Docker sandbox mode')
.action((options: LoopCommandOptions) => this.execute(options));
}
@@ -80,11 +82,17 @@ export class LoopCommand extends Command {
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.dim(`Preset: ${prompt}`));
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)
if (prompt === 'default') {
@@ -105,7 +113,8 @@ export class LoopCommand extends Command {
iterations,
prompt,
progressFile,
tag: options.tag
tag: options.tag,
sandbox: options.sandbox
};
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",
"open": "^10.2.0",
"ora": "^8.2.0",
"proper-lockfile": "^4.1.2",
"simple-git": "^3.28.0",
"steno": "^4.0.2",
"terminal-link": "^5.0.0",

View File

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

View File

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

View File

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