Skip to content

Commit 152829c

Browse files
solomonneascodex
andcommitted
feat(run): receipt route decisions
Co-Authored-By: Codex <codex@openai.com>
1 parent e5ebf09 commit 152829c

4 files changed

Lines changed: 344 additions & 0 deletions

File tree

src/brigade/cli/run.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,11 @@ def dispatch(args) -> int:
523523
seat=lifecycle_seat,
524524
)
525525
raise
526+
finally:
527+
if output_dir is not None and (output_dir / "run.json").is_file():
528+
from ..route_receipts import write_route_decision
529+
530+
write_route_decision(output_dir, loaded_roster)
526531
if args.worktree and output_dir is not None:
527532
# Until the patch is proven good, the worktree is the only
528533
# recoverable copy of the agents' edits; a collection failure

src/brigade/route_catalog.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@ def _real_auth_hit(text: str) -> bool:
397397
return bool(re.search(_SIGNAL_PATTERNS[1][1], stripped))
398398

399399

400+
ROUTE_TEMPLATE_VERSION = "brigade.route-template.v1"
401+
ROUTE_DECISION_CONFIDENCE = "deterministic"
402+
403+
400404
@dataclass(frozen=True)
401405
class RouteBrief:
402406
"""Deterministic route computed before planning; attached to the plan prompt."""
@@ -412,6 +416,8 @@ class RouteBrief:
412416
size: str = "empty"
413417
triggered_by: dict = field(default_factory=dict)
414418
dependencies: dict[str, tuple[str, ...]] = field(default_factory=dict)
419+
confidence: str = ROUTE_DECISION_CONFIDENCE
420+
template_version: str = ROUTE_TEMPLATE_VERSION
415421

416422
def payload(self) -> dict:
417423
"""Telemetry shape for run.json. Signals, approvals, and overrides
@@ -428,6 +434,8 @@ def payload(self) -> dict:
428434
"size": self.size,
429435
"triggered_by": dict(self.triggered_by),
430436
"dependencies": {k: list(v) for k, v in self.dependencies.items()},
437+
"confidence": self.confidence,
438+
"template_version": self.template_version,
431439
}
432440

433441

src/brigade/route_receipts.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Typed route-decision artifact serialization for brigade run output."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from pathlib import Path
7+
from typing import Any, TypedDict
8+
9+
from . import localio
10+
from . import roster as roster_mod
11+
from .roster import Roster
12+
13+
ROUTE_DECISION_SCHEMA_VERSION = "brigade.route-decision.v1"
14+
15+
16+
class RouteDecisionArtifact(TypedDict):
17+
schema_version: str
18+
chosen_route: list[str] | None
19+
confidence: str | None
20+
template_version: str | None
21+
admissible_seats: list[str]
22+
23+
24+
def admissible_seats(roster: Roster) -> list[str]:
25+
"""Sorted non-orchestrator worker seats from a validated roster."""
26+
return sorted(agent.name for agent in roster_mod.workers(roster))
27+
28+
29+
def route_decision_payload(
30+
run_receipt: dict[str, Any],
31+
roster: Roster,
32+
) -> RouteDecisionArtifact:
33+
route = run_receipt.get("route")
34+
if isinstance(route, dict) and route.get("attached"):
35+
raw_route = route.get("route")
36+
if isinstance(raw_route, list):
37+
chosen_route = [str(stage) for stage in raw_route]
38+
raw_confidence = route.get("confidence")
39+
confidence = str(raw_confidence) if raw_confidence is not None else None
40+
raw_template_version = route.get("template_version")
41+
template_version = str(raw_template_version) if raw_template_version is not None else None
42+
else:
43+
chosen_route = None
44+
confidence = None
45+
template_version = None
46+
else:
47+
chosen_route = None
48+
confidence = None
49+
template_version = None
50+
return {
51+
"schema_version": ROUTE_DECISION_SCHEMA_VERSION,
52+
"chosen_route": chosen_route,
53+
"confidence": confidence,
54+
"template_version": template_version,
55+
"admissible_seats": admissible_seats(roster),
56+
}
57+
58+
59+
def write_route_decision(
60+
output_dir: Path,
61+
roster: Roster,
62+
) -> Path:
63+
run_receipt = localio.read_json_dict(output_dir / "run.json")
64+
if run_receipt is None:
65+
raise ValueError(f"missing or invalid run receipt: {output_dir / 'run.json'}")
66+
payload = route_decision_payload(run_receipt, roster)
67+
ordered: dict[str, object] = {}
68+
for key, value in (
69+
("schema_version", payload["schema_version"]),
70+
("chosen_route", payload["chosen_route"]),
71+
("confidence", payload["confidence"]),
72+
("template_version", payload["template_version"]),
73+
("admissible_seats", payload["admissible_seats"]),
74+
):
75+
ordered[key] = value
76+
path = output_dir / "route-decision.json"
77+
localio.write_text_atomic(path, json.dumps(ordered, indent=2) + "\n")
78+
return path

tests/test_route_receipts.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import json
2+
from pathlib import Path
3+
4+
from brigade import aboyeur
5+
from brigade import agents
6+
from brigade import cli
7+
from brigade import roster as roster_mod
8+
from brigade.route_catalog import ROUTE_DECISION_CONFIDENCE, ROUTE_TEMPLATE_VERSION, route_brief
9+
from brigade.route_receipts import (
10+
ROUTE_DECISION_SCHEMA_VERSION,
11+
admissible_seats,
12+
route_decision_payload,
13+
write_route_decision,
14+
)
15+
16+
17+
def _roster() -> roster_mod.Roster:
18+
return roster_mod.Roster(
19+
orchestrator="chef",
20+
agents={
21+
"chef": roster_mod.Agent("chef", "codex", "plan and synthesize"),
22+
"coder": roster_mod.Agent("coder", "codex", "write code"),
23+
"reviewer": roster_mod.Agent("reviewer", "codex", "review code"),
24+
"researcher": roster_mod.Agent(
25+
"researcher",
26+
None,
27+
"research",
28+
endpoint="http://example.invalid/v1",
29+
model="research-model",
30+
),
31+
},
32+
max_workers=2,
33+
allow_models=("codex", "ollama:*"),
34+
)
35+
36+
37+
def _write_run_json(path: Path, payload: dict) -> None:
38+
path.write_text(json.dumps(payload, indent=2) + "\n")
39+
40+
41+
def test_route_brief_payload_includes_decision_metadata():
42+
payload = route_brief("implement the config loader helper", template="vertical-slice").payload()
43+
assert payload["confidence"] == ROUTE_DECISION_CONFIDENCE
44+
assert payload["template_version"] == ROUTE_TEMPLATE_VERSION
45+
46+
47+
def test_route_decision_payload_reads_route_from_run_json():
48+
run_receipt = {
49+
"route": {
50+
"attached": True,
51+
"route": ["implement", "correctness-review", "verify"],
52+
"size": "S",
53+
"confidence": ROUTE_DECISION_CONFIDENCE,
54+
"template_version": ROUTE_TEMPLATE_VERSION,
55+
}
56+
}
57+
payload = route_decision_payload(run_receipt, _roster())
58+
assert payload == {
59+
"schema_version": ROUTE_DECISION_SCHEMA_VERSION,
60+
"chosen_route": ["implement", "correctness-review", "verify"],
61+
"confidence": ROUTE_DECISION_CONFIDENCE,
62+
"template_version": ROUTE_TEMPLATE_VERSION,
63+
"admissible_seats": ["coder", "researcher", "reviewer"],
64+
}
65+
66+
67+
def test_route_decision_payload_no_route_nulls_route_fields():
68+
payload = route_decision_payload({"status": "ok"}, _roster())
69+
assert payload["chosen_route"] is None
70+
assert payload["confidence"] is None
71+
assert payload["template_version"] is None
72+
assert payload["admissible_seats"] == ["coder", "researcher", "reviewer"]
73+
74+
75+
def test_route_decision_payload_malformed_route_nulls_route_fields():
76+
payload = route_decision_payload(
77+
{
78+
"route": {
79+
"attached": True,
80+
"route": "implement",
81+
"confidence": ROUTE_DECISION_CONFIDENCE,
82+
"template_version": ROUTE_TEMPLATE_VERSION,
83+
}
84+
},
85+
_roster(),
86+
)
87+
assert payload["chosen_route"] is None
88+
assert payload["confidence"] is None
89+
assert payload["template_version"] is None
90+
91+
92+
def test_admissible_seats_includes_endpoint_backed_workers():
93+
seats = admissible_seats(_roster())
94+
assert seats == ["coder", "researcher", "reviewer"]
95+
96+
97+
def test_write_route_decision_preserves_field_order(tmp_path):
98+
output_dir = tmp_path / "run"
99+
output_dir.mkdir()
100+
_write_run_json(
101+
output_dir / "run.json",
102+
{
103+
"route": {
104+
"attached": True,
105+
"route": ["implement"],
106+
"size": "XS",
107+
}
108+
},
109+
)
110+
write_route_decision(output_dir, _roster())
111+
text = (output_dir / "route-decision.json").read_text()
112+
keys = ["schema_version", "chosen_route", "confidence", "template_version", "admissible_seats"]
113+
positions = [text.index(f'"{key}"') for key in keys]
114+
assert positions == sorted(positions)
115+
116+
117+
def _write_roster(tmp_path: Path) -> None:
118+
(tmp_path / ".brigade").mkdir(parents=True)
119+
(tmp_path / ".brigade" / "roster.toml").write_text(
120+
"""
121+
orchestrator = "chef"
122+
123+
[agents.chef]
124+
cli = "codex"
125+
role = "plan"
126+
127+
[agents.coder]
128+
cli = "codex"
129+
role = "code"
130+
131+
[agents.reviewer]
132+
cli = "codex"
133+
role = "review"
134+
"""
135+
)
136+
137+
138+
def test_run_cli_writes_route_decision_for_routed_run(tmp_path, monkeypatch):
139+
_write_roster(tmp_path)
140+
output_dir = tmp_path / "out"
141+
monkeypatch.setattr(
142+
aboyeur.agents,
143+
"run_agent",
144+
lambda *args, **kwargs: agents.AgentResult(
145+
text=json.dumps({"assignments": [{"worker": "coder", "task": "implement"}]}),
146+
ok=True,
147+
),
148+
)
149+
150+
rc = cli.main(
151+
[
152+
"run",
153+
"implement the config loader helper",
154+
"--cwd",
155+
str(tmp_path),
156+
"--output-dir",
157+
str(output_dir),
158+
"--dry-run",
159+
"--no-code-graph",
160+
"--no-evidence",
161+
"--route-template",
162+
"vertical-slice",
163+
]
164+
)
165+
166+
assert rc == 0
167+
run_meta = json.loads((output_dir / "run.json").read_text())
168+
decision = json.loads((output_dir / "route-decision.json").read_text())
169+
assert decision["schema_version"] == ROUTE_DECISION_SCHEMA_VERSION
170+
assert decision["chosen_route"] == run_meta["route"]["route"]
171+
assert decision["confidence"] == ROUTE_DECISION_CONFIDENCE
172+
assert decision["template_version"] == ROUTE_TEMPLATE_VERSION
173+
assert run_meta["route"]["confidence"] == ROUTE_DECISION_CONFIDENCE
174+
assert run_meta["route"]["template_version"] == ROUTE_TEMPLATE_VERSION
175+
assert decision["admissible_seats"] == ["coder", "reviewer"] # roster.toml has no endpoint seat
176+
177+
178+
def test_run_cli_writes_route_decision_for_no_route_run(tmp_path, monkeypatch):
179+
_write_roster(tmp_path)
180+
output_dir = tmp_path / "out"
181+
monkeypatch.setattr(
182+
aboyeur.agents,
183+
"run_agent",
184+
lambda *args, **kwargs: agents.AgentResult(
185+
text=json.dumps({"assignments": [{"worker": "coder", "task": "inspect"}]}),
186+
ok=True,
187+
),
188+
)
189+
190+
rc = cli.main(
191+
[
192+
"run",
193+
"inspect the tree",
194+
"--cwd",
195+
str(tmp_path),
196+
"--output-dir",
197+
str(output_dir),
198+
"--dry-run",
199+
"--no-code-graph",
200+
"--no-evidence",
201+
"--no-route",
202+
"--route-template",
203+
"vertical-slice",
204+
]
205+
)
206+
207+
assert rc == 0
208+
assert "route" not in json.loads((output_dir / "run.json").read_text())
209+
decision = json.loads((output_dir / "route-decision.json").read_text())
210+
assert decision["schema_version"] == ROUTE_DECISION_SCHEMA_VERSION
211+
assert decision["chosen_route"] is None
212+
assert decision["confidence"] is None
213+
assert decision["template_version"] is None
214+
assert decision["admissible_seats"] == ["coder", "reviewer"] # roster.toml has no endpoint seat
215+
216+
217+
def test_run_cli_writes_route_decision_when_run_fails_after_routing(tmp_path, monkeypatch):
218+
_write_roster(tmp_path)
219+
output_dir = tmp_path / "out"
220+
221+
def fail_after_routing(*args, **kwargs):
222+
run_path = kwargs["output_dir"] / "run.json"
223+
run_meta = json.loads(run_path.read_text())
224+
run_meta["route"] = {
225+
"attached": True,
226+
"route": ["implement", "verify"],
227+
"confidence": ROUTE_DECISION_CONFIDENCE,
228+
"template_version": ROUTE_TEMPLATE_VERSION,
229+
}
230+
_write_run_json(run_path, run_meta)
231+
raise RuntimeError("worker failed")
232+
233+
monkeypatch.setattr(aboyeur, "run", fail_after_routing)
234+
235+
rc = cli.main(
236+
[
237+
"run",
238+
"implement the config loader helper",
239+
"--cwd",
240+
str(tmp_path),
241+
"--output-dir",
242+
str(output_dir),
243+
"--dry-run",
244+
"--no-code-graph",
245+
"--no-evidence",
246+
]
247+
)
248+
249+
assert rc == 2
250+
decision = json.loads((output_dir / "route-decision.json").read_text())
251+
assert decision["chosen_route"] == ["implement", "verify"]
252+
assert decision["confidence"] == ROUTE_DECISION_CONFIDENCE
253+
assert decision["template_version"] == ROUTE_TEMPLATE_VERSION

0 commit comments

Comments
 (0)