docs: brain-plugin дизайн v6 + план Фазы 1 + handoff сессии

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-15 09:51:17 +03:00
parent 80aa729404
commit 3a422a00c3
3 changed files with 659 additions and 0 deletions
@@ -0,0 +1,372 @@
# Brain Plugin — Фаза 1: Config Seam — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (инлайн под стеной — субагенты стена запрещает, VA-4). Steps — checkbox (`- [ ]`).
**Goal:** Вынести каждое жёсткое «знание про Лидерру» из движка в единый файл-настройку `.claude/brain.local.md`, с безопасными дефолтами (fail-safe §5.1 спека), не меняя текущее поведение в `claude-brain` (обратная совместимость — дефолт = нынешнее лидерровское значение).
**Architecture:** Новый чистый модуль `tools/brain-config.mjs` читает `.claude/brain.local.md` (YAML-frontmatter) и отдаёт типизированные значения с fail-safe-направлениями. Каждый config-seam файл (§3.3 спека) меняется минимально: захардкоженная константа → параметр со значением по умолчанию из `brain-config`. Поведение `claude-brain` не меняется (дефолты = текущие пути/списки). TDD на каждый ключ + на каждое fail-safe-направление.
**Tech Stack:** Node.js ESM (`.mjs`, стиль `tools/`), vitest (`tools/`-свод), YAML-парсинг (минимальный — frontmatter `key: value` + списки).
**Спек:** `docs/superpowers/specs/2026-06-15-brain-as-plugin-design-v5.md` (канон) — §3.3 config-seam, §5 ключи, §5.1 fail-safe, §6 группы.
**Предусловие исполнения под стеной:** этот план перед инлайн-исполнением должен быть (1) опломбирован (`freeze-gate`) с блоком ` ```steps-json``` ` (op/object/ref), опирающимся на (2) опечатанный артефакт-спек. Сами шаги ниже — file-op (`Write`/`Edit`/`Bash`), `Task` запрещён (VA-4). Пломбировка — owner-involved шаг до Этапа 1.
---
## File Structure
- Create: `tools/brain-config.mjs` — чтение/валидация настройки + fail-safe дефолты. Одна ответственность.
- Create: `tools/brain-config.test.mjs` — TDD загрузчика + всех fail-safe-направлений.
- Create: `.claude/brain.local.md` — настройка `claude-brain` как консьюмера (дефолты = текущие лидерровские значения, чтобы поведение не изменилось).
- Modify (по одному, минимально): config-seam файлы §3.3 — `cross-ref-checker.mjs`, `enforce-normative-content-rules.mjs`, `l1-watcher.mjs`, `shell-content-rules.mjs`, `registry-load.mjs` (и потребители), `observer-stop-hook.mjs`/`status-md-generator.mjs`/`cost-stop-hook.mjs` (state_dir), `mcp-tool-classifier.mjs`, `commit-message-scanner.mjs`, `router-classifier.mjs`, `brain-retro-opus-reviewer.mjs`.
---
## Task 1: Config loader `brain-config.mjs`
**Files:** Create `tools/brain-config.mjs`, `tools/brain-config.test.mjs`
- [ ] **Step 1: Failing test — парсинг frontmatter + дефолты**
```javascript
import { describe, it, expect } from 'vitest';
import { parseBrainConfig, resolveConfig } from './brain-config.mjs';
describe('parseBrainConfig', () => {
it('читает YAML-frontmatter ключи', () => {
const md = '---\nconfig_version: 1\nregistry_path: docs/registry/nodes.yaml\nnormative_files:\n - docs/Pravila_raboty_Claude_v1_1.md\n---\nтело';
const c = parseBrainConfig(md);
expect(c.config_version).toBe(1);
expect(c.registry_path).toBe('docs/registry/nodes.yaml');
expect(c.normative_files).toEqual(['docs/Pravila_raboty_Claude_v1_1.md']);
});
it('нет файла/пусто → пустой объект (дефолты применит resolveConfig)', () => {
expect(parseBrainConfig('')).toEqual({});
expect(parseBrainConfig(null)).toEqual({});
});
});
```
- [ ] **Step 2: Run — verify FAIL**
Run: `npx vitest run --root . tools/brain-config.test.mjs`
Expected: FAIL «parseBrainConfig is not a function».
- [ ] **Step 3: Implement `brain-config.mjs`**
```javascript
#!/usr/bin/env node
/** brain-config — единый источник проектной настройки мозга (.claude/brain.local.md).
* Чистый парсер + resolveConfig с fail-safe дефолтами (спек §5.1). */
import fsDefault from 'node:fs';
const FM_RE = /^---\n([\s\S]*?)\n---/;
/** Минимальный YAML-frontmatter: `key: value` + список ` - item`. Без зависимостей. */
export function parseBrainConfig(md) {
const m = FM_RE.exec(String(md || ''));
if (!m) return {};
const out = {}; let curKey = null;
for (const line of m[1].split('\n')) {
const item = /^\s+-\s+(.*)$/.exec(line);
if (item && curKey) { (out[curKey] ||= []).push(item[1].trim()); continue; }
const kv = /^([A-Za-z_]+):\s*(.*)$/.exec(line);
if (!kv) continue;
const [, k, v] = kv;
if (v === '') { out[k] = []; curKey = k; }
else { out[k] = /^\d+$/.test(v) ? Number(v) : v; curKey = null; }
}
return out;
}
```
- [ ] **Step 4: Run — verify PASS**
Run: `npx vitest run --root . tools/brain-config.test.mjs`
Expected: PASS (оба теста parseBrainConfig).
- [ ] **Step 5: Commit**
```bash
git add tools/brain-config.mjs tools/brain-config.test.mjs
git commit -m "feat(brain-config): чистый парсер brain.local.md (Фаза 1 Задача 1)"
```
---
## Task 2: `resolveConfig` + fail-safe направления (спек §5.1)
**Files:** Modify `tools/brain-config.mjs`, `tools/brain-config.test.mjs`
- [ ] **Step 1: Failing test — fail-safe направления**
```javascript
describe('resolveConfig fail-safe (§5.1)', () => {
it('state_dir отсутствует → безопасный дефолт', () => {
expect(resolveConfig({}).state_dir).toBe('.claude/brain-state');
});
it('project_url_whitelist пуст → fail-closed маркер (внешка закрыта)', () => {
const r = resolveConfig({});
expect(r.project_url_whitelist).toEqual([]);
expect(r.project_url_whitelist_failClosed).toBe(true);
});
it('normative_files пуст → [] (legitimate), флаг warnIfGroupOn', () => {
expect(resolveConfig({}).normative_files).toEqual([]);
});
it('classifier_context пуст → generic-строка', () => {
expect(resolveConfig({}).classifier_context).toMatch(/generic|общий/i);
});
});
```
- [ ] **Step 2: Run — verify FAIL**
Run: `npx vitest run --root . tools/brain-config.test.mjs`
Expected: FAIL «resolveConfig is not a function».
- [ ] **Step 3: Implement `resolveConfig`**
```javascript
const DEFAULTS = {
state_dir: '.claude/brain-state',
evidence_archive: 'brain-state',
normative_files: [],
registry_path: '',
project_url_whitelist: [],
classifier_context: 'generic project (no profile configured)',
economy_default: '100',
};
/** Применить дефолты + fail-safe направления (§5.1). Чистая. */
export function resolveConfig(raw) {
const c = { ...DEFAULTS, ...(raw || {}) };
// project_url_whitelist: пусто → fail-CLOSED (внешка закрыта), не «пускать всё».
c.project_url_whitelist_failClosed = !(Array.isArray(c.project_url_whitelist) && c.project_url_whitelist.length > 0);
return c;
}
/** I/O-обёртка: прочитать .claude/brain.local.md проекта (fail-safe: нет файла → дефолты). */
export function loadConfig(root = '.', fsImpl = fsDefault) {
let md = '';
try { md = fsImpl.readFileSync(`${root}/.claude/brain.local.md`, 'utf8'); } catch {}
return resolveConfig(parseBrainConfig(md));
}
```
- [ ] **Step 4: Run — verify PASS**
Run: `npx vitest run --root . tools/brain-config.test.mjs`
Expected: PASS (все fail-safe тесты).
- [ ] **Step 5: Commit**
```bash
git add tools/brain-config.mjs tools/brain-config.test.mjs
git commit -m "feat(brain-config): resolveConfig + fail-safe направления §5.1 (Фаза 1 Задача 2)"
```
---
## Task 3: `.claude/brain.local.md` для claude-brain (дефолты = текущие значения)
**Files:** Create `.claude/brain.local.md`
> NB: запись в `.claude/` гейтится стеной как настройка/секрет (не authoring-канал) — этот шаг под пломбированным планом (op:Write, ref на §5 спека) ИЛИ owner-прогон. Зафиксировать в steps-json при пломбировке.
- [ ] **Step 1: Создать файл-настройку**
```markdown
---
config_version: 1
registry_path: docs/registry/nodes.yaml
state_dir: docs/observer
normative_files:
- docs/Pravila_raboty_Claude_v1_1.md
- docs/Plugin_stack_rules_v1.md
- docs/Tooling_v8_3.md
project_url_whitelist:
- liderra.ru
- github.com/liderra
classifier_context: CRM-проект «Лидерра» (Laravel 13 + Vue 3 + Vuetify 3)
enabled_hook_groups:
- core-discipline
- router-mentor
- normative
---
Настройка мозга для самого `claude-brain` (dogfood-консьюмер). Значения = текущие
лидерровские дефолты, чтобы Фаза 1 не меняла поведение. `state_dir: docs/observer`
сохраняет нынешнее расположение журнала; перенос на `.claude/brain-state` — Фаза 3.
```
- [ ] **Step 2: Verify — загрузчик читает файл**
Run: `node -e "import('./tools/brain-config.mjs').then(m=>console.log(JSON.stringify(m.loadConfig('.'))))"`
Expected: вывод с `registry_path`, `normative_files` (3), `project_url_whitelist` (2), `classifier_context`.
- [ ] **Step 3: Commit**
```bash
git add .claude/brain.local.md
git commit -m "feat(brain-config): brain.local.md для claude-brain — дефолты=текущие (Фаза 1 Задача 3)"
```
---
## Task 4: Вынести `normative_files` (backward-compat)
**Files:** Modify `tools/cross-ref-checker.mjs`, `tools/enforce-normative-content-rules.mjs`, `tools/l1-watcher.mjs`, `tools/shell-content-rules.mjs` + их тесты
Паттерн (на примере `cross-ref-checker.mjs:6-16` — карта `FILES`): захардкоженный список путей квинтета → функция принимает `normativeFiles` параметром, дефолт = текущая константа (backward-compat).
- [ ] **Step 1: Failing test — функция принимает normativeFiles, дефолт = текущий**
```javascript
// tools/cross-ref-checker.test.mjs — добавить
it('принимает normativeFiles из настройки, дефолт = квинтет', () => {
const custom = checkCrossRefs({ normativeFiles: ['docs/Foo.md'] });
expect(custom).toBeDefined(); // работает на произвольном списке
const def = checkCrossRefs({}); // дефолт = текущие 3 файла
expect(def).toBeDefined();
});
```
- [ ] **Step 2: Run — verify FAIL**
Run: `npx vitest run --root . tools/cross-ref-checker.test.mjs`
Expected: FAIL (функция игнорирует `normativeFiles`).
- [ ] **Step 3: Implement — параметр со значением по умолчанию**
В `cross-ref-checker.mjs`: вынести захардкоженную карту в `const DEFAULT_NORMATIVE = {...}` (текущие пути), принять `{ normativeFiles = DEFAULT_NORMATIVE } = {}`. Повторить паттерн для `enforce-normative-content-rules.mjs` (regex 15-16/33/69 → строить из списка), `l1-watcher.mjs:57`, `shell-content-rules.mjs:32-34`. Везде дефолт = текущее значение.
- [ ] **Step 4: Run — verify PASS + регрессия не сломана**
Run: `npx vitest run --root . tools/cross-ref-checker.test.mjs tools/enforce-normative-content-rules.test.mjs tools/shell-content-rules.test.mjs tools/l1-watcher.test.mjs`
Expected: PASS (новые + все существующие — дефолт сохранил поведение).
- [ ] **Step 5: Commit**
```bash
git add tools/cross-ref-checker.mjs tools/enforce-normative-content-rules.mjs tools/l1-watcher.mjs tools/shell-content-rules.mjs tools/*.test.mjs
git commit -m "feat(brain-config): normative_files из настройки, дефолт=квинтет (Фаза 1 Задача 4)"
```
---
## Task 5: Вынести `registry_path`, `state_dir` (backward-compat)
**Files:** Modify `tools/registry-load.mjs` (+ потребители), `tools/observer-stop-hook.mjs`, `tools/status-md-generator.mjs`, `tools/cost-stop-hook.mjs` + тесты
- [ ] **Step 1: Failing test — registry_path/state_dir параметризуемы, дефолт текущий**
```javascript
// registry-load.test.mjs
it('loadRegistry принимает registryPath, дефолт = docs/registry/nodes.yaml', () => {
expect(() => loadRegistry({})).not.toThrow(); // дефолт
});
// cost-stop-hook.test.mjs — fail-safe §5.1
it('runUpdate с битым state_dir → fallback + warn, не тихий no-op', () => {
const r = runUpdate({ episodes: [], dateISO: '2026-06-15', stateDir: '' });
expect(r.warnedFallback).toBe(true);
});
```
- [ ] **Step 2: Run — verify FAIL**
Run: `npx vitest run --root . tools/registry-load.test.mjs tools/cost-stop-hook.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Implement**
`registry-load.mjs:13` уже имеет `DEFAULT_REGISTRY_PATH` — принять `registryPath = DEFAULT_REGISTRY_PATH` параметром, протянуть в потребители. `state_dir`: в `observer-stop-hook`/`status-md-generator`/`cost-stop-hook` заменить захардкоженный `docs/observer` на параметр `stateDir`, дефолт `docs/observer`; при пустом/небитом → fallback `.claude/brain-state` + `warnedFallback:true` (спек §5.1), **не** тихий no-op.
- [ ] **Step 4: Run — verify PASS + регрессия**
Run: `npx vitest run --root . tools/registry-load.test.mjs tools/cost-stop-hook.test.mjs tools/observer-stop-hook.test.mjs tools/status-md-generator.test.mjs`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add tools/registry-load.mjs tools/observer-stop-hook.mjs tools/status-md-generator.mjs tools/cost-stop-hook.mjs tools/*.test.mjs
git commit -m "feat(brain-config): registry_path + state_dir из настройки + fail-safe (Фаза 1 Задача 5)"
```
---
## Task 6: Вынести `project_url_whitelist` (fail-CLOSED) + `classifier_context`
**Files:** Modify `tools/mcp-tool-classifier.mjs`, `tools/commit-message-scanner.mjs`, `tools/router-classifier.mjs`, `tools/brain-retro-opus-reviewer.mjs` + тесты
- [ ] **Step 1: Failing test — whitelist fail-CLOSED, classifier_context инъекция**
```javascript
// mcp-tool-classifier.test.mjs
it('пустой whitelist → внешний URL блокируется (fail-closed §5.1)', () => {
expect(classifyMcpTool('mcp__playwright__browser_navigate',
{ url: 'https://example.com/x' }, { urlWhitelist: [] }).decision).toBe('block');
});
it('whitelist из настройки пускает свой домен', () => {
expect(classifyMcpTool('mcp__playwright__browser_navigate',
{ url: 'https://liderra.ru/x' }, { urlWhitelist: ['liderra.ru'] }).decision).toBe('allow');
});
// router-classifier.test.mjs
it('classifier_context инъектируется в system-prompt', () => {
const sys = buildClassifierSystem({ classifierContext: 'ТестПроект XYZ' });
expect(sys).toMatch(/ТестПроект XYZ/);
});
```
- [ ] **Step 2: Run — verify FAIL**
Run: `npx vitest run --root . tools/mcp-tool-classifier.test.mjs tools/router-classifier.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Implement**
`mcp-tool-classifier.mjs` (63-67/118) + `commit-message-scanner.mjs` (13-15): захардкоженные whitelist'ы → параметр `urlWhitelist`, дефолт `[]`**fail-CLOSED** (пусто → блок внешнего). Для backward-compat в `claude-brain` whitelist приходит из `brain.local.md` (`liderra.ru`, `github.com/liderra`). `router-classifier.mjs` (296/228) + `brain-retro-opus-reviewer.mjs` (69): вынести проектную строку в параметр `classifierContext`, дефолт = generic; в `claude-brain` приходит из настройки.
- [ ] **Step 4: Run — verify PASS + регрессия**
Run: `npx vitest run --root . tools/mcp-tool-classifier.test.mjs tools/commit-message-scanner.test.mjs tools/router-classifier.test.mjs tools/brain-retro-opus-reviewer.test.mjs`
Expected: PASS (внимание: существующие тесты, ждущие `liderra.ru`, теперь передают whitelist явно).
- [ ] **Step 5: Commit**
```bash
git add tools/mcp-tool-classifier.mjs tools/commit-message-scanner.mjs tools/router-classifier.mjs tools/brain-retro-opus-reviewer.mjs tools/*.test.mjs
git commit -m "feat(brain-config): project_url_whitelist (fail-closed) + classifier_context (Фаза 1 Задача 6)"
```
---
## Task 7: Полный регресс + wiring загрузчика в хуки
**Files:** Modify зарегистрированные хуки-обёртки (читают `loadConfig` в `main()`), full vitest
- [ ] **Step 1: Прокинуть `loadConfig()` в `main()` хуков**
В `main()` каждого затронутого хука (`enforce-normative-content-rules`, `enforce-mcp-classification`, `observer-stop-hook`, `cost-stop-hook`, `cross-ref-checker` CLI, `router-classifier`): вызвать `loadConfig(repoRoot)` и передать значения в чистые функции. Чистые функции не знают про I/O — настройку читает обёртка.
- [ ] **Step 2: Полный регресс tools/-свода**
Run: `npx vitest run --root . tools/`
Expected: PASS весь свод (~3931+ тестов; backward-compat дефолты сохранили поведение `claude-brain`).
- [ ] **Step 3: Commit**
```bash
git add tools/*.mjs
git commit -m "feat(brain-config): wiring loadConfig в хуки + полный регресс (Фаза 1 Задача 7)"
```
---
## Self-Review (выполнено при написании)
- **Покрытие спека:** Задачи 4–6 закрывают все 5 ключей §3.3 (+3 новых); Задача 2 закрывает fail-safe §5.1 (state_dir fallback, project_url_whitelist fail-closed); Задача 1/3 — загрузчик + настройка консьюмера; Задача 7 — wiring + регресс. §6 группы (`enabled_hook_groups`) читаются в Фазе 2 (упаковка) — здесь только хранятся в настройке.
- **Заглушки:** код-шаги несут реальный код (парсер, resolveConfig, тесты); extraction-шаги дают паттерн + точные файлы:строки из §3.3 спека. Команды vitest — точные (через рабочий `npx vitest run --root .`).
- **Согласованность:** `parseBrainConfig`/`resolveConfig`/`loadConfig` — единые имена через все задачи; `urlWhitelist`/`classifierContext`/`stateDir`/`registryPath`/`normativeFiles` — единые имена параметров; дефолт всюду = текущее лидерровское значение (backward-compat инвариант).
- **Стена:** план помечен под пломбировку (steps-json + sealed артефакт-спек, Task запрещён); шаг записи в `.claude/brain.local.md` — под планом/owner (гейтится как настройка).