Skip to content

fix: parallel researcher spawning fails in ForkNode workflows (design mode)#1033

Open
ash-ding wants to merge 1 commit into
akashgit:mainfrom
ash-ding:fix/parallel-researcher-skill-export
Open

fix: parallel researcher spawning fails in ForkNode workflows (design mode)#1033
ash-ding wants to merge 1 commit into
akashgit:mainfrom
ash-ding:fix/parallel-researcher-skill-export

Conversation

@ash-ding

@ash-ding ash-ding commented Jul 22, 2026

Copy link
Copy Markdown

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:

  1. Separate code blocks break shell backgrounding. _fork_to_instruction() in factory/workflow/skill_export.py rendered 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, but wait runs in shell B where there are no children to await. The backgrounded agents become orphans and wait returns immediately.

  2. 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 wait in an empty shell.

Fix:

  • Extract _build_agent_command() helper from _agent_to_instruction()
  • Rewrite _fork_to_instruction() to render all commands + wait in a single code block
  • Add timeout guidance text (e.g. "use --timeout 650") when any agent's timeout exceeds the 120s Bash default
  • Bump _EXPORT_FORMAT_VERSION in skill_cache.py to invalidate stale cached SKILL.md files

Before — 4 separate code blocks, no timeout hint (each becomes its own Bash tool call):

```bash
factory agent researcher --review-tag similar ... &
```

```bash
factory agent researcher --review-tag techstack ... &
```

```bash
factory agent researcher --review-tag pitfalls ... &
```

```bash
wait
```

After — 1 combined code block + timeout guidance:

```bash
factory agent researcher --review-tag similar ... &
factory agent researcher --review-tag techstack ... &
factory agent researcher --review-tag pitfalls ... &
wait
echo "All parallel agents complete"
```

> Timeout note: the slowest agent above needs ~600s.
> Use --timeout 650 on this Bash tool call to avoid the default 120s ceiling.

Files changed

File Change
factory/workflow/skill_export.py Extract _build_agent_command(), rewrite _fork_to_instruction() for single code block + timeout guidance
factory/skill_cache.py Add _EXPORT_FORMAT_VERSION to cache checksum to invalidate stale SKILL.md
tests/test_skill_export.py Add test_fork_commands_in_single_code_block and test_fork_includes_timeout_guidance

Test plan

  • All 52 tests in test_skill_export.py pass (including 2 new tests)
  • All 238 tests in related files pass (test_splitter.py, test_annotations.py, test_workflow_definitions.py, test_workflow_qa.py)
  • ruff check passes on all changed files
  • Manual: run factory ceo "test idea" --mode design and verify 3 researchers spawn in parallel without timeout

🤖 Generated with Claude Code

_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>
georgosgeorgos added a commit that referenced this pull request Jul 22, 2026
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>
@ash-ding ash-ding changed the title fix: render ForkNode parallel commands in a single bash code block fix: parallel researcher spawning fails in ForkNode workflows (design mode) Jul 22, 2026
@osilkin98

Copy link
Copy Markdown
Collaborator

@ceo-review

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.46%. Comparing base (9bfb144) to head (e04a87a).
⚠️ Report is 14 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ 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:

wait

No 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

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.

2 participants