AGENTS.md · git:20260928.bb9f385 · 2026-09-28 · sha256 8621ade90841cb85

AGENTS.md git:20260928.bb9f385A

Immutable. This exact content never changes and is served at /api/v1/blob/8621ade90841cb85.

# Working in this repository

This repository holds one guideline (`architecture.md`), a lens
catalog derived from it (`lenses/`), Claude Code skills that apply the
lenses (`skills/`), and the checkers that keep the three consistent
(`scripts/`, `Makefile`).

## Layout

- `architecture.md` is the source of truth. Every rule in a lens or a
  skill restates a sentence in it; nothing adds a rule the guideline
  does not state.
- `lenses/<group>.md` holds one group of lenses in the format
  `lenses/README.md` defines. Ids are `<PREFIX>-NN`; every lens cites
  `Section title` or `Section title, Subsection`, by title and never
  by number.
- `skills/arch-review-<group>/SKILL.md` is generated from
  `skills/_template/review.SKILL.md`; edit the template and run
  `make gen-skills`. The other skills are hand-written. The six
  scaffold skills share `skills/_shared/scaffold-conventions.md`; the
  other hand-written skills do not. `skills/_shared/ops-skills/` holds
  the fifteen project-local skills, the nine operational skills, the
  four audits, and the two optional audits, as flat templates, one
  file each, with `acme` for the product; `arch-scaffold-new` copies
  them into a new tree, and `make leaks`, `make links`, and `make lint`
  hold them. They are not skills of this plugin, but a new tree runs
  them as skills, so `scripts/check_skills.py` reads their frontmatter.
  It holds the same flat `key: value` lines a
  skill's frontmatter has. The name equals the file name. The
  description is one double-quoted string of at most 1024 characters,
  outside the plugin's description budget. `allowed-tools` is
  comma-separated, with no bare `Bash`, and each Bash entry is the
  `Bash(cmd:*)` prefix form or an exact `Bash(make <target>)`. For an
  audit it reads two things more, and holds each to Operations
  (Operational Skills). Its `## Role and credential` section opens
  with the role the section's table gives it. Its fixes rank remove,
  fold, defer, cache, and only then parallel. The script also holds
  one sentence the text states and three files restate, "a work row
  is done once its item is queued": the text, STO-20, and the two
  scaffolds that write the outbox relay each say it.
  `skills/arch-new-aspect`
  is the one skill that edits this repository itself: it incorporates
  a new aspect into the guideline and cascades it through the lenses,
  skills, and docs, and names the release level.
- `agents/arch-reviewer.md` is the subagent `arch-review-full` fans out
  to. Its procedure and report shape mirror the review template, and
  `scripts/check_agents.py` holds the two together: the four decision
  words, the report block, and the count of procedure steps must
  agree. It also holds the agent's `maxTurns`, the count of turns
  after which the host stops it, since the host accepts the agent with
  that key missing or misspelled. The sentence "Never edit, stage, or
  commit" is repeated in every review skill on purpose.
- `.claude-plugin/` holds the plugin and marketplace manifests. The
  repository root is the plugin. `plugin.json` carries the one release
  version. `scripts/check_version.py` holds every copy to it: the
  marketplace manifest, the changelog's latest release heading, the
  pinned tags in `README.md`, `docs/adopting.md`, and
  `checkers/README.md`, and the arch-check package's version in
  `checkers/pyproject.toml` and `checkers/src/arch_check/__init__.py`.
- `benchmark/` holds the harness that measures a subject against a
  rubric (`.github/workflows/benchmark.yml` runs every scenario that
  lists the container runtime on demand, never on push): `run.py` with
  its inline dependencies, the `harness/` modules, the scenarios, the
  result schema, and `serve.py`. A scenario lists the runtimes it runs
  on, and `run.py` refuses any other. Every harness module
  imports the standard library only at import time, so the tests at
  `tests/test_benchmark_*.py` run with nothing installed; the provider
  clients, `pyyaml`, `jsonschema`, and `websockets` are imported inside
  the functions that use them. A run writes into `benchmark/runs/`,
  and a run folder is checked in once `run.py redact` has scanned it
  for keys. The pull request that adds one adds its row to
  `benchmark/runs/README.md`, the index, by hand, and
  `scripts/check_runs.py` holds every run folder to one row, to a
  runtime its scenario lists, to no rehearsal, to no key-shaped string
  inside a compressed file it holds, and to no `.git`. It reads the scenarios and
  scans the compressed files through the harness, so `make runs` brings
  `pyyaml`.
  `make benchmark` runs the smoke scenario in a container, its image
  built first, and is not part of `make check`, because a run calls
  paid APIs.
- `checkers/` holds `arch-check`, the static checker that decides the
  mechanical lenses. It is its own Python package
  (`checkers/src/arch_check/`, Python 3.11, standard library only).
  A rule's id is the id of the lens it decides, and a rule is one
  module under `rules/`; `checkers/README.md` says how to add one.
  `checkers/src/arch_check/lenses.py` carries every lens id and
  severity, and `tests/test_arch_check_framework.py` holds it equal
  to `lenses/*.md`. The tests are `tests/test_arch_check_*.py`, on
  trees built with `tests/arch_check_fixtures.py`; they skip on
  Python 3.10.
- `scripts/_common.py` holds what the scripts share, the heading
  anchor rule above all: the generator that writes anchors and the
  checker that resolves them use the same function. It also holds the
  one list of the repository's Markdown (`markdown_files`), which
  every script that scans Markdown reads, and the argument parser
  every script uses, and the one fence rule
  (`fenced_lines`) every script that skips code blocks uses.
- `scripts/check_lenses.py` also holds every identifier a lens quotes
  to the section the lens cites: a backticked name the guideline uses
  must appear in that section. `CROSS_REFERENCES` lists the few
  deliberate exceptions. `tests/` holds one
  pytest module per script, each on a small fixture tree, with a pass
  and a fail path per rule; `make test` runs them.

## Invariants

- No product or hardware vocabulary in the guideline, the lenses, the
  skills, the docs, the agents, the `.github/` templates, or this file
  (`scripts/check_leaks.py` lists the terms). The product list is a regression guard for the
  vocabulary of the one origin the guideline was extracted from, not a
  general check: it catches that vocabulary flowing back in, and a
  fork replaces it with its own. Agents are named as agents.
  `scripts/check_leaks.py` also reads the YAML and JSON under
  `.github/`, `.claude-plugin/`, `skills/`, `agents/`, and `benchmark/`,
  and the docstrings under `scripts/`, `checkers/`, and `benchmark/`.
- No history in the guideline: it states what we do, in the present
  tense, with no changelog phrasing and no survey of the alternatives
  weighed. Naming the near miss a rule rules out ("X, never Y") is
  part of the rule and stays.
- No section numbers anywhere: headings are unnumbered, and every
  cross-reference (in the guideline, the lenses, the skills, the docs)
  names the section by title; inside the guideline it is a named
  anchor link. Numbers shift when a section is inserted; titles do not.
- The guideline's Contents block is generated (`make gen-toc`) and
  checked (`make toc`).
- Every lens cites a section and subsection that exist.
- Every skill's `name` equals its folder name and starts with `arch-`;
  every `${CLAUDE_SKILL_DIR}/...` reference resolves; frontmatter is
  flat `key: value` lines; descriptions are one complete double-quoted
  string; `allowed-tools` is comma-separated and never empty, in every
  skill and ops-skill template. `scripts/check_skills.py`
  refuses a bare `Bash`, a trailing space inside the parentheses, and
  the `Bash(cmd *)` spelling; it accepts both the prefix form and an
  exact `Bash(make <target>)`: the exact form for a Makefile target,
  the prefix form `Bash(cmd:*)` for a tool that takes arguments.
  `allowed-tools` names only what the body runs, and
  `scripts/check_skills.py` holds the make targets to that: every
  `Bash(make <target>)` has `make <target>` in a backticked span of the
  body, or of `skills/_shared/scaffold-conventions.md` when the body
  references it. A description in prose is not a run. The git, uv, and
  pnpm entries are held to it by hand. `Bash(uv run:*)` and
  `Bash(pnpm run:*)` are a shell in practice: either runs whatever the
  workspace holds. They stay listed because a scaffold has to run the
  project's own tools through the workspace, and naming them says so
  in the frontmatter instead of hiding it behind a bare `Bash`.
  `Bash(python3:*)` in the review skills is the same kind of entry: it
  is there to run `arch-check`, and it could run anything, so a review
  skill's promise never to edit rests on its text, not its tools.
- Scaffold skills have the five sections Input, Created, Changed,
  Procedure, Output, in that order (`scripts/check_skills.py` holds
  them to it).
- A skill keeps its spine and names its detail. An invariant that must
  never be missed stays inline in the skill body, because a referenced
  file is a promise and an inlined line is a guarantee; long reference
  material moves into `skills/<name>/references/<file>.md` and is named
  by the step that reads it, which reads it when that step runs and not
  before. `scripts/check_skills.py` holds both ends: a reference file no
  step names is an orphan and an error, and a skill body past the word
  bound is an error whose fix is to move reference material into a
  step's file.
- A step that fixes and runs again states its count bound. A skill
  run by a strong model fixes and reruns until something stops it,
  and with no count only its session's turns or wall time would. For
  a gate the wording is one: the first run plus at most 3 reruns,
  then stop and say which gate fails and why. A step with a smaller
  unit (a dependency row, an audit's own flows file) states its own
  count in the same shape and says what happens past it.
  `scripts/check_skills.py` holds a paragraph or list item under
  `skills/` that says fix beside a rerun, or beside a `make <target>`
  it runs, to an `at most <n> reruns` in the same paragraph or item.
  A rerun is `rerun`, or `run` with `again` anywhere after it: the
  span is the whole paragraph or item, so a block that says fix, run,
  and again without a loop is reworded so it does not. It pairs a fix
  and a rerun only inside one paragraph or item, so a fix in a step
  with its rerun in a nested bullet is not paired. That loop, and a
  loop said in other words, is held by hand.
- The release version is written once, in `.claude-plugin/plugin.json`;
  every other copy is checked against it.
- Scaffold skills share `skills/_shared/scaffold-conventions.md`.
- Exactly one review skill per lens group; `arch-review-full` names all
  of them.

## Validate

```bash
make check                       # everything CI runs
make checkers-dist               # builds the arch-check wheel and runs its entry point
claude plugin validate . --strict   # manifests, skills, agents (when claude is installed)
```

CI runs `make test` and `make checkers-dist` on Python 3.11,
arch-check's floor. `.github/pins/` holds every tool version
(`requirements.txt` for uv and the Python tools, `package.json` for the
npm tools); the Makefile and the workflows read them there, and
dependabot updates them.

## Conventions

- Readability outranks density. A sentence a senior reader has to
  parse twice has failed, however well it is built.
- Short sentences, one idea each. A chain of clauses joined by commas
  is the thing to break up.
- A paragraph has no cap and an em-dash is allowed. Paragraph length
  is not the problem; sentence length is.
- A longer document is the right trade for a document that gets read.
- Wrap prose at about 72 columns in the guideline and the lenses.
- A concept the guideline uses before the section that defines it
  carries a named anchor link to that section at its first mention.
- A code snippet that shows a root with "one getter per X" shows two
  getters and a `# ...` line, so the pattern reads at a glance.
- When a fix is applied to one instance, search the repository for
  its siblings and fix them in the same change.
- When the guideline renames an identifier or changes a shape, grep
  every scaffold skill and `agents/` for the old spelling and fix them
  in the same change; no checker does this yet.
- Commit messages: a specific subject line, a short body naming the
  rule that changed and why.
- A change that removes or reverses a rule is a major release; one
  that adds or sharpens a rule is a minor release; before 1.0.0 a
  removed or reversed rule bumps the minor number, as semver reads
  0.x, and the changelog entry names the reversal (`CONTRIBUTING.md`,
  Versioning).
- A change never edits `CHANGELOG.md`. The changelog is written once
  per release, in the release pull request, from the squash commits
  since the last tag. A pull request that edited it put every other
  open pull request in conflict. So the pull request description
  carries what the release section needs: what changed, in the
  guideline's voice, the level, and a reversal named as one.
  `scripts/check_changelog.py`, run by `.github/workflows/changelog.yml`,
  refuses a pull request that edits `CHANGELOG.md` unless its title is
  `Release X.Y.Z: ...` or its branch is `release-X-Y-Z`.