fix(loop): improve error handling and use dangerously-skip-permissions (#1576)

This commit is contained in:
Ralph Khreish
2026-01-14 20:51:21 +01:00
committed by GitHub
parent a369c2a1a7
commit 097c8edcb0
5 changed files with 111 additions and 22 deletions

View File

@@ -0,0 +1,10 @@
---
"task-master-ai": "patch"
---
Improve loop command error handling and use dangerously-skip-permissions
- Add proper spawn error handling (ENOENT, EACCES) with actionable messages
- Return error info from checkSandboxAuth and runInteractiveAuth instead of silent failures
- Use --dangerously-skip-permissions for unattended loop execution
- Fix null exit code masking issue

View File

@@ -1,6 +1,5 @@
--- ---
"@tm/core": patch "task-master-ai": patch
"@tm/cli": patch
--- ---
Make Docker sandbox mode opt-in for loop command Make Docker sandbox mode opt-in for loop command

View File

@@ -6,9 +6,9 @@ import path from 'node:path';
import { import {
type LoopConfig, type LoopConfig,
type LoopResult, type LoopResult,
PRESET_NAMES,
type TmCore, type TmCore,
createTmCore, createTmCore
PRESET_NAMES
} from '@tm/core'; } from '@tm/core';
import chalk from 'chalk'; import chalk from 'chalk';
import { Command } from 'commander'; import { Command } from 'commander';
@@ -127,9 +127,13 @@ export class LoopCommand extends Command {
private handleSandboxAuth(): void { private handleSandboxAuth(): void {
console.log(chalk.dim('Checking sandbox auth...')); console.log(chalk.dim('Checking sandbox auth...'));
const isAuthed = this.tmCore.loop.checkSandboxAuth(); const authCheck = this.tmCore.loop.checkSandboxAuth();
if (isAuthed) { if (authCheck.error) {
throw new Error(authCheck.error);
}
if (authCheck.ready) {
console.log(chalk.green('✓ Sandbox ready')); console.log(chalk.green('✓ Sandbox ready'));
return; return;
} }
@@ -141,7 +145,10 @@ export class LoopCommand extends Command {
); );
console.log(chalk.dim('Please complete auth, then Ctrl+C to continue.\n')); console.log(chalk.dim('Please complete auth, then Ctrl+C to continue.\n'));
this.tmCore.loop.runInteractiveAuth(); const authResult = this.tmCore.loop.runInteractiveAuth();
if (!authResult.success) {
throw new Error(authResult.error || 'Interactive authentication failed');
}
console.log(chalk.green('✓ Auth complete\n')); console.log(chalk.green('✓ Auth complete\n'));
} }

View File

@@ -31,9 +31,9 @@ export class LoopDomain {
/** /**
* Check if Docker sandbox auth is ready * Check if Docker sandbox auth is ready
* @returns true if ready, false if auth needed * @returns Object with ready status and optional error message
*/ */
checkSandboxAuth(): boolean { checkSandboxAuth(): { ready: boolean; error?: string } {
const service = new LoopService({ projectRoot: this.projectRoot }); const service = new LoopService({ projectRoot: this.projectRoot });
return service.checkSandboxAuth(); return service.checkSandboxAuth();
} }
@@ -41,10 +41,11 @@ export class LoopDomain {
/** /**
* Run Docker sandbox session for user authentication * Run Docker sandbox session for user authentication
* Blocks until user completes auth * Blocks until user completes auth
* @returns Object with success status and optional error message
*/ */
runInteractiveAuth(): void { runInteractiveAuth(): { success: boolean; error?: string } {
const service = new LoopService({ projectRoot: this.projectRoot }); const service = new LoopService({ projectRoot: this.projectRoot });
service.runInteractiveAuth(); return service.runInteractiveAuth();
} }
// ========== Loop Operations ========== // ========== Loop Operations ==========

View File

@@ -1,5 +1,5 @@
/** /**
* @fileoverview Loop Service - Orchestrates running Claude Code in Docker sandbox iterations * @fileoverview Loop Service - Orchestrates running Claude Code iterations (sandbox or CLI mode)
*/ */
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
@@ -34,7 +34,7 @@ export class LoopService {
} }
/** Check if Docker sandbox auth is ready */ /** Check if Docker sandbox auth is ready */
checkSandboxAuth(): boolean { checkSandboxAuth(): { ready: boolean; error?: string } {
const result = spawnSync( const result = spawnSync(
'docker', 'docker',
['sandbox', 'run', 'claude', '-p', 'Say OK'], ['sandbox', 'run', 'claude', '-p', 'Say OK'],
@@ -42,16 +42,29 @@ export class LoopService {
cwd: this.projectRoot, cwd: this.projectRoot,
timeout: 30000, timeout: 30000,
encoding: 'utf-8', encoding: 'utf-8',
stdio: ['inherit', 'pipe', 'pipe'] // stdin from terminal, capture stdout/stderr stdio: ['inherit', 'pipe', 'pipe']
} }
); );
if (result.error) {
const code = (result.error as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
return {
ready: false,
error:
'Docker is not installed. Install Docker Desktop to use --sandbox mode.'
};
}
return { ready: false, error: `Docker error: ${result.error.message}` };
}
const output = (result.stdout || '') + (result.stderr || ''); const output = (result.stdout || '') + (result.stderr || '');
return output.toLowerCase().includes('ok'); return { ready: output.toLowerCase().includes('ok') };
} }
/** Run interactive Docker sandbox session for user authentication */ /** Run interactive Docker sandbox session for user authentication */
runInteractiveAuth(): void { runInteractiveAuth(): { success: boolean; error?: string } {
spawnSync( const result = spawnSync(
'docker', 'docker',
[ [
'sandbox', 'sandbox',
@@ -64,6 +77,34 @@ export class LoopService {
stdio: 'inherit' stdio: 'inherit'
} }
); );
if (result.error) {
const code = (result.error as NodeJS.ErrnoException).code;
if (code === 'ENOENT') {
return {
success: false,
error:
'Docker is not installed. Install Docker Desktop to use --sandbox mode.'
};
}
return { success: false, error: `Docker error: ${result.error.message}` };
}
if (result.status === null) {
return {
success: false,
error: 'Docker terminated abnormally (no exit code)'
};
}
if (result.status !== 0) {
return {
success: false,
error: `Docker exited with code ${result.status}`
};
}
return { success: true };
} }
/** Run a loop with the given configuration */ /** Run a loop with the given configuration */
@@ -232,7 +273,7 @@ Loop iteration ${iteration} of ${config.iterations}${tagInfo}`;
const command = sandbox ? 'docker' : 'claude'; const command = sandbox ? 'docker' : 'claude';
const args = sandbox const args = sandbox
? ['sandbox', 'run', 'claude', '-p', prompt] ? ['sandbox', 'run', 'claude', '-p', prompt]
: ['-p', prompt, '--allowedTools', 'Edit,Write,Bash,Read,Glob,Grep']; : ['-p', prompt, '--dangerously-skip-permissions'];
const result = spawnSync(command, args, { const result = spawnSync(command, args, {
cwd: this.projectRoot, cwd: this.projectRoot,
@@ -241,15 +282,46 @@ Loop iteration ${iteration} of ${config.iterations}${tagInfo}`;
stdio: ['inherit', 'pipe', 'pipe'] stdio: ['inherit', 'pipe', 'pipe']
}); });
// Check for spawn-level errors (command not found, permission denied, etc.)
if (result.error) {
const code = (result.error as NodeJS.ErrnoException).code;
let errorMessage: string;
if (code === 'ENOENT') {
errorMessage = sandbox
? 'Docker is not installed. Install Docker Desktop to use --sandbox mode.'
: 'Claude CLI is not installed. Install with: npm install -g @anthropic-ai/claude-code';
} else if (code === 'EACCES') {
errorMessage = `Permission denied executing '${command}'`;
} else {
errorMessage = `Failed to execute '${command}': ${result.error.message}`;
}
console.error(`[Loop Error] ${errorMessage}`);
return {
iteration: iterationNum,
status: 'error',
duration: Date.now() - startTime,
message: errorMessage
};
}
const output = (result.stdout || '') + (result.stderr || ''); const output = (result.stdout || '') + (result.stderr || '');
// Print output to console (spawnSync with pipe captures but doesn't display) // Print output to console (spawnSync with pipe captures but doesn't display)
if (output) console.log(output); if (output) console.log(output);
const { status, message } = this.parseCompletion( // Handle null status (spawn failed but no error object - shouldn't happen but be safe)
output, if (result.status === null) {
result.status ?? 1 return {
); iteration: iterationNum,
status: 'error',
duration: Date.now() - startTime,
message: 'Command terminated abnormally (no exit code)'
};
}
const { status, message } = this.parseCompletion(output, result.status);
return { return {
iteration: iterationNum, iteration: iterationNum,
status, status,