"Porque mis armas son los pinceles y mis municiones son las pinturas" — Peter de los Polvorines
Behavioral verification layer for the charly AI development ecosystem.
cargo install espectacularInstalls the ah binary.
brew tap charly-vibes/charly
brew install ahscoop bucket add charly https://github.com/charly-vibes/scoop-charly.git
scoop install ahgit clone https://github.com/charly-vibes/espectacular
cd espectacular
cargo build --releaseThis repo is configured to use:
wai— project context, reasoning, handoffsbd(beads) — issue tracking and dependenciesopenspec— specs and change proposalsdont— epistemic claim tracking and evidence grounding
just prime
just status
just validateOr run the tools directly:
wai status
bd ready
openspec list
dont prime --plainah is the CLI that enforces spec-test correspondence.
- Local hooks are a convenience layer.
ah initinstallsah checkinto supported pre-commit frameworks when it findslefthook.ymlor.prek. - CI is the enforcement gate. Run
ah checkin CI and fail the job on any non-zero exit. ah doctorhelps explain setup drift; it is not the enforcement command.
| Command | Purpose |
|---|---|
ah init |
Create or refresh .espectacular/ files and hook integration |
ah doctor |
Detect configured frameworks and diagnose config, path, hook, collision, orphan, and archetype issues |
ah doctor --enable <capability> |
Write the config block for a detected-but-unconfigured capability (pytest, cargo, vitest, mutation, property, snapshot) |
ah check |
Run fast structural analysis (spec/contract correspondence); use --run-tests to execute declared tests |
ah check --run-tests |
Run structural analysis plus declared contract tests |
ah check --changes <id> |
Validate deployed specs plus one or more staged change overlays |
ah report |
Generate a spec-to-contract coverage matrix (covered/missing/failing per spec) |
ah explain [topic] |
Print guidance for a finding kind or suggested action; omit topic for a list |
ah explain --list |
List all explainable topics |
ah explain --json |
Emit the topic list as JSON |
ah signals |
Read dont rejection events and emit drift signals as JSON |
ah type |
List built-in archetypes |
ah type <code> |
Print full built-in documentation for one archetype |
ah scenario new <change> <spec> --requirement "<requirement>" "<heading>" |
Append a new scenario and stage its TOML contract |
ah scenario supersede <spec> <old-id> --with=<new-id> --in-change=<change> |
Stage a supersession update for an existing contract |
ah archive <change> |
Move staged change contracts into deployed .espectacular/ locations |
ah feedback <kind> |
File an issue against the upstream repo via gh (--dry-run to preview) |
ah completions <shell> |
Generate shell completions (bash, zsh, fish, powershell, elvish) |
ah upgrade |
Report tool-version drift and update .espectacular/config.toml only |
ah upgrade exits non-zero when it detects drift, even after rewriting tool_version, so automation can notice compatibility changes.
.espectacular/
├── AGENTS.md
├── config.toml
├── <spec>/
│ └── <scenario-id>.toml
└── changes/
└── <change>/
└── <spec>/
└── <scenario-id>.toml
.espectacular/<spec>/<scenario-id>.tomlstores deployed scenario contracts..espectacular/changes/<change>/<spec>/<scenario-id>.tomlstores staged change overlays.openspec/specs/<spec>/spec.mdis the deployed spec source.openspec/changes/<change>/specs/<spec>/spec.mdis the staged change spec source.
Normative schema files:
schemas/check-output.schema.json—ah checkJSON envelopeschemas/config.schema.json—.espectacular/config.tomlschemas/scenario-contract.schema.json— scenario TOML contractsschemas/custom-runner.schema.json— custom runner JSON envelope
tool_version = "0.1.0"
[paths]
specs = "openspec/specs"
changes = "openspec/changes"
[runners]
pytest = ["pytest"]
cargo = ["cargo", "test"]
vitest = ["vitest", "run"]
[quality.mutation]
enabled = true
threshold = 0.80
command = ["/bin/sh", "{}"]
[capabilities.property]
enabled = true
[capabilities.snapshot]
enabled = trueRequired fields:
tool_version: pinnedahversion for compatibility modepaths.specs: deployed OpenSpec rootpaths.changes: staged OpenSpec change rootrunners.<type>: argv array used to execute non-shell test entries
Optional quality fields:
quality.mutation.enabled: activate mutation quality signalquality.mutation.threshold: minimum mutation score (default0.80)quality.mutation.command: argv template;{}is replaced with the runner script pathcapabilities.property.enabled: activate property-based testing quality signalcapabilities.snapshot.enabled: activate snapshot testing quality signal
id = "empty-input-rejected"
description = "Empty input is rejected before parsing."
archetype = "PF"
status = "active"
superseded_by = ""
authored_with = "0.1.0"
[[tests.unit]]
flags = "tests/compiler/test_parser.py::test_empty_input_rejected"
timeout_seconds = 60Required top-level fields:
iddescriptionarchetypestatus(activeorsuperseded)superseded_by(non-empty whenstatus = "superseded")authored_withtests
Test entry rules:
[[tests.shell]]entries usecommand- non-shell
[[tests.<type>]]entries useflags timeout_secondsis optional but must be positive when present
Scenarios are append-only.
- Do not rewrite or delete a deployed scenario to change intent.
- Add a new scenario under the targeted requirement with
ah scenario new .... - If the old scenario is replaced, stage a supersession with
ah scenario supersede .... - Once the change is accepted, run
ah archive <change>to move staged TOML files into deployed.espectacular/paths.
Requirement targeting is explicit: ah scenario new appends under the named ### Requirement: block and fails if that requirement is missing.
ah check always prints a stable JSON envelope to stdout.
Success shape (envelope wrapper shown; examples below show the data field only):
{
"ok": true,
"envelope_version": "0.1",
"cli_version": "0.4.0",
"envelope_kind": "ok",
"data": {
"scope": { "deployed": true, "changes": [] },
"summary": { "structural": 0, "execution": 0, "passed": 1, "counts_by_kind": {} },
"findings": []
},
"warnings": [],
"hints": [],
"meta": { "duration_ms": 0, "tx": null, "request_id": null, "author": null }
}data fields:
scope.deployed: alwaystruein v1scope.changes: selected change ids, sorted and deduplicatedsummary.structural: count of structural findingssummary.execution: count of execution findingssummary.passed: count of passing declared testssummary.counts_by_kind: map of finding kind → count (all categories, including quality)findings: sorted by(spec_path, scenario.id, kind, test)
Each finding includes:
kind: finding kind string (see table below)category:"structural","execution", or"quality"spec_path: spec path or synthetic path (e.g."(quality/mutation)")suggested_action: slug forah explain <suggested_action>playbook_command: pre-formed command string, e.g."ah explain fix_tool_invocation"
| Kind | Category | Meaning |
|---|---|---|
no-toml |
structural | scenario has no matching .espectacular/.../*.toml contract |
orphan-toml |
structural | contract exists without a matching OpenSpec scenario |
slug-collision |
structural | two scenarios in one spec slugify to the same id |
id-mismatch |
structural | scenario slug, TOML filename, and TOML id disagree |
no-tests-declared |
structural | contract has no runnable test entries |
missing-runner |
structural | a non-shell test type has no configured runner |
malformed-contract |
structural | TOML cannot be parsed or validated |
missing-replacement |
structural | a superseded contract points to a replacement scenario that is absent from scope |
overlay-conflict |
structural | selected changes define conflicting staged scenarios or staged contract updates |
test-failing |
execution | a declared test timed out or exited non-zero |
quality-mutation |
quality | mutation testing score meets or exceeds threshold |
quality-composability |
quality | composability tool (vampiro) reported findings |
quality-cost |
quality | cost analysis tool (crua) reported findings |
quality-boundary-coverage |
quality | boundary coverage tool (livin) reported findings |
quality-property |
quality | property-based testing is active and passing |
quality-snapshot |
quality | snapshot testing is active and passing |
test-failing findings include test execution details: type, command, exit_code, timed_out, stdout_tail, and stderr_tail.
Quality findings (quality-*) are informational — they appear in counts_by_kind but do not cause ah check to exit non-zero.
ah check maps each [[tests.<type>]] entry to a runner:
shellentries use thecommandfield directly.- Named types (
pytest,cargo,vitest) look uprunners.<type>inconfig.tomland prepend those argv entries to theflagsvalue. customentries invoke the configured runner and parse its JSON envelope (see Custom runner protocol below).
Detection precedence (highest to lowest):
- Explicitly configured in
[runners] - Detected via project manifest (e.g.
pytest.ini,Cargo.toml,package.jsonwith vitest) - Detected via binary on
$PATH
ah doctor reports each detected framework and its source. Detected-but-unconfigured frameworks appear as recommendations:
framework: pytest (configured)
recommendation: vitest detected via manifest — run: ah doctor --enable vitest
Run ah doctor --enable <capability> to write the corresponding config block automatically.
A custom runner is any executable that prints a JSON envelope to stdout:
{
"exit_code": 0,
"passed": true,
"findings": []
}exit_code: the runner's exit code (informational)passed:truewhen the test suite passed;falsetriggers atest-failingfindingfindings: additional findings to surface (empty whenpassedistrue)
Schema: schemas/custom-runner.schema.json. The findings array reuses the finding shape from schemas/check-output.schema.json.
Configure a custom runner in config.toml:
[runners]
my-tool = ["./scripts/my-runner.sh"]Then reference it in a scenario contract:
[[tests.custom]]
flags = "--suite integration"
timeout_seconds = 120Quality signals surface test-suite health metrics as findings. They are enabled via config.toml and never block ah check (exit code is unaffected).
[quality.mutation]
enabled = true
threshold = 0.80
command = ["/bin/sh", "{}"]{} in command is replaced with a generated runner script path. When the mutation score meets or exceeds threshold, a quality-mutation finding appears. If the tool exits non-zero, a test-failing finding is emitted instead.
[quality.composability]
enabled = true
command = ["vampiro", "check", "--format", "json"]
[quality.cost]
enabled = true
command = ["crua", "check", "--format", "json"]
[quality.boundary-coverage]
enabled = true
command = ["livin", "check", "--format", "json"]Each tool must emit a custom-runner JSON envelope on stdout with an optional
findings array. When findings are present, a quality-* finding is emitted.
If the tool exits non-zero, a tool error is reported. These signals are
informational only (do not cause ah check to exit non-zero).
Absence of these tools is a graceful no-op — no error is raised when the config section is missing.
[capabilities.property]
enabled = trueWhen a scenario has a [[tests.property]] entry and the run passes, a quality-property finding is emitted.
[capabilities.snapshot]
enabled = trueWhen a scenario has a [[tests.snapshot]] entry and the run passes, a quality-snapshot finding is emitted.
ah doctor exits zero with:
healthy: all checks passed
Otherwise it exits non-zero and emits diagnostics such as:
bad-configversion-driftmissing-pathcollisionorphan-contractunknown-archetypemissing-managed-blockhook-absent
Use ah type to list current archetypes and ah type <code> for full guidance.
Current catalog:
PF— Pure FunctionalSA— Stateful APIBP— Boundary ProtocolCE— Contract/EventNR— Non-Regression
The catalog is embedded, append-only, and version-addressable so older pinned projects remain readable in compatibility mode.
v1 does not:
- judge whether a test is meaningful
- inspect test internals for assertion quality
- detect prose drift inside a scenario body
Review and supersession discipline handle those concerns; ah only checks deterministic correspondence and execution.
waistate lives in.wai/- beads state lives in
.beads/ - OpenSpec files live in
openspec/ dontstate lives in.dont/
All the code in this repository was generated by a large language model. This is not a confession, nor an apology. It's a fact, like the one that says water boils at a hundred degrees at sea level: neutral, technical, and with consequences one discovers later.
What the human did is what tends to happen before and after things come into existence: thinking. Reviewing requirements, arguing about edge cases, understanding what needs to be built and why, deciding how the system should behave when reality —which is capricious and does not read documentation— confronts it with situations nobody anticipated. The hours of planning, of design, of reading specifications until exhaustion dissolves the boundary between understanding and hallucination.
The LLM writes. The human knows what it should say.
There is a distinction, even if looking at the commit history makes it hard to find. The distinction is that a machine can produce correct code without understanding anything, the same way a calculator can solve an integral without knowing what time is. Understanding what that integral is for, whether it actually solves the problem, whether the problem was the right problem to begin with — that remains human territory. For now.