Verify skills, feature maps, and behaviour checks
A feature map is a list of what a product can do, kept in the repository, with a behaviour check for each capability.
A behaviour check drives the product the way a person or a client would: a UI flow, a CLI command, an API call, or a
library call. This page explains how aayu verify reads the maps, compares them with the source, runs the checks in
the merge gate with no model, and hands map upkeep to an agent app. It also covers the red-on-base proof for Bug
stories. It is for engineers who write a map, debug a failing check, or change this code.
A failed behaviour check blocks at once. Drift in the map is a warning until drift_blocking_from, then it blocks.
How it works
The verify skill and its map
Each product owns a project skill at .agents/skills/verify-<product>/. It holds three things: SKILL.md, a control
tool, and a features/ directory. The skill can sit anywhere in the repository, so a monorepo has one per product.
verify finds maps with git ls-files --cached --others --exclude-standard. A map you are still writing is read
before you commit it, as long as git does not ignore it.
features/README.md holds one map block and the ordered index. The map block names the product, the control
tool's argv (run from the repository root), and surface rules. A surface is something a user can reach: a route, a
command, a flag, an exported function. Each rule has a kind, file globs, and a regular expression whose first group
is one surface. The index is the list of links to feature files.
Each other features/<id>.md is one capability. It has an H1, a paragraph, one feature block (id, layers,
sources, surfaces, verified_at), and four H2s in this order: Sub-features, How to reach it,
Behaviour checks, Gotchas. Under Behaviour checks, each check block has an id, a kind (ui, cli, api,
or lib), an evidence kind from the Completion Goal schema, steps, and an optional requires. The blocks are
strict JSON: an unknown field is a contract error.
aayu init --verify-skill writes a starter skill. It picks the control tool by the stack the repository has:
go.mod gives a Go tool, else package.json gives a Node and Playwright tool, else a shell stub that blocks until
you replace it. The starter map has no features and passes aayu verify --map. Init also asks which agent maintains
the map and offers only the agents it detects. The skill has one copy, in .agents/skills/, which Amp, Cursor, and
Codex read directly. For Claude Code, aayu install writes a pointer command for each root verify-* skill,
.claude/commands/aayudha/verify-<product>.md, so it runs as /aayudha:verify-<product>. The pointer carries the
skill's description and sends Claude Code to the skill's SKILL.md, so it follows the skill without a re-sync;
doctor reports a pointer whose description fell behind, and doctor --fix updates it. Cursor does not read that
folder, so it lists each verify skill once.
The drift check
aayu verify --map reads the maps and the checkout and runs no product code. It reports drift:
- contract errors (a block that does not parse, a missing H2, a check with no steps);
- feature files missing from the index, and index entries with no file;
- sources that match no file;
- handles absent from the feature's sources;
- surfaces the rules find that no feature lists, and listed surfaces the rules no longer find;
- evidence maps (
.aayudha/evidence/<story>.toml) that name a check the map lacks.
A handle is a name, label, within, or test value of a step's action object, such as {"click": {"name": "Ask"}}. The source must contain it quoted or as element text (>Ask<); a test handle, as a whole word. Values deeper in a step, such as a request body's "name", are data, not handles.
A feature is stale when its sources changed since its verified_at commit. Stale is reported, never counted as
drift, including "never verified" (empty verified_at) and a commit missing from a shallow clone. verify --map
exits 3 on drift and 0 otherwise.
The feature-map floor runs the same check in every verify run, including the merge gate on every pull request. It
is skipped when the checkout has no map. Its value is the drift count, and it writes reports/feature-map.json
into the pack. Before [verify] drift_blocking_from, drift passes with a detail that starts "advisory until". From
00:00 UTC on that date, drift fails the floor and the gate. A pull request that serves no story still runs this floor.
Behaviour checks
The behaviour floor picks the checks to run. The common case is a pull request that edits product code:
- every check of a feature whose sources or feature file the change touches;
- every check of a map whose README or control tool changed (a
SKILL.mdedit alone does not count); - every check a criterion names in the story's evidence map, as
checks = ["<feature>.<check>"]besidetests.
aayu verify --check <feature>, <feature>.<check>, or all runs a chosen set instead. Outside the merge gate that
run is a preview. In the gate, a pull request that serves no story runs only the two map floors.
For each check, aayu runs <control...> check in the verify sandbox, sends the check as JSON on stdin, and reads one
JSON result from stdout. A ui or api check needs the running app. aayu first refuses a production target, then
starts the app with [verify] seed, and lets those checks use the network. cli and lib checks run with no
network. Each check has three minutes.
The control tool answers passed, failed, or unreachable:
- failed fails the floor, so the gate turns red at once.
- unreachable means a precondition in
requiresis missing. It proves nothing and fails nothing. - no valid answer (malformed JSON, an unknown status, an artifact path outside the output directory, or an unknown artifact kind) blocks the floor.
A criterion mapped to checks is evidenced only when every one of them passed and produced an artifact of the
criterion's own kind. A cli or lib transcript uses the api-trace kind. aayu hashes every artifact into the pack
under behaviour/<feature>.<check>/ and writes reports/behaviour.json, which lists each check, why it was selected,
how it ran on the network, and its result.
Honest status: this repository has no feature map for aayudha itself. The two maps in it belong to the example products:
benefits-qa(sevenuiand threeapichecks) andnotes(fiveclichecks and onelibcheck). No evidence map under.aayudha/evidence/names a behaviour check yet, so no criterion here is evidenced by one. In the first live gate run,product-events.visit-and-askansweredunreachable, becauseserve.shstarts no events sink.
Keeping the map honest
No model key is ever put into a product repository's CI. The model-driven maintain pass runs in an agent app the user
installed instead. aayu verify --maintain drift or --maintain always works with --map or --check and posts the
maintain brief. The brief lists the drift, the failing checks, the edit scope (the skill directories only), and the
rules: never weaken a check, file a product regression as a Bug, and open at most one pull request.
The brief is a new comment on one tracking issue marked <!-- aayudha:verify-maintain -->, because GitHub notifies
a mention only in a new comment. In drift mode, aayu posts nothing when nothing drifted and no check failed, or when
the last brief carries the same digest of its report. always posts every time.
Who posts it matters, because the apps answer only the people who connected them:
- Outside CI, the brief is posted under the person's account and starts with the mention of
[verify] maintainer(@claude,@codex, or@cursor). Withamp, or with no maintainer set, there is no mention, and the Amp loop thread follows the brief. - In Actions, aayu posts a record instead: no mention, and the text "Recorded by the Actions loop; no agent was woken". Records are de-duplicated against records only, so a nightly record never stops the person's own run from handing off the same drift.
Two loops run this. verify-sweep runs verify --check all nightly at 02:30. It needs no model, so it is an
agent = false loop that runs in the free GitHub Actions fallback, and its run is red when a check fails (Appendix B
D-7.1f). verify-maintain runs verify --map --maintain always on Mondays at 09:00 in your agent tool, so the app
also looks for capabilities the map misses. Drift between those runs shows on every pull request, where the gate runs
verify --map (loops/README.md).
Map directories are not protected paths. inspect.Weakened flags a pull request that deletes a feature file or leaves
it with fewer check blocks or assertion keys (expect, stdout, exit, test, and others). A renamed handle is
not a weakening.
Honest status:
doctordoes not report map drift; onlyverify --mapand the floor do. An Actions loop never wakes an app, so the hand-off depends on the person's scheduled run.installwrites no Claude Code pointer for a nested map.
Red-on-base proof for Bug stories
A test that proves a bug fix must fail on the code before the fix. In the merge gate, when the story carries the
type:bug label and the full floor set runs, verify adds a red-on-base check:
- It reads the Go tests the story's evidence map lists under
tests. - It checks out the merge base in a temporary worktree, with the pull request's changed test files on top.
- It runs
go test -json -count=1 -run <mapped tests> ./...there, in the verify sandbox. The changed test files are every*_test.goandtestdatafile in the diff.
Each criterion is then red on base (a mapped test failed), green on base (every mapped test passed), or inconclusive
(a mapped test did not run, for example because it calls an API that does not exist on the base). Inconclusive never
blocks. The check passes when no criterion is green on base. A green-on-base criterion makes the check skipped,
with a detail that starts "advisory until", before [verify] red_on_base_blocking_from. From that date it fails. An
empty date keeps it advisory.
Honest status: red-on-base runs only in CI on a pull request, only for a Go module, and only for Go tests mapped by name; behaviour checks are not rerun on the base. Both dates are
2026-11-13, so both are advisory today.
Key data and file formats
The map block of the notes example, from
examples/notes/.agents/skills/verify-notes/features/README.md:
{"product": "notes",
"control": ["go", "run", "./examples/notes/.agents/skills/verify-notes/control"],
"surfaces": [{"kind": "command", "files": ["examples/notes/cmd/notes/main.go"], "pattern": "case \"([a-z]+)\":"},
{"kind": "flag", "files": ["examples/notes/cmd/notes/main.go"], "pattern": "fs\\.\\w+\\(\"([a-z-]+)\""},
{"kind": "func", "files": ["examples/notes/store/store.go"], "pattern": "(?m)^func ([A-Z]\\w*)\\("}]}
The feature block and one check block of
examples/notes/.agents/skills/verify-notes/features/mark-done.md:
{"id": "mark-done", "layers": ["L02"],
"sources": ["examples/notes/cmd/notes/main.go", "examples/notes/store/store.go"],
"surfaces": {"command": ["done"]},
"verified_at": "c514f9a54b6f60eedc10f619292e093ce314007d"}
{"id": "missing", "kind": "cli", "evidence": "api-trace", "steps": [
{"build": "./examples/notes/cmd/notes"},
{"run": ["notes", "add", "buy milk"], "exit": 0},
{"run": ["notes", "done", "5"], "exit": 1, "stderr": ["no such note: 5 (there are 1)"]},
{"run": ["notes", "list"], "exit": 0, "stdout": ["1 [ ] buy milk"]}
]}
The control tool contract, from the package comment in internal/verify/behaviour.go (its tabs shown as spaces):
// <control...> check stdin: {"product", "feature", "check", "out", "base_url", "root"}
// stdout: {"status": "passed"|"failed"|"unreachable", "reason",
// "artifacts": [{"path", "kind"}]}
The two dated keys in internal/config/defaults.toml, trimmed to the keys:
[verify]
red_on_base_blocking_from = "2026-11-13"
drift_blocking_from = "2026-11-13"
This repository's own settings, from .aayudha/config.toml:
[verify]
seed = ["examples/benefits-qa/deploy/serve.sh"]
# The feature maps' maintain pass (D-4.5b.8): a headless Amp run on the founder's side (`amp -x`, loops/README.md)
# follows the brief; Amp has no @-mention.
maintainer = "amp"
Invariants
verify --mapreports every kind of drift and exits 3 on drift, 0 when in sync or with no map. Enforced byCheckMapsin internal/verify/featuremap.go andverifyMapin internal/cli/verify.go,TestMapDriftandTestVerifyMapExitCodes.- A stale feature is never drift. Enforced by
staleandMapDrift.Items,TestMapStaleOnly. - Drift passes as advisory until 23:59 UTC the day before
drift_blocking_fromand fails the floor from 00:00 on that day. Enforced byMapFloor.Run,TestMapFloor. - A failed check fails the floor, a control tool error blocks it, an unreachable check proves nothing, a criterion
is evidenced only by an artifact of its own kind, and only
uiandapichecks get the network. Enforced byBehaviour.RunandrunCheckin internal/verify/behaviour.go,TestBehaviourFloor. - An artifact path outside the check's output directory blocks the floor. Enforced by
runCheck,TestBehaviourArtifactEscape. - A
uiorapicheck never runs against production. Enforced byResolveTargetinBehaviour.Run,TestBehaviourRefusesProduction. - The maintain brief goes to one tracking issue, is not posted twice for the same report in
driftmode, and an Actions run posts a record with no mention. Enforced bymaintainHandoffin internal/cli/verifymaintain.go andBrief.Commentin internal/verify/maintain.go,TestVerifyMaintainHandoff. - A pull request that drops a check, drops assertions, or deletes a feature file is flagged as weakening. Enforced by
Weakenedin internal/inspect/coverage.go,TestWeakenedChecks. - A Bug story's proving test that passes on the base makes
red-on-baseadvisory before the date and a failure from it; inconclusive never blocks; no date stays advisory. Enforced byredOnBasein internal/cli/redbase.go,TestRedOnBase. init --verify-skillwrites a map that passesverify --mapand offers only detected maintainers;installgives Claude Code a pointer command to it and Cursor a single copy. Enforced in internal/initrepo/verifyskill.go andProductPointerin internal/hosts/install.go,TestInitVerifySkill,TestInitVerifySkillMaintainer,TestInstallMirrorsVerifySkill, andTestInstallGivesCursorOneCopy.
Where it lives in the code
- internal/verify/featuremap.go: map reading, the drift check, the
feature-mapfloor. - internal/verify/behaviour.go: check selection, the control tool contract, the
behaviourfloor,MapFloors. - internal/verify/maintain.go and internal/cli/verifymaintain.go: the maintain brief and hand-off.
- internal/cli/verify.go:
--map,--check,--maintain, and the no-story gate. - internal/cli/redbase.go: the red-on-base check.
- internal/initrepo/verifyskill.go and hosts/verify-skill: the starter skill and control tool templates.
- skills/verify/references: the create and maintain procedures.
- loops/verify-sweep.toml and loops/verify-maintain.toml: the two loops.
Honest status: the plan for WP4.5b gives
--verify-skilla kind, hasdoctorreport drift, and has Actions mention the app. The code, recorded in Appendix B of the build plan, does none of these. This page follows the code.
Read next
- Floors in detail: the other floors
verifyruns beside these two. - The merge gate: where the checks run on every pull request.
- Proof packs: where
reports/behaviour.jsonand the check artifacts are kept. - Prove behaviour: the user guide for writing a map and its checks.