0074 — Main is red or green by one declaration, and a red ends on green evidence, not a clock
- Status: Accepted 2026-10-27. Recorded at decision time (ADR-0004). Part 1 is agent-harness #1581; part 2 is listed under What is left.
- Date: 2026-10-27
- Deciders: Gera, 2026-10-27, ruling on agent-harness #1465 (a 120-minute crash budget for a red-main bridge claim with no
until): "We actually need a better fix, because then we are for sure waiting 120 and I don't like that. We need whoever fixes it to have to broadcast something, maybe on the engineering bridge, saying main is green, continue work, and that somehow messages all the sessions on Smith and on Trinity … maybe we could make another command that basically says what's going on, like oh it's green or oh it's red." - Amends: ADR-0015 rule 2 (where the red-main record lives) and the second half of rule 4 ("when the claim releases (main green), the hold ends": it now ends on decision 5). Rule 4's first half (no merge but the fix while red) is unchanged and still a convention. Rules 1, 3 and 5 are unchanged.
- Systems:
main-status(new, SYSTEMS.md);red-main(superseded, unchanged); Agent Smith's channel-alert lane (agent-harnessred_main_owner.py).
The gap
ADR-0015 says one lane owns a red main and every other lane holds. Its record was the Supervisor's ~/.claude/jobs/red-main/claim.json. The Supervisor was deleted on 2026-10-18 (ADR-0067), and nothing has written that file since (agent-harness #1512). The only live record left was the sibling seat's BRIDGE CLAIM v1 on the claims board. A claim there with no readable until held Smith down for about a week (red-main-d020e2d1be, 8 stand-downs in 7 days, one of them a deploy failure it had nothing to do with). #1465 proposed ending such a claim after 120 minutes. Gera refused a timer: with a timer, everyone waits the full 120 minutes even when the fix landed in ten.
Nothing told the other sessions when main was green again either. A hold ended in silence.
The decision
- One command declares main red or green:
main-status red/main-status green. Run with no verb,main-statusprints the current state, who declared it, when, and the evidence: the newest completed push run of propflowai'sci.ymlonmain, with its URL. It is a command, not a flag: there is one way to declare, and no second door. - One record.
~/.claude/jobs/main-status/state.jsonholds the last declaration;history.jsonlbeside it gets every declaration appended. It is plain JSON, socatanswers the question. Every consumer on the mini reads that file. Trinity reads its bridge form (decision 7). - Whoever takes the red declares it.
main-status rednames the owner (by default, the caller) and the failing sha (by default, the newest failing push run). A secondredfrom a different owner is refused and names the holder.--takeover "<reason>"is the explicit way to take it over, and the reason goes into the history. A red is never refused on CI grounds: a known-bad merge can be declared before its run fails. - "Main" is
ci.ymlon push tomain, and green is checked against it. That is the suite whose red fires thepush-main-redalert Smith stands down on. The heavy lane (post-merge-heavy.yml, ADR-0015's 2026-10-01 and 2026-10-07 amendments) is not part of the evidence in part 1, so a heavy-lane red is out of this command's scope until part 2 adds it. Green is checked against main's real CI.main-status greenis refused unless the newest completed push run ofci.ymlonmainconcludedsuccess. A run still in progress counts for nothing. A CI read that fails also refuses, because "cannot verify" is not green. There is no force flag. - A declared red ends on evidence, never on a clock. It owns the red until someone declares green, or until main's push CI answers it, whichever comes first. "Answers it" is the rule that already closes a red-main episode (
red_main_green.green_after), reused rather than restated: a successful run created after the declared red run, or that red run itself re-run to green. The cutoff is the red run's own creation time, not the declaration's, because the next push's run is usually already going when someone typesred.rednames that run when it is declared (the newest failed run, or with--shathe newest failed run on that sha). A pre-emptive--shais matched to its run once the run is listed. Only when no run on the sha is listed does the cutoff fall back to the declaration's time.mainis linear and a run is created at push time, so a run created after the red run tests the red commit or a newer one. That is why no separate ancestry check is made on the green run. The status command and Smith read through the same function, so they cannot disagree about whether a declaration still holds. The second rule handles the forgotten declaration without the timer Gera refused: a red after a green run is a new breakage, and the old owner does not hold it.main-statusflags that case so the owner can close it, andredwarns at once when a CI run already answers the red being declared (an old sha, usually).greenover a declared red is refused unless a run answers that red, so all three (status,green, Smith) use one rule. - Smith stands down on the declaration and resumes by reading it again. Smith's alert lane reads the declaration first, before the dead claim file and the bridge board (
red_main_owner.read_declared_owner). A declared red that covers this failure stands Smith down, with one line in the alert thread naming the owner. If CI cannot be read, Smith holds and says so. Smith keeps no vigil and builds no watcher. "Resume" means the nextpush-main-redalert reads green or a stale declaration and proceeds. A stale declaration reaches the PR body asred-main claim present but not usable: stale-declaration, never asno owner claim found. - The bridge carries it to humans and Trinity (part 2), in the shape Trinity already reads. Declaring red posts the
id:/driver:form ofBRIDGE CLAIM v1that Trinity'sred-main-claim.shalready writes and dedups on. Declaring green posts the sameid:withstatus: RESOLVED. No new wire format. See The Trinity contract. - Sessions are told by message, not by keystrokes (part 2). A declaration sends one line to every live session on the mini through
msg --all(the sanctioned channel, ADR-0002). It does not usewake-session, which is reserved for slash commands. The message is a courtesy. The record is the signal (ADR-0003): a session that missed it still reads the right answer frommain-status.
The Trinity contract
Trinity is Fede's system, and nothing in propflowai's or Trinity's code changes for part 1. Part 2 needs exactly this, and most of it already exists:
- Trinity declares red. Trinity posts
BRIDGE CLAIM v1top-level in #engineering-bridge (C0C10AS4U15) withid: red-main-<sha>anddriver: <who>. It does this today. Smith's socket already mirrors it onto the claims board (bridge_protocol, theid:/driver:shape), and part 2 folds that mirror intostate.jsonas a red declared by the Trinity seat. - Trinity declares green. Trinity edits or re-posts the same
id:withstatus: RESOLVED …. That already releases the board row, and part 2 folds it intostate.jsonas green. New ask: Trinity posts RESOLVED only when main's push CI is green, the same check as decision 4. - Trinity reads Smith's side. Smith's
main-status red/greenpost the same two shapes under Smith's bot. Trinity'sred-main-claim.shdedups by searching forid: red-main-…, so a Smith-side declaration reads as a held red to it. New ask: Trinity treats astatus: RESOLVEDfrom either seat as "main is green, continue work". - No timer on either side. A claim with no
untilholds until RESOLVED, or until CI is green after it (decision 5). The 30-minute silence takeover in the script'sask:text stays Trinity's own rule. This ADR does not change it.
What is left (part 2)
Order, and why (2026-10-27, from the first review of the bridge post). The other seat runs this same harness. Its socket holds an id:/driver: claim for RED_MAIN_ID_HOLD_MINUTES (30) after the claim was last posted or edited (slack_socket._claim_from_other_shapes). A red posted once from here would read as free over there half an hour later. That is a clock, which decision 5 rules out. So the reader lands before the writer (ADR-0004's seam, and the harness constitution's §6):
- Sessions told by
msg --all(decision 8). Built as the generalagent-broadcastcommand (agent-harness #1648, with Smith's ledger in #1652 and an engineering-bridge door for humans in #1660);main-status red/greencalling it is next. - Each seat folds the other seat's red-main claims and RESOLVED posts into its own
state.json. There, a red ends on evidence, not on the 30-minute hold. This is a per-seat setting inconfig/agents.toml(the pattern ofprompt_snapshot_off, agent-harness #1585). It is on for Smith. Trinity stays on its 30-minute rule until Fede agrees. The worker is not a session, somsg --allrefuses to broadcast from it today ("cannot identify this session"); telling the mini's sessions about a folded claim needs a smallmsgchange, or an existing session-less path. - Bridge posting on
red/green(decision 7), behind a stub in tests. It must name humans without paging them (bridge_protocol.name_human_mentions/mention_free, #937) and respect the seat's sibling switch.agent-broadcast's bridge post (AGENT BROADCAST v1, #1660) is for humans and does not do this job: Trinity's harness drops it. The red/green post is theid:/driver:CLAIM and its RESOLVED, which Trinity's script already reads. - The Trinity ask for Fede: agreeing to step 2 on his seat, the RESOLVED-only-on-green rule, and whether
agent-broadcastposts should reach Trinity at all (today they do not; it would take an allowed mention or the socket admittingAGENT BROADCAST v1).
Also open:
- Smith's alert lane declaring
reditself when it takes a red, andgreenwhenred_main_greencloses the episode. - A Slack door:
main statusin plain text, through the brain, the waygera-tabsworks. - Retire the dead
claim.jsonreader (closes agent-harness #1512) and point the relay'sGET /api/red-mainatstate.json. - ADR-0015 rule 4 (no merge but the fix while red) is still a convention. A
pr-mergegate that readsstate.jsonis a separate decision, because it gates merges.
Open: the heavy lane as evidence (not Gera's ruling; parked)
The coordinator ruled on 2026-10-27 that the heavy lane (post-merge-heavy.yml) counts as evidence for red, and that its green is required for green. This was the coordinator's ruling, not Gera's. agent-harness #1606 built it and is parked as a draft. Until it is revisited, "main" means ci.yml push runs only (decision 4).
Why it was parked. One "cutoff plus one lane" per declaration kept breaking when the two lanes interacted. Two review rounds found four cases:
- a red declared in both lanes read like a
ci.yml-only record; - an oldest-red cutoff could make a two-lane red hold nobody;
red --shanever recorded a red in both lanes;- a one-lane false alarm could end a pre-emptive red.
The design to build next time:
- A cutoff per red lane, recorded when
redis declared: the newest failed run of each lane that is red at that moment. - A red ends when every red lane has passed after its own cutoff, or that lane's red run is re-run to green.
- A red whose lanes are unknown (CI unreadable, or a pre-emptive
--shawith nothing listed) needs every lane to pass after the declaration. A pre-emptive--shagains its lanes as their failed runs on that sha appear. greenneeds every lane's newest completed run to have succeeded, and every red lane answered.- Smith stands down on
push-main-red, aci.ymlalert, so it asks one question only: hasci.ymlpassed afterci.yml's own cutoff, or after the declaration ifci.ymlwas not red? That keeps part 1's behaviour exactly. - Already measured, in #1606's body: the heavy lane triggers on
schedule(cron7 */2 * * *) andworkflow_dispatchonly.event=pushreturns no runs, and the oldworkflow_runtrigger last ran on 2026-10-17.
Consequences
- agent-harness #1465 (the 120-minute budget) is superseded by decision 5. Park it rather than merge it.
- A hold ends when the fix's CI is green, not 120 minutes later.
- A forgotten declaration is visible (
main-statusflags it) and harmless (decision 5), so nobody needs to babysit it.
Signals
main-status has no daemon. Its record is written only when someone declares. SYSTEMS.md watches history.jsonl with a 7-day file-age. That window is a judgement, not a measurement: the 8 stand-downs above came from one claim, so they are not a count of reds. A week with no declaration most likely means the fleet stopped using the command while main went red. It does not mean the command is broken. Check how the last reds were handled before acting on it. An absent state.json reads as nothing declared, never as green.