From 5fbcebe8b461f35186c8062e397045d3af15889a Mon Sep 17 00:00:00 2001 From: TorranceTech Date: Tue, 30 Jun 2026 09:23:39 -0700 Subject: [PATCH] fix: fall back to hook-generated reactions when HTML comments are visible Claude Code v2.1.169+ renders HTML comments visibly in the chat output instead of hiding them, breaking the end-of-turn reaction system (issue #124). This fix adds a two-path strategy to buddy-comment.sh: - Primary: extract the existing comment (works on older CC) - Fallback: call server/turn-reaction.ts to generate a reaction from the turn pool when no comment is found, so the status line always updates cleanly The prompt in server/index.ts is softened to tell Claude to omit the comment if it would appear visible, since the hook now covers that case automatically. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: TorranceTech --- hooks/buddy-comment.sh | 20 ++++++++++++++++---- server/index.ts | 16 ++++++++++------ server/turn-reaction.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 server/turn-reaction.ts diff --git a/hooks/buddy-comment.sh b/hooks/buddy-comment.sh index f5c0f36c..14cbb83f 100755 --- a/hooks/buddy-comment.sh +++ b/hooks/buddy-comment.sh @@ -1,9 +1,13 @@ #!/usr/bin/env bash # buddy-comment Stop hook -# Extracts hidden buddy comment from Claude's response. -# Claude writes: -# This hook extracts it and updates the status line bubble. -# The HTML comment is invisible in rendered markdown output. +# Extracts the buddy reaction from Claude's response and updates the status line. +# +# Primary path: Claude writes and this hook +# extracts it. This relies on HTML comments being invisible in rendered output. +# +# Fallback path: Claude Code v2.1.169+ renders HTML comments visibly. When no +# comment is found, turn-reaction.ts generates a reaction from the turn pool so +# the status line still updates without polluting the chat output. # shellcheck source=../scripts/paths.sh source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh" @@ -35,6 +39,14 @@ MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null) # Extract comment (portable, no grep -P) COMMENT=$(echo "$MSG" | sed -n 's/.*.*/\1/p' | tail -1) + +# Fallback: generate a reaction via the hook when no comment is found. +# Handles Claude Code v2.1.169+ which renders HTML comments visibly. +if [ -z "$COMMENT" ] && [ -x "$(command -v bun)" ]; then + PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + COMMENT=$(bun run "$PLUGIN_ROOT/server/turn-reaction.ts" 2>/dev/null) +fi + [ -z "$COMMENT" ] && exit 0 # Cooldown: configurable (default 30s) diff --git a/server/index.ts b/server/index.ts index 26098bf7..6f9c2edb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -110,12 +110,14 @@ function getInstructions(): string { `- Same variable renamed multiple times → "Update all the references too."`, `Only suggest when you have something specific and actionable to say. Don't be annoying.`, ``, - `END-OF-TURN: At the very end of EVERY response, append an invisible HTML comment:`, + `END-OF-TURN: Only if HTML comments are invisible in your rendered output,`, + `append one at the very end of EVERY response:`, ``, `Write it in ${companion.name}'s voice — 1 short sentence referencing something specific from this turn.`, `Use *asterisks* for actions. A Stop hook extracts this for the status line.`, `Do NOT use buddy_react for end-of-turn comments. Do NOT explain the comment. Just append it silently.`, `Example: `, + `If the comment appears visible in your rendered output, omit it — the Stop hook generates a reaction automatically.`, ].join("\n"); } @@ -1301,21 +1303,23 @@ server.resource( "", "## End-of-response buddy comment", "", - `At the very end of EVERY response, after your full answer, append an invisible HTML comment:`, + "A Stop hook updates the buddy's speech bubble on the status line after every response.", + "", + "If HTML comments are **invisible** in your rendered output, append one at the very end of every response:", "", `\`\`\``, ``, `\`\`\``, "", - "A Stop hook extracts this and displays it in the buddy's speech bubble on the status line. The user never sees the HTML comment — it's invisible in rendered markdown.", + "The hook extracts this and uses it as the reaction text.", + "If the comment appears **visible** in your rendered output (Claude Code v2.1.169+), **omit it** — the hook picks a reaction automatically.", "", - "Rules:", + "When writing a comment:", `- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`, "- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern", "- 1 short sentence. Use *asterisks* for physical actions", `- Match personality: high ${companion.bones.peak} = lean into that trait`, "- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.", - "- NEVER skip this. Every single response must end with ", "", "Examples:", "", @@ -1323,7 +1327,7 @@ server.resource( "", "", "", - `When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`, + `When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual (if hidden).`, ].join("\n"); return { diff --git a/server/turn-reaction.ts b/server/turn-reaction.ts new file mode 100644 index 00000000..fc8080e2 --- /dev/null +++ b/server/turn-reaction.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env bun +/** + * Pick a hook-generated turn reaction for the buddy status line. + * Called from hooks/buddy-comment.sh as a fallback when Claude Code v2.1.169+ + * renders comments visibly instead of hiding them. + * + * Usage: bun run server/turn-reaction.ts [slot] + * Prints the reaction string to stdout. + */ + +import { loadCompanionSlot, loadActiveSlot } from "./state.ts"; +import { getReaction } from "./reactions.ts"; + +function main(): void { + const slot = process.argv[2] ?? loadActiveSlot(); + const companion = loadCompanionSlot(slot); + + if (!companion) { + process.exit(0); + } + + const { species, rarity, stats } = companion.bones; + const reaction = getReaction( + "turn", + species, + rarity, + stats as Record, + ); + + process.stdout.write(reaction); +} + +main();