Skip to content

feat(phase3-A): PostToolUse relevance detector + session-state protocol - #35

Merged
CameronCrow merged 1 commit into
mainfrom
feat/31-postooluse-relevance
Jul 22, 2026
Merged

CameronCrow merged 1 commit into
mainfrom
feat/31-postooluse-relevance

Conversation

@CameronCrow

Copy link
Copy Markdown
Owner

Summary

Phase 3, deliverable A — the cheap, deterministic relevance test half of the
autonomous trigger. Implements exactly the A decomposition in
planning/PHASE_3.md (Implementation design → FROZEN INTERFACE), building to the
frozen state-file schema issue #32's Stop hook is built against in parallel.

  • hooks/flag_ui_change.py — a PostToolUse-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.
    • Reads tool_input.file_path from the hook stdin JSON.
    • Matches it against a default UI-glob set (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).
    • On a match, appends the de-duplicated repo-relative path to
      <project>/.cyclaudes/pending-ui/<session_id>.json:
      { "session_id": "...", "ui_touched": ["relpath/one.tsx", "relpath/two.xaml"] }
      Creates the dir/file on first write; read-modify-write preserves existing
      entries; idempotent on repeat edits of the same file.
    • No-op (exit 0) on a non-UI path or an incomplete payload — never raises, never
      disrupts the tool call (PostToolUse can't block anyway; this only flags).
  • hooks/hooks.json (new) — registers the hook with matcher Edit|Write. Kept
    minimal/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 lives
    outside the cyclaudes package on purpose, so the bare python ${CLAUDE_PLUGIN_ROOT}/hooks/flag_ui_change.py invocation needs no install step)
    and exercises flag() directly against tmp_path project dirs:
    • UI path → state file contains the repo-relative path.
    • Non-UI path → no-op, no state file.
    • Dedup (same file twice → one entry) and accumulation (distinct UI files).
    • Two session_ids → separate files, no cross-contamination.
    • Glob override → changes what matches.
    • Missing session_id/file_path → no-op.

Does not touch the Stop hook, verify-result/*.json, or anything outside
hooks/, hooks.json, .gitignore, planning/TODO.md, and tests/ — scoped
strictly to the "A" decomposition so the hooks.json collision with issue #32 stays
a trivial rebase.

Closes #31

Test plan

  • python -m pytest tests/test_flag_ui_change.py -p no:cacheprovider -v — 7
    passed
  • python -m pytest tests/ -p no:cacheprovider — full suite, 155 passed, 6
    deselected (live, unaffected by this change)
  • No pip install -e . run, per instructions (shared site-packages)

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
CameronCrow merged commit a96319a into main Jul 22, 2026
1 check passed
@CameronCrow
CameronCrow deleted the feat/31-postooluse-relevance branch July 22, 2026 21:56
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(phase3-A): PostToolUse relevance detector + session-state protocol

1 participant