# Видимость вердиктов всех участников стены — Implementation Plan

> **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:** Контроллер видит подписанный вердикт каждого участника стены (роутер + наставник + судья) с деталями в начале хода, а не один безымянный `✅ GO — опечатано`.

**Architecture:** Вариант 1 (одобрен владельцем). Новый чистый модуль `verdict-surface-detail.mjs` форматирует подписанные строки; результат роутера и номер круга протягиваются наружу из `on-plan-write.mjs`; в трёх точках вердикта аддитивно вызывается `appendVerdictOutcome`. Surface-хранилище и UserPromptSubmit-хук не трогаются (многострочность уже есть). Всё всплытие — best-effort/fail-open, на решения гейтов не влияет.

**Tech Stack:** Node ESM (`.mjs`), vitest (tools-сюита: `vitest.config.tools.mjs`).

> Этот план **уточняет** спеку 2026-06-17-verdict-surface-all-participants-design.md: вместо «2 файла» — **3** discipline-source файла (`on-plan-write.mjs` тоже), т.к. результат роутера и `round` зарыты в `on-plan-write.mjs` и не возвращались. Живой роутер отдаёт рекомендованную цепочку навыков (не «уверенность 0.9»). Судья строки `[круг N]` не несёт (отдельный процесс) — осознанный объём v1.

---

## File Structure

- **Create** `tools/verdict-surface-detail.mjs` — 3 тотальных строителя строк (роутер/наставник/судья). Чистый, без I/O.
- **Create** `tools/verdict-surface-detail.test.mjs` — unit-тесты всех веток.
- **Modify (discipline-source, терминал+коммит)** `tools/on-plan-write.mjs` — вернуть `router` + `round`.
- **Modify (discipline-source, терминал+коммит)** `tools/enforce-mentor-on-plan-write.mjs` — протянуть `router/round`; всплыть строки роутера и наставника.
- **Modify (discipline-source, терминал+коммит)** `tools/enforce-judge-gate.mjs` — всплыть строку судьи.
- **Delete** `tools/wall-verdicts-demo.mjs` — мусор из фазы выяснения задачи.

**Правка discipline-source (F-K):** файлы `on-plan-write.mjs` / `enforce-*.mjs` под стеной через Write не возьмутся (`normative §6`). Каждый «Modify»-таск исполняется в **терминале** (node string-replace по якорям + бэкап `.bak` + verify diff), затем коммит. Новые файлы (`verdict-surface-detail*`) — обычным Write.

---

### Task 1: Чистый модуль строителей + тесты

**Files:**
- Create: `tools/verdict-surface-detail.mjs`
- Test: `tools/verdict-surface-detail.test.mjs`

- [ ] **Step 1: Написать падающий тест**

```js
import { describe, it, expect } from 'vitest';
import { buildRouterSurface, buildMentorSurface, buildJudgeSurface } from './verdict-surface-detail.mjs';

describe('buildRouterSurface', () => {
  it('рекомендация + круг', () => expect(buildRouterSurface({ recommendedChain: ['billing-audit'], declared: ['tdd'] }, { round: 2 }))
    .toBe('🧭 РОУТЕР [круг 2]: рекомендует [billing-audit] · объявлено [tdd]'));
  it('classify недоступен (null chain)', () => expect(buildRouterSurface({ recommendedChain: null, declared: [] }, { round: 1 }))
    .toBe('🧭 РОУТЕР [круг 1]: скил-сверка пропущена (classify недоступен/сбой)'));
  it('мусор → пусто', () => { expect(buildRouterSurface(null)).toBe(''); expect(buildRouterSurface(42)).toBe(''); });
});

describe('buildMentorSurface', () => {
  it('GO с деталью', () => expect(buildMentorSurface({ kind: 'go' }, { round: 1, verdict: { reasoning: 'связно', recommendation: 'назвать тест' } }))
    .toBe('🧑‍🏫 НАСТАВНИК [круг 1]: GO · reasoning: связно · recommendation: назвать тест'));
  it('NO-GO с текстом', () => expect(buildMentorSurface({ kind: 'nogo', text: 'переделай D2' }, { round: 3 }))
    .toBe('🧑‍🏫 НАСТАВНИК [круг 3]: NO-GO · переделай D2'));
  it('degraded', () => expect(buildMentorSurface({ kind: 'degraded', reason: 'нет ключа' }, {}))
    .toBe('🧑‍🏫 НАСТАВНИК: ⚠ не дозвонился (нет ключа)'));
  it('skip', () => expect(buildMentorSurface({ kind: 'skip', reason: 'не судится' }, {}))
    .toBe('🧑‍🏫 НАСТАВНИК: ⏭ пропуск (не судится)'));
  it('мусор → пусто', () => { expect(buildMentorSurface(null)).toBe(''); expect(buildMentorSurface({ kind: 'wat' }, {})).toBe(''); });
});

describe('buildJudgeSurface', () => {
  it('GO', () => expect(buildJudgeSurface({ kind: 'go' }, { gate: 'gate1' })).toBe('⚖️ СУДЬЯ gate1: GO'));
  it('NO-GO с текстом', () => expect(buildJudgeSurface({ kind: 'nogo', text: 'completeness пуст' }, { gate: 'gate2' }))
    .toBe('⚖️ СУДЬЯ gate2: NO-GO · completeness пуст'));
  it('degraded', () => expect(buildJudgeSurface({ kind: 'degraded', reason: 'нет ключа судьи' }, { gate: 'gate1' }))
    .toBe('⚖️ СУДЬЯ gate1: ⚠ не дозвонился (нет ключа судьи)'));
  it('skip', () => expect(buildJudgeSurface({ kind: 'skip', reason: 'не судится' }, { gate: 'gate1' }))
    .toBe('⚖️ СУДЬЯ gate1: ⏭ пропуск (не судится)'));
  it('мусор → пусто', () => { expect(buildJudgeSurface(null, {})).toBe(''); expect(buildJudgeSurface({ kind: 'x' }, {})).toBe(''); });
});
```

- [ ] **Step 2: Прогнать — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/verdict-surface-detail.test.mjs`
Expected: FAIL — «Failed to resolve import './verdict-surface-detail.mjs'».

- [ ] **Step 3: Реализовать модуль**

```js
#!/usr/bin/env node
/** verdict-surface-detail — подписанные строки вердиктов на участника (роутер/наставник/судья).
 *  Тотальны: мусор/null → ''. Без побочных эффектов. Видимость, не гейт. */

export function buildRouterSurface(router, { round = null } = {}) {
  if (!router || typeof router !== 'object') return '';
  const r = round ? ` [круг ${round}]` : '';
  const chain = Array.isArray(router.recommendedChain) ? router.recommendedChain : null;
  const declared = Array.isArray(router.declared) ? router.declared : [];
  if (!chain) return `🧭 РОУТЕР${r}: скил-сверка пропущена (classify недоступен/сбой)`;
  return `🧭 РОУТЕР${r}: рекомендует [${chain.join(', ')}] · объявлено [${declared.join(', ')}]`;
}

export function buildMentorSurface(kindObj, { round = null, verdict = null } = {}) {
  if (!kindObj || typeof kindObj !== 'object') return '';
  const r = round ? ` [круг ${round}]` : '';
  const k = String(kindObj.kind || '');
  if (k === 'go') {
    const reasoning = (verdict && typeof verdict.reasoning === 'string' && verdict.reasoning.trim()) ? verdict.reasoning.trim() : '—';
    const rec = (verdict && typeof verdict.recommendation === 'string' && verdict.recommendation.trim()) ? verdict.recommendation.trim() : '—';
    return `🧑‍🏫 НАСТАВНИК${r}: GO · reasoning: ${reasoning} · recommendation: ${rec}`;
  }
  if (k === 'nogo') {
    const t = (typeof kindObj.text === 'string' && kindObj.text.trim()) ? kindObj.text.trim() : '(без текста)';
    return `🧑‍🏫 НАСТАВНИК${r}: NO-GO · ${t}`;
  }
  if (k === 'degraded') {
    const reason = (typeof kindObj.reason === 'string' && kindObj.reason.trim()) ? kindObj.reason.trim() : 'причина неизвестна';
    return `🧑‍🏫 НАСТАВНИК${r}: ⚠ не дозвонился (${reason})`;
  }
  if (k === 'skip') {
    const reason = (typeof kindObj.reason === 'string' && kindObj.reason.trim()) ? kindObj.reason.trim() : 'причина неизвестна';
    return `🧑‍🏫 НАСТАВНИК${r}: ⏭ пропуск (${reason})`;
  }
  return '';
}

export function buildJudgeSurface(outcome, { gate = '' } = {}) {
  if (!outcome || typeof outcome !== 'object') return '';
  const g = gate ? ` ${gate}` : '';
  const k = String(outcome.kind || '');
  if (k === 'go') return `⚖️ СУДЬЯ${g}: GO`;
  if (k === 'nogo') {
    const t = (typeof outcome.text === 'string' && outcome.text.trim()) ? outcome.text.trim() : '(без текста)';
    return `⚖️ СУДЬЯ${g}: NO-GO · ${t}`;
  }
  if (k === 'degraded') {
    const reason = (typeof outcome.reason === 'string' && outcome.reason.trim()) ? outcome.reason.trim() : 'причина неизвестна';
    return `⚖️ СУДЬЯ${g}: ⚠ не дозвонился (${reason})`;
  }
  if (k === 'skip') {
    const reason = (typeof outcome.reason === 'string' && outcome.reason.trim()) ? outcome.reason.trim() : 'причина неизвестна';
    return `⚖️ СУДЬЯ${g}: ⏭ пропуск (${reason})`;
  }
  return '';
}
```

- [ ] **Step 4: Прогнать — убедиться, что зелено**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/verdict-surface-detail.test.mjs`
Expected: PASS (все describe зелёные).

- [ ] **Step 5: Коммит** (под стеной — через escape владельца ИЛИ его терминал; см. «Коммит» в router-mentor-wall-GUIDE)

```bash
git add tools/verdict-surface-detail.mjs tools/verdict-surface-detail.test.mjs
git commit -m "feat(wall): чистый модуль подписанных строк вердиктов участников"
```

---

### Task 2: Вернуть router + round из on-plan-write.mjs (discipline-source, терминал)

**Files:**
- Modify: `tools/on-plan-write.mjs:74-87`

- [ ] **Step 1: Бэкап**

Run: `cp tools/on-plan-write.mjs tools/on-plan-write.mjs.bak`

- [ ] **Step 2: Поднять `round` в область функции** — заменить точную подстроку

Было (строки 74-77):
```js
  let journal = null;
  let journalOk = false;
  try {
    const round = roundCount(journalEntries, taskId) + 1;
```
Стало:
```js
  const round = roundCount(journalEntries, taskId) + 1;
  let journal = null;
  let journalOk = false;
  try {
```

- [ ] **Step 3: Добавить `router` + `round` в return** — заменить строку 87

Было:
```js
  return { taskId, taskIdPersisted, ok: r.ok, wired: r.wired, verdict: r.verdict ?? null, reason: r.reason, journal, journalOk };
```
Стало:
```js
  return { taskId, taskIdPersisted, ok: r.ok, wired: r.wired, verdict: r.verdict ?? null, reason: r.reason, journal, journalOk, router: { recommendedChain, declared: declaredSkills }, round };
```

- [ ] **Step 4: Verify diff и регрессия**

Run: `git diff tools/on-plan-write.mjs` → ровно две правки выше.
Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: PASS, 0 регрессий (существующие on-plan-write тесты не завязаны на форму return сверх ok/wired/verdict).

- [ ] **Step 5: Удалить бэкап и коммит**

```bash
rm tools/on-plan-write.mjs.bak
git add tools/on-plan-write.mjs
git commit -m "feat(wall): on-plan-write возвращает рекомендацию роутера и номер круга"
```

---

### Task 3: Протянуть router/round через runMentorOnPlanWrite (discipline-source, терминал)

**Files:**
- Modify: `tools/enforce-mentor-on-plan-write.mjs:189`

- [ ] **Step 1: Бэкап**

Run: `cp tools/enforce-mentor-on-plan-write.mjs tools/enforce-mentor-on-plan-write.mjs.bak`

- [ ] **Step 2: Заменить return runMentorOnPlanWrite (строка 189)**

Было:
```js
  return { ran: true, ok: r.ok, wired: r.wired, reason: r.reason, taskId: r.taskId, planHash, verdict: r.verdict };
```
Стало:
```js
  return { ran: true, ok: r.ok, wired: r.wired, reason: r.reason, taskId: r.taskId, planHash, verdict: r.verdict, router: r.router, round: r.round };
```

- [ ] **Step 3: Verify**

Run: `git diff tools/enforce-mentor-on-plan-write.mjs` → одна правка.
(Бэкап оставить — Task 4 правит тот же файл; удалим в конце Task 4.)

---

### Task 4: Всплыть строки роутера и наставника (discipline-source, терминал)

**Files:**
- Modify: `tools/enforce-mentor-on-plan-write.mjs:228-237`

- [ ] **Step 1: Заменить блок всплытия (строки 228-237)**

Было:
```js
      try {
        const m1 = await import('./verdict-surface-store.mjs');
        const m2 = await import('./verdict-outcome-line.mjs');
        const kindObj = decision.degraded ? { kind: 'degraded', reason: res.reason || 'транспорт недоступен' }
          : decision.block ? { kind: 'nogo', text: formatMentorObjection(res) }
          : decision.recordMentorGo ? { kind: 'go' }
          : { kind: 'skip', reason: res.reason || 'не судится' };
        const line = m2.buildVerdictOutcomeLine(kindObj);
        if (line) m1.appendVerdictOutcome({ sessionId: sess, line, nowMs: Date.now() });
      } catch { /* best-effort */ }
```
Стало:
```js
      try {
        const m1 = await import('./verdict-surface-store.mjs');
        const det = await import('./verdict-surface-detail.mjs');
        const kindObj = decision.degraded ? { kind: 'degraded', reason: res.reason || 'транспорт недоступен' }
          : decision.block ? { kind: 'nogo', text: formatMentorObjection(res) }
          : decision.recordMentorGo ? { kind: 'go' }
          : { kind: 'skip', reason: res.reason || 'не судится' };
        const routerLine = det.buildRouterSurface(res.router, { round: res.round });
        const mentorLine = det.buildMentorSurface(kindObj, { round: res.round, verdict: res.verdict });
        for (const line of [routerLine, mentorLine]) {
          if (line) m1.appendVerdictOutcome({ sessionId: sess, line, nowMs: Date.now() });
        }
      } catch { /* best-effort */ }
```

- [ ] **Step 2: Verify diff и регрессия**

Run: `git diff tools/enforce-mentor-on-plan-write.mjs` → правки Task 3 + Task 4, импорт `verdict-outcome-line` в этом блоке убран.
Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: PASS, 0 регрессий.

- [ ] **Step 3: Удалить бэкап и коммит**

```bash
rm tools/enforce-mentor-on-plan-write.mjs.bak
git add tools/enforce-mentor-on-plan-write.mjs
git commit -m "feat(wall): подписанные строки роутера и наставника во всплытии"
```

---

### Task 5: Всплыть строку судьи (discipline-source, терминал)

**Files:**
- Modify: `tools/enforce-judge-gate.mjs:494-502`

- [ ] **Step 1: Бэкап**

Run: `cp tools/enforce-judge-gate.mjs tools/enforce-judge-gate.mjs.bak`

- [ ] **Step 2: Заменить блок всплытия (строки 494-502)**

Было:
```js
  try {
    const judged = extractGate1Product(event).shouldJudge || extractGate2Product(event).shouldJudge;
    if (judged) {
      const m1 = await import('./verdict-surface-store.mjs');
      const m2 = await import('./verdict-outcome-line.mjs');
      const line = m2.buildVerdictOutcomeLine(classifyJudgeOutcome(result, result.verdict));
      if (line) m1.appendVerdictOutcome({ sessionId: (event && event.session_id) || 'unknown', line, nowMs: Date.now() });
    }
  } catch { /* best-effort */ }
```
Стало:
```js
  try {
    const g1 = extractGate1Product(event).shouldJudge;
    const judged = g1 || extractGate2Product(event).shouldJudge;
    if (judged) {
      const m1 = await import('./verdict-surface-store.mjs');
      const det = await import('./verdict-surface-detail.mjs');
      const outcome = classifyJudgeOutcome(result, result.verdict);
      const line = det.buildJudgeSurface(outcome, { gate: g1 ? 'gate1' : 'gate2' });
      if (line) m1.appendVerdictOutcome({ sessionId: (event && event.session_id) || 'unknown', line, nowMs: Date.now() });
    }
  } catch { /* best-effort */ }
```

- [ ] **Step 3: Verify diff и регрессия**

Run: `git diff tools/enforce-judge-gate.mjs` → одна правка, импорт `verdict-outcome-line` убран.
Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: PASS, 0 регрессий.

- [ ] **Step 4: Удалить бэкап и коммит**

```bash
rm tools/enforce-judge-gate.mjs.bak
git add tools/enforce-judge-gate.mjs
git commit -m "feat(wall): подписанная строка судьи во всплытии (с именем gate)"
```

---

### Task 6: Уборка демо-файла + финальная регрессия

**Files:**
- Delete: `tools/wall-verdicts-demo.mjs`

- [ ] **Step 1: Удалить демо-файл**

Run: `rm tools/wall-verdicts-demo.mjs`

- [ ] **Step 2: Полная tools-регрессия**

Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: PASS, 0 регрессий (новые тесты Task 1 зелёные).

- [ ] **Step 3: Коммит**

```bash
git add -A tools/wall-verdicts-demo.mjs
git commit -m "chore(wall): убрать демо-файл фазы выяснения"
```

- [ ] **Step 4: Живая проверка видимости**

Прогнать настоящую церемонию (спека→план) и убедиться, что в начале следующего хода всплыли подписанные строки `🧭 РОУТЕР … / 🧑‍🏫 НАСТАВНИК … / ⚖️ СУДЬЯ …` вместо безымянного `✅ GO — опечатано`.

---

## Follow-ups (вне этого плана)

- Синхрон правок в канонический репозиторий **claude-brain** (ADR-020).
- Опционально: `[круг N]` у судьи (требует загрузки журнала наставника в отдельном процессе судьи).
- Опционально: резолв `recommendedChain` ids → человекочитаемые имена в строке роутера.
