12 KiB
Фаза 1 config-seam — state_dir резолвер + classifier_context параметр — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: superpowers:executing-plans (инлайн под стеной — субагенты запрещены, VA-4). Steps — checkbox (
- [ ]).
Goal: Добавить чистый fail-safe резолвер resolveStateDir в brain-config и параметр classifierContext в два prompt-builder'а (router-classifier, brain-retro-opus-reviewer), не меняя поведение claude-brain (backward-compat: дефолт = текущая строка/каталог).
Architecture: Чистые pure-seam'ы — новые параметры со значением по умолчанию / новая чистая функция. Ни один файл не gated (не discipline-source / не normative). Подключение в main() — отдельная задача (wiring).
Tech Stack: Node.js ESM (tools/), vitest (vitest.config.tools.mjs).
Спек: docs/superpowers/specs/2026-06-15-task56-statedir-classifiercontext-spec-v2.md (§D1 контракт, §D2 fail-safe state_dir, §D3 крайние случаи + критерий).
Цель
Закрыть два чистых config-seam ключа Фазы 1 (state_dir резолвер + classifier_context) на уровне pure-функций, backward-compat (дефолт = текущее значение). Wiring и project_url_whitelist — отдельно.
Переговоры
Позиция контроллера по типовым замечаниям ревью к правке существующих файлов:
- Edit, не Write-overwrite. Все три файла прочитаны в этой сессии;
old_stringкаждого Edit — байт-точная подстрока текущего состояния. Правки аддитивны (новый параметр со значением по умолчанию / новый экспорт) — существующие сигнатуры/вызовы с одним аргументом не ломаются. - Бэкап — git. Файлы под версионным контролем; откат —
git restore/git show HEAD:<путь>. - Backward-compat доказывается тестами. Каждая правка: RED→GREEN + кейс «дефолт = текущая строка байт-в-байт» + полный регресс. Авторитетный полный свод — в терминале владельца.
- fail-safe направление.
resolveStateDirпри пустом/невалидном входе возвращает безопасный дефолт +warnedFallback: true(не тихий no-op) — §5.1.
["test-driven-development"]
[
{"op":"Edit","object":"tools/brain-config.test.mjs","ref":"D1"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/brain-config.mjs","ref":"D1"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/router-classifier.test.mjs","ref":"D2"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/router-classifier.mjs","ref":"D2"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/brain-retro-opus-reviewer.test.mjs","ref":"D3"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Edit","object":"tools/brain-retro-opus-reviewer.mjs","ref":"D3"},
{"op":"Edit","object":"tools/brain-retro-opus-reviewer.mjs","ref":"D3"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"},
{"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs","ref":"D3"}
]
[
{"id":"D1","kind":"EXTRACTED","ref":"tools/brain-config.mjs","anchor":"const DEFAULTS = Object.freeze({"},
{"id":"D2","kind":"EXTRACTED","ref":"tools/router-classifier.mjs","anchor":"export function buildClassifierPromptStructured"},
{"id":"D3","kind":"EXTRACTED","ref":"tools/brain-retro-opus-reviewer.mjs","anchor":"export function buildReviewPromptStructured"}
]
Task A: resolveStateDir в brain-config (fail-safe §5.1)
Files: Modify tools/brain-config.mjs, tools/brain-config.test.mjs
- Step 1 (Edit test, RED): в
tools/brain-config.test.mjsдобавить в конец (отдельный import + describe):
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 });
});
});
-
Step 2 (Bash, verify FAIL):
npx vitest run --config vitest.config.tools.mjsExpected: новый тест FAIL (resolveStateDir is not a function). Авторитетно — терминал владельца. -
Step 3 (Edit impl): в
tools/brain-config.mjsдобавить функцию в конец (послеloadConfig;old_string= последние строкиloadConfig—return resolveConfig(parseBrainConfig(md));\n}):
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 };
}
- Step 4 (Bash, verify PASS):
npx vitest run --config vitest.config.tools.mjsExpected: PASS (новые + существующие).
Task B: classifierContext в router-classifier
Files: Modify tools/router-classifier.mjs, tools/router-classifier.test.mjs
- Step 5 (Edit test, RED): в
tools/router-classifier.test.mjsдобавить describe (импортbuildClassifierPromptStructuredуже есть в файле — иначе добавитьimport { buildClassifierPromptStructured } from './router-classifier.mjs';рядом):
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');
});
});
-
Step 6 (Bash, verify FAIL):
npx vitest run --config vitest.config.tools.mjsExpected: FAIL (инъекция-кейс — параметр игнорируется). -
Step 7 (Edit impl): в
tools/router-classifier.mjsзаменить началоbuildClassifierPromptStructured(old_string= строки от сигнатуры до строкиconst system =Ты классификатор задач для CRM-проекта «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3).`):
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 = `Ты классификатор задач для ${classifierContext}.
- Step 8 (Bash, verify PASS):
npx vitest run --config vitest.config.tools.mjsExpected: PASS (дефолт = байт-в-байт текущая строка;buildClassifierPromptвызывает безclassifierContext→ дефолт).
Task C: classifierContext в brain-retro-opus-reviewer
Files: Modify tools/brain-retro-opus-reviewer.mjs, tools/brain-retro-opus-reviewer.test.mjs
- Step 9 (Edit test, RED): в
tools/brain-retro-opus-reviewer.test.mjsдобавить describe (импортbuildReviewPromptStructuredуже есть — иначе добавить):
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');
});
});
-
Step 10 (Bash, verify FAIL):
npx vitest run --config vitest.config.tools.mjsExpected: FAIL (инъекция-кейс). -
Step 11 (Edit impl — сигнатура): в
tools/brain-retro-opus-reviewer.mjsзаменить строку сигнатуры (old_string=export function buildReviewPromptStructured(episode) {):
export function buildReviewPromptStructured(episode, { classifierContext = 'Лидерра' } = {}) {
- Step 12 (Edit impl — строка system): заменить захардкоженную строку (
old_string='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.`,
- Step 13 (Bash, verify PASS):
npx vitest run --config vitest.config.tools.mjsExpected: PASS (дефолтЛидерра= байт-в-байт).
Task D: финальный регресс
- Step 14 (Bash, полный регресс):
npx vitest run --config vitest.config.tools.mjsExpected: PASS весь свод. Авторитетный прогон — в терминале владельца.
Self-Review
- Покрытие спека: §D1 — Task A (resolveStateDir) + B (classifierContext router) + C (classifierContext reviewer); §D2 fail-safe — тесты resolveStateDir (пусто/не-строка → fallback+warned); §D3 крайние случаи — дефолт байт-в-байт + инъекция + null/undefined; критерий — Step 14.
- Заглушек нет: каждый impl-шаг несёт полный код; тесты полные; команды vitest точные.
- Согласованность имён:
resolveStateDir/classifierContextединые; дефолты = точная копия текущих строк (backward-compat инвариант). - Стена: brain-config / router-classifier / brain-retro-opus-reviewer — НЕ discipline-source и НЕ normative → нормативный гейт не engage; правки проходят как обычные шаги запечатанного плана (стена М2 + content-floor + TDD-gate govern). Bash floor-safe (
npx vitest). MultiEdit не используется (brain-retro — два отдельных Edit-шага 11/12). project_url_whitelist + wiring — вне scope.