mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-29 22:02:04 +00:00
fix(loop): improve error handling and use dangerously-skip-permissions (#1576)
This commit is contained in:
10
.changeset/loop-error-handling.md
Normal file
10
.changeset/loop-error-handling.md
Normal 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
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
"@tm/core": patch
|
||||
"@tm/cli": patch
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Make Docker sandbox mode opt-in for loop command
|
||||
|
||||
@@ -6,9 +6,9 @@ import path from 'node:path';
|
||||
import {
|
||||
type LoopConfig,
|
||||
type LoopResult,
|
||||
PRESET_NAMES,
|
||||
type TmCore,
|
||||
createTmCore,
|
||||
PRESET_NAMES
|
||||
createTmCore
|
||||
} from '@tm/core';
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
@@ -127,9 +127,13 @@ export class LoopCommand extends Command {
|
||||
|
||||
private handleSandboxAuth(): void {
|
||||
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'));
|
||||
return;
|
||||
}
|
||||
@@ -141,7 +145,10 @@ export class LoopCommand extends Command {
|
||||
);
|
||||
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'));
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ export class LoopDomain {
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
return service.checkSandboxAuth();
|
||||
}
|
||||
@@ -41,10 +41,11 @@ export class LoopDomain {
|
||||
/**
|
||||
* Run Docker sandbox session for user authentication
|
||||
* 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 });
|
||||
service.runInteractiveAuth();
|
||||
return service.runInteractiveAuth();
|
||||
}
|
||||
|
||||
// ========== Loop Operations ==========
|
||||
|
||||
@@ -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';
|
||||
@@ -34,7 +34,7 @@ export class LoopService {
|
||||
}
|
||||
|
||||
/** Check if Docker sandbox auth is ready */
|
||||
checkSandboxAuth(): boolean {
|
||||
checkSandboxAuth(): { ready: boolean; error?: string } {
|
||||
const result = spawnSync(
|
||||
'docker',
|
||||
['sandbox', 'run', 'claude', '-p', 'Say OK'],
|
||||
@@ -42,16 +42,29 @@ export class LoopService {
|
||||
cwd: this.projectRoot,
|
||||
timeout: 30000,
|
||||
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 || '');
|
||||
return output.toLowerCase().includes('ok');
|
||||
return { ready: output.toLowerCase().includes('ok') };
|
||||
}
|
||||
|
||||
/** Run interactive Docker sandbox session for user authentication */
|
||||
runInteractiveAuth(): void {
|
||||
spawnSync(
|
||||
runInteractiveAuth(): { success: boolean; error?: string } {
|
||||
const result = spawnSync(
|
||||
'docker',
|
||||
[
|
||||
'sandbox',
|
||||
@@ -64,6 +77,34 @@ export class LoopService {
|
||||
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 */
|
||||
@@ -232,7 +273,7 @@ Loop iteration ${iteration} of ${config.iterations}${tagInfo}`;
|
||||
const command = sandbox ? 'docker' : 'claude';
|
||||
const args = sandbox
|
||||
? ['sandbox', 'run', 'claude', '-p', prompt]
|
||||
: ['-p', prompt, '--allowedTools', 'Edit,Write,Bash,Read,Glob,Grep'];
|
||||
: ['-p', prompt, '--dangerously-skip-permissions'];
|
||||
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: this.projectRoot,
|
||||
@@ -241,15 +282,46 @@ Loop iteration ${iteration} of ${config.iterations}${tagInfo}`;
|
||||
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 || '');
|
||||
|
||||
// Print output to console (spawnSync with pipe captures but doesn't display)
|
||||
if (output) console.log(output);
|
||||
|
||||
const { status, message } = this.parseCompletion(
|
||||
output,
|
||||
result.status ?? 1
|
||||
);
|
||||
// Handle null status (spawn failed but no error object - shouldn't happen but be safe)
|
||||
if (result.status === null) {
|
||||
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 {
|
||||
iteration: iterationNum,
|
||||
status,
|
||||
|
||||
Reference in New Issue
Block a user