Skip to content

fix(hooks): #170 — gate post-preflight capture reminder on implementation intent#570

Merged
jinhongkuan merged 2 commits into
mainfrom
fix/170-capture-impl-intent-gate
Jun 9, 2026
Merged

fix(hooks): #170 — gate post-preflight capture reminder on implementation intent#570
jinhongkuan merged 2 commits into
mainfrom
fix/170-capture-impl-intent-gate

Conversation

@Knapp-Kevin

@Knapp-Kevin Knapp-Kevin commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #170. Supersedes the closed #535.

Problem

The PostToolUse capture reminder (added #168) fires its AskUserQuestion disambiguation on every preflight that surfaces ≥1 decision — including read-only/debug prompts where no refinement is possible. That's the #170 spam.

The #170#175 conflict, and the resolution

#170's original approaches (verb-list / topic-similarity gates) tried to lexically decide "is this a refinement?" — exactly the unreliable judging #175 closed ("a lexical scan can't tell a compatible 'add tests for X' from a refinement smuggled under 'add'"). Lexically suppressing on compatibility is what #535 died on.

Resolution (operator-confirmed): impl-intent gate only. Separate the two concerns:

  • "Is there any implementation work?" — deterministic. Suppress the reminder only when the prompt carries no implementation signal at all (read-only). Safe — a read-only prompt can't be refining anything; judging impl-intent is not judging contradiction.
  • "Is this impl prompt a refinement?" — semantic. Stays with the user (feat(skill): user-disambiguation question before Step 5.6 contradiction capture #175). Never lexically pre-judged.

Mechanism

  • NEW has_implementation_signal(prompt) in preflight_intent.py = verb-regex OR indirect-intent-phrase, independent of SKIP_PATTERNS. should_fire_preflight refactored to delegate (not skip-listed AND has_implementation_signal).
    • Why a new predicate, not should_fire_preflight: the latter skip-lists (fix|add|update|write).*test, so it returns False for "add tests for X". Gating on it would lexically suppress the feat(skill): user-disambiguation question before Step 5.6 contradiction capture #175-forbidden case. has_implementation_signal ignores skip-patterns → "add tests for X" (verb "add") still reaches the user. (This was caught by the failing TDD test — the audit/implement loop working.)
  • _prompt_intent_state.py (new): session-scoped handoff file (system tempdir, sanitized session_id, 24h TTL sweep → GC across session boundaries with no new SessionEnd wiring). The UserPromptSubmit hook persists has_implementation_signal; the PostToolUse capture hook suppresses iff read_intent(...) is False (absent/unreadable → fires; missed capture is irreversible).

Acceptance (issue #170)

  • Gate suppresses the reminder for read-only/tangential-no-signal prompts.
  • e2e/unit fixtures: hard contradiction (impl-intent) → fires; add tests for X → fires + user disambiguates (acceptance (b) revised — lexical suppression rejected per feat(skill): user-disambiguation question before Step 5.6 contradiction capture #175); read-only → suppressed.
  • No regression on the contradiction capture path (Flow-2a / the existing preflight hook tests stay green).
  • New shared state GC'd on a TTL (no orphan tmp files).

Tests

tests/test_post_preflight_capture_reminder.py, wired into test-mcp-regression.yml (the recurring CI-wiring gap; #562/#402/#170 each needed it): suppress-on-read-only, fire-on-impl-intent, end-to-end add-tests-fires (#175 guard), default-fire-on-absent, write-side persistence, TTL sweep, state round-trip. ruff + mypy clean; existing test_preflight_hook.py/test_preflight_intent.py pass unchanged.

Summary by CodeRabbit

  • Improvements

    • Preflight capture reminder now intelligently suppresses notifications when context doesn't indicate implementation intent, reducing unnecessary alerts.
  • Tests

    • Added comprehensive test coverage for conditional reminder behavior, session state persistence, and intent detection.
  • Chores

    • Updated regression testing workflow for enhanced test coverage.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jinhongkuan, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 minutes and 43 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51b4277e-83a9-4db5-9050-3c60f88399b2

📥 Commits

Reviewing files that changed from the base of the PR and between eb8ad70 and 543f5fc.

📒 Files selected for processing (6)
  • .github/workflows/test-mcp-regression.yml
  • scripts/hooks/_prompt_intent_state.py
  • scripts/hooks/post_preflight_capture_reminder.py
  • scripts/hooks/preflight_intent.py
  • scripts/hooks/preflight_reminder.py
  • tests/test_post_preflight_capture_reminder.py
📝 Walkthrough

Walkthrough

This PR implements a conditional gate for the post-preflight capture reminder by detecting code-implementation intent in user prompts. Intent signals are persisted via session state between two hooks: the UserPromptSubmit hook writes a boolean classification, and the PostToolUse hook reads it to decide whether to emit the Step 5.6 disambiguation reminder.

Changes

Implementation-intent gate on post-preflight capture reminder (#170)

Layer / File(s) Summary
Intent signal classification
scripts/hooks/preflight_intent.py
has_implementation_signal(prompt) detects implementation verbs and phrases; should_fire_preflight(prompt) refactored to delegate intent detection and preserve skip-pattern rejection logic.
Session state persistence layer
scripts/hooks/_prompt_intent_state.py
Persist boolean intent flags to session-scoped JSON with sanitized filenames, 24-hour TTL staleness sweep, and best-effort error handling; read_intent() returns `bool
Pre-hook intent persistence (UserPromptSubmit)
scripts/hooks/preflight_reminder.py
Import intent helpers and persist has_implementation_signal(prompt) to state; gate reminder emission on should_fire_preflight(prompt) independently.
Post-hook intent read and suppression (PostToolUse)
scripts/hooks/post_preflight_capture_reminder.py
Import read_intent() and add early-return guard: suppress hookSpecificOutput when intent is False, default to firing if state absent.
Test coverage and CI integration
tests/test_post_preflight_capture_reminder.py, .github/workflows/test-mcp-regression.yml
Comprehensive end-to-end tests for state persistence (round-trip, TTL sweep, None when absent), suppression behavior (reminder suppressed on False, fires on True and when state absent), and pre-hook write-side correctness. New test file added to CI regression matrix.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

agent-needs-human

Suggested reviewers

  • jinhongkuan

Poem

🐰 A session state store takes root,
Intent signals pass the torch between hooks—
UserPromptSubmit writes, PostToolUse reads,
The reminder gate now honors what the prompt truly needs.
No spam for read-only requests, no noise for truth,
Just timely nudges when the user seeks refinement proof. 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and specifically describes the main change: gating the post-preflight capture reminder on implementation intent, directly addressing issue #170.
Linked Issues check ✅ Passed All core coding requirements from #170 and #535 are met: read-only suppression gate implemented via has_implementation_signal, session-scoped handoff via _prompt_intent_state.py with 24h TTL, UserPromptSubmit persists intent, PostToolUse suppresses conditionally, fail-open design preserves #175 invariant, comprehensive end-to-end tests added.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #170/#535 objectives: intent detection refactoring, session state persistence, hook integration, CI wiring, and test coverage. No extraneous modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/170-capture-impl-intent-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_post_preflight_capture_reminder.py (1)

149-149: 💤 Low value

Optional: chained comparison style could be clearer.

The assertion read_intent(sid) == has_implementation_signal(prompt) is True is correct but relies on Python's chained comparison semantics ((a == b) and (b is True)). Consider splitting for clarity:

expected = has_implementation_signal(prompt)
assert expected is True
assert read_intent(sid) is True

This applies to line 157 as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_post_preflight_capture_reminder.py` at line 149, The chained
assertion uses ambiguous comparison semantics; change the assertion using
read_intent(sid) and has_implementation_signal(prompt) into two explicit checks:
evaluate expected = has_implementation_signal(prompt) then assert expected is
True and assert read_intent(sid) is True (repeat the same refactor for the
similar assertion at line 157) so the intent of
has_implementation_signal(prompt) and read_intent(sid) is unambiguous.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/hooks/_prompt_intent_state.py`:
- Around line 7-9: Update the module docstring in
scripts/hooks/_prompt_intent_state.py (lines describing the persisted predicate)
to accurately state that the pre-hook writes the boolean returned by
has_implementation_signal(prompt) (from scripts/hooks/preflight_reminder.py)
keyed by session_id, rather than should_fire_preflight(prompt), so the docstring
matches the actual contract read by the capture hook.
- Around line 70-80: read_intent currently returns a persisted "fire" value
regardless of file age; update read_intent to enforce the same TTL used by
_sweep_stale: after obtaining the path via state_path(session_id) and before
loading JSON, stat the file's mtime and compare to time.time() against
_TTL_SECONDS, and if the file is older than _TTL_SECONDS return None (treat as
absent/stale); otherwise proceed to read and validate the JSON as before,
catching OSError/JSONDecodeError/ValueError and returning None on error.

---

Nitpick comments:
In `@tests/test_post_preflight_capture_reminder.py`:
- Line 149: The chained assertion uses ambiguous comparison semantics; change
the assertion using read_intent(sid) and has_implementation_signal(prompt) into
two explicit checks: evaluate expected = has_implementation_signal(prompt) then
assert expected is True and assert read_intent(sid) is True (repeat the same
refactor for the similar assertion at line 157) so the intent of
has_implementation_signal(prompt) and read_intent(sid) is unambiguous.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a702a41a-b56e-4517-8c88-54ffd431b9d2

📥 Commits

Reviewing files that changed from the base of the PR and between b214e4e and eb8ad70.

📒 Files selected for processing (6)
  • .github/workflows/test-mcp-regression.yml
  • scripts/hooks/_prompt_intent_state.py
  • scripts/hooks/post_preflight_capture_reminder.py
  • scripts/hooks/preflight_intent.py
  • scripts/hooks/preflight_reminder.py
  • tests/test_post_preflight_capture_reminder.py

Comment thread scripts/hooks/_prompt_intent_state.py Outdated
Comment thread scripts/hooks/_prompt_intent_state.py
Knapp-Kevin and others added 2 commits June 9, 2026 18:00
…tion intent

The PostToolUse capture reminder (added #168) fired its disambiguation question
on EVERY preflight that surfaced ≥1 decision — including read-only prompts where
no refinement is possible (#170 spam). Gate it on implementation intent.

Design (operator-confirmed): suppress the reminder ONLY when the originating
prompt carries no implementation signal at all; keep the user-disambiguation for
every implementation-intent prompt — honoring #175 (the agent/lexical scan can't
reliably judge "compatible vs refinement"; "add tests for X" must reach the user).

Mechanism:
- NEW `has_implementation_signal(prompt)` in preflight_intent.py = verb-regex OR
  indirect-intent-phrase, INDEPENDENT of SKIP_PATTERNS. `should_fire_preflight`
  refactored to delegate (`not skip-listed AND has_implementation_signal`).
  Crucial: `should_fire_preflight` skip-lists `(fix|add|update|write).*test`, so
  it returns False for "add tests for X" — gating on it would lexically suppress
  exactly the #175-forbidden case. `has_implementation_signal` ignores skip
  patterns, so "add tests for X" (verb "add") still reaches the user.
- `_prompt_intent_state.py` (new): session-scoped state file (system tempdir,
  sanitized session_id, 24h TTL sweep — GC across session boundaries, no new
  SessionEnd wiring). UserPromptSubmit hook persists has_implementation_signal;
  PostToolUse capture hook suppresses iff `read_intent(...) is False` (absent →
  fires; missed capture is irreversible).

Tests (tests/test_post_preflight_capture_reminder.py, wired into
test-mcp-regression.yml): suppress-on-read-only, fire-on-impl-intent, end-to-end
add-tests-still-fires (#175 guard), default-fire-on-absent-state, write-side
persistence, TTL sweep, state round-trip.

Revises #170 acceptance (b): "add tests for X" is NOT lexically suppressed — it
fires and the user one-click-dismisses (the lexical-suppress approach is what

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jinhongkuan jinhongkuan force-pushed the fix/170-capture-impl-intent-gate branch from 6bafe39 to 543f5fc Compare June 9, 2026 09:01
@jinhongkuan jinhongkuan merged commit a2bc2b3 into main Jun 9, 2026
9 checks passed
@jinhongkuan jinhongkuan deleted the fix/170-capture-impl-intent-gate branch June 9, 2026 09:04
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.

Reduce collision-capture reminder spam from PostToolUse hook on bicameral_preflight

2 participants