Files
brain/docs/superpowers/plans/2026-06-15-task56-statedir-classifiercontext-plan-v3.md
T

15 KiB
Raw Blame History

Фаза 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 — отдельно.

Переговоры

Позиция контроллера по типовым замечаниям ревью:

  1. Каждый мутирующий шаг проверяем (DR-1). Правка brain-retro-opus-reviewer.mjs — ОДИН Edit (сигнатура функции + строка system одним блоком), сразу за ним Bash-проверка. Двух Edit подряд без Bash между нет.
  2. Нет дублирующих шагов. Финальный Bash служит и GREEN-проверкой Task C, и полным регрессом — отдельного повторного прогона нет.
  3. Edit, не Write-overwrite. Все три файла прочитаны в этой сессии; old_string каждого Edit — байт-точная подстрока текущего состояния. Правки аддитивны (новый параметр со значением по умолчанию / новый экспорт) — существующие сигнатуры/вызовы с одним аргументом не ломаются.
  4. Бэкап — git. Откат — git restore / git show HEAD:<путь>.
  5. Backward-compat + fail-safe доказываются тестами. Каждая правка: RED→GREEN + кейс «дефолт = текущая строка байт-в-байт»; resolveStateDir при пустом/невалидном входе → безопасный дефолт + warnedFallback (§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":"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.mjs Expected: новый тест FAIL (resolveStateDir is not a function). Авторитетно — терминал владельца.

  • Step 3 (Edit impl): в tools/brain-config.mjs добавить функцию в конец (после loadConfig; old_string = последние строки loadConfigreturn 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.mjs Expected: 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.mjs Expected: 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.mjs Expected: PASS (дефолт = байт-в-байт текущая строка; buildClassifierPrompt вызывает без classifierContext → дефолт).

Task C: classifierContext в brain-retro-opus-reviewer (ОДИН Edit — сигнатура + строка system)

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.mjs Expected: FAIL (инъекция-кейс).

  • Step 11 (Edit impl — ОДИН Edit, сигнатура + строка system): в tools/brain-retro-opus-reviewer.mjs заменить блок от сигнатуры функции до первой строки массива system (DR-1: единый Edit, без двух правок подряд). old_string (текущий блок):

export function buildReviewPromptStructured(episode) {
  const v = Number(episode?.schema_version) || 0;
  const cues = [
    'node_quality: correct | wrong_node | overkill | underkill | disputable',
    'chain_quality: correct | missing_step | extra_step | wrong_order | n/a',
    'gap_assessment: acceptable | mistake_should_complete | mistake_should_not_start | n/a',
    'agent_self_assessment_accuracy: accurate | over_confident | under_confident | no_self_assessment',
    'error_root_cause: wrong_skill | wrong_tool | wrong_chain_order | external_failure | n/a',
    'alternative_better: <node_id> | null',
    'outcome_reviewed: success | soft_success | rework | blocked',
    'reasoning: 1-3 sentences',
  ];

  const adaptiveNotes = [];
  if (v >= 3) {
    adaptiveNotes.push('Episode is v3+: primary_rationale carries triggers/candidates/boundaries.');
  }
  if (v >= 4) {
    adaptiveNotes.push('Episode is v4: classifier_output.alternatives_considered tells you what the classifier weighed.');
    adaptiveNotes.push('self_assessment (if present and not pending) is the agent\'s post-hoc judgement — compare honesty.');
    adaptiveNotes.push('execution_trace.chain_gaps shows whether the recommended chain ran in full.');
  }

  const system = [
    'You are the independent reviewer of routing decisions for the Лидерра brain-governance experiment.',

new_string (сигнатура +param, строка system — шаблон; всё между байт-в-байт):

export function buildReviewPromptStructured(episode, { classifierContext = 'Лидерра' } = {}) {
  const v = Number(episode?.schema_version) || 0;
  const cues = [
    'node_quality: correct | wrong_node | overkill | underkill | disputable',
    'chain_quality: correct | missing_step | extra_step | wrong_order | n/a',
    'gap_assessment: acceptable | mistake_should_complete | mistake_should_not_start | n/a',
    'agent_self_assessment_accuracy: accurate | over_confident | under_confident | no_self_assessment',
    'error_root_cause: wrong_skill | wrong_tool | wrong_chain_order | external_failure | n/a',
    'alternative_better: <node_id> | null',
    'outcome_reviewed: success | soft_success | rework | blocked',
    'reasoning: 1-3 sentences',
  ];

  const adaptiveNotes = [];
  if (v >= 3) {
    adaptiveNotes.push('Episode is v3+: primary_rationale carries triggers/candidates/boundaries.');
  }
  if (v >= 4) {
    adaptiveNotes.push('Episode is v4: classifier_output.alternatives_considered tells you what the classifier weighed.');
    adaptiveNotes.push('self_assessment (if present and not pending) is the agent\'s post-hoc judgement — compare honesty.');
    adaptiveNotes.push('execution_trace.chain_gaps shows whether the recommended chain ran in full.');
  }

  const system = [
    `You are the independent reviewer of routing decisions for the ${classifierContext} brain-governance experiment.`,
  • Step 12 (Bash, verify PASS + полный регресс): npx vitest run --config vitest.config.tools.mjs Expected: PASS весь свод (дефолт Лидерра = байт-в-байт; backward-compat сохранён). Авторитетный прогон — в терминале владельца.

Self-Review

  • Покрытие спека: §D1 — Task A (resolveStateDir) + B (classifierContext router) + C (classifierContext reviewer); §D2 fail-safe — тесты resolveStateDir (пусто/не-строка → fallback+warned); §D3 крайние случаи — дефолт байт-в-байт + инъекция + null/undefined; критерий — Step 12.
  • DR-1: каждый мутирующий шаг сопровождается Bash-проверкой; brain-retro — ОДИН Edit (сигнатура+system вместе), без двух правок подряд; дублирующего финального прогона нет.
  • Заглушек нет: каждый impl-шаг несёт полный код; тесты полные; команды vitest точные.
  • Согласованность имён: resolveStateDir / classifierContext единые; дефолты = точная копия текущих строк (backward-compat инвариант).
  • Стена: brain-config / router-classifier / brain-retro-opus-reviewer — НЕ discipline-source и НЕ normative → нормативный гейт не engage; правки проходят как обычные шаги запечатанного плана. Bash floor-safe. project_url_whitelist + wiring — вне scope.