Skip to content

ci(gate): make policy-budget see the file batten.toml declares as its input - #654

Merged
wenzowski merged 1 commit into
mainfrom
claude/serena-tickets-audit-bwy7fn
Aug 22, 2026
Merged

ci(gate): make policy-budget see the file batten.toml declares as its input#654
wenzowski merged 1 commit into
mainfrom
claude/serena-tickets-audit-bwy7fn

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Closes CLOUD-899.

hk.pkl's policy-budget step globbed AGENTS.md, .serena/memories/always/*.md
and batten.toml. The first and third are inputs. The second has never existed. The
file it omitted is one batten.toml declares as counted.

The gate could not see the file it budgets

batten.toml:1147-1149 declares .serena/project.yml's initial_prompt an embedded
budget entry, so a commit changing that string is exactly what this gate exists to
price — and the step did not list it. batten-check globs the path (hk.pkl:857) but
is slow-profile and skipped at commit time, so coverage held under verify and ci
while the pre-commit tier stayed blind.

That was invisible while the value was empty. CLOUD-864 put a line in it on
2026-08-22 at 07:16:51Z, and the budget became real while nothing re-ran the gate —
so that issue's acceptance §4 was satisfied by a hand-run and held by nothing.

Same shape hk.pkl:236-239 already records for CLOUD-770: "a step that does not list
it never fires on a memory-only commit."

The dead entry

.serena/memories/always/*.md matches nothing. batten.toml:1092-1097 says the
directory has never existed, and CLOUD-298 removed the engine-side copy of the same
dead entry — [budget.instructions].files is ["AGENTS.md"] alone. This copy
survived, matching nothing and reading to the next author as evidence of an always/
convention.

Shown able to fail, both halves

The discriminating case for a glob change is whether the step is selected, which
inspection cannot answer. Measured with hk check --step policy-budget -v over a
.serena/project.yml-only diff:

verdict
old glob DEBUG policy-budget: no file matches for step
new glob policy-budget – 1 file – AGENTS.md .serena/project.yml batten.toml

And the gate itself, with five scratch lines added to initial_prompt:

.serena/project.yml#initial_prompt ~157 tokens 6 lines
AGENTS.md ~3204 tokens 197 lines
policy-budget instructions: ~3361 tokens of 3500, 203 lines of 199

Exit 2, naming the embedded entry. Reverted; green at 198/199.

Dropping the dead entry changes no verdict, which is its own assertion — a removal
that altered a result would mean it had been matching after all.

Two corrections to CLOUD-899's own body

  • Its §2 says an over-budget count is exit 1. Measured: exit 2, which is correct
    per the exit contract for a policy verdict (CLOUD-292). The Ready text is what was
    wrong.
  • It was claimed under BATTEN_CLAIM_CHECK_BYPASS. claim-check refused it
    correctly: CLOUD-431 asks whether the story was refined before the session
    implementing it, and it was not — the same session filed it and pulled it. Recorded
    on the row and in the receipt.

Provenance

Found by auditing the Serena ticket set against the tree rather than by a failure —
nothing was red, because nothing was looking.

@linear-code

linear-code Bot commented Aug 22, 2026

Copy link
Copy Markdown
CLOUD-899 `policy-budget`'s step glob names a directory that has never existed and omits the file `batten.toml` declares as its input

Why

hk.pkl:614-617, measured on main @ 1aadba0, 2026-08-22:

["policy-budget"] {
  glob = List("AGENTS.md", ".serena/memories/always/*.md", "batten.toml")
  check = "mise run policy-budget"
}

Two defects in one three-entry list, pointing opposite ways.

1. The gate cannot see a file it budgets

batten.toml:1147-1149 declares .serena/project.yml's initial_prompt a counted budget entry:

[[budget.instructions.embedded]]
path = ".serena/project.yml"
key = "initial_prompt"

The step glob does not name that path. So a commit that changes initial_prompt — the one edit this entry exists to price — does not fire policy-budget at the pre-commit tier.

batten-check does glob .serena/project.yml (hk.pkl:857), and its own comment says why: "budget::measure_all, because a declared budget is a gate under check and not only under policy budget (CLOUD-50)." But that step is profiles = List("slow") — skipped at commit time by construction. So the surface is covered under verify/ci and blind in the tier that runs on every commit.

This is the shape hk.pkl:236-239 already names for CLOUD-770: "a step that does not list it never fires on a memory-only commit — the widening would hold in verify and ci while the pre-commit tier stayed blind to the surface it was added for." Same defect, different step, and the precedent is in the same file.

It is load-bearing now — PR #638 landed at 07:16:51Z on 2026-08-22 and did not fix the glob. Verified on main @ 5c510fa: .serena/project.yml:198 is initial_prompt: | with a one-line body, and hk.pkl:615 is byte-identical to what it was before the merge. So the acceptance below was met by a hand-run and is held by nothing. What follows was written while #638 was still a draft:

CLOUD-864's acceptance §4 requires initial_prompt to be non-empty with policy-budget green, and PR #638 is the change that fills it. Verified on that PR's head c757a33: the glob is byte-identical to main's. So the acceptance would be met by a hand-run, not by wiring — half a change under non-negotiable rule 2, on the one entry batten.toml:1142-1146 says was gated specifically so the first person to put text there pays for it.

2. A dead entry survived CLOUD-298 on the hk side

.serena/memories/always/*.md matches nothing. git ls-tree -r origin/main -- .serena/memories/always returns empty, and batten.toml:1092-1097 states it outright:

"The shell task also globbed .serena/memories/always/*.md, a directory that has never existed — a dead entry contributing nothing while the rest counted, which is exactly the defect CLOUD-298 measured and which the engine now refuses outright (exit 1, per entry)."

CLOUD-298 fixed the engine-side copy: [budget.instructions].files is ["AGENTS.md"] alone, and an entry matching no file is now exit 1. The hk-side copy was not touched, so the tree carries a glob whose own authority documents it as dead. It is inert rather than harmful — a never-matching glob never triggers a step — but it is a second authority over the counted set, and it reads to the next author as evidence that an always/ convention exists.

The two halves are one edit and are filed together: splitting them would leave whichever landed second re-opening the same three-entry list.


Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Source of truth (§1). batten.toml's [budget.instructions] block is the authority on what is counted; hk.pkl's step glob is the trigger that must cover it. No new authority — the fix makes the trigger follow the authority it already has. Generalising that coupling (the batten-glob-check equivalent policy-budget has never had) is out of scope and belongs with CLOUD-224.
  • Computable predicate (§2). No new gate. Two edits to hk.pkl:615: add .serena/project.yml, remove .serena/memories/always/*.md. The predicate that then holds is policy-budget's own, already implemented — an embedded entry whose file the engine cannot parse is exit 1, and an over-budget count is exit 1.
  • Effect (§3). read. No verb, no rule kind, nothing under crates/.
  • Generated artifacts (§4). None.
  • Output & exit (§5). Untouched.
  • Commit / bump (§6). cino bump. Gate wiring only; nothing under crates/batten/src.
  • Test obligation (§7). The step shown to fire, which is the discriminating case and the one a glob change can silently fail: a commit touching only .serena/project.yml must run policy-budget, verified with hk check --from-ref -v (the only way to see a selector's real reach — mem:toolchain-and-hooks, and the method CLOUD-865 used for shellcheck). Plus mise run policy-budget red on an over-budget initial_prompt and green on the empty one, so the added path is shown able to fail rather than merely present. Removing the dead entry must not change any verdict — its own assertion, since a glob removal that altered a result would mean it was matching after all.
  • Blockers (§8). None. Ordering only: PR feat(policy): gate tool substitution, name shell programs .sh, and fix what that rename broke #638 fills initial_prompt, so landing this with or before that PR is what makes its acceptance §4 mechanical rather than hand-checked.

Acceptance

  1. hk.pkl's policy-budget step globs .serena/project.yml and no longer globs .serena/memories/always/*.md.
  2. A .serena/project.yml-only commit is shown to fire the step, by selector output rather than by inspection.
  3. mise run policy-budget is shown able to fail on an over-budget initial_prompt.
  4. No other verdict changes when the dead entry is dropped.

Filed from an audit of the Serena ticket set, 2026-08-22, while PR #638 was still a draft. Found by reading the gate wiring against batten.toml's declared budget rather than by a failure.

Updated after #638 merged the same morning. The original closing line read "nothing is red today, which is the point." True of a prospective defect; the merge made the budgeted string real — nothing is red because nothing is looking. Kept visible rather than edited away, because the deferral this row could have absorbed ("it costs nothing yet") expired at 07:16:51Z.

Claimed under BATTEN_CLAIM_CHECK_BYPASS. claim-check correctly refused this row: CLOUD-431 asks whether the story was refined before the session implementing it, and it was not — the same session filed it and pulled it. Recorded here as well as in the receipt.

Review in Linear

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • hk.pkl is excluded by !**/*.pkl

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ad45a7c-1053-4546-a609-210b948c101e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

… input

The step globbed AGENTS.md, .serena/memories/always/*.md and batten.toml. The
first and third are inputs; the second has never existed, and the file it
omitted is one that batten.toml declares as counted.

[[budget.instructions.embedded]] counts .serena/project.yml's initial_prompt,
so a commit changing that string is exactly what this gate exists to price --
and the step did not list it, so it never fired at the pre-commit tier.
batten-check globs the path but is slow-profile and skipped at commit time, so
coverage held under verify and ci only. That was invisible while the value was
empty; CLOUD-864 put a line in it, and the budget became real while nothing
re-ran the gate.

.serena/memories/always/*.md is dropped. batten.toml states the directory has
never existed, and CLOUD-298 removed the engine-side copy of the same dead
entry; this one survived, matching nothing.

Closes CLOUD-899.
@wenzowski
wenzowski marked this pull request as ready for review August 22, 2026 09:05
@wenzowski
wenzowski force-pushed the claude/serena-tickets-audit-bwy7fn branch from f514bc0 to 28309e8 Compare August 22, 2026 09:05
@sonarqubecloud

Copy link
Copy Markdown

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 28309e8 into main Aug 22, 2026
5 of 6 checks passed
@wenzowski
wenzowski deleted the claude/serena-tickets-audit-bwy7fn branch August 22, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant