Skip to content

feat: add parallel sandbox execution via --multi flag (v2.1)#54

Merged
frankbria merged 1 commit intomainfrom
feature/parallel-sandbox-multi
Feb 3, 2026
Merged

feat: add parallel sandbox execution via --multi flag (v2.1)#54
frankbria merged 1 commit intomainfrom
feature/parallel-sandbox-multi

Conversation

@frankbria
Copy link
Owner

@frankbria frankbria commented Feb 3, 2026

Summary

  • Add support for executing multiple tasks in parallel E2B sandboxes
  • New ParallelExecutor class orchestrates concurrent task execution with configurable limits
  • Extend sandbox run command with --multi mode and supporting options

New CLI Options

Option Description
--multi Execute multiple tasks in parallel
--task <text...> Task description (repeatable for multiple tasks)
--task-file <path> File with one task per line
--max-concurrent <n> Max parallel sandboxes (default: 3)
--fail-fast Stop all tasks on first failure
--output-dir <path> Results directory (default: ./parallel-results)

Usage Examples

# Multiple tasks via CLI flags
parallel-cc sandbox run --repo . --multi \
  --task "Implement authentication" \
  --task "Add unit tests" \
  --task "Update documentation"

# Tasks from file with custom concurrency
parallel-cc sandbox run --repo . --multi \
  --task-file tasks.txt \
  --max-concurrent 5

# Fail-fast mode (stop on first failure)
parallel-cc sandbox run --repo . --multi \
  --task "Task 1" \
  --task "Task 2" \
  --fail-fast

New Files

File Description
src/e2b/parallel-executor.ts ParallelExecutor class for multi-task orchestration
src/utils/concurrency.ts ConcurrencyLimiter utility for controlling parallelism
tests/e2b/parallel-executor.test.ts 28 unit tests for ParallelExecutor
tests/cli-sandbox-run-multi.test.ts 24 CLI integration tests

Test plan

  • 28 unit tests for ParallelExecutor class
  • 24 CLI integration tests for --multi options
  • All 1058 existing tests pass
  • Build succeeds with no TypeScript errors
  • Manual testing with real E2B credentials (optional)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added parallel task execution mode allowing multiple tasks to run concurrently in sandboxes with configurable concurrency limits, optional fail-fast behavior, and structured result reporting.
  • Tests

    • Added integration and unit tests for parallel execution workflows and CLI functionality.
  • Chores

    • Updated .gitignore to exclude parallel execution output directory.

Add support for executing multiple tasks in parallel E2B sandboxes:

- New ParallelExecutor class orchestrates concurrent task execution
- ConcurrencyLimiter utility provides semaphore-based parallelism control
- Extend sandbox run command with --multi, --task, --task-file options
- Support fail-fast mode, configurable concurrency, and result aggregation
- Generate markdown summary reports with execution statistics

New CLI options:
  --multi                 Execute multiple tasks in parallel
  --task <text...>        Task description (repeatable)
  --task-file <path>      File with one task per line
  --max-concurrent <n>    Max parallel sandboxes (default: 3)
  --fail-fast             Stop all tasks on first failure
  --output-dir <path>     Results directory (default: ./parallel-results)

Includes 28 unit tests and 24 CLI integration tests.
@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Walkthrough

This pull request introduces parallel multi-task execution capabilities (v2.1) for sandbox runs, enabling users to execute multiple tasks concurrently with configurable concurrency limits, fail-fast semantics, and aggregated results reporting. The implementation adds new CLI flags, a ParallelExecutor orchestrator, concurrency utilities, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Configuration & Ignores
.gitignore
Adds parallel-results/ directory to ignore rules for default parallel execution output.
Type Definitions
src/types.ts
Introduces parallel execution types: ParallelTaskStatus, ParallelExecutionConfig, TaskResult, ParallelExecutionSummary, ParallelExecutionResult, ParallelProgressUpdate, and ParallelProgressCallback for comprehensive data modeling.
CLI Multi-Task Interface
src/cli.ts
Extends SandboxRunOptions with --multi, --task, --task-file, --max-concurrent, --fail-fast, --output-dir flags. Adds validation, task file parsing, credential checks, and routes multi-task requests to new handleSandboxRunMulti handler with progress reporting and JSON output support.
Parallel Execution Engine
src/e2b/parallel-executor.ts
New ParallelExecutor class orchestrating concurrent sandbox tasks with per-task worktrees, session management, progress callbacks, fail-fast cancellation, resource cleanup, and markdown summary report generation.
Concurrency Utilities
src/utils/concurrency.ts
Introduces ConcurrencyLimiter class for bounding concurrent operations, plus helper functions withConcurrencyLimit and withConcurrencyLimitSettled for managing task queues.
Test Suites
tests/cli-sandbox-run-multi.test.ts, tests/e2b/parallel-executor.test.ts
Comprehensive integration tests validating CLI multi-task flags, task parsing, JSON/non-JSON output modes, and ParallelExecutor behavior including concurrency, fail-fast logic, progress reporting, and error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant CLI as CLI Handler
    participant PE as ParallelExecutor
    participant CL as ConcurrencyLimiter
    participant Coord as Coordinator
    participant SM as SandboxManager
    participant SB as Sandbox
    participant FS as File System

    User->>CLI: sandbox run --multi --task="..." --max-concurrent=3
    CLI->>CLI: Parse & validate tasks, credentials
    CLI->>PE: new ParallelExecutor(config)
    CLI->>PE: execute(onProgress callback)
    
    PE->>PE: Initialize task queue
    PE->>CL: Enforce max concurrency
    
    par Parallel Task Execution
        PE->>Coord: registerSession(task1)
        PE->>SM: createWorktree(task1)
        PE->>SM: uploadTarballs(task1)
        Coord->>SB: Launch sandbox
        PE->>SB: Execute Claude in sandbox
        Note over SB: Process task logic
        PE->>SM: downloadChangedFiles(task1)
        PE->>FS: Write output/task1/
        PE->>PE: Emit progress update
        
        PE->>Coord: registerSession(task2)
        PE->>SM: createWorktree(task2)
        PE->>SM: uploadTarballs(task2)
        Coord->>SB: Launch sandbox
        PE->>SB: Execute Claude in sandbox
        PE->>SM: downloadChangedFiles(task2)
        PE->>FS: Write output/task2/
        PE->>PE: Emit progress update
        
        PE->>Coord: registerSession(task3)
        PE->>SM: createWorktree(task3)
        PE->>SM: uploadTarballs(task3)
        Coord->>SB: Launch sandbox
        PE->>SB: Execute Claude in sandbox
        PE->>SM: downloadChangedFiles(task3)
        PE->>FS: Write output/task3/
        PE->>PE: Emit progress update
    end
    
    PE->>PE: Aggregate results & summary
    PE->>FS: Write markdown report
    PE->>SM: Cleanup (terminate sandboxes)
    PE->>Coord: Release sessions
    PE-->>CLI: ParallelExecutionResult
    CLI->>User: Output summary (JSON or human-readable)
    CLI->>User: Exit with status code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes span multiple systems with substantial new logic: a complex ParallelExecutor with concurrent task orchestration, extensive CLI modifications with validation and routing, new type definitions, concurrency utilities, and 2,500+ lines across heterogeneous domains. Review requires understanding parallel execution semantics, state management, error handling, and integration across CLI, coordination, and sandbox layers.

Possibly related PRs

Poem

🐰 A rabbit hops through tasks galore,
Three at once? Now four, now more!
With ConcurrencyLimiter's gentle gate,
Parallel dreams? They won't be late!
Progress whispers, summaries shine, 🌙
Multi-task magic—configuration so fine!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature: adding parallel sandbox execution via --multi flag. It directly matches the primary change in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/parallel-sandbox-multi

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp
Copy link
Contributor

macroscopeapp bot commented Feb 3, 2026

Add parallel sandbox execution to CLI sandbox run via --multi with --max-concurrent 3, --fail-fast, and output to ./parallel-results in v2.1

Introduce a new parallel executor and CLI flow that runs multiple sandbox tasks concurrently with bounded concurrency, fail‑fast cancellation, progress reporting, JSON/human summaries, and a markdown report. Core logic lives in ParallelExecutor and is invoked from handleSandboxRunMulti in cli.ts.

📍Where to Start

Start with handleSandboxRunMulti in cli.ts, then review the orchestration in ParallelExecutor.execute in parallel-executor.ts.


📊 Macroscope summarized 1a40ac6. 5 files reviewed, 11 issues evaluated, 0 issues filtered, 3 comments posted. View details

@frankbria frankbria linked an issue Feb 3, 2026 that may be closed by this pull request
5 tasks
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

Code Review: Parallel Sandbox Execution (v2.1)

Overall Assessment

This PR adds a well-architected parallel execution feature for E2B sandboxes. The code follows project conventions, includes comprehensive tests (28 unit + 24 CLI integration tests), and passes the TypeScript build. The implementation is clean with proper error handling and resource cleanup.

Code Quality ✅

Strengths:

  • Clean separation of concerns with ParallelExecutor class and ConcurrencyLimiter utility
  • Comprehensive error handling with proper cleanup in finally blocks
  • Progress callbacks for real-time status updates
  • Summary report generation with time saved calculation
  • Follows existing code style and TypeScript patterns

Minor Suggestions:

  1. PID generation (parallel-executor.ts:249): Consider including task index for better traceability. While the current approach is unlikely to collide, including the index makes task IDs more predictable during debugging.

  2. TypeScript strictness: The CLI option maxConcurrent is typed as string but parsed as number. Consider using a custom type validator in commander or ensure consistent typing throughout.

  3. Documentation: Consider adding a brief comment explaining why random PIDs are needed (each task needs a unique PID for Coordinator registration).

Potential Bugs / Issues ⚠️

None found - The implementation appears robust. The following areas are worth noting but not bugs:

  1. Worktree isolation: Each parallel task creates its own worktree via gtr new. This is by design and provides proper isolation, but means multiple worktrees will be created during parallel execution.

  2. Fail-fast timing: The fail-fast cancellation is asynchronous and may allow one more task to start before cancellation takes effect. This is acceptable behavior for this use case.

  3. Empty task files: The code correctly handles empty lines and comments in task files (cli.ts:375-378), which is good practice.

Performance Considerations ⚡

Strengths:

  • Concurrency limiting prevents resource exhaustion
  • Efficient semaphore pattern in ConcurrencyLimiter
  • Parallel tarball creation and upload
  • Proper cleanup of resources on completion

Considerations:

  1. Default concurrency of 3 is reasonable but may need adjustment based on E2B API rate limits, repository size, network bandwidth. This is configurable via --max-concurrent, so users can optimize for their use case.

  2. Output directory: Each task creates its own subdirectory, which could consume significant disk space for large results. The default parallel-results/ is gitignored appropriately.

Security Considerations 🔒

Good Practices Observed:

  • Validates repository path exists before execution
  • Validates E2B API key before starting
  • Validates authentication credentials
  • No credential leakage in logs or error messages
  • Proper cleanup of sensitive resources (SSH keys, tarballs)

Considerations:

  1. Task file input: Task files are read as-is. If users store sensitive prompts in task files, ensure these are not committed to git (already handled by .gitignore).

  2. Budget limits: The --budget-per-task feature is well-implemented to prevent runaway costs.

Test Coverage ✅

Excellent Coverage:

  • 28 unit tests for ParallelExecutor (all passing)
  • 24 CLI integration tests (all passing)
  • Tests cover: constructor validation, execution flow, concurrency limits, fail-fast, error handling, cleanup, progress callbacks, summary generation
  • Existing test suite (1058 tests) still passes

Suggestions:

  1. Consider adding a test for edge case: single task with maxConcurrent=1
  2. Test for very large number of tasks (100+) to verify concurrency limit enforcement
  3. Integration test with actual E2B credentials (noted as optional in PR description)

Integration with Existing Code ✅

Seamless Integration:

  • Follows v2.0 CLI subcommand pattern (sandbox run --multi)
  • Uses existing Coordinator for session/worktree management
  • Reuses SandboxManager for E2B operations
  • Compatible with existing options (git, SSH keys, NPM tokens, templates)

Minor Note:

  • The PR title says "v2.1" but package.json shows version "2.0.0". Consider updating version to "2.1.0" before release.

Documentation ✅

Well-documented:

  • Clear help text with examples
  • Inline comments in complex sections
  • JSDoc comments on public methods
  • Progress updates are user-friendly

Recommendation

APPROVE with minor suggestions

This PR is production-ready with excellent code quality, comprehensive test coverage, and proper error handling. The suggested improvements are minor and don't block approval. The implementation significantly enhances the project by enabling efficient parallel task execution in E2B sandboxes.

private async cancelRemainingTasks(): Promise<void> {
this.cancelled = true;
this.logger.info('Cancelling remaining tasks');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

src/e2b/parallel-executor.ts:431

Race condition: tasks initializing when cancelRemainingTasks runs can escape cancellation. The sandbox ID isn't in taskSandboxIds until after createSandbox completes, and executeTask doesn't re-check this.cancelled after its await points. Consider adding cancellation checks after createSandbox and register calls in executeTask.

🚀 Want me to fix this? Reply ex: "fix it for me".

worktreePath = registerResult.worktreePath;

this.logger.info(`[${taskId}] Registered session ${sessionId}, worktree: ${worktreePath}`);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

src/e2b/parallel-executor.ts:255

Fake PIDs generated here will fail the isProcessAlive check in Coordinator, causing all tasks to use the main repo instead of isolated worktrees. Consider using actual child process PIDs or modifying the Coordinator API to accept session-based registration without PID validation.

🚀 Want me to fix this? Reply ex: "fix it for me".

const downloadPath = path.join(outputPath, 'changed-files');
await fs.mkdir(downloadPath, { recursive: true });

const downloadResult = await downloadChangedFiles(sandbox, '/workspace', downloadPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

src/e2b/parallel-executor.ts:388

Consider checking downloadResult.success before returning. Currently, a download failure returns success: true with 0 files changed, masking potential data loss.

-      const downloadResult = await downloadChangedFiles(sandbox, '/workspace', downloadPath);
+      const downloadResult = await downloadChangedFiles(sandbox, '/workspace', downloadPath);
+      if (!downloadResult.success) {
+        return {
+          success: false,
+          exitCode: executionResult.exitCode,
+          filesChanged: 0,
+          error: `Download failed: ${downloadResult.error}`
+        };
+      }

🚀 Want me to fix this? Reply ex: "fix it for me".

@github-actions
Copy link

github-actions bot commented Feb 3, 2026

fatal: could not read Username for 'https://github.com': No such device or address

opencode session  |  github run

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/cli.ts`:
- Around line 2396-2485: The code does not validate options.authMethod and the
parsed maxConcurrent, so an invalid auth method can bypass checks and NaN
maxConcurrent stalls the limiter; update the validation around authMethod (the
authMethod variable derived from options.authMethod) to explicitly reject values
other than 'api-key' or 'oauth' with the same JSON/console error +
process.exit(1) flow used elsewhere, and validate maxConcurrent by parsing
parseInt(options.maxConcurrent || '3', 10), checking Number.isFinite(parsed) and
falling back to a safe default (e.g., 3) or erroring if invalid before building
the config (the config.maxConcurrent property), ensuring the limiter always
receives a valid numeric concurrency.

In `@src/e2b/parallel-executor.ts`:
- Around line 383-413: The code currently ignores failures from
downloadChangedFiles; update the block after calling
downloadChangedFiles(sandbox, '/workspace', downloadPath) to check
downloadResult.success and, if false, set executionResult.success = false (or a
local success flag), merge downloadResult.error into the metadata error field,
include a clear error message in the metadata.json and return payload (the
returned object from this function) with success: false, error:
downloadResult.error (and filesChanged: downloadResult.filesDownloaded if
present); ensure the log (execution.log) is still written but that metadata and
the returned object reflect the download failure so callers do not treat the
task as successful.

In `@tests/cli-sandbox-run-multi.test.ts`:
- Around line 12-37: The runCli helper currently shells the joined args string
through execSync which breaks quoted arguments like "Task 1"; change runCli to
call a child-process API that accepts an args array (e.g., spawnSync or
execFileSync) instead of execSync with a joined string so spaces are preserved;
invoke the binary as ['dist/cli.js', ...args] (or spawnSync('node',
['dist/cli.js', ...args], ...)) and update the result/error handling to read
stdout, stderr, and status from the sync call’s return value
(result.stdout/result.stderr/result.status) rather than relying on thrown
error.stdout/error.stderr so tests keep arguments with spaces intact.
🧹 Nitpick comments (1)
src/cli.ts (1)

2400-2441: Include actionable hint fields in JSON errors for missing credentials.
This keeps machine output consistent with the guidance printed in non-JSON mode. As per coding guidelines: Use consistent error messages with actionable troubleshooting guidance for common failures.

🛠️ Suggested fix
       if (!process.env.ANTHROPIC_API_KEY) {
         if (options.json) {
           console.log(JSON.stringify({
             success: false,
-            error: 'ANTHROPIC_API_KEY environment variable not set'
+            error: 'ANTHROPIC_API_KEY environment variable not set',
+            hint: 'Set ANTHROPIC_API_KEY or use --auth-method oauth'
           }));
         } else {
           console.error(chalk.red('✗ ANTHROPIC_API_KEY environment variable not set'));
           console.log(chalk.dim('Set ANTHROPIC_API_KEY or use --auth-method oauth'));
         }
@@
       if (!existsSync(credentialsPath)) {
         if (options.json) {
           console.log(JSON.stringify({
             success: false,
-            error: 'OAuth credentials not found. Run "claude /login" first'
+            error: 'OAuth credentials not found. Run "claude /login" first',
+            hint: 'Run `claude /login` to create ~/.claude/.credentials.json'
           }));
         } else {
           console.error(chalk.red('✗ OAuth credentials not found'));
           console.log(chalk.dim('Run "claude /login" to authenticate with your Claude subscription'));
         }
@@
     if (!process.env.E2B_API_KEY) {
       if (options.json) {
         console.log(JSON.stringify({
           success: false,
-          error: 'E2B_API_KEY environment variable not set'
+          error: 'E2B_API_KEY environment variable not set',
+          hint: 'Set E2B_API_KEY to use E2B sandbox execution'
         }));
       } else {
         console.error(chalk.red('✗ E2B_API_KEY environment variable not set'));
         console.log(chalk.dim('Set E2B_API_KEY to use E2B sandbox execution'));
       }

Comment on lines +2396 to +2485
// Step 2: Validate authentication
const authMethod = options.authMethod as 'api-key' | 'oauth';
let oauthCredentials: string | undefined;

if (authMethod === 'api-key') {
if (!process.env.ANTHROPIC_API_KEY) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'ANTHROPIC_API_KEY environment variable not set'
}));
} else {
console.error(chalk.red('✗ ANTHROPIC_API_KEY environment variable not set'));
console.log(chalk.dim('Set ANTHROPIC_API_KEY or use --auth-method oauth'));
}
process.exit(1);
}
} else if (authMethod === 'oauth') {
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json');
if (!existsSync(credentialsPath)) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'OAuth credentials not found. Run "claude /login" first'
}));
} else {
console.error(chalk.red('✗ OAuth credentials not found'));
console.log(chalk.dim('Run "claude /login" to authenticate with your Claude subscription'));
}
process.exit(1);
}
oauthCredentials = await fs.readFile(credentialsPath, 'utf-8');
}

// Step 3: Validate E2B API key
if (!process.env.E2B_API_KEY) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'E2B_API_KEY environment variable not set'
}));
} else {
console.error(chalk.red('✗ E2B_API_KEY environment variable not set'));
console.log(chalk.dim('Set E2B_API_KEY to use E2B sandbox execution'));
}
process.exit(1);
}

// Step 4: Resolve repository path
const repoPath = path.resolve(options.repo);
if (!existsSync(repoPath)) {
if (options.json) {
console.log(JSON.stringify({ success: false, error: `Repository path not found: ${repoPath}` }));
} else {
console.error(chalk.red(`✗ Repository path not found: ${repoPath}`));
}
process.exit(1);
}

// Step 5: Create output directory
const outputDir = path.resolve(options.outputDir || './parallel-results');
await fs.mkdir(outputDir, { recursive: true });

// Step 6: Create sandbox manager
const sandboxImage = options.template ||
(process.env.E2B_TEMPLATE?.trim() || '') ||
'anthropic-claude-code';
const sandboxManager = new SandboxManager(logger, { sandboxImage });

// Step 7: Build configuration
const config = {
tasks,
maxConcurrent: parseInt(options.maxConcurrent || '3', 10),
failFast: options.failFast || false,
outputDir,
repoPath,
authMethod,
sandboxImage: options.template,
templateName: options.useTemplate,
branch: options.branch,
gitLive: options.gitLive || false,
targetBranch: options.targetBranch || 'main',
gitUser: options.gitUser,
gitEmail: options.gitEmail,
oauthCredentials,
budgetPerTask: options.budget ? parseFloat(options.budget) : undefined,
npmToken: options.npmToken || process.env.PARALLEL_CC_NPM_TOKEN,
npmRegistry: options.npmRegistry,
sshKeyPath: options.sshKey
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate --auth-method and --max-concurrent before building config.
An invalid auth method bypasses checks, and a non-numeric --max-concurrent (NaN) causes the limiter to never acquire a slot, stalling all tasks.

🛠️ Suggested fix
-    const authMethod = options.authMethod as 'api-key' | 'oauth';
+    if (options.authMethod !== 'api-key' && options.authMethod !== 'oauth') {
+      const message = `Invalid --auth-method "${options.authMethod}". Use "api-key" or "oauth".`;
+      if (options.json) {
+        console.log(JSON.stringify({ success: false, error: message, hint: 'Example: --auth-method api-key' }));
+      } else {
+        console.error(chalk.red(`✗ ${message}`));
+      }
+      process.exit(1);
+    }
+    const authMethod = options.authMethod as 'api-key' | 'oauth';
@@
-    const config = {
-      tasks,
-      maxConcurrent: parseInt(options.maxConcurrent || '3', 10),
+    const maxConcurrent = Number.parseInt(options.maxConcurrent || '3', 10);
+    if (!Number.isFinite(maxConcurrent) || maxConcurrent < 1) {
+      const message = `Invalid --max-concurrent "${options.maxConcurrent}". Must be a positive integer.`;
+      if (options.json) {
+        console.log(JSON.stringify({ success: false, error: message, hint: 'Example: --max-concurrent 3' }));
+      } else {
+        console.error(chalk.red(`✗ ${message}`));
+      }
+      process.exit(1);
+    }
+
+    const config = {
+      tasks,
+      maxConcurrent,
       failFast: options.failFast || false,
       outputDir,
       repoPath,
       authMethod,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Step 2: Validate authentication
const authMethod = options.authMethod as 'api-key' | 'oauth';
let oauthCredentials: string | undefined;
if (authMethod === 'api-key') {
if (!process.env.ANTHROPIC_API_KEY) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'ANTHROPIC_API_KEY environment variable not set'
}));
} else {
console.error(chalk.red('✗ ANTHROPIC_API_KEY environment variable not set'));
console.log(chalk.dim('Set ANTHROPIC_API_KEY or use --auth-method oauth'));
}
process.exit(1);
}
} else if (authMethod === 'oauth') {
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json');
if (!existsSync(credentialsPath)) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'OAuth credentials not found. Run "claude /login" first'
}));
} else {
console.error(chalk.red('✗ OAuth credentials not found'));
console.log(chalk.dim('Run "claude /login" to authenticate with your Claude subscription'));
}
process.exit(1);
}
oauthCredentials = await fs.readFile(credentialsPath, 'utf-8');
}
// Step 3: Validate E2B API key
if (!process.env.E2B_API_KEY) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'E2B_API_KEY environment variable not set'
}));
} else {
console.error(chalk.red('✗ E2B_API_KEY environment variable not set'));
console.log(chalk.dim('Set E2B_API_KEY to use E2B sandbox execution'));
}
process.exit(1);
}
// Step 4: Resolve repository path
const repoPath = path.resolve(options.repo);
if (!existsSync(repoPath)) {
if (options.json) {
console.log(JSON.stringify({ success: false, error: `Repository path not found: ${repoPath}` }));
} else {
console.error(chalk.red(`✗ Repository path not found: ${repoPath}`));
}
process.exit(1);
}
// Step 5: Create output directory
const outputDir = path.resolve(options.outputDir || './parallel-results');
await fs.mkdir(outputDir, { recursive: true });
// Step 6: Create sandbox manager
const sandboxImage = options.template ||
(process.env.E2B_TEMPLATE?.trim() || '') ||
'anthropic-claude-code';
const sandboxManager = new SandboxManager(logger, { sandboxImage });
// Step 7: Build configuration
const config = {
tasks,
maxConcurrent: parseInt(options.maxConcurrent || '3', 10),
failFast: options.failFast || false,
outputDir,
repoPath,
authMethod,
sandboxImage: options.template,
templateName: options.useTemplate,
branch: options.branch,
gitLive: options.gitLive || false,
targetBranch: options.targetBranch || 'main',
gitUser: options.gitUser,
gitEmail: options.gitEmail,
oauthCredentials,
budgetPerTask: options.budget ? parseFloat(options.budget) : undefined,
npmToken: options.npmToken || process.env.PARALLEL_CC_NPM_TOKEN,
npmRegistry: options.npmRegistry,
sshKeyPath: options.sshKey
};
// Step 2: Validate authentication
if (options.authMethod !== 'api-key' && options.authMethod !== 'oauth') {
const message = `Invalid --auth-method "${options.authMethod}". Use "api-key" or "oauth".`;
if (options.json) {
console.log(JSON.stringify({ success: false, error: message, hint: 'Example: --auth-method api-key' }));
} else {
console.error(chalk.red(`✗ ${message}`));
}
process.exit(1);
}
const authMethod = options.authMethod as 'api-key' | 'oauth';
let oauthCredentials: string | undefined;
if (authMethod === 'api-key') {
if (!process.env.ANTHROPIC_API_KEY) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'ANTHROPIC_API_KEY environment variable not set'
}));
} else {
console.error(chalk.red('✗ ANTHROPIC_API_KEY environment variable not set'));
console.log(chalk.dim('Set ANTHROPIC_API_KEY or use --auth-method oauth'));
}
process.exit(1);
}
} else if (authMethod === 'oauth') {
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json');
if (!existsSync(credentialsPath)) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'OAuth credentials not found. Run "claude /login" first'
}));
} else {
console.error(chalk.red('✗ OAuth credentials not found'));
console.log(chalk.dim('Run "claude /login" to authenticate with your Claude subscription'));
}
process.exit(1);
}
oauthCredentials = await fs.readFile(credentialsPath, 'utf-8');
}
// Step 3: Validate E2B API key
if (!process.env.E2B_API_KEY) {
if (options.json) {
console.log(JSON.stringify({
success: false,
error: 'E2B_API_KEY environment variable not set'
}));
} else {
console.error(chalk.red('✗ E2B_API_KEY environment variable not set'));
console.log(chalk.dim('Set E2B_API_KEY to use E2B sandbox execution'));
}
process.exit(1);
}
// Step 4: Resolve repository path
const repoPath = path.resolve(options.repo);
if (!existsSync(repoPath)) {
if (options.json) {
console.log(JSON.stringify({ success: false, error: `Repository path not found: ${repoPath}` }));
} else {
console.error(chalk.red(`✗ Repository path not found: ${repoPath}`));
}
process.exit(1);
}
// Step 5: Create output directory
const outputDir = path.resolve(options.outputDir || './parallel-results');
await fs.mkdir(outputDir, { recursive: true });
// Step 6: Create sandbox manager
const sandboxImage = options.template ||
(process.env.E2B_TEMPLATE?.trim() || '') ||
'anthropic-claude-code';
const sandboxManager = new SandboxManager(logger, { sandboxImage });
// Step 7: Build configuration
const maxConcurrent = Number.parseInt(options.maxConcurrent || '3', 10);
if (!Number.isFinite(maxConcurrent) || maxConcurrent < 1) {
const message = `Invalid --max-concurrent "${options.maxConcurrent}". Must be a positive integer.`;
if (options.json) {
console.log(JSON.stringify({ success: false, error: message, hint: 'Example: --max-concurrent 3' }));
} else {
console.error(chalk.red(`✗ ${message}`));
}
process.exit(1);
}
const config = {
tasks,
maxConcurrent,
failFast: options.failFast || false,
outputDir,
repoPath,
authMethod,
sandboxImage: options.template,
templateName: options.useTemplate,
branch: options.branch,
gitLive: options.gitLive || false,
targetBranch: options.targetBranch || 'main',
gitUser: options.gitUser,
gitEmail: options.gitEmail,
oauthCredentials,
budgetPerTask: options.budget ? parseFloat(options.budget) : undefined,
npmToken: options.npmToken || process.env.PARALLEL_CC_NPM_TOKEN,
npmRegistry: options.npmRegistry,
sshKeyPath: options.sshKey
};
🤖 Prompt for AI Agents
In `@src/cli.ts` around lines 2396 - 2485, The code does not validate
options.authMethod and the parsed maxConcurrent, so an invalid auth method can
bypass checks and NaN maxConcurrent stalls the limiter; update the validation
around authMethod (the authMethod variable derived from options.authMethod) to
explicitly reject values other than 'api-key' or 'oauth' with the same
JSON/console error + process.exit(1) flow used elsewhere, and validate
maxConcurrent by parsing parseInt(options.maxConcurrent || '3', 10), checking
Number.isFinite(parsed) and falling back to a safe default (e.g., 3) or erroring
if invalid before building the config (the config.maxConcurrent property),
ensuring the limiter always receives a valid numeric concurrency.

Comment on lines +383 to +413
// Step 4: Download changed files
this.logger.info(`[${taskId}] Downloading changed files`);
const downloadPath = path.join(outputPath, 'changed-files');
await fs.mkdir(downloadPath, { recursive: true });

const downloadResult = await downloadChangedFiles(sandbox, '/workspace', downloadPath);

// Step 5: Save execution log
const logPath = path.join(outputPath, 'execution.log');
const logContent = executionResult.fullOutput || executionResult.output || '';
await fs.writeFile(logPath, logContent);

// Step 6: Save metadata
const metadataPath = path.join(outputPath, 'metadata.json');
await fs.writeFile(metadataPath, JSON.stringify({
taskId,
prompt,
exitCode: executionResult.exitCode,
executionTime: executionResult.executionTime,
filesDownloaded: downloadResult.filesDownloaded,
success: executionResult.success,
state: executionResult.state,
error: executionResult.error
}, null, 2));

return {
success: executionResult.success,
exitCode: executionResult.exitCode,
filesChanged: downloadResult.filesDownloaded,
error: executionResult.error
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle failed downloads so task status reflects missing outputs.
downloadChangedFiles can fail, but the result is ignored, so tasks can report success with empty outputs. Check downloadResult.success and surface the error.

🛠️ Suggested fix
-      const downloadResult = await downloadChangedFiles(sandbox, '/workspace', downloadPath);
+      const downloadResult = await downloadChangedFiles(sandbox, '/workspace', downloadPath);
+      const downloadError = !downloadResult.success
+        ? `Download failed: ${downloadResult.error || 'Unknown error'}`
+        : undefined;
@@
       await fs.writeFile(metadataPath, JSON.stringify({
         taskId,
         prompt,
         exitCode: executionResult.exitCode,
         executionTime: executionResult.executionTime,
         filesDownloaded: downloadResult.filesDownloaded,
         success: executionResult.success,
         state: executionResult.state,
-        error: executionResult.error
+        error: downloadError ?? executionResult.error
       }, null, 2));

       return {
-        success: executionResult.success,
+        success: executionResult.success && !downloadError,
         exitCode: executionResult.exitCode,
-        filesChanged: downloadResult.filesDownloaded,
-        error: executionResult.error
+        filesChanged: downloadResult.success ? downloadResult.filesDownloaded : 0,
+        error: downloadError ?? executionResult.error
       };
🤖 Prompt for AI Agents
In `@src/e2b/parallel-executor.ts` around lines 383 - 413, The code currently
ignores failures from downloadChangedFiles; update the block after calling
downloadChangedFiles(sandbox, '/workspace', downloadPath) to check
downloadResult.success and, if false, set executionResult.success = false (or a
local success flag), merge downloadResult.error into the metadata error field,
include a clear error message in the metadata.json and return payload (the
returned object from this function) with success: false, error:
downloadResult.error (and filesChanged: downloadResult.filesDownloaded if
present); ensure the log (execution.log) is still written but that metadata and
the returned object reflect the download failure so callers do not treat the
task as successful.

Comment on lines +12 to +37
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { execSync, spawn } from 'child_process';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';

// Helper to run CLI command and capture output
function runCli(args: string[], options: { env?: Record<string, string> } = {}): {
stdout: string;
stderr: string;
exitCode: number | null;
} {
try {
const stdout = execSync(`node dist/cli.js ${args.join(' ')}`, {
encoding: 'utf-8',
env: { ...process.env, ...options.env },
timeout: 5000
});
return { stdout, stderr: '', exitCode: 0 };
} catch (error: any) {
return {
stdout: error.stdout || '',
stderr: error.stderr || '',
exitCode: error.status || 1
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid shell-splitting CLI args so tasks with spaces are preserved.
execSync with a joined string splits "Task 1" into separate tokens, which can make the tests less representative. Use spawnSync/execFileSync with an args array instead.

🛠️ Suggested fix
-import { execSync, spawn } from 'child_process';
+import { spawnSync } from 'child_process';
@@
-function runCli(args: string[], options: { env?: Record<string, string> } = {}): {
+function runCli(args: string[], options: { env?: Record<string, string> } = {}): {
   stdout: string;
   stderr: string;
   exitCode: number | null;
 } {
-  try {
-    const stdout = execSync(`node dist/cli.js ${args.join(' ')}`, {
-      encoding: 'utf-8',
-      env: { ...process.env, ...options.env },
-      timeout: 5000
-    });
-    return { stdout, stderr: '', exitCode: 0 };
-  } catch (error: any) {
-    return {
-      stdout: error.stdout || '',
-      stderr: error.stderr || '',
-      exitCode: error.status || 1
-    };
-  }
+  const result = spawnSync('node', ['dist/cli.js', ...args], {
+    encoding: 'utf-8',
+    env: { ...process.env, ...options.env },
+    timeout: 5000
+  });
+  return {
+    stdout: result.stdout ?? '',
+    stderr: result.stderr ?? (result.error ? String(result.error) : ''),
+    exitCode: result.status ?? 1
+  };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { execSync, spawn } from 'child_process';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
// Helper to run CLI command and capture output
function runCli(args: string[], options: { env?: Record<string, string> } = {}): {
stdout: string;
stderr: string;
exitCode: number | null;
} {
try {
const stdout = execSync(`node dist/cli.js ${args.join(' ')}`, {
encoding: 'utf-8',
env: { ...process.env, ...options.env },
timeout: 5000
});
return { stdout, stderr: '', exitCode: 0 };
} catch (error: any) {
return {
stdout: error.stdout || '',
stderr: error.stderr || '',
exitCode: error.status || 1
};
}
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { spawnSync } from 'child_process';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
// Helper to run CLI command and capture output
function runCli(args: string[], options: { env?: Record<string, string> } = {}): {
stdout: string;
stderr: string;
exitCode: number | null;
} {
const result = spawnSync('node', ['dist/cli.js', ...args], {
encoding: 'utf-8',
env: { ...process.env, ...options.env },
timeout: 5000
});
return {
stdout: result.stdout ?? '',
stderr: result.stderr ?? (result.error ? String(result.error) : ''),
exitCode: result.status ?? 1
};
}
🤖 Prompt for AI Agents
In `@tests/cli-sandbox-run-multi.test.ts` around lines 12 - 37, The runCli helper
currently shells the joined args string through execSync which breaks quoted
arguments like "Task 1"; change runCli to call a child-process API that accepts
an args array (e.g., spawnSync or execFileSync) instead of execSync with a
joined string so spaces are preserved; invoke the binary as ['dist/cli.js',
...args] (or spawnSync('node', ['dist/cli.js', ...args], ...)) and update the
result/error handling to read stdout, stderr, and status from the sync call’s
return value (result.stdout/result.stderr/result.status) rather than relying on
thrown error.stdout/error.stderr so tests keep arguments with spaces intact.

@frankbria frankbria merged commit 7d5209c into main Feb 3, 2026
9 of 10 checks passed
@frankbria frankbria deleted the feature/parallel-sandbox-multi branch February 3, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v2.1] Parallel sandbox execution (sandbox-run-multi command)

1 participant