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
29 changes: 27 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ The fleet's checks and review tooling, run by hand, with the deterministic ones

**Hosted here and reached, never carried.** These are not declared in [`spec/files.json`][files], so the audit does not expect a downstream repo to ship them, the same footing as `spec/audit.py`. That is the fleet model rather than an omission: a gate holding no per-repo content has one canonical implementation for the fleet. The prose and repository gate implementations live beside their composite actions under `.github/actions/`, which lets reusable workflows reach the implementation at their pinned hub commit through `$/.github/actions/` without checking out the hub. The same-named files in `scripts/` are thin local entry points that call those action-owned implementations, so maintainers and hooks retain the established commands from a hub checkout without creating a second copy of either gate. A repository that cannot reach the hub reports a local check as not run rather than reconstructing it, since a rebuilt gate encodes its author's reading of the rule and agrees with no other repository.

Python only, standard library only, no third-party packages. Every check script is read-only and exits non-zero on a finding. `build_dist.py` and `skills_install.py` below are the two exceptions, since a generator and an installer both exist to write, and each still offers a read-only mode (`--check`, `--report`) for CI and for asking without changing anything.
Python only, standard library only, no third-party packages. Every check script is read-only and exits non-zero on a finding. `build_dist.py`, `local_review.py`, and `skills_install.py` below are the exceptions, since a generator, a recorder, and an installer all exist to write. `build_dist.py --check` and `skills_install.py --report` are read-only modes for CI and for asking without changing anything. `local_review.py status` and `check` answer without recording anything, but they are not read-only in that same sense: reading the working tree stages it, which writes inside `.git` and runs any `filter.*.clean` the repository configures, and only the receipt is left untouched.

The directory separates its kinds by name and by tree. A gate checks and exits non-zero on a finding, and its name carries a `_lint` or `_gate` suffix saying what it gates. The `prose_lint.py` and `repo_gate.py` entry points delegate to the action-owned implementations that gate this tree in CI, and `host_gate.py` gates the machine it runs on. A utility does work rather than gating and carries no suffix: `build_dist.py`, `pr_review.py`, `skills_install.py`. The unit tests live apart under [`scripts/tests/`][tests], one `test_<script>.py` per script, driving the canonical implementations against input they must reject. A gate nobody has watched fail is a gate nobody knows works. Where a case covers a table it reads the live table rather than restating it, and each one asserts a floor on what a healthy run reaches, since a check whose scan matches nothing reports zero findings and reads exactly like a pass.
The directory separates its kinds by name and by tree. A gate checks and exits non-zero on a finding, and its name carries a `_lint` or `_gate` suffix saying what it gates. The `prose_lint.py` and `repo_gate.py` entry points delegate to the action-owned implementations that gate this tree in CI, and `host_gate.py` gates the machine it runs on. A utility does work rather than gating and carries no suffix: `build_dist.py`, `local_review.py`, `pr_review.py`, `skills_install.py`. The unit tests live apart under [`scripts/tests/`][tests], one `test_<script>.py` per script, driving the canonical implementations against input they must reject. A gate nobody has watched fail is a gate nobody knows works. Where a case covers a table it reads the live table rather than restating it, and each one asserts a floor on what a healthy run reaches, since a check whose scan matches nothing reports zero findings and reads exactly like a pass.

```sh
python3 scripts/tests/test_prose_lint.py
python3 scripts/tests/test_repo_gate.py
python3 scripts/tests/test_pr_review.py
python3 scripts/tests/test_local_review.py
python3 scripts/tests/test_build_dist.py
python3 scripts/tests/test_skills_install.py
python3 -m unittest discover -s scripts/tests # all of them, and exits 5 if the suite vanishes
Expand Down Expand Up @@ -217,6 +218,29 @@ Undecided is a third answer here for the reason it is one in `repo_gate.py`. A r

The match is on the block's heading rather than anywhere in the body, and on the runbook's alternation rather than on one phrasing, since the wording has already appeared two ways. A case asserts the script's pattern is the one the runbook publishes rather than a copy of it that can drift. Reading the whole body was the first implementation and its own review caught it: a review whose overview prose discusses suppressed findings carries none, and reporting that as a finding trains the reader to skim the field. A heading outside any `<details>` wrapper is still read, because reporting zero when the markup moves is the same false clean one level up, and that fallback takes a count so ordinary prose does not become one.

## `local_review.py`

Records and verifies that a local review pass covered a branch's current content. [`AGENTS.md`][agents] and the `local-strict-review` Skill hold the review-before-every-push rule itself, and this gives that rule something a capture point can actually check rather than restating it. A pass records a receipt keyed on the content it reviewed, and a capture point asks whether that receipt still covers what is about to be pushed. The capture points themselves (a git pre-push hook, the Skill steps that invoke the pass) are a separate change, so this script is the capability they will call rather than a gate that is already wired up.

The key is over the content of the changed files, plus the merge-base commit that fixes what "changed" means. It is deliberately not over the diff text and not over `HEAD`, which is what lets a review run before the commit while the check runs at the push: reviewing untracked work and then committing it unchanged leaves the key identical. Each path contributes the state it is in on each side, the working tree's and the index's, both as git's own mode-and-blob identity so the two compare directly. Where they agree the path reads as one state, which is what makes `git add` and `git commit` invisible. Where they diverge, because content is staged that the working tree no longer holds, or because a file mode changed, the key moves. Reading the index rather than the working tree alone is what stops content being committed that the key never saw.

Every identity is computed by git rather than reconstructed, which matters more than it sounds: a blob id built by hashing raw working-tree bytes cannot equal the one git stores wherever a `text` attribute or any clean filter sits between them, and this repository applies one to every text file, so `git add` moved the key on any CRLF file until that was fixed. The working tree's side is therefore read by staging it into a throwaway index that git builds, with its object writes redirected into a throwaway object directory and the real one attached as an alternate. That redirection is not incidental: staging writes blobs, so without it a read would permanently deposit the content of every unignored untracked file, a stray secret among them, into the repository. Nothing about the real index, the working tree, or the object database is modified. One requirement this places on a repository is that a `clean` filter be deterministic, since the same content is staged once when a pass is recorded and again when it is checked.

What the key covers is the net content the branch introduces against its target, not the commit series that produced it. A branch that adds a file and later deletes it, or a rebase that rewrites only messages, keeps its key. That is the intended scope, since a feature branch lands as a squash and the net diff is what a reviewer reads, but a capture point built on this does not cover the commit series. The receipt lives in the worktree's own git directory rather than in the working tree, so it is per-task, can never be committed by accident, and needs no `.gitignore` entry of its own.

`record` requires `--expect-digest`, the digest `status` reported before the pass ran, and refuses when the content has moved since. It is required rather than optional because an optional guard is the one a caller in a hurry omits, and the omission looks exactly like a pass that was properly bound. Nothing else binds the moment a review finished to the moment it is recorded, so a format-on-save or a hook autofix in between would otherwise be stamped as reviewed content no reviewer saw, which is the one claim the receipt exists to make. A headless backend is recorded by running it rather than by hand, since recording it directly would bypass the completion check that makes its pass mean anything.

It holds no review logic. It drives backends: `agent-skill` is the `local-strict-review` subagent pass, which only a live agent session can run, so the engine records it rather than invoking it, and `coderabbit-cli` is headless and is the one backend a git hook can execute by itself. A CodeRabbit CLI run draws on the same hourly budget as this account's pull request reviews, so a run that is rate limited, errors, or ends without a completion event records no pass rather than a clean one. The invocation follows the CLI's documented flag semantics: `--base-commit` for the merge-base hash, since `--base` takes a branch name, and `--include-untracked`, since the CLI excludes untracked files by default while this receipt's scope includes them. A completion reporting `review_skipped` is refused too, that being the CLI saying it looked at nothing. None of it has been executed against a real CLI on this host, so the backend stays opt-in until someone runs it. A recorded pass means a review ran over exactly this content, never that the content is clean: disposing of what a review found is judgment, per the `pr-review-conduct` Skill's five outcomes, so a pass is recorded whether it raised findings or none.

```shell
python3 scripts/local_review.py status # what covers the current content. 0 either way, 2 if it cannot run
python3 scripts/local_review.py record --reviewer agent-skill --findings 3 --expect-digest <digest>
python3 scripts/local_review.py check # 0 covered, 1 not, 2 could not run
python3 scripts/local_review.py run --backend coderabbit-cli
```

The exit code is three-valued on purpose, per [`AGENTS.md`][agents] "Report an execution boundary separately from a check finding". `0` and `1` are findings a caller acts on, and `2` means the check itself never ran, whether because no git repository resolved, the target ref is absent from this checkout, the receipt could not be read off disk, or a backend binary is not installed. Any unexpected failure reports `2` as well, rather than falling through to the interpreter's own exit `1`, which a caller would read as the "not covered" verdict. `status` reports rather than gating, so it exits `0` whether or not the content is covered, and only reports `2` where it could not run at all.

## `build_dist.py`

Regenerates [`.github/skills/`][github-skills-dist] and [`.claude-plugin/fleet-skills/`][fleet-skills-dist] from [`.agents/skills/`][agents-skills], the hub's own hand-authored fleet Skills. Codex and opencode read `.agents/skills/` directly, GitHub Copilot reads `.github/skills/`, and Claude Code reads the generated plugin published through [`.claude-plugin/marketplace.json`][marketplace]. `.agents/skills/` stays the one place a skill is hand-edited. Both generated trees are never hand-edited.
Expand Down Expand Up @@ -244,6 +268,7 @@ Installs the fleet's Skills for the current machine, cross-platform and idempote

[agent-safety-install]: ../host-setup/agent-safety/claude/install.py
[agents-skills]: ../.agents/skills/README.md
[agents]: ../AGENTS.md
[audit]: ../spec/audit.py
[copilot-instructions]: ../.github/copilot-instructions.md
[editorconfig]: ../.editorconfig
Expand Down
Loading