Skip to content

Commit 59547e6

Browse files
authored
Merge pull request #472 from escoffier-labs/feat/roster-scheduler-default
feat(run): allow limits.scheduler roster default for --scheduler
2 parents cdeaff1 + 5ba09f6 commit 59547e6

4 files changed

Lines changed: 75 additions & 3 deletions

File tree

src/brigade/cli/run.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,11 @@ def register(sub: argparse._SubParsersAction) -> None:
256256
p_run.add_argument(
257257
"--scheduler",
258258
choices=("waves", "dag"),
259-
default="waves",
260-
help="Worker scheduling: fixed integer-stage waves (default) or router-DAG ready queue.",
259+
default=None,
260+
help=(
261+
"Worker scheduling: fixed integer-stage waves or router-DAG ready queue. "
262+
"Defaults to limits.scheduler from the roster, then waves."
263+
),
261264
)
262265
p_run.set_defaults(func=dispatch)
263266

@@ -487,7 +490,7 @@ def dispatch(args) -> int:
487490
if args.deliberate:
488491
run_kwargs["deliberation"] = True
489492
run_kwargs["fail_fast"] = not args.keep_going
490-
run_kwargs["scheduler"] = args.scheduler
493+
run_kwargs["scheduler"] = args.scheduler or loaded_roster.scheduler or "waves"
491494
try:
492495
rc = aboyeur_mod.run(args.task, loaded_roster, **run_kwargs)
493496
except runguard.RetainRunLockError:

src/brigade/roster.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class Roster:
5757
allow_models: tuple[str, ...] = ()
5858
timeout_seconds: float = 600.0
5959
sandbox: str | None = None
60+
scheduler: str | None = None
6061
codex_transport: str = "exec"
6162
resolution: RosterResolution | None = None
6263

@@ -321,6 +322,10 @@ def load_roster(path: Path, *, resolution: RosterResolution | None = None) -> Ro
321322
timeout_seconds = _as_positive_number(limits.get("timeout_seconds", 600.0), "limits.timeout_seconds")
322323
sandbox = _as_sandbox(limits.get("sandbox"))
323324

325+
scheduler = limits.get("scheduler")
326+
if scheduler is not None and scheduler not in ("waves", "dag"):
327+
raise ValueError("limits.scheduler must be one of: waves, dag")
328+
324329
codex_transport = data.get("codex_transport", "exec")
325330
if codex_transport not in CODEX_TRANSPORT_CHOICES:
326331
choices = ", ".join(CODEX_TRANSPORT_CHOICES)
@@ -505,6 +510,7 @@ def load_roster(path: Path, *, resolution: RosterResolution | None = None) -> Ro
505510
allow_models=allow_models,
506511
timeout_seconds=timeout_seconds,
507512
sandbox=sandbox,
513+
scheduler=scheduler,
508514
codex_transport=codex_transport,
509515
resolution=resolution,
510516
)

tests/test_roster.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,19 @@ def test_collect_seat_receipt_stats_counts_failures_without_durations(tmp_path):
13891389
assert coder.sample_count == 2
13901390
assert coder.median_duration_seconds == pytest.approx(10.0)
13911391
assert coder.failure_rate == pytest.approx(0.5)
1392+
1393+
1394+
def test_load_accepts_scheduler_limit(tmp_path):
1395+
for scheduler in ("waves", "dag"):
1396+
text = VALID.replace("[limits]\n", f'[limits]\nscheduler = "{scheduler}"\n')
1397+
assert roster_mod.load_roster(_write(tmp_path, text)).scheduler == scheduler
1398+
1399+
1400+
def test_load_defaults_scheduler_to_none(tmp_path):
1401+
assert roster_mod.load_roster(_write(tmp_path, VALID)).scheduler is None
1402+
1403+
1404+
def test_load_rejects_invalid_scheduler_limit(tmp_path):
1405+
text = VALID.replace("[limits]\n", '[limits]\nscheduler = "ready-queue"\n')
1406+
with pytest.raises(ValueError, match="limits.scheduler"):
1407+
roster_mod.load_roster(_write(tmp_path, text))

tests/test_run_cli.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2894,3 +2894,50 @@ def fail_run(*args, **kwargs):
28942894
err = capsys.readouterr().err
28952895
assert "chef" in err
28962896
assert "orchestrator" in err.lower()
2897+
2898+
2899+
def test_run_cli_scheduler_defaults_from_roster_limits(tmp_path, monkeypatch):
2900+
roster_path = tmp_path / "roster.toml"
2901+
roster_path.write_text(
2902+
"""
2903+
orchestrator = "chef"
2904+
2905+
[limits]
2906+
scheduler = "dag"
2907+
2908+
[agents.chef]
2909+
cli = "codex"
2910+
role = "plan"
2911+
2912+
[agents.coder]
2913+
cli = "ollama:llama3.3"
2914+
role = "code"
2915+
"""
2916+
)
2917+
seen = {}
2918+
2919+
def fake_run(task, loaded_roster, **kwargs):
2920+
seen.update(kwargs)
2921+
return 0
2922+
2923+
monkeypatch.setattr(aboyeur, "run", fake_run)
2924+
rc = cli.main(["run", "do something", "--roster", str(roster_path), "--cwd", str(tmp_path), "--no-artifacts"])
2925+
assert rc == 0
2926+
assert seen["scheduler"] == "dag"
2927+
2928+
seen.clear()
2929+
rc = cli.main(
2930+
[
2931+
"run",
2932+
"do something",
2933+
"--roster",
2934+
str(roster_path),
2935+
"--cwd",
2936+
str(tmp_path),
2937+
"--scheduler",
2938+
"waves",
2939+
"--no-artifacts",
2940+
]
2941+
)
2942+
assert rc == 0
2943+
assert seen["scheduler"] == "waves"

0 commit comments

Comments
 (0)