diff --git a/tools/brain-config.mjs b/tools/brain-config.mjs index dc06e8d..74d58eb 100644 --- a/tools/brain-config.mjs +++ b/tools/brain-config.mjs @@ -67,3 +67,11 @@ export function loadConfig(root = '.', fsImpl = fsDefault) { } return resolveConfig(parseBrainConfig(md)); } + +/** fail-safe резолвер state_dir (§5.1): непустая строка → как есть; иначе → безопасный дефолт + * .claude/brain-state + warnedFallback (НЕ тихий no-op — wiring издаёт warn и пишет в fallback). */ +export function resolveStateDir(value) { + const v = typeof value === 'string' ? value.trim() : ''; + if (v.length > 0) return { stateDir: v, warnedFallback: false }; + return { stateDir: '.claude/brain-state', warnedFallback: true }; +} diff --git a/tools/brain-config.test.mjs b/tools/brain-config.test.mjs index 52e6a61..646a62a 100644 --- a/tools/brain-config.test.mjs +++ b/tools/brain-config.test.mjs @@ -77,3 +77,19 @@ describe('resolveConfig protected_paths (Task 4 security, §D1/§D2)', () => { .toEqual(['secrets/keys']); }); }); + +import { resolveStateDir } from './brain-config.mjs'; + +describe('resolveStateDir fail-safe (§D2)', () => { + it('непустая строка → как есть, без fallback', () => { + expect(resolveStateDir('docs/observer')).toEqual({ stateDir: 'docs/observer', warnedFallback: false }); + }); + it('пусто / пробелы → безопасный дефолт + warnedFallback', () => { + expect(resolveStateDir('')).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true }); + expect(resolveStateDir(' ')).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true }); + }); + it('не-строка → безопасный дефолт + warnedFallback (не падает)', () => { + expect(resolveStateDir(null)).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true }); + expect(resolveStateDir(undefined)).toEqual({ stateDir: '.claude/brain-state', warnedFallback: true }); + }); +}); diff --git a/tools/brain-retro-opus-reviewer.mjs b/tools/brain-retro-opus-reviewer.mjs index 8ea4823..54a761a 100644 --- a/tools/brain-retro-opus-reviewer.mjs +++ b/tools/brain-retro-opus-reviewer.mjs @@ -42,7 +42,7 @@ const REQUIRED_REVIEW_FIELDS = [ * The split is still applied so the moment either condition flips, caching * activates with zero further code changes. */ -export function buildReviewPromptStructured(episode) { +export function buildReviewPromptStructured(episode, { classifierContext = 'Лидерра' } = {}) { const v = Number(episode?.schema_version) || 0; const cues = [ 'node_quality: correct | wrong_node | overkill | underkill | disputable', @@ -66,7 +66,7 @@ export function buildReviewPromptStructured(episode) { } const system = [ - 'You are the independent reviewer of routing decisions for the Лидерра brain-governance experiment.', + `You are the independent reviewer of routing decisions for the ${classifierContext} brain-governance experiment.`, 'Return ONLY a JSON object with the 8 fields below. No prose, no code fences.', '', 'Fields:', diff --git a/tools/brain-retro-opus-reviewer.test.mjs b/tools/brain-retro-opus-reviewer.test.mjs index 9ab2120..dfe757a 100644 --- a/tools/brain-retro-opus-reviewer.test.mjs +++ b/tools/brain-retro-opus-reviewer.test.mjs @@ -114,3 +114,13 @@ describe('parseReview — 8-dim review schema (spec §4.6)', () => { expect(r?.reviewer_error).toBe('malformed episode'); }); }); + +describe('buildReviewPromptStructured classifierContext (config-seam §D1)', () => { + it('дефолт → Лидерра', () => { + expect(buildReviewPromptStructured({ schema_version: 4 }).system).toContain('Лидерра'); + }); + it('classifierContext инъектируется', () => { + expect(buildReviewPromptStructured({ schema_version: 4 }, { classifierContext: 'ProjZ' }).system) + .toContain('ProjZ'); + }); +}); diff --git a/tools/router-classifier.mjs b/tools/router-classifier.mjs index 3d6479a..c13a782 100644 --- a/tools/router-classifier.mjs +++ b/tools/router-classifier.mjs @@ -288,12 +288,12 @@ export function buildClassifierPrompt(userPrompt, registry, { enrichment = true * Cache-eligibility: Sonnet requires ≥1024 tokens in the cached block. * Active node registry (~85 nodes × ~100 tokens) easily clears this. */ -export function buildClassifierPromptStructured(userPrompt, registry, { enrichment = true } = {}) { +export function buildClassifierPromptStructured(userPrompt, registry, { enrichment = true, classifierContext = 'CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)' } = {}) { const pamyatka = enrichment ? `\n\n${PAMYATKA}\n` : '\n'; const nodesBlock = buildNodesBlock(registry); const chainsBlock = buildChainsBlock(registry); - const system = `Ты классификатор задач для CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3). + const system = `Ты классификатор задач для ${classifierContext}. ОБЯЗАТЕЛЬНЫЕ выходные правила: 0. task_type — ОБЯЗАТЕЛЬНОЕ поле (ровно так, snake_case), одно из: feature, planning, bugfix, refactor, cleanup, marketing, security, analysis, monitoring, memory-sync, question, unknown. @@ -557,7 +557,10 @@ export async function callAnthropicAPI(promptOrMessages, { if (onUsage && data.usage) { try { onUsage(data.usage); } catch { /* swallow callback errors */ } } - return data.content?.[0]?.text || ''; + const _blocks = Array.isArray(data.content) ? data.content : []; + const _textBlock = _blocks.find((b) => b && b.type === 'text' && typeof b.text === 'string') + || _blocks.find((b) => b && typeof b.text === 'string'); + return (_textBlock && _textBlock.text) || ''; } // Retry on 5xx and 429; fail fast on 4xx (auth/quota/bad request — retry won't help). if (r.status >= 500 || r.status === 429) { diff --git a/tools/router-classifier.test.mjs b/tools/router-classifier.test.mjs index 02a8dbb..06fc394 100644 --- a/tools/router-classifier.test.mjs +++ b/tools/router-classifier.test.mjs @@ -654,3 +654,14 @@ describe('buildClassifierPromptStructured — requires task_type field (rasinhro expect(system).toContain('"task_type"'); }); }); + +describe('buildClassifierPromptStructured classifierContext (config-seam §D1)', () => { + const reg = { nodes: [], chains: {} }; + it('дефолт → текущая строка «Лидерра»', () => { + expect(buildClassifierPromptStructured('p', reg).system).toContain('«Лидерра»'); + }); + it('classifierContext инъектируется', () => { + expect(buildClassifierPromptStructured('p', reg, { classifierContext: 'ТестПроект XYZ' }).system) + .toContain('ТестПроект XYZ'); + }); +});