feat(phase3-A): PostToolUse relevance detector + session-state protocol - #35
Merged
Merged
Conversation
Adds the cheap, deterministic half of the Phase 3 autonomous trigger: a PostToolUse hook (matcher Edit|Write) that matches tool_input.file_path against a per-repo UI-glob set and, on a match, records the de-duplicated repo-relative path in .cyclaudes/pending-ui/<session_id>.json per the frozen schema in planning/PHASE_3.md, for the Stop hook (issue #32) to read. No-op on non-UI paths and never blocks the tool call. Closes #31 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CameronCrow
added a commit
that referenced
this pull request
Jul 23, 2026
#33) The cohesive Phase-3 acceptance proof, closing deliverable C now that A (#35) and B (#32) have landed. Drives the three real cores wired exactly as Claude Code runs them — PostToolUse `flag_ui_change.flag` -> `verify_result.write_result` (the `cyclaudes verify` writer) -> Stop `stop_gate.decide` — against a temp project with synthetic hook payloads (no live Claude Code runtime needed for the deterministic contract). Scenarios (one per PHASE_3.md success criterion): - Happy unattended cycle: UI edit -> flag records pending-ui -> Stop blocks (nothing verified) -> pass covering the touched file -> Stop allows. Zero human input (criterion 1). - Self-correct loop: fail -> Stop blocks WITH the expected-vs-actual diff -> agent fixes -> re-verify pass -> allow; the break is caught, never reclassified as a pass (criterion 3). - Non-UI change: does not flag and the Stop gate does not block on it — the cheap relevance guard (criterion 4). - Abstain escalates, never thrashes (the load-bearing rule, criterion 2): an abstain makes Stop ALLOW + escalate; 12 re-entries stay block_count == 0 and it is tallied once, proving it can't consume the block budget toward the 8-consecutive-block cap and false-pass there. - Fail bounded retry: blocks with the diff up to CYCLAUDES_RETRY_CAP, then escalates (allow + "after N attempts") instead of blocking forever. - Wiring honesty: the exit-code -> outcome mapping can't silently reclassify a real failure as pass/abstain, and the writer rejects an out-of-contract outcome. Deterministic/fake-driven; green under default `python -m pytest` (239 passed, 6 deselected). TODO + PLAN_MAIN Phase-3 status updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CameronCrow
added a commit
that referenced
this pull request
Jul 23, 2026
#33) (#38) The cohesive Phase-3 acceptance proof, closing deliverable C now that A (#35) and B (#32) have landed. Drives the three real cores wired exactly as Claude Code runs them — PostToolUse `flag_ui_change.flag` -> `verify_result.write_result` (the `cyclaudes verify` writer) -> Stop `stop_gate.decide` — against a temp project with synthetic hook payloads (no live Claude Code runtime needed for the deterministic contract). Scenarios (one per PHASE_3.md success criterion): - Happy unattended cycle: UI edit -> flag records pending-ui -> Stop blocks (nothing verified) -> pass covering the touched file -> Stop allows. Zero human input (criterion 1). - Self-correct loop: fail -> Stop blocks WITH the expected-vs-actual diff -> agent fixes -> re-verify pass -> allow; the break is caught, never reclassified as a pass (criterion 3). - Non-UI change: does not flag and the Stop gate does not block on it — the cheap relevance guard (criterion 4). - Abstain escalates, never thrashes (the load-bearing rule, criterion 2): an abstain makes Stop ALLOW + escalate; 12 re-entries stay block_count == 0 and it is tallied once, proving it can't consume the block budget toward the 8-consecutive-block cap and false-pass there. - Fail bounded retry: blocks with the diff up to CYCLAUDES_RETRY_CAP, then escalates (allow + "after N attempts") instead of blocking forever. - Wiring honesty: the exit-code -> outcome mapping can't silently reclassify a real failure as pass/abstain, and the writer rejects an out-of-contract outcome. Deterministic/fake-driven; green under default `python -m pytest` (239 passed, 6 deselected). TODO + PLAN_MAIN Phase-3 status updated. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3, deliverable A — the cheap, deterministic relevance test half of the
autonomous trigger. Implements exactly the
Adecomposition inplanning/PHASE_3.md(Implementation design → FROZEN INTERFACE), building to thefrozen state-file schema issue #32's Stop hook is built against in parallel.
hooks/flag_ui_change.py— aPostToolUse-invoked Python script (portable;Windows-primary repo, so no bash). Core logic lives in an importable
flag(payload, project_dir)function; the__main__block is a thin stdin/stdout adapter.tool_input.file_pathfrom the hook stdin JSON.ui/**,**/*.tsx,**/*.jsx,**/*.xaml,**/*.css,frontend/**), overridable per-repo via.cyclaudes/ui-globs.txt(one glob per line,#comments/blank lines ignored;presence replaces the defaults entirely — documented in the module docstring).
<project>/.cyclaudes/pending-ui/<session_id>.json:{ "session_id": "...", "ui_touched": ["relpath/one.tsx", "relpath/two.xaml"] }entries; idempotent on repeat edits of the same file.
disrupts the tool call (
PostToolUsecan't block anyway; this only flags).hooks/hooks.json(new) — registers the hook with matcherEdit|Write. Keptminimal/localized per the issue's instruction, since the Stop hook (issue feat(phase3-B): Stop-gate enforcement, three-outcome routing, bounded retry + instrumentation #32)
will add its own entry to the same file in parallel.
.gitignore— adds.cyclaudes/(the session-state directory).planning/TODO.md— checks off the Phase 3 "Trigger + cheap relevance test"line with a pointer to what landed.
tests/test_flag_ui_change.py— loads the hook script by file path (it livesoutside the
cyclaudespackage on purpose, so the barepython ${CLAUDE_PLUGIN_ROOT}/hooks/flag_ui_change.pyinvocation needs no install step)and exercises
flag()directly againsttmp_pathproject dirs:session_ids → separate files, no cross-contamination.session_id/file_path→ no-op.Does not touch the Stop hook,
verify-result/*.json, or anything outsidehooks/,hooks.json,.gitignore,planning/TODO.md, andtests/— scopedstrictly to the "A" decomposition so the
hooks.jsoncollision with issue #32 staysa trivial rebase.
Closes #31
Test plan
python -m pytest tests/test_flag_ui_change.py -p no:cacheprovider -v— 7passed
python -m pytest tests/ -p no:cacheprovider— full suite, 155 passed, 6deselected (
live, unaffected by this change)pip install -e .run, per instructions (shared site-packages)