Skip to content

Outer loop: sub-CEOs receive no task instruction (--focus) #1338

Description

@lukeinglis

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:

  1. Candidates work on unrelated things — topology gets ranked on work unrelated to what the gate measures
  2. 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
  3. 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:

  1. Add task_instruction: str = "" to SwarmConfig
  2. Accept --focus on factory outer-loop calibrate CLI, stored in SwarmConfig.task_instruction
  3. In InnerLoop.step(), when task_instruction is set, append --focus to the subprocess command
  4. 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"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions