fix: parallel researcher spawning fails in ForkNode workflows (design mode)#1033
fix: parallel researcher spawning fails in ForkNode workflows (design mode)#1033ash-ding wants to merge 1 commit into
Conversation
_fork_to_instruction() was calling _agent_to_instruction() per target, which wraps each command in its own ```bash``` code block. The CEO (a Claude Code instance) interprets each code block as a separate Bash tool call, so the shell `&` and `wait` end up in different processes — `wait` has nothing to wait for, and the backgrounded agents become orphans. The CEO then reports "parallel research spawn timed out" and falls back to sequential execution. Root causes: 1. Separate code blocks: each `&` runs in its own shell; `wait` in a new shell has no children to await 2. Bash tool timeout: even when the CEO combines commands, the default 120s Bash tool timeout kills the shell before 600s researchers finish Fix: - Extract _build_agent_command() from _agent_to_instruction() so the raw CLI string can be reused without code block wrapping - Rewrite _fork_to_instruction() to collect all commands and render them in a single ```bash``` block with `wait` at the end - Add timeout guidance so the CEO sets an appropriate Bash tool timeout - Bump _EXPORT_FORMAT_VERSION in skill_cache.py to invalidate stale cached SKILL.md files that still have the old separate-block format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add founder_workflow() — a 5-node terminal workflow for rapid prototyping: study → strategist → builder → gate_tests (pytest+ruff) → finalize(--force). No deep-QA, no eval scoring, no research phase. Triggers on HAS_FACTORY + mode=founder. Includes WORKFLOW_META entry for skill export and 13 test methods covering graph validation, registration, trigger, terminal flag, node structure, and skill export. Closes #1033 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@ceo-review |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1033 +/- ##
==========================================
+ Coverage 89.10% 89.46% +0.36%
==========================================
Files 124 127 +3
Lines 14638 15178 +540
==========================================
+ Hits 13043 13579 +536
- Misses 1595 1599 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
✅ Factory Review: KEEP
Verdict: KEEP
Reason: QA: CLEAN — 3424 tests pass, 0 failures, lint clean, types clean, 9/9 adversarial tests verified with evidence, 7/7 code review categories PASS
QA Analysis
Adversarial QA — PR #1033 (fix/parallel-researcher-skill-export)
Detected project type: Library (Python CLI with workflow graph engine)
Smoke test: PASSED (52/52 pytest tests in tests/test_skill_export.py)
Test Plan
Derived from acceptance criteria: verify that _fork_to_instruction() renders parallel commands in a single bash code block, timeout guidance is conditional on agent timeout, helper extraction is clean, and skill cache version bump invalidates checksums.
Feature Tests with Evidence
T1: ForkNode with 3 researcher targets produces exactly 1 bash code block
Status: VERIFIED
Command:
uv run python -c "..._fork_to_instruction(fork, wf) with 3 researcher AgentNodes..."
Output:
T1 RESULT: bash code blocks = 1
Single ```bash block confirmed. No separate per-agent blocks.
T2: Single code block contains all 3 commands with &, wait, and echo
Status: VERIFIED
Command:
uv run python -c "...extract bash block content via regex, count ampersands..."
Output:
T2 RESULT: ampersands=3, wait=True, echo=True, all_3_cmds=True
Block content:
factory agent researcher --review-tag a --task "..." --project "$PROJECT_PATH" --timeout ... &
factory agent researcher --review-tag b --task "..." --project "$PROJECT_PATH" --timeout ... &
factory agent researcher --review-tag c --task "..." --project "$PROJECT_PATH" --timeout ... &
wait
echo "All parallel agents complete"
T3: Timeout guidance appears when agents have timeout > 120s
Status: VERIFIED
Command:
uv run python -c "..._fork_to_instruction with default researcher timeout (600s)..."
Output:
T3 RESULT: timeout_guidance=True, mentions_600s=True
Guidance line present:
**Important:** Run ALL commands above in a **single** Bash tool call with timeout set to at least 600 seconds.
T4: Timeout guidance does NOT appear when all agents have timeout <= 120s
Status: VERIFIED
Command:
uv run python -c "..._fork_to_instruction with timeout=60 on both agents..."
Output:
T4 RESULT: timeout_guidance_present=False
No "Run ALL commands above in a single Bash tool call" line in output. Code block ends cleanly after the closing ```.
T5: _agent_to_instruction() output unchanged from pre-refactor behavior
Status: VERIFIED
Command:
uv run python -c "..._agent_to_instruction(builder_node, wf)..."
Output:
T5 RESULT: bash_blocks=1, factory_agent=True, timeout=True, annotations=True, no_ampersand_for_blocking=True
Produces its own ```bash code block with factory agent builder, --timeout, annotation comments, and no trailing & for blocking agents.
T6: _build_agent_command() returns raw command string without code block markers
Status: VERIFIED
Command:
uv run python -c "..._build_agent_command(researcher_node, wf, is_parallel=True)..."
Output:
T6 RESULT: no_backticks=True, has_factory_agent=True, has_trailing_ampersand=True
T6 RAW COMMAND: factory agent researcher --review-tag a --task "..." --project "$PROJECT_PATH" --timeout ... &
No ``` markers anywhere in the returned string. Pure CLI command.
T7: _EXPORT_FORMAT_VERSION is '2'
Status: VERIFIED
Command:
uv run python -c "from factory.skill_cache import _EXPORT_FORMAT_VERSION; print(repr(_EXPORT_FORMAT_VERSION))"
Output:
T7 RESULT: _EXPORT_FORMAT_VERSION = '2'
T8: _compute_checksum produces different results when _EXPORT_FORMAT_VERSION changes
Status: VERIFIED
Command:
uv run python -c "...compute checksum with version '2' and manually with version '1', compare..."
Output:
T8 RESULT: checksum_v2=8ad870813313bf39, checksum_v1=db9ad3ea9b72b680
T8 DIFFERENT: True
Same workflow data, different version suffix → different checksum. Cache invalidation works.
T9: ForkNode with zero AgentNode targets (only FnNode) falls back to bare wait block
Status: VERIFIED
Command:
uv run python -c "..._fork_to_instruction(fork with only FnNode target)..."
Output:
T9 RESULT: has_wait=True, no_factory_agent=True, bare_wait_block=True
Output contains only:
waitNo factory agent commands. Graceful fallback confirmed.
Acceptance Criteria Verification
| # | Criterion | Status |
|---|---|---|
| T1 | ForkNode with 3 targets → exactly 1 bash code block | VERIFIED |
| T2 | Single block has all 3 & commands + wait + echo |
VERIFIED |
| T3 | Timeout guidance present when max_timeout > 120s | VERIFIED |
| T4 | Timeout guidance absent when all timeouts <= 120s | VERIFIED |
| T5 | _agent_to_instruction() behavior unchanged by refactor |
VERIFIED |
| T6 | _build_agent_command() returns raw string, no code block |
VERIFIED |
| T7 | _EXPORT_FORMAT_VERSION is '2' |
VERIFIED |
| T8 | Checksum changes when format version changes | VERIFIED |
| T9 | ForkNode with zero AgentNode targets → bare wait block |
VERIFIED |
Adversarial Verdict: PASS
All 9 acceptance criteria verified with evidence. The fix correctly renders parallel ForkNode commands in a single bash code block, conditionally emits timeout guidance, cleanly extracts the _build_agent_command() helper, and bumps the skill cache version to invalidate stale cached skills.
Posted by Factory CEO
Summary
Problem: In design mode (and all modes with parallel researchers), the CEO agent reports "parallel research spawn timed out" and falls back to sequential execution.
Two root causes, both equally critical:
Separate code blocks break shell backgrounding.
_fork_to_instruction()infactory/workflow/skill_export.pyrendered each parallel agent command in a separate fenced code block. The CEO (a Claude Code instance) treats each code block as an independent Bash tool call — each runs in its own shell process. So&backgrounds a process in shell A, butwaitruns in shell B where there are no children to await. The backgrounded agents become orphans andwaitreturns immediately.Missing timeout guidance causes premature kill. Even when the CEO manages to put all commands in a single shell (e.g. following the text instructions in
ceo.md), Claude Code's Bash tool has a default 120-second timeout. Researchers are configured with--timeout 600, but the Bash tool kills the entire shell after 120s — long before any researcher finishes. Without explicit timeout guidance in the SKILL.md, the CEO has no way to know it should override the default.Both causes must be fixed together: a single code block without timeout guidance still gets killed at 120s; timeout guidance without a single code block still runs
waitin an empty shell.Fix:
_build_agent_command()helper from_agent_to_instruction()_fork_to_instruction()to render all commands +waitin a single code block_EXPORT_FORMAT_VERSIONinskill_cache.pyto invalidate stale cached SKILL.md filesBefore — 4 separate code blocks, no timeout hint (each becomes its own Bash tool call):
After — 1 combined code block + timeout guidance:
Files changed
factory/workflow/skill_export.py_build_agent_command(), rewrite_fork_to_instruction()for single code block + timeout guidancefactory/skill_cache.py_EXPORT_FORMAT_VERSIONto cache checksum to invalidate stale SKILL.mdtests/test_skill_export.pytest_fork_commands_in_single_code_blockandtest_fork_includes_timeout_guidanceTest plan
test_skill_export.pypass (including 2 new tests)test_splitter.py,test_annotations.py,test_workflow_definitions.py,test_workflow_qa.py)ruff checkpasses on all changed filesfactory ceo "test idea" --mode designand verify 3 researchers spawn in parallel without timeout🤖 Generated with Claude Code