Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
# One entry per tool. Add a directory here when you add a package.
package: [ai-failure-notifier]
package: [agents-md, ai-failure-notifier]
python-version: ['3.10', '3.12', '3.14']
defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Each tool is its own package in its own top-level directory, with its own `pypro

| directory | what it does |
|---|---|
| [`agents-md`](agents-md) | Checks that a repository's `AGENTS.md` is current and load-bearing. |
| [`ai-failure-notifier`](ai-failure-notifier) | Triages and enriches the issue opened when a scheduled workflow fails. |

Code here is consumed by workflow YAML in the repository that runs it, pinned by commit SHA:
Expand Down
25 changes: 25 additions & 0 deletions agents-md/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# agents-md

Keeps the `AGENTS.md` files across the Charm Tech estate current and load-bearing. This is the deterministic half of the validation: a line in `AGENTS.md` earns its place either as an *override* (the agent would confidently do the wrong thing without it) or as a *cache* (the agent would get there eventually, by reading the Makefile, tox config and CI every session). A stale line is worse than a missing one, because agents trust the file over the repo.

## Checks

| ID | What it does |
|---|---|
| `agents-md` | The file exists, and is a pointer rather than an encyclopaedia. |
| `agents-md-content` | Staleness. Extracts every command, path, symbol and tool version, runs or resolves each against the repo, and reports what no longer exists. |
| `agents-md-battery` | Runs the repo's question battery: question, checkable answer, source line. Tests whether the file changes what an agent does, rather than whether it conforms to a style. |

`agents-md fix add-agents-md` writes the template into a repo that has none.

## Use

```
uvx --from charm-tech-code-agents-md agents-md check
uvx --from charm-tech-code-agents-md agents-md check --only=agents-md-content --format=markdown
uvx --from charm-tech-code-agents-md agents-md list
```

## Question batteries

`assets/question-batteries/*.yaml`, one per repo, keyed by upstream name. They live here rather than in the skill so that the check and the data it reads ship together. Each entry carries the question, the answer that counts as correct, and the line of `AGENTS.md` it came from, so a battery failure points at the line to fix.
34 changes: 34 additions & 0 deletions agents-md/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[project]
name = "charm-tech-code-agents-md"
version = "0.1.0"
description = "Check that a repository's AGENTS.md is current and load-bearing."
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name = "The Charm Tech team at Canonical Ltd."},
]
license = "Apache-2.0"
# PyYAML only, for the question batteries. Everything else is stdlib, and
# `gh` and `git` are called as subprocesses rather than through a library.
dependencies = ["pyyaml"]

[project.scripts]
agents-md = "charm_tech_code.agents_md._cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/charm_tech_code"]

[dependency-groups]
unit = ["pytest"]

[tool.pytest.ini_options]
testpaths = ["tests"]

# The real ruff configuration is at the root of the monorepo; extending it
# means a setting added here overrides one key rather than the whole config.
[tool.ruff]
extend = "../pyproject.toml"
23 changes: 23 additions & 0 deletions agents-md/src/charm_tech_code/agents_md/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2026 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Keep the estate's AGENTS.md files honest.

Three checks and one fix, plus the per-repo question batteries they read. A
line in AGENTS.md earns its place either as an override (the agent would
confidently do the wrong thing without it) or as a cache (the agent would get
there eventually, by reading the Makefile, tox config and CI every session).
The checks here test both, and the agent-facing half lives in the
`charm-tech-baseline` skill in `canonical/charm-tech`.
"""
13 changes: 13 additions & 0 deletions agents-md/src/charm_tech_code/agents_md/_checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2026 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
76 changes: 76 additions & 0 deletions agents-md/src/charm_tech_code/agents_md/_checks/agents_md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2026 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Check: AGENTS.md present.

Convention: keep it minimal — a short pointer file, not an encyclopaedia.
"""

from __future__ import annotations

import pathlib
import sys

from .. import _common

CHECK_ID = 'agents-md'


def main() -> int:
_common.cd_repo_root()

p = pathlib.Path('AGENTS.md')
if p.is_file():
lines = p.read_text().count('\n')
if lines > 200:
_common.emit_check(
CHECK_ID,
'fail',
f"AGENTS.md present but at {lines} lines is well past the 'keep it minimal' "
f'convention.',
{'path': 'AGENTS.md', 'lines': lines},
{
'kind': 'judgement',
'human_review': (
'Trim AGENTS.md down — point at HACKING/CONTRIBUTING for depth; keep '
'AGENTS.md to setup commands and conventions only.'
),
},
)
return _common.EXIT_FAIL
_common.emit_check(
CHECK_ID,
'pass',
f'AGENTS.md present ({lines} lines).',
{'path': 'AGENTS.md', 'lines': lines},
)
return _common.EXIT_PASS

_common.emit_check(
CHECK_ID,
'fail',
'No AGENTS.md found.',
{},
{
'kind': 'mechanical',
'script': 'scripts/fixes/add-agents-md.py',
'human_review': 'Customise the dev-setup commands for this repo (uv / go / make / '
'just).',
},
)
return _common.EXIT_FAIL


if __name__ == '__main__':
sys.exit(main())
Loading
Loading