feat(brain): add fix hints to /brain for actionable degraded state

Adds buildSplitBrainFixHints() to split-brain-status.ts — maps each
degraded component to the exact just command that repairs it:
  - missing artifact → just setup --step skills-memory
  - outdated artifact → just setup --step skills-memory
  - skills DB down   → just setup-db
  - runtime lookup   → just setup --step skills-init

/brain now shows issues followed by → fix hints when degraded, turning
the command from a diagnostic into an operator runbook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---
Build: pass | Tests: pass — Tests  2067 passed (2067)
This commit is contained in:
Operator & Claude Code 2026-04-29 12:31:25 +02:00
parent ca3a02283e
commit 2ee217f42b
4 changed files with 96 additions and 0 deletions

View file

@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import {
collectSplitBrainIssues,
deriveSplitBrainReadiness,
buildSplitBrainFixHints,
type SplitBrainStatus,
} from './split-brain-status.js';
@ -82,3 +83,29 @@ describe('collectSplitBrainIssues', () => {
expect(issues).toEqual(['built-in knowledge artifact is incomplete']);
});
});
describe('buildSplitBrainFixHints', () => {
it('returns no hints for a ready status', () => {
expect(buildSplitBrainFixHints(readyStatus())).toEqual([]);
});
it('returns skills-memory hint for missing artifact', () => {
const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsArtifact: 'missing' });
expect(hints).toContain('Import knowledge artifact: just setup --step skills-memory');
});
it('returns skills-memory hint for outdated artifact', () => {
const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsArtifact: 'incomplete' });
expect(hints).toContain('Refresh knowledge artifact: just setup --step skills-memory');
});
it('returns db hint for unavailable skills db', () => {
const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsDb: 'missing' });
expect(hints).toContain('Fix skills DB: check PostgreSQL, then run: just setup-db');
});
it('returns skills-init hint for missing runtime lookup', () => {
const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsRuntimeLookup: 'missing' });
expect(hints).toContain('Restore runtime lookup: just setup --step skills-init');
});
});

View file

@ -221,3 +221,30 @@ export function collectSplitBrainIssues(status: SplitBrainStatus): string[] {
return issues;
}
// Returns operator-facing fix hints for each degraded component.
// Kept separate from collectSplitBrainIssues so startup report stays concise
// while /brain can show the full remediation path.
export function buildSplitBrainFixHints(status: SplitBrainStatus): string[] {
const hints: string[] = [];
if (status.skillsDb !== 'available') {
hints.push('Fix skills DB: check PostgreSQL, then run: just setup-db');
}
if (status.memoryDb !== 'available') {
hints.push('Fix memory DB: check PostgreSQL, then run: just setup-db');
}
if (status.skillsArtifact !== 'ready') {
if (status.skillsArtifact === 'missing') {
hints.push('Import knowledge artifact: just setup --step skills-memory');
} else {
// outdated or incomplete
hints.push('Refresh knowledge artifact: just setup --step skills-memory');
}
}
if (status.skillsRuntimeLookup !== 'present') {
hints.push('Restore runtime lookup: just setup --step skills-init');
}
return hints;
}

View file

@ -19,6 +19,7 @@ vi.mock('./split-brain-status.js', () => ({
})),
collectSplitBrainIssues: vi.fn(() => []),
deriveSplitBrainReadiness: vi.fn(() => 'ready'),
buildSplitBrainFixHints: vi.fn(() => []),
}));
afterEach(() => {
@ -74,4 +75,37 @@ describe('/brain command', () => {
expect(replies[0]?.text).not.toContain('(vv0.7.0)');
expect(replies[0]?.opts).toEqual({ parse_mode: 'HTML' });
});
it('shows fix hints when degraded', async () => {
const { collectSplitBrainIssues, deriveSplitBrainReadiness, buildSplitBrainFixHints } =
await import('./split-brain-status.js');
vi.mocked(deriveSplitBrainReadiness).mockReturnValue('degraded');
vi.mocked(collectSplitBrainIssues).mockReturnValue([
'built-in knowledge artifact is outdated (loaded: v0.6.0, expected: v0.7.0)',
]);
vi.mocked(buildSplitBrainFixHints).mockReturnValue([
'Refresh knowledge artifact: just setup --step skills-memory',
]);
process.env.TELEGRAM_ADMIN_IDS = '123';
process.env.TELEGRAM_OPS_CHAT_ID = 'tg:999';
const replies: Array<{ text: string; opts?: unknown }> = [];
const ctxArg = {
from: { id: 123 },
chat: { id: 999 },
reply: async (text: string, opts?: unknown) => {
replies.push({ text, opts });
},
};
const { handleBrainCommand } = await import('./telegram-commands.js');
await handleBrainCommand(ctxArg, 'tg:999');
expect(replies).toHaveLength(1);
const text = replies[0]?.text ?? '';
expect(text).toContain('⚠ Brain: <b>degraded</b>');
expect(text).toContain('⚠ built-in knowledge artifact is outdated');
expect(text).toContain('→ Refresh knowledge artifact: just setup --step skills-memory');
});
});

View file

@ -74,6 +74,7 @@ import {
collectSplitBrainStatus,
collectSplitBrainIssues,
deriveSplitBrainReadiness,
buildSplitBrainFixHints,
} from './split-brain-status.js';
import {
buildTestReport,
@ -783,6 +784,13 @@ export async function handleBrainCommand(
for (const issue of issues) {
lines.push(`${issue}`);
}
const hints = buildSplitBrainFixHints(status);
if (hints.length > 0) {
lines.push('');
for (const hint of hints) {
lines.push(`${hint}`);
}
}
}
await ctxArg.reply(lines.join('\n'), { parse_mode: 'HTML' });