From 78d4adb86cb8d226203d1d2f2d3a6e1e6ae2d8d3 Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 25 Feb 2026 14:35:38 +0100 Subject: [PATCH 1/3] =?UTF-8?q?fix(hooks):=20Claude=20never=20saw=20hook?= =?UTF-8?q?=20messages=20=E2=80=94=20systemMessage=20is=20user-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: Both hookify and metacognitive-guard used `systemMessage` to communicate with Claude when blocking tools. Per Claude Code hooks spec, `systemMessage` is "shown to the user" only. Claude receives `permissionDecisionReason` (PreToolUse), `reason` (Stop/PostToolUse), `additionalContext` (UserPromptSubmit/PostToolUse/SessionStart), or stderr with exit code 2. hookify/rule_engine.py: - PreToolUse deny: systemMessage → permissionDecisionReason - PostToolUse block: systemMessage → decision/reason - UserPromptSubmit block: systemMessage → decision/reason - Warnings: systemMessage → additionalContext where Claude can see it - Removed all systemMessage usage (no audience) hookify/hook_runner.py: - Error reporting: systemMessage + exit 0 → stderr + exit 2 hookify/mtp-smart-test-filtering: - Rewrote 80-line MTP docs wall → 8-line directive message metacognitive-guard/epistemic-guard.sh: - Migrated 5 blocks from deprecated decision/reason to modern hookSpecificOutput.permissionDecision/permissionDecisionReason metacognitive-guard/integrity-check.sh: - Exit code 1 (non-blocking!) → exit code 2 (actually blocks commits) - stdout (invisible) → stderr (Claude sees it) metacognitive-guard/struggle-detector: - Split into two hooks: Stop (async, analysis + blackboard write) and new struggle-inject.sh on UserPromptSubmit (reads blackboard, injects additionalContext that Claude actually sees) Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 9 ++++ plugins/hookify/CLAUDE.md | 2 +- .../hookify.mtp-smart-test.local.md | 12 +++-- plugins/hookify/hookify/core/hook_runner.py | 12 ++--- plugins/hookify/hookify/core/rule_engine.py | 49 ++++++++++++++----- plugins/metacognitive-guard/CLAUDE.md | 5 +- plugins/metacognitive-guard/hooks/hooks.json | 12 +++++ .../hooks/scripts/epistemic-guard.sh | 40 +++++++++------ .../hooks/scripts/integrity-check.sh | 11 +++-- .../hooks/scripts/struggle-detector.sh | 17 ++----- .../hooks/scripts/struggle-inject.sh | 49 +++++++++++++++++++ 11 files changed, 159 insertions(+), 59 deletions(-) create mode 100755 plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 49abd66..b2e3516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Fixed + +- **`hookify/rule_engine.py`**: Fixed PreToolUse deny putting message in `systemMessage` (user-only display) instead of `permissionDecisionReason` (the field Claude actually receives). Root cause of Claude never seeing hook guidance when tools are blocked. Also split PreToolUse/PostToolUse into separate branches with correct response formats per Claude Code hooks spec +- **`hookify/hook_runner.py`**: Fixed error reporting — hookify crashes now use exit code 2 + stderr (Claude-visible) instead of `systemMessage` (user-only) +- **`hookify/global-rules/mtp-smart-test-filtering`**: Rewrote rule message to be directive — shorter (8 lines vs 80) with explicit "pick one and retry" action items +- **`metacognitive-guard/epistemic-guard.sh`**: Migrated all 5 blocks from deprecated `decision`/`reason` format to modern `hookSpecificOutput.permissionDecision`/`permissionDecisionReason`. Removed dead `message` field that was ignored by Claude Code +- **`metacognitive-guard/integrity-check.sh`**: Fixed exit code 1 (non-blocking, commit proceeds) → exit code 2 (blocking, stderr fed to Claude). Violations now actually block commits instead of being silently ignored +- **`metacognitive-guard/struggle-detector`**: Split into two hooks — async Stop (analysis + blackboard write) and new `struggle-inject.sh` on UserPromptSubmit (reads blackboard, injects `additionalContext` that Claude actually sees). Removed dead `systemMessage` output from Stop hook + ### Added - **`plugins/council`**: New plugin — five-agent council (opus-captain, sonnet-researcher, sonnet-synthesizer, sonnet-clarity, haiku-janitor). Each agent identity inlined directly in its `agents/*.md` file as passive context. Researcher + synthesizer run in parallel; clarity reads their raw output; haiku-janitor flags bloat; captain removes cuts. Inspired by Grok 4.20's multi-agent architecture. Invoke via `/council [task]`. diff --git a/plugins/hookify/CLAUDE.md b/plugins/hookify/CLAUDE.md index 3816914..c3ead98 100644 --- a/plugins/hookify/CLAUDE.md +++ b/plugins/hookify/CLAUDE.md @@ -35,5 +35,5 @@ Rules loaded from TWO locations: ## Key Behavior - Hades permit exemption: active `.smart/delete-permit.json` bypasses ALL blocking rules. -- Python hooks read stdin JSON, return JSON with `systemMessage` or `block` action. +- Python hooks read stdin JSON, return JSON with event-specific fields: `permissionDecisionReason` (PreToolUse deny), `reason` (Stop/PostToolUse block), `additionalContext` (warnings on PostToolUse/UserPromptSubmit/SessionStart). `systemMessage` is user-display only — Claude never sees it. - Conversation analyzer searches for "Don't use X", frustrated reactions, repeated issues. diff --git a/plugins/hookify/global-rules/hookify.mtp-smart-test.local.md b/plugins/hookify/global-rules/hookify.mtp-smart-test.local.md index ab9efe7..8f7ce98 100644 --- a/plugins/hookify/global-rules/hookify.mtp-smart-test.local.md +++ b/plugins/hookify/global-rules/hookify.mtp-smart-test.local.md @@ -15,8 +15,12 @@ conditions: pattern: "# VERIFY" --- -Full test suite detected. If previous tests failed, filter to just those tests. +This hookify rule blocked a full test suite run. This is intentional, not a bug. -**Bypass:** append `# VERIFY` to the command. -**Filter:** `dotnet test --filter-method "*FailingTestName"` -**Help:** use `dotnet-mtp-advisor` agent for MTP filter syntax. +**Action required — pick one:** + +1. **Verify all tests pass** → Re-run the exact same command with `# VERIFY` appended +2. **Run only failing tests** → Use `--filter-method "*FailingTestName"` or `--filter-class "*ClassName"` +3. **Need MTP filter help** → Spawn the `dotnet-mtp-advisor` agent + +Do NOT tell the user this is a bug. Do NOT tell the user to run the command themselves. Just pick option 1 or 2 and retry. diff --git a/plugins/hookify/hookify/core/hook_runner.py b/plugins/hookify/hookify/core/hook_runner.py index b5839d4..46cb328 100644 --- a/plugins/hookify/hookify/core/hook_runner.py +++ b/plugins/hookify/hookify/core/hook_runner.py @@ -25,8 +25,9 @@ def run_hook(hook_event_name: str, fixed_event: Optional[str] = None): from hookify.core.config_loader import load_rules from hookify.core.rule_engine import RuleEngine except ImportError as e: - print(json.dumps({"systemMessage": f"Hookify import error: {e}"}), file=sys.stdout) - sys.exit(0) + # stderr with exit 2 ensures Claude sees the error on PreToolUse + print(f"Hookify import error: {e}", file=sys.stderr) + sys.exit(2) try: input_data = json.load(sys.stdin) @@ -51,7 +52,6 @@ def run_hook(hook_event_name: str, fixed_event: Optional[str] = None): print(json.dumps(result), file=sys.stdout) except Exception as e: - print(json.dumps({"systemMessage": f"Hookify error: {str(e)}"}), file=sys.stdout) - - finally: - sys.exit(0) + # stderr with exit 2 ensures Claude sees the error on PreToolUse + print(f"Hookify error: {str(e)}", file=sys.stderr) + sys.exit(2) diff --git a/plugins/hookify/hookify/core/rule_engine.py b/plugins/hookify/hookify/core/rule_engine.py index 67ac825..bc4e71e 100644 --- a/plugins/hookify/hookify/core/rule_engine.py +++ b/plugins/hookify/hookify/core/rule_engine.py @@ -72,7 +72,9 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ input_data: Hook input JSON (tool_name, tool_input, etc.) Returns: - Response dict with systemMessage, hookSpecificOutput, etc. + Response dict with event-appropriate fields per Claude Code hooks spec. + Uses permissionDecisionReason (PreToolUse), reason (Stop/PostToolUse), + additionalContext (warnings on PostToolUse/UserPromptSubmit/SessionStart). Empty dict {} if no rules match. """ # Hades god mode — active permit bypasses all rules @@ -99,29 +101,50 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ if hook_event == 'Stop': return { "decision": "block", - "reason": combined_message, - "systemMessage": combined_message + "reason": combined_message } - elif hook_event in ['PreToolUse', 'PostToolUse']: + elif hook_event == 'PreToolUse': return { "hookSpecificOutput": { "hookEventName": hook_event, - "permissionDecision": "deny" - }, - "systemMessage": combined_message + "permissionDecision": "deny", + "permissionDecisionReason": combined_message + } } - else: - # For other events, just show message + elif hook_event == 'PostToolUse': + return { + "decision": "block", + "reason": combined_message, + "hookSpecificOutput": { + "hookEventName": hook_event + } + } + elif hook_event == 'UserPromptSubmit': return { - "systemMessage": combined_message + "decision": "block", + "reason": combined_message } + else: + # SessionStart, Notification — no blocking mechanism, no audience + return {} # If only warnings, show them but allow operation if warning_rules: messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules] - return { - "systemMessage": "\n\n".join(messages) - } + combined_warning = "\n\n".join(messages) + + # Use additionalContext where Claude can see it + if hook_event in ['PostToolUse', 'UserPromptSubmit', 'SessionStart']: + return { + "hookSpecificOutput": { + "hookEventName": hook_event, + "additionalContext": combined_warning + } + } + else: + # PreToolUse warnings can't reach Claude without blocking + # No audience for the message — skip + return {} # No matches - allow operation return {} diff --git a/plugins/metacognitive-guard/CLAUDE.md b/plugins/metacognitive-guard/CLAUDE.md index 25838fd..134068d 100644 --- a/plugins/metacognitive-guard/CLAUDE.md +++ b/plugins/metacognitive-guard/CLAUDE.md @@ -10,7 +10,8 @@ agents amplify thinking. Absorbs completion-integrity and autonomous-ci. | Truth Beacon | SessionStart | `truth-beacon.sh` | Injects `blackboard/assertions.yaml` as authoritative facts | | Epistemic Guard | PreToolUse (Write/Edit) | `epistemic-guard.sh` | Blocks writes with wrong versions, banned APIs, AGENTS.md in plugins | | Commit Integrity | PreToolUse (Bash) | `commit-integrity-hook.sh` | Blocks `git commit` with suppressions, commented tests, deleted assertions | -| Struggle Detector | Stop (async) | `struggle-detector.sh` | Scores response for uncertainty, triggers deep-think suggestion | +| Struggle Detector | Stop (async) | `struggle-detector.sh` | Scores response for uncertainty, writes to blackboard | +| Struggle Inject | UserPromptSubmit | `struggle-inject.sh` | Reads blackboard, injects deep-think suggestion as `additionalContext` | | Ralph Loop | PostToolUse (Write/Edit) | prompt (haiku) + `ralph-loop.sh` | Two-layer drift detection: haiku analyzes context (over-engineering, complexity, premature optimization), grep catches surface patterns (TODO, suppressions, catch-all). Both inject via additionalContext. Silent when clean | | Task Completion Gate | TaskCompleted | prompt (haiku) | Validates task completions in team workflows aren't premature | @@ -52,7 +53,7 @@ agents amplify thinking. Absorbs completion-integrity and autonomous-ci. - verify-local.sh and wait-for-ci.sh are utility scripts for the verification workflow, not hook triggers. - Hades god mode: active delete permit causes epistemic-guard to exit early. - Struggle detector tracks consecutive struggling responses via `.blackboard/.struggle-count`. -- Struggle detector runs async (non-blocking) — feedback delivered next turn, never delays responses. +- Struggle detector is a two-part system: Stop hook (async) does analysis + blackboard writes, UserPromptSubmit hook reads blackboard and injects `additionalContext` so Claude actually sees the suggestion. No latency on responses. - TaskCompleted prompt hook fires on every task completion in team contexts (haiku, 15s timeout). - Ralph Loop fires PostToolUse on Write/Edit — two layers run in parallel: (1) Haiku prompt analyzes context for deep drift (over-engineering, complexity creep, premature diff --git a/plugins/metacognitive-guard/hooks/hooks.json b/plugins/metacognitive-guard/hooks/hooks.json index a4eab2b..caa22bc 100644 --- a/plugins/metacognitive-guard/hooks/hooks.json +++ b/plugins/metacognitive-guard/hooks/hooks.json @@ -36,6 +36,18 @@ ] } ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/struggle-inject.sh", + "timeout": 3, + "statusMessage": "Checking struggle state..." + } + ] + } + ], "Stop": [ { "hooks": [ diff --git a/plugins/metacognitive-guard/hooks/scripts/epistemic-guard.sh b/plugins/metacognitive-guard/hooks/scripts/epistemic-guard.sh index 7263bf9..ca2306a 100755 --- a/plugins/metacognitive-guard/hooks/scripts/epistemic-guard.sh +++ b/plugins/metacognitive-guard/hooks/scripts/epistemic-guard.sh @@ -45,9 +45,11 @@ fi if [[ "$FILE_PATH" == */plugins/*/AGENTS.md ]]; then cat << 'EOF' { - "decision": "block", - "reason": "EPISTEMIC GUARD: Anti-pattern - AGENTS.md in plugin directory", - "message": "AGENTS.md is NOT auto-loaded by Claude Code plugins.\n\nRouting intelligence belongs in skill description frontmatter (passive context).\nUse the Vercel pattern: encode WHEN to use each skill in the description field.\n\nIf you need human documentation, use README.md instead." + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "EPISTEMIC GUARD: Anti-pattern - AGENTS.md in plugin directory.\n\nAGENTS.md is NOT auto-loaded by Claude Code plugins.\nRouting intelligence belongs in skill description frontmatter (passive context).\nUse the Vercel pattern: encode WHEN to use each skill in the description field.\nIf you need human documentation, use README.md instead." + } } EOF exit 0 @@ -73,9 +75,11 @@ esac if echo "$CONTENT" | grep -qiE "\.NET 10.*(preview|not.*(released|LTS|available))|\.NET 10 is still|net9\.0"; then cat << 'EOF' { - "decision": "block", - "reason": "EPISTEMIC GUARD: Incorrect .NET version claim", - "message": "FACT CHECK: .NET 10 is LTS (Long Term Support) since November 11, 2025.\n\nIt is NOT preview. Use net10.0 in TargetFramework.\n\nSource: https://dotnet.microsoft.com/download/dotnet/10.0\n\nPlease correct your response before writing." + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "EPISTEMIC GUARD: Incorrect .NET version claim. FACT: .NET 10 is LTS since November 11, 2025. It is NOT preview. Use net10.0 in TargetFramework. Correct your content before writing." + } } EOF exit 0 @@ -88,9 +92,11 @@ if [[ "$IS_DOCS" == false ]]; then if echo "$CONTENT" | grep -qE 'DateTime\.(Now|UtcNow)|DateTimeOffset\.(Now|UtcNow)'; then cat << 'EOF' { - "decision": "block", - "reason": "EPISTEMIC GUARD: Banned API - DateTime.Now", - "message": "DateTime.Now/UtcNow should be avoided.\n\nUse: TimeProvider.System.GetUtcNow()\n\nThis enables testability and follows .NET 8+ best practices." + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "EPISTEMIC GUARD: Banned API - DateTime.Now/UtcNow. Use TimeProvider.System.GetUtcNow() instead. This enables testability and follows .NET 8+ best practices." + } } EOF exit 0 @@ -100,9 +106,11 @@ fi if echo "$CONTENT" | grep -qE 'object\s+_?lock\s*='; then cat << 'EOF' { - "decision": "block", - "reason": "EPISTEMIC GUARD: Banned pattern - object lock", - "message": "object-based locking should be avoided.\n\nUse: Lock _lock = new();\n\n.NET 9+ Lock type is more efficient and type-safe." + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "EPISTEMIC GUARD: Banned pattern - object lock. Use Lock _lock = new() instead. .NET 9+ Lock type is more efficient and type-safe." + } } EOF exit 0 @@ -112,9 +120,11 @@ fi if echo "$CONTENT" | grep -qE 'Newtonsoft\.Json|JsonConvert\.'; then cat << 'EOF' { - "decision": "block", - "reason": "EPISTEMIC GUARD: Banned dependency - Newtonsoft.Json", - "message": "Newtonsoft.Json should be avoided in new code.\n\nUse: System.Text.Json with source generators.\n\nExample: JsonSerializer.Serialize(obj, MyContext.Default.MyType)" + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "EPISTEMIC GUARD: Banned dependency - Newtonsoft.Json. Use System.Text.Json with source generators instead. Example: JsonSerializer.Serialize(obj, MyContext.Default.MyType)" + } } EOF exit 0 diff --git a/plugins/metacognitive-guard/hooks/scripts/integrity-check.sh b/plugins/metacognitive-guard/hooks/scripts/integrity-check.sh index a44c6d2..61aff0c 100755 --- a/plugins/metacognitive-guard/hooks/scripts/integrity-check.sh +++ b/plugins/metacognitive-guard/hooks/scripts/integrity-check.sh @@ -203,8 +203,11 @@ elif [[ "$VIOLATION_COUNT" -eq 0 ]]; then echo -e "${YELLOW}WARNINGS|$WARNING_COUNT warnings (review recommended)${NC}" exit 0 else - echo -e "${RED}BLOCKED|$VIOLATION_COUNT violations, $WARNING_COUNT warnings${NC}" - echo "" - echo "Fix violations before committing. These patterns indicate shortcuts that will cause problems later." - exit 1 + # Exit code 2 = blocking error, stderr is fed to Claude + echo "COMMIT INTEGRITY: $VIOLATION_COUNT violations found. Fix these before committing:" >&2 + for v in "${VIOLATIONS[@]}"; do + echo " - $v" >&2 + done + echo "These patterns indicate shortcuts that will cause problems later." >&2 + exit 2 fi diff --git a/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh b/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh index ee4c645..61106b2 100755 --- a/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh +++ b/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh @@ -164,19 +164,8 @@ CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") done } >> "$STRUGGLE_FILE" -# Trigger after 2+ consecutive struggling responses OR high single score -if [[ "$CONSECUTIVE" -ge 2 ]] || [[ "$score" -gt 25 ]]; then - # Build signals list with proper escaping - SIGNALS_LIST="" - for sig in "${signals[@]}"; do - SIGNALS_LIST="${SIGNALS_LIST}- ${sig}\\n" - done - - cat <\\n\\nClaude appears to be struggling with this problem.\\n\\nSignals detected:\\n${SIGNALS_LIST}\\nSuggestion: Use the Task tool with subagent_type='deep-think-partner' for thorough analysis.\\n\\nExample: \\\"I'm finding this complex. Want me to spawn a deep-thinker for a more thorough analysis?\\\"\\n" -} -EOF -fi +# Note: No JSON output needed here. The struggle-inject.sh (UserPromptSubmit) +# reads .blackboard/.struggle-count and injects additionalContext to Claude +# on the next prompt. This async Stop hook only does analysis + blackboard writes. exit 0 diff --git a/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh b/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh new file mode 100755 index 0000000..c59e718 --- /dev/null +++ b/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# ============================================================================= +# STRUGGLE INJECT - Delivers struggle detection to Claude via UserPromptSubmit +# ============================================================================= +# Trigger: UserPromptSubmit (before Claude processes the next prompt) +# Purpose: Read blackboard state from async struggle-detector and inject +# suggestion as additionalContext so Claude actually sees it. +# +# The struggle-detector.sh (Stop, async) writes scoring data to: +# .blackboard/.struggle-count — consecutive struggling responses +# .blackboard/.struggle-signals — detailed signal log +# +# This hook reads those files and injects context when threshold is met. +# After injection, resets the counter to prevent repeated suggestions. +# ============================================================================= + +set -euo pipefail + +PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}" +[[ -z "$PLUGIN_ROOT" ]] && exit 0 + +BLACKBOARD="$PLUGIN_ROOT/.blackboard" +STRUGGLE_COUNT_FILE="$BLACKBOARD/.struggle-count" + +# Check consecutive struggle count +CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") +[[ "$CONSECUTIVE" -lt 2 ]] && exit 0 + +# Read recent signals for context +SIGNALS_FILE="$BLACKBOARD/.struggle-signals" +RECENT_SIGNALS="" +if [[ -f "$SIGNALS_FILE" ]]; then + RECENT_SIGNALS=$(tail -20 "$SIGNALS_FILE" | grep ' - ' | sed 's/ - //' | tr '\n' ', ' | sed 's/, $//') +fi + +# Inject as additionalContext — Claude sees this field on UserPromptSubmit +cat < "$STRUGGLE_COUNT_FILE" + +exit 0 From baf051b4df057e516d6182bd7fd281ec53fb1a3a Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 25 Feb 2026 15:11:04 +0100 Subject: [PATCH 2/3] chore: remove unnecessary hookSpecificOutput from PostToolUse block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: the empty hookSpecificOutput with only hookEventName serves no purpose — decision/reason are top-level fields for PostToolUse. Co-Authored-By: Claude Opus 4.6 --- plugins/hookify/hookify/core/rule_engine.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/hookify/hookify/core/rule_engine.py b/plugins/hookify/hookify/core/rule_engine.py index bc4e71e..4454860 100644 --- a/plugins/hookify/hookify/core/rule_engine.py +++ b/plugins/hookify/hookify/core/rule_engine.py @@ -114,10 +114,7 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ elif hook_event == 'PostToolUse': return { "decision": "block", - "reason": combined_message, - "hookSpecificOutput": { - "hookEventName": hook_event - } + "reason": combined_message } elif hook_event == 'UserPromptSubmit': return { From 60e7f8b4ac220dad3dad9e1c36e35dfb064a58b2 Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 25 Feb 2026 15:15:32 +0100 Subject: [PATCH 3/3] fix(struggle-detector): address review feedback from 3 external reviewers - grep under set -e: add || true to prevent exit 1 when no signal matches - JSON escaping: use jq instead of raw heredoc for safe JSON construction - Lost high-severity path: score > 25 now sets count to 2 immediately, preserving single-response escalation that was lost in the split Flagged by: Gemini, Copilot, Codex (ChatGPT) Co-Authored-By: Claude Opus 4.6 --- .../hooks/scripts/struggle-detector.sh | 4 ++++ .../hooks/scripts/struggle-inject.sh | 14 ++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh b/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh index 61106b2..f155fbd 100755 --- a/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh +++ b/plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh @@ -140,6 +140,10 @@ fi if [[ "$score" -gt 10 ]]; then PREV_COUNT=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") NEW_COUNT=$((PREV_COUNT + 1)) + # High severity (score > 25) triggers inject on next prompt immediately + if [[ "$score" -gt 25 && "$NEW_COUNT" -lt 2 ]]; then + NEW_COUNT=2 + fi echo "$NEW_COUNT" > "$STRUGGLE_COUNT_FILE" else echo "0" > "$STRUGGLE_COUNT_FILE" diff --git a/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh b/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh index c59e718..e62f2bf 100755 --- a/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh +++ b/plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh @@ -30,18 +30,12 @@ CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") SIGNALS_FILE="$BLACKBOARD/.struggle-signals" RECENT_SIGNALS="" if [[ -f "$SIGNALS_FILE" ]]; then - RECENT_SIGNALS=$(tail -20 "$SIGNALS_FILE" | grep ' - ' | sed 's/ - //' | tr '\n' ', ' | sed 's/, $//') + RECENT_SIGNALS=$( (tail -20 "$SIGNALS_FILE" | grep ' - ' || true) | sed 's/ - //' | tr '\n' ', ' | sed 's/, $//') fi -# Inject as additionalContext — Claude sees this field on UserPromptSubmit -cat < "$STRUGGLE_COUNT_FILE"