docs(mentor): two-level negotiation spec-v2 (anchored) + plans

This commit is contained in:
Дмитрий
2026-06-12 19:15:48 +03:00
parent 27082a42de
commit 155e9dc24a
5 changed files with 1080 additions and 11 deletions
+11 -11
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-06-12T16:04:08.911Z
Last updated: 2026-06-12T16:10:10.230Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -8,7 +8,7 @@ Last updated: 2026-06-12T16:04:08.911Z
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 2 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ✅ | 1131 episode(s) this month · Stop-hook + post-commit OK |
| C5 Observer-coverage | ✅ | 1133 episode(s) this month · Stop-hook + post-commit OK |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Кто на посту (оборона М1–М6)
@@ -54,6 +54,8 @@ Last updated: 2026-06-12T16:04:08.911Z
| Время | Действие | Причина |
|---|---|---|
| 2026-06-12T16:09:11.538Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
| 2026-06-12T16:05:49.898Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
| 2026-06-12T16:03:58.200Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
| 2026-06-12T16:02:06.577Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
| 2026-06-12T16:00:15.257Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
@@ -62,13 +64,11 @@ Last updated: 2026-06-12T16:04:08.911Z
| 2026-06-12T15:50:57.734Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
| 2026-06-12T15:48:39.482Z | write:c:/моя/проекты/портал crm/документация/tools/enforce-judge-gate.mjs | §6: правка ЗАКОНА (discipline source edited ad-hoc (outside sealed plan)) требует escape владельца — скил недостаточен |
| 2026-06-12T15:46:29.868Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
| 2026-06-12T15:45:34.668Z | write:c:/моя/проекты/портал crm/документация/tools/enforce-judge-gate.mjs | §6: правка ЗАКОНА (discipline source edited ad-hoc (outside sealed plan)) требует escape владельца — скил недостаточен |
| 2026-06-12T15:43:16.857Z | write:c:/users/administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 | path «C:/Users/Administrator/.claude/projects/c---------------------crm-------------/196f7d18-2a2c-4f25-966f-ebc43f93e38 |
## Метрики (информационные, не алерты)
- Observer evidence: 1131 episodes this month, 0 observer_error markers, 5 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 1131
- Observer evidence: 1133 episodes this month, 0 observer_error markers, 5 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 1133
- Last /brain-retro: 16 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
@@ -84,9 +84,9 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| bugfix | 33 | 12.1% | 18.2% |
| cleanup | 1 | 0.0% | 0.0% |
Router step distribution: 1: 576, 2: 396, 3: 18, 5: 120
Router step distribution: 1: 578, 2: 396, 3: 18, 5: 120
Boundaries applied (ADR / границы): 8 of 1110 эпизодов (0.7%).
Boundaries applied (ADR / границы): 8 of 1112 эпизодов (0.7%).
## Активные многоэтапные проекты
@@ -103,7 +103,7 @@ Boundaries applied (ADR / границы): 8 of 1110 эпизодов (0.7%).
| session_id | макс. ход | % regulated | последний эпизод |
|---|---|---|---|
| `f3950d20` | 60 | 4% | 2026-06-12T12:46:45.175Z |
| `196f7d18` | 53 | 2% | 2026-06-12T16:03:27.956Z |
| `196f7d18` | 56 | 2% | 2026-06-12T16:06:19.199Z |
Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.
@@ -111,7 +111,7 @@ Long sessions correlate with discipline drift. Если % regulated просел
| Компонент | Токены (in/out) | USD |
|---|---|---|
| Classifier (Sonnet 4.6) | 63266/226370 | $3.59 |
| Classifier (Sonnet 4.6) | 63301/226633 | $3.59 |
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
| **Итого** | | **$3.59** |
@@ -127,7 +127,7 @@ Episodes since last run: 542 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 1131.
0 эпизодов проверено из 1133.
## Reviewer findings
@@ -0,0 +1,749 @@
# Двухуровневые переговоры + арбитраж владельца — план реализации
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Достроить двухуровневые переговоры (контроллер↔наставник L1, контроллер↔судья L2) с двумя независимыми счётчиками (потолок 3) и арбитражем владельца на тупике — переиспользуя живые кирпичи.
**Architecture:** Подход 1 — максимум переиспользования. Замечание едет на блок-сообщении хука (П1); позиция контроллера — в разделе «## Переговоры» план-файла (П2); решение владельца — через существующий escape-grant / waivers (П3). Новый код мал и чист: счётчик наставника (зеркало `bumpJudgeNoGo`), форматтеры замечаний, парсер «Переговоры», модуль карточки арбитража. L2 живёт с судьёй в Фазе 8 — все задачи проверяются юнит-тестами при судье inert.
**Tech Stack:** Node ESM (`tools/*.mjs`), vitest (tools-only конфиг). Спека: `docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md`.
**Пусковой рецепт регрессии (канон роадмапа):** `npx vitest run --root app --config vitest.config.tools.mjs` (npx-кэш, vitest 4.1.8). Запуск через `node app/node_modules/vitest/...` ПАДАЕТ на out-of-root тестах — не использовать.
**Живое состояние / границы:** наставник АКТИВЕН (Этап 4) — трогать бережно, регрессия на каждом шаге. Судья inert в main → L2 тестируется без живого блока, активация — Фаза 8 (шаг владельца). Read-deny и страж секретов НЕ ослабляем.
---
## Задачи
### Task 1: Дописать §12 «брешь seed-write» в спеку + закоммитить спеку (обещание владельцу, «не забудь»)
Это первый шаг намеренно: текст §12 был заблокирован `supreme-gate` в разговорном режиме; после печати плана реализация открыта.
**Files:**
- Modify: `docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md` (вставить «## 12» перед «## 12. Ссылки», переименовать старую в «## 13. Ссылки»)
- [ ] **Step 1: Вставить секцию «брешь seed-write»**
Заменить строку `## 12. Ссылки` на блок (текст согласован с владельцем 12.06):
```markdown
## 12. Известная брешь процесса: seed-write без ИИ-сторожа (честно)
Эта спека сама написана в условиях бреши, которую закрывает эпик.
**Что произошло.** Запись файла спеки (Write) прошла без проверки наставником и судьёй. Это не случайность, а **семя**: `supreme-gate` пропускает «печать артефакта» в разговорном режиме (иначе bootstrap-круг — нельзя написать план/спеку, не имея права что-то записать; ср. OQ4/D12). Но содержание семени в такой сессии не прошло ни одного ИИ-сторожа: судья inert в main, а хук наставника сторожит запись `plans/` и исполнение, не запись `specs/`. Брешь не в том, что «можно записать файл», а в том, что **содержимое семени-артефакта не проверяется ИИ-стражем при записи**.
**Как закрыть (по убыванию силы):**
1. **Фаза 8 — включить судью в main.** Судья на Stop / на Гейте-1 (`extractGate1Product` уже судит `specs/`) проверяет содержание. Без Фазы 8 в main брешь принципиально не закрывается. Цена: живой LLM + активация владельцем.
2. **Расширить триггер наставника на `specs/`, не только `plans/`** — запись спеки тоже будит наставника, вердикт едет на блок-сообщении (П1).
3. **Зажать семя (H4 — enforcement, не обещание):** печать артефакта может РОВНО создать файл, без чтений/команд/иных правок.
**Честный предел (не сводится к нулю машинами):** запись первого артефакта невозможной сделать нельзя (bootstrap); проверяющий — сам ИИ. Последний рубеж — владелец на видимом артефакте (D7/D17), предел recursive-trust (L2).
**Связь с дизайном:** двухуровневая схема — часть закрытия. После сборки + Фазы 8 записи плана идут через наставника (L1) и судью (L2). Сегодня не действовала: схемы ещё нет и судья выключен.
---
## 13. Ссылки
```
- [ ] **Step 2: Закоммитить спеку + план**
Run:
```bash
git add docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md docs/superpowers/plans/2026-06-12-router-mentor-two-level-negotiation.md
git commit -m "docs(mentor): спека+план двухуровневых переговоров + арбитраж владельца (+§12 брешь seed-write)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
Expected: docs-only коммит проходит (`enforce-verify-before-push` пропускает .md per CLAUDE.md §5 п.13). Если `supreme-gate` ещё держит — это значит план не считается «напечатанным» до завершения; тогда коммит выполнить в конце Task 2 вместе с первым кодовым изменением.
---
### Task 2: Счётчик NO-GO наставника (`bumpMentorNoGo`) — зеркало `bumpJudgeNoGo`
**Files:**
- Create: `tools/mentor-nogo-counter.mjs`
- Test: `tools/mentor-nogo-counter.test.mjs`
- [ ] **Step 1: Написать падающий тест**
```js
import { describe, it, expect } from 'vitest';
import { bumpMentorNoGo, MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs';
describe('bumpMentorNoGo', () => {
const mem = () => {
const store = {};
return {
readFileSync: (p) => { if (!(p in store)) { const e = new Error('no'); e.code = 'ENOENT'; throw e; } return store[p]; },
writeFileSync: (p, d) => { store[p] = d; },
mkdirSync: () => {},
};
};
it('инкремент на blocked, сброс на allow', () => {
const fsImpl = mem(); const dir = '/r'; const sessionId = 's1';
expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(1);
expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(2);
expect(bumpMentorNoGo({ sessionId, blocked: false, fsImpl, dir })).toBe(0);
expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(1);
});
it('порог эскалации = 3', () => { expect(MENTOR_ESCALATE_AFTER).toBe(3); });
it('небезопасный sessionId санитизируется (не падает)', () => {
const fsImpl = mem();
expect(bumpMentorNoGo({ sessionId: '../../x', blocked: true, fsImpl, dir: '/r' })).toBe(1);
});
});
```
- [ ] **Step 2: Запустить — убедиться, что падает**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-nogo-counter.test.mjs`
Expected: FAIL — модуль не существует.
- [ ] **Step 3: Реализовать (точное зеркало `bumpJudgeNoGo` из `enforce-judge-gate.mjs:319-327`)**
```js
#!/usr/bin/env node
/**
* mentor-nogo-counter — счётчик подряд идущих NO-GO наставника (L1), зеркало
* bumpJudgeNoGo. blocked=true → +1; allow → сброс 0. Порог 3 → арбитраж (round-control C-12).
*/
import fsDefault from 'node:fs';
import { join } from 'node:path';
import { runtimeDir } from './enforce-hook-helpers.mjs';
export const MENTOR_ESCALATE_AFTER = 3;
export function bumpMentorNoGo({ sessionId, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `mentor-nogo-${safe}.json`);
let count = 0;
try { count = (JSON.parse(fsImpl.readFileSync(path, 'utf8')).count) || 0; } catch { count = 0; }
const next = blocked ? count + 1 : 0;
try { fsImpl.mkdirSync(dir, { recursive: true }); fsImpl.writeFileSync(path, JSON.stringify({ count: next })); } catch { /* best-effort */ }
return next;
}
```
- [ ] **Step 4: Запустить — убедиться, что проходит**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-nogo-counter.test.mjs`
Expected: PASS (3 теста).
- [ ] **Step 5: Коммит**
```bash
git add tools/mentor-nogo-counter.mjs tools/mentor-nogo-counter.test.mjs
git commit -m "feat(mentor): счётчик NO-GO наставника (L1, зеркало bumpJudgeNoGo)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 3: Модуль карточки арбитража (`arbitration-card.mjs`) — чистый, дословные цитаты
**Files:**
- Create: `tools/arbitration-card.mjs`
- Test: `tools/arbitration-card.test.mjs`
- [ ] **Step 1: Написать падающий тест**
```js
import { describe, it, expect } from 'vitest';
import { buildArbitrationCard, ARBITRATION_OPTIONS } from './arbitration-card.mjs';
describe('buildArbitrationCard', () => {
const base = {
side: 'mentor', // 'mentor' | 'judge'
level: 'L1',
round: 3,
objectionVerbatim: 'Шаг 2 склеен: чтение и правка в одном пункте — разбей.',
controllerPositionVerbatim: 'Не согласен: шаг 2 атомарен, чтение нужно ровно для этой правки.',
};
it('цитаты переданы ДОСЛОВНО (без перефраза)', () => {
const c = buildArbitrationCard(base);
expect(c.objection).toBe(base.objectionVerbatim);
expect(c.position).toBe(base.controllerPositionVerbatim);
});
it('три варианта выбора в фиксированном порядке', () => {
const c = buildArbitrationCard(base);
expect(c.options.map((o) => o.key)).toEqual(['hold', 'accept', 'own']);
expect(ARBITRATION_OPTIONS).toEqual(['hold', 'accept', 'own']);
});
it('каждый вариант несёт нейтральную строку «что меняет выбор»', () => {
const c = buildArbitrationCard(base);
for (const o of c.options) expect(typeof o.whatChanges === 'string' && o.whatChanges.length > 0).toBe(true);
});
it('есть аффорданс «объясни подробнее»', () => {
expect(buildArbitrationCard(base).explainAffordance).toBe(true);
});
it('сторона судьи меняет заголовок, не ломает форму', () => {
const c = buildArbitrationCard({ ...base, side: 'judge', level: 'L2' });
expect(c.title).toMatch(/судь/i);
expect(c.options.map((o) => o.key)).toEqual(['hold', 'accept', 'own']);
});
it('пустые/нестроковые цитаты не роняют (fail-safe → пустая строка)', () => {
const c = buildArbitrationCard({ ...base, objectionVerbatim: null, controllerPositionVerbatim: undefined });
expect(c.objection).toBe('');
expect(c.position).toBe('');
});
});
```
- [ ] **Step 2: Запустить — убедиться, что падает**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/arbitration-card.test.mjs`
Expected: FAIL — модуль не существует.
- [ ] **Step 3: Реализовать**
```js
#!/usr/bin/env node
/**
* arbitration-card — чистый сборщик карточки арбитража (спека §6). Дословные цитаты обеих
* сторон + нейтральная строка «что меняет выбор» на каждый вариант + аффорданс «объясни
* подробнее». НЕ перефразирует: objection/position проходят как есть (только String-coerce).
* Решение владельца пишется НЕ здесь, а через escape-grant/waivers (П3).
*/
export const ARBITRATION_OPTIONS = ['hold', 'accept', 'own'];
const verbatim = (s) => (typeof s === 'string' ? s : '');
export function buildArbitrationCard({ side, level, round, objectionVerbatim, controllerPositionVerbatim } = {}) {
const who = side === 'judge' ? 'судьи' : 'наставника';
return {
side, level, round,
title: `Арбитраж (${level}): тупик спора с ${side === 'judge' ? 'судьёй' : 'наставником'} после ${round} кругов`,
objection: verbatim(objectionVerbatim),
position: verbatim(controllerPositionVerbatim),
options: [
{ key: 'hold', label: 'Держусь позиции контроллера', whatChanges: `Ворота пропустят версию контроллера — замечание ${who} отведено тобой (escape-грант).` },
{ key: 'accept', label: 'Согласиться с замечанием', whatChanges: `Контроллер переделает по замечанию ${who}; счётчик круга сбрасывается.` },
{ key: 'own', label: 'Своё решение', whatChanges: 'Ты вписываешь своё — оно становится обязательным для контроллера.' },
],
explainAffordance: true,
};
}
```
- [ ] **Step 4: Запустить — убедиться, что проходит**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/arbitration-card.test.mjs`
Expected: PASS (6 тестов).
- [ ] **Step 5: Коммит**
```bash
git add tools/arbitration-card.mjs tools/arbitration-card.test.mjs
git commit -m "feat(mentor): чистый сборщик карточки арбитража (дословные цитаты + что меняет выбор)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 4: Парсер раздела «## Переговоры» план-файла (позиция контроллера, П2)
Позиция контроллера в споре живёт в плане; парсер достаёт её дословно — для подачи наставнику (negotiationLog) и для карточки арбитража.
**Files:**
- Create: `tools/negotiation-section.mjs`
- Test: `tools/negotiation-section.test.mjs`
- [ ] **Step 1: Написать падающий тест**
```js
import { describe, it, expect } from 'vitest';
import { parseNegotiationSection } from './negotiation-section.mjs';
const PLAN = `# План
## Цель
Сделать X.
## Переговоры
### Круг 1
Не согласен с замечанием про шаг 2: он атомарен.
### Круг 2
Принял про шаг 4, переписал.
## Шаги
- [ ] ...`;
describe('parseNegotiationSection', () => {
it('достаёт круги с дословным текстом позиции', () => {
const r = parseNegotiationSection(PLAN);
expect(r).toEqual([
{ round: 1, position: 'Не согласен с замечанием про шаг 2: он атомарен.' },
{ round: 2, position: 'Принял про шаг 4, переписал.' },
]);
});
it('нет раздела → пустой массив', () => {
expect(parseNegotiationSection('# План\n## Цель\nY')).toEqual([]);
});
it('последний круг (для арбитража) = max по round', () => {
const r = parseNegotiationSection(PLAN);
const last = r[r.length - 1];
expect(last.round).toBe(2);
});
it('не падает на мусоре', () => {
expect(parseNegotiationSection(null)).toEqual([]);
});
});
```
- [ ] **Step 2: Запустить — убедиться, что падает**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/negotiation-section.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализовать**
```js
#!/usr/bin/env node
/**
* negotiation-section — парсер раздела «## Переговоры» план-файла. Возвращает круги
* [{round, position}] с ДОСЛОВНЫМ текстом позиции контроллера (для наставника и арбитража).
* Раздел = «## Переговоры» до следующего «## », внутри — «### Круг N» блоки.
*/
export function parseNegotiationSection(md) {
const text = typeof md === 'string' ? md : '';
const sec = text.match(/^##\s*Переговоры[^\n]*\n([\s\S]*?)(?:\n##\s|$)/im);
if (!sec) return [];
const body = sec[1];
const out = [];
const re = /^###\s*Круг\s*(\d+)[^\n]*\n([\s\S]*?)(?=\n###\s|$)/gim;
let m;
while ((m = re.exec(body)) !== null) {
out.push({ round: Number(m[1]), position: m[2].trim() });
}
return out;
}
```
- [ ] **Step 4: Запустить — убедиться, что проходит**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/negotiation-section.test.mjs`
Expected: PASS (4 теста).
- [ ] **Step 5: Коммит**
```bash
git add tools/negotiation-section.mjs tools/negotiation-section.test.mjs
git commit -m "feat(mentor): парсер раздела «Переговоры» план-файла (позиция контроллера, П2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 5: Форматтер замечания судьи (`formatJudgeObjection`) + вставка в блок-сообщение
Сейчас блок-сообщение судьи (`decide()` в `enforce-judge-gate.mjs:50`) = «судья=NO-GO, пол=… → блок» — без текста возражения. Добавляем дословный текст возражений из `verdict.objections`/`verdict.slots`.
**Files:**
- Create: `tools/judge-objection-format.mjs`
- Test: `tools/judge-objection-format.test.mjs`
- Modify: `tools/enforce-judge-gate.mjs` (импорт + использование в `decide()` message)
- [ ] **Step 1: Написать падающий тест форматтера**
```js
import { describe, it, expect } from 'vitest';
import { formatJudgeObjection } from './judge-objection-format.mjs';
describe('formatJudgeObjection', () => {
it('склеивает дословные возражения (anchor.ref + severity)', () => {
const v = { objections: [
{ verdict: 'NO', anchor: { kind: 'failed_criterion', ref: 'шаг 3 не проверяемый исход' }, severity: 'heavy' },
], slots: {} };
const s = formatJudgeObjection(v);
expect(s).toContain('шаг 3 не проверяемый исход');
expect(s).toContain('heavy');
});
it('нет возражений → пустая строка', () => {
expect(formatJudgeObjection({ objections: [], slots: {} })).toBe('');
});
it('мусорный вердикт не роняет', () => {
expect(formatJudgeObjection(null)).toBe('');
expect(formatJudgeObjection({ objections: 'x' })).toBe('');
});
});
```
- [ ] **Step 2: Запустить — убедиться, что падает**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/judge-objection-format.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализовать форматтер**
```js
#!/usr/bin/env node
/**
* judge-objection-format — дословная сводка возражений судьи для блок-сообщения (П1).
* Берёт verdict.objections (форма parseJudgeResponse: {verdict,anchor:{kind,ref},severity}).
* Fail-safe: не-массив/пусто → ''.
*/
export function formatJudgeObjection(verdict) {
const objs = verdict && Array.isArray(verdict.objections) ? verdict.objections : [];
const lines = objs
.filter((o) => o && o.anchor && o.anchor.ref)
.map((o) => `— [${o.severity || 'light'}] ${o.anchor.ref}`);
return lines.join('\n');
}
```
- [ ] **Step 4: Запустить форматтер — PASS**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/judge-objection-format.test.mjs`
Expected: PASS (3 теста).
- [ ] **Step 5: Вставить замечание в блок-сообщение `decide()`**
В `tools/enforce-judge-gate.mjs`: добавить импорт `import { formatJudgeObjection } from './judge-objection-format.mjs';` и в `decide()` (строка ~50) расширить message:
```js
return gate === 'allow'
? { block: false, reason: 'live-block: судья GO + пол чист' }
: { block: true, message: `[judge-gate] live-block: судья=${decision}, пол=${floorBlocked} → блок` + (formatJudgeObjection(verdict) ? `\nЗамечание судьи:\n${formatJudgeObjection(verdict)}` : '') };
```
- [ ] **Step 6: Тест на расширенный `decide()`-message (добавить в существующий `tools/enforce-judge-gate.test.mjs`)**
```js
it('decide() включает дословное замечание судьи в блок-сообщение', async () => {
const { decide } = await import('./enforce-judge-gate.mjs');
const verdict = { decision: 'NO-GO', objections: [{ anchor: { ref: 'шаг 3 не проверяемый' }, severity: 'heavy' }] };
const r = decide({ mode: 'live-block', verdict, floorBlocked: false });
expect(r.block).toBe(true);
expect(r.message).toContain('шаг 3 не проверяемый');
});
```
- [ ] **Step 7: Запустить полный файл судьи + форматтер — PASS**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/judge-objection-format.test.mjs tools/enforce-judge-gate.test.mjs`
Expected: PASS (включая старые тесты судьи — не сломаны).
- [ ] **Step 8: Коммит**
```bash
git add tools/judge-objection-format.mjs tools/judge-objection-format.test.mjs tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit -m "feat(judge): дословное замечание судьи в блок-сообщении (П1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 6: Замечание наставника доходит до контроллера (non-blocking surface, П1)
Наставник — производитель (`enforce-mentor-on-plan-write.mjs`, exit всегда allow). Его NO-GO должен дойти до контроллера сообщением, НЕ блокируя запись.
**Files:**
- Create: `tools/mentor-objection-format.mjs`
- Test: `tools/mentor-objection-format.test.mjs`
- Modify: `tools/enforce-mentor-on-plan-write.mjs``main()` — surface сообщения при `ran && wired && !ok`)
- [ ] **Step 1: Сначала прочитать `tools/enforce-hook-helpers.mjs` — подтвердить, поддерживает ли `exitDecision` non-blocking message (PostToolUse additionalContext/systemMessage)**
Run: `grep -n "export function exitDecision" tools/enforce-hook-helpers.mjs` и прочитать тело.
Если `exitDecision` умеет `{ block:false, message }` (выводит сообщение, не блокируя) — использовать его. Если НЕТ — surface через `console.error(...)` (PostToolUse stderr на allow показывается контроллеру; так делает `logGuardBlock`-сосед) ИЛИ через возврат PostToolUse JSON `{ "decision": undefined, "reason": "..." }`. Выбрать поддержанный путь, зафиксировать в коде Step 4.
- [ ] **Step 2: Написать падающий тест форматтера**
```js
import { describe, it, expect } from 'vitest';
import { formatMentorObjection } from './mentor-objection-format.mjs';
describe('formatMentorObjection', () => {
it('собирает дословный reason + замечания вердикта', () => {
const s = formatMentorObjection({ ok: false, wired: true, reason: 'шаг 2 склеен', verdict: { objections: [{ anchor: { ref: 'разбей шаг 2' } }] } });
expect(s).toContain('шаг 2 склеен');
expect(s).toContain('разбей шаг 2');
});
it('ok=true → пустая строка (нет замечания)', () => {
expect(formatMentorObjection({ ok: true, wired: true })).toBe('');
});
it('не wired → пустая строка ($0, наставник не судил)', () => {
expect(formatMentorObjection({ ok: false, wired: false })).toBe('');
});
it('мусор не роняет', () => { expect(formatMentorObjection(null)).toBe(''); });
});
```
- [ ] **Step 3: Запустить — FAIL**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-objection-format.test.mjs`
Expected: FAIL.
- [ ] **Step 4: Реализовать форматтер**
```js
#!/usr/bin/env node
/**
* mentor-objection-format — дословная сводка замечания наставника для surface контроллеру
* (П1, non-blocking). Источник — результат onPlanWrite {ok,wired,reason,verdict}.
* Fail-safe: не wired или ok → '' (нечего показывать).
*/
export function formatMentorObjection(r) {
if (!r || r.wired !== true || r.ok === true) return '';
const head = typeof r.reason === 'string' && r.reason ? `Замечание наставника: ${r.reason}` : 'Замечание наставника.';
const objs = r.verdict && Array.isArray(r.verdict.objections) ? r.verdict.objections : [];
const lines = objs.filter((o) => o && o.anchor && o.anchor.ref).map((o) => `${o.anchor.ref}`);
return [head, ...lines].join('\n');
}
```
- [ ] **Step 5: Запустить форматтер — PASS**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-objection-format.test.mjs`
Expected: PASS (4 теста).
- [ ] **Step 6: Вставить surface + bump счётчика в `main()` наставника**
В `tools/enforce-mentor-on-plan-write.mjs` после вызова `runMentorOnPlanWrite(...)` сохранить результат в `const res = await runMentorOnPlanWrite(...)` и перед `exitDecision({ block: false })`:
```js
import { formatMentorObjection } from './mentor-objection-format.mjs';
import { bumpMentorNoGo } from './mentor-nogo-counter.mjs';
// ... в main(), после res:
try {
if (res && res.ran) {
const blocked = res.wired === true && res.ok !== true;
bumpMentorNoGo({ sessionId: sess, blocked });
const msg = formatMentorObjection(res);
if (msg) console.error(msg); // surface контроллеру, НЕ блокирует (producer)
}
} catch { /* surface best-effort */ }
```
(Если Step 1 показал поддержку `exitDecision({block:false, message})` — использовать его вместо `console.error`.)
- [ ] **Step 7: Тест non-blocking surface (добавить в `tools/enforce-mentor-on-plan-write.test.mjs`)**
```js
it('NO-GO наставника не блокирует, но surface даёт текст замечания', () => {
const r = formatMentorObjection({ ok: false, wired: true, reason: 'переформулируй шаг 3' });
expect(r).toContain('переформулируй шаг 3'); // exit остаётся allow — проверяется отдельным существующим тестом producer
});
```
- [ ] **Step 8: Регрессия наставника — PASS (producer по-прежнему allow)**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-mentor-on-plan-write.test.mjs tools/mentor-objection-format.test.mjs`
Expected: PASS (старые producer-тесты не сломаны).
- [ ] **Step 9: Коммит**
```bash
git add tools/mentor-objection-format.mjs tools/mentor-objection-format.test.mjs tools/enforce-mentor-on-plan-write.mjs tools/enforce-mentor-on-plan-write.test.mjs
git commit -m "feat(mentor): замечание наставника доходит до контроллера + счётчик NO-GO (П1, L1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 7: Эскалация судьи (3 NO-GO) → карточка арбитража
Сейчас при 3-м NO-GO `main()` судьи (`enforce-judge-gate.mjs:346`) даёт generic-сообщение «escape или подтверди отказ». Заменяем на карточку арбитража с дословной позицией контроллера (из плана).
**Files:**
- Modify: `tools/enforce-judge-gate.mjs` (ветка `n >= JUDGE_ESCALATE_AFTER`)
- Test: `tools/enforce-judge-gate.test.mjs`
- [ ] **Step 1: Написать падающий тест на формат эскалации**
```js
it('3-й NO-GO судьи даёт карточку арбитража с дословной позицией контроллера', async () => {
const mod = await import('./enforce-judge-gate.mjs');
// через buildJudgeArbitrationMessage(verdict, planContent, n)
const msg = mod.buildJudgeArbitrationMessage(
{ objections: [{ anchor: { ref: 'шаг 3 не проверяем' }, severity: 'heavy' }] },
'## Переговоры\n### Круг 3\nНе согласен: критерий проверяемый — pest зелёный.',
3,
);
expect(msg).toContain('шаг 3 не проверяем'); // замечание судьи дословно
expect(msg).toContain('критерий проверяемый — pest зелёный'); // позиция контроллера дословно
expect(msg).toMatch(/держусь|согласиться|своё/i); // три выбора
});
```
- [ ] **Step 2: Запустить — FAIL**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-judge-gate.test.mjs`
Expected: FAIL — `buildJudgeArbitrationMessage` не существует.
- [ ] **Step 3: Реализовать `buildJudgeArbitrationMessage` + вставить в `main()`**
В `tools/enforce-judge-gate.mjs`:
```js
import { buildArbitrationCard } from './arbitration-card.mjs';
import { formatJudgeObjection } from './judge-objection-format.mjs';
import { parseNegotiationSection } from './negotiation-section.mjs';
export function buildJudgeArbitrationMessage(verdict, planContent, n) {
const neg = parseNegotiationSection(planContent);
const position = neg.length ? neg[neg.length - 1].position : '(позиция не указана в разделе «Переговоры» плана)';
const card = buildArbitrationCard({
side: 'judge', level: 'L2', round: n,
objectionVerbatim: formatJudgeObjection(verdict) || '(судья не дал текста возражения)',
controllerPositionVerbatim: position,
});
const opts = card.options.map((o) => `${o.label}: ${o.whatChanges}`).join('\n');
return [
`[judge-gate] ${card.title}`,
`Замечание судьи:\n${card.objection}`,
`Позиция контроллера:\n${card.position}`,
`Что меняет выбор:\n${opts}`,
'Можно сказать «объясни подробнее» — дам нейтральное пояснение. Решение действует только через escape-грант/вейвер владельца (не со слов Claude).',
].join('\n\n');
}
```
В `main()` заменить тело ветки `if (result.block && n >= JUDGE_ESCALATE_AFTER)` на:
```js
const planContent = String((event && event.tool_input && event.tool_input.content) ?? '');
result = { ...result, message: buildJudgeArbitrationMessage(result.verdictForCard || (result.verdict), planContent, n) };
```
Примечание: вердикт для карточки нужно прокинуть из `runJudgeTurn`. Прокинуть в `result` поле `verdict` (расширить возврат `runJudgeTurn` полем `verdict` из `runJudgeGate`) ИЛИ повторно префетчить нельзя (дорого) — взять из лога. **Шаг реализации:** в `runJudgeTurn` вернуть `{ block, message, verdict }` (verdict уже в области видимости после `runJudgeGate`); в `main()` использовать `result.verdict`.
- [ ] **Step 4: Запустить — PASS (включая старые тесты судьи)**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-judge-gate.test.mjs`
Expected: PASS.
- [ ] **Step 5: Коммит**
```bash
git add tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit -m "feat(judge): эскалация 3 NO-GO → карточка арбитража (дословные обе стороны, L2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 8: Эскалация наставника (3 NO-GO) → карточка арбитража + регрессия
**Files:**
- Modify: `tools/enforce-mentor-on-plan-write.mjs``main()` — при `bumpMentorNoGo >= MENTOR_ESCALATE_AFTER` собрать карточку наставника и surface)
- Test: `tools/enforce-mentor-on-plan-write.test.mjs`
- [ ] **Step 1: Написать падающий тест на карточку наставника**
```js
it('3-й NO-GO наставника даёт карточку арбитража (сторона mentor)', async () => {
const mod = await import('./enforce-mentor-on-plan-write.mjs');
const msg = mod.buildMentorArbitrationMessage(
{ ok: false, wired: true, reason: 'шаг 2 склеен', verdict: { objections: [{ anchor: { ref: 'разбей шаг 2' } }] } },
'## Переговоры\n### Круг 3\nШаг 2 атомарен — настаиваю.',
3,
);
expect(msg).toContain('разбей шаг 2'); // замечание наставника дословно
expect(msg).toContain('Шаг 2 атомарен — настаиваю'); // позиция контроллера дословно
expect(msg).toMatch(/L1/);
});
```
- [ ] **Step 2: Запустить — FAIL**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-mentor-on-plan-write.test.mjs`
Expected: FAIL.
- [ ] **Step 3: Реализовать `buildMentorArbitrationMessage` + вставить в `main()`**
В `tools/enforce-mentor-on-plan-write.mjs`:
```js
import { buildArbitrationCard } from './arbitration-card.mjs';
import { formatMentorObjection } from './mentor-objection-format.mjs';
import { parseNegotiationSection } from './negotiation-section.mjs';
import { MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs';
export function buildMentorArbitrationMessage(res, planContent, n) {
const neg = parseNegotiationSection(planContent);
const position = neg.length ? neg[neg.length - 1].position : '(позиция не указана в разделе «Переговоры» плана)';
const card = buildArbitrationCard({
side: 'mentor', level: 'L1', round: n,
objectionVerbatim: formatMentorObjection(res) || '(нет текста замечания)',
controllerPositionVerbatim: position,
});
const opts = card.options.map((o) => `${o.label}: ${o.whatChanges}`).join('\n');
return [`[mentor] ${card.title}`, `Замечание наставника:\n${card.objection}`, `Позиция контроллера:\n${card.position}`, `Что меняет выбор:\n${opts}`, 'Скажи «объясни подробнее» — поясню. Решение — через escape/вейвер владельца.'].join('\n\n');
}
```
В `main()` (Task 6 Step 6 блок) — расширить: если `bumpMentorNoGo(...) >= MENTOR_ESCALATE_AFTER` и blocked, то `console.error(buildMentorArbitrationMessage(res, content, n))` вместо обычного surface (`content` уже извлечён выше в `runMentorOnPlanWrite`; пробросить через возврат или повторно прочитать `event.tool_input.content`).
- [ ] **Step 4: Запустить — PASS (producer по-прежнему allow)**
Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-mentor-on-plan-write.test.mjs`
Expected: PASS.
- [ ] **Step 5: Коммит**
```bash
git add tools/enforce-mentor-on-plan-write.mjs tools/enforce-mentor-on-plan-write.test.mjs
git commit -m "feat(mentor): эскалация 3 NO-GO → карточка арбитража (сторона mentor, L1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
### Task 9: Полная регрессия tools-only + финальный коммит
**Files:** нет правок кода — только прогон и фиксация.
- [ ] **Step 1: Полная регрессия tools-only**
Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: все GREEN (baseline 3754 + новые тесты Tasks 2-8; 0 регрессий). Зафиксировать число в финальном коммите/заметке.
- [ ] **Step 2: Если есть незакоммиченное — финальный коммит**
```bash
git add -A tools/
git commit -m "test(mentor): двухуровневые переговоры — полная регрессия GREEN
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
- [ ] **Step 3: Зафиксировать в роадмапе/памяти (отдельный ход с активным claude-md-management + coverage direct:memory-sync)**
Дописать в `docs/superpowers/specs/2026-06-12-router-mentor-roadmap.md` пункт «двухуровневые переговоры собраны (L1+L2 механика, L2 живёт с Фазой 8)» — через нормальный docs-канал.
---
## Self-Review (против спеки)
**Покрытие спеки:**
- §3/§4 L1 поток + счётчик L1 → Task 2 (`bumpMentorNoGo`) + Task 6 (surface + bump) + Task 8 (эскалация-карточка). ✓
- §5 L2 поток + счётчик L2 → счётчик `bumpJudgeNoGo` уже живой; Task 5 (замечание судьи в блоке) + Task 7 (эскалация-карточка). ✓
- §6 карточка арбитража + канал решения → Task 3 (карточка) + Task 7/8 (вставка) + переиспользование escape-grant/`applyWaivers` (П3, существуют — отдельная сборка не нужна). ✓
- §2 П1 (замечание на блоке) → Task 5 (судья) + Task 6 (наставник). П2 (позиция в плане) → Task 4 (парсер). П3 (канал решения) → escape-grant/waivers (живые). ✓
- §8 fail-closed → формат-функции fail-safe (Tasks 3/5/6 «мусор не роняет»); счётчики best-effort (Task 2). Битый счётчик/нет решения → консервативно (наследуется от round-control/escape-grant). ✓
- §12 (брешь seed-write) → Task 1 (дописать в спеку + коммит). ✓ (обещание владельцу «не забудь»).
**Гэп честно:** «канал решения владельца» (П3) в плане не строит нового модуля — опирается на живые escape-grant (`escapeGrantOpen`) + `applyWaivers`. Если при реализации окажется, что маппинг «вариант карточки → escape/вейвер» требует кода — добавить Task 8a (тонкая привязка). Это отмечено, не placeholder.
**Плейсхолдеры:** код-шаги несут реальный код; «read-then-confirm» шаги (Task 6 Step 1) — настоящие шаги проверки живого модуля перед правкой, не заглушки.
**Согласованность имён:** `bumpMentorNoGo`/`MENTOR_ESCALATE_AFTER` (Task 2) используются в Tasks 6/8; `buildArbitrationCard`/`ARBITRATION_OPTIONS` (Task 3) — в Tasks 7/8; `parseNegotiationSection` (Task 4) — в Tasks 7/8; `formatJudgeObjection` (Task 5) — в Task 7; `formatMentorObjection` (Task 6) — в Task 8. Совпадают.
@@ -0,0 +1,86 @@
# Волна 1 — счётчик NO-GO наставника (inline, sealed-plan проба)
> **For agentic workers:** REQUIRED SUB-SKILL: inline-исполнение (субагенты `op:Task` запрещены машиной). Шаги — checkbox.
**Goal:** Собрать `mentor-nogo-counter.mjs` (счётчик NO-GO наставника, зеркало `bumpJudgeNoGo`) inline по запечатанному плану — и тем самым проверить, что цепь «якорная спека → печать → план → печать → стена открылась» реально работает на живом судье.
**Architecture:** Чистый модуль + тест, без правок существующих файлов. Это Волна 1 из серии; остальные модули фичи (карточка арбитража, парсер «Переговоры», форматтеры, эскалации) — отдельными волнами после проверки цепи.
**Tech Stack:** Node ESM, vitest tools-only (`npx vitest run --root app --config vitest.config.tools.mjs`).
**Опора на спеку (артефакт):** `2026-06-12-router-mentor-two-level-negotiation-design-v2.md`, якоря `{#testing}`, `{#components}`.
**Честная оговорка о хрупкости:** стена матчит каждое мутирующее действие со следующим шагом по порядку, точным совпадением. Любое отклонение (правка теста, лишний прогон) = действие вне плана → блок → пере-печать этой волны. Прогон vitest (проверка) делаю как наблюдение между шагами; если стена его не пустит — добавлю отдельным шагом и пере-печатаю.
```steps-json
[
{"op":"Write","object":"tools/mentor-nogo-counter.test.mjs","ref":"testing"},
{"op":"Write","object":"tools/mentor-nogo-counter.mjs","ref":"components"},
{"op":"Bash","object":"git add tools/mentor-nogo-counter.mjs tools/mentor-nogo-counter.test.mjs && git commit -m wave1-mentor-counter","ref":"components"}
]
```
---
## Шаги
### Шаг 1 — Write `tools/mentor-nogo-counter.test.mjs` (падающий тест)
```js
import { describe, it, expect } from 'vitest';
import { bumpMentorNoGo, MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs';
describe('bumpMentorNoGo', () => {
const mem = () => {
const store = {};
return {
readFileSync: (p) => { if (!(p in store)) { const e = new Error('no'); e.code = 'ENOENT'; throw e; } return store[p]; },
writeFileSync: (p, d) => { store[p] = d; },
mkdirSync: () => {},
};
};
it('инкремент на blocked, сброс на allow', () => {
const fsImpl = mem(); const dir = '/r'; const sessionId = 's1';
expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(1);
expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(2);
expect(bumpMentorNoGo({ sessionId, blocked: false, fsImpl, dir })).toBe(0);
expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(1);
});
it('порог эскалации = 3', () => { expect(MENTOR_ESCALATE_AFTER).toBe(3); });
it('небезопасный sessionId санитизируется (не падает)', () => {
const fsImpl = mem();
expect(bumpMentorNoGo({ sessionId: '../../x', blocked: true, fsImpl, dir: '/r' })).toBe(1);
});
});
```
### Шаг 2 — Write `tools/mentor-nogo-counter.mjs` (реализация, зеркало `bumpJudgeNoGo`)
```js
#!/usr/bin/env node
/**
* mentor-nogo-counter — счётчик подряд идущих NO-GO наставника (L1), зеркало
* bumpJudgeNoGo. blocked=true → +1; allow → сброс 0. Порог 3 → арбитраж (round-control C-12).
*/
import fsDefault from 'node:fs';
import { join } from 'node:path';
import { runtimeDir } from './enforce-hook-helpers.mjs';
export const MENTOR_ESCALATE_AFTER = 3;
export function bumpMentorNoGo({ sessionId, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
const path = join(dir, `mentor-nogo-${safe}.json`);
let count = 0;
try { count = (JSON.parse(fsImpl.readFileSync(path, 'utf8')).count) || 0; } catch { count = 0; }
const next = blocked ? count + 1 : 0;
try { fsImpl.mkdirSync(dir, { recursive: true }); fsImpl.writeFileSync(path, JSON.stringify({ count: next })); } catch { /* best-effort */ }
return next;
}
```
Между шагами 2 и 3 — прогон проверки (наблюдение): `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-nogo-counter.test.mjs` (ожидаю PASS).
### Шаг 3 — Bash коммит волны
`git add tools/mentor-nogo-counter.mjs tools/mentor-nogo-counter.test.mjs && git commit -m wave1-mentor-counter`
@@ -0,0 +1,69 @@
# Двухуровневые переговоры + арбитраж владельца (роутер-наставник) — дизайн (v2, якорный)
**Дата:** 2026-06-12. **Кодовая фраза:** «роутер-наставник».
**Статус:** ДИЗАЙН утверждён владельцем. v2 = тот же дизайн + явные якоря `{#id}` на разделах (нужно для печати артефакта: секции собираются только из якорей `## … {#id}`).
**Подход:** №1 — переиспользование живых кирпичей, TDD. **Ветка:** `main`.
**Первоисточник:** `2026-06-01-router-discipline-plan-anchor-design-notes.md` (D16/D23/D24/D30/D34/В5, R-COMM) + роадмап `2026-06-12-router-mentor-roadmap.md`.
---
## 1. Зачем (проблема) {#problem}
Двухуровневые переговоры с арбитражем:
- **L1 (контроллер ↔ наставник):** готовлю план → наставник даёт замечания → обсуждаем → договорились → к судье. Не договорились за 3 круга → эскалация владельцу.
- **L2 (судья):** замечание судьи → правлю → снова к наставнику → судье. До 3 раз. После 3 отказов → владельцу замечание судьи + моя позиция → владелец арбитр (моя позиция / судьи / своё).
- **Эскалация = арбитраж:** владельцу показывают обе стороны.
Не хватает 4 вещей: (1) контроллер видит замечания; (2) интерактивное обсуждение с наставником; (3) эскалация как арбитраж; (4) счётчики на обоих уровнях (живёт только судейский).
## 2. Три неизменных принципа {#principles}
- **П1.** Замечание доходит до контроллера **в тексте блок-сообщения хука** (read-deny не ослабляем).
- **П2.** Позиция контроллера живёт **в разделе «## Переговоры» план-файла** (видна владельцу, новый канал записи не нужен).
- **П3.** Решение владельца пишется в **защищённый журнал одобрений** (escape-grant / waivers), ворота читают его; со слов Claude не принимается.
- **R-COMM:** только по-русски, простыми словами, не вводить владельца в заблуждение.
## 3. Архитектура — две петли, два счётчика {#architecture}
L1 (контроллер↔наставник): круги, потолок 3 → арбитраж с наставником. L2 (контроллер↔судья): отказы судьи NO-GO, потолок 3 → арбитраж с судьёй. Связь: наставник GO → к судье; судья NO-GO → правка → снова к наставнику (счётчик L1 сбрасывается) → судье (L2 +1). Внешний пинг-понг ограничен потолком судьи (≤3). Счётчики независимы.
## 4. Поток L1 (контроллер ↔ наставник) {#l1-flow}
Пишу план (`writing-plans`, семя) → хук наставника выносит вердикт. GO → счётчик L1 сброшен, к судье. NO-GO → замечание в блок-сообщении → на круге: правлю план ИЛИ пишу «не согласен, потому что X» в раздел «Переговоры» → пере-запись → наставник читает весь план и пересматривает (+1 круг). Круги в `mentor-journal`. 3 круга без согласия → арбитраж. Счётчик L1 = managed-счётчик `round-control` + новый `bumpMentorNoGo` (зеркало `bumpJudgeNoGo`).
## 5. Поток L2 (судья) + вложенность {#l2-flow}
Наставник GO → судье (`enforce-judge-gate`). GO судьи → план запечатан, исполняю. NO-GO → замечание судьи в блок-сообщении → правлю → пере-запись возвращает к наставнику (новый круг L1) → наставник GO → снова судье (L2 +1, живой `bumpJudgeNoGo`). 3 отказа → арбитраж с судьёй. Активация L2 — Фаза 8 (тестируется при судье inert).
## 6. Арбитраж: карточка + канал решения {#arbitration}
Карточка (контроллер собирает, цитаты дословны): дословное замечание стороны (из блок-сообщения/журнала) + дословная позиция контроллера (из раздела «Переговоры») + нейтральная строка «что меняет каждый выбор» + аффорданс «объясни подробнее». Выбор владельца (`AskUserQuestion`, семя): держусь позиции / соглашаюсь / своё. Запись решения — через `escape-grant` (держусь/разрешаю) + `applyWaivers` (снятые возражения). После: держусь → ворота пропускают версию контроллера; соглашаюсь → переделываю, счётчик сброшен; своё → владелец вписывает обязательное.
## 7. Компоненты (всё в `tools/`, TDD) {#components}
Переиспользуем (бережно): `round-control.mjs` (счётчик L1 к живому циклу); `enforce-mentor-on-plan-write.mjs` (замечание в блок-сообщение, читает раздел «Переговоры»); `mentor-journal*.mjs` (круги + счётчик NO-GO); `enforce-judge-gate.mjs` (замечание судьи в блок-сообщение). Новое (малое, чистое): `mentor-nogo-counter.mjs` (зеркало `bumpJudgeNoGo`); `arbitration-card.mjs` (карточка); `negotiation-section.mjs` (парсер раздела «Переговоры»); `judge-objection-format.mjs` / `mentor-objection-format.mjs` (форматтеры). Канал решения — `escape-grant`/`applyWaivers` (живут).
## 8. Обработка сбоев (fail-closed) {#failures}
Битый счётчик → консервативно эскалация (как в `round-control`). Нет записи решения владельца → «не одобрено». LLM лёг (D27): код сверил с планом → пропуск; нужно суждение, а LLM нет → СТОП + эскалация; деньги кончились → громко владельцу; transient → тихий retry. Форматтеры fail-safe (мусор → пустая строка). Owner-facing — простой русский.
## 9. Границы {#boundaries}
Наставник живой — бережно, регрессия на каждом шаге. Судья inert в main → L2 тестируется без живого блока, активация Фаза 8 (шаг владельца). Read-deny и страж секретов не ослабляем. Субагенты (`op:Task`) запрещены машиной — исполнение inline.
## 10. Тестирование {#testing}
TDD. Регрессия tools-only: `npx vitest run --root app --config vitest.config.tools.mjs` (npx-кэш, vitest 4.1.8). Юнит-тесты: `arbitration-card` (дословность, 3 варианта, «что меняет», аффорданс); счётчики L1/L2 (сброс/потолок 3, битый → эскалация); канал решения (расход 1 раз, окно, отказ принять «решение» авторства Claude); хуки (замечание в блок-сообщении; раздел «Переговоры» прочитан); L2 при судье inert.
## 11. Открытые вопросы {#open-questions}
OВ-1 ручка видимости спора (В5 закрыт «итог+эскалации»). OВ-2 авто-одобрение волн (вне scope). OВ-3 калибровка наставника/судьи не-программистом (H1, отложено).
## 12. Известная брешь процесса: seed-write без ИИ-сторожа {#seed-gap}
Эта спека написана в условиях бреши, которую закрывает эпик. Запись файла-артефакта (Write) проходит без проверки наставником/судьёй — это **семя** (`supreme-gate` пропускает «печать артефакта», иначе bootstrap-круг). Но содержание семени в такой сессии не проходит ИИ-сторожа: судья судит на печати, наставник сторожит `plans/`. **Закрытие:** (1) Фаза 8 — живой судья на Гейте-1 (`extractGate1Product` судит `specs/`) проверяет содержание; (2) расширить наставника на `specs/`; (3) зажать семя (H4). **Предел (recursive-trust, L2):** запись первого артефакта невозможной не сделать; последний рубеж — владелец на видимом артефакте (D7/D17). **Урок сессии 12.06:** машина требует для inline-исполнения якорную спеку + исчерпывающий `steps-json` + отсутствие субагентов + нулевую итеративность — это ограничивает фиче-работу (находка для эпика).
## 13. Ссылки {#refs}
Замысел: `2026-06-01-router-discipline-plan-anchor-design-notes.md`. Роадмап: `2026-06-12-router-mentor-roadmap.md`. Живые модули: `round-control.mjs`, `enforce-mentor-on-plan-write.mjs`, `mentor-journal-store.mjs`, `enforce-judge-gate.mjs`, `judge-seal-channel.mjs`, `plan-lock.mjs`, `seal-orchestration.mjs`.
@@ -0,0 +1,165 @@
# Двухуровневые переговоры + арбитраж владельца (роутер-наставник) — дизайн
**Дата:** 2026-06-12
**Кодовая фраза:** «роутер-наставник».
**Статус:** ДИЗАЙН утверждён владельцем (секции 1–5 одобрены в брейнсторме 12.06). Реализация — после плана (`writing-plans`).
**Подход:** №1 — «вкрутить готовые кирпичи» (максимум переиспользования живых модулей, минимум нового кода, TDD).
**Первоисточник замысла:** заметки `2026-06-01-router-discipline-plan-anchor-design-notes.md` (D16/D23/D24/D30/D34/В5, R-COMM) + роадмап `2026-06-12-router-mentor-roadmap.md`.
**Ветка работ:** `main` (по требованию владельца — «только работаем на мэйне»).
---
## 1. Зачем (проблема)
Замысел владельца — двухуровневые переговоры с арбитражем:
- **Уровень 1 (контроллер ↔ наставник):** готовлю план → наставник даёт замечания → **обсуждаем** → договорились → наставник пропускает к судье. Не договорились за 3 круга → эскалация владельцу.
- **Уровень 2 (судья):** судья даёт замечание → правлю → снова к наставнику (обсуждение) → судье. До 3 раз. После 3 отказов судьи → приношу владельцу **замечание судьи + свою позицию** → владелец **арбитр** (моя позиция / судьи / своё).
- **Эскалация = арбитраж:** владельцу показывают **обе стороны**.
Кирпичи уже есть (судья пишет objections в `judge-verdicts.jsonl`; наставник — вердикт+reasoning в `mentor-journal`; `round-control.mjs` умеет `escalate-owner` после 3 кругов; 12.06 добавлены `bumpJudgeNoGo` + «ЭСКАЛАЦИЯ» после 3 + судья чтит escape). **Не хватает четырёх вещей:**
1. Контроллер **видит** замечания судьи/наставника (сейчас мешает runtime read-deny) → правит по ним.
2. **Интерактивное обсуждение** контроллер↔наставник (наставник сейчас «один выстрел»).
3. Эскалация как **арбитраж**: показать владельцу замечание + позицию контроллера + выбор.
4. **Счётчики на обоих уровнях** (наставник + судья); сейчас живьём считает только судья.
---
## 2. Три неизменных принципа (на всё)
- **П1. Замечание доходит до контроллера в тексте блок-сообщения хука.** Новый канал чтения к журналам (`~/.claude/runtime`) **не открываем** — read-deny не ослабляется. Замечание «едет» на самом блоке (как контроллер уже видит сообщения хуков вроде `supreme-gate`).
- **П2. Позиция контроллера в споре живёт в разделе «переговоры» план-файла.** Видна владельцу (совпадает с D7 «всё видно в плане»), новый канал записи в runtime не нужен.
- **П3. Решение владельца на арбитраже пишется в защищённый журнал одобрений**, ворота читают его (шаблон git-подтверждений `judge-seal-channel`/`askuser-decisions`). **Со слов Claude «хозяин разрешил» не принимается.**
- **R-COMM (на весь owner-facing текст):** только по-русски, простыми словами, но информативно; не вводить владельца в заблуждение, не обходить его.
---
## 3. Архитектура — две петли, два счётчика
| Уровень | Кто спорит | О чём | Счётчик | Тупик → |
|---|---|---|---|---|
| **L1** | контроллер ↔ **наставник** | хорош ли план (метод, шаги, специалисты) | круги L1, потолок 3 | арбитраж: замечание наставника vs позиция контроллера |
| **L2** | контроллер ↔ **судья** | можно ли план пропустить (сторож ворот) | отказы судьи (NO-GO), потолок 3 | арбитраж: замечание судьи vs позиция контроллера |
**Связь петель (вложенность):** наставник согласовал → план идёт судье. Судья отказал → правлю → **снова к наставнику** (свежий круг L1, счётчик L1 сбрасывается) → снова судье (L2 +1). Внешний пинг-понг ограничен потолком судьи (≤3), внутри каждого внешнего шага наставник ограничен ≤3. Худший случай ≤ 3×3 = 9 кругов наставника, затем гарантированная эскалация.
**Два независимых счётчика** (выбор владельца): у каждого свой потолок 3, свой арбитраж (с соответствующей стороной). L1-счётчик **сбрасывается**, когда отказ судьи возвращает к наставнику (новый свежий круг).
---
## 4. Поток L1 (контроллер ↔ наставник)
1. Контроллер пишет/правит план через `writing-plans` (семя — ворота всегда пропускают).
2. Запись плана дёргает хук наставника (`enforce-mentor-on-plan-write`). Наставник выносит вердикт + замечания.
3. **GO** → круг сошёлся, счётчик L1 сброшен, план уходит судье (L2).
4. **NO-GO** → текст замечания приходит контроллеру в блок-сообщении (П1). На одном круге контроллер делает **одно из двух** (гибрид):
- правит план под замечание, **или**
- пишет «не согласен, потому что X» в раздел «переговоры» плана (П2).
Пере-запись плана → наставник читает **весь** план (включая возражение) и пересматривает. Это +1 круг L1.
5. Круги нумеруются в `mentor-journal` (для оценщика и аудита; владельца кругами не заваливаем — В5).
6. **3 круга без согласия** → счётчик упёрся → **арбитраж владельцу** (раздел 6).
Счётчик L1 — это managed-счётчик `round-control` (`escalate-owner` при потолке 3), сейчас спящий: подключаем к живому циклу наставника + добавляем счётчик NO-GO наставника зеркалом живого `bumpJudgeNoGo`.
---
## 5. Поток L2 (судья) + вложенность
Наставник согласовал → план уходит судье (`enforce-judge-gate`). Судья — независимый сторож ворот: перепроверяет даже одобренное наставником (D30).
1. **GO судьи** → план «запечатан», контроллер исполняет; на каждом действии судья сверяет действие↔план (вторая роль судьи — надзор на исполнении, без изменений).
2. **NO-GO судьи** → текст замечания судьи приходит контроллеру в блок-сообщении (П1). Контроллер правит план → пере-запись плана **возвращает к наставнику** (новый круг L1, счётчик L1 сбрасывается) → наставник согласовал → снова судье. Это **L2 +1** (живой `bumpJudgeNoGo`).
3. **3 отказа судьи** → счётчик L2 упёрся → **арбитраж владельцу** (замечание судьи vs позиция контроллера).
«Снова к наставнику» = контроллер просто правит план-файл, хук наставника срабатывает сам. Отдельного механизма «вернуть к наставнику» строить не надо.
**Активация:** механику L2 строим и юнит-тестируем сейчас, но **живой она станет вместе с судьёй в Фазе 8** переезда М7 (в основном репо судья пока inert). Это активационный шаг владельца (регистрация в `.claude/settings.json`), не часть кода этой задачи.
---
## 6. Арбитраж: карточка + канал решения владельца
Арбитраж одинаков по форме для обоих уровней; меняется только сторона (наставник или судья).
**Сборка карточки** (контроллер собирает, но цитаты дословны):
- **Дословно** замечание стороны — из блок-сообщения/журнала (не пересказ).
- **Дословно** позиция контроллера — из раздела «переговоры» плана.
- Нейтральная строка **«что меняет каждый выбор»** простым русским.
- Аффорданс **«объясни подробнее/проще»** — контроллер даёт развёрнутое нейтральное пояснение, потом владелец решает.
**Выбор владельца** (через `AskUserQuestion` — семя, всегда разрешено): держусь позиции контроллера / соглашаюсь с замечанием / своё решение.
**Запись решения** — в защищённый журнал одобрений (шаблон `judge-seal-channel`/`askuser-decisions`): страж-хук пишет выбор владельца в защищённый файл; ворота читают точное совпадение, **одноразовый расход + окно времени** (как git-подтверждения). Со слов Claude не принимается (П3).
**После решения:**
- «держусь позиции» → ворота пропускают версию контроллера (сторона переспорена владельцем);
- «соглашаюсь с замечанием» → контроллер переделывает под замечание, круг/счётчик сбрасывается;
- «своё» → владелец вписывает своё, оно становится обязательным.
---
## 7. Компоненты (всё в `tools/`, по TDD)
**Переиспользуем (правим осторожно — наставник живой):**
- `round-control.mjs` — подключить спящий managed-счётчик L1 к живому циклу наставника.
- `enforce-mentor-on-plan-write.mjs` — выдавать замечание наставника **в тексте блок-сообщения**; кормить счётчик L1; читать раздел «переговоры» плана.
- `mentor-journal*.mjs` — нумерованные круги переговоров + **счётчик NO-GO наставника** (зеркало живого `bumpJudgeNoGo`).
- `enforce-judge-gate.mjs` — выдавать замечание судьи в блок-сообщении (счётчик L2 `bumpJudgeNoGo` уже живой).
**Новое (маленькое, чистое):**
- `arbitration-card.mjs` — сборщик карточки: дословные цитаты обеих сторон + строка «что меняет выбор» + аффорданс «объясни подробнее». Чистая функция.
- **Канал решения владельца** — переиспользуем шаблон `judge-seal-channel`/`askuser-decisions`: запись выбора в защищённый файл, чтение воротами с одноразовым расходом + окном времени.
- **Соглашение «раздел переговоры» в план-файле** — формат, где живёт позиция контроллера; парсер в хуке наставника.
---
## 8. Обработка сбоев (fail-closed, R-COMM)
- Битый/нечитаемый счётчик → консервативно **эскалируем владельцу** (уже так в `round-control`: не-целое ≥0 → эскалация).
- Нет записи решения владельца в журнале → «не одобрено» (не пропускаем).
- LLM-слой лёг (D27): код однозначно сверил действие с одобренным планом → пропуск; нужно суждение, а LLM нет → **СТОП + эскалация**; **деньги кончились → громко кричим владельцу**; transient (сеть/таймаут) → тихий retry. (D27 в основном уже реализован — здесь только не нарушить.)
- Весь owner-facing текст — простым русским (R-COMM).
---
## 9. Что НЕ ломаем / границы
- Наставник живой (Этап 4, aitunnel) — трогаем бережно, регрессия на каждом шаге.
- Судья inert в main → L2 строим и юнит-тестируем, **живёт с Фазой 8**; активация (регистрация в settings.json) — шаг владельца.
- Read-deny **не ослабляем** (П1). Write в runtime контроллером **не открываем** (П2 — позиция в план-файле).
- Страж секретов (read-protection, D18) работает всегда, поверх всего.
---
## 10. Тестирование
TDD. Регрессия tools-only — **только** `npx vitest run --root app --config vitest.config.tools.mjs` (npx-кэш, vitest 4.1.8; запуск через `node app/node_modules/vitest/...` падает на out-of-root тестах — баг пина 4.1.5, см. роадмап).
Юнит-тесты на каждый модуль:
- `arbitration-card`: дословность цитат сохранена; три варианта выбора; нейтральная строка «что меняет выбор»; аффорданс «объясни подробнее».
- счётчики L1/L2: сброс/потолок 3; L1 сбрасывается при возврате от судьи; битый счётчик → эскалация.
- канал решения владельца: расход один раз; окно времени; **отказ принять «решение» авторства Claude** (только запись стража).
- хук наставника: замечание попадает в блок-сообщение; раздел «переговоры» прочитан и учтён.
- L2-механика проверяется при судье inert (без живого блока).
---
## 11. Открытые вопросы (на будущее, не блокируют реализацию)
- **OВ-1. Ручка видимости спора (D16, В5 закрыт в пользу «итог+эскалации»).** Кругов в журнале достаточно; при желании владельца — режим «показывать каждый круг» добавляется отдельной ручкой.
- **OВ-2. Авто-одобрение волн** (D17 смягчение «ослабим потом») — вне scope этой задачи.
- **OВ-3. Калибровка наставника/судьи не-программистом** (H1) — отдельная давняя тема, отложена.
---
## 12. Ссылки
- Замысел и решения: `docs/superpowers/specs/2026-06-01-router-discipline-plan-anchor-design-notes.md` (D16/D23/D24/D30/D34/D35/D36/В5/В6, R-COMM).
- Состояние эпика: `docs/superpowers/specs/2026-06-12-router-mentor-roadmap.md`.
- Живые модули: `tools/round-control.mjs`, `tools/enforce-mentor-on-plan-write.mjs`, `tools/mentor-journal*.mjs`, `tools/enforce-judge-gate.mjs`, `tools/judge-seal-channel.mjs`.