diff --git a/.serena/memories/workflow/board-states.md b/.serena/memories/workflow/board-states.md index cf5c4dade..f7b65a916 100644 --- a/.serena/memories/workflow/board-states.md +++ b/.serena/memories/workflow/board-states.md @@ -92,6 +92,39 @@ evidence. A row whose acceptance does not fully resolve **stays In Review with the shortfall recorded on it.** Promoting it anyway is the exact defect above, reproduced by hand. +### When a row's prose prescribes a repair, its own Acceptance overrules the prose + +An acceptance-read is usually run against the tree. Run it against the row's **own +prescriptive prose** too, because the two can disagree and the prose is the half +that gets followed. + +Measured 2026-08-22 on CLOUD-858. Its diagnosis section prescribed, for CLOUD-134, +_"converging the dialect is what would make that visible"_ — convert a retired Ready +dialect to the canonical one so `ready-lint` can read it. Executed literally that +produces a block which PASSES the gate. But CLOUD-134's own Decision section says +_"Gate on a named first consumer being concrete"_, and no consumer is named — so the +passing block would have certified an unmet precondition. CLOUD-858's own last +acceptance bullet forbids exactly that: _"a row that genuinely has open questions +leaves the ready queue rather than acquiring a Ready block that papers over them."_ + +The prescription and the acceptance were in the same body, four paragraphs apart, +and only one of them was wrong. The repair that satisfies both is to converge the +dialect **and** leave the queue — a Ready block that declares the open question, +in Backlog. + +Why this recurs rather than being one author's slip: a diagnosis is written while +reading the defect, and an Acceptance while imagining the finished state. Nothing +compares them, and `ready-lint` cannot — it decides a block's clauses against each +other, never a body's prose against its own acceptance. So the check is manual and +cheap: **before executing a prescription a row hands you, read that row's Acceptance +and ask whether the prescribed result satisfies it.** Where they conflict, the +Acceptance is the specification and the prescription is a note. + +The tell that you are in this case: the prescription names a mechanical edit +("converge", "rename", "add the label") and the Acceptance names a property +("does not paper over", "no scope moves"). A mechanical edit can always be +performed; whether it produces the property is the question the edit does not ask. + ## Two things that trip agents up 1. **"Ready" is not a status.** It is the **Ready block** — text inside the issue diff --git a/install.sh b/install.sh index 131bd0270..1fe34d63a 100755 --- a/install.sh +++ b/install.sh @@ -22,11 +22,11 @@ # and a signature format (CLOUD-264); this is the floor beneath them, and it is # a gate rather than a sensor. # -# WHILE THE REPOSITORY IS PRIVATE (CLOUD-205) a GitHub token is required, read -# from BATTEN_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN. Nothing here requires one -# by construction — the same script installs unauthenticated the day the repo -# goes public, which is the "so the flip is cheap" property that decision asks -# the release machinery to keep. +# NO TOKEN IS REQUIRED BY CONSTRUCTION (CLOUD-205), which is the "so the flip is +# cheap" property that decision asks the release machinery to keep. One is still +# read from BATTEN_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN when present, because +# the unauthenticated release API is rate-limited per source address and a busy +# shared runner exhausts it. # # Output is pointer-only (non-negotiable rule 4): asset names, a target, a # destination path. Never a token, never file contents. Exit 0 installed / 1 @@ -76,7 +76,7 @@ usage() { BATTEN_TARGET override target detection BATTEN_INSTALL_DIR destination; default \${XDG_BIN_HOME:-\$HOME/.local/bin} BATTEN_GITHUB_TOKEN token for the release API (also GH_TOKEN, - GITHUB_TOKEN). Required while the repo is private. + GITHUB_TOKEN). Optional; raises the rate limit. EOF } @@ -236,7 +236,7 @@ main() { rel_url="$API/repos/$REPO/releases/latest" fi api_get "$rel_url" "application/vnd.github+json" "$tmp/release.json" || - die 2 "cannot read the release list from $REPO. While this repository is private a token is required — set BATTEN_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN." + die 2 "cannot read the release list from $REPO. If you are being rate-limited, set BATTEN_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN." flatten "$tmp/release.json" >"$tmp/release.line" tag=$(json_string "$tmp/release.line" tag_name) diff --git a/mise-tasks/land.sh b/mise-tasks/land.sh index 6a5f0f10a..77b65a505 100755 --- a/mise-tasks/land.sh +++ b/mise-tasks/land.sh @@ -1473,7 +1473,7 @@ $verify_tail" # is not evidence of anything. note_phase "deferral-check(lap $lap)" body=$(gh pr view "$pr" --json body --jq .body 2>/dev/null || true) - if [[ -n "$body" ]] && ! printf '%s' "$body" | mise run deferral-check; then + if [[ -n "$body" ]] && ! mise run deferral-check <<<"$body"; then die "#$pr defers a decision with no ticket. File it and name the issue in that paragraph, then run land again." fi @@ -1502,7 +1502,7 @@ $verify_tail" # fetch failed) simply yields no exemption, which is the pre-CLOUD-774 # behaviour and refuses rather than waves through. note_phase "filed-here-check(lap $lap)" - if ! printf '%s' "$body" | mise run filed-here-check; then + if ! mise run filed-here-check <<<"$body"; then die "#$pr filed a row that was never groomed to Ready, or names code this branch has open without closing it. Fix it here and close the row, comment on the issue that already owns it, or groom the row and run land again." fi @@ -1516,7 +1516,7 @@ $verify_tail" # Same body, fetched once above, and the same fail-open reasoning: a body this # never saw is not evidence that the PR closes nothing. note_phase "closing-key-check(lap $lap)" - if [[ -n "$body" ]] && ! printf '%s' "$body" | mise run closing-key-check; then + if [[ -n "$body" ]] && ! mise run closing-key-check <<<"$body"; then die "#$pr names its issue but never closes it, or closes some of the keys its commits served and strands the rest (CLOUD-674) — the gate's own output above names which. Merging either way leaves the board a column behind for at least one row. Write \"Closes \" for every row this PR completes (or DO-NOT-CLOSE if it is not meant to complete them), then run land again." fi diff --git a/tests/land.bats b/tests/land.bats index 506cedac5..eb75070a6 100644 --- a/tests/land.bats +++ b/tests/land.bats @@ -350,6 +350,12 @@ echo "\$*" >>"$BATS_TEST_TMPDIR/misecalls" # wired to read stdin that nobody pipes to is inert, and it shipped inert once. # NO BACKTICKS IN THIS HEREDOC: it is unquoted, so a backticked word is command # substitution and the comment would try to RUN the task it names. +# CLOUD-995: a real gate can exit BEFORE it reads stdin. filed-here-check does +# exactly that under its bypass and on a detached HEAD, and closing-key-check on +# every could-not-read path. Modelled here, ahead of every read, so a case can +# prove the EPIPE it leaves the producer with is not read as a refusal. Only a +# case that asks for it is affected, so what CLOUD-774 pins below is untouched. +if [ -f "$BATS_TEST_TMPDIR/nodrain.\$2" ]; then exit 0; fi if [ "\$2" = filed-here-check ]; then cat >"$BATS_TEST_TMPDIR/filedhere.stdin"; fi rc="$BATS_TEST_TMPDIR/rc.mise.\$2" if [ -f "\$rc" ]; then @@ -616,6 +622,9 @@ ready_fails() { : >"$BATS_TEST_TMPDIR/rc.ready"; } # case skips the check entirely rather than having to opt out of it. pr_body() { printf '%s' "$1" >"$BATS_TEST_TMPDIR/prbody"; } task_fails() { echo 1 >"$BATS_TEST_TMPDIR/rc.mise.$1"; } +# CLOUD-995. A task that succeeds WITHOUT reading the stdin it was handed, which +# is what a gate's own early exit looks like from the caller's side. +task_exits_undrained() { : >"$BATS_TEST_TMPDIR/nodrain.$1"; } # A task that fails with a specific code, for one call only. `verify` answering # 2 is "main moved while I ran" (CLOUD-318), and a lap that recovers from it is # only a real lap if the next call succeeds. @@ -1264,6 +1273,30 @@ runs_query_403() { : >"$BATS_TEST_TMPDIR/rc.runs"; } [[ "$(cat "$BATS_TEST_TMPDIR/filedhere.stdin")" == *"Closes CLOUD-900"* ]] } +# CLOUD-995. THE GATE'S OWN EXIT, not the pipeline's. `land` runs under pipefail, +# so a gate that exits before reading leaves the producer with EPIPE and the +# pipeline reports failure -- and `land` then reports the gate as having REFUSED. +# Two gates do exit early: filed-here-check returns 0 under its bypass and on a +# detached HEAD, closing-key-check exits 2 on every could-not-read path. So the +# bypass reddened the very lap it exists to wave through. +# +# THE BODY HAS TO BE BIGGER THAN THE PIPE BUFFER or the case proves nothing: a +# short one fits in the kernel buffer, the producer completes before the reader +# is gone, and the old piped form passes too. At 100k the write blocks and the +# EPIPE is certain, which is what makes this able to fail against the old shape. +# +# closing-key-check is failed on purpose so the lap stops somewhere nameable; +# without it `land` would run on to the CI poll and never end. +@test "CLOUD-995: a gate that exits before reading stdin is not a refusal" { + pr_body "$(printf 'x%.0s' $(seq 1 100000)) Closes CLOUD-900" + task_exits_undrained filed-here-check + task_fails closing-key-check + run "$LAND" + [ "$status" -eq 1 ] + [[ "$output" != *"filed a row that was never groomed to Ready"* ]] + [[ "$output" == *"names its issue but never closes it"* ]] +} + @test "a body that names its issue but never closes it stops before review is asked for" { # CLOUD-192's stop, and it sits beside the deferral one for the same reason: # readying is the commitment to review, and the board move is what tells