Problem
When the outer loop evaluates candidates, InnerLoop.step() spawns sub-CEOs with no task instruction:
# factory/inner_loop.py:198-199
result = subprocess.run(
[sys.executable, "-m", "factory", "ceo", str(self.project_dir),
"--mode", self.mode, "--headless", "--no-worktree"],
cwd=self.project_dir,
)
No --focus, no --prompt, no directives. Each sub-CEO falls back to reading .factory/strategy/backlog.md and autonomously picking whatever looks interesting. This means:
- Candidates work on unrelated things — topology gets ranked on work unrelated to what the gate measures
- The search selects for inaction — candidates that don't touch the forecaster reliably return baseline, while candidates that attempt real changes risk losing a rung
- No fair comparison — different topologies solve different problems, so the evolutionary search can't tell which topology is better at the intended task
Observed in practice
Generation 0 on forecastbench with a Brier Index gate (uv run pytest gate/ -q):
| Candidate |
Score |
Nodes |
What it did |
| 8b49ebea |
0.5355 |
1 |
Logging (never touched forecaster) |
| cb2ef3fc |
0.5255 |
2 |
Logging (never touched forecaster) |
| 33bd7880 |
0.4345 |
2 |
CutoffEnvironment integration (changed eval.py) |
The top two scored identically because the cache held — they never changed forecasting behavior. The one that tried real work lost a rung. The search will now converge on "do nothing useful."
Proposed fix
Add a task_instruction field (or reuse --focus) that flows from config to sub-CEOs:
- Add
task_instruction: str = "" to SwarmConfig
- Accept
--focus on factory outer-loop calibrate CLI, stored in SwarmConfig.task_instruction
- In
InnerLoop.step(), when task_instruction is set, append --focus to the subprocess command
- This uses existing plumbing:
--focus writes to the backlog, injects a Focus Directive into the CEO task, and forces single-item mode
The directives mechanism (InnerLoop._write_directives() → .factory/messages/) is another option but requires the CEO to read pending messages, which is less guaranteed than --focus.
Context
This surfaced while using the multi-benchmark support from #1332. The benchmark config (forecastbench.toml) and pluggable evaluators work, but the sub-CEOs still don't know what to optimize. The benchmark config could also be extended with a task_instruction field that auto-populates the focus.
Example of desired usage
factory outer-loop calibrate ~/projects/work/forecastbench \
--benchmark forecastbench \
--focus "Improve the Brier Index by modifying the forecasting pipeline in lab_forecaster.py" \
--test-command "uv run pytest gate/ -q" \
--test-format pytest \
--population-size 3 \
--budget 9
Or via TOML:
[meta]
name = "forecastbench"
description = "ForecastBench — Brier score evaluation"
task_instruction = "Improve the Brier Index by modifying the forecasting pipeline"
[test]
format = "pytest"
command = "uv run pytest gate/ -q"
Problem
When the outer loop evaluates candidates,
InnerLoop.step()spawns sub-CEOs with no task instruction:No
--focus, no--prompt, no directives. Each sub-CEO falls back to reading.factory/strategy/backlog.mdand autonomously picking whatever looks interesting. This means:Observed in practice
Generation 0 on forecastbench with a Brier Index gate (
uv run pytest gate/ -q):The top two scored identically because the cache held — they never changed forecasting behavior. The one that tried real work lost a rung. The search will now converge on "do nothing useful."
Proposed fix
Add a
task_instructionfield (or reuse--focus) that flows from config to sub-CEOs:task_instruction: str = ""toSwarmConfig--focusonfactory outer-loop calibrateCLI, stored inSwarmConfig.task_instructionInnerLoop.step(), whentask_instructionis set, append--focusto the subprocess command--focuswrites to the backlog, injects a Focus Directive into the CEO task, and forces single-item modeThe
directivesmechanism (InnerLoop._write_directives()→.factory/messages/) is another option but requires the CEO to read pending messages, which is less guaranteed than--focus.Context
This surfaced while using the multi-benchmark support from #1332. The benchmark config (
forecastbench.toml) and pluggable evaluators work, but the sub-CEOs still don't know what to optimize. The benchmark config could also be extended with atask_instructionfield that auto-populates the focus.Example of desired usage
Or via TOML: