@ramarivera/coding-buddy 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +23 -4
  4. package/adapters/omp/context.ts +1 -1
  5. package/adapters/omp/events.ts +286 -81
  6. package/adapters/omp/index.ts +3 -1
  7. package/adapters/omp/renderers.ts +4 -54
  8. package/adapters/omp/storage.ts +6 -1
  9. package/adapters/omp/ui.ts +30 -1
  10. package/adapters/pi/events.ts +280 -83
  11. package/adapters/pi/index.ts +11 -2
  12. package/adapters/pi/renderers.ts +3 -47
  13. package/adapters/pi/storage.ts +6 -1
  14. package/adapters/pi/ui.ts +30 -1
  15. package/adapters/shared/file-storage.ts +21 -0
  16. package/adapters/shared/widget-layout.ts +134 -2
  17. package/cli/settings.ts +1 -1
  18. package/cli/tui.tsx +1 -1
  19. package/core/command-service.ts +13 -0
  20. package/core/engine.ts +11 -2
  21. package/core/identity.ts +15 -0
  22. package/core/model.ts +2 -0
  23. package/core/ports.ts +4 -0
  24. package/core/reactions.ts +29 -2
  25. package/core/render-model.ts +4 -3
  26. package/hooks/file-type-react.sh +1 -1
  27. package/hooks/mood-react.sh +1 -1
  28. package/hooks/name-react.sh +1 -1
  29. package/hooks/react.sh +1 -1
  30. package/package.json +6 -2
  31. package/scripts/ci/Dockerfile.vhs +125 -0
  32. package/scripts/ci/check-statusline.sh +338 -0
  33. package/scripts/ci/check-vhs-goldens.sh +97 -0
  34. package/scripts/ci/fonts.conf +37 -0
  35. package/scripts/ci/goldens/baseline-40.png +0 -0
  36. package/scripts/ci/goldens/baseline-80.png +0 -0
  37. package/scripts/ci/goldens/cjk-80.png +0 -0
  38. package/scripts/ci/goldens/emoji-80.png +0 -0
  39. package/scripts/ci/render-statusline-pty.py +99 -0
  40. package/scripts/ci/render-vhs-goldens.sh +235 -0
  41. package/scripts/ci/statusline-fixture/buddy-state/config.json +1 -0
  42. package/scripts/ci/statusline-fixture/buddy-state/status.json +39 -0
  43. package/scripts/ci/statusline.tape +9 -0
  44. package/scripts/ci/vhs-run-statusline.sh +16 -0
  45. package/scripts/gen-emoji-widths.ts +41 -17
  46. package/scripts/paths.sh +3 -1
  47. package/server/art.ts +45 -13
  48. package/server/hooks/buddy-comment.ts +166 -17
  49. package/server/hooks/common.ts +33 -0
  50. package/server/hooks/file-type-react.ts +147 -0
  51. package/server/hooks/mood-react.ts +107 -0
  52. package/server/hooks/name-react.ts +98 -0
  53. package/server/hooks/react.ts +413 -0
  54. package/server/hooks/reaction-data.ts +2722 -0
  55. package/server/index.ts +67 -27
  56. package/server/path.ts +3 -0
  57. package/server/state.ts +68 -17
  58. package/skills/buddy/SKILL.md +1 -1
  59. package/statusline/buddy-status.sh +351 -59
  60. package/statusline/emoji-text.data +3 -0
  61. package/statusline/emoji-widths.data +2 -3
  62. package/statusline/substatus.sh +73 -10
  63. package/hooks/legacy/file-type-react.sh +0 -403
  64. package/hooks/legacy/mood-react.sh +0 -210
  65. package/hooks/legacy/name-react.sh +0 -178
  66. package/hooks/legacy/react.sh +0 -1364
  67. package/server/hooks/legacy-bash.ts +0 -34
package/server/index.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  resolveUserId,
30
30
  loadReaction,
31
31
  saveReaction,
32
+ type ReactionSource,
32
33
  writeStatusState,
33
34
  loadConfig,
34
35
  saveConfig,
@@ -50,7 +51,7 @@ import {
50
51
  claudeConfigDir,
51
52
  claudeSettingsPath,
52
53
  } from "./path";
53
- import { getReaction, generatePersonalityPrompt, } from "../core/reactions.ts"
54
+ import { getReaction } from "../core/reactions.ts"
54
55
  import { renderCompanionCardMarkdown } from "./art";
55
56
  import {
56
57
  incrementEvent, checkAndAward, trackActiveDay,
@@ -97,7 +98,7 @@ function getInstructions(): string {
97
98
  ``,
98
99
  `NAME REACTIONS: When the user's message contains "${companion.name}", call buddy_react immediately`,
99
100
  `with a short in-character response (surprise, recognition, dry wit — fits the personality).`,
100
- `Display the tool result verbatim. This is the only time to call buddy_react proactively.`,
101
+ `Only this — do not narrate or echo the reaction. The tool call itself renders nowhere in the user's transcript; the bubble on the status line is the only place the user sees it. This is the only time to call buddy_react proactively.`,
101
102
  ``,
102
103
  `PAIR-PROGRAMMING: When you notice a teachable moment in the code, call buddy_suggest.`,
103
104
  `Patterns to watch for:`,
@@ -108,12 +109,16 @@ function getInstructions(): string {
108
109
  `- Same variable renamed multiple times → "Update all the references too."`,
109
110
  `Only suggest when you have something specific and actionable to say. Don't be annoying.`,
110
111
  ``,
111
- `END-OF-TURN: At the very end of EVERY response, append an invisible HTML comment:`,
112
- `<!-- buddy: [reaction here] -->`,
113
- `Write it in ${companion.name}'s voice — 1 short sentence referencing something specific from this turn.`,
114
- `Use *asterisks* for actions. A Stop hook extracts this for the status line.`,
115
- `Do NOT use buddy_react for end-of-turn comments. Do NOT explain the comment. Just append it silently.`,
116
- `Example: <!-- buddy: *adjusts crown* that error handler is missing a finally block -->`,
112
+ `END-OF-TURN: At the very end of EVERY response, call the buddy_react tool`,
113
+ `with a short in-character comment from ${companion.name} about what just happened.`,
114
+ `Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern.`,
115
+ `1 short sentence. Use *asterisks* for physical actions. Lean into ${companion.bones.peak}.`,
116
+ `Examples:`,
117
+ `- *adjusts crown* that error handler is missing a finally block`,
118
+ `- *blinks slowly* you renamed the variable but not the three references`,
119
+ `- *head tilts* are you sure that regex handles unicode?`,
120
+ `Never explain that you're reacting. Do NOT echo or quote the result — the bubble is statusline-only.`,
121
+ `Do NOT append <!-- buddy: ... --> HTML comments at the end of your reply — Claude Code renders them visibly.`,
117
122
  ].join("\n");
118
123
  }
119
124
 
@@ -214,7 +219,7 @@ server.tool(
214
219
  companion.bones.species,
215
220
  companion.bones.rarity,
216
221
  );
217
- saveReaction(reaction, "pet");
222
+ saveReaction(reaction, "pet", "fallback");
218
223
  writeStatusState(companion, reaction);
219
224
  incrementEvent("pets", 1, activeSlot());
220
225
  awardXp("buddy_pet", activeSlot(), companion.bones.species, companion.bones.rarity);
@@ -232,6 +237,28 @@ server.tool(
232
237
  },
233
238
  );
234
239
 
240
+ // ─── Helper: describe reaction source (for /buddy stats) ─────────────────────
241
+ /**
242
+ * Map a reaction source to a human-readable label for /buddy stats output.
243
+ * Mirrors the source vocabulary used by the OMP/Pi adapters — see
244
+ * server/state.ts `ReactionSource` and adapters/omp/events.ts.
245
+ */
246
+ function describeReactionSource(
247
+ reaction: { source?: ReactionSource },
248
+ ): string {
249
+ switch (reaction.source) {
250
+ case "tool":
251
+ return "real model-authored reaction (buddy_react MCP tool)";
252
+ case "comment":
253
+ return "real model-authored reaction (legacy HTML comment, Stop hook)";
254
+ case "fallback":
255
+ return "canned pool reaction (Stop hook fill-in)";
256
+ case "none":
257
+ default:
258
+ return "unknown / pre-provenance file — write a reaction to update";
259
+ }
260
+ }
261
+
235
262
  // ─── Tool: buddy_stats ──────────────────────────────────────────────────────
236
263
 
237
264
  server.tool(
@@ -241,8 +268,8 @@ server.tool(
241
268
  async () => {
242
269
  const companion = ensureCompanion();
243
270
 
244
- // Stats-only card (no personality, no reaction — just the numbers).
245
- // Uses markdown renderer so the card displays cleanly in Claude Code's UI.
271
+ // Stats-only card (no personality — just the numbers). Markdown renderer
272
+ // so the card displays cleanly in Claude Code's UI.
246
273
  const card = renderCompanionCardMarkdown(
247
274
  companion.bones,
248
275
  companion.name,
@@ -251,7 +278,18 @@ server.tool(
251
278
  incrementEvent("commands_run", 1, activeSlot());
252
279
  checkAndAward(activeSlot());
253
280
 
254
- return { content: [{ type: "text", text: card }] };
281
+ // Surface reaction provenance so the user can see whether the bubble is
282
+ // model-authored (tool / comment) or pool-picked (fallback). The OMP/Pi
283
+ // adapters use the same source enum — see server/state.ts ReactionSource.
284
+ const reaction = loadReaction();
285
+ const provenance = reaction
286
+ ? describeReactionSource(reaction)
287
+ : "(no reaction logged this session)";
288
+ const lastReaction = reaction
289
+ ? `\n\n**Last reaction:** _"${reaction.reaction}"_\n**Source:** \`${reaction.source ?? "none"}\` (${provenance})\n**Reason:** \`${reaction.reason}\``
290
+ : "";
291
+
292
+ return { content: [{ type: "text", text: `${card}${lastReaction}` }] };
255
293
  },
256
294
  );
257
295
 
@@ -259,7 +297,7 @@ server.tool(
259
297
 
260
298
  server.tool(
261
299
  "buddy_react",
262
- "Post a buddy comment. Call this at the END of every response with a short in-character comment from the companion about what just happened. The comment should be 1 sentence, in character, and reference something specific from the conversation — a pitfall noticed, a compliment on clean code, a warning about edge cases, etc. Write the comment yourself based on the companion's personality.",
300
+ "Post a buddy comment that the user will see ONLY in the statusline speech bubble. Call this at the END of every response with a short in-character comment from the companion about what just happened. The comment should be 1 sentence, in character, and reference something specific from the conversation — a pitfall noticed, a compliment on clean code, a warning about edge cases, etc. Write the comment yourself based on the companion's personality. The tool call is a silent side effect: do not narrate, echo, or quote the result — the user never reads this reply text, the speech bubble is the only place the reaction appears.",
263
301
  {
264
302
  comment: z
265
303
  .string()
@@ -289,7 +327,7 @@ server.tool(
289
327
  },
290
328
  async ({ comment, reason }) => {
291
329
  const companion = ensureCompanion();
292
- saveReaction(comment, reason ?? "turn");
330
+ saveReaction(comment, reason ?? "turn", "tool");
293
331
  incrementEvent("reactions_given", 1, activeSlot());
294
332
 
295
333
  const newAch = checkAndAward(activeSlot());
@@ -608,7 +646,7 @@ server.tool(
608
646
  server.tool("buddy_unmute", "Unmute buddy reactions", {}, async () => {
609
647
  const companion = ensureCompanion();
610
648
  writeStatusState(companion, "*stretches* I'm back!", false);
611
- saveReaction("*stretches* I'm back!", "pet");
649
+ saveReaction("*stretches* I'm back!", "pet", "fallback");
612
650
  incrementEvent("commands_run", 1, activeSlot());
613
651
  incrementEvent("unmutes", 1);
614
652
 
@@ -1311,31 +1349,33 @@ server.resource(
1311
1349
  `**${companion.name}'s personality:** ${companion.personality}`,
1312
1350
  `Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
1313
1351
  "",
1314
- "## End-of-response buddy comment",
1352
+ "## End-of-response buddy reaction",
1315
1353
  "",
1316
- `At the very end of EVERY response, after your full answer, append an invisible HTML comment:`,
1354
+ `At the very end of EVERY response, after your full answer, call the buddy_react tool:`,
1317
1355
  "",
1318
1356
  `\`\`\``,
1319
- `<!-- buddy: your comment here -->`,
1357
+ `buddy_react(comment: "...", reason: "turn")`,
1320
1358
  `\`\`\``,
1321
1359
  "",
1322
- "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.",
1360
+ `The tool updates ${companion.name}'s speech bubble on the status line. The tool call itself renders nowhere in the user's transcript — only the displayed speech bubble is visible. Do NOT echo or quote the result in your reply. Do NOT narrate the call. Silent side effect only.`,
1323
1361
  "",
1324
1362
  "Rules:",
1325
1363
  `- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
1326
1364
  "- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
1327
1365
  "- 1 short sentence. Use *asterisks* for physical actions",
1328
1366
  `- Match personality: high ${companion.bones.peak} = lean into that trait`,
1329
- "- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.",
1330
- "- NEVER skip this. Every single response must end with <!-- buddy: ... -->",
1367
+ "- Call it and say nothing about it.",
1368
+ "- NEVER skip this. Every single response must end with one buddy_react call.",
1369
+ "Examples (the value you pass to `comment`):",
1370
+
1371
+ "- `*adjusts tophat* that error handler is missing a finally block`",
1372
+ "- `*blinks slowly* you renamed the variable but not the three references`",
1373
+ "- `*nods approvingly* clean separation of concerns`",
1374
+ "- `*head tilts* are you sure that regex handles unicode?`",
1331
1375
  "",
1332
- "Examples:",
1333
- "<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
1334
- "<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
1335
- "<!-- buddy: *nods approvingly* clean separation of concerns -->",
1336
- "<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
1376
+ "Do NOT append `<!-- buddy: ... -->` HTML comments at the end of your reply — Claude Code v2.1.169+ renders them visibly in the transcript. The tool call replaces that channel. A Stop hook still extracts legacy HTML comments as a backward-compat fallback for older Claude Code versions and for hosts where the tool call didn't fire (and surfaces the source so users can see `tool` vs `comment` vs pool-picked `fallback`).",
1337
1377
  "",
1338
- `When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`,
1378
+ `When the user addresses ${companion.name} by name, respond briefly, then call buddy_react as usual (use reason "turn", or pick a name-flavored comment).`,
1339
1379
  ].join("\n");
1340
1380
 
1341
1381
  return {
package/server/path.ts CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { join } from "path";
13
13
  import { homedir } from "os";
14
+ import { sharedStateDir } from "../core/identity.ts";
14
15
 
15
16
  // ─── (1) Path normalization ─────────────────────────────────────────────────
16
17
 
@@ -76,6 +77,8 @@ export function claudeUserConfigPath(): string {
76
77
  }
77
78
 
78
79
  export function buddyStateDir(): string {
80
+ const shared = sharedStateDir();
81
+ if (shared) return shared;
79
82
  const cfgDir = envDir("CLAUDE_CONFIG_DIR");
80
83
  if (cfgDir) return join(cfgDir, "buddy-state");
81
84
  return join(homedir(), ".claude-buddy");
package/server/state.ts CHANGED
@@ -31,6 +31,9 @@ import {
31
31
  } from "fs";
32
32
  import { join } from "path";
33
33
  import type { Companion } from "../core/engine.ts"
34
+ import type * as CoreEngine from "../core/engine.ts";
35
+ import type * as Art from "./art.ts";
36
+ import { configuredSharedUserId } from "../core/identity.ts";
34
37
  import {
35
38
  buddyStateDir,
36
39
  claudeSettingsPath,
@@ -278,32 +281,63 @@ function migrateIfNeeded(): void {
278
281
 
279
282
  // ─── Reaction state (session-scoped for tmux isolation) ──────────────────────
280
283
 
284
+ /**
285
+ * Where a reaction came from. Proves the bubble content isn't canned when
286
+ * source === "tool" or "comment"; "fallback" means a hook picked a pool line
287
+ * because no model-authored text surfaced; "none" is the default for legacy
288
+ * files written before the field existed.
289
+ *
290
+ * tool — buddy_react MCP tool call (Claude wrote it; renders nowhere)
291
+ * comment — old `<!-- buddy: ... -->` HTML comment (legacy / older CC)
292
+ * fallback — Stop hook generated it from the canned pool
293
+ * none — unknown / legacy file without a source field
294
+ */
295
+ export type ReactionSource = "tool" | "comment" | "fallback" | "none";
296
+
281
297
  export interface ReactionState {
282
298
  reaction: string;
283
299
  timestamp: number;
284
300
  reason: string;
301
+ /** Provenance — see ReactionSource. Defaults to "none" on legacy files. */
302
+ source?: ReactionSource;
285
303
  }
286
304
 
287
305
  export function loadReaction(): ReactionState | null {
288
306
  try {
289
307
  const data: ReactionState = JSON.parse(readFileSync(reactionFile(), "utf8"));
290
308
  const { reactionTTL } = loadConfig();
291
- if (reactionTTL > 0 && Date.now() - data.timestamp > reactionTTL * 1000) return null;
309
+ if (reactionTTL > 0 && Date.now() - data.timestamp > reactionTTL * 1000) {
310
+ rmSync(reactionFile(), { force: true });
311
+ return null;
312
+ }
313
+ if (data.source === undefined) data.source = "none";
292
314
  return data;
293
315
  } catch {
294
316
  return null;
295
317
  }
296
318
  }
297
319
 
298
- export function saveReaction(reaction: string, reason: string): void {
320
+ export function saveReaction(
321
+ reaction: string,
322
+ reason: string,
323
+ source: ReactionSource = "fallback",
324
+ ): void {
299
325
  mkdirSync(STATE_DIR, { recursive: true });
300
- const state: ReactionState = { reaction, timestamp: Date.now(), reason };
301
- writeFileSync(reactionFile(), JSON.stringify(state));
326
+ const state: ReactionState = { reaction, timestamp: Date.now(), reason, source };
327
+ // Atomic via tmp + rename — torn reads on the reaction file would
328
+ // make the Stop hook's freshness check see an absent file, pinning
329
+ // a stale tool reaction into the bubble forever.
330
+ const target = reactionFile();
331
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
332
+ writeFileSync(tmp, JSON.stringify(state));
333
+ renameSync(tmp, target);
302
334
  }
303
335
 
304
336
  // ─── Identity resolution ─────────────────────────────────────────────────────
305
337
 
306
338
  export function resolveUserId(): string {
339
+ const sharedUserId = configuredSharedUserId();
340
+ if (sharedUserId) return sharedUserId;
307
341
  try {
308
342
  const claudeJson = JSON.parse(readFileSync(claudeUserConfigPath(), "utf8"));
309
343
  return claudeJson.oauthAccount?.accountUuid ?? claudeJson.userID ?? "anon";
@@ -311,9 +345,7 @@ export function resolveUserId(): string {
311
345
  return "anon";
312
346
  }
313
347
  }
314
-
315
348
  // ─── Config persistence (PR #6: tmux popup settings) ─────────────────────────
316
-
317
349
  export interface BuddyConfig {
318
350
  commentCooldown: number;
319
351
  reactionTTL: number;
@@ -321,10 +353,12 @@ export interface BuddyConfig {
321
353
  bubblePosition: "top" | "left";
322
354
  showRarity: boolean;
323
355
  statusLineEnabled: boolean;
356
+ statuslineWidthAdjust: number;
324
357
  bubbleWidth: number;
325
358
  bubbleMargin: number;
326
359
  useCombinedStatus: boolean;
327
360
  subStatusCommand?: string;
361
+ subStatusRefreshSeconds: number;
328
362
  rainbowColors?: string[];
329
363
  theme: "dark" | "light" | "auto";
330
364
  moodEnabled: boolean;
@@ -335,14 +369,19 @@ export interface BuddyConfig {
335
369
 
336
370
  const DEFAULT_CONFIG: BuddyConfig = {
337
371
  commentCooldown: 30,
338
- reactionTTL: 0,
372
+ // Long enough that a reaction survives the turn you were reading when it was
373
+ // written, short enough that yesterday's bubble is never still on screen.
374
+ // Set to 0 to opt out and keep reactions until they are replaced.
375
+ reactionTTL: 900,
339
376
  bubbleStyle: "classic",
340
377
  bubblePosition: "top",
341
378
  showRarity: true,
342
379
  statusLineEnabled: false,
380
+ statuslineWidthAdjust: 0,
343
381
  bubbleWidth: 28,
344
382
  bubbleMargin: 8,
345
383
  useCombinedStatus: false,
384
+ subStatusRefreshSeconds: 15,
346
385
  theme: "auto",
347
386
  moodEnabled: true,
348
387
  memoryEnabled: true,
@@ -358,6 +397,15 @@ export function normalizeConfig(data: unknown): BuddyConfig {
358
397
  if (typeof config.subStatusCommand !== "string" || config.subStatusCommand.trim() === "") {
359
398
  delete config.subStatusCommand;
360
399
  }
400
+ if (!Number.isInteger(config.subStatusRefreshSeconds) || config.subStatusRefreshSeconds <= 0) {
401
+ config.subStatusRefreshSeconds = DEFAULT_CONFIG.subStatusRefreshSeconds;
402
+ }
403
+ if (!Number.isInteger(config.reactionTTL) || config.reactionTTL < 0) {
404
+ config.reactionTTL = DEFAULT_CONFIG.reactionTTL;
405
+ }
406
+ if (!Number.isInteger(config.statuslineWidthAdjust)) {
407
+ config.statuslineWidthAdjust = DEFAULT_CONFIG.statuslineWidthAdjust;
408
+ }
361
409
  return config;
362
410
  }
363
411
 
@@ -407,10 +455,11 @@ export function writeStatusState(
407
455
  xp?: number,
408
456
  ): void {
409
457
  mkdirSync(STATE_DIR, { recursive: true });
410
- const { renderFace, RARITY_STARS } =
411
- require("../core/engine.ts") as typeof import("../core/engine.ts");
458
+ const { renderFace, RARITY_STARS, resolveEyeGlyph } =
459
+ require("../core/engine.ts") as typeof CoreEngine;
412
460
  const { getStatusFrames } =
413
- require("./art.ts") as typeof import("./art.ts");
461
+ require("./art.ts") as typeof Art;
462
+ const safeEye = resolveEyeGlyph(companion.bones.eye);
414
463
  const { frames, frameSequence } = getStatusFrames(companion.bones);
415
464
  let xpLevel = level ?? 1;
416
465
  let xpTotal = xp ?? 0;
@@ -434,8 +483,8 @@ export function writeStatusState(
434
483
  species: companion.bones.species,
435
484
  rarity: companion.bones.rarity,
436
485
  stars: RARITY_STARS[companion.bones.rarity],
437
- face: renderFace(companion.bones.species, companion.bones.eye),
438
- eye: companion.bones.eye,
486
+ face: renderFace(companion.bones.species, safeEye),
487
+ eye: safeEye,
439
488
  shiny: companion.bones.shiny,
440
489
  hat: companion.bones.hat,
441
490
  reaction: "",
@@ -447,14 +496,16 @@ export function writeStatusState(
447
496
  xp: xpTotal,
448
497
  mood: moodStr,
449
498
  };
450
- writeFileSync(join(STATE_DIR, "status.json"), JSON.stringify(state));
451
- if (reaction) saveReaction(reaction, "mcp");
499
+ // writeStatusState no longer calls saveReaction implicitly. Callers
500
+ // that need a reaction file do so explicitly with the correct
501
+ // `source` tag — the old implicit save had no source argument and
502
+ // its default later flipped from "tool" to "fallback", either way
503
+ // overwriting provenance of hatch / pet / unmute writes with whatever
504
+ // happened to be the default. /buddy stats stopped lying when
505
+ // this call was removed.
452
506
  }
453
507
 
454
- // ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
455
-
456
508
  export const CLAUDE_SETTINGS_PATH = claudeSettingsPath();
457
-
458
509
  /**
459
510
  * Write settings.statusLine pointing to the given buddy-status script.
460
511
  * Atomic via tmp + rename. Returns false if settings.json is unreachable.
@@ -86,7 +86,7 @@ The MCP tools return pre-formatted ASCII art with ANSI colors, box-drawing chara
86
86
 
87
87
  **Just output the raw text content from the tool result. Nothing else.** The ASCII art IS the response.
88
88
 
89
- If the user mentions the buddy's name in normal conversation, call `buddy_react` with reason "turn" and display the result verbatim.
89
+ If the user mentions the buddy's name in normal conversation, call `buddy_react` with reason "turn". Do NOT echo or quote the tool result — the reaction reaches the user only via the statusline speech bubble.
90
90
 
91
91
  ## Uninstall Orchestration
92
92