Skip to content

refactor: robust prompt handling in subagent dispatcher and build orchestrator#142

Open
RinZ27 wants to merge 8 commits intoSynkraAI:mainfrom
RinZ27:robust/prompt-handling-stdin-clean
Open

refactor: robust prompt handling in subagent dispatcher and build orchestrator#142
RinZ27 wants to merge 8 commits intoSynkraAI:mainfrom
RinZ27:robust/prompt-handling-stdin-clean

Conversation

@RinZ27
Copy link
Copy Markdown

@RinZ27 RinZ27 commented Feb 13, 2026

Refactored the Claude execution logic in SubagentDispatcher and BuildOrchestrator to use stdin instead of shell interpolation.

During a review of the execution layer, I noticed prompts were being passed via echo and shell pipes. This approach is prone to failure if the LLM output or task description contains shell-sensitive characters (like complex quotes or pipes), even with basic escaping.

This update switches to a direct stdin write, which:

  1. Ensures consistent prompt handling across all providers (aligning with GeminiProvider).
  2. Eliminates shell-related execution errors when handling complex autonomous tasks.
  3. Improves the overall robustness of the core execution loop.

Verified with existing test suites.

Summary by CodeRabbit

  • Bug Fixes

    • More reliable assistant invocation: prompts are validated, sent via a direct CLI stdin stream, and failures (stdin errors, abnormal termination, timeouts) report clearer, more specific errors.
  • Tests

    • New tests cover prompt delivery over stdin, process interactions, and returned execution results.
  • Chores

    • Updated verification checksums for a couple of registry entries.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces shell-echo piping with direct claude process spawning in two execution modules; prompts are validated, written to the child process stdin, and stdin errors are handled. Adds tests that mock spawn/stdin behavior. Updates two checksums and verification timestamps in the entity registry.

Changes

Cohort / File(s) Summary
CLI Invocation Refactor
.​aios-core/core/execution/build-orchestrator.js, .​aios-core/core/execution/subagent-dispatcher.js
Removed shell echo/pipe approach and shell-escape logic. Now spawns claude directly with args, validates non-empty prompt strings, writes prompt to child.stdin then calls stdin.end(), and attaches stdin error handling; expands process close handling to surface stdin/write and signal-termination errors; stdout/stderr streaming retained.
Tests (Claude stdin behavior)
tests/core/claude-execution-robustness.test.js
New tests mock child_process.spawn to assert direct claude spawn with expected args/cwd, verify prompt is written to stdin and ended, simulate stdout emission, and validate returned result shapes and outputs.
Entity Registry Update
.​aios-core/data/entity-registry.yaml
Updated two observable checksums and their lastVerified timestamps; content-only change, no API/signature modifications.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels: core, agents, tests

🚥 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 title accurately captures the main refactoring effort: replacing shell-based prompt handling with robust stdin-based delivery in both SubagentDispatcher and BuildOrchestrator.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In @.aios-core/core/execution/subagent-dispatcher.js:
- Around line 653-661: Add an 'error' event handler on the spawned process stdin
to prevent unhandled stream errors: after creating the child via spawn(...) and
before/around child.stdin.write(prompt) attach child.stdin.on('error', (err) =>
{ /* log with context and safely ignore or handle EPIPE/stream errors */ });
also guard the write by checking child.stdin.writable (or wrap the write in
try/catch) so that the code using child, spawn, and child.stdin.write cannot
throw an unhandled exception if the pipe breaks or the child exits early.

Comment thread .aios-core/core/execution/subagent-dispatcher.js Outdated
Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In @.aios-core/core/execution/subagent-dispatcher.js:
- Around line 651-653: In executeClaude, validate the prompt before any
childProcess.stdin.write occurs: inside the Promise wrapper at the top of
executeClaude check if prompt === null || prompt === undefined and immediately
reject the Promise (or throw a TypeError) with a clear message instead of
allowing "null"/"undefined" to be written to stdin; update callers only if
necessary, and ensure the check is placed before the code that builds args and
calls child.stdin.write so the guard prevents downstream issues.
🧹 Nitpick comments (1)
tests/core/claude-execution-robustness.test.js (1)

35-63: Only happy-path tests — consider adding error and edge-case coverage.

Both test blocks verify the success scenario exclusively. For a robustness-focused test suite, consider adding cases for:

  • Non-zero exit code from the spawned process (verifies reject path)
  • spawn emitting an 'error' event (e.g., claude binary not found — ENOENT)
  • Prompts containing edge-case content (empty string, very large input)

This is especially relevant since the PR title emphasizes robustness.

Also applies to: 66-88

Comment thread .aios-core/core/execution/subagent-dispatcher.js
@RinZ27 RinZ27 force-pushed the robust/prompt-handling-stdin-clean branch from 33578b1 to dce23fb Compare February 13, 2026 14:18
@RinZ27 RinZ27 force-pushed the robust/prompt-handling-stdin-clean branch from dce23fb to 229b80b Compare February 13, 2026 14:22
coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 13, 2026
Copy link
Copy Markdown
Contributor

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

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

Excellent security improvement. Approving.

What this fixes

The previous implementation used shell pipe (echo '${escapedPrompt}' | claude) which is vulnerable to:

  • Command injection via crafted prompts containing shell metacharacters
  • Quote escaping failures — the replace(/'/g, "'\\\\''") approach is fragile and breaks with certain Unicode characters or nested quotes

The new approach uses child.spawn('claude', args) with stdin.write(prompt) which completely bypasses the shell, eliminating the injection surface.

Code quality observations

  • ✅ Input validation added (if (!prompt || typeof prompt !== 'string'))
  • ✅ Graceful stdin error handling (child.stdin.on('error', ...))
  • ✅ Guard for writable check (if (child.stdin.writable))
  • ✅ Both SubagentDispatcher.executeClaude and BuildOrchestrator.runClaudeCLI are updated consistently
  • ✅ Comprehensive test suite covering success, validation, error, and spawn failure scenarios
  • ✅ entity-registry.yaml checksums updated to match the new file hashes

Minor note

The child.stdin.on('error') callback is empty — this is fine since errors are handled via the child.on('error') / child.on('close') events. A debug log there could help troubleshooting in the future, but not blocking.

Well done — this is exactly the kind of security hardening the codebase needs. 🔒

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 19, 2026

@RinZ27 is attempting to deploy a commit to the Pedro Valério Lopez's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
.aios-core/core/execution/subagent-dispatcher.js (1)

665-668: Silent stdin error handler loses observability context.

The no-op handler prevents the unhandled-exception crash (past comment addressed ✅), but an EPIPE or write error on stdin is completely invisible — no log entry, no metric. In the EPIPE case the close handler will ultimately reject, but you lose the root-cause signal. A single structured log call here would make debugging much easier in production.

🔍 Suggested improvement
-      child.stdin.on('error', (err) => {
-        // Handled via child 'error' or 'close' events
-      });
+      child.stdin.on('error', (err) => {
+        // EPIPE/ECONNRESET are expected if the child exits early;
+        // the 'close' handler below will reject the promise with the exit code.
+        this.log('stdin_write_error', { error: err.message, code: err.code });
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.aios-core/core/execution/subagent-dispatcher.js around lines 665 - 668, The
current no-op error handler on child.stdin ('child.stdin.on("error", (err) => {
... })') swallows EPIPE/write errors and loses observability; update that
handler to emit a single structured log (e.g., processLogger.error or
logger.error) including the error object and contextual identifiers (child.pid,
command/args or task id available in the surrounding scope) and then return
without rethrowing so the unhandled crash is still avoided; ensure the handler
does not alter control flow (no throw) and keeps the existing behavior of
letting the child's 'error' or 'close' handlers handle lifecycle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.aios-core/core/execution/build-orchestrator.js:
- Around line 536-544: The current stdin error handler for child.stdin
(child.stdin.on('error', ...)) silently swallows errors and write/end are
conditional on child.stdin.writable, which can both mask EPIPE failures and
leave the child hanging; update the logic so that (1) the child.stdin 'error'
handler captures the error into a scoped variable (e.g., stdinError) and does
not ignore it, (2) the Promise resolution in the child's 'close' handler checks
for that stdinError and rejects with it (or includes it in the
rejection/returned error), and (3) always signal EOF to the child (call
child.stdin.end()) even if the writable flag is false/unreliable—ensure end() is
invoked in a safe pathway (e.g., try/catch/finally or a fallback setImmediate)
after attempting child.stdin.write(prompt) so the child cannot block waiting for
input; reference child.stdin.on('error'), child.stdin.write(prompt),
child.stdin.end(), and the child's 'close' handler to implement these changes.
- Around line 528-533: The child process 'child' created via spawn('claude',
args, ...) has a 'close' event handler that only inspects 'code' and logs
"Claude CLI exited with code null", which hides timeout signals; update the
'close' listener to accept both (code, signal) parameters and include the
'signal' value in the error/log output (e.g., when code === null or non-zero,
report both code and signal and special-case SIGTERM/SIGKILL to indicate a
timeout) so timeout diagnostics are clear.

In @.aios-core/core/execution/subagent-dispatcher.js:
- Around line 671-674: The current branch that writes to the child's stdin
checks child.stdin.writable but lacks an else path, which can leave the Promise
hanging; modify the block around child.stdin.writable so that if it's false you
immediately reject the surrounding Promise (include an Error with context) and
ensure any necessary cleanup is performed; specifically update the logic that
calls child.stdin.write(prompt) / child.stdin.end() to add an else that calls
reject(new Error(...)) (reference child.stdin.writable, child.stdin.write,
child.stdin.end, and the child 'close' event) so the caller is not left waiting
indefinitely.

---

Duplicate comments:
In @.aios-core/core/execution/subagent-dispatcher.js:
- Around line 652-654: The prompt validation in executeClaude (the guard `if
(!prompt || typeof prompt !== 'string') { return Promise.reject(new
Error('executeClaude requires a non-empty string prompt')); }`) is correct and
requires no changes—leave the check as-is since it safely handles
null/undefined/empty/non-string inputs and returning Promise.reject from this
non-async function is valid.

---

Nitpick comments:
In @.aios-core/core/execution/subagent-dispatcher.js:
- Around line 665-668: The current no-op error handler on child.stdin
('child.stdin.on("error", (err) => { ... })') swallows EPIPE/write errors and
loses observability; update that handler to emit a single structured log (e.g.,
processLogger.error or logger.error) including the error object and contextual
identifiers (child.pid, command/args or task id available in the surrounding
scope) and then return without rethrowing so the unhandled crash is still
avoided; ensure the handler does not alter control flow (no throw) and keeps the
existing behavior of letting the child's 'error' or 'close' handlers handle
lifecycle.

Comment thread .aiox-core/core/execution/build-orchestrator.js
Comment thread .aios-core/core/execution/build-orchestrator.js
Comment thread .aios-core/core/execution/subagent-dispatcher.js
Copy link
Copy Markdown

@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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.aios-core/core/execution/build-orchestrator.js:
- Around line 564-570: Update the child.on('close', ...) handler in
build-orchestrator.js to accept both parameters (code, signal) and use signal
when code is null so timeout/kill cases are reported clearly; if signal is
present, reject with a message like "Claude CLI terminated by signal SIGTERM"
(instead of "code null"), and also include any existing stdinError variable in
the rejection message or log to surface IO errors—modify the resolve/reject
logic inside the close callback to prefer a human-readable signal description
when code === null and to append stderr and stdinError details for complete
diagnostics.
- Around line 535-545: Replace the current stdin error-swallowing logic: install
a variable (e.g., stdinError) outside the handlers, keep the
child.stdin.on('error', ...) handler but make it assign the error to stdinError
(not just console.debug) so the Promise/close handler can see it; always attempt
to close the stream (call child.stdin.end()) in a safe try/catch/finally path
even if child.stdin.writable is false (so the child won't hang), and wrap
child.stdin.write(prompt) in try/catch to capture write failures and set
stdinError; finally, update the process close handler to fail/reject or
propagate an error when stdinError is set (rather than treating a zero exit with
empty stdout as success). Ensure references to child.stdin.on('error'),
child.stdin.write(prompt), child.stdin.end(), the stdinError variable, and the
close handler are updated accordingly.

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.aios-core/core/execution/build-orchestrator.js:
- Around line 535-550: Attach the child process 'error' listener before you
check stdin.writable so the listener (child.on('error', reject)) is always
registered even if you take the early-return path, and replace the direct
console.debug call in the stdin error handler with the BuildOrchestrator logging
method (e.g., this.log.debug or this.log with appropriate verbosity) so debug
output respects this.config.verbose; keep the existing stdinError capture and
the reject call in the non-writable branch but ensure the error listener is
already attached prior to potentially returning.

In @.aios-core/core/execution/subagent-dispatcher.js:
- Around line 659-663: executeClaude currently spawns Claude without a timeout
causing hung processes; add a configurable timeout (e.g., claudeTimeout or
subtaskTimeout) to SubagentDispatcher (initialize in the constructor with a
sensible default like 10 minutes), expose it on the instance so callers can
override, and pass that value into the spawn options inside executeClaude
(include timeout and keep signal handling intact). Also ensure the existing
close handler in executeClaude (the branch that checks for signal) will be
reachable on timeout by confirming the spawn timeout triggers the child
termination and that any try/catch around the spawn/stream handling wraps and
rethrows errors with context (including when timeout occurs). Ensure dispatch
callers that create SubagentDispatcher can opt in via the new constructor
parameter or config field.
- Around line 665-680: Move the child process "error" listener to immediately
after the spawn so it is registered in all paths (e.g., add child.on('error',
reject) right after creating child) instead of after the stdin writable check;
ensure the listener uses the same reject handler used elsewhere. Also replace
the console.debug call that logs stdin errors with the class logging mechanism
(this.log or dispatchLog) so diagnostics follow existing logging conventions
(use stdinError variable and the same message text when calling this.log).

Comment thread .aios-core/core/execution/build-orchestrator.js Outdated
Comment thread .aios-core/core/execution/subagent-dispatcher.js
Comment thread .aios-core/core/execution/subagent-dispatcher.js Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 19, 2026
@RinZ27 RinZ27 requested a review from nikolasdehor February 21, 2026 08:06
Copy link
Copy Markdown
Contributor

@nikolasdehor nikolasdehor left a comment

Choose a reason for hiding this comment

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

Re-review: Novos commits desde 18/02

Revisitando apos 5 novos commits adicionados desde meu review original.

O que mudou

Os commits 70933742, 6b0e158b, e 352027e0 endereçam diretamente o ponto que levantei no review anterior sobre o callback vazio no child.stdin.on('error'). Especificamente:

  1. Debug logging no stdin -- Agora ambos os arquivos logam o erro com mensagem e code:

    • build-orchestrator.js: this.log(\Claude stdin stream error: ${err.message}`, 'debug')`
    • subagent-dispatcher.js: this.log('stdin_write_error', { error: err.message, code: err.code })
  2. Diagnostico de exit por signal -- O handler close agora recebe (code, signal) e diferencia 3 cenarios:

    • stdinError --> rejeita com mensagem clara de que o prompt nao foi entregue
    • signal --> rejeita com mensagem indicando timeout ou kill externo
    • code !== 0 --> comportamento anterior mantido
  3. Timeout configuravel -- SubagentDispatcher agora aceita config.claudeTimeout (default 10min) e passa para o spawn(). Isso e uma melhoria pratica -- antes o timeout era herdado apenas do config.subtaskTimeout no BuildOrchestrator.

  4. Registro de erro antecipado -- child.on('error') foi movido para antes do stdin.write(), garantindo que erros de spawn (ENOENT) sejam capturados mesmo se ocorrerem antes da escrita.

  5. Merge conflicts -- Os dois commits de merge (273bed5b, 6746e5e8) sao apenas resolucoes de conflito no entity-registry.yaml, sem impacto funcional.

Verificacao

  • O padrao stdin e consistente entre ambos os arquivos (SubagentDispatcher e BuildOrchestrator)
  • Os testes em claude-execution-robustness.test.js cobrem os cenarios de sucesso, validacao de input, exit code nao-zero, e erro de spawn
  • A prioridade correta de erros no close handler: stdinError > signal > exit code

Todas as sugestoes do review anterior foram endereçadas. Aprovando.

re-reviewed by @nikolasdehor

@github-actions
Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had
recent activity. Please update it or it may be closed.

@github-actions github-actions Bot added the status: stale No activity for 30+ days label Apr 18, 2026
@github-actions github-actions Bot added area: agents Agent system related area: workflows Workflow system related labels Apr 18, 2026
@github-actions github-actions Bot added squad mcp type: test Test coverage and quality area: core Core framework (.aios-core/core/) area: installer Installer and setup (packages/installer/) area: synapse SYNAPSE context engine area: cli CLI tools (bin/, packages/aios-pro-cli/) area: pro Pro features (pro/) area: health-check Health check system area: docs Documentation (docs/) area: devops CI/CD, GitHub Actions (.github/) labels Apr 18, 2026
@RinZ27
Copy link
Copy Markdown
Author

RinZ27 commented Apr 18, 2026

Merged the latest main to resolve the path conflicts from the project rename. While updating the test suite, I refined the stdin write logic to handle spawn failures more robustly using a dedicated error flag. Everything is passing locally now that the paths are corrected to .aiox-core. @oalanicolas @Pedrovaleriolopez, please take another look.

@github-actions github-actions Bot removed the status: stale No activity for 30+ days label Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent system related area: cli CLI tools (bin/, packages/aios-pro-cli/) area: core Core framework (.aios-core/core/) area: devops CI/CD, GitHub Actions (.github/) area: docs Documentation (docs/) area: health-check Health check system area: installer Installer and setup (packages/installer/) area: pro Pro features (pro/) area: synapse SYNAPSE context engine area: workflows Workflow system related mcp squad type: test Test coverage and quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants