From 0083d0334af2e450a234d7e5f131437796adc7b5 Mon Sep 17 00:00:00 2001 From: Daedalus Date: Sat, 25 Jul 2026 17:43:12 +0000 Subject: [PATCH] fix(claude-code): surface advisory Stop findings --- src/claude-code.js | 1 + test/claude-code.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/claude-code.js b/src/claude-code.js index 329335f..576b922 100644 --- a/src/claude-code.js +++ b/src/claude-code.js @@ -324,6 +324,7 @@ function handleClaudeHookLocked(input, { projectDir, packageRoot }) { appendTrace(traces, decision.trace); if (decision.block) return { decision: 'block', reason: decision.message }; if (decision.trace?.receipt?.anti_lockup_downgrade) return { systemMessage: decision.message }; + if (engine.mode === 'advisory' && decision.message) return { systemMessage: decision.message }; return {}; } diff --git a/test/claude-code.test.js b/test/claude-code.test.js index c199117..ddce9fd 100644 --- a/test/claude-code.test.js +++ b/test/claude-code.test.js @@ -89,6 +89,38 @@ test('advisory PostToolUse feedback never blocks the Claude loop', () => { assert.match(feedback.hookSpecificOutput.additionalContext, /Receipt failed/); }); +test('advisory Stop surfaces an unmet obligation without blocking', () => { + const target = project(); + installClaudeCode({ projectDir: target, packageRoot: root, mode: 'advisory' }); + handleClaudeHook(event(target, 'UserPromptSubmit', { prompt: 'Who calls performSync?' }), { projectDir: target, packageRoot: root }); + + const stopped = handleClaudeHook(event(target, 'Stop', { + stop_hook_active: false, last_assistant_message: 'Done.', + }), { projectDir: target, packageRoot: root }); + + assert.equal(stopped.decision, undefined); + assert.equal(stopped.hookSpecificOutput, undefined); + assert.match(stopped.systemMessage, /Unmet obligation/); +}); + +test('advisory Stop stays quiet when every obligation is satisfied', () => { + const target = project(); + installClaudeCode({ projectDir: target, packageRoot: root, mode: 'advisory' }); + handleClaudeHook(event(target, 'UserPromptSubmit', { prompt: 'Who calls performSync?' }), { projectDir: target, packageRoot: root }); + const graphInput = event(target, 'PreToolUse', { + tool_name: 'mcp__gbrain__code_callers', tool_input: { symbol: 'performSync' }, tool_use_id: 'tool-right', + }); + handleClaudeHook(graphInput, { projectDir: target, packageRoot: root }); + handleClaudeHook(event(target, 'PostToolUse', { + tool_name: 'mcp__gbrain__code_callers', tool_input: { symbol: 'performSync' }, + tool_response: { callers: ['runSync calls performSync'] }, tool_use_id: 'tool-right', + }), { projectDir: target, packageRoot: root }); + + assert.deepEqual(handleClaudeHook(event(target, 'Stop', { + stop_hook_active: false, last_assistant_message: 'Verified callers.', + }), { projectDir: target, packageRoot: root }), {}); +}); + test('UserPromptSubmit injects a compact route block and stays silent without obligations', () => { const target = project(); installClaudeCode({ projectDir: target, packageRoot: root, mode: 'enforce' });