I sat a designer in front of one review card. An agent had rewritten a hover state overnight. The card asked for a yes. Tab order proof was empty. Who owns that yes? The designer does. The consequence is a keyboard trap in production. The point of reversibility is this card, not a later hotfix.
This is a from-zero research tutorial. It is a protocol, not a shipped study. I label every check as unexecuted until you run it. I will not treat a fluent agent as a witness. Fluency is not evidence.
Why this decision, and not another? Agents love visual states. They describe hover, press, and selected. They forget the path a Tab key must walk. A pretty state with no focus proof is a locked door. Would you approve a door you cannot open?
Name the decision before you name the tool
Write the decision in one sentence. Keep the owner visible. Keep the stop condition visible. I use this line and nothing softer.
DECISION: May this agent state ship to the design system?
OWNER: product designer on the review card
CONSEQUENCE: keyboard users lose a recoverable path
REVERSIBILITY: reject now; do not patch after merge
STOP IF: tab_order_proof is empty OR focus_visible is unproven
NOISE IF: model confidence, token spend, or poetic rationale
Verify this stage. Read the line aloud. If you cannot name the owner, stop. If you cannot name the stop, stop. A tool cannot rescue a nameless decision.
I rehearse the protocol on a cheap bed. MonkeyCode offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I do not need a named model. I do not need a quota story. I need a place to fail the card in public.
Stage 1: scaffold a local review record
Create a folder. Treat it as the only source of truth. Do not keep proof in chat scrollback. Chat is a hallway. Hallways forget.
mkdir -p agent-state-review/records agent-state-review/fixtures
cd agent-state-review
printf '%s\n' 'review protocol v1' > README.txt
ls -la
Verify this stage. ls must show records and fixtures. If a file lives only in a model window, it does not exist. Would you defend that file in an incident review?
Now write the card schema. These fields are interface copy, not decoration. Evidence stays separate from guesses.
{
"component": "FilterChip",
"proposed_state": "hover-and-selected",
"decision_owner": "",
"tab_order_proof": {
"status": "empty",
"from_control": "",
"to_control": "",
"escape_control": ""
},
"focus_visible_proof": {
"status": "empty",
"contrast_note": "",
"not_obscured_note": ""
},
"hand_back": {
"needed": true,
"reason": ""
},
"hypothesis": "",
"approval": "blocked"
}
Save it as records/card.schema.json. Verify the keys exist with a dry read.
python3 - <<'PY'
import json
from pathlib import Path
card = json.loads(Path('records/card.schema.json').read_text())
need = ['decision_owner','tab_order_proof','focus_visible_proof','hand_back','hypothesis','approval']
missing = [k for k in need if k not in card]
print('missing', missing or 'none')
print('approval', card['approval'])
assert card['approval'] == 'blocked'
PY
If that assert fails, your default is already too kind. Kind defaults leak traps.
Stage 2: fill a failed scenario, not a happy path
Happy paths teach almost nothing. I seed a chip that looks selected on hover. The agent wrote a glow. It never said where Tab lands. It never said how Escape returns. That silence is the finding.
{
"component": "FilterChip",
"proposed_state": "hover-and-selected",
"decision_owner": "product-designer",
"tab_order_proof": {
"status": "empty",
"from_control": "",
"to_control": "",
"escape_control": ""
},
"focus_visible_proof": {
"status": "empty",
"contrast_note": "agent described a glow only",
"not_obscured_note": ""
},
"hand_back": {
"needed": true,
"reason": "no keyboard path, no unobscured focus"
},
"hypothesis": "users will understand selection from color alone",
"approval": "blocked"
}
Save as fixtures/filterchip-hover-selected.failed.json. Notice the split. The hypothesis can be poetic. The proof cannot. Color is not a path. A glow is not a focus ring.
Verify this stage with a diff of statuses.
python3 - <<'PY'
import json
from pathlib import Path
fx = json.loads(Path('fixtures/filterchip-hover-selected.failed.json').read_text())
print('tab', fx['tab_order_proof']['status'])
print('focus', fx['focus_visible_proof']['status'])
print('hand_back', fx['hand_back']['needed'])
assert fx['tab_order_proof']['status'] == 'empty'
assert fx['approval'] == 'blocked'
print('failed scenario is honest')
PY
If the fixture already says approved, you staged theater. Theater is not research.
Stage 3: encode stop conditions as a checker
People forgive empty fields under time pressure. A checker does not. This script is a proposed gate. Label it unexecuted until you run it on your card.
# check_review_card.py — protocol checker, not a product claim
import json, sys
from pathlib import Path
NOISE = ("confidence", "tokens", "rationale_poem")
def load(p):
return json.loads(Path(p).read_text())
def stop_reasons(card):
reasons = []
tab = card.get("tab_order_proof", {})
focus = card.get("focus_visible_proof", {})
if not card.get("decision_owner"):
reasons.append("no decision owner")
if tab.get("status") != "proven":
reasons.append("tab order unproven")
if not tab.get("escape_control"):
reasons.append("no escape control")
if focus.get("status") != "proven":
reasons.append("focus visible unproven")
if not focus.get("not_obscured_note"):
reasons.append("focus may be obscured")
if card.get("approval") == "approved" and reasons:
reasons.append("approval contradicts empty proof")
return reasons
def main(path):
card = load(path)
reasons = stop_reasons(card)
print("component", card.get("component"))
print("hypothesis_is_not_evidence", bool(card.get("hypothesis")))
if reasons:
print("STOP")
for r in reasons:
print("-", r)
sys.exit(2)
print("PASS: proof present, still needs a human yes")
if __name__ == "__main__":
main(sys.argv[1])
Run it on the failed fixture. Expect a stop. A green check here would be a bug in the protocol.
python3 check_review_card.py fixtures/filterchip-hover-selected.failed.json
echo "exit:$?"
Verify this stage. Exit code must be 2. The word STOP must print. If you only read the hypothesis, you approved a rumor.
What extra fields would only add noise? Model confidence. Token spend. A paragraph about being helpful. Those fields soothe the reviewer. They do not move a Tab key. I keep them out of the card on purpose.
Stage 4: walk the user flow, then the keyboard flow
The visual flow is the easy story. A person clicks a chip. The chip glows. Filters update. That story is incomplete. The keyboard flow is the real product.
flowchart TD
A[Reviewer opens agent state card] --> B{tab_order_proof proven?}
B -->|no| C[Stop. Hand back to designer]
B -->|yes| D{focus visible and unobscured?}
D -->|no| C
D -->|yes| E{escape control named?}
E -->|no| C
E -->|yes| F[Human may approve or still refuse]
C --> G[Agent may retry only inside the card]
F --> H[Record discarded paths beside the yes]
Read the diagram as a conversation. The agent speaks first. The card interrupts. The human speaks last. If the human speaks first, you have theater again.
I still need a keyboard script. This is a research script, not a browser driver. A facilitator reads it. A participant uses only Tab, Shift+Tab, Enter, and Escape. No mouse. No trackpad. No charity.
SCENARIO: Approve or reject FilterChip hover-and-selected
START: focus on the search field before the chip row
TASK: select Cuisine, then leave the row without a mouse
SUCCESS: chip is selected, focus ring visible, escape returns to search
STOP: focus disappears, order skips the chip, or escape does nothing
DO NOT HINT: do not point at the glow
Verify this stage by timing silence. If the facilitator explains the glow, the scenario is contaminated. Contaminated runs cannot support approval.
Which missing evidence should stop approval? A blank from_control. A blank escape_control. A focus ring that a sticky header covers. Which extra information only adds noise? Screenshots of hover without focus. A transcript of the model apologizing. An aesthetic preference from a sighted reviewer.
Stage 5: accessibility review as a gate, not a garnish
I ground the gate in published criteria, not vibes. WCAG 2.2 Success Criterion 2.4.7 Focus Visible asks that the keyboard focus indicator can be seen. WCAG 2.2 Success Criterion 2.4.11 Focus Not Obscured (Minimum) asks that the focused item is not entirely hidden. Those pages are the primary sources. I am not inventing a third rule.
Map each criterion onto a card field. Then refuse to merge on a mapped miss.
2.4.7 -> focus_visible_proof.status must equal proven
2.4.11 -> focus_visible_proof.not_obscured_note must name the overlapping chrome
2.1.1 -> tab_order_proof must name from, to, and escape
recovery -> hand_back.reason must stay in the record after a retry
A proposed pass fixture looks like this. It is still not an approval. Proof only unlocks a human decision.
{
"component": "FilterChip",
"proposed_state": "hover-and-selected",
"decision_owner": "product-designer",
"tab_order_proof": {
"status": "proven",
"from_control": "SearchInput",
"to_control": "FilterChip:Cuisine",
"escape_control": "SearchInput"
},
"focus_visible_proof": {
"status": "proven",
"contrast_note": "2px ring against surface, not hue-only",
"not_obscured_note": "ring clears sticky filter header"
},
"hand_back": {
"needed": false,
"reason": "keyboard path demonstrated in scenario"
},
"hypothesis": "users will understand selection from color alone",
"approval": "blocked"
}
See the last field? I still keep approval blocked. The checker can pass proof and still refuse to ship. A human must type the yes. Why? Because proof of a path is not proof of the right path.
python3 check_review_card.py fixtures/filterchip-hover-selected.proof.json
echo "exit:$?"
Verify this stage. Exit code should be 0 only when proof fields are complete. approval may remain blocked. That tension is the product.
Stage 6: hand-back and keep the discarded path
When the checker stops, the agent does not get the canvas. It gets the card. The designer writes the missing path in the same record. The failed order stays visible. Discarded paths are not shame. They are the map of near misses.
HAND_BACK
- agent may fill tab_order_proof only
- agent may not flip approval
- designer may accept, rewrite, or kill the state
- discarded order remains under records/history/
Copy the failed fixture into history before any retry. If you overwrite, you teach the team to hide bruises.
mkdir -p records/history
cp fixtures/filterchip-hover-selected.failed.json \
records/history/filterchip-hover-selected.failed.001.json
ls records/history
Verify this stage. History must contain the empty proof. A later pass without that file is amnesia. Amnesia ships the same trap twice.
The recovery frame is small on purpose. Stop. Show the blank slots. Hand the work back. Keep the bruise. Resume only inside the card. That is the whole loop. It is closer to a coat check than a launch sequence. You do not walk out wearing a coat you never claimed.
What this protocol is not
It is not a benchmark of free models. I did not time tokens. I did not rank vendors. It is not a frontend implementation guide. Component code belongs elsewhere. It is not a claim that a free server makes states accessible. A server hosts the rehearsal. It does not see for you.
Who should not use this approach? Teams that already merge agent states from chat. Teams that treat confidence as contrast. Teams that cannot name a decision owner. Teams that need statistical usability claims. This tutorial does not produce those claims. It produces a stop.
Limitations sit in the open. The checker cannot detect a real obscured ring. A human still looks. The scenario cannot cover every control. You still sample. The free rehearsal bed can vanish or change. I do not claim duration, hardware, or permanence. If the bed moves, the card schema still travels. That is the point of putting proof in files.
I keep asking the same two questions at the end of every review. Which missing evidence should stop approval? Which extra information would only add noise? If your room cannot answer both, you are not ready for an agent to touch state. You are ready to keep the card blank, and that is honest work.
Top comments (0)