From 097c8edcb0ca065218e9b51758ad370ac7475f1a Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:51:21 +0100 Subject: [PATCH] fix(loop): improve error handling and use dangerously-skip-permissions (#1576) --- .changeset/loop-error-handling.md | 10 ++ .changeset/loop-sandbox-optional.md | 3 +- apps/cli/src/commands/loop.command.ts | 17 +++- .../tm-core/src/modules/loop/loop-domain.ts | 9 +- .../src/modules/loop/services/loop.service.ts | 94 ++++++++++++++++--- 5 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 .changeset/loop-error-handling.md diff --git a/.changeset/loop-error-handling.md b/.changeset/loop-error-handling.md new file mode 100644 index 00000000..4f0445b7 --- /dev/null +++ b/.changeset/loop-error-handling.md @@ -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 diff --git a/.changeset/loop-sandbox-optional.md b/.changeset/loop-sandbox-optional.md index a5fe135a..b1bfe944 100644 --- a/.changeset/loop-sandbox-optional.md +++ b/.changeset/loop-sandbox-optional.md @@ -1,6 +1,5 @@ --- -"@tm/core": patch -"@tm/cli": patch +"task-master-ai": patch --- Make Docker sandbox mode opt-in for loop command diff --git a/apps/cli/src/commands/loop.command.ts b/apps/cli/src/commands/loop.command.ts index 0fa6c1ce..95c2931e 100644 --- a/apps/cli/src/commands/loop.command.ts +++ b/apps/cli/src/commands/loop.command.ts @@ -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')); } diff --git a/packages/tm-core/src/modules/loop/loop-domain.ts b/packages/tm-core/src/modules/loop/loop-domain.ts index ffe9d9aa..342fa011 100644 --- a/packages/tm-core/src/modules/loop/loop-domain.ts +++ b/packages/tm-core/src/modules/loop/loop-domain.ts @@ -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 ========== diff --git a/packages/tm-core/src/modules/loop/services/loop.service.ts b/packages/tm-core/src/modules/loop/services/loop.service.ts index 7aa1c8b4..fd08d6d6 100644 --- a/packages/tm-core/src/modules/loop/services/loop.service.ts +++ b/packages/tm-core/src/modules/loop/services/loop.service.ts @@ -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,