diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index a912d5e4..f2ceccb4 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -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 diff --git a/docs/superpowers/plans/2026-06-12-router-mentor-two-level-negotiation.md b/docs/superpowers/plans/2026-06-12-router-mentor-two-level-negotiation.md new file mode 100644 index 00000000..3bd01f7b --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-router-mentor-two-level-negotiation.md @@ -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) " +``` + +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) " +``` + +--- + +### 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) " +``` + +--- + +### 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) " +``` + +--- + +### 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) " +``` + +--- + +### 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) " +``` + +--- + +### 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) " +``` + +--- + +### 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) " +``` + +--- + +### 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) " +``` + +- [ ] **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. Совпадают. diff --git a/docs/superpowers/plans/2026-06-12-router-mentor-wave1-mentor-counter.md b/docs/superpowers/plans/2026-06-12-router-mentor-wave1-mentor-counter.md new file mode 100644 index 00000000..ad4eb977 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-router-mentor-wave1-mentor-counter.md @@ -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` diff --git a/docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design-v2.md b/docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design-v2.md new file mode 100644 index 00000000..44f306a0 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design-v2.md @@ -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`. diff --git a/docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md b/docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md new file mode 100644 index 00000000..7732bf11 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md @@ -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`.