docs(mentor): two-level negotiation spec-v2 (anchored) + plans
This commit is contained in:
+11
-11
@@ -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`.
|
||||
Reference in New Issue
Block a user