From 1cae2a57f4576157133d2555efd956909bf301e4 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Tue, 1 Sep 2026 16:51:47 +0000 Subject: [PATCH] docs(agents): a board state is a claim about the tree, and the tree wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLOUD-1253 builds the gate for the closing half: a closed retirement row judged against the tree, from the `conserves` arms that already exist. It cannot carry the READING half, and the reading half is where the loss happens. A gate runs at `verify` and at close; an agent consults a row's state continuously and acts on it at once, so between two runs of any gate a wrong state is load-bearing prose that an agent trusts. MEASURED TWICE IN ONE SESSION, both mine. `CLOUD-1162` sat In Review with `board-diff-overlap.sh` still tracked, because the merge moves a row the moment a PR attaches to it. My first correction was a warning paragraph inside the body with the state left alone — which is that row's own recorded finding one level up, where a correction block does not correct a title. A state is read by more automation than a title is. `CLOUD-1160` sat In Progress with nothing shipped and no PR. Its only attachment was #804, whose title reads "CLOUD-312 row 10 + CLOUD-1294: session-start.sh retires" — different work, already merged. I read "attachment present + In Progress" as another session's live work and declined to race it, reporting the largest single-program retirement available as taken. The contradicting evidence was in the payload I had already printed. THE THREE CLAUSES, and each names a failure that actually happened rather than one that is easy to imagine: the tree settles it; the row moves BACK to Backlog rather than Todo, since parking an unpullable row in the ready queue hands the next agent work that cannot be started; and the move owes a comment, never a note inside the body. NOT GATED, AND SAID SO. Non-negotiable rule 3 puts "did the agent consult the tree before believing a row" outside what a gate decides — it is a model verdict. So this is feedforward with CLOUD-1253's predicate as the gated half, which is `.claude/rules/scanning.md`'s own shape for its suitability axis. LINE-NEUTRAL, because `[budget.instructions]` was at 199/199. The section it joins is compressed to pay for it: 198 lines and 3405 tokens, three fewer than before the rule existed. No budget was raised to make room. Five cases in the new tier, and the fourth is not decoration on the third: annotating in place is the failure that happened, so a reader who takes "move it back" as satisfied by an explanatory paragraph has made the same mistake. The fifth is the anti-vacuity one — it pins the text inside the board section of the always-loaded file, since the other four would pass just as well over a rules file that loads at a trigger, and "I am about to trust a row's state" has none. Shown able to fail on the real file rather than a fixture: replacing one clause turned exactly its own case red with the other four green, and restoring it returned all five. Refs: CLOUD-1305 Refs: CLOUD-1253 --- AGENTS.md | 22 ++-- crates/batten/tests/it/board_state_claim.rs | 132 ++++++++++++++++++++ crates/batten/tests/it/main.rs | 1 + 3 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 crates/batten/tests/it/board_state_claim.rs diff --git a/AGENTS.md b/AGENTS.md index cc997d9a5..f2c16a70f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,17 +81,17 @@ predicate, not a list**: enumeration is why the previous version did not hold ## The board: move the issue as you move the work The board is the observability surface: **the state transition IS how others -know**, and there is no separate "tell people." Move the `CLOUD-*` issue in -lockstep: **Todo** = the ready queue ("Ready" is the issue's Ready block, not a -status); **In Progress** = pulled — claim it **by hand, before writing code** -(`mise run claim-check`) and assign yourself: the automation fires on the PR -event, the _end_ of the work, so waiting for it reserves nothing; **In Review** -= landed on `main`, written by the merge **iff the PR body closes the key** -(`closing-key-check`) — [trunk-based development](https://trunkbaseddevelopment.com/) reviews after merge, -flagged not withheld; **Done** = the DoR/DoD spec's Done holds — **released**, yours to -set, never the merge (`done-check`). Detail: `mem:workflow/board-states`. -**Branching is trunk-based**: `main` is the one long-lived, always-releasable -branch, and short-lived branches land by fast-forward, keeping it linear and tested. +know**. Move the `CLOUD-*` issue in lockstep: **Todo** = the ready queue (the +Ready block, not a status); **In Progress** = pulled — claim **by hand, before +writing code** (`mise run claim-check`) and assign yourself, since the +automation fires only at the PR event; **In Review** = landed on `main`, by the +merge **iff the body closes the key** (`closing-key-check`) — +[trunk-based](https://trunkbaseddevelopment.com/) reviews after merge, flagged not withheld, `main` the +one long-lived branch and short-lived ones landing by fast-forward; **Done** = +**released**, yours to set, never the merge (`done-check`). `mem:workflow/board-states`. +**A STATE IS A CLAIM ABOUT THE TREE, AND THE TREE WINS**: read code refuting one +— a retired path still tracked, no PR behind an In Progress, an attachment that +is another row's — move it BACK to Backlog with a comment, never a note inside it. ## Workflow contract: verify locally, then land diff --git a/crates/batten/tests/it/board_state_claim.rs b/crates/batten/tests/it/board_state_claim.rs new file mode 100644 index 000000000..f20d10b28 --- /dev/null +++ b/crates/batten/tests/it/board_state_claim.rs @@ -0,0 +1,132 @@ +//! The board's read discipline is present in the tree, not only in a habit +//! (CLOUD-1305). +//! +//! CLOUD-1253 builds the gate for the closing half: a closed retirement row is +//! judged against the tree, from the `conserves` arms that already exist. It +//! cannot carry the READING half, and the reading half is where the loss +//! happens — a gate runs at `verify` and at close, where an agent consults a +//! row's state continuously and acts on it at once. Between two runs of any +//! gate, a wrong state is load-bearing prose that an agent trusts. +//! +//! WHAT THIS FILE ASSERTS, AND WHAT IT CANNOT. It asserts **presence**: the +//! always-loaded file still carries the claim, still names the destination a +//! refuted row moves to, still demands a comment, and still refuses the +//! annotate-in-place shortcut. That catches deletion and drift in the prose. +//! +//! It does **not** catch an agent who does not check, and it cannot. +//! Non-negotiable rule 3 says a gate resolves to a command and an exit code over +//! an object it decides, never a model verdict — and "did the agent consult the +//! tree before believing a row" is exactly a model verdict. So that axis is +//! feedforward, the rule says so where it lives, and a §7 claiming otherwise +//! here would be the defect `.claude/rules/scanning.md` already records for its +//! own case. +//! +//! Same shape as `scanner_taxonomy.rs`: the prose carries the position, and the +//! test keeps the prose from evaporating. +//! +//! # Measured, and why the third clause is asserted separately +//! +//! Both instances are 2026-09-01. `CLOUD-1162` sat In Review with +//! `board-diff-overlap.sh` still tracked, and the first correction attempted was +//! **a warning paragraph inside the body with the state left alone** — which is +//! that row's own recorded finding one level up, where a correction block does +//! not correct a title. `CLOUD-1160` sat In Progress with nothing shipped and no +//! PR, its only attachment belonging to a different row, and was reported as +//! another session's live work. +//! +//! So `refuses_the_annotate_in_place_shortcut` is not decoration on the move +//! clause: annotating is the failure that actually happened, and a reader who +//! takes "move it back" as satisfied by an explanatory paragraph has made the +//! same mistake. + +// Panicking on setup failure is the idiomatic way for a test to fail loudly. +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use crate::common; + +use std::fs; + +use common::at_root; + +/// The always-loaded file the rule has to live in. +/// +/// `.claude/rules/*` load at a trigger, and "I am about to trust a row's state" +/// has no file to trigger on — which is why this binds every turn instead. +/// `CLAUDE.md` is a symlink to this; the tracked path is the one asserted. +const INDEX: &str = "AGENTS.md"; + +fn index() -> String { + fs::read_to_string(at_root(INDEX)).unwrap() +} + +#[test] +fn the_tree_outranks_the_state() { + // The claim itself. Asserted on the two load-bearing halves rather than the + // whole sentence, so rewording survives and deletion does not. + let text = index(); + assert!( + text.contains("A STATE IS A CLAIM ABOUT THE TREE"), + "{INDEX} no longer says a board state is a claim about the tree" + ); + assert!( + text.contains("THE TREE WINS"), + "{INDEX} states the claim without saying which side settles it" + ); +} + +#[test] +fn a_refuted_row_moves_back_and_the_destination_is_named() { + // BACKLOG, not Todo, and the distinction is the point: Todo is the ready + // queue, so parking an unpullable row there hands the next agent work that + // cannot be started. Naming the destination is what stops "move it back" + // resolving to whichever column is nearest. + let text = index(); + assert!( + text.contains("BACK to Backlog"), + "{INDEX} no longer names where a row refuted by the tree goes" + ); +} + +#[test] +fn the_move_owes_a_comment() { + // The state change says a row is wrong; only the comment says what was read. + // Without it the next reader re-derives the check, which is the cost this + // whole class keeps charging. + let text = index(); + assert!( + text.contains("with a comment"), + "{INDEX} moves the row without recording what the tree said" + ); +} + +#[test] +fn refuses_the_annotate_in_place_shortcut() { + // The clause that names the failure that actually happened, rather than the + // one it is easy to imagine. See the header: the first attempt at correcting + // CLOUD-1162 was a paragraph, not a state change. + let text = index(); + assert!( + text.contains("never a note inside it"), + "{INDEX} no longer refuses correcting a state with prose in the body" + ); +} + +#[test] +fn the_rule_lives_where_it_binds_every_turn() { + // ANTI-VACUITY, and it is the one that stops the four above passing over a + // file nobody loads: the assertions read `AGENTS.md`, so they would all hold + // just as well if the section were moved into a rules file that loads at a + // trigger — where "I am about to trust a row's state" has none. This pins + // that the text sits inside the board section of the always-loaded file, not + // merely somewhere in it. + let text = index(); + let board = text + .split("## The board: move the issue as you move the work") + .nth(1) + .expect("AGENTS.md still has a board section"); + let board = board.split("\n## ").next().unwrap(); + assert!( + board.contains("A STATE IS A CLAIM ABOUT THE TREE"), + "the rule left the board section of {INDEX}" + ); +} diff --git a/crates/batten/tests/it/main.rs b/crates/batten/tests/it/main.rs index 9d1541f62..cb0f5193f 100644 --- a/crates/batten/tests/it/main.rs +++ b/crates/batten/tests/it/main.rs @@ -51,6 +51,7 @@ mod baseline; mod bats_invocation; mod board_receipts; mod board_record; +mod board_state_claim; mod bundle; mod bypass_scrub; mod call_arguments;