mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +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
|
"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
|
||||||
|
|||||||
@@ -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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ==========
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user