Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .serena/memories/workflow/agent-fanout.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ minutes becoming ~7.5 against the same 7.3-minute gap. So the odds a re-invoked
is not the same bet as raising `LAND_MAX_LAPS`. Neither is a substitute for the
lever; both beat engineering around the loop.

**What the backstop is asking when it stops you.** `still not linear after 8
laps; main is moving faster than a lap takes. Look before lapping again` — the
thing to look at is whether the laps lost for a REASON (rebase conflict, red CI,
failed `verify`) or purely because `main` moved. Only the second is contention,
and only the second is safe to re-run unchanged; the first is a defect the loop
is correctly refusing to paper over. Measured again on CLOUD-282, which spent a
full 8 laps to the backstop with every `verify` green and CI green three separate
times, then lost the next invocation to a genuine rebase conflict — two different
answers to that question, one invocation apart.

A land that stops on the backstop is **re-run, not re-engineered**: wrapping it
in bespoke retry or pre-check logic is forbidden (AGENTS.md), and re-invoking
`mise run land` after confirming the laps lost only to movement is the sanctioned
move.

## Planners fan out freely; implementers do not

The cap above is a **build** cap, and reading it as a cap on sessions is the
Expand Down
115 changes: 115 additions & 0 deletions batten.toml
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,121 @@ severity = "deny"
scope = "tree"
no_fix_reason = "pin the tool in mise.toml so CI installs exactly what the lockfile names"

# Portability: the shell layer must run on a machine that is not the CI runner
# (CLOUD-282).
#
# `mise run verify` is the authorization the whole workflow contract rests on —
# "verify exhaustively before CI", "the gates ARE your authorization". On macOS
# it could not pass, for reasons no branch caused: two gates used sed's
# NUL-separated mode (GNU only, BSD exits `illegal option -- z`), two used
# `mapfile` (bash 4; macOS ships 3.2), one used xargs' short
# `--no-run-if-empty`, and three fixture repos force-created a branch that git
# refuses to force while it is checked out. An agent there either stopped on a
# red `verify` it did not break, or landed on CI's word alone — the exact
# failure the contract exists to prevent. CI runs ubuntu, so CI is
# STRUCTURALLY BLIND to every one of these: the same "verdict is a property of
# the machine" shape as CLOUD-227.
#
# These are `forbid` rows and not a 50th `mise-tasks/*-check` program, per the
# 2026-08-07 engine-first amendment: `batten check` already runs in the shared hk
# gate with no glob, so the whole construct set is config a reviewer can read
# (§9) rather than a second copy inside a shell script. That amendment exists
# because the older wording made a bash task the cheapest satisfying artifact,
# and CLOUD-202 is in flight moving the existing guards OUT of that layer.
#
# WHY THE GLOBS ARE `mise-tasks/**` AND `tests/**/*.bats`:
#
# * two or more segments, so neither can match the root `batten.toml` and make
# the rule fire on its own definition — the walk is `fs::read_dir` and skips
# only `.git`.
# * recursive, matching `no-bash4-wait-n` and `bats-tests-not-deleted` rather
# than inventing a third depth convention. Both were narrowed to one segment
# once, only to dodge the vendored `tests/bats` submodule the walker used to
# descend into; since CLOUD-328 the walk stops at a nested repository, so
# depth is free again and hk.pkl's `batten-check` glob spans it too. That
# coupling is not cosmetic: `batten-glob-check` subsumes a required glob only
# by verbatim match or a `P/**` prefix, so `tests/*.bats` would be uncovered
# by hk's `tests/**/*.bats` and the step would stop running for exactly the
# commits this rule judges.
#
# WHAT IS DELIBERATELY NOT HERE, and why it is a gap rather than an oversight:
# the `tests/**/*.bats` half of the sed and flock rows. `tests/memory-guard.bats`
# carries the in-place form five times as FIXTURE STRINGS — it is the suite
# proving `memory-guard` denies that shape — and `tests/target-ensure.bats` names
# the util-linux lock in a test title. A `forbid` pattern is a literal substring
# with no `exclude` column, so those rows cannot be written today without firing
# on the tests that prove the guards work. That is exactly CLOUD-283. The
# `mise-tasks/**` half — the layer that actually could not run — lands now.
#
# The rationale comments in the guarded files deliberately do NOT spell the
# literals they explain, for the same reason the agnosticism rules above do not
# embed theirs: a substring rule over a directory fires on its own explanation.
[[rule]]
id = "no-gnu-sed-z"
kind = "forbid"
glob = "mise-tasks/**"
pattern = "sed -zE"
severity = "deny"
scope = "tree"

# The trailing space is load-bearing: it is what distinguishes the GNU-only
# no-suffix spelling from `sed -i.bak`, which BSD accepts and which
# `tests/helpers.bash` uses.
[[rule]]
id = "no-gnu-sed-in-place"
kind = "forbid"
glob = "mise-tasks/**"
pattern = "sed -i "
severity = "deny"
scope = "tree"

[[rule]]
id = "no-bash4-mapfile"
kind = "forbid"
glob = "mise-tasks/**"
pattern = "mapfile "
severity = "deny"
scope = "tree"

[[rule]]
id = "no-gnu-xargs-r"
kind = "forbid"
glob = "mise-tasks/**"
pattern = "xargs -r"
severity = "deny"
scope = "tree"

# CLOUD-286 replaced flock(1) with a `mkdir` mutex and named this row as its own
# standing half: the refusal that fired before every other gate on a Mac was
# correct behaviour over a Linux-only primitive.
[[rule]]
id = "no-util-linux-flock"
kind = "forbid"
glob = "mise-tasks/**"
pattern = "flock "
severity = "deny"
scope = "tree"

# git refuses `branch -f` on the branch that is currently checked out. Three
# fixture setups did it anyway, and passed only because the runner's git still
# defaults to `master`, which made `main` a different branch — an accident of the
# runner's configuration, not a property of the test. It is a `setup()` failure,
# so every case in the suite errors, not one.
#
# The replacement is `git init -b <name>` plus a plain `git branch main`, NOT
# `branch -M main`: `-M` renames the current branch, and these fixtures want a
# SECOND branch marking the trunk while HEAD stays on the feature branch. That
# distinction is measured, not assumed — rewriting them to `-M` first made
# `tests/verified.bats`'s moved-main case fail, because it checks that other
# branch out.
[[rule]]
id = "no-branch-f-main"
kind = "forbid"
glob = "tests/**/*.bats"
pattern = "branch -f main"
severity = "deny"
scope = "tree"

# Which programs change the world, in the one §5 effect vocabulary, each with the
# sanctioned mutation to point at when it is refused.
#
Expand Down
120 changes: 120 additions & 0 deletions crates/batten/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3788,6 +3788,126 @@ fn the_committed_repo_agnosticism_rules_fire_on_every_banned_shape() {
);
}

#[test]
fn the_committed_portability_rules_fire_on_every_banned_shape() {
// CLOUD-282. `mise run verify` is the authorization the workflow contract
// rests on, and on macOS it could not pass: two gates used sed's GNU-only
// NUL-separated mode, two used bash 4's `mapfile` against a 3.2 /bin/bash,
// one used xargs' short --no-run-if-empty, and three fixture repos
// force-created a checked-out branch. CI runs ubuntu, so CI is structurally
// blind to every one of them — which is exactly why the standing gate is a
// property of the COMMITTED TREE rather than a job.
//
// Same shape as `the_committed_repo_agnosticism_rules_fire_on_every_banned_
// shape` above, and full stdout equality is the load-bearing part for the
// same reason: findings sort by the `(path, line, rule)` pointer tuple, so
// the expected bytes are fixed, and a rule that is deleted, renamed,
// mis-globbed or set to `severity = "allow"` changes them. A `contains`
// assertion would pass with five of the six rules gone.
//
// Unlike that test, the banned literals CAN be written as source text here:
// both globs are anchored at a first segment (`mise-tasks/`, `tests/`) that
// this file, at `crates/batten/tests/cli.rs`, does not sit under.
let committed = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../batten.toml");
let contents = fs::read_to_string(&committed).expect("read batten.toml");

let _ = fs::remove_dir_all(
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("config-portability-dirty"),
);
let dirty = repo_with_config("config-portability-dirty", &contents);
committed_budget_surfaces(&dirty);
committed_config_fixture_git(&dirty);

// One file per glob, each carrying every literal its rules ban, so a rule
// scoped to the wrong directory shows up as a missing line rather than as a
// silent pass. The task file's five are written in the order they sort by
// line, and the sed rows are on separate lines because a single line can
// only ever produce one finding per rule.
fs::create_dir_all(dirty.join("mise-tasks")).expect("create fixture task dir");
fs::write(
dirty.join("mise-tasks/seed"),
"printf x | sed -zE 's/a/b/'\n\
sed -i 's/a/b/' file\n\
mapfile -t lines < <(printf 'x')\n\
git ls-files | xargs -r grep x\n\
flock /tmp/lock true\n",
)
.expect("write fixture task");
fs::create_dir_all(dirty.join("tests")).expect("create fixture test dir");
fs::write(dirty.join("tests/seed.bats"), "\tgit branch -f main\n")
.expect("write fixture suite");

// `enforce`, not `check`: the committed ruleset carries `no-conflict-markers`,
// a kind that runs a configured command, and the read-effect verb refuses the
// whole config rather than silently skipping that one row (exit 1, pinned by
// `the_committed_config_refuses_to_run_a_spawning_kind_under_check`). Every
// sibling test over the committed bytes takes the same verb for the same
// reason.
let output = batten()
.arg("enforce")
.current_dir(&dirty)
.env_remove("BATTEN_STRICTNESS")
.env_remove("BATTEN_FAIL_ON_WARNING")
.output()
.expect("run batten enforce");
assert_eq!(
output.status.code(),
Some(2),
"a construct that cannot run on macOS is a policy violation"
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"mise-tasks/seed:1 no-gnu-sed-z\n\
mise-tasks/seed:2 no-gnu-sed-in-place\n\
mise-tasks/seed:3 no-bash4-mapfile\n\
mise-tasks/seed:4 no-gnu-xargs-r\n\
mise-tasks/seed:5 no-util-linux-flock\n\
tests/seed.bats:1 no-branch-f-main\n",
"one sorted pointer per banned construct, and nothing else"
);

// The discriminator: the portable spellings this change adopted must be
// SILENT, or the rows would forbid their own replacements. `-i.bak` is the
// one in-place form BSD accepts, a `read` loop replaces `mapfile`, bare
// `xargs` replaces the short flag, and a plain `branch main` off an explicitly
// named branch replaces the force.
let _ = fs::remove_dir_all(
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("config-portability-clean"),
);
let clean = repo_with_config("config-portability-clean", &contents);
committed_budget_surfaces(&clean);
committed_config_fixture_git(&clean);
fs::create_dir_all(clean.join("mise-tasks")).expect("create clean task dir");
fs::write(
clean.join("mise-tasks/seed"),
"printf x | perl -0777 -pe 's/a/b/'\n\
sed -i.bak 's/a/b/' file\n\
while IFS= read -r l; do :; done < <(printf 'x\\n')\n\
git ls-files | xargs grep x\n",
)
.expect("write clean task");
fs::create_dir_all(clean.join("tests")).expect("create clean test dir");
fs::write(clean.join("tests/seed.bats"), "\tgit branch main\n").expect("write clean suite");

let output = batten()
.arg("enforce")
.current_dir(&clean)
.env_remove("BATTEN_STRICTNESS")
.env_remove("BATTEN_FAIL_ON_WARNING")
.output()
.expect("run batten enforce");
assert_eq!(
output.status.code(),
Some(0),
"the portable spellings are not violations"
);
assert!(
output.stdout.is_empty(),
"a portable tree prints nothing, got: {}",
String::from_utf8_lossy(&output.stdout)
);
}

#[test]
fn the_committed_example_config_loads_over_the_binary() {
// DoD: `batten.example.toml` loads and round-trips — asserted against the
Expand Down
10 changes: 9 additions & 1 deletion mise-tasks/checksums
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ rm -f "$scratch/$MANIFEST"
# LC_ALL=C so the order is byte order on every machine: two runs over one release
# produce identical bytes, which is what makes the published artifact a function
# of the release rather than of who cut it.
mapfile -t assets < <(cd "$scratch" && shopt -s nullglob && for f in *; do printf '%s\n' "$f"; done | LC_ALL=C sort)
#
# A `read` loop rather than `mapfile` (CLOUD-282): `mapfile` is bash 4 and macOS
# ships 3.2, so it would die here on the one platform whose release artifacts
# this manifest covers. `no-bash4-mapfile` in batten.toml is the standing gate —
# it caught this line the day the file landed.
assets=()
while IFS= read -r asset; do
assets+=("$asset")
done < <(cd "$scratch" && shopt -s nullglob && for f in *; do printf '%s\n' "$f"; done | LC_ALL=C sort)

if [ "${#assets[@]}" -eq 0 ]; then
echo "::error:: checksums: release $tag carries no assets to hash. A manifest that covers nothing is indistinguishable from no manifest, so none is written." >&2
Expand Down
14 changes: 13 additions & 1 deletion mise-tasks/gh-guard-check
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,19 @@ cmd="${1:-}"

# Neutralise quoted spans, then split on shell separators.
scrubbed=$(printf '%s' "$cmd" | sed -E "s/'[^']*'/QUOTED/g; s/\"[^\"]*\"/QUOTED/g")
mapfile -t segments < <(printf '%s' "$scrubbed" | sed -E 's/(\&\&|\|\||[;|&])/\n/g')

# A `read` loop and not `mapfile` (CLOUD-282): `mapfile` is bash 4 and macOS
# ships 3.2 as /bin/bash, so the guard would die on its own first statement.
# Same idiom as `issue-guard` and `ready-guard`, which never used mapfile.
#
# `printf '%s\n'`, WITH the trailing newline, is the load-bearing half. `read`
# returns false on a final segment that carries no terminator, so without it the
# last — and for a single-segment command like `gh pr merge` the ONLY — segment
# is dropped and the guard silently allows everything it exists to deny.
segments=()
while IFS= read -r seg; do
segments+=("$seg")
done < <(printf '%s\n' "$scrubbed" | sed -E 's/(\&\&|\|\||[;|&])/\n/g')

for seg in "${segments[@]}"; do
read -r -a toks <<<"$seg"
Expand Down
10 changes: 9 additions & 1 deletion mise-tasks/memories-check
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,17 @@ while IFS=: read -r file line ref; do
[ -f "$memories/$name.md" ] || report "$file:$line" "mem-ref-stale ($name)"
# CHANGELOG.md is excluded with the template: release-plz owns it, so a stale
# reference there could only be fixed by hand-editing a generated file.
#
# Bare `xargs` below, with no `--no-run-if-empty` (CLOUD-282): the short form
# of that flag is a GNU extension BSD xargs did not carry until recently. It
# bought nothing here — the `/dev/null` sentinel argument means a zero-file
# invocation still runs `grep` against one unmatchable file, which prints
# nothing and exits 1, which is exactly what the flag was suppressing.
# `no-gnu-xargs-r` in batten.toml keeps it out; the literal is not spelled
# here because that row is a substring rule over this directory.
done < <(git ls-files '*.md' ':!:tests/bats' ':!:CHANGELOG.md' |
grep -vxF "$memories/memory_maintenance.md" |
xargs -r grep -noE 'mem:[A-Za-z0-9_/-]+' /dev/null 2>/dev/null | sort -u)
xargs grep -noE 'mem:[A-Za-z0-9_/-]+' /dev/null 2>/dev/null | sort -u)

if [ "$violations" -ne 0 ]; then
echo "::error:: memories-check: $violations violation(s) — the memory graph has dangling edges" >&2
Expand Down
13 changes: 10 additions & 3 deletions mise-tasks/memory-guard-check
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,16 @@ body=$(awk '
inheredoc { if ($0 ~ "^[[:space:]]*" term "[[:space:]]*$") inheredoc = 0; next }
{ print }
' <<<"$value")
# -z, so a quoted span is neutralised across NEWLINES — a multi-line commit
# message is the single most likely place for someone to write these shapes down.
scrubbed=$(printf '%s' "$body" | sed -zE "s/'[^']*'/QUOTED/g; s/\"[^\"]*\"/QUOTED/g")
# Slurped whole, so a quoted span is neutralised across NEWLINES — a multi-line
# commit message is the single most likely place for someone to write these
# shapes down.
#
# `perl -0777` and not sed's NUL-separated mode (CLOUD-282): `-z` is a GNU
# extension, BSD sed exits `illegal option -- z`, and this is a GATE — a macOS
# checkout could not run it at all. The literal is deliberately not spelled here:
# `no-gnu-sed-z` in batten.toml is a substring rule over this very directory, so
# a comment quoting the banned form would make the row fire on its own rationale.
scrubbed=$(printf '%s' "$body" | perl -0777 -pe "s/'[^']*'/QUOTED/g; s/\"[^\"]*\"/QUOTED/g")

# Judged per SEGMENT, not per command string. A read in one segment must not be
# condemned by a write in another, and — the direction that actually matters —
Expand Down
7 changes: 6 additions & 1 deletion mise-tasks/mutant
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,16 @@ for gate in ${gates//,/ }; do
continue
fi

if ! sed -i "$script" "$work/$src"; then
# `-i.bak` and not the bare in-place flag (CLOUD-282): BSD sed reads the
# next argument as the suffix, so the no-suffix spelling consumes the
# script on a Mac. The backup is removed rather than kept — it exists
# only to satisfy the one form both seds accept.
if ! sed -i.bak "$script" "$work/$src"; then
echo "$gate/$slug unappliable-mutation"
failures=$((failures + 1))
continue
fi
rm -f "$work/$src.bak"
# A mutation that changed nothing proves nothing, and would otherwise be
# reported as caught or missed on the strength of an unrelated case.
if cmp -s "$src" "$work/$src"; then
Expand Down
Loading
Loading