# Redesign согласования L1→L2 — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:test-driven-development (inline).
> Стена СНЯТА (maintenance) — реализация без цикла печати. Шаги — чекбоксы `- [ ]`.
> Спека: `docs/superpowers/specs/2026-06-13-l1-l2-negotiation-redesign-design.md`.

**Goal:** Пересобрать согласование спеки/плана так, чтобы шло «наставник (L1) → судья (L2) →
печать судьи», замечания доходили до контроллера, счётчики были per-task, и план чисто завершался.

**Architecture:** Способ B (два PostToolUse-хука по порядку наставник→судья; судья печатает,
fail-safe без одобрения наставника). Главное — рабочий канал доставки замечаний. Плюс per-task
счётчики и чистое завершение плана в стене.

**Tech Stack:** Node ESM (.mjs), vitest (tools-config). Регрессия:
`npx vitest run --root app --config vitest.config.tools.mjs` (база ~3829).

**Порядок фаз (Фаза 0 — ГЕЙТ):** 0 smoke-канал → 1 канал-замечаний → 2 порядок/роли (B) →
3 спека тем же сценарием → 4 счётчики per-task → 5 чистое завершение → 6 регрессия+settings+смоук.

---

## ФАЗА 0 — Smoke: доходит ли PostToolUse-фидбэк до контроллера (ГЕЙТ)

Без этого весь канал замечаний бессмыслен. Делаем ПЕРВЫМ, эмпирически.

### Task 0.1: Временный PostToolUse-хук, отдающий маркер через exit-2

**Files:**

- Create: `tools/smoke-objection-channel.mjs`

- [ ] **Step 1: Написать хук.**

```js
#!/usr/bin/env node
// Временный smoke: PostToolUse-хук выводит известный маркер в stderr и выходит exit 2.
// Цель — проверить, ДОХОДИТ ли этот текст до контроллера (Claude) как feedback.
import { readStdin, parseEventJson } from './enforce-hook-helpers.mjs';
async function main() {
  try { await parseEventJson(await readStdin()); } catch { /* ignore */ }
  process.stderr.write('SMOKE-OBJECTION-CHANNEL-MARKER-7f3a: замечание дошло до контроллера\n');
  process.exit(2);
}
import { fileURLToPath } from 'node:url';
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();
```

- [ ] **Step 2: Владелец временно регистрирует хук** в `.claude/settings.json` (maintenance) как
  PostToolUse matcher `Write|Edit` → `node tools/smoke-objection-channel.mjs`, рестарт VS Code.
  *(Шаг владельца — Claude settings.json в maintenance не трогает без явной просьбы.)*

- [ ] **Step 3: Контроллер делает любой Edit/Write** (например, правит scratch-файл).
  **Ожидание:** в контексте контроллера появляется строка с `SMOKE-OBJECTION-CHANNEL-MARKER-7f3a`.
  - **ДОШЛО** → канал работает (exit-2 PostToolUse-фидбэк виден). Идём в Фазу 1.
  - **НЕ ДОШЛО** → СТОП. Канал другой (варианты: JSON-output хука `{"decision":..,"reason":..}`;
    или surface через отдельный allow-лог; или Stop-хук-сводка). Пересматриваем спеку §4 до стройки.

- [ ] **Step 4: Владелец снимает временный хук** из settings.json. Файл `smoke-objection-channel.mjs`
  удалить (throwaway) либо оставить до конца redesign и удалить в Фазе 6.

> Примечание: в этой сессии feedback PreToolUse (блок стены) и Stop-хука (coverage) ДОХОДИЛ до
> контроллера — это сильный довод, что и PostToolUse exit-2 дойдёт. Smoke это подтверждает фактом.

---

## ФАЗА 1 — Канал замечаний: наставник и судья отдают ПОЛНЫЙ текст контроллеру

Опирается на подтверждённый Фазой 0 механизм (ниже — для exit-2; если smoke показал иной канал,
заменить способ доставки, контракт функций сохранить).

### Task 1.1: Чистая функция доставки замечания

**Files:**

- Create: `tools/objection-delivery.mjs`
- Test: `tools/objection-delivery.test.mjs`

- [ ] **Step 1: Падающий тест.**

```js
import { describe, it, expect } from 'vitest';
import { buildObjectionFeedback } from './objection-delivery.mjs';

describe('buildObjectionFeedback', () => {
  it('собирает полный текст замечания с пометкой стороны', () => {
    const out = buildObjectionFeedback({ side: 'mentor', text: 'пункт 2 плохой' });
    expect(out).toContain('наставник');
    expect(out).toContain('пункт 2 плохой');
  });
  it('пустой текст → безопасная заглушка, не пусто', () => {
    expect(buildObjectionFeedback({ side: 'judge', text: '' })).toMatch(/судья/);
  });
});
```

- [ ] **Step 2: Прогон — FAIL** (`buildObjectionFeedback is not a function`).
  Run: `npx vitest run --root app --config vitest.config.tools.mjs objection-delivery`

- [ ] **Step 3: Реализация.**

```js
#!/usr/bin/env node
/** Сборка текста замечания для доставки контроллеру (полный текст, не обрезаем). */
export function buildObjectionFeedback({ side, text } = {}) {
  const who = side === 'judge' ? 'судья' : 'наставник';
  const body = (typeof text === 'string' && text.trim()) ? text.trim() : '(текст замечания пуст)';
  return `[${who}] замечание (нужно учесть и переписать):\n${body}`;
}
```

- [ ] **Step 4: Прогон — PASS.** Команда та же.

### Task 1.2: Хук наставника отдаёт замечание контроллеру (не в пустоту)

**Files:**

- Modify: `tools/enforce-mentor-on-plan-write.mjs` (main: на NO-GO — exit-2 с полным текстом
  через `buildObjectionFeedback`, вместо `console.error`+`block:false`)
- Test: `tools/enforce-mentor-on-plan-write.test.mjs`

- [ ] **Step 1: Падающий тест** — на NO-GO main отдаёт объект-решение `{block:true, message}` с
  полным текстом замечания (mock'аем runMentorOnPlanWrite → wired:true, ok:false, verdict с текстом).
  *(точная форма — по текущему контракту exitDecision; тест проверяет, что message содержит текст замечания)*
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Реализация** — заменить `console.error(msg)` + всегда `block:false` на: если NO-GO
  (wired && !ok) → `exitDecision({ block: true, message: buildObjectionFeedback({side:'mentor', text: formatMentorObjection(res)}) })`. На карточке (3-й NO-GO) — message = карточка. На GO/прочем — `block:false`.
- [ ] **Step 4: PASS** + не сломаны соседние тесты хука.

### Task 1.3: Хук судьи отдаёт замечание контроллеру

**Files:**

- Modify: `tools/enforce-judge-gate.mjs` (main: на NO-GO — exit-2 с полным текстом судьи)
- Test: `tools/enforce-judge-gate.test.mjs`

- [ ] Аналогично 1.2: на реальном NO-GO судьи main отдаёт `{block:true, message: buildObjectionFeedback({side:'judge', text: formatJudgeObjection(verdict)})}`; на 3-м — карточка. Тест на доставку текста. FAIL → реализация → PASS.

---

## ФАЗА 2 — Порядок и роли (способ B): наставник первым, судья печатает

### Task 2.1: Стор «наставник одобрил план» (зеркало judge-go-store)

**Files:**

- Create: `tools/mentor-go-store.mjs`
- Test: `tools/mentor-go-store.test.mjs`

- [ ] **Step 1: Падающий тест** — `buildMentorGo({planHash, key})` подписан; `mentorGoValidFor(rec, {planHash, key})` true только для совпадающего plan_hash и валидной подписи; чужой/битый → false.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Реализация** — зеркало `judge-go-store.mjs`: домен `MENTOR_GO` в `receipt-sign.mjs`
  (добавить), `persistMentorGo`/`loadMentorGo`/`buildMentorGo`/`mentorGoValidFor`.
- [ ] **Step 4: PASS.**

### Task 2.2: Наставник записывает mentor-GO и НЕ печатает

**Files:**

- Modify: `tools/enforce-mentor-on-plan-write.mjs` (убрать `postSealPlan`; на GO — `persistMentorGo`)
- Test: тот же

- [ ] Тест: на mentor-GO main вызывает `persistMentorGo` с plan_hash; `postSealPlan` НЕ вызывается
  (наставник больше не печатает). FAIL → реализация (удалить блок печати наставника, добавить запись
  mentor-GO на wired GO) → PASS.

### Task 2.3: Судья — PostToolUse, fail-safe, сам печатает

**Files:**

- Modify: `tools/enforce-judge-gate.mjs` (gate2: судить/печатать только при валидном mentor-GO;
  на GO — `sealPlan` + персист `frozen-plan` САМ судья; перенос регистрации в Post — Фаза 6/settings)
- Test: тот же

- [ ] **Step 1: Падающий тесты:**
  - нет валидного mentor-GO для plan_hash → судья **молчит** (skip, без вызова LLM, без печати);
  - есть mentor-GO + судья GO → `frozen-plan` появляется (судья запечатал);
  - судья судит, не получая вердикт наставника в промпте (проверка независимости — промпт не
    содержит mentor-текста).
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Реализация** — в judge-gate перед судом: `loadMentorGo` + `mentorGoValidFor(plan_hash)`;
  нет → `{skip:'no_mentor_go'}` (молчит). Есть → судить (промпт = только план) → на wired GO:
  `freeze-gate` (verity/VA-9 по verified-context) → `sealPlan` → `saveFrozenPlan` (САМ судья). Убрать
  отложенную печать через наставника (deadlock-fix больше не нужен — порядок правильный).
- [ ] **Step 4: PASS.**

---

## ФАЗА 3 — Спека идёт тем же сценарием (наставник→судья)

### Task 3.1: Наставник смотрит и спеку (gate1), не только план

**Files:**

- Modify: `tools/enforce-mentor-on-plan-write.mjs` (matcher/детект: PLAN_PATH_RE ИЛИ SPEC_PATH_RE)
- Test: тот же

- [ ] Тест: запись спеки (`specs/*.md`) тоже будит наставника → вердикт; на GO — mentor-GO для
  артефакта спеки. FAIL → расширить детект на спеку → PASS.

### Task 3.2: Судья печатает артефакт спеки только при mentor-GO

**Files:**

- Modify: `tools/enforce-judge-gate.mjs` (gate1: тот же fail-safe + судья печатает артефакт)
- Test: тот же

- [ ] Тест: спека + mentor-GO + судья GO → `frozen-artifact` появляется (судья запечатал);
  без mentor-GO → судья молчит. FAIL → реализация → PASS.

---

## ФАЗА 4 — Счётчики per-task (стэк спека+план)

### Task 4.1: Счётчик наставника по task-id

**Files:**

- Modify: `tools/mentor-nogo-counter.mjs` (`bumpMentorNoGo` ключ — task-id, не sessionId)
- Modify: `tools/enforce-mentor-on-plan-write.mjs` (передавать task-id из `deriveTaskId`)
- Test: `tools/mentor-nogo-counter.test.mjs`

- [ ] Тест: два разных task-id в одной сессии → независимые счётчики (не смешиваются); сброс на GO
  только своего task-id. FAIL → ключ файла `mentor-nogo-<taskId>.json` → PASS.

### Task 4.2: Счётчик судьи по task-id

**Files:**

- Modify: `tools/enforce-judge-gate.mjs` (`bumpJudgeNoGo` ключ — task-id)
- Test: `tools/enforce-judge-gate.test.mjs`

- [ ] Аналогично 4.1 для судьи. FAIL → реализация → PASS.

---

## ФАЗА 5 — Чистое завершение плана (выход в разговорный)

### Task 5.1: Стена снимает печать после последнего шага

**Files:**

- Modify: `tools/plan-lock.mjs` (добавить `removeFrozenPlan({sessionId, runtimeDir})` — unlink)
- Modify: `tools/enforce-supreme-gate.mjs` (после allow последнего шага ИЛИ при «план исчерпан» —
  снять печать → вернуться в разговорный, вместо вечного блока «план исчерпан»)
- Test: `tools/enforce-supreme-gate.test.mjs` + `tools/plan-lock.test.mjs`

- [ ] **Step 1: Падающие тесты:**
  - `removeFrozenPlan` удаляет файл (mock fs);
  - после выполнения последнего шага плана `frozen-plan` снимается → следующее действие в
    разговорном режиме (decideMode → conversational, не «план исчерпан»).
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Реализация** — в `runGate`/`decideMode`: когда `advanceTo` уводит указатель за
  последний лист (план выполнен) → вызвать `removeFrozenPlan` (через инъекцию I/O) → последующий
  decideMode видит `!frozenPlan` → conversational. Граница: снятие — best-effort, не ломает allow.
- [ ] **Step 4: PASS.**

### Task 5.2: Досрочное завершение владельцем («готово»)

**Files:**

- Modify: `tools/enforce-supreme-gate.mjs` (owner-escape «готово»/abandon → снять печать)
- Test: тот же

- [ ] Тест: при открытом owner-escape «завершить план» → `removeFrozenPlan` + conversational.
  FAIL → реализация (переиспользовать escape-grant как сигнал завершения) → PASS.

---

## ФАЗА 6 — Регрессия + settings + сквозной смоук

### Task 6.1: Полная регрессия GREEN

- [ ] Run: `npx vitest run --root app --config vitest.config.tools.mjs`
  Expected: весь набор зелёный (база ~3829 + новые тесты), 0 регрессий. RED → systematic-debugging.

### Task 6.2: Владелец обновляет settings.json (возврат стены)

- [ ] Хук судьи плана/спеки → **PostToolUse, ПОСЛЕ наставника** (порядок в массиве); снять старую
  Pre-регистрацию судьи. Зарегистрировать наставника + судью + (опц.) clean-exit. Полный рестарт VS Code.
  *(Шаг владельца. Claude даёт точный фрагмент settings.json.)*

### Task 6.3: Сквозной смоук под стеной

- [ ] Под поднятой стеной: пишу спеку → наставник→судья→печать; пишу план → наставник (замечание
  ДОХОДИТ до меня!) → правлю → наставник GO → судья GO → печать; Edit по шагу проходит; последний
  шаг → стена сама вернулась в разговор. Зафиксировать результат в роадмап/протокол.

---

## Self-Review

**Spec coverage:** §2 проблема → Фазы 1-2/5; §3 поток → Фазы 1-2; §4 канал → Фаза 0-1; §5 способ B
→ Фаза 2; §6 re-plan → существующий механизм (тест в 6.3); §6.5 чистое завершение → Фаза 5; §7
счётчики per-task → Фаза 4; §8 спека+план → Фаза 3; §10 максимум тестов → каждая фаза TDD. ✅
**Placeholders:** код есть в Task 0.1/1.1/смоках; в хук-тасках — точные файлы + контракт + тест-намерение
(точный код хуков рождается TDD на текущем коде, не выдумываем заранее). Это осознанно для крупного
redesign'а: критичные ранние таски (0/1) — полный код; хук-правки — контракт+тесты.
**Type consistency:** `buildObjectionFeedback({side,text})→string`; `mentorGoValidFor(rec,{planHash,key})→bool`;
`removeFrozenPlan({sessionId,runtimeDir})`. Имена согласованы с `judge-go-store`/`plan-lock`.

**Главный риск/гейт:** Фаза 0. Если канал не дойдёт — стоп и пересмотр §4 спеки.
