docs: brain-plugin дизайн v6 + план Фазы 1 + handoff сессии
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (гейтится как настройка).
|
||||
Reference in New Issue
Block a user