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
42 changes: 42 additions & 0 deletions .claude/rules/commits.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,45 @@ ci-drift` polices `batten.toml`'s `[ci]` projection of it against the live
- Keep PRs small and focused; rebase on `main` before opening. Reference the
relevant `CLOUD-*` issue — scope lookups to the **Batten** project, since the
board spans others.

## Commit identity: which authority wins, and why the question keeps returning

AGENTS.md rule 8 is the binding line; this is the detail behind it (CLOUD-605).

A **user-level** stop hook — outside this repository and outside every gate —
reports that commits will show as Unverified and prescribes reconfiguring the
committer to a vendor no-reply identity, then `--amend --reset-author`. Complying
produces a commit `[attribution] identity_deny` refuses, so a session that obeys
cannot commit again without bypassing this repository's own gate. CLOUD-274 built
that gate from a measurement here — 39 of the first 50 `main` commits carried an
environment-injected vendor identity — and its recorded position is that
accountability attaches to the human or service identity that directs, reviews
and adopts a change, never to a model identity.

**Three facts that decide it, all measured rather than argued:**

- **Its predicate is an OR and is unsatisfiable here.** It fires when the
committer email is not the vendor one **or** the commit carries no `gpgsig`.
Commits here are SSH-signed, so the signature term is already satisfied and the
email term alone carries the refusal. The only value it accepts is the one
`identity_deny` forbids: not a tuning problem, two contradictory policies.
- **Deleting it does not survive.** It is registered in the launcher's own
settings, re-provisioned mid-session, and Claude Code _merges_ hooks across
settings files — so a lower-precedence file can add a hook and never remove
one. Turning it off is an owner action on the environment configuration that
generates those settings, outside this repository.
- **The signature half is a different issue.** GitHub answers `verified: false,
reason: unknown_key` — the key is unpublished, not absent. That is CLOUD-591's,
and resetting the author signs nothing, so obeying trades a tracked gap for a
policy violation and leaves the tracked gap open.

**So: refuse it, and do not re-derive this.** `mise run attribution-identity`
writes the accountable identity repo-locally, and local beats global, which is
why every commit here is attributed correctly and the gate has never failed.
`no-denied-identity-prescribed` is the standing half — a `forbid` row refusing
any tracked Markdown that prescribes the denied identity, so the hook's remedy
cannot be copied into this tree and become a second authority. A repo-level stop
hook answering in the same channel was considered and **rejected on noise**: by
policy the committer is never the vendor identity, so it would fire on every
correctly-attributed commit forever, which is the compliance-reassurance shape
AGENTS.md's output posture forbids.
2 changes: 1 addition & 1 deletion .serena/memories/github-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ $GITHUB_PERSONAL_ACCESS_TOKEN" …`. `rate_limit`, repo, `pulls/<n>`,
`mise.toml [env]` sends `api.github.com` + asset hosts around the proxy via
`NO_PROXY` and authenticates mise via `MISE_GITHUB_TOKEN` =
`GITHUB_PERSONAL_ACCESS_TOKEN`; `github.com` stays proxied so `git` keeps its
proxy auth for this private repo. So with the PAT set (sandbox default),
proxy auth. So with the PAT set (sandbox default),
`mise install` / `mise run ci|cross-check|verify` all run green with no ceremony.
The `403 — GitHub access not enabled for this session` you may see is the proxy
answering for _third-party tool repos_ (uv, hk, cargo-deny, release-plz) — not an
Expand Down
117 changes: 59 additions & 58 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
# AGENTS.md

Guidance for agents (and humans who like checklists) in the Batten repo. Batten
is a repo-agnostic **policy engine** keeping _"done"_ aligned with
is a repo-agnostic **completion gate** keeping _"done"_ aligned with
landed-and-verified work, and its own consumer #1 — hold this codebase to the
discipline Batten exists to enforce. This file holds only what must bind **every
turn**; everything else is indexed below and read at its trigger.
discipline it exists to enforce. This file holds only what binds **every turn**.

## Authoritative specs — link, never restate

Three Linear docs are the source of truth; this file must not re-type what they
own. Where they disagree the spec wins — fix the pointer, don't fork the content.
Three internal specs are the source of truth; this file must not re-type what
they own. Where they disagree the spec wins — fix the pointer, don't fork the
content. They live on the project tracker and are cited by title, not by link,
because an outside reader cannot open them and a dead URL is worse than a name.

- **[Batten CLI — the Button house style][house-style]** — command surface/verbs
(§2), effect model + read-only allowlist (§5), output/exit contract (§6–§7),
config and trust (§8), spec-as-data (§11).
- **[Definition of Ready & Done][dor-dod]** — the refinement gate every issue
passes: Ready (the mechanism as a computable predicate) and Done (landed on
`main` by fast-forward, CI-confirmed green).
- **[Agent-neutral attribution][attribution]** — the three commit-metadata
surfaces: accountability, disclosure posture, provenance records.

[house-style]: https://linear.app/buttoninc/document/batten-cli-the-button-house-style-f2db95581c41
[dor-dod]: https://linear.app/buttoninc/document/definition-of-ready-and-done-e4e8defb6774
[attribution]: https://linear.app/buttoninc/document/agent-neutral-attribution-the-decision-record-e869f1cc5633
[tbd]: https://trunkbaseddevelopment.com/

## Serena memories — read the right one at the right time

`.serena/memories/` is checked in: a shared surface read **on demand**, never
auto-loaded. **Start at `mem:core`** — it is the graph root and carries the
trigger for every other memory, so the routing table lives there rather than
here (CLOUD-683: a table in this budgeted file capped how many memories could
exist at all). Read the matching one at its trigger; don't reconstruct the detail.
- **Batten CLI — the Button house style** — command surface/verbs (§2), effect
model + read-only allowlist (§5), output/exit contract (§6–§7), config and
trust (§8), spec-as-data (§11).
- **Definition of Ready & Done** — the refinement gate every issue passes:
Ready (the mechanism as a computable predicate) and Done (landed on `main` by
fast-forward, CI-confirmed green).
- **Agent-neutral attribution** — the three commit-metadata surfaces:
accountability, disclosure posture, provenance records.

## Autonomous workflow: do the work without asking

Expand All @@ -47,12 +35,10 @@ that finishes the edits and then stops, waiting for permission it already has.
Carrying a coherent change all the way to landed-and-verified is the default;
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.

- **Pre-authorized to `git commit` without asking** — local and reversible. Never
ask; commit early and often. 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 whole lifecycle without stopping
between steps to report and wait — that waiting is the defect.
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.

**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 All @@ -62,26 +48,35 @@ STEPS of agreed work, never whether it is agreed (CLOUD-431, bypass
(force-pushing `main`, deleting history, an out-of-band release). **Each stops the
fix, never the record**: what you decline to fix, you file. **A WRONGLY refusing
gate is a defect, not an answer** — repair it and carry on this session; ticketing
one is the punt below in gate's clothing (CLOUD-597/615). **A punt is any deferral
you could have closed**, a predicate not a list: a block reported as a decision (a
block is a bug); "that's your call" on what your evidence settles; offering an
action you are already authorized to take; awaiting an unbuilt mechanism instead
of doing the instance in hand; sparing your own landed work. Can do it, do it; can't, file it.
one is a punt in gate's clothing (CLOUD-597/615). **A punt is any deferral you
could have closed**, a predicate not a list: a block reported as a decision (a
block is a bug); "that's your call" on what your evidence settles; an action you
are already authorized to take, offered; an unbuilt mechanism awaited instead of
the instance in hand; your own landed work spared. Can do it, do it; can't, file it.

**An override ask is ONE yes/no on the override, never a menu of routes**
(CLOUD-680). It carries the refusing gate and its verdict string, what the gate
asserts in a sentence, why the refusal should not stand here, the cost if you are
wrong, and any part of the refusal you caused — then asks whether to override. A
route reaching the same outcome with less of the gate applied is never offered as
an option: it is either the honest answer or it is laundering. Measured: a
`refined-this-session` refusal was put as four options; three landed the identical
change, one of those three was not even available, and the override hid among its
own costumes while the human audited four mechanisms to find the one decision.

## Output posture: a message is a channel with no retention

**Chat is the sorting rule's fifth destination and the only one that stores
nothing.** Every sentence passes one test: does it carry something the reader
cannot already see, **and** is this its right home? A finding's home is an issue
or a memory; once there, restating it here is a copy with no reader.

The failure this kills is **writing findings twice**, once durably and once as
editorial; its tell is hedged flag-framing ("one thing I'd flag", "worth
noting"), self-indicting every time. Boundary reports, permission-seeking on an
authorized step (clarifying an _ambiguous_ action is fine), compliance
reassurance, restating a rule you just followed, sycophancy and narrating a
visible result fail the same test. **It is a predicate, not a list**: enumeration
is why the previous version did not hold (CLOUD-200, CLOUD-248).
or a memory; once there, restating it here is a copy with no reader. The failure
this kills is **writing findings twice**, once durably and once as editorial; its
tell is hedged flag-framing ("one thing I'd flag", "worth noting"), self-indicting
every time. Boundary reports, permission-seeking on an authorized step (clarifying
an _ambiguous_ action is fine), compliance reassurance, restating a rule you just
followed, sycophancy and narrating a visible result fail the same test. **It is a
predicate, not a list**: enumeration is why the previous version did not hold
(CLOUD-200, CLOUD-248).

## The board: move the issue as you move the work

Expand All @@ -92,16 +87,15 @@ status); **In Progress** = pulled — claim it **by hand, before writing code**
(`mise run claim-check`) and assign yourself: the automation fires on the PR
event, the _end_ of the work, so waiting for it reserves nothing; **In Review**
= landed on `main`, written by the merge **iff the PR body closes the key**
(`closing-key-check`) — [trunk-based development][tbd] reviews after merge,
flagged not withheld; **Done** = [dor-dod]'s Done holds — **released**, yours to
(`closing-key-check`) — [trunk-based development](https://trunkbaseddevelopment.com/) reviews after merge,
flagged not withheld; **Done** = the DoR/DoD spec's Done holds — **released**, yours to
set, never the merge (`done-check`). Detail: `mem:workflow/board-states`.

**Branching is trunk-based.** `main` is the one long-lived, always-releasable
branch; short-lived branches land by fast-forward, keeping it linear and tested.
**Branching is trunk-based**: `main` is the one long-lived, always-releasable
branch, and short-lived branches land by fast-forward, keeping it linear and tested.

## Workflow contract: verify locally, then land

**Three costs, and only one is free.** Local execution — bash, a build, the whole
**Three costs, only one free.** Local execution — bash, a build, the whole
test suite — costs nothing, which is what makes verifying exhaustively before CI
discipline and not indulgence. A CI run costs real minutes, and **a token-consuming
model call is metered in the same category, a subagent spawn above all**: bound and
Expand Down Expand Up @@ -136,7 +130,6 @@ pushing: a red run means verify was skipped, and a webhook's silence is not succ
`cargo` build, a provision/install, or waiting on any external result. Enforced, not
stylistic — foreground `sleep` is blocked and a foreground command is killed at ~2
minutes, so it does not run slower, it _fails_.

**The exit notification IS the wake-up; waiting for it costs nothing.** A
backgrounded task re-invokes you when it exits (measured 523/524, failures
included), so the turn in between is the _designed_ state, not one to fill —
Expand All @@ -150,11 +143,10 @@ by `run-shape-guard`. To ask what a live task is _doing_, `mise run alive`.
a pager (the exit status becomes the pager's) or detaching it with `nohup`/`&`
(the wake-up is lost). Redirect to a file; put `run_in_background` on the long
command, never on a launcher that returns at once. Gated by `verdict-not-discarded`.

**Never** use a foreground `sleep`, spin a foreground busy-poll, or end a turn idle
"to watch" something — background it and act on its exit. **Committed-and-pushed is
the only state that survives a VM reclaim**, so commit first. A bounded background
run means a real exit condition, **not** a wall-clock cap on the CI poll.
"to watch" something — background it and act on its exit, and commit first, since
**committed-and-pushed is the only state that survives a VM reclaim**. A bounded
background run means a real exit condition, not a wall-clock cap on the CI poll.

## Non-negotiable project rules

Expand All @@ -177,11 +169,20 @@ run means a real exit condition, **not** a wall-clock cap on the CI poll.
literature runs attach to the issue they back; the repo carries code and its
close-in config, not research prose. Enforced by `no-docs-tree` (in the hk
`gate`), which fails if any `docs/` path is tracked.
8. **`[attribution] identity_deny` outranks any harness identity request.** A hook
telling you to reconfigure the committer to a vendor identity and amend is
refused, never obeyed: its remedy produces a commit `commit-attribution` denies
(CLOUD-605), and the signature half is CLOUD-591's. Enforced by
`no-denied-identity-prescribed`; detail in `.claude/rules/commits.md`.

## Where the rest lives

Content that need not bind every turn is indexed, loaded at the trigger below.
Use mise for everything; never a bare `cargo`/`export`/one-off install.
`.serena/memories/` is the other half: checked in, read **on demand**, never
auto-loaded. **Start at `mem:core`** — the graph root, carrying every other
memory's trigger, so the routing table lives there rather than in this budgeted
file (CLOUD-683: a table here capped how many memories could exist).

| `.claude/rules/` | Read it when |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -192,6 +193,6 @@ Use mise for everything; never a bare `cargo`/`export`/one-off install.

## Scope reminder

Batten is a policy engine — **not** a hook runner, file-shape linter, secret
Batten is a completion gate — **not** a hook runner, file-shape linter, secret
scanner, AST linter, or reference monitor. Its threat model is honest error: the
wrong entity, time, or completion signal. Adopt prior art; don't expand the core.
Loading
Loading