# Находки и проблемы машинерии арбитража (роутер-наставник) — для проекта claude-brain

**Дата:** 15.06.2026. **Источник:** сессия 4 «поиск хвостов Лидерры», Этап 2б (доменный go-live
аудит через церемонию стены). **Назначение:** перенести правки в `C:\моя\проекты\claude-brain`
(дом разработки мозга). Здесь — рабочая (замороженная) копия мозга; чинить **в claude-brain**.
Код-ссылки даны на файлы текущей копии (`tools/*.mjs` + `.claude/skills/`).

> **Что произошло за сессию (контекст для понимания находок).** Цель — реально прогнать 4
> доменных скила (`threat-model`/`pdn-152fz-audit`/`billing-audit`/`security-go-live`) и записать
> авторизованный отчёт. Под стеной это требует опечатанного плана; судья опечатать аудит-план
> отказывался. Итог: отчёт **получен**, но **НЕ через сквозной проход арбитража до печати** — через
> per-action escape-гранты владельца в обход печати. Ниже — почему «чистый» путь не прошёл и что
> чинить.

---

## 0. Подтверждённая механика (не баг — для справки команды brain)

- **Счётчик эскалации судьи** (`enforce-judge-gate.mjs` `bumpJudgeNoGo` + `JUDGE_ESCALATE_AFTER=3`):
  ключ — **task-id** (`router-task-id.mjs`: присваивается при первом плане, персистится, новое имя
  файла НЕ сбрасывает). Считает **подряд идущие** NO-GO; сброс на GO ИЛИ degraded (судья не
  дозвонился). На 3-м подряд NO-GO → `buildJudgeArbitrationMessage`.
- **`parseNegotiationSection`** (`negotiation-section.mjs`) берёт позицию контроллера ТОЛЬКО из
  подзаголовков `### Круг N` внутри `## Переговоры`. Нумерованный список (`1. 2. 3.`) **не
  парсится** → в карточку арбитража уходит «(позиция не указана)». (Сессия 3 писала список →
  позиция терялась; в сессии 4 формат `### Круг N` исправлен.)

---

## F-A. `cards:[]` зашит в детекторах продукта судьи (повтор, подтверждён)

**Симптом:** судья валит `card_need` для любого плана со скилами.
**Корень:** `enforce-judge-gate.mjs` `extractGate2Product` (~стр.192) и `extractGate1Product`
(~стр.206) возвращают `cards: []` жёстко → `judge-engine.mjs:56` рендерит «КАРТОЧКИ НАВЫКОВ: []»
всегда. Карточки существуют (`docs/registry/contracts/*.contract.json`), судье не подаются.
**Обход (рабочий):** вписать карточки дословно в тело плана (секция `## Карточки навыков`) — судья
читает «ПРОДУКТ НА СУД» и `card_need` снимается.
**Фикс (claude-brain, ~10 строк TDD):** в `extractGate2Product` распарсить skills-json
(`parsePlanSkills` из `plan-skills.mjs`) → загрузить карточки (`loadRegistry({dir: docs/registry/
contracts})` + `dispatchContract`) → вернуть `cards` вместо `[]`.

## F-B. Структурная самопротиворечивость судьи на аудит-задачах (catch-22) — ГЛАВНОЕ

**Симптом:** GO для аудит-плана недостижим переоценкой.
**Корень:** на gate2 судья (линзы `spec_fidelity`/`verifiability`/`step_clarity`,
`judge-engine.mjs` `VOTE_LAYOUTS.gate2`) требует в судимом продукте **заполненный отчёт**
(«схема D5 не заполнена ни одним фактическим значением», «нет дословного вывода grep»), НО его же
контракт — «План — это БУДУЩЕЕ; слово „сделано“ за факт не принимай» (`buildJudgePrompt`). А прошлые
круги заворачивали **пред-заполненный** отчёт как «циркулярный/пред-зафиксированный». Для плана,
чей продукт-при-исполнении и есть отчёт, требования **логически несовместимы**: gate2 судит план
ДО шагов, поэтому фактические значения не могут присутствовать, не нарушив «план = будущее».
**Обход:** не нашли (это и был тупик → ушли в escape).
**Фикс (claude-brain):** ввести для gate2 режим «план с динамическим выводом / аудит» — принимать
план, который декларирует **схему результата + правило агрегации + шаги-сборщики**, не требуя
пред-вычисленных значений. Либо отдельная линза, различающая «маршрутная карта без схемы» (плохо)
от «схема + детерминированная сборка на исполнении» (норма).

## F-C. L1-арбитраж наставника несёт устаревшее + невыполнимое возражение

**Симптом:** на `negotiation-8` наставник вынес L1-арбитраж с двумя «дефектами», из которых один
**уже исправлен**, второй **невозможен по контракту**.
**Корень:**
1. Наставник заявил «`grep charge_source` ОТСУТСТВУЕТ … 5 кругов», хотя шаг **был** добавлен
   (negotiation-7/8, шаг 13 `grep -n charge_source …LedgerService.php`; и инвариант реально верен —
   `LedgerService.php:95 'charge_source' => 'rub'`). Т.е. накопленное возражение **не
   переоценило текущий контент** перед выносом в арбитраж.
2. Наставник потребовал шаг `op=Gate` перед Write, но контракт `steps-json` допускает только
   `Write|Edit|Bash|MultiEdit` (Task запрещён, «Gate» нет) → требование **невыполнимо по
   конструкции**.
**Фикс (claude-brain):** (1) наставник обязан переоценивать **текущий** план каждый круг и не
тащить снятое возражение в арбитражную карточку; (2) не требовать механизмов, которых нет в
`steps-json` — либо добавить декларативный gate-узел в контракт, либо переформулировать требование
(owner-gate как секция плана + процедурный AskUser — это и было сделано, но наставник не зачёл).

## F-D. escape ≠ seal — «hold» на арбитраже не доводит до исполнения под печатью

**Симптом:** опция арбитража «Держусь контроллера» обещает «ворота пропустят версию контроллера …
(escape-грант)», но escape **не опечатывает** план.
**Корень:** escape (Машина 6, `escape-grant.mjs`) даёт ALLOW на **одно** действие; печать требует
`verdict.wired === 'GO'` (`enforce-judge-gate.mjs` `sealOnWiredGo`). На plan-level арбитраже hold →
escape пускает Write плана, но судья всё равно NO-GO → печати нет → шаги под печатью не идут.
**Обход (рабочий):** per-action escape на **каждый** скил + финальный Write (5 грантов) в обход
печати. Сработало, но это не «проход арбитража», а его обход.
**Фикс (claude-brain):** либо (а) hold на арбитраже даёт **разовый owner-authorized seal-override**
(печать по решению владельца, не по вердикту судьи); либо (б) текст опции hold не должен обещать
проход ворот, которого не даёт, а per-action-escape должен быть задокументирован как штатный путь
для «владелец продавил».

## F-E. Хрупкость escape: метка обязана быть в ЛЕЙБЛЕ опции, не в описании

**Симптом:** escape #5 не записался; Write молча упал в «разговорный режим» без диагностики.
**Корень:** `enforce-askuser-answer-parser` (PostToolUse AskUserQuestion) читает **ответ = лейбл
выбранной опции**. Я положил `FLOOR-ESCAPE: <action>` в **описание** опции (лейбл был «Accepted-risk
+ escape») → в ответе метки нет → грант не записан → нет диагностики, просто блок стены.
**Обход:** перенести метку в лейбл (escape #1–4 так и работали).
**Фикс (claude-brain):** (1) парсер должен сканировать ТАКЖЕ `description`/`annotations` опции, не
только лейбл; ИЛИ (2) при блоке Write/Skill, когда в недавнем AskUser есть строка `FLOOR-ESCAPE:`
вне лейбла, выдавать подсказку «метка должна быть в лейбле опции»; ИЛИ (3) жёстко задокументировать.

## F-F. write-escape: каноническая строка пути хрупка (кириллица/регистр/realpath)

**Симптом:** даже корректная по смыслу метка не совпала с первого раза.
**Корень:** `canonicalAction` (`escape-grant.mjs`) для Write = `write:pathNormalizeSafe(file_path)`.
`pathNormalizeSafe` (`path-normalization.mjs`): для **несуществующего** целевого файла `realpathSync`
бросает ENOENT → берётся `path.resolve` (сохраняет регистр ввода) → `caseFold` lowercase (win32) →
слэши `/` → `NFC`. Владелец обязан ввести **точный** lowercase + forward-slash + NFC абсолютный путь
с кириллицей и пробелами (`c:/моя/проекты/портал crm/документация/…`). Любое расхождение → тихий
no-match.
**Фикс (claude-brain):** (1) для AskUser-driven escape выводить **готовую** канон-строку из самого
pending tool-события (а не из набранной владельцем метки) — устранит ручной ввод пути; ИЛИ (2)
хелпер `node tools/print-canonical-action.mjs <toolEventJson>`; ИЛИ (3) нормализовать обе стороны
одинаково и допускать вариативность регистра/разделителей.

## F-G. Непостоянство surfacing вердикта (то синхронный блок, то тишина)

**Симптом:** `negotiation-5` (судья) и `negotiation-6/8` (наставник) выдали полный вердикт
синхронно (PostToolUse-блок). `negotiation-4` и `negotiation-7` вернулись «File created
successfully» **без** вердикта — состояние счётчика/печати **невидимо** контроллеру.
**Корень:** вероятно async-латентность LLM-вызова (~26–83с): иногда укладывается в окно
PostToolUse (синхронный блок), иногда возвращает чисто, вердикт оседает позже. Контроллер слеп —
не отличить NO-GO / skip / sealed без отдельной пробы.
**Фикс (claude-brain):** на каждый судимый Write плана/спеки выдавать **детерминированную строку
исхода** (GO-sealed / NO-GO / skipped-no-mentor-GO / degraded), даже когда не блокирует.

## F-H. readonly-Bash в разговорном режиме создаёт ложное ощущение «исполнения»

**Симптом:** 12 grep-шагов выполнились — я решил, что план опечатан и указатель идёт. На деле
readonly-Bash просто разрешён в разговорном режиме; печати не было (вскрылось на блоке Skill).
**Корень:** грепы НЕ двигали никакой указатель (опечатанного плана не существовало).
**Фикс/доку:** поведение скорее корректное (readonly разрешён), но стоит явно документировать
«успех readonly-Bash ≠ опечатанный план», и/или при readonly-Bash в разговорном режиме отдавать
тихую пометку «conversational, no active seal».

## F-I. Взаимодействие счётчиков наставника (L1) и судьи (L2)

**Симптом:** judge-L2 арбитраж (catch-22) в этой сессии так и не достигнут — раньше срабатывал
наставник (L1).
**Гипотеза (проверить в claude-brain):** mentor NO-GO (`negotiation-6`) сбрасывает judge-счётчик —
при отсутствии mentor-GO судья **skip → allow** (`runJudgeGate` `skip:'no_mentor_go'`) →
`bumpJudgeNoGo({blocked:false})` сбрасывает в 0. Перемежающиеся mentor-блоки не дают judge-счётчику
накопить до L2.
**Фикс (claude-brain):** определить, должны ли счётчики L1/L2 быть независимыми; задокументировать
взаимодействие; возможно, mentor-блок не должен трактоваться как judge-allow для сброса.

---

## Что в итоге сработало (компенсаторы — для понимания, что чем закрыли)

| Проблема | Компенсатор этой сессии |
|---|---|
| F-A `cards:[]` | карточки дословно в тело плана |
| F-B catch-22 судьи | не закрыт → ушли в escape |
| F-C наставник | charge_source-шаг добавлен; op=Gate невозможен → owner-gate секцией прозы |
| F-D escape≠seal | per-action escape на 4 скила + Write (обход печати) |
| F-E метка в описании | перенос метки в лейбл |
| F-F канон-путь | точная строка из чтения кода `path-normalization.mjs` |

## Достигнутый результат (несмотря на проблемы)

4 доменных скила реально загружены и применены; авторизованный отчёт записан —
`docs/superpowers/audits/2026-06-15-portal-domain-audit-report.md` (вердикт NO-GO промежуточный,
блокеры F-T2/F-P1/F-T1, owner accepted-risk interim). Данные — статические grep (live-DAST отложен).

## Приоритет правок для claude-brain (предложение)

1. **F-B** (catch-22 судьи на аудитах) — самый блокирующий; без него аудит-планы не печатаются.
2. **F-D** (escape≠seal vs текст hold) — чтобы «продавил владелец» был штатным, а не обходным.
3. **F-A** (cards:[]) — дешёвый фикс, снимает ложный card_need.
4. **F-C** (наставник: устаревшее/невыполнимое возражение) — корректность арбитража.
5. **F-E/F-F** (хрупкость escape) — UX владельца, тихие провалы.
6. **F-G/F-I** (видимость вердикта, счётчики L1/L2) — наблюдаемость.
