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;