Skip to content
62 changes: 49 additions & 13 deletions .aiox-core/core/execution/build-orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,18 @@ The subtask is complete only when verification passes.
}

/**
* Run Claude CLI with prompt
* Run Claude CLI with prompt delivered via stdin.
*
* @param {string} prompt - The prompt to execute
* @param {string} workDir - Working directory for execution
* @param {Object} config - Orchestrator configuration
* @returns {Promise<Object>} - Execution result with stdout and exit code
*/
async runClaudeCLI(prompt, workDir, config) {
if (!prompt || typeof prompt !== 'string') {
throw new Error('runClaudeCLI requires a non-empty string prompt');
}

return new Promise((resolve, reject) => {
const args = [
'--print', // Non-interactive mode
Expand All @@ -514,21 +523,46 @@ The subtask is complete only when verification passes.
args.push('--model', config.claudeModel);
}

// Escape prompt for shell
const escapedPrompt = prompt.replace(/'/g, "'\\''");

const fullCommand = `echo '${escapedPrompt}' | claude ${args.join(' ')}`;

this.log(`Running Claude CLI in ${workDir}`, 'debug');

const child = spawn('sh', ['-c', fullCommand], {
const child = spawn('claude', args, {
cwd: workDir,
env: { ...process.env },
timeout: config.subtaskTimeout,
stdio: ['pipe', 'pipe', 'pipe'],
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let stdout = '';
let stderr = '';
let stdinError = null;
let hasError = false;

child.on('error', (error) => {
hasError = true;
reject(error);
});

// Prevent unhandled stream errors if the pipe breaks or process exits early
child.stdin.on('error', (err) => {
stdinError = err;
this.log(`Claude stdin stream error: ${err.message}`, 'debug');
});

// Write prompt via stdin to avoid shell-related issues and command injection
if (child.stdin.writable) {
child.stdin.write(prompt);
child.stdin.end();
} else {
// If stdin is not writable, the process might have failed to start or exited immediately
// We wait a bit for 'error' event to fire if it's a spawn error (like ENOENT)
setImmediate(() => {
if (!hasError) {
if (child.kill) child.kill();
reject(new Error('Claude stdin was not writable immediately after spawn'));
}
});
return;
}

child.stdout.on('data', (data) => {
stdout += data.toString();
Expand All @@ -544,17 +578,19 @@ The subtask is complete only when verification passes.
}
});

child.on('close', (code) => {
if (code === 0) {
child.on('close', (code, signal) => {
if (hasError) return; // Already rejected in 'error' handler

if (stdinError) {
reject(new Error(`Claude CLI stdin write failed (prompt not delivered): ${stdinError.message}`));
} else if (code === 0) {
resolve({ stdout, stderr, code });
} else if (signal) {
reject(new Error(`Claude CLI killed by signal ${signal} (timeout or external kill): ${stderr}`));
} else {
reject(new Error(`Claude CLI exited with code ${code}: ${stderr}`));
}
});

child.on('error', (error) => {
reject(error);
});
});
}

Expand Down
63 changes: 51 additions & 12 deletions .aiox-core/core/execution/subagent-dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ class SubagentDispatcher extends EventEmitter {
this.maxRetries = config.maxRetries || 2;
this.retryDelay = config.retryDelay || 2000;

// Timeout configuration
this.claudeTimeout = config.claudeTimeout || 10 * 60 * 1000; // 10 minutes

// Dependencies
this.memoryQuery = config.memoryQuery || (MemoryQuery ? new MemoryQuery() : null);
this.gotchasMemory = config.gotchasMemory || (GotchasMemory ? new GotchasMemory() : null);
Expand Down Expand Up @@ -642,24 +645,58 @@ class SubagentDispatcher extends EventEmitter {
}

/**
* Execute prompt via Claude CLI
* @param {string} prompt - Prompt to execute
* @returns {Promise<Object>} - Execution result
* Execute prompt via Claude CLI using stdin for robust handling.
* This approach avoids shell-related parsing issues and command injection.
*
* @param {string} prompt - The prompt content to send to Claude
* @returns {Promise<Object>} - Execution result with success flag and output
*/
executeClaude(prompt) {
if (!prompt || typeof prompt !== 'string') {
return Promise.reject(new Error('executeClaude requires a non-empty string prompt'));
}

return new Promise((resolve, reject) => {
const args = ['--print', '--dangerously-skip-permissions'];
const escapedPrompt = prompt.replace(/'/g, "'\\''");
const fullCommand = `echo '${escapedPrompt}' | claude ${args.join(' ')}`;

const child = spawn('sh', ['-c', fullCommand], {
const child = spawn('claude', args, {
cwd: this.rootPath,
env: { ...process.env },
stdio: ['pipe', 'pipe', 'pipe'],
timeout: this.claudeTimeout,
});

let stdout = '';
let stderr = '';
let stdinError = null;
let hasError = false;

child.on('error', (error) => {
hasError = true;
reject(error);
});

// Prevent unhandled stream errors if the pipe breaks or process exits early
child.stdin.on('error', (err) => {
stdinError = err;
this.log('stdin_write_error', { error: err.message, code: err.code });
});

// Write prompt via stdin to avoid shell-related issues and command injection
if (child.stdin.writable) {
child.stdin.write(prompt);
child.stdin.end();
} else {
// If stdin is not writable, the process might have failed to start or exited immediately
// We wait a bit for 'error' event to fire if it's a spawn error (like ENOENT)
setImmediate(() => {
if (!hasError) {
if (child.kill) child.kill();
reject(new Error('Claude stdin was not writable immediately after spawn'));
}
});
return;
}

child.stdout.on('data', (data) => {
stdout += data.toString();
Expand All @@ -669,21 +706,23 @@ class SubagentDispatcher extends EventEmitter {
stderr += data.toString();
});

child.on('close', (code) => {
if (code === 0) {
child.on('close', (code, signal) => {
if (hasError) return; // Already rejected in 'error' handler

if (stdinError) {
reject(new Error(`Claude CLI stdin write failed (prompt not delivered): ${stdinError.message}`));
} else if (code === 0) {
resolve({
success: true,
output: stdout,
filesModified: this.extractModifiedFiles(stdout),
});
} else if (signal) {
reject(new Error(`Claude CLI killed by signal ${signal} (timeout or external kill): ${stderr || stdout}`));
} else {
reject(new Error(`Claude CLI exited with code ${code}: ${stderr || stdout}`));
}
});

child.on('error', (error) => {
reject(error);
});
});
}

Expand Down
10 changes: 5 additions & 5 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# - File types for categorization
#
version: 5.0.3
generated_at: "2026-03-11T15:04:09.395Z"
generated_at: "2026-04-18T04:12:10.295Z"
generator: scripts/generate-install-manifest.js
file_count: 1090
files:
Expand Down Expand Up @@ -401,9 +401,9 @@ files:
type: core
size: 34050
- path: core/execution/build-orchestrator.js
hash: sha256:3698635011a845dfc43c46dfd7f15c0aa3ab488938ea3bd281de29157f5c4687
hash: sha256:24878fba391fe2ac055695fbc7fbc6cdc62fcc200b6e8e865e72757c0cbf03c4
type: core
size: 31780
size: 33328
- path: core/execution/build-state-manager.js
hash: sha256:415fca3ad3d750db1f41f14f33971cf661ee9d6073a570175d695bb3c1be655c
type: core
Expand Down Expand Up @@ -433,9 +433,9 @@ files:
type: core
size: 51556
- path: core/execution/subagent-dispatcher.js
hash: sha256:7affbc04de9be2bc53427670009a885f0b35e1cc183f82c2e044abf9611344b6
hash: sha256:e7c803cab96b837cba5e8ca16790e4a9fd6c0b1137ae492dd8ab5cfe03bc4b9e
type: core
size: 25738
size: 27350
- path: core/execution/wave-executor.js
hash: sha256:4e2324edb37ae0729062b5ac029f2891e050e7efd3a48d0f4a1dc4f227a6716b
type: core
Expand Down
Loading
Loading