Skip to content
Closed
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
23 changes: 23 additions & 0 deletions .claude/rules/policy-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ The measured reason: one concept was spelled 19 different ways across 17 shell
programs before the registry existed. A convention would not have stopped that; a
load-time refusal does.

**NEVER BUILD A PREDICATE ON TEXT A ROUND TRIP REWRITES, and never spell a
threshold as a pattern.** Two failures, one root: reaching for the registry
because it is the nearest declaration surface rather than because the thing being
declared is a concept with one spelling.

A tracker sanitises what it stores. This consumer already declares
`ready-issue-mention-markup` **because** a bare issue key comes back wrapped in
`<issue …>` markup — so a rule matching key text is matching the one thing the
round trip is known to mangle, and it will pass in a fixture and fail in
production. Measured 2026-09-01: a prose-dialect ratchet was drafted as
`^CLOUD-([0-9]{1,3}|1[0-3][0-9]{2})$`, a key range in alternation. Wrong twice —
arithmetic is not a concept, so a range is unreadable and unmovable in a regex,
and the decision turned on rewritten text. It is a **value** now, in `[ready]`.

Its replacement carried a subtler form of the same error and is worth the
sentence: a key ORDINAL — trailing digits, no separator assumed — reaches no
consumer literal and passes `no-tracker-key-in-core`, yet still requires keys
that are numeric AND monotonic with creation order. Three popular trackers give
that and a slug- or UUID-keyed one does not, where it would resolve to nothing
and **fail silently**. Prefer a fact every tracker actually stamps: the row's
creation instant, compared as fixed-width ISO-8601, which is what
`filed-here.rego`'s `predates_the_branch` already does.

**A PRESET IS EXEMPT, AND IN A PRESET YOU WRITE THE LITERAL INLINE.** This
paragraph told authors the opposite — that the exemption was "a hole rather than
a design" and to "write the row" anyway — and following it produces a **dead
Expand Down
32 changes: 28 additions & 4 deletions .claude/rules/toolchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,22 @@ retired, and `crates/batten/tests/session_provisioning.rs` carries both its
ledger and the tier that proves the door does what the rows say. Add a
provisioning step by adding a task and a row — never by putting a second step
inside an existing task's body, which is the shape that made the script
unreadable from the committed authority. Not `hk
unreadable from the committed authority.

**AND NO NEW MECHANISM GOES BACK INTO A HARNESS'S OWN DIRECTORY.** That retirement
was a direction, not a one-off: `batten.toml` is the authority and `batten hook`
is the one entry, so a capability declared under `.claude/` — a hook, an agent
definition, a command — exists for **one** of the five wired harnesses and is
invisible to the other four, which is the reach the engine was built to have.
Measured 2026-09-01: designing a way to record that a pressure-test subagent had
actually run, an agent proposed `.claude/agents/*.md` as the prompt's home, which
would have bound the whole mechanism to Claude Code while `hook.rs` already
normalises a spawn to `Operation::Subagent` across three harnesses and reports
could-not-look on the two that declare no spelling. The declaration belongs in
`batten.toml` over a tracked file; the harness's directory is where a capability
goes to be unavailable. A harness that offers no spelling for something must read
as **unanswered**, never as absent-and-therefore-fine, which is a property only
the engine can hold. Not `hk
install`: its generated hook calls `hk` bare, which does not resolve where
mise's shims are off PATH, so the installed body is `.claude/hooks/git-hook.sh` —
which also refuses to re-enter a gate that is already running, the recursion
Expand Down Expand Up @@ -511,9 +526,18 @@ call` with no `CLOUD-*` key **in that same paragraph** stops the lap. Two open
the current branch carries no claim receipt. `claim-check` still mints that
receipt on its pullable path, under `.git/batten-receipts/`, and the engine
reads the same file: keyed by **branch**, not by SHA like `ready-guard`'s,
because a claim attests to a decision about an _issue_ that every commit on the
branch continues to serve, and a SHA-keyed one would demand a re-claim per
commit. The naive form ("refuse unless a `CLOUD-<n>` is In Progress") is not
because a claim attests to a decision that every commit on the branch continues
to serve, and a SHA-keyed one would demand a re-claim per commit.
**THE KEY IS STORAGE, NOT CARDINALITY, and this clause used to imply
otherwise** — it read "a decision about an _issue_ that every commit on the
branch continues to serve", singular, which is the only sentence in the whole
instruction surface that touches issue-per-branch and it pointed the wrong way.
A branch carries **as many claims as it has rows** — AGENTS.md states the model
in its autonomous-workflow paragraph rather than its board one, because
`policy-budget` refused the fuller wording at its own ceiling, and
`mem:workflow/board-states` carries the rationale. Measured 2026-09-01: reading this sentence, an agent declined to
pull a second row onto an open branch and reported the receipt as forbidding
it, when the receipt is a file name. The naive form ("refuse unless a `CLOUD-<n>` is In Progress") is not
computable in a hook at all: no tracker credential exists there, which is why
`claim-check` is a pure function of piped stdin. Scratch work is excluded
structurally rather than by tuning — git-ignored, out-of-repo and `.git` paths
Expand Down
70 changes: 65 additions & 5 deletions .serena/memories/workflow/agent-fanout.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,31 @@ it were one, which is how a number nobody approved becomes a standing constraint
The measurement below is unaffected and is NOT the cap: N ≈ 2.9 prices _land
contention_, and the lever that measurement argues for is still "serialise the
landing, shorten the lap, quiet `main`". The cap is a separate, owner-set bound on
how many implementers may hold a claim at once, and the two must not be conflated
how many BUILDS may be in flight at once, and the two must not be conflated
again — if the arithmetic below argues for a different number, that is an argument
to bring to the owner, not a licence to edit this one.

**THE UNIT IS A BUILD, NOT A TICKET, AND THIS FILE SAID THE WRONG ONE.** It read
"how many implementers may hold a claim at once", which is the PR/issue
conflation one layer down: a branch carries as many rows as the work needs, so a
PR closing ten tickets is **WIP 1**. It contends for the lease once, rebases
once, runs `verify` once. Counting claims makes the cap punish exactly the
bundling the section below tells you to maximise — an eight-row bundle in one
domain would read as WIP 8 while costing the trunk what WIP 1 costs.

Measured 2026-09-01: reading "enforced at claim time", an agent reported the WIP
cap as a bound on how many tickets it could take, twice.

**AND THE MECHANISM COUNTS THE WRONG THING TOO**, so this is not merely a wording
fix: `mise-tasks/graph-check.sh` emits
`wip $(jq -r '[.[] | select(.status == "In Progress")] | length')` — one per
ISSUE. The board-computable count of builds is the distinct PR attachments among
In Progress rows plus the In Progress rows carrying none; that over-counts a
pre-PR bundle and never under-counts, which is the safe direction. `graph-check`
is governed shell, so the fix is a retirement row rather than an edit
(`.claude/rules/toolchain.md`), and until it lands the emitted `wip` number reads
high for anybody who bundles.

Past the cap the binding constraint is **land contention**, not compute: every
land forces siblings to rebase and re-run `verify`, so N ≈ time-between-lands ÷
verify-duration. **A rising re-verify rate is NOT the stop signal** — an
Expand Down Expand Up @@ -261,10 +282,49 @@ procedure; this section owns why it is shaped that way.

**Dispatch bundles, not single tickets.** A session handed one ticket stops when
it lands, and its container plus its warm context are thrown away. A session
handed an ordered chain in one file domain keeps going, and — the part that
matters for the cap above — amortises several commits over one rebase cost
instead of paying that cost per ticket. Bundling is what raises the ceiling;
adding sessions is not.
handed an ordered chain in one file domain keeps going instead. Bundling is what
raises the ceiling; adding sessions is not.

**AND THE REASON IS NOT REBASE AMORTISATION — that argument is refuted by this
file's own next section.** It used to read "amortises several commits over one
rebase cost instead of paying that cost per ticket", which contradicts the
caps section directly: _"a fast-forward refusal rebases and re-verifies with no
model turn, so a moved base costs CPU and wall-clock, both of which are free
here, and zero tokens. Re-verifying is the loop working."_ You cannot amortise a
free thing. `land` laps unattended and an agent absorbs rebases without a turn,
so a rebase is not a cost that bounds anything.

**What bundling actually saves is the METERED half of AGENTS.md's three costs.**
Local execution — a build, a rebase, the whole suite — is free. A CI run costs
real minutes and a model call is metered in the same category. Ten rows in one
PR buy **one** CI matrix, one review, one lease acquisition and one landing
sequence, where ten PRs buy ten of each. That is a real multiple on the only
costs that are real, and it does not weaken as the fleet gets faster — where the
rebase argument got weaker the better the automation got, which is the tell that
it was never the reason.

**SO MAXIMISE THE BUNDLE, subject to file-domain coherence and nothing else.**
The direction is not "a few is better than one" — it is _as many related rows as
the domain holds_. Every extra row in a bundle is one more thing landed per
rebase, per `verify`, per CI run and per lease acquisition, so it makes the
measured constraint smaller rather than larger. A bundle of eight in one domain
lands faster than four bundles of two, and the four bundles also contend with
each other.

**THERE IS NO PR-SIZE CAP HERE, AND NONE SHOULD BE INFERRED.** The cap of 2 and
the WIP cap of 6 are bounds on concurrent LANDING; neither says anything about
how large a diff may be. Nothing in this repository caps lines changed, and a
reviewer reading a coherent domain-scoped diff is reading one story either way.
Measured 2026-09-01: an agent invented a "2500 lines is big" threshold, cited it
as a reason to split work across PRs, and it appears nowhere in this repository —
inventing a size limit is how the amortisation above gets thrown away by an agent
being careful about the wrong thing. The context window is not the binding
constraint on a frontier model, and treating it as one costs laps.

The real bound on a bundle is the one already stated: **it must be one file
domain**, read off open PRs' file lists rather than their titles. Two rows that
sound unrelated and both edit `mise-tasks/land.sh` belong in the same bundle;
two that sound related and touch disjoint trees do not.

Order within a bundle by real dependency: the ticket whose gate the next one
needs goes first, and the ticket that _replaces_ what an earlier one fixed goes
Expand Down
24 changes: 24 additions & 0 deletions .serena/memories/workflow/board-states.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,30 @@ which is what this wanted: the predicate needs a network call, and no rule kind
can make one on a mediated call (CLOUD-446). `verify` is the earliest surface
that still sits on every path to a published PR.

**ONE COMMIT ONE ISSUE; ONE BRANCH MANY ISSUES; ONE PR EVERYTHING, NO PUNTS.**
AGENTS.md carries the rule; this is why it needs saying at all. **The commit is
the unit of work and the branch is not a row.** A second row you find mid-branch
is claimed and worked THERE — cutting a fresh branch for it is precisely the
batching `land`'s lap loop exists to prevent (each lap rebases onto a little more
landed work, so conflicts arrive one resolvable increment at a time), and
stopping at the first row is the punt `filed-and-left-open` prices. Drafts run no
CI, so the PR is finished before a runner is spent; readying to "get a first
signal" spends a matrix on work you already know is incomplete.

**Nothing keys work to a branch except the claim receipt's FILENAME, and that is
storage rather than the model.** The distinction is worth spelling out because
both the prose and the mechanism used to point the other way. `.claude/rules/toolchain.md`
explained the branch keying as attesting "a decision about an _issue_ that every
commit on the branch continues to serve" — singular, and the only sentence in the
whole instruction surface touching issue-per-branch cardinality. Meanwhile
`claim::mint` took a slice and wrote line 1 as an id LIST, but wrote the file
with `fs::write`, so a second `claim check` INVOCATION replaced the first row's
claim silently. Measured 2026-09-01: an agent read the receipt as forbidding a
second row on an open branch and reported the filename as the rule. Both halves
are fixed — the ids union now, guarded by the recorded `base` so a restarted
branch (`git checkout -B <name> origin/main`, which keeps the name and discards
the commits) starts a fresh list rather than carrying CLOUD-516's stale claim.

**`claim-check` runs BEFORE the board move, not after — and the order is not
interchangeable.** It refuses `not-todo`, so once the issue is In Progress it
refuses the very claim you just made, and it cannot tell your own move from a
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ stopping short to ask is the deviation. **The gates ARE your authorization** —
you run them yourself, and they halt you by _failing_, not by needing a blessing.
So **`git commit` needs no asking** — local, reversible, and commit early and
often, since a sprawling uncommitted tree is what this kills. Establish base
state first (`git fetch origin main`), work on a short-lived branch, never author
on `main`, and carry the lifecycle without stopping between steps to report.
state first (`git fetch origin main`), never author on `main`, and work ONE
short-lived branch: one commit one issue, one branch many rows, one PR all of it.

**When you SHOULD still stop** (real exceptions, not an escape hatch): a gate
fails and the fix is genuinely ambiguous; a rebase conflict needs a human
Expand Down
Loading