Imported from andresmassello/uscha (
uscha-kit/skills/uscha-devloop/SKILL.md). Install upstream withnpx skills add andresmassello/uscha --skill uscha-devloop. Copyright stays with the author.
dev-loop orchestrator
Who this is for: a single operator driving one non-trivial or risk-bearing change, kept honest by a deterministic ledger and a human merge gate. NOT for trivial or throwaway work — a one-file fix or config tweak runs build+test only and skips discovery/ADR/uscha-sysdoc entirely (see the risk-profile table in the playbook, play 03).
You are running a disciplined development + QA cycle across one or more repositories.
The measurement engine is ./.claude/skills/uscha-devloop/qa_ledger.py per project, or the installed Codex/Claude global engine (stdlib Python 3).
All metrics come from the ledger — never estimate counts from memory. Run every
QA tool through the ledger so the final retrospective is real. The ledger contract has
two tiers: measured records (snapshots, ingest-gate, log-gate — parsed from real
artifacts; these can block) and self-reported agent counts (log-step — narration
recorded for the retrospective; a measured red always overrides a narrated green).
First contact (show ONCE, then never again)
Only when this project has no uscha artifacts yet -- no QA-LEDGER.json, no SPEC.md or
ACCEPTANCE.md, no docs/adr/ -- open with this block, then start working. If any of those
exist, the operator already knows the method: skip it entirely and go straight to the
breadcrumb. Repeating it every run would be exactly the ceremony the method forbids.
[uscha · devloop · START]
Method: you bring the idea, the method builds the rest. Facts block, guesses advise;
nothing closes on a checkbox, and the human approves the merge.
Here: plan → build → a severity-gated QA loop that converges instead of chasing zero → PR.
Output: code, tests, and QA-LEDGER.json (every gate recorded with its evidence)
Next: the human gate: review the PR and decide the merge. I stop there.
Stop: say so at any point -- whatever is already written stays.
Bilingual by construction. The labels (START, Method, Here, Output, Next,
Stop) stay VERBATIM in English -- they are the method's vocabulary and the smoke suite checks
for them mechanically, which is only possible if they never move. The wording after each label
is the canonical English; render it in the operator's language. If they are writing to you
in Spanish, the whole block reads in Spanish under English labels. Do not translate the labels,
do not leave the content in English when they are not writing in English.
Unlike the close block, Next here MAY name the nominal route: on a first run there is no
measured state to derive from yet, so the nominal path is the honest answer. From the close
block onward, derived state wins.
Orientation markers (non-negotiable)
The operator must never have to ask "where am I?" or "what happens now?". Two markers, always. They are navigation, not ceremony: one line per turn, one block at the end.
No statusline (kit 2.4.0): the visible reply IS the statusline. On a surface with no live
statusline — Codex, pi, a plain terminal — these markers are the only signal the operator gets,
so they MUST appear in the visible reply text, never only inside a collapsed tool-call log the
operator may not expand. On such a surface, the FINAL message of every turn STARTS with the
compact uscha-status block (derived phase · loop · measured acceptance · next criterion, read
from the ledger — see the uscha-status skill), immediately before the breadcrumb/close marker
below.
Open every turn with a breadcrumb, then the content:
[uscha · devloop · <step> → <target>]
<step>—Q<n>for a question,pass <n>for a loop iteration,step <n>otherwise. Count what has actually happened. Never write a denominator (Q4/12): this phase converges, its length is not known in advance, and an invented total is exactly the kind of narrated number the method forbids. When the ledger already measures the count (the QA loop'sloop_count), use the measured number — never keep a parallel tally of your own.<target>— the artifact this turn feeds (SPEC,ADR-003,ACCEPTANCE,LEDGER,RECEIVED, ...). Drop→ <target>only when the turn genuinely feeds none.
Close with the close block ONCE, when the skill finishes — not on every turn. Ending without it is a defect, even when the phase converged cleanly:
[uscha · devloop · CLOSED]
Produced: <files actually written, or "nothing">
Blocks: <what stands between here and the next phase, or "nothing">
Next: <the next action, and why it is that one>
Run: <the exact command or skill to invoke>
This is not the implementation handoff some skills also emit: that one is a prompt for whoever implements next, this one is navigation for the human operator, and both can appear.
Blocks and Next are derived from the state you just produced — never copied from a
fixed route, including any Flow: line in this file. Those lines are the nominal path;
open ADR experiments, an unclosed spike, an unapproved golden or a red gate all change what
genuinely comes next, and the derived answer wins. If the next phase cannot start yet, name it
and say exactly what unblocks it.
Keep the CONTENT in the conversation's language, but keep the labels (CLOSED, Produced,
Blocks, Next, Run) verbatim — they are the method's vocabulary and the smoke checks them.
Non-negotiable principles
-
Converge, don't chase zero. Block only on findings at or above the severity gate (
config.defaults.severity_gate, default BLOCKER/CRITICAL/HIGH). Everything below goes toISSUES-DEFERRED.md, never into the loop. Polishing Medium/Low findings forever is the failure mode this skill exists to prevent. -
Tests are a guardrail, not a finale. Run the repo's test command (
mvn test/flutter test/pytest/npm test/go test/cargo test/dotnet test/ctest/./gradlew test/swift test) after every tool pass that changed code. A red suite stops the loop. -
Generating tests is not running tests.
/improve test(writing coverage) runs ONCE at the end against stabilized code — never inside the loop. Inside the loop you only run the existing suite. -
Stop at merge. You create the PR and confirm CI is green. You do NOT merge. The human owns the merge gate.
-
Tracked-markdown protocol: READ the file, never ask for it. Before modifying any tracked
.md(CLAUDE.md, plan/delta docs, docs/adr), READ the current version out of the tree. Those files carry real progress (checkboxes, notes): preserve it and edit in place, never regenerate from scratch. The current version of a tracked file is INFORMATION the tree already holds, so asking the human for it is noise — you ask the human for DECISIONS, never for information you can read (ADR-044). -
origin: agent— a decision the human never made does not enter scope silently. Every acceptance criterion, ADR decision item or HANDOFF rule that YOU introduce (rather than one the human answered) carries the marker on its own line:- [ ] AC-12 — when X then Y. (origin: agent) - [ ] AC-12 — when X then Y. (origin: agent, confirmed: 2026-09-07)Unmarked items are human-origin by default; nothing existing is retro-tagged. An unconfirmed item is not in scope: do not implement it, do not gate on it, do not quote it as agreed. Ask the human to confirm it ONE BY ONE — a package-level "ok" over a twenty-item summary confirms nothing — and only then write
confirmed: YYYY-MM-DDon the same line. List every still-unconfirmed item in the PR body under Agent-origin, unconfirmed.spec-checknames them with file:line andreadinesscounts them on their own line; both are ADVISORY and neither changes an exit code or the score. -
The golden is the one artifact you cannot author. For migration/legacy work,
.approvedfixtures are field truth captured from the ORIGINAL code and approved by a HUMAN. You emit.receivedand stop; you never write, edit or rename.approved(aPreToolUsehook denies the write — INV-GOLDEN-01).
Setup (once per run)
QL="./.claude/skills/uscha-devloop/qa_ledger.py" # instalacion por proyecto
[ -f "$QL" ] || QL="$HOME/.codex/skills/uscha-devloop/qa_ledger.py" # Codex raw-skills install
[ -f "$QL" ] || QL="$HOME/plugins/uscha/skills/uscha-devloop/qa_ledger.py" # Codex plugin install
[ -f "$QL" ] || QL="$HOME/.claude/skills/uscha-devloop/qa_ledger.py" # Claude global install
python3 $QL init --config uscha.config.json
# adding a service LATER never re-runs init: that would build a NEW ledger and drop every
# step. --add-repo appends and re-seals the checksum, leaving every existing repo untouched
# (kit 2.2.0). The new repo has no evidence yet, so it reads UNMEASURED and the repo
# averages read lower until its first snapshot — that is absence, not a regression:
python3 $QL init --add-repo <NAME> --path <path> --type <type> --test-command "<cmd>"
Execution policy line (before every phase)
Before you start a phase, print the one-line routing note from the ledger, then do the work. This tells the human what the methodology is doing and which model/effort policy is selected for that phase:
python3 $QL execution-policy --phase <idea|disc|spec|adr|build|qa|verify|prod>
This is routing metadata only: it may guide the operator/adapter to use a stronger or
uncorrelated checker, but it never changes readiness and never overrides fact gates. If
model=default, keep the current agent model; if a phase declares a model/effort and the
host supports switching, use it for that phase.
Discovery intake: reality can reopen the contract
Three facts are allowed to push work back into discovery/SPEC instead of being buried as narration:
python3 $QL production-finding --repo <REPO> --severity HIGH \
--source <sentry|support|user|prod-log> --title "..." --evidence "..."
python3 $QL spec-doubt --repo <REPO> --kind spec-wrong \
--note "why the SPEC/AC seems wrong" --evidence "demo/log/file pointer"
python3 $QL spec-change-request --repo <REPO> --source SD-001 \
--requested-change "what must change in SPEC/ADR" --evidence "demo/log/file pointer"
production-findingis for post-merge/prod feedback. It feeds the next discovery/SPEC cycle.spec-doubtis the builder's escape hatch when the SPEC conflicts with reality. Do not code around a doubtful SPEC silently; log it and ask the human.spec-change-requestis the structured bridge from evidence/doubt to a human-approved SPEC/ADR amendment; an open SCR keeps the phase escalated.- Resolve them only after the feedback was incorporated, explicitly reviewed, or the SPEC/ADR amendment decision was recorded:
production-finding --id PF-001 --resolve --note "fed into SPEC"/spec-doubt --id SD-001 --resolve --decision "SPEC amended"/spec-change-request --id SCR-001 --resolve --decision accepted --amended ACCEPTANCE.md.
The config lists every repo and its type (maven|flutter|python|node|go|rust|dotnet|cpp|gradle|swift). In a multi-repo session the
other repos must be mounted via --add-dir or additionalDirectories; the path
fields in the config are relative to where you run init.
For migration/legacy (risk profile E) work, also wire the golden invariant once:
install hooks/block-approved-writes.py as a PreToolUse hook in
settings.json, and add *.approved.* binary to .gitattributes (ships in
templates/.gitattributes) so line endings can't lie in the byte-compare.
Phase 0a — Fast-path check (ADR-003; run FIRST, before planning ceremony)
If defaults.fast_path exists in config, run the measured classifier before demanding a
full spec package:
python qa_ledger.py fastpath-eval --repo <name> --json # dry-run first
- ALLOW and the operator wants the shortcut: re-run with
--intent "<one sentence: what and why>"to record it, then skip Phase 0's full-package demand. The micro-contract replaces it: the recorded INTENT plus at least one new/modified asserting test (readiness stays capped until that test shows up in measured evidence). - DENY: state WHICH measured signal denied it — echo the engine's breakdown verbatim. The skill wires; it never computes and never argues with the verdict. Proceed with the normal full path. The operator may always choose the full path over an ALLOW; nothing — operator, agent or flag — can force ALLOW over a DENY (INV-RIGOR-02).
- Re-evaluate before the PR step (same command, same intent): thresholds exceeded mid-run
flip the run to
ESCALATED— the derived phase blocks pr-ready and readiness is capped. Produce the ADR + ACCEPTANCE the change turned out to deserve, thenresolve-escalation.
Phase 0 — Plan (ADR-first)
- Read
CONSTITUTION.mdfirst (if present). It lists the project invariants no SPEC or ADR may violate. A change that would breach an invariant is a BLOCKER — escalate, do not work around it. The CONSTITUTION constrains the whole build. - The ADR set +
ACCEPTANCE.mdare the input to this loop. They typically come from theuscha-adr-refineskill (the front-half counterpart):/uscha-adr-refine→ ADR set →/uscha-devloop. - Confirm or write the ADR + PLAN. The plan must state acceptance criteria (the
ACCEPTANCE.mdcheckboxes) and the severity gate / coverage threshold up front. The loop targets the plan, not "no issues". If acceptance criteria are missing, stop and runuscha-adr-refinefirst (or ask the human). - Acceptance criteria become the contract tests in Phase 1.
Phase 1 — Coverage gate → conditional characterization (per repo)
For each repo, decide whether a safety net exists before any refactoring:
python3 $QL snapshot --repo <REPO> --phase pre
python3 $QL check-coverage --repo <REPO>
# exit 0 = OK, exit 1 = BELOW threshold (a real report), exit 2 = UNMEASURED (no report)
- Coverage >= threshold: the existing suite is the guardrail. Skip to Phase 2.
- Coverage < threshold (a real report, exit 1): write characterization / contract tests at the boundary (public API, endpoints, input→output behavior) — NOT internals. These must survive refactoring. The ADR acceptance criteria are the spec for these. Have the human review these tests before trusting them as a gate — a test that passes for the wrong reason poisons the whole loop. Characterization tests are for EXISTING code nobody has tested yet, not for code that has not been run at all.
- UNMEASURED (exit 2, no report found): on a greenfield repo this means the test
command has never been run with coverage — not that the code failed a test. Produce
the report first: run the repo's test command with coverage, then re-run
check-coverage. Only if a real report then shows coverage below threshold does the characterization path above apply. - Migration/legacy (profile E): capture the golden BEFORE touching anything. Run
the
uscha-characterizeskill (oruscha-reverse-discoveryfor a whole-system map first): it executes the ORIGINAL code against a real input corpus, emits.receivedfixtures, and STOPS for the human to approve them as.approved. No approved golden = no migration build. This is the baselinegolden-diffgates against in Phase 3.
Phase 2 — Build
Implement per the PLAN. Commit per logical step with conventional commits
(feat:, fix:, refactor:…) so the trail is reviewable and revertible.
ADR discipline during build
- Consult before touching governed areas. Before working on an area covered by an
accepted ADR, read it and follow its Implementation Plan (affected paths, patterns,
tests). Also re-check
CONSTITUTION.md: if the change would breach an invariant, stop and escalate — a constitution breach is a BLOCKER and is never resolved silently in code. If the code contradicts the ADR, flag it to the human — never resolve the conflict silently in code. - Proactive ADR triggers — stop and propose an ADR when you are about to: introduce
a new dependency, create a new architectural pattern others must follow, choose between
real alternatives with non-obvious trade-offs, or contradict an accepted ADR. Tell the
human the decision, why it matters, and ask whether to capture it. If no, leave a short
// ADR-not-taken: <why>comment and move on. - Link code ↔ ADR. When implementing a decision, add one lightweight comment at the
entry point:
// ADR: <slug> — see docs/adr/ADR-NNN-<slug>.md. This makes supersede safe (you can find all code an ADR governs). - Treat
Status: Experimentas a visible hypothesis, not a license to improvise. It may proceed only when the ADR declares Hypothesis, Feedback Signal, Review By/Trigger, Promote Criteria and Rollback/Supersede Criteria.dashboard --json/Mirador expose malformed or expired experiment metadata as advisory; it is not a readiness score and not a hard PR gate by itself. - Never edit the SPEC/ADR to make the implementation look correct. If reality forces a change, amend the SPEC (version it) and return to Ready.
Phase 2b — Simplicity check ("Reduce") — ADVISORY by default (kit 2.1.0)
Before the QA loop, check the change isn't overbuilt. This is the CONSTITUTION's Simplicidad invariant made deterministic — diff minimality, nesting depth and new abstractions, scored over the diff (not AST cyclomatic complexity; honest proxies):
git diff --unified=0 <base> | python3 $QL simplicity-check --config uscha.config.json
# or: python3 $QL simplicity-check --from-git --base <base>
Reads SIMPLICITY: NN/100 — SIMPLE | ACCEPTABLE | OVERBUILT (advisory | declared gate).
Advisory is the default and it exits 0 (ADR-043). Every budget is the KIT'S OPINION until the project declares its own; an opinion that stops a loop is a gate nobody asked for. In advisory mode an OVERBUILT verdict is information for the human: cut what is cheap to cut, report it in the PR body with the score and the flags, and never block on it — do not loop, do not refuse to converge, do not "fix" the diff to chase the number.
It gates only when the project says so: at least one declared budget in
config.defaults.simplicity AND defaults.simplicity.gate: true (or --gate). Then
OVERBUILT is exit 1 and a BLOCKER again: reduce first (guard clauses, drop speculative
types/layers, split giant hunks) and re-run. gate: true with no budget declared is a
config error, exit 2 — a gate with no budget is not a gate.
max_nesting is an INDENTATION-DEPTH proxy, not AST nesting — it counts leading
indentation on added lines. A wrapped call argument, JSX, or a multi-line Java/Kotlin literal
raises it with no control flow present at all, which is the single most common false OVERBUILT.
Discount it accordingly; the kit does not try to make it language-aware. For a 2-space codebase
pass --indent-width 2.
Tests are OUTSIDE the budget (kit 1.11.0): test files (the 9 stack conventions) are
counted and reported apart (test_lines_added) but never gate — writing tests must not
push a diff toward OVERBUILT (deleting them is already blocked by gate-check). A good
project can have MORE test code than production code.
Persist the verdict so convergence and readiness see it (facts block through the ledger, not through your goodwill) — and persist it as what it WAS:
# advisory mode (the default): the run is recorded, and it caps nothing and blocks nothing
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
--verdict advisory [--note "OVERBUILT 58/100 — advisory, no budget declared"]
# declared gate only (defaults.simplicity.gate: true + budgets)
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
--verdict <pass|fail> [--note "OVERBUILT: +612 lines vs 400 budget"]
Never log an advisory run as pass. pass means a declared gate ran and came back clean;
an advisory run means there was no gate. Readiness prints them apart (N ok · 1 advisory) and
the mirador shows ADVISORY instead of OK — but only if you tell it the truth here.
Phase 2c — REUSE-FIRST gate (kit 1.26.0)
Simplicity scores the diff in ISOLATION; it cannot see that the new block re-implements
something the repo already has. waste-check closes that gap — deterministic Type-1/2
clone detection of the diff vs the repo (the muda GitClear calls dominant in AI code):
python3 $QL waste-check --from-git --base <base> --repo-root . --config uscha.config.json
Reads WASTE: NN/100 — LEAN | ACCEPTABLE | WASTEFUL. Advisory by default (exit 0): the
flags name the existing file:line to reuse instead of cloning. It reports a FACT (the block
exists elsewhere) but "wasteful" is a heuristic with false positives (boilerplate, DTOs,
embedded SQL/JSON), so it does NOT block unless the human declares it — defaults.waste.gate: true or --gate. When gated and WASTEFUL, reuse/refactor before the QA loop and persist:
python3 $QL log-gate --repo <REPO> --iteration <N> --kind waste \
--verdict <pass|fail> [--note "clona util/money.py:40 — reusar"]
It runs on prod code only (tests excluded, like simplicity) and skips the files the diff
touches (no self-match). Honest scope: Type-1/2 over normalized lines, not semantic clones;
tune defaults.waste (allow_paths for legit boilerplate, window_size, budgets). In risk
profile A (trivial change) skip it. It collapses into readiness as a gate:waste line.
Phase 3 — QA loop (per repo)
Run the tools in config.defaults.qa_tools_order (default: code-review → judgment-day
→ improve). One pass of all tools = one cycle.
Read the EFFECTIVE order, not the config file: since 2.0.0 uscha init generates a minimal
config, so qa_tools_order is often absent there and resolves from defaults.risk_profile
(ADR-001) or from the engine default. qa_ledger.py doctor --json prints it as
effective.qa_tools_order with its origin; the ledger froze the same value at init. A
project on profile A runs code-review only — do NOT invoke judgment-day or improve there,
and convergence must not wait for them.
After each tool pass:
- Apply only fixes at/above the severity gate. Send the rest to
ISSUES-DEFERRED.md. - Run the repo test command. If red and the fix isn't obvious → escalate.
- Log the agent QA tools (code-review / judgment-day / improve) with
log-step, using counts from each tool's own summary. These counts are self-reported narration (recorded for churn/retrospective); the blocking signals in this loop are the MEASURED records — ingest-gate, log-gate, snapshots:
python3 $QL log-step --repo <REPO> --tool <code-review|judgment-day|improve> \
--iteration <N> \
--reported <total findings> --gated-reported <findings at/above gate> \
--fixed <fixed this pass> --deferred <sent to backlog> --suppressed <false positives> \
--tests-passed <true|false> --files-changed <count> \
--fingerprint <stable,finding,ids> # enables oscillation detection
3b. Fact gates — on every pass that changed code, run gate-check (did the change
weaken the measuring apparatus? did it ADD a secret? — private keys, cloud tokens and
key-container files block as facts, kit 1.12.0; generic password/token literals advise;
new dependencies advise too — the change-budget rule "no new deps without approval" made
visible, kit 1.30.0). A rename is reported as a MOVE, never as a deleted test, and --repo
SCOPES the diff to that repo's path, so a monorepo sibling's hunks are neither this repo's
findings nor this repo's exit code (kit 2.2.0)
and, for migration work, golden-diff (does behavior still match the human-approved
baseline?). Then PERSIST each verdict with log-gate — a failing fact gate blocks
convergence and caps readiness ≤65 through the ledger:
Rubric — the non-testable layer (kit 1.23.0). If a RUBRIC.md exists (or
defaults.rubric.file declares one), on passes that changed code run the
uscha-rubric grade (isolated context — the grader reads ONLY diff + rubric;
evidence-or-nothing) and ingest the contract JSON:
rubric-ingest --repo <REPO> --report reports/rubric-grade.json --iteration <N>.
Advisory by default — it gates ONLY when the human declared
defaults.rubric.gate: true (then a below-threshold score blocks convergence like
any fact gate). Never declare the gate yourself.
Find Bugs Once (kit 1.16.0). When a pass you log carries --fixed > 0, run
regression-check --repo <REPO> --from-git --base <base>: closing findings without
adding a single NON-BLANK test-tree line is NARRATED closure — the failing test
goes BEFORE the fix (Tip 31), and every bug is found ONCE (Tip 94). It is a tripwire,
not a judge: any non-blank test line counts (even a comment — content across 9 stacks
is not guessed), so honor the has_test_definition/has_assertion facts and the
"evidencia DÉBIL" warning; test QUALITY is pit-check's job. NARRATED advises by
default; persist log-gate --kind regression --verdict fail if the team gates it.
Resolving a flag-blocker now REQUIRES --escape-analysis "<which gate/test should have caught this and what was done>" — reflection is part of closing, not optional.
python3 $QL gate-check --from-git --base <base> --repo <REPO> # exit 1 = BLOCKER
python3 $QL log-gate --repo <REPO> --iteration <N> --kind gate-check --verdict <pass|fail>
# CI, when the pipeline has run for this commit (kit 2.2.0). A green run is a FACT and
# belongs in the ledger; --ref is the receipt that outlives the conversation:
python3 $QL log-gate --repo <REPO> --iteration <N> --kind ci --verdict <pass|fail|not-run> --ref <run URL or id>
python3 $QL golden-diff [--dir <fixtures-root>] # exit 0 CLEAN · 1 DIVERGE · 2 NOT-RUN
python3 $QL log-gate --repo <REPO> --iteration <N> --kind golden-diff \
--verdict <pass|fail|not-run> # not-run records the absence — it is never green
(pit-check stays on its scheduled/incremental tier — CONSTITUTION §Tests efectivos —
but when a PIT report EXISTS and fails the gate, persist it the same way:
log-gate --kind pit-check --verdict fail.)
Field truth — the greenfield evidence class (kit 2.2.0, ADR-046). characterize and
golden-diff answer "does it still do what the OLD code did?", which greenfield has no way to
ask: there is no old code, and every test payload was invented by the agent that wrote the
subject. corpus-run is the evidence class for that gap — REAL inputs with their real expected
outputs, one JSON object per line (input, expected, optional id), each fed to the command
on stdin and compared against its trimmed stdout. Run it when the repo declares a corpus
(repos[R].corpus) or when the human hands you one; it is NOT part of the inner loop, since a
real corpus can be large (schedule it like pit-check). It is advisory until the project
declares a budget — with no --threshold, repos[R].corpus_threshold or
defaults.corpus_threshold, the percentage is measured and gates nothing, and it never counts as
an ok gate. With one declared, a run under it caps readiness ≤65 and blocks convergence like
any fact gate. A missing, empty or malformed corpus is exit 2 naming the line — never a scored
0 %. Never author a corpus yourself: an invented corpus is the invented input this instrument
exists to expose.
python3 $QL corpus-run --repo <REPO> --corpus <path.jsonl> --command "<cmd>" [--threshold <P>] [--ac AC-FIELD-01] [--timeout 30] # exit 1 = under the declared budget
# a corpus measured elsewhere (CI, a nightly) goes in through the parity door:
python3 $QL log-gate --repo <REPO> --iteration <N> --kind corpus --verdict <pass|fail|advisory|not-run>
--ac stamps criterion ids on the record: a criterion whose only evidence is a corpus record
closes MEASURED iff that record PASSED, and a ticked criterion without one reports
narrated_only — the same rule a green JUnit testcase has always obeyed. readiness prints one
conditional --- field <repo>: ... line per repo that declares a corpus or ran one; it carries
no weight in the score (that dimension is deferred to its own ADR).
- The static analysis gate (
java-qa-gate: Checkstyle/PMD/SpotBugs/FindSecBugs) is NOT counted by hand. Run the gate so its XML reports are written, then ingest them — the ledger parses the reports, normalizes severities to the common gate scale, splits FindSecBugs (SECURITY) out from SpotBugs, and computes the realfixedcount by diffing finding-IDs against the previous run:
# run your java-qa-gate first (it must emit:
# target/checkstyle-result.xml, target/pmd.xml, target/spotbugsXml.xml)
python3 $QL ingest-gate --repo <REPO> --iteration <N>
# one static-gate step is logged per linter; pass --combined to merge into one.
Severity normalization: Checkstyle error→HIGH / warning→MEDIUM; PMD priority 1→BLOCKER … 5→LOW; SpotBugs priority 1→HIGH / 2→MEDIUM / 3→LOW; FindSecBugs (category SECURITY) floored to HIGH. A report file that EXISTS but is empty credits the fix; an ABSENT report is not treated as clean (means the gate didn't run).
End-of-cycle checks (advisory — you make the final call):
python3 $QL converged --repo <REPO> --tools-per-cycle <count> # exit 0 = converged
python3 $QL oscillation --repo <REPO> --tool <tool> # exit 1 = oscillating
Convergence requires ALL of: the latest agent step of EVERY tool in qa_tools_order
clean (zero gated findings, zero files changed, tests green — padding the window with
extra clean steps does not help, and a red snapshot vetoes a narrated green), AND the
latest static-gate run of every linter clean at the gate level, AND every persisted
fact gate (gate:*, blocker:* records from log-gate/flag-blocker) clean. A clean
agent cycle alone does not converge if any measured gate still flags something.
- Converged → leave the loop for this repo.
- Not converged → next cycle, up to
config.defaults.max_iterations.
Phase 4 — Integration / contract pass (multi-repo)
After each repo converges individually, run the cross-repo layer with all repos mounted.
Run the integration/contract test command from the config and treat contract breakages
as gated findings. Log under --repo integration. This is the second layer of your
two-layer QA architecture; per-repo green does not imply the seams are green.
Phase 5 — Verify (coverage generation, once)
Now that code is stable:
# /improve test → write the fine-grained coverage you deferred earlier
# Then run the full suite and regenerate coverage reports.
python3 $QL snapshot --repo <REPO> --phase post # for every repo + integration
Full suite must be green and coverage at/above threshold before proceeding.
reports/ is EVIDENCE, not build noise (kit 2.4.0). JUnit XML, coverage reports
(JaCoCo/Cobertura/lcov/go cover), and reports/smoke.json are what snapshot,
check-coverage and smoke-ingest read to turn "we ran tests" into a measured fact.
Keep it, commit it, and never delete or .gitignore it to get a clean-looking commit —
a repo with no reports/ reads UNMEASURED, not "clean". (uscha init's generated
.gitignore, kit 2.4.0, never lists reports/ for exactly this reason.)
Phase 5b — Rebuild test (optional; risk profile C+/E or periodic CI)
Completeness of the SPEC, not correctness of the build: is the spec package enough to regenerate the system from scratch? Worth running for critical/legacy work or on a schedule, not every feature.
# 1) capture the signature of the system as it stands now
python3 $QL rebuild --mode baseline --config uscha.config.json
# 2) in a CLEAN tree / fresh session, regenerate PRODUCTION code from SPEC/ADR/
# ACCEPTANCE only, PRESERVING the test suite, then run the tests.
# 3) score the regenerated tree against the baseline
python3 $QL rebuild --mode compare --baseline REBUILD-BASELINE.json # exit 0 = COVERS
The dominant signal is the preserved suite: tests that passed originally but fail on regenerated code are behavior the SPEC left implicit. Verdicts: COVERS (≥90), PARTIAL (≥70), DIVERGE (<70). Feed the listed gaps back into the SPEC, then re-run — divergence is a spec gap, not a code bug.
Phase 6 — PR (stop at merge)
- Gate the PR on the DERIVED state (kit 1.18.0) — per repo:
python3 $QL phase --repo <REPO> --require pr-ready # exit 1 = the facts say no
pr-ready is a PHASE VALUE, not a subcommand — there is no qa_ledger.py pr-ready.
It is always read through phase --repo <REPO> --require pr-ready as shown above
(qa_ledger.py pr-ready alone is an argparse error).
The state is COMPUTED from the ledger (converged + green tests + zero
BLOCKER/CRITICAL + no open escalation), never self-declared — if it exits 1, the
output lists exactly which facts are missing; do NOT open the PR, close the gap.
A spike/* branch NEVER passes this gate (kit 1.19.0): spike code is disposable
by contract — its only legitimate output is an ADR with lessons, never a merge.
- Before declaring TERMINADO, run the seal (kit 1.92.0, INV-T1 / ADR-038):
python3 $QL check-terminado # 0 = sealed · 1 = broken · 2 = UNMEASURED
It recomputes, from the ledger and the tree, whether the recorded evidence still belongs to
the code on disk: the repo subtree clean, no source-relevant change since the last
snapshot's commit (a non-source difference — docs, the ledger, the reports themselves —
seals with a note naming what moved; ADR-039), every ingested report still hashing to
what was recorded. Exit 1 — do not declare TERMINADO:
re-snapshot on the CURRENT state (snapshot --repo <REPO> --phase post) and record why the
seal broke. Exit 2 — the seal is UNMEASURED (no git, or no snapshot recorded): say so
plainly; an answer nobody could measure is not a TERMINADO either.
- Ensure conventional-commit history is clean.
- List the agent-origin items that are still unconfirmed in the PR body, under
Agent-origin, unconfirmed (principle 6).
spec-checknames them with file:line; copy that list. An empty list is written as "none" — silence and "none" are not the same fact. - Cite the operability line in the PR body, under Operability (kit 2.2.0, ADR-048):
copy
readiness's--- operability: ...line verbatim, whichever four verdicts it carries. A reviewer should not have to ask whether this change ships with a release path, a reset and a RUNBOOK — and a red line in the PR body is a conversation, which is exactly the conversation two projects never had until their last week. - Open the PR(s). Confirm CI is green.
- STOP. Present the PR link(s) and wait for the human to merge.
Phase 7 — Smoke run (measured, kit 2.2.0, ADR-047)
Evidence is executed, not narrated. This phase used to end in a prose checklist, and
prose is exactly where a smoke run goes to die: "the jar served /admin", "the simulator
answered 200 in 6 ms" are an agent's sentences, believed because they are written
confidently. A real one cost a field team a release — every simulator run returned an
empty list because the database had no rows, and a smoke narrated as "verified" would
have hidden it behind a paragraph. So RUN the smoke paths (real endpoints, real user
flows, real device paths for this change) with the project's own tool, have that tool
write reports/smoke.json, and INGEST it:
{"checks": [{"name": "AC-28 the jar serves /admin", "ok": true, "status": 200,
"latency_ms": 6, "evidence": "curl -sS localhost:8080/admin | head -1"}]}
python3 $QL smoke-ingest --repo <REPO> --report reports/smoke.json # exit 1 = a check failed
name and a boolean ok are the whole mandatory contract; status, latency_ms and
evidence are optional and never invented. A missing checks, an EMPTY list, or a check
without a name or a boolean ok is exit 2 naming it — a run that verified nothing is not
a clean gate. A failed check is a BLOCKER like any other fact gate: readiness ≤65,
convergence blocked, cleared by a later clean report. smoke is a FACT kind and never
runs advisory — a check either answered or it did not. Name a check AC-nn ... (the same
tag grammar a JUnit testcase name uses) and a green check inside a passing report closes
that criterion MEASURED; a failed one vetoes it, exactly like a red test.
log-gate --kind smoke --verdict pass|fail|not-run is the parity door for a smoke
measured elsewhere. uscha-kit/templates/scripts/smoke-report-example.json is the
reference report. If the human runs the smoke by hand, they still write the report: a
checklist a human ticked is evidence, a checklist an agent narrated is not.
The PR body cites the gate (phase 6): paste the smoke-ingest verdict line —
<repo>/gate:smoke: PASS — 8/8 checks ok — not a prose claim that the smoke passed.
Phase 8 — Hand off to docs + retrospective
python3 $QL summary # human-readable
python3 $QL summary --json # machine-readable, includes post_merge_calibration
Operability is MEASURED here, not promised (kit 2.2.0, ADR-048). Release by CI, the reset/seed script and the RUNBOOK used to live in this phase as PROSE, and prose is what lets "we do that at the end" survive every gate the kit has. Run it, per repo, BEFORE readiness so the record is in the ledger when the KPI renders:
python3 $QL operability --repo <REPO> # exit 0 always; the gate is the profile's
It reads four FACTS out of the tree and never runs anything: a .github/workflows step
that runs the repo's configured test command, a workflow that publishes or attaches a
release asset, a RUNBOOK.md (docs/ first) naming start/config/rollback/smoke in EN or
ES, and a seed_command the config declares whose script is on disk. Repo path first,
CONFIG ROOT second, and it NAMES which it read. A CI system it does not read (GitLab,
Jenkins, Azure) is reported unknown — never green, never red.
The POSTURE is the risk profile's, not the kit's: on A/B the record is advisory
(caps nothing, blocks nothing, never counted as ok); on C/D/E — where
defaults.operability.gate is true — a missing check is a BLOCKER that caps readiness
≤65, blocks convergence, and makes phase --require pr-ready refuse NAMING the missing
one. Do NOT hand-wave a missing check: write the workflow, write the RUNBOOK section,
declare the seed command. If the project genuinely does not want the gate, that is a
declaration (defaults.operability.gate: false), not a silence.
readiness then prints one conditional line — --- operability: ci ok · release missing · runbook ok · seed missing (advisory) — and that is the line you cite in the PR body.
Readiness KPI — show this after finishing ANY task, not only full runs. It measures the STATE of the result (not effort spent), as a weighted score 0..100 with hard caps:
python3 $QL readiness --acceptance <ACCEPTANCE.md> --tools-per-cycle <count> --record
Always pass --record here (kit 1.47.0): it persists the score to readiness_history
(the mirador time-lapse) and the measured summary + loop odometer — derived phase, pass
count, plateau flag per repo — to ledger["measured"] (what the statusline reads).
Without it the trail starves: the mirador shows "no history yet" and the statusline
falls back to counting checkboxes. Recording is append-only facts, never a gate.
Tick measured-but-unchecked boxes before closing (kit 2.4.0). readiness reports
measured_unchecked (--json) / a · measured but unticked: AC-... line (default view):
AC-IDs the ledger already closed with a green, name-tagged test but whose ACCEPTANCE.md
checkbox is still [ ]. For every ID it lists, flip that box to [x] in ACCEPTANCE.md —
the engine measured it; ticking is bookkeeping, never the other way round. Then re-run
readiness and confirm the list is empty before the close block. This is ONE-DIRECTIONAL:
never tick a box the ledger has not closed (that would be narrating, and readiness
already reports narrated-but-unmeasured boxes separately as narrated_only) — a tick
without a green test stays narrated-only, not measured.
Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
python3 $QL spec-drift --repo <REPO>
It searches the repo's own path first and the CONFIG ROOT (where uscha.config.json and
the ledger live) second, and NAMES which of the two it read — in a monorepo the single
SPEC.md at the root is the spec of every repo, and "no spec documents" used to be
indistinguishable from "no drift" (kit 2.2.0).
Milliseconds, deterministic, exit 0 always — it never gates, so running it every pass
adds zero ceremony. What it adds is VISIBILITY: the run lands in the ledger, so the
mirador card and /uscha-status show drift without anyone remembering the command —
the user this advisory exists for is precisely the one who never types it. If any doc
reads SPEC_STALE, mention it in the close block's Blocks: line as advisory context
(it blocks nothing; it informs the human's next conversation).
Single-verdict view (kit 1.25.0, anti-ceremony). By default readiness is ONE
screen: the verdict line, any conditional warning that actually fired (it speaks only
when it matters), and a --- gates: line that COLLAPSES every persisted gate record —
the per-language linter gates (*-qa-gate from ingest-gate) plus the discrete fact
gates (gate:* from log-gate, rubric:grade, blocker:* from flag-blocker) — into
N ok / M bloqueando (repo/gate...). This
is presentation over facts already in the ledger — it never recomputes the score, so
the KPI is identical to before the rollup existed. When the acceptance file has traceable
AC-IDs, the default view also prints the honest acceptance medido: X% line (kit
1.28.0 — criteria closed by a green name-tagged test / total; the measured_pct field in
--json): the one "% done" the kit can sign, MEASURED not narrated, informational and
never a gate. With no AC-IDs it does not print (a % over bare checkboxes would be
narrated). Pass --verbose to expand the dimensions table, the acceptance/coverage/churn
summary and the per-repo breakdown.
Show the human the default; reach for --verbose only when they ask what is inside the
number.
Dimensions and default weights: acceptance (traced, MEASURED) 30, ADR/checkbox
completion 15, coverage 15, static gate 20, convergence 10, integration 10. A
lint-capable repo whose static gate NEVER ran scores that dimension UNMEASURED (0.0) —
silence is not success. Hard caps override the weighted score: tests red → ≤35,
BLOCKER/CRITICAL open → ≤65, unresolved escalation → ≤75 (held until
resolve-escalation — a recorded event, not an implication). A CONSTITUTION.md
breach does NOT reach the engine by itself: you MUST log it —
flag-blocker --repo <REPO> --kind constitution --note "<invariant breached>" — and
once logged it caps readiness ≤65 and blocks convergence until resolved with
flag-blocker --repo <REPO> --kind constitution --resolve --escape-analysis "Added a regression test for the invariant.". Bands:
<50 NOT READY, 50–79 IN PROGRESS, 80–94 RELEASE CANDIDATE, 95–100 READY.
Acceptance traceability (the DOMINANT dimension — kit 1.10.0). Each ACCEPTANCE
criterion carries a stable ID: - [ ] AC-01 — when X then Y. A criterion counts as
CLOSED only when ≥1 GREEN testcase whose name carries the tag (test_ac1_x,
testAC01X, "AC-01: ..." — IDs normalize by number, AC-01 == AC_1 == ac1) exists
in the ingested JUnit reports AND no tagged testcase is red. Since kit 1.87.0 (ADR-036)
a FAMILY prefix is read the same way: - [ ] AC-BC-07 — ... closes on AC-BC-07_x,
test_ac_bc_7_y or AC_BC_7 (normalized to AC-BC-7; the family needs a separator on
both sides — camelCase testACBC07 is NOT a tag, and AC-7-x is still the bare AC-7). The checkbox is the
NARRATIVE; the testcase is the FACT — a checked box without a green tagged test shows
up as narrated_only and does NOT close (measured beats narrated, per criterion).
A JUnit report older than the repo's source code is treated as STALE (the code changed
after the tests ran) and is DISCARDED — a criterion backed only by stale reports stays
UNMEASURED, never falsely closed or vetoed (kit 1.31.0; surfaced as stale_reports).
Since 1.93.0 (ADR-039) that clock rule is not the only one: a report the clock rejects is still
FRESH when its sha256 matches what the last snapshot recorded for it AND git shows no
source-relevant change since that snapshot's commit — so a clone, a git worktree add, a merge
or a CI checkout, which re-date every file without changing a byte, no longer un-measure green
evidence. Either rule suffices; with no git, no recorded commit or no recorded hash the clock
rule decides alone, exactly as before.
So: when you write the tests for a criterion, put its AC-n in the test name; run
spec-check --acceptance ACCEPTANCE.md up front (zero traceable criteria / duplicate
IDs block as structural FACTS). Files without IDs fall back to the checkbox ratio
with a warning (legacy mode — adopt incrementally).
Heed the two readiness advisories (kit 1.14.0 — they recommend, never gate).
stall: = gated findings flat or RISING for 3 agent cycles: more iteration is not
approaching the solution — stop iterating, go back to the ADR / re-plan WITH the human.
stop-signal: = every repo converged and zero blocking facts remain: what's left is
measurable debt (coverage/acceptance), not findings — raise "candidate to cut and go
to PR" to the human instead of polishing further.
ADR completion is parsed from the acceptance task list (markdown - [x]/- [ ]),
read-only — set the path via config.defaults.acceptance_file or --acceptance. Count
the WHOLE file (the CLI default); only pass --section if you have verified the heading
text matches your template exactly — a mismatched section silently zeroes a heavy
dimension. Present the single-verdict headline (the number, the capping blocker if any,
and the collapsed --- gates: line); add --verbose only if the human wants the
dimension breakdown behind the number. Cycles/regressions are churn (process health) and
are reported separately — they never raise readiness.
Optionally (on request — reporting, not part of the verified build) invoke the uscha-sysdoc
skill to generate the two-view HTML deck. Finish with a retrospective drawn FROM the
ledger summary: total steps, %fixed per tool, coverage, prod LOC vs test LOC, test
count, tests/kLOC, first-time yield (kit 1.27.0 — the Lean % of repos that cleared
QA on the first cycle with no second pass, regression or escalation; passive, derived
from the ledger, informational only — never a gate), plus concrete methodology
improvements.
Escalation contract — STOP and ask the human when:
- The iteration cap is hit without convergence.
- Oscillation is detected (a finding set keeps returning).
- A previously-passing test now fails and the fix is non-trivial.
- Two tools give contradictory directives on the same code.
- A fix would require an architectural decision (ADR-level change).
- A change would breach the
CONSTITUTION.md(an inviolable invariant) — never trade it away; changing the constitution is a separate, explicit human decision.
Record every escalation, and record its CLOSURE — the readiness cap holds until the human resolves it:
python3 $QL escalate --repo <REPO> --reason "<what blocked + what you need from me>"
# ... human reviews/decides ...
python3 $QL resolve-escalation --repo <REPO> --note "<how it was closed>"
A CONSTITUTION breach is escalated AND flagged as a first-class blocker:
python3 $QL flag-blocker --repo <REPO> --kind constitution --note "<invariant breached>"
# after the human decision: flag-blocker --repo <REPO> --kind constitution --resolve
Never auto-merge, never silently exceed the iteration cap, never fix below the gate to make the number look better.