From b705e37f749256b23edec003b445f41d7ded4fa8 Mon Sep 17 00:00:00 2001 From: Koukyosyumei Date: Sat, 11 Jul 2026 15:45:52 -0400 Subject: [PATCH 1/3] update --- examples/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/README.md b/examples/README.md index 148b338..ddfbf77 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,9 +23,9 @@ Then run any example as a plain Python script. Three take the task as an optional CLI argument (falling back to a demo task): ```bash -python examples/ensemble_score.py "implement \`h5i pull\` mirroring \`h5i push\`" -python examples/arena_score.py "make \`h5i doctor\` exit non-zero on repair failures" -python examples/review_escalation.py "fix the flaky msg_integration test" +python examples/ensemble_score.py "implement quicksort" +python examples/arena_score.py "implement quicksort" +python examples/review_escalation.py "implement quicksort" ``` The rest are self-contained — the task is written into the score: From 3f53e6704f4861040806c4c0dd1d48c03e3efce7 Mon Sep 17 00:00:00 2001 From: Koukyosyumei Date: Sat, 11 Jul 2026 15:54:54 -0400 Subject: [PATCH 2/3] fix(examples): respect lazy resident startup --- examples/README.md | 10 ++++++++-- examples/arena_score.py | 4 +++- examples/ensemble_score.py | 4 +++- tests/test_examples.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests/test_examples.py diff --git a/examples/README.md b/examples/README.md index ddfbf77..3d945a3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,8 +16,14 @@ You need four things: most also hire `runtime="codex"`. Both CLIs must be installed and logged in. `launcher="resident"` additionally needs `tmux` (it spawns the sessions). 4. **A repo to work on** — each score opens `Conductor(".", …)`, so run it from - the repository the agents should modify, with a **clean worktree** (most - scores `preflight(clean_worktree=True)` and fail fast otherwise). + the repository the agents should modify, with a **clean worktree** (the + arena and ensemble scores call `preflight(clean_worktree=True)` and fail + fast otherwise). + +`preflight(live=...)` is intended for the default `"attach"` launcher, where +sessions must already be parked on their inboxes. Do not use that check before +the first turn with `launcher="resident"`: resident sessions are started lazily +when a turn is dispatched. Then run any example as a plain Python script. Three take the task as an optional CLI argument (falling back to a demo task): diff --git a/examples/arena_score.py b/examples/arena_score.py index cc33dd6..75ade8f 100644 --- a/examples/arena_score.py +++ b/examples/arena_score.py @@ -23,7 +23,9 @@ async def main(task: str) -> None: c.hire("codex", runtime="codex"), c.hire("haiku", runtime="claude", model="claude-haiku-4-5"), ) - await c.preflight(live=agents, clean_worktree=True) + # LaunchResident starts each session on its first turn, so there is no + # live session to check yet. Still fail fast on repository hygiene. + await c.preflight(clean_worktree=True) outcome = await patterns.arena( c, diff --git a/examples/ensemble_score.py b/examples/ensemble_score.py index 764fd73..69d5fef 100644 --- a/examples/ensemble_score.py +++ b/examples/ensemble_score.py @@ -21,7 +21,9 @@ async def main(task: str) -> None: codex = await c.hire("codex", runtime="codex") # Fail the predictable ways now, not at minute 30. - await c.preflight(live=[claude, codex], clean_worktree=True) + # LaunchResident starts each session on its first turn, so there is no + # live session to check yet. Still fail fast on repository hygiene. + await c.preflight(clean_worktree=True) # Independent attempts, in parallel — then seal the round. a, b = await asyncio.gather( diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..0674459 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,30 @@ +import ast +from pathlib import Path + +import pytest + + +EXAMPLES = Path(__file__).parents[1] / "examples" + + +@pytest.mark.parametrize("path", sorted(EXAMPLES.glob("*.py")), ids=lambda p: p.name) +def test_examples_are_valid_python(path: Path): + ast.parse(path.read_text(), filename=str(path)) + + +@pytest.mark.parametrize("name", ["arena_score.py", "ensemble_score.py"]) +def test_resident_examples_do_not_preflight_live_sessions(name: str): + tree = ast.parse((EXAMPLES / name).read_text()) + live_checks = [ + call + for call in ast.walk(tree) + if isinstance(call, ast.Call) + and isinstance(call.func, ast.Attribute) + and call.func.attr == "preflight" + and any(keyword.arg == "live" for keyword in call.keywords) + ] + + assert not live_checks, ( + 'launcher="resident" starts sessions lazily on the first turn; ' + "preflight(live=...) fails before that turn can start them" + ) From 30d2d678fc91f7d0e6d92e7889325fda10af143e Mon Sep 17 00:00:00 2001 From: Koukyosyumei Date: Sat, 11 Jul 2026 17:00:04 -0400 Subject: [PATCH 3/3] chore(examples): pin inexpensive agent models --- examples/README.md | 20 +++++++++++--------- examples/arena_score.py | 4 ++-- examples/custom_control_flow.py | 11 +++++++++-- examples/debate_then_build.py | 8 +++++--- examples/ensemble_score.py | 6 ++++-- examples/judge_panel_score.py | 8 ++++---- examples/pipeline_score.py | 10 +++++++--- examples/review_escalation.py | 4 +++- examples/tournament.py | 6 +++--- tests/test_examples.py | 24 ++++++++++++++++++++++++ 10 files changed, 72 insertions(+), 29 deletions(-) diff --git a/examples/README.md b/examples/README.md index 3d945a3..7e20cbb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -45,15 +45,17 @@ python examples/custom_control_flow.py # uses the default "attach" launcher: # park resident sessions yourself first ``` -Model coverage varies: `arena_score.py` and `review_escalation.py` hire -`claude-haiku-4-5`, and `judge_panel_score.py`, `review_escalation.py`, and -`tournament.py` hire `claude-opus-4-8` — edit the `model=` arguments if your -account lacks a model. To resume an interrupted run, just re-run the same -command — each script fixes its run id (`"arena-demo"`, `"pipeline-demo"`, …) -and the journal replays completed steps. Scores that end in an `apply` pause -at a durable human gate: the question is delivered over `h5i msg`, so answer -it from the inbox (`h5i msg inbox`, then `h5i msg ack ` / `h5i msg reply - …`) before the winner touches your worktree. +Every example pins inexpensive models instead of inheriting a potentially +costly CLI default: Claude seats use `claude-haiku-4-5` and Codex seats use +`gpt-5.4-mini`. Edit the `model=` arguments if your account lacks either model. +Changing a model on an existing run does not rewrite a journaled hire, so also +change the run id (for example, `"ensemble-demo-v2"`) when experimenting with +another model. To resume an interrupted run without configuration changes, +just re-run the same command — each script fixes its run id (`"arena-demo"`, +`"pipeline-demo"`, …) and the journal replays completed steps. Scores that end +in an `apply` pause at a durable human gate: the question is delivered over +`h5i msg`, so answer it from the inbox (`h5i msg inbox`, then `h5i msg ack ` +/ `h5i msg reply …`) before the winner touches your worktree. ## One pattern each diff --git a/examples/arena_score.py b/examples/arena_score.py index 75ade8f..9586054 100644 --- a/examples/arena_score.py +++ b/examples/arena_score.py @@ -19,8 +19,8 @@ async def main(task: str) -> None: async with Conductor(".", "arena-demo", launcher="resident") as c: agents = await asyncio.gather( - c.hire("claude", runtime="claude"), - c.hire("codex", runtime="codex"), + c.hire("claude", runtime="claude", model="claude-haiku-4-5"), + c.hire("codex", runtime="codex", model="gpt-5.4-mini"), c.hire("haiku", runtime="claude", model="claude-haiku-4-5"), ) # LaunchResident starts each session on its first turn, so there is no diff --git a/examples/custom_control_flow.py b/examples/custom_control_flow.py index 015d63c..9502f6b 100644 --- a/examples/custom_control_flow.py +++ b/examples/custom_control_flow.py @@ -12,8 +12,15 @@ async def main() -> None: async with Conductor(".", "triage-and-fix") as c: - lead = await c.hire("lead", runtime="claude") - crew = [await c.hire(f"fixer{i}", runtime="claude") for i in range(2)] + lead = await c.hire( + "lead", runtime="claude", model="claude-haiku-4-5" + ) + crew = [ + await c.hire( + f"fixer{i}", runtime="claude", model="claude-haiku-4-5" + ) + for i in range(2) + ] # A data turn: the agent replies with JSON, not code. The reply is # journaled — on resume this list comes back without re-asking. diff --git a/examples/debate_then_build.py b/examples/debate_then_build.py index dbca893..2003da5 100644 --- a/examples/debate_then_build.py +++ b/examples/debate_then_build.py @@ -20,9 +20,11 @@ async def main() -> None: async with Conductor(".", "debate-demo", launcher="resident") as c: - pro = await c.hire("pro", runtime="claude") - con = await c.hire("con", runtime="codex") - moderator = await c.hire("moderator", runtime="claude") + pro = await c.hire("pro", runtime="claude", model="claude-haiku-4-5") + con = await c.hire("con", runtime="codex", model="gpt-5.4-mini") + moderator = await c.hire( + "moderator", runtime="claude", model="claude-haiku-4-5" + ) outcome = await patterns.debate( c, QUESTION, [pro, con], moderator=moderator, rounds=2 diff --git a/examples/ensemble_score.py b/examples/ensemble_score.py index 69d5fef..8ed67d7 100644 --- a/examples/ensemble_score.py +++ b/examples/ensemble_score.py @@ -17,8 +17,10 @@ async def main(task: str) -> None: async with Conductor(".", "ensemble-demo", launcher="resident") as c: - claude = await c.hire("claude", runtime="claude") - codex = await c.hire("codex", runtime="codex") + claude = await c.hire( + "claude", runtime="claude", model="claude-haiku-4-5" + ) + codex = await c.hire("codex", runtime="codex", model="gpt-5.4-mini") # Fail the predictable ways now, not at minute 30. # LaunchResident starts each session on its first turn, so there is no diff --git a/examples/judge_panel_score.py b/examples/judge_panel_score.py index 6b2eec3..efe44a0 100644 --- a/examples/judge_panel_score.py +++ b/examples/judge_panel_score.py @@ -26,12 +26,12 @@ async def main() -> None: async with Conductor(".", "panel-demo", launcher="resident") as c: workers = await asyncio.gather( - c.hire("claude", runtime="claude"), - c.hire("codex", runtime="codex"), + c.hire("claude", runtime="claude", model="claude-haiku-4-5"), + c.hire("codex", runtime="codex", model="gpt-5.4-mini"), ) judges = await asyncio.gather( - c.hire("judge-a", runtime="claude"), - c.hire("judge-b", runtime="claude", model="claude-opus-4-8"), + c.hire("judge-a", runtime="claude", model="claude-haiku-4-5"), + c.hire("judge-b", runtime="claude", model="claude-haiku-4-5"), ) # Independent attempts → seal → neutral evidence for the panel. diff --git a/examples/pipeline_score.py b/examples/pipeline_score.py index 87d71f3..b0b1843 100644 --- a/examples/pipeline_score.py +++ b/examples/pipeline_score.py @@ -18,9 +18,13 @@ async def main() -> None: async with Conductor(".", "pipeline-demo", launcher="resident") as c: - architect = await c.hire("architect", runtime="claude") - builder = await c.hire("builder", runtime="codex") - hardener = await c.hire("hardener", runtime="claude") + architect = await c.hire( + "architect", runtime="claude", model="claude-haiku-4-5" + ) + builder = await c.hire("builder", runtime="codex", model="gpt-5.4-mini") + hardener = await c.hire( + "hardener", runtime="claude", model="claude-haiku-4-5" + ) design, impl, hardened = await patterns.pipeline( c, diff --git a/examples/review_escalation.py b/examples/review_escalation.py index 9847d8a..0cc39a9 100644 --- a/examples/review_escalation.py +++ b/examples/review_escalation.py @@ -20,7 +20,9 @@ async def main(task: str) -> None: async with Conductor(".", "escalation-demo", launcher="resident") as c: junior = await c.hire("junior", runtime="claude", model="claude-haiku-4-5") - senior = await c.hire("senior", runtime="claude", model="claude-opus-4-8") + senior = await c.hire( + "senior", runtime="claude", model="claude-haiku-4-5" + ) artifact = await junior.work(task) await c.freeze() diff --git a/examples/tournament.py b/examples/tournament.py index 7b43451..fa6db42 100644 --- a/examples/tournament.py +++ b/examples/tournament.py @@ -18,10 +18,10 @@ #: (name, runtime, model) seeds, bracket order. SEEDS = [ - ("claude", "claude", None), - ("codex", "codex", None), + ("claude", "claude", "claude-haiku-4-5"), + ("codex", "codex", "gpt-5.4-mini"), ("haiku", "claude", "claude-haiku-4-5"), - ("opus", "claude", "claude-opus-4-8"), + ("haiku-2", "claude", "claude-haiku-4-5"), ] diff --git a/tests/test_examples.py b/tests/test_examples.py index 0674459..4e46125 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -5,6 +5,7 @@ EXAMPLES = Path(__file__).parents[1] / "examples" +CHEAP_MODELS = {"claude-haiku-4-5", "gpt-5.4-mini"} @pytest.mark.parametrize("path", sorted(EXAMPLES.glob("*.py")), ids=lambda p: p.name) @@ -28,3 +29,26 @@ def test_resident_examples_do_not_preflight_live_sessions(name: str): 'launcher="resident" starts sessions lazily on the first turn; ' "preflight(live=...) fails before that turn can start them" ) + + +@pytest.mark.parametrize("path", sorted(EXAMPLES.glob("*.py")), ids=lambda p: p.name) +def test_examples_pin_cheap_models(path: Path): + tree = ast.parse(path.read_text()) + hire_calls = [ + call + for call in ast.walk(tree) + if isinstance(call, ast.Call) + and isinstance(call.func, ast.Attribute) + and call.func.attr == "hire" + ] + + assert all(any(keyword.arg == "model" for keyword in call.keywords) for call in hire_calls) + + explicit_models = { + node.value + for node in ast.walk(tree) + if isinstance(node, ast.Constant) + and isinstance(node.value, str) + and (node.value.startswith("claude-") or node.value.startswith("gpt-")) + } + assert explicit_models <= CHEAP_MODELS