Phase 3 (autonomous trigger), deliverable B. Build to the FROZEN INTERFACE in
planning/PHASE_3.md (Implementation design → FROZEN INTERFACE); do not change those shapes.
Reads the pending-ui/<session_id>.json that issue A writes.
What this is
The deterministic enforcement + loop integration: a Stop hook that refuses to let the agent
finish while UI changes are unverified, and routes the three outcomes. This is the phase's
load-bearing correctness.
Definition of done
- A
Stop hook (plugin hooks/hooks.json) reading the hook stdin JSON (session_id,
stop_hook_active), plus pending-ui/<session_id>.json and verify-result/<session_id>.json
per the frozen schemas. Prefer a Python script (python ${CLAUDE_PLUGIN_ROOT}/hooks/…).
- Decision rules (verbatim from PHASE_3.md — get these exactly right):
- UI touched, no matching
verify-result yet → block ({"decision":"block","reason":…} or
exit 2 + stderr), reason naming the files to verify.
outcome == "fail" → block, reason = the detail (expected-vs-actual) so the agent
self-corrects.
outcome == "pass" covering the touched set → allow (exit 0).
outcome == "abstain" → allow, AND surface the abstain detail to the user. Abstain is a
legitimate stopping point that escalates — NOT a reason to keep blocking. This is the single
most important rule: if abstain does not satisfy the gate, an unverifiable change thrashes into
the 8-block cap and then false-passes anyway.
- Idempotent +
stop_hook_active-aware + 8-block-cap-safe: never nag a turn with nothing to
verify; detect re-entry; never burn the block budget in a loop.
- The gate is satisfied only when
covered covers ui_touched; a later edit to a new UI file
re-opens it.
- The mechanism that writes
verify-result/<session_id>.json when UI checks run — a small
addition to the cyclaudes pytest plugin (record outcome on session finish) OR a thin
cyclaudes verify wrapper. Your call, behind the frozen file contract.
- Bounded retry: cap the correct→verify cycles; on exhaustion, escalate (allow stop + surface
a clear "could not satisfy after N tries" message) rather than looping invisibly.
- Instrumentation: pass/fail/abstain counters persisted for audit, so early runs can be checked
for swallowed abstentions.
Tests
- Each decision branch: block-when-unverified, block-on-fail (reason carries the diff),
allow-on-pass, allow-and-escalate-on-abstain.
stop_hook_active re-entry does not thrash.
- Coverage: a new UI file after a pass re-opens the gate.
- The verify-result writer produces the frozen schema for pass/fail/abstain.
- Retry exhaustion escalates rather than blocking forever.
Note
Sibling issue A concurrently creates hooks/hooks.json; you both add an entry — keep your Stop
entry localized, a rebase on that one file is expected and fine.
Phase 3 (autonomous trigger), deliverable B. Build to the FROZEN INTERFACE in
planning/PHASE_3.md(Implementation design → FROZEN INTERFACE); do not change those shapes.Reads the
pending-ui/<session_id>.jsonthat issue A writes.What this is
The deterministic enforcement + loop integration: a
Stophook that refuses to let the agentfinish while UI changes are unverified, and routes the three outcomes. This is the phase's
load-bearing correctness.
Definition of done
Stophook (pluginhooks/hooks.json) reading the hook stdin JSON (session_id,stop_hook_active), pluspending-ui/<session_id>.jsonandverify-result/<session_id>.jsonper the frozen schemas. Prefer a Python script (
python ${CLAUDE_PLUGIN_ROOT}/hooks/…).verify-resultyet → block ({"decision":"block","reason":…}orexit 2 + stderr), reason naming the files to verify.
outcome == "fail"→ block, reason = thedetail(expected-vs-actual) so the agentself-corrects.
outcome == "pass"covering the touched set → allow (exit 0).outcome == "abstain"→ allow, AND surface the abstaindetailto the user. Abstain is alegitimate stopping point that escalates — NOT a reason to keep blocking. This is the single
most important rule: if abstain does not satisfy the gate, an unverifiable change thrashes into
the 8-block cap and then false-passes anyway.
stop_hook_active-aware + 8-block-cap-safe: never nag a turn with nothing toverify; detect re-entry; never burn the block budget in a loop.
coveredcoversui_touched; a later edit to a new UI filere-opens it.
verify-result/<session_id>.jsonwhen UI checks run — a smalladdition to the cyclaudes pytest plugin (record outcome on session finish) OR a thin
cyclaudes verifywrapper. Your call, behind the frozen file contract.a clear "could not satisfy after N tries" message) rather than looping invisibly.
for swallowed abstentions.
Tests
allow-on-pass, allow-and-escalate-on-abstain.
stop_hook_activere-entry does not thrash.Note
Sibling issue A concurrently creates
hooks/hooks.json; you both add an entry — keep your Stopentry localized, a rebase on that one file is expected and fine.