claude-skills/engineering/human-gate/README.md
Claude 5d03ed9e9b
fix(human-gate): pin the round cap in gate state and assert the URL allowances in --sample
Two of three notes from the eighth PR review round; the third needed no change.

1. `--max-rounds` was per-invocation, so `open` and a later `close` could
   disagree about the cap and G5 escalation depended on how the command
   happened to be typed. `open` now records the agreed cap in gate state and
   status/collect/close inherit it. Passing the flag again is an explicit
   renegotiation and prints the change rather than silently overriding.

   Before: `open --max-rounds 2` then `status` -> "max_rounds": 5
   After:  same sequence -> "max_rounds": 2

2. The protocol-relative and own-asset URL allowances were documented in a
   comment but nothing checked them, unlike the void-element and template-token
   regressions which each got a fixture. `--sample` fixtures now carry
   must-keep / must-drop URL assertions alongside the block count, and
   `SAMPLE_HTML` exercises all three cases (`//host/x`, `https:` image,
   `javascript:`). Verified the assertion bites: adding "javascript" to the
   scheme allowlist turns --sample red with `FAIL - kept javascript:`, exit 2.

3. No change for cross-origin `<svg><use href="https://...">`. `href` is
   already in URL_ATTRS and scheme-checked; `https:` is allowed there by the
   same deliberate rule that lets a reviewed page's own `<img src>` load. It is
   not a gap in the allowlist, it is the allowlist working as designed.

Gates: derive_counters --check pass, check_plugin_json --all pass, all three
scripts --help/--sample exit 0, write-a-skill checklist PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01233Eggb2cjSYf96X6C3pCm
2026-08-09 07:14:50 +00:00

151 lines
7.3 KiB
Markdown

# human-gate
**The human-verification lane for an agent loop.**
Machine checks answer *"do the tests pass?"*. This answers the other question:
**has a person actually looked at this, and are their objections resolved?**
```sh
S=engineering/human-gate/skills/human-gate/scripts
python3 $S/human_gate.py --sample # the whole loop, refusals included, in ~1s
```
---
## Why
Human feedback usually arrives as chat prose:
> Change the third paragraph, cut the second card, rewrite the CTA, and the table numbers
> look stale.
Four instructions in one blob. Nothing enumerates them, so nothing detects a miss.
"The third paragraph" drifts as the document changes. Severity is invisible — a wrong
number and a style preference look identical. And round 2 cannot tell what round 1 asked
for.
**human-gate makes feedback an artifact instead of a message:** anchored to blocks,
severity-graded, countable, diffable — and read by a gate that either passes or names
exactly what is still open.
## The loop
```sh
python3 $S/human_gate.py open plan.md --launch # build review page, start round N
# → hand over path, END YOUR TURN
python3 $S/human_gate.py status plan.md # non-blocking: 0 clear · 2 blocked · 3 collect · 4 none
python3 $S/human_gate.py collect plan.md --output json # batch.v1 — apply every item
python3 $S/human_gate.py close plan.md # exit 2 = you are NOT done
```
## What's inside
| File | Purpose |
|---|---|
| `scripts/review_page_builder.py` | Markdown/HTML → single-file review page, every block anchored. The page itself makes **zero network requests** — no CDN, no fonts, no server, no socket; ~11 KB, opens over `file://`. Reviewed HTML is sanitized first: `on*` handlers, `javascript:` URLs (including control-character-smuggled ones), `xlink:href`, `srcdoc`/`srcset`, inline `style` (a `background-image:url(...)` beacons on open), and `iframe`/`object`/`embed`/`base` are dropped, so a draft cannot execute inside the page. A reviewed HTML artifact's own `https:` images still load, as they must for the review to be faithful, and so do protocol-relative (`//host/x`) URLs — neither can execute. Both allowances are pinned by `--sample` assertions, not just by prose. |
| `scripts/feedback_parser.py` | Review sidecar → `batch.v1` JSON, with quote verification against the real file. |
| `scripts/human_gate.py` | State machine + the gate. `open`/`status`/`collect`/`close`/`reset`. |
| `references/human_in_the_loop_canon.md` | Bainbridge, Fagan, Wiegers, Weinberg, Google SWE ch.9, Klein pre-mortem. |
| `references/feedback_batching.md` | W3C Web Annotation selectors, Conventional Comments, GitHub batched-review model. |
| `references/review_loop_discipline.md` | Why there is no blocking poll, why rounds are capped, why exhaustion escalates. |
| `assets/batch.v1.schema.json` | The feedback batch contract. |
| `assets/example_review_sidecar.md` | Worked example of the hand-writable sidecar. |
Stdlib-only Python. No dependencies, no build step.
## The sidecar is hand-writable — on purpose
The review page exports it, but any reviewer with an editor can write it directly. That
is what keeps the loop working over SSH and in CI, where no browser exists:
```markdown
# Review feedback: plan.md
<!-- human-gate:v1 target=plan.md round=1 -->
reviewer: reza
## BLOCKER b2
> We expect a 40% lift in activation.
No source for 40%, and it drives the whole plan. Cite it or cut it.
## EDIT b7
- before: The team will endeavour to deliver incremental value
+ after: The team ships one usable slice per week
## NOTE
Structure is right. Fix the blocker and this is good to go.
```
Severities are **BLOCKER / MAJOR / MINOR / NIT** — the same ladder
`markdown-html/md-review` uses, from Google's *Code Review Developer Guide*. Plus **EDIT**
(a verbatim replacement), **NOTE** (unanchored), **APPROVE** (explicit sign-off).
## The gate
| Rule | Refuses to close when |
|---|---|
| **G1** | no review round collected — nobody has looked |
| **G2** | a BLOCKER or MAJOR is still open |
| **G3** | no named reviewer — approval belongs to a person |
| **G4** | the sidecar changed after the last collect |
| **G5** | round cap exhausted → **escalate**, never pass |
| **G6** | a waiver is used without a recorded reason — and **G1 is never waivable** |
| **G7** | the round carries unresolved integrity problems — a mistyped severity (`## BLOKCER`) silently downgrades to NIT, an EDIT has no replacement text, or a quote is in neither the raw source nor its rendered text |
Overrides are legitimate and must be explicit:
```sh
python3 $S/human_gate.py close plan.md --waive "reviewer on leave; CTO accepted risk in writing"
```
The reason and every refusal it overrode are stored in gate state. **`--waive` cannot
override G1**: it accepts objections a reviewer raised, it cannot manufacture a review that
never happened — otherwise the whole gate would be one flag away from opt-out.
## Loop discipline
There is deliberately **no blocking poll**. `status` returns immediately.
- Open a round, hand over the path, **end the turn**. State lives in `.human-gate/`.
- **Headless guard** — on CI, SSH, or no `DISPLAY`, `open` says so and skips the browser.
- **Round cap** (`--max-rounds`, default 5). Exhaustion exits **5 = ESCALATE**, not 0.
`open` records the cap in gate state and every later command inherits it, so
escalation can't drift with how a command happened to be typed; passing the flag
again renegotiates the cap and says so.
- **`status` previews the whole gate** — it shares `gate_refusals()` with `close`, so the two
can never disagree about whether the work is done.
Reasoning in `references/review_loop_discipline.md`.
## Design notes
Two things this plugin deliberately does **not** ship, and why:
- **No local HTTP server.** A file:// page plus a sidecar covers the review loop without
a listening socket, a token scheme, or a background process to reason about.
- **No rich block-drag / image-paste editor.** That is genuinely nice and genuinely
expensive — it is the bulk of upstream's ~5,200 LOC. If a reviewer wants it, the
optional pinned bridge in `SKILL.md` hands off to upstream, and the gate still governs
closure.
## Related
- **`engineering/agent-harness`** — machine verification and loop control. This is the
human lane it lacks. Pair them.
- **`markdown-html/md-review`** — renders a code review *to* HTML, one-way. Use when
*you* are the reviewer; use human-gate when someone else is.
- **`engineering/grill-me`** — interrogates a plan in conversation, before an artifact exists.
- **`marketing-skill/content-humanizer`**, **`engineering/behuman`** — make AI text sound
human. Different problem: this is human *approval*, not human *voice*.
## Attribution
The batched-review pattern derives from
[`petergyang/human-review`](https://github.com/petergyang/human-review) (MIT © 2026 Peter
Yang). **No upstream code is used** — this is a conceptual derivation with a deliberately
different design (stdlib Python instead of Node, no server, no network fetch, non-blocking
loop with a round cap, plus a closing gate upstream does not have).
The audit that drove those choices — including running upstream's own test suite
(**90/90 passing**) — is at
[`audit/human-review-2026-08/AUDIT.md`](../../audit/human-review-2026-08/AUDIT.md).