tachibot-mcp 2.18.0 → 2.19.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to TachiBot MCP will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.19.0] - 2026-03-21
9
+
10
+ ### Added
11
+ - **Sparse render mode** (`RENDER_OUTPUT=sparse`) — lightweight output formatting with ~72 tokens overhead per response
12
+ - **ANSI model badges** — colored background badges for model name (provider color) + tool name (charcoal bg)
13
+ - **Pastel section headers** — emoji section headers (`🧠 HEADER ───`) rendered as teal bg + dark bold text badges
14
+ - **Color-coded verdicts** — `✅ pass` (sage green), `🫠 partial` (soft yellow), `💀 fail` (rose) with colored bg badges
15
+ - **Summary badge** — tool name displayed as bold charcoal badge next to model badge
16
+ - **`stripMarkdown` options** — `{ boldHeaders: true }` converts markdown/emoji headers to ANSI-styled badges
17
+ - **Empty input guard** on `stripMarkdown` — early return for empty/whitespace input
18
+ - **Strip markdown headers** — `##` prefixes and `───` decorators removed from output
19
+ - **8 unit tests** for `stripMarkdown` covering headers, bold, bullets, code blocks, HR, empty input
20
+
21
+ ### Fixed
22
+ - **ANSI truncation corruption** — truncate raw content BEFORE applying ANSI badges (prevents mid-escape code corruption)
23
+ - **Summary badge without model** — tools returning null from `inferModelFromTool` (think, focus) still show tool name badge
24
+ - **Unused imports** — cleaned up 10+ unused imports/variables in server.ts
25
+
26
+ ### Changed
27
+ - **Emoji palette** — analysis 🧠, insight 🔮, key 🗝, verdict 👩‍⚖️ (replaced 🔍🧿🪩🎯)
28
+ - **Auditor/Challenger** — use `EMOJI_PALETTE` constants instead of hardcoded emoji
29
+ - **Planner** — topological task ordering with T-ID preservation and Dependencies metadata
30
+
8
31
  ## [2.18.0] - 2026-03-21
9
32
 
10
33
  ### Added
@@ -1,3 +1,4 @@
1
+ import { EMOJI_PALETTE } from '../utils/format-constants.js';
1
2
  export class Auditor {
2
3
  constructor() {
3
4
  this.defaultModel = 'perplexity-sonar-pro';
@@ -328,7 +329,7 @@ export class Auditor {
328
329
  // Critical assumptions
329
330
  const criticalAssumptions = assumptions.filter(a => a.risk === 'high');
330
331
  if (criticalAssumptions.length > 0) {
331
- synthesis += `**Critical Assumptions** 🔴:\n`;
332
+ synthesis += `**Critical Assumptions** ${EMOJI_PALETTE.bad}:\n`;
332
333
  criticalAssumptions.forEach(assumption => {
333
334
  synthesis += `- ${assumption.description}\n`;
334
335
  synthesis += ` Validation: ${assumption.validationMethod}\n`;
@@ -20,6 +20,7 @@ import { getChallengerModels } from '../config/model-defaults.js';
20
20
  import { createProgressStream } from '../utils/progress-stream.js';
21
21
  import { smartAPIClient } from '../utils/smart-api-client.js';
22
22
  import { getSmartTimeout } from '../config/timeout-config.js';
23
+ import { EMOJI_PALETTE } from '../utils/format-constants.js';
23
24
  import { execSync } from 'child_process';
24
25
  // import {
25
26
  // renderTable,
@@ -755,7 +756,7 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
755
756
  lines.push('');
756
757
  const challengesTableData = challenges.map((ch, i) => {
757
758
  const claim = claims.find(c => c.id === ch.claimId);
758
- const severityIcon = ch.severity === 'high' ? '🔴' : ch.severity === 'medium' ? '🟡' : '🟢';
759
+ const severityIcon = ch.severity === 'high' ? EMOJI_PALETTE.bad : ch.severity === 'medium' ? EMOJI_PALETTE.warn : EMOJI_PALETTE.good;
759
760
  return {
760
761
  '#': String(i + 1),
761
762
  Severity: `${severityIcon} ${ch.severity}`,
@@ -768,7 +769,7 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
768
769
  // Show full challenges below
769
770
  challenges.forEach((ch, i) => {
770
771
  const claim = claims.find(c => c.id === ch.claimId);
771
- const severityIcon = ch.severity === 'high' ? '🔴' : ch.severity === 'medium' ? '🟡' : '🟢';
772
+ const severityIcon = ch.severity === 'high' ? EMOJI_PALETTE.bad : ch.severity === 'medium' ? EMOJI_PALETTE.warn : EMOJI_PALETTE.good;
772
773
  lines.push(`${severityIcon} **Challenge #${i + 1}** (${ch.severity} severity)`);
773
774
  lines.push(`Original: "${claim?.text}"`);
774
775
  lines.push('');
@@ -863,9 +864,9 @@ Write concrete, specific analysis. Do NOT include brackets or placeholders.`;
863
864
  lines.push('');
864
865
  lines.push(renderKeyValueTable({
865
866
  'Total Challenges': String(challenges.length),
866
- '🔴 High Severity': String(highSeverity),
867
- '🟡 Medium Severity': String(mediumSeverity),
868
- '🟢 Low Severity': String(lowSeverity),
867
+ [EMOJI_PALETTE.bad + ' High Severity']: String(highSeverity),
868
+ [EMOJI_PALETTE.warn + ' Medium Severity']: String(mediumSeverity),
869
+ [EMOJI_PALETTE.good + ' Low Severity']: String(lowSeverity),
869
870
  'Alternatives': String(alternatives.length),
870
871
  'Groupthink Risk': groupthinkRisk,
871
872
  }));
@@ -25,7 +25,7 @@ const savedThemeVars = {
25
25
  RENDER_OUTPUT: process.env.RENDER_OUTPUT,
26
26
  TACHIBOT_THEME: process.env.TACHIBOT_THEME,
27
27
  };
28
- const envResult = dotenvConfig({
28
+ dotenvConfig({
29
29
  path: envPath,
30
30
  override: true // API keys from .env take priority
31
31
  });
@@ -52,7 +52,7 @@ import { z } from "zod";
52
52
  import { InstructionOrchestrator } from "./orchestrator-instructions.js";
53
53
  import { validateToolInput, sanitizeForLogging } from "./utils/input-validator.js";
54
54
  import { isToolEnabled, logToolConfiguration } from "./utils/tool-config.js";
55
- import { stripMarkdown } from "./utils/ansi-renderer.js";
55
+ import { renderOutput } from "./utils/ansi-renderer.js";
56
56
  import { getToolAnnotations } from "./utils/tool-annotations.js";
57
57
  import { truncateSmart } from "./utils/stream-distill.js";
58
58
  import { trackToolCall, inferModelFromTool, estimateTokens, isTrackingEnabled, getUsageSummary, getAllReposSummary, getStatsJson, resetStats } from "./utils/usage-tracker.js";
@@ -158,16 +158,17 @@ function safeAddTool(tool) {
158
158
  // Silently ignore tracking errors
159
159
  }
160
160
  }
161
- // Return clean plain text strip markdown formatting since Claude Code
162
- // doesn't render markdown in tool results (shows raw ** and ### as text)
161
+ // Apply render mode (sparse = badge + bold headers + stripped, etc.)
163
162
  if (typeof result === 'string') {
164
- let cleanText = stripMarkdown(result);
165
- // Safety net: cap at 25K chars to prevent Claude Code's 30K truncation
166
- const MAX_RESPONSE_CHARS = 25000;
167
- if (cleanText.length > MAX_RESPONSE_CHARS) {
168
- cleanText = truncateSmart(cleanText, MAX_RESPONSE_CHARS);
163
+ const model = inferModelFromTool(tool.name) || undefined;
164
+ // Truncate raw content BEFORE ANSI rendering prevents mid-escape corruption
165
+ let raw = result;
166
+ const MAX_RAW_CHARS = 24000;
167
+ if (raw.length > MAX_RAW_CHARS) {
168
+ raw = truncateSmart(raw, MAX_RAW_CHARS);
169
169
  }
170
- return { type: "text", text: cleanText };
170
+ const rendered = renderOutput(raw, { model, summary: tool.name });
171
+ return { type: "text", text: rendered };
171
172
  }
172
173
  return result;
173
174
  }
@@ -226,7 +227,7 @@ safeAddTool({
226
227
  }),
227
228
  execute: async (args, mcpContext) => {
228
229
  const { log } = mcpContext;
229
- let { query, mode = "simple", context, domain, tokenEfficient = false, rounds = 5, executeNow = true, models, temperature = 0.7, saveSession = true, maxTokensPerRound = 2000, pingPongStyle = "collaborative" } = args;
230
+ let { query, mode = "simple", context, domain, tokenEfficient = false, rounds = 5, executeNow: _executeNow = true, models, temperature = 0.7, saveSession: _saveSession = true, maxTokensPerRound: _maxTokensPerRound = 2000, pingPongStyle: _pingPongStyle = "collaborative" } = args;
230
231
  // Validate and sanitize input
231
232
  const queryValidation = validateToolInput(query);
232
233
  if (!queryValidation.valid) {
@@ -332,8 +333,7 @@ TIPS FOR EFFECTIVE SYNTHESIS:
332
333
  Ready to help synthesize your collective intelligence results!`;
333
334
  default: // simple mode
334
335
  // BigText header disabled - plain text only
335
- const focusHeader = '';
336
- const focusBadge = '';
336
+ // BigText header/badge removed — plain text only
337
337
  return `Enhanced reasoning for: "${query}"
338
338
  ${context ? `Context: ${context}` : ''}
339
339
 
@@ -458,8 +458,7 @@ MemoryProvider: Pluggable memory (devlog, mem0, custom). Set TACHIBOT_MEMORY_PRO
458
458
  });
459
459
  // Build response with model output if available
460
460
  // BigText header disabled - plain text only
461
- const thinkBadge = '';
462
- const thinkHeader = '';
461
+ // BigText header/badge removed — plain text only
463
462
  let response = '';
464
463
  if (result.modelResponse) {
465
464
  response += `## Model Response (${args.model}):\n\n${result.modelResponse}\n\n---\n\n`;
@@ -691,7 +690,7 @@ async function initializeServer() {
691
690
  console.error("✅ Server.start() called successfully");
692
691
  // Keep the process alive with a heartbeat
693
692
  // This ensures the server doesn't exit prematurely
694
- const heartbeatInterval = setInterval(() => {
693
+ setInterval(() => {
695
694
  // Heartbeat to keep process alive
696
695
  // Log every 30 seconds to show we're still alive
697
696
  const now = new Date().toISOString();
@@ -774,7 +774,8 @@ ${codeContext ? "\nNote: All analysis was performed on actual code provided." :
774
774
 
775
775
  OUTPUT FORMAT — Each task must be bite-sized (2-5 min):
776
776
 
777
- ### Task N: [Component Name]
777
+ ### Task [T-ID]: [Component Name]
778
+ **Dependencies:** Blocked by: T1,T3 | Blocks: T4 | Parallel: yes/no | Complexity: Low/Med/High
778
779
  **Files:** Create: path/to/file | Modify: path/to/file:lines | Test: path/to/test
779
780
  **Step 1:** Write the failing test (show test code)
780
781
  **Step 2:** Run test to verify it fails (exact command + expected output)
@@ -789,7 +790,8 @@ REQUIREMENTS:
789
790
  4. Exact commands with expected output
790
791
  5. Checkpoints at 50%, 80%, and 100%
791
792
  6. Address ALL pre-mortem failure causes from critique as mitigations
792
- 7. Order tasks simplest → hardest (least-to-most)
793
+ 7. Order tasks topologically: a task MUST appear after all tasks that block it. Within independent groups, order simplest → hardest (using Complexity).
794
+ 8. Preserve exact task IDs from TASK DECOMPOSITION (T1, T1.1, T2). Do NOT renumber. Each task MUST include the Dependencies line with Blocked by/Blocks/Parallel/Complexity fields.
793
795
 
794
796
  **QUALITY ASSESSMENT (at the end):**
795
797
  - Code Quality: X/10
@@ -5,6 +5,7 @@
5
5
  * Uses the theme system from ansi-styles.ts and ink-markdown-renderer.tsx.
6
6
  *
7
7
  * Configuration:
8
+ * RENDER_OUTPUT=sparse - ANSI badge header for model/tool name + raw markdown body (~1.01x tokens)
8
9
  * RENDER_OUTPUT=markdown - Raw markdown, no processing (default, ~1x tokens)
9
10
  * RENDER_OUTPUT=ink - React Ink rendering with themes, gradients, tables (~12x tokens)
10
11
  * RENDER_OUTPUT=ansi - Legacy marked-terminal rendering
@@ -20,7 +21,7 @@ import chalk from 'chalk';
20
21
  chalk.level = 3;
21
22
  // Force gradient-string colors
22
23
  process.env.FORCE_COLOR = '3';
23
- import { getTheme, renderModelBadge, toolResultHeader, dividers, } from './ansi-styles.js';
24
+ import { getTheme, renderModelBadge, renderSummaryBadge, toolResultHeader, dividers, } from './ansi-styles.js';
24
25
  const renderGradientDivider = (width = 50, preset) => '-'.repeat(width);
25
26
  const renderGradientModelName = (model) => `[${model.toUpperCase()}]`;
26
27
  // import { renderMarkdownToAnsi as renderInkMarkdown } from './ink-markdown-renderer.js';
@@ -105,7 +106,7 @@ export function clearThemeCache() {
105
106
  export function getRenderMode() {
106
107
  if (!cachedRenderMode) {
107
108
  const mode = process.env.RENDER_OUTPUT?.toLowerCase();
108
- if (mode === 'ink' || mode === 'plain' || mode === 'ansi') {
109
+ if (mode === 'ink' || mode === 'plain' || mode === 'ansi' || mode === 'sparse') {
109
110
  cachedRenderMode = mode;
110
111
  }
111
112
  else {
@@ -176,6 +177,18 @@ export function renderOutput(content, modelOrOptions) {
176
177
  output += renderAnsi(content);
177
178
  output += '\n' + simpleGradient(60) + '\n';
178
179
  break;
180
+ case 'sparse': {
181
+ // Sparse mode: badge bar (model + summary) + bold headers + stripped text
182
+ const parts = [];
183
+ if (options.model)
184
+ parts.push(renderModelBadge(options.model));
185
+ if (options.summary)
186
+ parts.push(renderSummaryBadge(options.summary));
187
+ if (parts.length)
188
+ output += parts.join('') + '\n';
189
+ output += stripMarkdown(content, { boldHeaders: true });
190
+ break;
191
+ }
179
192
  case 'plain':
180
193
  output += stripMarkdown(content);
181
194
  break;
@@ -422,18 +435,10 @@ function renderAnsi(md) {
422
435
  return md;
423
436
  }
424
437
  }
425
- // ============================================================================
426
- // PLAIN TEXT STRIPPER
427
- // ============================================================================
428
- /**
429
- * Strip decorative markdown formatting but KEEP structural elements.
430
- * Keeps: # headers, - bullets, 1. numbered lists, > blockquotes, | tables, indentation
431
- * Strips: **bold**, *italic*, ~~strike~~, `inline code` backticks, [link](url), ![img](url), ``` fences
432
- *
433
- * Code blocks are protected via placeholder extraction to avoid corrupting code content.
434
- * Underscore italic is skipped to avoid mangling snake_case identifiers.
435
- */
436
- export function stripMarkdown(md) {
438
+ export function stripMarkdown(md, options) {
439
+ if (!md || !md.trim())
440
+ return '';
441
+ const { boldHeaders = false } = options || {};
437
442
  // 1. Extract code blocks to placeholders (protect from stripping)
438
443
  const codeBlocks = [];
439
444
  let text = md.replace(/```[\s\S]*?```/g, (match) => {
@@ -443,6 +448,20 @@ export function stripMarkdown(md) {
443
448
  });
444
449
  // 2. Strip decorative formatting on non-code text
445
450
  text = text
451
+ // Markdown headers — strip # prefix (or bold if boldHeaders)
452
+ .replace(/^#{1,6}\s+(.+)$/gm, boldHeaders ? '\x1b[1m$1\x1b[0m' : '$1')
453
+ // Emoji section headers — e.g. "🔍 TYPE SAFETY ───" → soft teal bg, dark bold text
454
+ .replace(/^(.{1,2})\s+([A-Z][A-Z\s&]+?)\s*─+$/gm, boldHeaders ? '\x1b[48;5;73m\x1b[30m\x1b[1m $1 $2 \x1b[0m' : '$1 $2')
455
+ // Verdict lines — color-coded: green=pass, yellow=partial, red=fail
456
+ .replace(/^(✅|🫠|💀|🟢|🟡|🔴)\s*(pass|partial|fail)\b(.*)$/gmi, (_match, emoji, status, rest) => {
457
+ if (!boldHeaders)
458
+ return `${emoji} ${status}${rest}`;
459
+ const s = status.toLowerCase();
460
+ const bg = s === 'pass' ? '\x1b[48;5;151m' : s === 'partial' ? '\x1b[48;5;186m' : '\x1b[48;5;174m';
461
+ return `${bg}\x1b[30m\x1b[1m ${emoji} ${status} \x1b[0m${rest}`;
462
+ })
463
+ // Horizontal rules
464
+ .replace(/^[-*_]{3,}\s*$/gm, '')
446
465
  // Normalize * bullets to - before italic strip (prevents stray * on "* Security:")
447
466
  .replace(/^(\s{0,3})\*(\s+)/gm, '$1-$2')
448
467
  // Images before links (avoid ![...] conflict)
@@ -409,18 +409,19 @@ const fgMap = {
409
409
  };
410
410
  // Unicode colored emoji for badges (ANSI-free, works everywhere)
411
411
  const MODEL_EMOJI = {
412
- grok: '🟣', // purple
413
- gemini: '🔵', // blue
414
- openai: '🟢', // green
415
- perplexity: '🔷', // cyan diamond
416
- kimi: '🟡', // yellow
417
- qwen: '🔴', // red
418
- focus: '',
419
- workflow: '',
420
- scout: '🔍',
412
+ grok: '🔮',
413
+ gemini: '🌀',
414
+ openai: '🧠',
415
+ perplexity: '🔭',
416
+ kimi: '🐉',
417
+ qwen: '🐉',
418
+ minimax: '🤖',
419
+ focus: '🎯',
420
+ workflow: '🌊',
421
+ scout: '🔎',
421
422
  verifier: '✅',
422
423
  challenger: '⚔️',
423
- think: '💭',
424
+ think: '🫨',
424
425
  };
425
426
  /**
426
427
  * Render a model badge with ANSI background color
@@ -446,6 +447,16 @@ export function renderModelBadge(model, theme) {
446
447
  const label = badgeStyle.label || ` ${normalized} `;
447
448
  return `${bg}${fg}${ANSI.bold}${label}${ANSI.reset}`;
448
449
  }
450
+ /**
451
+ * Render a summary badge with dimmed bg + dark text (pairs with model badge)
452
+ * e.g. model badge: [bright magenta] grok [reset] + summary: [dim gray] search query [reset]
453
+ */
454
+ export function renderSummaryBadge(summary) {
455
+ // Dimmed bg + bold sits next to the bright model badge
456
+ const bg = '\x1b[48;5;238m'; // dark gray bg (ansi256 color 238)
457
+ const fg = '\x1b[97m'; // bright white text
458
+ return `${bg}${fg}${ANSI.bold} ${summary} ${ANSI.reset}`;
459
+ }
449
460
  /**
450
461
  * Get model badge (alias for backward compatibility)
451
462
  */
@@ -19,29 +19,67 @@ LISTS: Hierarchy for structure:
19
19
 
20
20
  CODE: \`\`\`lang blocks. Minimal inline comments.
21
21
 
22
- SECTIONS: 🔍 HEADER ───
22
+ SECTIONS: 🧠 HEADER ───
23
23
  Blank line after. 2-4 sections max.
24
24
 
25
- VERDICT: 🟢 pass 🟡 partial 🔴 fail
25
+ VERDICT: pass 🫠 partial 💀 fail
26
26
  End only. One line + reason.`;
27
27
  export const EMOJI_PALETTE = {
28
28
  // Section headers
29
- analysis: '🔍',
30
- verdict: '🎯',
31
- insight: '💡',
32
- key: '',
29
+ analysis: '🧠',
30
+ verdict: '👩‍⚖️',
31
+ insight: '🔮',
32
+ key: '🗝',
33
33
  judge: '⚖️',
34
- fix: '🔧',
34
+ fix: '🪛',
35
35
  search: '🔎',
36
- idea: '💭',
37
- warning: '⚠️',
36
+ idea: '🫧',
37
+ warning: '🫠',
38
+ sources: '📎',
39
+ compare: '⚡',
40
+ code: '🦾',
41
+ brainstorm: '🌩️',
42
+ reason: '🧪',
43
+ plan: '📋',
44
+ debug: '🐛',
45
+ security: '🛡️',
46
+ performance: '🔥',
47
+ architecture: '🏗️',
48
+ consensus: '🤝',
49
+ conflict: '⚔️',
50
+ caveat: '📌',
51
+ thinking: '🫨',
52
+ decompose: '🪆',
53
+ workflow: '🌊',
54
+ summary: '🧾',
38
55
  // Status indicators
39
56
  good: '🟢',
40
57
  warn: '🟡',
41
58
  bad: '🔴',
42
59
  info: '🔵',
60
+ pass: '✅',
61
+ fail: '💀',
62
+ partial: '🫠',
63
+ running: '⏳',
64
+ blocked: '🚫',
65
+ hot: '🔥',
66
+ cold: '🧊',
67
+ // Models
68
+ grok: '🔮',
69
+ gemini: '🌀',
70
+ openai: '🧠',
71
+ perplexity: '🔭',
72
+ kimi: '🐉',
73
+ qwen: '🐉',
74
+ minimax: '🤖',
43
75
  // Actions
44
76
  arrow: '→',
45
77
  check: '✓',
46
78
  cross: '✗',
79
+ link: '🔗',
80
+ pin: '📍',
81
+ time: '⏱️',
82
+ cost: '💰',
83
+ sparkle: '✨',
84
+ nuke: '💥',
47
85
  };
@@ -0,0 +1,252 @@
1
+ # Sparse Mode: ANSI Bold Headers Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Convert `## headers` to ANSI bold text (instead of stripping them) in sparse render mode, giving readable section separation at ~3 tokens/header instead of 0 (stripped) or ~12 (colored badge).
6
+
7
+ **Architecture:** Modify `stripMarkdown()` in `ansi-renderer.ts` to accept an options parameter. When `boldHeaders: true`, headers get `\x1b[1m` ANSI bold wrapping instead of being stripped to plain text. The sparse case in `renderOutput()` calls `stripMarkdown` with this option enabled.
8
+
9
+ **Tech Stack:** TypeScript, raw ANSI escape codes (`\x1b[1m`, `\x1b[0m`)
10
+
11
+ ---
12
+
13
+ ### Task 1: Add ANSI Bold Header Support to stripMarkdown
14
+
15
+ **Files:**
16
+ - Modify: `src/utils/ansi-renderer.ts:552-592` (stripMarkdown function)
17
+ - Test: `src/utils/__tests__/strip-markdown.test.ts`
18
+
19
+ - [ ] **Step 1: Write the failing test**
20
+
21
+ ```typescript
22
+ // src/utils/__tests__/strip-markdown.test.ts
23
+ import { describe, it, expect } from 'vitest';
24
+ import { stripMarkdown } from '../ansi-renderer.js';
25
+
26
+ describe('stripMarkdown', () => {
27
+ it('strips headers to plain text by default', () => {
28
+ expect(stripMarkdown('## Analysis')).toBe('Analysis');
29
+ expect(stripMarkdown('### Summary')).toBe('Summary');
30
+ });
31
+
32
+ it('converts headers to ANSI bold when boldHeaders is true', () => {
33
+ const result = stripMarkdown('## Analysis', { boldHeaders: true });
34
+ expect(result).toBe('\x1b[1mAnalysis\x1b[0m');
35
+ });
36
+
37
+ it('handles h1-h6 with ANSI bold', () => {
38
+ const result = stripMarkdown('# Title\n## Section\n### Sub', { boldHeaders: true });
39
+ expect(result).toContain('\x1b[1mTitle\x1b[0m');
40
+ expect(result).toContain('\x1b[1mSection\x1b[0m');
41
+ expect(result).toContain('\x1b[1mSub\x1b[0m');
42
+ });
43
+
44
+ it('strips bold/italic/strikethrough normally', () => {
45
+ expect(stripMarkdown('**bold** and *italic* and ~~strike~~')).toBe('bold and italic and strike');
46
+ });
47
+
48
+ it('preserves bullets and indentation', () => {
49
+ const input = '- Item 1\n - Sub item\n - Deep';
50
+ expect(stripMarkdown(input)).toBe(input);
51
+ });
52
+
53
+ it('preserves code blocks untouched', () => {
54
+ const input = '```js\nconst x = 1;\n```';
55
+ expect(stripMarkdown(input)).toContain('const x = 1;');
56
+ });
57
+
58
+ it('strips horizontal rules', () => {
59
+ expect(stripMarkdown('---')).toBe('');
60
+ expect(stripMarkdown('***')).toBe('');
61
+ });
62
+
63
+ it('adds blank line after bold header for readability', () => {
64
+ const result = stripMarkdown('## Analysis\nSome text', { boldHeaders: true });
65
+ expect(result).toBe('\x1b[1mAnalysis\x1b[0m\nSome text');
66
+ });
67
+ });
68
+ ```
69
+
70
+ - [ ] **Step 2: Run test to verify it fails**
71
+
72
+ Run: `npx vitest run src/utils/__tests__/strip-markdown.test.ts`
73
+ Expected: FAIL — `stripMarkdown` does not accept options parameter
74
+
75
+ - [ ] **Step 3: Add options parameter to stripMarkdown**
76
+
77
+ In `src/utils/ansi-renderer.ts`, change the `stripMarkdown` signature and header regex:
78
+
79
+ ```typescript
80
+ export interface StripMarkdownOptions {
81
+ /** Convert ## headers to ANSI bold instead of stripping # prefix */
82
+ boldHeaders?: boolean;
83
+ }
84
+
85
+ export function stripMarkdown(md: string, options?: StripMarkdownOptions): string {
86
+ const { boldHeaders = false } = options || {};
87
+
88
+ // ... existing code block extraction ...
89
+
90
+ // 2. Strip decorative formatting on non-code text
91
+ text = text
92
+ // Headers — ANSI bold or strip # prefix
93
+ .replace(/^#{1,6}\s+(.+)$/gm, boldHeaders ? '\x1b[1m$1\x1b[0m' : '$1')
94
+ // ... rest of existing regexes unchanged ...
95
+ ```
96
+
97
+ - [ ] **Step 4: Run test to verify it passes**
98
+
99
+ Run: `npx vitest run src/utils/__tests__/strip-markdown.test.ts`
100
+ Expected: ALL PASS
101
+
102
+ - [ ] **Step 5: Commit**
103
+
104
+ ```bash
105
+ git add src/utils/__tests__/strip-markdown.test.ts src/utils/ansi-renderer.ts
106
+ git commit -m "feat(sparse): convert headers to ANSI bold in stripMarkdown"
107
+ ```
108
+
109
+ ---
110
+
111
+ ### Task 2: Wire Sparse Mode to Use Bold Headers
112
+
113
+ **Files:**
114
+ - Modify: `src/utils/ansi-renderer.ts:240-253` (sparse case in renderOutput)
115
+
116
+ - [ ] **Step 1: Update sparse case to pass boldHeaders option**
117
+
118
+ In the `renderOutput` function's sparse case:
119
+
120
+ ```typescript
121
+ case 'sparse': {
122
+ // Sparse mode: ANSI badge header + clean text with bold section headers
123
+ if (options.model) {
124
+ const badge = renderModelBadge(options.model);
125
+ if (options.summary) {
126
+ output = badge + renderSummaryBadge(options.summary, options.model) + '\n';
127
+ } else {
128
+ output = badge + '\n';
129
+ }
130
+ }
131
+ output += stripMarkdown(content, { boldHeaders: true });
132
+ break;
133
+ }
134
+ ```
135
+
136
+ - [ ] **Step 2: Verify plain mode still strips headers (no bold)**
137
+
138
+ The `plain` case should remain unchanged:
139
+ ```typescript
140
+ case 'plain':
141
+ output += stripMarkdown(content); // no options = headers stripped flat
142
+ break;
143
+ ```
144
+
145
+ - [ ] **Step 3: Build and test end-to-end**
146
+
147
+ Run: `npm run build && node -e "
148
+ import { renderOutput } from './dist/src/utils/ansi-renderer.js';
149
+ const md = '## Analysis\n\n**Critical bug** in server.ts:\n- Missing null check\n\n### Summary\nLooks good.';
150
+ console.log('--- sparse ---');
151
+ console.log(renderOutput(md, { model: 'gemini', summary: 'gemini_analyze_code', mode: 'sparse' }));
152
+ console.log('--- plain ---');
153
+ console.log(renderOutput(md, { model: 'gemini', mode: 'plain' }));
154
+ "`
155
+
156
+ Expected:
157
+ - sparse: badge + `\x1b[1mAnalysis\x1b[0m` (bold) + stripped body
158
+ - plain: `Analysis` (no bold) + stripped body
159
+
160
+ - [ ] **Step 4: Commit**
161
+
162
+ ```bash
163
+ git add src/utils/ansi-renderer.ts
164
+ git commit -m "feat(sparse): wire bold headers in sparse render mode"
165
+ ```
166
+
167
+ ---
168
+
169
+ ### Task 3: Fix TS Unused Import Warnings in server.ts
170
+
171
+ **Files:**
172
+ - Modify: `src/server.ts:34,63,67,71,73,78,84,339,342`
173
+
174
+ - [ ] **Step 1: Remove unused imports and variables**
175
+
176
+ Lines to fix (from TS diagnostics):
177
+ - Line 34: `envResult` — prefix with `void` or remove assignment: `dotenvConfig({ path: envPath, override: true });`
178
+ - Line 63: `stripMarkdown` — already removed in this session
179
+ - Line 67: `getUpdateStatus` — remove from import
180
+ - Line 71: `TechnicalDomain` — remove import
181
+ - Line 73: `getUnifiedAITools` — remove from import
182
+ - Line 78: `createFocusDeepPlan`, `generateFocusDeepVisualization` — remove from import
183
+ - Line 84: `getAllOpenRouterTools` — remove from import
184
+ - Line 339: `executeNow` — remove or prefix unused destructure
185
+ - Line 342: `saveSession` — remove or prefix unused destructure
186
+
187
+ - [ ] **Step 2: Build to verify no errors**
188
+
189
+ Run: `npm run build`
190
+ Expected: Clean build, no warnings
191
+
192
+ - [ ] **Step 3: Run existing tests**
193
+
194
+ Run: `npm test`
195
+ Expected: ALL PASS
196
+
197
+ - [ ] **Step 4: Commit**
198
+
199
+ ```bash
200
+ git add src/server.ts
201
+ git commit -m "chore: remove unused imports in server.ts"
202
+ ```
203
+
204
+ ---
205
+
206
+ ### Task 4: Set RENDER_OUTPUT=sparse as Default
207
+
208
+ **Files:**
209
+ - Modify: `.env` (add `RENDER_OUTPUT=sparse`)
210
+ - Modify: `src/utils/ansi-renderer.ts:113` (optional: change default from `markdown` to `sparse`)
211
+
212
+ - [ ] **Step 1: Add to .env**
213
+
214
+ ```bash
215
+ echo 'RENDER_OUTPUT=sparse' >> .env
216
+ ```
217
+
218
+ - [ ] **Step 2: Decide on code default**
219
+
220
+ Option A: Keep `markdown` as code default, override in `.env` only (safer, no behavior change for other users)
221
+ Option B: Change code default to `sparse` (all users get badges by default)
222
+
223
+ Recommend: Option A — set in `.env`, keep code default as `markdown`.
224
+
225
+ - [ ] **Step 3: Restart MCP server and test with a real tool call**
226
+
227
+ Call any tachibot tool (e.g., `gemini_analyze_code`) and verify:
228
+ - Colored badge appears at top
229
+ - Summary badge next to it
230
+ - Headers are bold
231
+ - Body is clean (no `**`, `##`, `~~`)
232
+
233
+ - [ ] **Step 4: Commit**
234
+
235
+ ```bash
236
+ git add .env
237
+ git commit -m "feat: enable sparse render mode in .env"
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Token Cost Summary
243
+
244
+ | Element | Tokens per instance | Instances/response | Total |
245
+ |---------|-------------------:|-------------------:|------:|
246
+ | Model badge (ANSI) | ~12 | 1 | 12 |
247
+ | Summary badge (ANSI) | ~8 | 1 | 8 |
248
+ | Bold header (ANSI) | ~3-4 | 2-4 | 8-16 |
249
+ | Stripped `**`/`##`/`~~` | -2 each | 10-15 | -20 to -30 |
250
+ | **Net per response** | | | **~0 to +6** |
251
+
252
+ Sparse mode with bold headers is token-neutral vs raw markdown. You get visual readability for free.
@@ -0,0 +1,187 @@
1
+ # Sparse Mode Fixes — Multi-Model Jury Findings
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Fix critical bugs and edge cases found by 4-model jury (Gemini, GPT, Qwen, background Gemini) in sparse render mode.
6
+
7
+ **Architecture:** Three targeted fixes to existing functions — no new files, no architectural changes. Truncation order swap in server.ts, input guards in stripMarkdown, cleanup of renderSummaryBadge.
8
+
9
+ **Tech Stack:** TypeScript, raw ANSI escape codes
10
+
11
+ ---
12
+
13
+ ### Task 1: Truncate Raw Content BEFORE Rendering (Critical)
14
+
15
+ **Why:** All 4 models flagged this. `truncateSmart` runs after `renderOutput`, which adds ANSI escape codes. If truncation lands mid-escape (`\x1b[1`), it corrupts terminal output and bleeds colors. Truncating the raw text first, then applying ANSI badges, guarantees clean output.
16
+
17
+ **Files:**
18
+ - Modify: `src/server.ts:262-272`
19
+ - Test: Manual — build and verify with long content
20
+
21
+ - [ ] **Step 1: Swap truncation order in server.ts**
22
+
23
+ Change from:
24
+ ```typescript
25
+ const model = inferModelFromTool(tool.name) || undefined;
26
+ let rendered = renderOutput(result, { model, summary: tool.name });
27
+ const MAX_RESPONSE_CHARS = 25000;
28
+ if (rendered.length > MAX_RESPONSE_CHARS) {
29
+ rendered = truncateSmart(rendered, MAX_RESPONSE_CHARS);
30
+ }
31
+ return { type: "text" as const, text: rendered };
32
+ ```
33
+
34
+ To:
35
+ ```typescript
36
+ const model = inferModelFromTool(tool.name) || undefined;
37
+ // Truncate raw content BEFORE adding ANSI — prevents mid-escape corruption
38
+ let raw = result;
39
+ const MAX_RAW_CHARS = 24000;
40
+ if (raw.length > MAX_RAW_CHARS) {
41
+ raw = truncateSmart(raw, MAX_RAW_CHARS);
42
+ }
43
+ const rendered = renderOutput(raw, { model, summary: tool.name });
44
+ return { type: "text" as const, text: rendered };
45
+ ```
46
+
47
+ Note: Use 24K (not 25K) to leave ~1K headroom for badge ANSI codes.
48
+
49
+ - [ ] **Step 2: Build and verify**
50
+
51
+ Run: `npm run build`
52
+
53
+ - [ ] **Step 3: Commit**
54
+
55
+ ```bash
56
+ git add src/server.ts
57
+ git commit -m "fix: truncate raw content before ANSI rendering to prevent escape corruption"
58
+ ```
59
+
60
+ ---
61
+
62
+ ### Task 2: Show Summary Badge Even Without Model
63
+
64
+ **Why:** GPT flagged that tools returning `null` from `inferModelFromTool` (e.g., `think`, `focus`, `nextThought`) lose their summary badge entirely. The tool name should still appear.
65
+
66
+ **Files:**
67
+ - Modify: `src/utils/ansi-renderer.ts:240-253` (sparse case)
68
+
69
+ - [ ] **Step 1: Update sparse case to show summary independently**
70
+
71
+ Change from:
72
+ ```typescript
73
+ case 'sparse': {
74
+ if (options.model) {
75
+ const badge = renderModelBadge(options.model);
76
+ if (options.summary) {
77
+ output = badge + renderSummaryBadge(options.summary, options.model) + '\n';
78
+ } else {
79
+ output = badge + '\n';
80
+ }
81
+ }
82
+ output += stripMarkdown(content, { boldHeaders: true });
83
+ break;
84
+ }
85
+ ```
86
+
87
+ To:
88
+ ```typescript
89
+ case 'sparse': {
90
+ const parts: string[] = [];
91
+ if (options.model) {
92
+ parts.push(renderModelBadge(options.model));
93
+ }
94
+ if (options.summary) {
95
+ parts.push(renderSummaryBadge(options.summary));
96
+ }
97
+ if (parts.length) output += parts.join('') + '\n';
98
+ output += stripMarkdown(content, { boldHeaders: true });
99
+ break;
100
+ }
101
+ ```
102
+
103
+ - [ ] **Step 2: Remove unused `model` param from renderSummaryBadge**
104
+
105
+ In `src/utils/ansi-styles.ts`, change:
106
+ ```typescript
107
+ export function renderSummaryBadge(summary: string, model?: string): string {
108
+ ```
109
+ To:
110
+ ```typescript
111
+ export function renderSummaryBadge(summary: string): string {
112
+ ```
113
+
114
+ - [ ] **Step 3: Update import in ansi-renderer.ts if needed**
115
+
116
+ Verify the call site no longer passes `model` to `renderSummaryBadge`.
117
+
118
+ - [ ] **Step 4: Build and test**
119
+
120
+ Run: `npm run build && node -e "
121
+ import { renderOutput } from './dist/src/utils/ansi-renderer.js';
122
+ // Test: no model, has summary
123
+ console.log(renderOutput('Hello', { summary: 'think', mode: 'sparse' }));
124
+ // Test: model + summary
125
+ console.log(renderOutput('Hello', { model: 'grok', summary: 'grok_search', mode: 'sparse' }));
126
+ // Test: no model, no summary
127
+ console.log(renderOutput('Hello', { mode: 'sparse' }));
128
+ "`
129
+
130
+ - [ ] **Step 5: Commit**
131
+
132
+ ```bash
133
+ git add src/utils/ansi-renderer.ts src/utils/ansi-styles.ts
134
+ git commit -m "fix(sparse): show summary badge even when model is null"
135
+ ```
136
+
137
+ ---
138
+
139
+ ### Task 3: Add Input Guards to stripMarkdown
140
+
141
+ **Why:** GPT and Qwen flagged that empty/null input and missing early return can cause unnecessary regex processing.
142
+
143
+ **Files:**
144
+ - Modify: `src/utils/ansi-renderer.ts:557` (stripMarkdown function body)
145
+ - Modify: `src/utils/__tests__/strip-markdown.test.ts` (add empty input test)
146
+
147
+ - [ ] **Step 1: Add test for empty input**
148
+
149
+ Add to the test file:
150
+ ```typescript
151
+ it('returns empty string for empty/null input', () => {
152
+ expect(stripMarkdown('')).toBe('');
153
+ expect(stripMarkdown(' ')).toBe('');
154
+ });
155
+ ```
156
+
157
+ - [ ] **Step 2: Add early return to stripMarkdown**
158
+
159
+ Add at the start of the function body (after destructuring options):
160
+ ```typescript
161
+ if (!md || !md.trim()) return '';
162
+ ```
163
+
164
+ - [ ] **Step 3: Run tests**
165
+
166
+ Run: `npx vitest run src/utils/__tests__/strip-markdown.test.ts`
167
+
168
+ - [ ] **Step 4: Commit**
169
+
170
+ ```bash
171
+ git add src/utils/ansi-renderer.ts src/utils/__tests__/strip-markdown.test.ts
172
+ git commit -m "fix: add empty input guard to stripMarkdown"
173
+ ```
174
+
175
+ ---
176
+
177
+ ### Summary of What We're NOT Fixing
178
+
179
+ | Finding | Decision | Reason |
180
+ |---------|----------|--------|
181
+ | Replace regex with markdown parser | Skip | Overkill — model output is ~95% simple markdown |
182
+ | `@@CODE` placeholder collision | Skip | Probability near zero in real model output |
183
+ | Code block with nested backticks | Skip | Models rarely output nested fences |
184
+ | Link with `)` in URL | Skip | Models rarely output such URLs |
185
+ | Windows `\r\n` line endings | Skip | MCP runs on Unix; models output `\n` |
186
+ | ANSI enters LLM context | Accept | 1M context window; ~1.2x overhead acceptable |
187
+ | Decouple UI from LLM payload | Skip | MCP architecture doesn't support split rendering |
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "tachibot-mcp",
3
3
  "mcpName": "io.github.byPawel/tachibot-mcp",
4
4
  "displayName": "TachiBot MCP - Universal AI Orchestrator",
5
- "version": "2.18.0",
5
+ "version": "2.19.0",
6
6
  "type": "module",
7
7
  "main": "dist/src/server.js",
8
8
  "bin": {