Skip to content

feat: add agents-md, the AGENTS.md validation tool - #3

Draft
tonyandrewmeyer wants to merge 6 commits into
canonical:mainfrom
tonyandrewmeyer:feat/agents-md
Draft

feat: add agents-md, the AGENTS.md validation tool#3
tonyandrewmeyer wants to merge 6 commits into
canonical:mainfrom
tonyandrewmeyer:feat/agents-md

Conversation

@tonyandrewmeyer

Copy link
Copy Markdown
Collaborator

Every Charm Tech repo carries an AGENTS.md, several agents read it, and a stale line in one is worse than a missing line: agents trust the file over the repo, so a wrong line produces confident errors where absence would have produced exploration. This package is the deterministic half of the scheme for keeping them honest : three checks and one fix, plus the ten per-repo questions the check reads.

The intention is that this runs monthly across the repos and on every PR that touches an AGENTS.md.

(Some of the top-level files here are duplicates of #1, whichever lands first will trigger the removal in the other via a rebase.)

tonyandrewmeyer and others added 5 commits September 8, 2026 18:55
Every Charm Tech repo carries an AGENTS.md, several agents read it, and a
stale line in one is worse than a missing line: agents trust the file over
the repo, so a wrong line produces confident errors where absence would
have produced exploration. This package is the deterministic half of the
scheme for keeping them honest -- three checks and one fix, plus the ten
per-repo question batteries the battery check reads.

The checks were written as part of the charm-tech-baseline audit tool and
would otherwise ship with it. They are separated here because the two have
different consumers and very different cadences: the baseline audit runs
against a repo when someone asks it to, while this runs monthly across the
estate and on every PR that touches an AGENTS.md. Splitting them means the
monthly routine can pin a package that is only these checks, and a change
to either does not force a re-review of the other.

The batteries move with the checks rather than staying with the skill, so
that the code that reads them and the data it reads ship together.

common.py and tier.py are the same as their charm-tech-baseline
counterparts. That duplication is deliberate for now: the alternative is a
third package for 150 lines of exit codes and a JSON emitter, and a forced
release order between the two. Worth revisiting if a third tool wants them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdsdR8QdcZY6GUJgd1Wt7c
Matches the root README and ai-failure-notifier's, which are both soft
wrapped. GitHub wraps to the reader's window, so the hard wraps only made
the paragraphs ragged there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdsdR8QdcZY6GUJgd1Wt7c
Four conventions settled on canonical#1 that apply here too, so that the second
package in the repo does not arrive with a different set.

* Apache licence header on every Python file, which none of these had.
* Modules are private (`_cli`, `_common`, `_tier`, `_checks`, `_fixes`), so
  it stays hard to depend on tool internals from a CI hack later.
* `__init__.py` is a docstring and nothing else -- the tool is a console
  script, not something to import -- with the entry point moved to `._cli`.
* `[tool.ruff] extend = "../pyproject.toml"` rather than a comment asking
  people not to add a `[tool.ruff]` block, since extending means a setting
  added here overrides one key instead of the whole shared config.

The modules inside `_checks` and `_fixes` keep their public-looking names:
the runner derives check and fix identifiers from them, so `add_agents_md`
is the name of the fix on the command line rather than an importable API.

Also drops two comments explaining what a check used to do when it was a
standalone script, which is the same leftover-from-one-file class that was
picked up on canonical#1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdsdR8QdcZY6GUJgd1Wt7c
All three checks declared `product,canonical,personal`, so the tier gate
never excluded anything -- it computed a value and then applied it to
nothing. That is not an accident of this extraction: a well-maintained
AGENTS.md is worth the same in a personal fork as in a product repo, which
is not true of the requirements the tiers were built for (SBOM submission,
TICS targets, security documentation). The baseline audit needs tiers; this does
not have to inherit them.

Removing it takes a network round trip out of every run: on a fork, tier
detection shelled out to `gh repo view` to find the upstream, for a check
that is meant to run in CI on every PR touching the file.

Goes with it: `--tier=`, the `detect-tier` command, `parse_tier` and
`tier_applies`, and the `tier`/`tier_source` fields in the report. Anything
reading the JSON should not miss them, since they never varied by anything
except the flag it passed in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdsdR8QdcZY6GUJgd1Wt7c
Follows the team Python style guide in canonical/charm-tech
(style/python.md): a name prefixed with its module says where it came from.
So `import pathlib` and `pathlib.Path`, `import types` and
`types.ModuleType`, and `from .. import _common` with every use prefixed,
rather than pulling the names in one at a time.

`typing.Any` and `collections.abc.Iterator` stay as they are, under the
guide's exception for typing names -- `Iterator` is only ever an
annotation here, and the same verbosity argument applies to it.

Worth noting for the review: `run` is a common enough word that prefixing
its uses also caught it inside two regexes and a summary string, where
`go run` and `make run` are side-effect keywords. Those are back as they
were. The checks report the same findings against concierge and operator
clones as they did before this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdsdR8QdcZY6GUJgd1Wt7c
Nothing in a public repo should point at a path only some of us can read.

* `seeded_from:` in all ten batteries named a file in that tree and
  nothing else, so the field goes rather than gets reworded. `seeded_on`
  and the AGENTS.md ref/SHA it was seeded against stay, since those are
  the provenance a reader can actually check.
* The "Layer 1/2/3" numbering came from that document too, and without it
  the numbers say nothing. Each is now named for what it does, which
  mostly means saying `agents-md-content` where the text said Layer 1.
* Two docstrings pointed at `lib/common.py` and `check.py`, which is the
  layout this code had two moves ago.

Ten batteries still parse, entry counts unchanged, and the checks report
the same findings against concierge and operator clones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdsdR8QdcZY6GUJgd1Wt7c
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