From 280b4c71acf376a1effdfdfe1da01166baa42d1e Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:16:13 +0900 Subject: [PATCH] fix(hooks): make the recall opener survive tool use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit real-world test: on tool-heavy prompts the model explored the codebase and dropped the soft "open your reply with" instruction from its final message, so recall ran invisibly — the exact symptom the banners exist to kill. verified on a live project: memory-shaped prompts opened with "From vouch memory:", tool-heavy ones did not. state the opener as a hard output contract in all three banners: the final reply MUST open with the exact words, even if the model uses tools or explores first. re-verified end-to-end on the same project — the previously-failing prompt now opens with "From vouch memory:", grounds in cited ids, and still does its tool exploration afterwards. --- src/vouch/hooks.py | 23 ++++++++++++++++------- tests/test_hooks.py | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/vouch/hooks.py b/src/vouch/hooks.py index 6b1276b4..79d8dbf8 100644 --- a/src/vouch/hooks.py +++ b/src/vouch/hooks.py @@ -185,8 +185,9 @@ def build_claude_prompt_hook(store: KBStore, stdin_text: str) -> str: # was consulted, not silence that looks like vouch did nothing. return _envelope( "[vouch memory] I searched the project's vouch knowledge base for this " - 'prompt and found nothing relevant. Open your reply with "Nothing in ' - 'vouch on this." then answer from your own knowledge.' + "prompt and found nothing relevant. Your final reply MUST open with " + 'the exact words "Nothing in vouch on this." — even if you use tools ' + "or explore the codebase first — then answer from your own knowledge." ) sc_enabled, min_conf = short_circuit_cfg(cfg) @@ -201,16 +202,24 @@ def build_claude_prompt_hook(store: KBStore, stdin_text: str) -> str: 'prompt, reply with ONLY "From vouch memory:" then the relevant ' "item(s) verbatim and their [ev-...] id(s), and STOP -- no extra " "reasoning, caveats, or tool calls. If they do NOT fully answer it, " - 'continue normally: open with "From vouch memory:" and ground in them.' + "continue normally: ground in them, and your final reply MUST still " + 'open with "From vouch memory:" even after tool use.' "\n\n" + body ) else: + # The opener is a hard output contract, not a suggestion: models that + # explore with tools before answering routinely drop soft "open your + # reply with" phrasing from their final message (observed in the field + # on tool-heavy prompts), so state it as a MUST that survives tool use. block = ( "[vouch memory] I searched the project's vouch knowledge base for this " - "prompt. Approved, cited items are below. Before you reason on your own: " - 'open your reply with "From vouch memory:" and ground your answer in the ' - "relevant item(s), citing each id in [brackets]. If none are actually " - 'relevant, say "Nothing relevant in vouch on this" and then answer from ' + "prompt. Approved, cited items are below — check them BEFORE reasoning " + "or exploring on your own, and ground your answer in the relevant " + "item(s), citing each id in [brackets]. Your final reply MUST open " + 'with the exact words "From vouch memory:" — even if you use tools or ' + "explore the codebase first, that opener comes before everything else " + "in your answer. If none of the items are actually relevant, open " + 'with "Nothing relevant in vouch on this" instead and answer from ' "your own knowledge.\n\n" + body ) return _envelope(block) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 12d69b0a..c5262a3f 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -55,7 +55,7 @@ def test_relevant_prompt_banner_instructs_from_vouch_memory( out = hooks.build_claude_prompt_hook(store, json.dumps({"prompt": "when do deploys run"})) ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"] assert ctx.startswith("[vouch memory]") - assert 'open your reply with "From vouch memory:"' in ctx + assert 'MUST open with the exact words "From vouch memory:"' in ctx def test_raw_non_json_stdin_is_tolerated(store: KBStore, monkeypatch: pytest.MonkeyPatch) -> None: @@ -117,7 +117,7 @@ def test_short_circuit_never_fires_for_action_prompts( out = hooks.build_claude_prompt_hook(store, json.dumps({"prompt": "fix the deploy pipeline"})) ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"] assert "high-confidence match" not in ctx - assert 'open your reply with "From vouch memory:"' in ctx + assert 'MUST open with the exact words "From vouch memory:"' in ctx def test_short_circuit_off_by_default(