Deterministic checklist gate for AI-driven development. Agents are non-deterministic; a content-addressed attestation is not. qc records evidenced answers bound to file bytes and refuses to clear until every applicable item has one.
Install (macOS / Linux / Git Bash; one static binary, no runtime needed):
curl -fsSL https://raw.githubusercontent.com/probelabs/qc/main/scripts/install.sh | shTurn it on in your repo:
cd your-repo
qc init
qc --version # 0.2.0 or newer for everything belowWindows PowerShell and other install options: Install.
Now hand your agent a checklist. There are two ways: paste it into the chat, or point the agent at a markdown file.
A checklist is plain markdown. qc turns it into a gate: the agent has to answer every item with evidence tied to the exact bytes it checked, and qc verify stays red until it does.
Paste this into Claude Code, Cursor, Codex, or any agent that can run shell commands. The first line is the instruction; the checklist goes under it:
Using the QC CLI, follow this checklist until it is done. If this checklist is only in the message, save it to a file first, then use that path. When qc verify is clear, show me the qc report --md output.
---
id: release-check
scope: [src/**]
---
- [ ] behaviour :: Does the change still do what the ticket says? @evidence(the function and line that implement it)
- [ ] happy :: Run the example. Does the normal case work? @evidence(the output line, copied verbatim)
- [ ] edge :: Is the boundary case covered by a test? @evidence(the test name and the test summary line with its count)
- [ ] tests :: Does the suite pass? @run(make test)
- [ ] files :: Which files changed? @scope(src/**, tests/**) @evidence(the `git status --short` output)
qc cannot read the chat, so the agent first saves the checklist as release-check.md (qc help tells it to). Then it runs qc plan, does the work, marks each item, runs qc verify, and shows you the report.
Keep checklists in your repo (a release checklist, a review checklist, a runbook) and name the file:
Using the QC CLI, follow the checklist in docs/release-check.md until it is done. When qc verify is clear, show me the qc report --md output.
The agent passes the path to every command. You can run the same loop yourself:
export QC_BY=agent
qc plan --checklist docs/release-check.md # questions + what evidence each needs
qc mark release-check/happy --pass -m "make run printed \`total=42\`" --checklist docs/release-check.md
qc verify --checklist docs/release-check.md # exit 0 only when every item is attested
qc report --md --checklist docs/release-check.md # the evidence tableRepeat --checklist to combine files, for example qc verify --checklist org/release.md --checklist org/security.md. Pass the same paths to every command; every file contributes to the gate. QC_CHECKLIST=docs/release-check.md supplies one file when no --checklist flags are given. To make a checklist part of every qc verify, copy it to qc/checklists/<id>.md and drop the flag.
From a real Claude Code run of examples/paste-a-checklist/ (trimmed):
| item | status | by | digest | evidence | parent | asked for |
|---|---|---|---|---|---|---|
| reservation-release/promise | CLEAR | agent | 2bf544b4… | src/gate.py:9 'return is_retry and age_hours <= 24' | | the line number and text of the return statement in src/gate.py |
| ↳ reservation-release/row-10 | CLEAR | agent | b11d0e9c… | python3 src/gate.py row: 'retry=True age=10 same=True expected=True' | promise | the age=10 output row, copied verbatim |
| ↳ reservation-release/boundary | CLEAR | agent | 7df1f0c3… | Added test_retry_at_exactly_24_hours_is_same to tests/test_gate.py; 'Ran 4 tests' / 'OK' | promise | the new test name and the unittest summary line with its count |
| reservation-release/suite | CLEAR | qc | | qc ran `python3 -m unittest discover -s tests -q`: exit 0 | | |
| reservation-release/bytes | CLEAR | agent | 102dd37b… | git status --short: ' M tests/test_gate.py' | | the `git status --short` output, one entry per file |
7 of 7 items attested to the current bytes.
Each row says who answered, what they cited, and what the checklist asked for. The digest covers the scoped files. If one of them changes later, the item opens again and qc verify exits 1.
Try it in two minutes: cd examples/paste-a-checklist && COPY=1 ./setup.sh. That makes a throwaway repo and puts the message on your clipboard. Paste it into Claude Code.
- Frontmatter
idmust equal the filename stem: saveid: release-checkasrelease-check.md.scopeis the files every item depends on. - [ ] <name> :: <question>is one item. Indent two spaces to nest. Children are marked before their parent; a parent pass is refused while a child is open.@evidence(...)tells the agent what its mark must name.qc planshows it under the question, andqc report --mdshows it next to the answer.@run(cmd)items are run byqcitself. The agent cannot mark them.@scope(...)overrides the files an item depends on. Edit one of them and the item opens again.@humanitems are for a person. An agent is refused.- A mark like
tests pass,doneorlgtmexits 2. Evidence has to name a path, a number, a URL or abacktickedcommand. - Prose between items is free text for the agent (for example: "go one item at a time and
sleep 2between items so people can follow").qcparses only the- [ ]lines.
qc is a Cosmopolitan APE universal binary — one release asset runs on Linux, macOS, and Windows from a shell (do not open it from a GUI file manager). Release assets: qc and qc.exe (same bytes; Windows-friendly name), plus LICENSE.
macOS / Linux
curl -fsSL https://raw.githubusercontent.com/probelabs/qc/main/scripts/install.sh | shInstalls to ~/.local/bin/qc. No root required.
Windows (PowerShell)
irm https://raw.githubusercontent.com/probelabs/qc/main/scripts/install.ps1 | iexInstalls to %LOCALAPPDATA%\qc\bin\qc.exe. The installer prints a PATH export if needed.
Windows (Git Bash / MSYS / Cygwin)
Same curl|sh as Unix — detects the Windows-ish host and installs ~/.local/bin/qc.exe:
curl -fsSL https://raw.githubusercontent.com/probelabs/qc/main/scripts/install.sh | shOverrides (pass env to the installer process, not only to curl/irm):
- Unix / Git Bash:
curl … | QC_INSTALL_DIR=~/bin sh,curl … | QC_VERSION=v0.2.0 sh - PowerShell:
$env:QC_INSTALL_DIR = "$env:USERPROFILE\bin";$env:QC_VERSION = "v0.2.0"thenirm … | iex
Then:
qc helpOn Apple Silicon, the first run may self-extract an APE loader under TMPDIR/HOME (no sudo required for a smoke test).
Needs cosmocc:
git clone https://github.com/probelabs/qc.git
cd qc
# Makefile defaults COSMOCC to a local path; override for your machine:
export COSMOCC=/path/to/cosmocc/bin/cosmocc
make # writes ./qc (APE). Invoke it from a shell as ./qc
./qc helpIn a consumer repo, qc init vendors that APE as qc/qc. Call it as ./qc/qc, or put qc/ on your PATH so plain qc works (the quickstart below assumes that).
From a git repo (throwaway is fine). Assumes qc resolves to the APE (export PATH="/path/to/qc-build:$PATH" before init, or export PATH="$PWD/qc:$PATH" after).
qc init
qc template list
qc add docs
qc planplan prints the ordered worklist. A checklist with applies_when starts with a decision:
export QC_BY=agent
qc decide docs --yes -m "README.md documents the public install and usage surface for this change"
qc planAnswer with falsifiable evidence (a real path, digit, URL, or backtick span). Vague ticks are rejected; the tool names the next command, never a passing phrase:
qc mark docs/accuracy --pass -m "checked README.md section headings against the files present in the worktree"
qc mark docs/stale --pass -m "README.md is new; no prior install steps to retire"
qc verify # loop mode: scratch + committed stateWhen the worktree is what you intend to ship:
git add qc/
qc seal --staged # promote matching scratch into this branch segment
qc verify --staged # predicts CI: committed/index state only, no scratch
qc report --md # claims table for the PR summaryExit codes: 0 all clear · 1 open items · 2 config/parse error.
An attestation is a claim about content. Its logical key is (item_ref, digest). The digest is a 16-hex SHA-256 prefix over the item identity text plus the sorted, normalized hashes of files in the item's scope. Rewording the question or changing scoped bytes forces the item again. Forcing means staleness, never, or expiry — there is no second mechanism.
| Path | Git | Role |
|---|---|---|
qc/ |
committed | Vendored APE (qc/qc), checklists, config, state segments/base — the review surface |
.qc/ |
ignored | Scratch attestations, manifests, cache — disposable local workspace |
rm -rf .qc/ is always safe (same class as qc reset). Hand-editing qc/state/ or .qc/scratch.qcs is not; only qc mark / qc decide / qc baseline / qc seal / qc compact write state.
| Code | Meaning |
|---|---|
| 0 | All clear |
| 1 | Open items (forced, failed, or awaiting decision) |
| 2 | Config or parse error |
This triple is the machine API. Help and errors name the next command; they never name a string that would satisfy the gate (I5).
| Mode | State sources | Question |
|---|---|---|
qc verify |
scratch + committed | Am I ready? (agent loop) |
qc verify --staged |
index/committed only | Will this commit pass CI? |
qc verify --ci |
committed only, read-only | Gate of record |
Unsealed scratch is invisible to --staged and --ci. Forgetting seal does not corrupt state; it leaves items forced where it matters.
Also: --only <ref>, --quiet, --json.
mark / decide / baseline write scratch only. seal appends to a branch-owned qc/state/seg-*.qcs. compact (trunk only) folds segments into base.qcs. Parallel branches write different segment files, so merges are both-add — correctness does not depend on merge drivers.
Help, errors, and worklists name the next qc … command. They never print a phrase that would satisfy the evidence checker. Agents copy NEXT lines; applause tokens (lgtm, done, ok) are rejected at mark time.
Set export QC_BY=agent (or pass --by agent) so attestations record provenance. Agents must not mark @human items — the CLI refuses and tells you to surface the item to a person.
Topic help: qc help then qc help <topic>. Every topic ends with NEXT.
qc help
qc help mark
qc help verifyTopics: init add template plan decide mark baseline seal reset verify compact report.
Creates committed qc/ (vendors this APE as qc/qc), ignored .qc/, and one-line .gitignore / .gitattributes updates.
qc init
# Created qc/ (tool + checklists, committed) and .qc/ (local workspace, ignored)
# NEXT: qc template listGallery of bundled checklists followed by local qc/templates/*.md files, marked local and sorted by filename. show prints a bundled template's minimal file. Bundled names take precedence over matching local names.
qc template list
# docs claims verified against code
# code-quality quality questions for application code
# …
# area-keys local (qc/templates/area-keys.md)
qc template show docs
# ---
# id: docs
# applies_when: The change is documentation
# scope: [docs/**, README.md]
# …Copies a template into qc/checklists/. Minimal by default; --full keeps every item.
qc add docs
# Copied builtin/docs@1 (minimal) → qc/checklists/docs.md
qc add security-review --full
# Copied builtin/security-review@1 (full) → qc/checklists/security-review.mdAdapt after copy; there is no runtime extends (legibility beats DRY).
Local templates copy with qc add <name> and receive from: local/<name> in frontmatter, unless the template already has a from: line, which is preserved. Bundled copies keep from: builtin/<name>@1.
For a template containing {{area}}, use qc add area-keys --set area=storage.
Repeat --set for more keys; --set=key=value also works. Values are copied
literally into every occurrence, including scopes and questions. Empty values are
allowed, and the last assignment to a key wins. The source template is unchanged.
A qc placeholder is exactly {{key}} where key matches
[A-Za-z_][A-Za-z0-9_-]*, with no spaces, dots or other characters. Only this form
is substituted. Values are never recursively expanded. Malformed assignments or
unresolved qc placeholders, including any remaining after substitution, exit 2
before copying. Loaded checklists containing unresolved qc placeholders also
exit 2; fill in the values before running qc plan. Other brace text, including
{{ key }}, {{.}}, {{ .Values.x }}, {{ user }} and an unclosed {{, passes
through unchanged in templates and replacement values and is never a placeholder
error.
Ordered worklist before work: forced items, questions, previous answers, decision requests. Nested items are indented, and each @evidence(...) hint is printed under its question as Evidence -m must name: …. Every command accepts repeated --checklist <path> flags to add files outside qc/checklists/ in argument order. With no flags, QC_CHECKLIST adds one file.
qc plan
# 1 docs/@applies FORCED (never)
# Q: The change is documentation
# NEXT: qc decide docs --yes|--no -m "…"
qc plan --json | head -c 200
# {"items":[{"ref":"docs/@applies","state":"FORCED (never)",…},…]}Applicability for checklists with applies_when. Writes scratch only. Exclusions are diffable Statements of Applicability.
export QC_BY=agent
qc decide docs --yes -m "README.md documents the public install and usage surface for this change"
qc decide code-quality --no -m "this change is docs-only README.md; src.rs is a stub not shipped"Evidenced answer bound to the current worktree digest → scratch. A --pass on a parent item is refused (exit 2) while any of its children is open.
qc mark docs/accuracy --pass -m "checked README.md section headings against the files present in the worktree"
qc mark docs/stale --fail -m "README.md still mentions cosmocc.zip path that Makefile no longer uses"
qc mark docs/stale --na -m "docs/ is empty; only README.md exists and it is new in this PR"Grandfather current content at adoption. Writes scratch; seal afterward so trunk sees it.
qc baseline -m "adopting docs checklist on an existing README.md that already matches the code"
qc baseline --item code-quality/naming -m "greenfield src.rs with a single fn main; naming debt accepted at adoption"
qc seal --stagedPromote scratch entries whose digest still matches into this branch's qc/state/seg-*.qcs.
git add qc/ README.md
qc seal --staged
# seal: promoted 4 of 4 → qc/state/seg-main-28cd64.qcs
# NEXT: qc verify --stagedWithout --staged, seal uses the worktree view; --staged matches what the index will commit.
Wipe scratch, or one scratch entry. Committed qc/state/ is untouched. Same safety class as rm -rf .qc/.
qc reset --item docs/stale
# removed docs/stale from scratch. Committed state is untouched.
qc reset # wipe all scratch
qc planqc verify # loop: scratch + committed
qc verify --staged # index/committed only — predicts CI
qc verify --ci # committed only, read-only gate of record
qc verify --only docs/accuracy # single ref
qc verify --quiet # exit code only (still prints forced/failed lines)
qc verify --json # machine-readable items[]Example --only clear:
qc verify --only docs/accuracy
# CLEAR docs/accuracy
# all clearFold segments into base.qcs, one line per item, drop orphans, commit. Refuses off-trunk so parallel branches cannot rewrite shared base.
git checkout main
qc compact
# On a feature branch:
qc compact
# REFUSED — compact rewrites shared base.qcs; it runs on trunk only.
# NEXT: git checkout main && qc compactClaims table for reviewers, CI summaries, and the end of an agent's task. age is when the answer was recorded. parent and asked for (the @evidence hint) come from the checklist. @run rows show qc ran \`: exit N, by qc`.
qc report --md
# | item | status | by | age | digest | evidence | parent | asked for |
# |---|---|---|---|---|---|---|---|
# | docs/accuracy | CLEAR | agent | 2026-10-28T15:04:14Z | b1a1bbdb28626343 | checked README.md section headings… | | |
#
# 1 of 1 items attested to the current bytes.Every pass / fail / n_a needs -m evidence. Shape check at mark time:
- Rejects empty applause (
done,ok,lgtm, … anddeny_evidence). - Requires enough substance and at least one falsifiable token: an existing repo path, a digit, a URL, or a
backtickedspan. - Refuses common secret shapes (AWS keys, GitHub
gh*_tokens, Slackxox*-, Stripe andsk-keys, JWTs, PEM blocks, high-entropy blobs). Long CamelCase test names and package paths are evidence, not blobs.
qc mark docs/accuracy --pass -m "lgtm"
# REJECTED — evidence must point at something falsifiable (a path, a number, a command, a link),
# not restate that checking happened.
# NEXT: qc mark <ref> --pass -m "…"Rejection messages state the principle and name the next qc mark command. They do not reveal a phrase that would pass.
Items tagged @human require a human --by <name>. With QC_BY=agent the CLI refuses:
export QC_BY=agent
qc mark code-quality/diff-read --pass -m "read src.rs top to bottom; only fn main present"
# REFUSED — this item is @human. An agent must not attest it.
# Principle: human approval is provenance the PR author cannot forge locally.
# NEXT: surface code-quality/diff-read to the user; they run
# qc mark code-quality/diff-read --pass -m "…" --by <their-name>@run(cmd) items are executed by the gate (exit 0 clears). A missing binary is a loud FAILED with an install hint — never a skip.
qc verify --only code-quality/lint
# ruff: No such file or directory
# FAILED code-quality/lint
# @run exited 127. binary ruff not found. Install the command named in @run, then: qc verify
# → The gate does not skip a missing checker. NEXT: install it, then qc verifyStart here: docs/agentic-guide.md — Claude Code / Cursor / AGENTS.md wiring, exact loop, CI sketch, failure recovery, and copy-paste snippets.
Paste-a-checklist walkthrough you can run in two minutes: examples/paste-a-checklist/.
Short skill card to load into the agent: SKILL.md.
Worked demo with a real fail-then-pass transcript: examples/agent-demo/.
Point agents at SKILL.md (full guide: docs/agentic-guide.md). Contract:
- Set
QC_BY=agent. - Run
qc planat task start; treat questions as requirements, not an end exam. - Mark at natural boundaries with falsifiable evidence.
- Never mark
@humanitems — surface them to the user. The CLI refusesby:agenton@human. - Never hand-edit
qc/state/or.qc/scratch.qcs. - Loop:
qc plan→ work →qc mark …→qc seal --staged && qc verify --staged. - On failure, read the worklist top-to-bottom and fix; do not re-attest blindly.
| Template | Use when |
|---|---|
code-quality |
Application behavior changes |
api-change |
Public surface / compatibility |
security-review |
Auth, secrets, untrusted input |
data-migration |
Stored data or schema (includes @strict killers) |
dependency-update |
Add or bump a dependency |
release |
Release or cut |
docs |
Documentation changes |
ai-generated-code |
Agent-authored edits (memory APIs, weakened tests, suppressions) |
qc add copies the minimal variant unless --full. Adapt after copy; there is no runtime extends (legibility beats DRY).
make # cosmocc → ./qc (APE)
make test # kernel + shell acceptance + acceptance binary
make cover-host # host gcc-15 MC/DC path (not the APE; see scripts/host-c-mcdc.sh)
make cleanHost coverage needs Homebrew gcc-15 / gcov-15 as wired in scripts/host-c-mcdc.sh. The APE product binary remains the cosmocc build.
SPEC.md— specification v0.4 (purpose, invariants, digest, state, evidence, CLI, phasing).SKILL.md— short agent loop contract.docs/agentic-guide.md— agent wiring (Claude Code, Cursor, CI) + demo pointer.
Phase 1 is what this binary implements. Features marked Phase 2 in the spec (template diff, qc update, qc stats, qc diff / --carry, merge-queue mode, …) are not shipped here.
Licensed under the Apache License 2.0. SPDX identifier: Apache-2.0.