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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 0.27.1 — 2026-06-08

### fix: L3 rate gate counts mutations, not creations — unblocks the initial bootstrap commit (BRO-1435)

Found via live dogfooding (P11) of the v0.27.0 autonomous-loop demo: `bstack bootstrap` activates the L3 rate gate, then the user's very first `git commit` of the freshly-*scaffolded* governance is blocked — the gate counted the 5 newly-**created** L3 files as 5 L3 mutations (`5/1 EXCEEDED`). Creation is not mutation: there is no prior governance state to destabilize.

### Fixed

- **`scripts/l3-rate-gate.sh` — staged count:** a staged L3 path counts as a mutation only if it already exists at `HEAD` (`git cat-file -e HEAD:<path>`). Newly-created L3 files (and the first-ever commit, where `HEAD` is absent) are exempt.
- **`scripts/l3-rate-gate.sh` — committed count:** uses `git log --diff-filter=M` so commits that *added* L3 files (e.g. the bootstrap scaffold) don't consume the per-window budget; only commits that *modified* them do. Also switched the counter from `wc -l` on a `format:` stream to `grep -c .`, fixing a latent off-by-one (a single committed L3 mutation previously counted as 0).
- The pre-commit template calls the script (no duplicated logic), so this fixes both `bstack bootstrap` Day-1 UX and every deployed `.githooks/pre-commit`.

### Added

- **`tests/l3-rate-gate.test.sh`** — 4 hermetic cases: (A) creating 5 L3 files is exempt, (B) 1 modification is within budget, (C) a 2nd modification in the same window is blocked, (D) non-governance changes are ignored. 4/4 pass.

### Notes

- The gate's actual purpose is preserved: governance *modifications* are still rate-limited to one per `τ_a₃` window (verified by case C). Only *creation* is exempt.
- Primitive count unchanged (**20**). `VERSION` 0.27.0 → 0.27.1.

## 0.27.0 — 2026-06-08

### fix: ship + deploy the dangling P1/P2/P6/P7 hook scripts; scaffold METALAYER + schemas; document the two-flow bootstrap (BRO-1431)
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.27.0
0.27.1
25 changes: 18 additions & 7 deletions scripts/l3-rate-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,29 @@ fi
NOW=$(date +%s)
CUTOFF=$((NOW - TAU_A_L3_INT))

# Count L3-class commits in the window
# Build a pathspec list for `git log -- <paths>` — git accepts multiple paths
COUNT_COMMITTED=$(git log --since="@$CUTOFF" --pretty=format:%H -- "${L3_PATHS[@]}" 2>/dev/null | wc -l | tr -d '[:space:]')
# Count L3-class commits in the window that MODIFIED an L3 file (--diff-filter=M).
# Additions (creation — e.g. the initial `bstack bootstrap` scaffold) are not
# mutations: there is no prior governance state to destabilize, so they do not
# consume the rate budget (BRO-1435). `grep -c .` counts robustly regardless of
# a trailing newline (fixes a latent off-by-one in the prior `wc -l` form).
COUNT_COMMITTED=$(git log --diff-filter=M --since="@$CUTOFF" --format='%H' -- "${L3_PATHS[@]}" 2>/dev/null | grep -c '.' || true)
COUNT_COMMITTED=${COUNT_COMMITTED:-0}

COUNT_STAGED=0
STAGED_FILES=""
if [ "$INCLUDE_STAGED" = "1" ]; then
# Check if any staged files match L3 paths
# Count a staged L3 file as a mutation ONLY if it already exists at HEAD.
# Newly-created L3 files (e.g. the initial `bstack bootstrap` scaffold) are
# creation, not mutation — there is no prior governance state to destabilize,
# so they are exempt from the rate budget (BRO-1435). If HEAD does not exist
# yet (first commit ever), every path is a creation → exempt.
staged_now="$(git diff --cached --name-only 2>/dev/null)"
for path in "${L3_PATHS[@]}"; do
if git diff --cached --name-only 2>/dev/null | grep -qFx "$path"; then
COUNT_STAGED=$((COUNT_STAGED + 1))
STAGED_FILES="$STAGED_FILES $path"
if printf '%s\n' "$staged_now" | grep -qFx "$path"; then
if git cat-file -e "HEAD:$path" 2>/dev/null; then
COUNT_STAGED=$((COUNT_STAGED + 1))
STAGED_FILES="$STAGED_FILES $path"
fi
fi
done
fi
Expand Down
57 changes: 57 additions & 0 deletions tests/l3-rate-gate.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# tests/l3-rate-gate.test.sh — L3 rate gate counts MUTATIONS, not creations (BRO-1435).
#
# Regression guard for the Day-1 bug where `bstack bootstrap`'s initial commit
# (which CREATES the governance files) was blocked by the rate gate. The gate
# must:
# A. exempt newly-CREATED L3 files (creation is not mutation),
# B. allow 1 L3 MODIFICATION per window,
# C. block the 2nd L3 modification in the same window,
# D. ignore non-governance changes.

set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
GATE="$SCRIPT_DIR/scripts/l3-rate-gate.sh"

pass=0; fail=0
check() { # name want_exit got_exit
if [ "$3" = "$2" ]; then echo " [pass] $1"; pass=$((pass + 1))
else echo " [FAIL] $1 — want exit $2, got $3"; fail=$((fail + 1)); fi
}

WS="$(mktemp -d)"; trap 'rm -rf "$WS"' EXIT
cd "$WS" || exit 2
git init -q; git config user.email t@t; git config user.name t
printf 'x\n' > app.py; git add app.py; git commit -q -m seed

mkdir -p .control
cp "$SCRIPT_DIR/assets/templates/rcs-parameters.toml.template" .control/rcs-parameters.toml
printf '# gov\n' > CLAUDE.md; printf '# gov\n' > AGENTS.md; printf '# gov\n' > METALAYER.md
printf 'version: "1.0"\n' > .control/policy.yaml

echo "L3 rate gate — creation vs mutation (BRO-1435)"

# A — create governance files (the bstack bootstrap scenario): EXEMPT
git add -A
BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "A: creation of 5 L3 files is exempt (exit 0)" 0 $?
git commit -q -m "create governance"

# B — modify ONE existing governance file: within budget
printf '# tweak\n' >> CLAUDE.md; git add CLAUDE.md
BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "B: 1 modification within budget (exit 0)" 0 $?
git commit -q -m "modify governance #1"

# C — modify again in the same window: EXCEEDED
printf '# tweak2\n' >> CLAUDE.md; git add CLAUDE.md
BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "C: 2nd modification in window blocked (exit 1)" 1 $?

# D — non-governance change only: ignored
git restore --staged . 2>/dev/null || git reset -q
git checkout -q -- CLAUDE.md 2>/dev/null || true
printf 'y\n' >> app.py; git add app.py
BROOMVA_WORKSPACE="$WS" bash "$GATE" --staged >/dev/null 2>&1; check "D: non-governance change ignored (exit 0)" 0 $?

echo "─────────────────────────────────────"
echo "Passed: $pass Failed: $fail"
[ "$fail" -eq 0 ] && echo "All tests passed." || exit 1
Loading