0074 — Main is red or green by one declaration, and a red ends on green evidence, not a clock

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

  1. One command declares main red or green: main-status red / main-status green. Run with no verb, main-status prints the current state, who declared it, when, and the evidence: the newest completed push run of propflowai's ci.yml on main, with its URL. It is a command, not a flag: there is one way to declare, and no second door.
  2. One record. ~/.claude/jobs/main-status/state.json holds the last declaration; history.jsonl beside it gets every declaration appended. It is plain JSON, so cat answers the question. Every consumer on the mini reads that file. Trinity reads its bridge form (decision 7).
  3. Whoever takes the red declares it. main-status red names the owner (by default, the caller) and the failing sha (by default, the newest failing push run). A second red from 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.
  4. "Main" is ci.yml on push to main, and green is checked against it. That is the suite whose red fires the push-main-red alert 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 green is refused unless the newest completed push run of ci.yml on main concluded success. 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.
  5. 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 types red. red names that run when it is declared (the newest failed run, or with --sha the newest failed run on that sha). A pre-emptive --sha is 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. main is 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-status flags that case so the owner can close it, and red warns at once when a CI run already answers the red being declared (an old sha, usually). green over a declared red is refused unless a run answers that red, so all three (status, green, Smith) use one rule.
  6. 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 next push-main-red alert reads green or a stale declaration and proceeds. A stale declaration reaches the PR body as red-main claim present but not usable: stale-declaration, never as no owner claim found.
  7. The bridge carries it to humans and Trinity (part 2), in the shape Trinity already reads. Declaring red posts the id: / driver: form of BRIDGE CLAIM v1 that Trinity's red-main-claim.sh already writes and dedups on. Declaring green posts the same id: with status: RESOLVED. No new wire format. See The Trinity contract.
  8. 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 use wake-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 from main-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:

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):

  1. Sessions told by msg --all (decision 8). Built as the general agent-broadcast command (agent-harness #1648, with Smith's ledger in #1652 and an engineering-bridge door for humans in #1660); main-status red / green calling it is next.
  2. 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 in config/agents.toml (the pattern of prompt_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, so msg --all refuses to broadcast from it today ("cannot identify this session"); telling the mini's sessions about a folded claim needs a small msg change, or an existing session-less path.
  3. 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 the id:/driver: CLAIM and its RESOLVED, which Trinity's script already reads.
  4. The Trinity ask for Fede: agreeing to step 2 on his seat, the RESOLVED-only-on-green rule, and whether agent-broadcast posts should reach Trinity at all (today they do not; it would take an allowed mention or the socket admitting AGENT BROADCAST v1).

Also open:

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:

The design to build next time:

Consequences

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.