docs(brain-config): дизайн+план normative_files-модели + handoff-5

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-16 04:25:02 +03:00
parent 60dc4d8264
commit 03a1f2c995
3 changed files with 356 additions and 0 deletions
@@ -0,0 +1,188 @@
# normative_files config-model + cross-ref/l1 wiring — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (под стеной — escape-per-step,
> наставник H4 печать не ставит). Steps — checkbox (`- [ ]`).
**Goal:** Развести `normative_files` (проектные доки) и универсальные CLAUDE.md/MEMORY.md; прокинуть
cross-ref (config universal) и l1 (`tool_registry_path`) в config, сохранив поведение claude-brain.
**Architecture:** Спека `2026-06-15-normative-files-config-model-design.md`. cross-ref строит набор
version-tracked = config `normative_files` встроенные `{CLAUDE.md, MEMORY.md}`; l1 берёт один
`tool_registry_path` (fail-safe skip при отсутствии). Дефолты = текущие → backward-compat байт-в-байт.
**Tech Stack:** Node ESM (.mjs), vitest, brain-config (loadConfig).
---
## File Structure
- Modify: `tools/brain-config.mjs` — добавить дефолт `tool_registry_path`.
- Modify: `.claude/brain.local.md` — добавить `tool_registry_path: docs/Tooling_v8_3.md`.
- Modify: `tools/cross-ref-checker.mjs` (+ `.test.mjs`) — `UNIVERSAL_VERSION_TRACKED` + `buildNormativeMap` + `loadFiles` param + CLI wiring.
- Modify: `tools/l1-watcher.mjs` (+ `.test.mjs`) — `loadInputs` param `toolRegistryPath` + `toolingPresent` skip + CLI wiring.
---
## Task 1: brain-config дефолт `tool_registry_path`
- [ ] **Step 1:** В `tools/brain-config.mjs`, в `DEFAULTS` (Object.freeze) добавить строку после `protected_paths: []`:
```javascript
tool_registry_path: 'docs/Tooling_v8_3.md',
```
- [ ] **Step 2 (verify):** Владелец: `npx vitest run --config vitest.config.tools.mjs tools/brain-config.test.mjs` — все зелёные (новый дефолт не ломает; при желании добавить assert `resolveConfig({}).tool_registry_path === 'docs/Tooling_v8_3.md'`).
---
## Task 2: cross-ref-checker — config universal
**Files:** `tools/cross-ref-checker.mjs`, `tools/cross-ref-checker.test.mjs`
- [ ] **Step 1 (RED test):** В `cross-ref-checker.test.mjs` добавить:
```javascript
import { buildNormativeMap } from './cross-ref-checker.mjs';
describe('buildNormativeMap (Task 7 follow-up)', () => {
it('config-список встроенные CLAUDE/MEMORY; дефолт claude-brain = 5', () => {
const m = buildNormativeMap(['docs/Pravila_raboty_Claude_v1_1.md', 'docs/Plugin_stack_rules_v1.md', 'docs/Tooling_v8_3.md']);
expect(m.Pravila).toBe('docs/Pravila_raboty_Claude_v1_1.md');
expect(m.PSR_v1).toBe('docs/Plugin_stack_rules_v1.md');
expect(m.Tooling).toBe('docs/Tooling_v8_3.md');
expect(m.CLAUDE).toBe('CLAUDE.md'); // universal built-in
expect(m.MEMORY).toBe('MEMORY.md'); // universal built-in
});
it('пустой список → только universal', () => {
expect(buildNormativeMap([])).toEqual({ CLAUDE: 'CLAUDE.md', MEMORY: 'MEMORY.md' });
});
});
```
- [ ] **Step 2:** Run RED — `npx vitest run --config vitest.config.tools.mjs tools/cross-ref-checker.test.mjs` → FAIL «buildNormativeMap is not a function».
- [ ] **Step 3 (impl):** В `cross-ref-checker.mjs` после `PATH_TO_NAME` добавить:
```javascript
// Универсальные version-tracked доки (есть у любого проекта; не в настройке).
const UNIVERSAL_VERSION_TRACKED = Object.freeze({ CLAUDE: 'CLAUDE.md', MEMORY: 'MEMORY.md' });
// Собрать {name:path} из проектного списка (config) universal. Имя — из PATH_TO_NAME,
// иначе basename без .md (greenfield; regex-имена — отдельный заход, см. дизайн §4.1/§7).
export function buildNormativeMap(normativeFilesList = []) {
const map = {};
for (const p of (Array.isArray(normativeFilesList) ? normativeFilesList : [])) {
const name = PATH_TO_NAME[p] || String(p).split('/').pop().replace(/\.md$/, '');
map[name] = p;
}
return { ...map, ...UNIVERSAL_VERSION_TRACKED };
}
```
- [ ] **Step 4:** Run GREEN — тот же прогон → PASS (buildNormativeMap).
- [ ] **Step 5 (loadFiles param):** В `cross-ref-checker.mjs` `loadFiles` принять карту параметром:
```javascript
function loadFiles(root = process.cwd(), normativeMap = NORMATIVE_FILES) {
const out = {};
for (const [, path] of Object.entries(normativeMap)) {
const abs = join(root, path);
if (existsSync(abs)) out[path] = readFileSync(abs, 'utf-8');
}
return out;
}
```
- [ ] **Step 6 (CLI wiring):** В CLI-блоке (`if (process.argv...)`) заменить начало:
```javascript
let normativeMap = NORMATIVE_FILES;
try {
const { loadConfig } = await import('./brain-config.mjs');
normativeMap = buildNormativeMap(loadConfig().normative_files);
} catch { /* brain-config недоступен → дефолт NORMATIVE_FILES */ }
const files = loadFiles(process.cwd(), normativeMap);
const m = detectMismatches(files, { normativeFiles: normativeMap });
```
(top-level await допустим в CLI-блоке ESM; backward-compat: config-3 universal = те же 5.)
- [ ] **Step 7 (verify):** Владелец: `npx vitest run --config vitest.config.tools.mjs tools/cross-ref-checker.test.mjs` → PASS (новые + 20 прежних).
---
## Task 3: l1-watcher — `tool_registry_path` + skip
**Files:** `tools/l1-watcher.mjs`, `tools/l1-watcher.test.mjs`
- [ ] **Step 1 (RED test):** В `l1-watcher.test.mjs` добавить:
```javascript
it('loadInputs: tool_registry_path отсутствует → toolingPresent:false (skip-сигнал)', () => {
const r = loadInputs(process.cwd(), '');
expect(r.toolingPresent).toBe(false);
});
```
(нужен `import { loadInputs } from './l1-watcher.mjs'` — добавить, если нет.)
- [ ] **Step 2:** Run RED → FAIL (`toolingPresent` undefined).
- [ ] **Step 3 (impl):** В `l1-watcher.mjs` `loadInputs`:
```javascript
export function loadInputs(projectRoot = process.cwd(), toolRegistryPath = 'docs/Tooling_v8_3.md') {
const userSettings = JSON.parse(loadFileMaybe(join(homedir(), '.claude', 'settings.json')) || '{}');
const projectSettings = JSON.parse(loadFileMaybe(join(projectRoot, '.claude', 'settings.json')) || '{}');
const merged = {
enabledPlugins: { ...(userSettings.enabledPlugins || {}), ...(projectSettings.enabledPlugins || {}) },
};
const toolingRaw = toolRegistryPath ? loadFileMaybe(join(projectRoot, toolRegistryPath)) : null;
const tooling = toolingRaw || '';
const toolingPresent = toolingRaw !== null;
const aliasesRaw = loadFileMaybe(join(projectRoot, 'tools', '.l1-watcher-aliases.txt'));
const aliases = parseAliases(aliasesRaw);
return { settings: merged, tooling, aliases, toolingPresent };
}
```
- [ ] **Step 4:** Run GREEN.
- [ ] **Step 5 (CLI wiring + skip):** В CLI-блоке заменить:
```javascript
let toolRegistryPath = 'docs/Tooling_v8_3.md';
try {
const { loadConfig } = await import('./brain-config.mjs');
toolRegistryPath = loadConfig().tool_registry_path;
} catch { /* дефолт */ }
const { settings, tooling, aliases, toolingPresent } = loadInputs(process.cwd(), toolRegistryPath);
if (!toolingPresent) {
console.log('[l1-watcher] OK — справочник инструментов не задан/не найден (skip)');
process.exit(0);
}
const drift = detectDrift(settings, tooling, aliases);
```
- [ ] **Step 6 (verify):** Владелец: `npx vitest run --config vitest.config.tools.mjs tools/l1-watcher.test.mjs` → PASS (новый + 12 прежних).
---
## Task 4: brain.local.md — `tool_registry_path`
- [ ] **Step 1:** В `.claude/brain.local.md`, во frontmatter после `state_dir: docs/observer` добавить:
```yaml
tool_registry_path: docs/Tooling_v8_3.md
```
- [ ] **Step 2 (verify):** Владелец: полный свод `npx vitest run --config vitest.config.tools.mjs` — мои файлы GREEN; 3 пре-существующих deepseek-провала — вне scope.
---
## Self-Review
- **Spec coverage:** §3 модель (Task 1 ключ дефолт + Task 4 brain.local.md); §4.1 cross-ref (Task 2 buildNormativeMap universal + loadFiles + CLI); §4.2 l1 (Task 3 toolRegistryPath + skip); §5 fail-safe (Task 2 пустой→universal, Task 3 absent→skip). Граница §4.1 (regex-имена дефолтные) — соблюдена (не трогаем CROSS_REF_RE). §7 follow-up — вне scope.
- **Placeholders:** нет; весь код реальный.
- **Consistency:** `buildNormativeMap`/`UNIVERSAL_VERSION_TRACKED`/`toolingPresent`/`tool_registry_path` — единые имена.
- **Стена:** исполнение escape-per-step (печать H4 не встаёт); каждый Edit/Write — owner escape; полный свод + коммит — терминал владельца.
@@ -0,0 +1,90 @@
# Brain-as-plugin — handoff сессии №5 (закрытие 2026-06-15)
**Кодовая фраза стены:** «роутер-наставник». **Канон дизайна:** `2026-06-15-brain-as-plugin-design-v6.md`.
**План Фазы 1:** `2026-06-15-brain-plugin-phase1-config-seam.md`. Предыдущие: handoff-4/3/2/handoff.
Эта сессия — **Task 7 (финальный wiring loadConfig в хуки)**: исполнено по живым хукам + status-md;
остаток (CLI normative_files + router classifier_context) вынесен в follow-up из-за design-вопроса.
---
## 1. Что сделано
- **Батч A-C (живые хуки) — DONE, коммит `165ff3a`** (8 files, в терминале владельца):
- `cost-stop-hook.mjs``currentMonthFile(now, repoRoot, stateDir='docs/observer')` + `main()` читает
`resolveStateDir(loadConfig(repoRoot).state_dir)` через динамический import + try/catch fallback.
- `observer-stop-hook.mjs``appendEpisode`/`bumpPiiCounter` получили `stateDir`; CLI основной +
error-marker пути читают config; ноль хардкодов `docs/observer` кроме дефолтов.
- `enforce-mcp-classification.mjs``decide({urlWhitelist})``classifyMcpTool`; `main()` читает
`project_url_whitelist` (fail-CLOSED при сбое импорта).
- `enforce-normative-content-rules.mjs``main()` прокидывает `protected_paths` в `isNormativePath`
(база всегда защищена; augment=[] → поведение не меняется).
- `.claude/brain.local.md` — настройка консьюмера (значения = текущие лидерровские; `state_dir: docs/observer`).
- Тесты: cost-stop + observer + mcp (config-seam). **Авторитетный свод: 3981 passed** (мои файлы GREEN).
- **status-md-generator.mjs — DONE (коммит dcc14f83)**: module-level `STATE_DIR` (дефолт `docs/observer`),
CLI читает `state_dir` из config (top-level await import); все 8 хардкодов `docs/observer``STATE_DIR`.
---
## 2. УРОКИ ПРО СТЕНУ (НОВОЕ — для GUIDE)
1. **escape ≠ печать — ПОДТВЕРЖДЕНО кодом.** `enforce-judge-gate.sealOnWiredGo` (стр. 337) печатает
ТОЛЬКО при `verdict.wired===true && decision==='GO'`. Escape-ветка (316-325) возвращает `block:false`,
но `sealOnWiredGo` НЕ зовёт → печати нет. Значит escape на запись плана разблокирует судью, но режим
исполнения не открывает. Печать плана дополнительно требует валидный `mentor-GO` (freeze-gate 350-353).
2. **Баг наставника H4 — структурный, не сходится.** Наставник (`onPlanWrite`) видит только `steps-json`
(`{op,object,ref}`), НЕ тело плана/шаблоны/код. Поэтому требует «покажи шаблон/код» (формат не несёт) и
«создай заглушки для отсутствующих модулей» — даже когда модули СУЩЕСТВУЮТ (выполнить = перезаписать
рабочее). 4 круга L1-арбитража, требования растут. Раздел `## Переговоры` парсится
(`parseNegotiationSection`), но вердикт наставника от него не меняется. Лечение — **owner escape-per-step**.
3. **Escape-per-step — рабочий канал исполнения без печати.** Супрем-гейт (М2) чтит escape-грант в
разговорном режиме: на КАЖДЫЙ Edit/Write/Bash — отдельный одноразовый грант (`canonicalAction`,
окно 5 мин, гасится `floor-escape-consume`). Метка `FLOOR-ESCAPE: write:<путь>` / `bash:<команда>` в
опции AskUserQuestion → клик владельца подписывает. `replace_all`-батчинг сокращает число грантов.
4. **Вердикты читаются Grep'ом, не Read.** `~/.claude/runtime/{judge-verdicts,seal-attempts}.jsonl`
hard-deny на Read (`enforce-read-path-deny §3.1`), но Grep их видит. `seal-attempts.jsonl` несёт
`wired/decision/sealed/reason/at` на каждую попытку — точная диагностика GO / NO-GO / degraded.
5. **deepseek-v4-pro миграция (НЕ моя, пре-существующая).** `tools/router-config.mjs` изменён
(`CLASSIFIER_MODEL='deepseek-v4-pro'`, `HEAVY_LLM_TIMEOUT_MS=300000`). 3 теста свода красные
(`router-config.test` ждёт sonnet; `enforce-mentor-on-plan-write` ждёт timeout 90000;
`enforce-judge-gate` parse). Это незавершённая миграция модели — обновить тесты под deepseek (follow-up).
---
## 3. Остаток Task 7 — FOLLOW-UP (design-вопрос)
| Потребитель | Ключ | Почему follow-up |
|---|---|---|
| `cross-ref-checker` | normative_files | Нужен map из **5** version-tracked файлов (+CLAUDE.md +MEMORY.md); config = плоский список 3 → прокинуть наивно = регрессия (перестанет проверять CLAUDE/MEMORY). |
| `l1-watcher` | normative_files | Нужен **один** Tooling-путь, не список. |
| `registry-render` / `observer-transcript-parser` / `shell-content-rules` | normative_files | Свои нужды; менять список влияет на всех. |
| `router-classifier` / `brain-retro-opus-reviewer` | classifier_context | В пути стены (mentor/judge зовут classify); дефолт уже Лидерра (backward-compat сохранён). Правка callers рискованна. |
**Design-вопрос:** ключ `normative_files` смоделирован плоским списком, но потребители ждут РАЗНЫЕ
формы/подмножества. Нужно решение модели: (а) расширить config (отдельные ключи tooling_path /
version_tracked_files), или (б) оставить эти CLI на своих дефолтах (claude-brain-контекст), или
(в) per-consumer view над одним списком. **НЕ решать наугад** (правило #1/#6).
Плюс: обновить 3 теста под deepseek-v4-pro миграцию (см. §2 п.5).
---
## 4. Скилл-цепочка (как раньше)
using-superpowers → writing-plans (спека+план) → executing-plans ИНЛАЙН + test-driven-development →
escape для памяти/правок → systematic-debugging для трения судьи → verification-before-completion.
В этой сессии печать не использовалась (наставник H4) — исполнение через owner escape-per-step.
---
## 5. Состояние config-seam Фазы 1
| Ключ | Live-хуки | CLI |
|---|---|---|
| `state_dir` | ✅ cost-stop, observer-stop | ✅ status-md-generator |
| `project_url_whitelist` | ✅ enforce-mcp-classification | (commit-scanner — мёртв в claude-brain) |
| `protected_paths` | ✅ enforce-normative-content-rules | — |
| `normative_files` | (norm-content использует protected_paths) | ⏳ cross-ref, l1 (follow-up §3) |
| `classifier_context` | ⏳ router-classifier (путь стены, follow-up) | ⏳ brain-retro-opus-reviewer |
| `registry_path` | уже параметр (loadRegistry) | wiring потребителей — follow-up |
**Коммиты сессии:** `165ff3a` (батч A-C) + status-md (dcc14f83). Оба в терминале владельца.
@@ -0,0 +1,78 @@
# Дизайн: модель `normative_files` + wiring cross-ref/l1 (Task 7 follow-up)
**Дата:** 2026-06-15
**Статус:** Approved (brainstorming, владелец одобрил)
**Контекст:** Task 7 (config-seam Фазы 1) закрыл живые хуки + status-md (`165ff3a` + `dcc14f83`).
Остаток — CLI-потребители `normative_files`, которые вскрыли смысловую тонкость (handoff №5 §3).
---
## 1. Проблема
Ключ `normative_files` в `.claude/brain.local.md` смоделирован плоским списком (3 пути:
Pravila/PSR/Tooling), но потребители ждут РАЗНОЕ:
| Потребитель | Нужный набор |
|---|---|
| `cross-ref-checker` | 5 version-tracked файлов (3 ядра + `CLAUDE.md` + `MEMORY.md`) — сверка версий |
| `l1-watcher` | ОДИН файл-справочник инструментов (`docs/Tooling_v8_3.md`) |
| `shell-content-rules` / `observer-transcript-parser` | 3 ядра, по regex-именам (greenfield-hardening — отложено) |
Наивно прокинуть config-список (3) в cross-ref → регрессия (перестанет сверять CLAUDE/MEMORY).
## 2. Корневое решение: два рода документов
- **Проектные** — Pravila/PSR/Tooling: у каждого проекта свои → живут в настройке (`normative_files`).
- **Универсальные** — `CLAUDE.md`/`MEMORY.md`: есть у ЛЮБОГО проекта, имена фиксированы → **встроены
в проверки**, НЕ в настройке.
## 3. Модель настройки
- `normative_files` — список проектных нормативных доков. Дефолт claude-brain = 3 (уже есть).
- `tool_registry_path`**новый ключ**, один путь к справочнику инструментов. Дефолт `docs/Tooling_v8_3.md`.
## 4. Компоненты
### 4.1. cross-ref-checker
- Константа `UNIVERSAL_VERSION_TRACKED = ['CLAUDE.md', 'MEMORY.md']` (встроена).
- `loadFiles(root, normativeFiles)` и CLI строят набор = `normative_files` (config) **** universal.
- `detectMismatches` уже принимает `normativeFiles` (config-шов с Task 4) — CLI передаёт собранный набор.
- Для claude-brain: 3 2 = те же 5 → **поведение байт-в-байт** (backward-compat инвариант).
- **Граница (явная):** `CROSS_REF_RE`/`PATH_TO_NAME` (имена для матча cross-ref'ов в тексте) пока
остаются дефолтными (5 известных имён). cross-ref становится config-driven по *путям-файлам*;
генерация regex-имён из config (для greenfield с другими именами) — отложенный заход (§7).
### 4.2. l1-watcher
- `loadInputs(projectRoot, toolRegistryPath)` читает `tool_registry_path` (дефолт Tooling).
- **Fail-safe:** путь пуст / файла нет → l1 **пропускает** (возвращает «нет дрейфа»), НЕ флагует все
включённые плагины как «отсутствующие в справочнике». Иначе greenfield без справочника ложно падал бы.
### 4.3. brain.local.md
- Добавить `tool_registry_path: docs/Tooling_v8_3.md`. `normative_files` (3) — без изменений.
## 5. Fail-safe (спек §5.1 стиль)
| Ключ | При отсутствии/пустоте | Направление |
|---|---|---|
| `normative_files` | пусто → cross-ref сверяет только universal (CLAUDE/MEMORY); shell/observer — дефолт | safe degrade |
| `tool_registry_path` | пусто / файла нет → l1 **skip** (нет ложного дрейфа) | safe skip |
## 6. Тесты (TDD)
- `cross-ref-checker.test.mjs``loadFiles`/CLI собирают `normative_files universal`; кастомный
`normativeFiles` даёт кастомный набор; дефолт = 5 (backward-compat).
- `l1-watcher.test.mjs``loadInputs` принимает `toolRegistryPath`; пустой/несуществующий → skip
(`missingInTooling` пуст независимо от enabledPlugins).
## 7. Follow-up (вне этой спеки)
- Генерация regex-имён cross-ref / `shell-content-rules` / `observer-transcript-parser` из config
(greenfield с произвольными именами доков). Подход #2 из brainstorming.
- `classifier_context` wiring (`router-classifier`/`brain-retro-opus-reviewer`) — путь стены.
- deepseek-v4-pro тест-дрейф (3 теста, пре-существующая миграция).
- Затем Фаза 2 (plugin packaging, design v6 §14).
## 8. Канон
design v6 + план Фазы 1 + handoff №5 (`...session-handoff-5.md`). Исполнение — под стеной
escape-per-step (наставник H4 структурный, печать не встаёт; handoff №5 §2).