# Арбитраж опечатывает: owner-seal (Фикс 3) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (inline).

**Goal:** Выбор владельца «Держусь/Своё» на арбитраже опечатывает план/спеку (owner-seal), несмотря на NO-GO судьи и mentor freeze-gate.

**Architecture:** Подписанный floor_escape `owner-seal:<hash>` + чистое решение печати (decideSeal) + sealOnWiredGo печатает по owner-seal (минуя mentor freeze-gate) + честный текст карточки.

**Tech Stack:** Node.js ESM, vitest.

## Цель

Закрыть F-D: твоё «Держусь/Своё» на арбитраже реально пишет frozenPlan/frozenArtifact (стена в
реализацию), несмотря на NO-GO. Сигнал — зарезервированная подписанная floor_escape-метка
`owner-seal:<hash>`.

```skills-json
["test-driven-development"]
```

## File Structure

- Create: `tools/seal-override.mjs` (+`.test.mjs`) — ownerSealAction + decideSeal.
- Create: `tools/seal-override-gate.test.mjs` — тест печати по owner-seal в sealOnWiredGo.
- Modify: `tools/enforce-judge-gate.mjs` — sealOnWiredGo (owner-seal + bypass mentorGate) + импорты + sealTurnProd (вычисление ownerSealOpen).
- Create: `tools/arbitration-card.test.mjs` — честный текст + sealAction.
- Modify: `tools/arbitration-card.mjs` — текст hold/own + поле sealAction.

---

### Task 1: seal-override (канон + решение печати)

- [ ] **Step 1: Тест** — `tools/seal-override.test.mjs`

```javascript
import { describe, it, expect } from 'vitest';
import { ownerSealAction, decideSeal } from './seal-override.mjs';

describe('seal-override', () => {
  it('ownerSealAction → owner-seal:<hash>', () => {
    expect(ownerSealAction('abc')).toBe('owner-seal:abc');
  });
  it('decideSeal: ownerSealOpen → true даже при NO-GO', () => {
    expect(decideSeal({ verdict: { wired: true, decision: 'NO-GO' }, ownerSealOpen: true })).toBe(true);
  });
  it('decideSeal: wired GO → true', () => {
    expect(decideSeal({ verdict: { wired: true, decision: 'GO' }, ownerSealOpen: false })).toBe(true);
  });
  it('decideSeal: NO-GO без owner-seal → false', () => {
    expect(decideSeal({ verdict: { wired: true, decision: 'NO-GO' }, ownerSealOpen: false })).toBe(false);
  });
});
```

- [ ] **Step 2: vitest RED** — `npx vitest run --root app --config vitest.config.tools.mjs seal-override` → FAIL (модуль не найден; имя `seal-override` совпадёт и с gate-тестом — ок, оба упадут на отсутствии модуля).

- [ ] **Step 3: Модуль** — `tools/seal-override.mjs`

```javascript
#!/usr/bin/env node
/** seal-override — owner-seal на арбитраже: печать по воле владельца despite NO-GO (Фикс 3). */

/** Зарезервированная floor_escape-метка (как plan-done): не порождается реальным действием. */
export function ownerSealAction(hash) {
  return `owner-seal:${String(hash || '')}`;
}

/** Печатать? owner-seal перевешивает NO-GO; иначе — только настоящий wired-GO судьи. */
export function decideSeal({ verdict, ownerSealOpen = false } = {}) {
  if (ownerSealOpen === true) return true;
  return !!(verdict && verdict.wired === true && verdict.decision === 'GO');
}
```

- [ ] **Step 4: vitest GREEN** — `npx vitest run --root app --config vitest.config.tools.mjs seal-override.test` → PASS (4).

---

### Task 2: sealOnWiredGo печатает по owner-seal

- [ ] **Step 1: Тест** — `tools/seal-override-gate.test.mjs`

```javascript
import { describe, it, expect } from 'vitest';
import { sealOnWiredGo } from './enforce-judge-gate.mjs';

const planEvent = { tool_input: { file_path: 'docs/superpowers/plans/x.md', content: 'c' } };

describe('sealOnWiredGo owner-seal', () => {
  it('NO-GO + ownerSealOpen → печать плана, mentorGate пропущен', () => {
    let mentorCalled = false; let sealCalled = false;
    const deps = {
      key: 'k',
      sealPlan: () => { sealCalled = true; return { sealed: true, seal: {} }; },
      persistPlan: () => {},
      loadCurrentArtifact: () => null,
      mentorGate: () => { mentorCalled = true; return { pass: false }; },
    };
    const r = sealOnWiredGo({ event: planEvent, verdict: { wired: true, decision: 'NO-GO' }, judgeMode: 'live-block', ownerSealOpen: true, deps });
    expect(r.sealed).toBe(true);
    expect(mentorCalled).toBe(false);
    expect(sealCalled).toBe(true);
  });
  it('NO-GO без ownerSeal → не печатает', () => {
    const r = sealOnWiredGo({ event: planEvent, verdict: { wired: true, decision: 'NO-GO' }, ownerSealOpen: false, deps: { sealPlan: () => ({ sealed: true }) } });
    expect(r.sealed).toBe(false);
  });
});
```

- [ ] **Step 2: vitest RED** — `npx vitest run --root app --config vitest.config.tools.mjs seal-override-gate` → FAIL (sealOnWiredGo ещё не знает ownerSealOpen → первый кейс sealed:false).

- [ ] **Step 3: Edit `tools/enforce-judge-gate.mjs`** — заменить функцию `sealOnWiredGo` целиком:

old:

```javascript
export function sealOnWiredGo({ event, verdict, judgeMode, deps = {} }) {
  if (!(verdict && verdict.wired === true && verdict.decision === 'GO')) return { sealed: false };
  const fp = String((event && event.tool_input && event.tool_input.file_path) || '');
  const content = String((event && event.tool_input && event.tool_input.content) ?? '');
  const key = deps.key !== undefined ? deps.key : (deps.resolveReceiptKey ? deps.resolveReceiptKey() : null);
  if (SPEC_PATH_RE.test(fp)) {
    const r = deps.sealArtifact({ md: content, verdict, key, judgeMode });
    if (r && r.sealed && deps.persistArtifact) deps.persistArtifact(r.seal);
    return { sealed: !!(r && r.sealed), kind: 'artifact' };
  }
  if (PLAN_PATH_RE.test(fp)) {
    // T6 «зубы» наставника (решение владельца 2026-06-12): freeze-gate ПЕРЕД печатью
    // плана. mentorGate инъектируется прод-сборкой ТОЛЬКО при mentorSeamActive() —
    // выключен рубильник → undefined → печать как раньше. Бросок гейта → fail-CLOSE.
    if (typeof deps.mentorGate === 'function') {
      let g;
      try { g = deps.mentorGate({ content }); } catch { g = { pass: false, reason: 'mentor freeze-gate бросил (fail-CLOSE)' }; }
      if (!g || g.pass !== true) return { sealed: false, kind: 'plan', reason: `mentor freeze-gate: ${(g && g.reason) || 'нет pass'}` };
    }
    const cur = deps.loadCurrentArtifact ? deps.loadCurrentArtifact() : null;
    const r = deps.sealPlan({ md: content, currentArtifact: cur, verdict, key, judgeMode });
    if (r && r.sealed && deps.persistPlan) deps.persistPlan(r.seal);
    return { sealed: !!(r && r.sealed), kind: 'plan' };
  }
  return { sealed: false };
}
```

new:

```javascript
export function sealOnWiredGo({ event, verdict, judgeMode, ownerSealOpen = false, deps = {} }) {
  // Фикс 3: печать на настоящем wired-GO ИЛИ по owner-seal владельца (арбитраж перевешивает NO-GO).
  if (!(ownerSealOpen || (verdict && verdict.wired === true && verdict.decision === 'GO'))) return { sealed: false };
  const fp = String((event && event.tool_input && event.tool_input.file_path) || '');
  const content = String((event && event.tool_input && event.tool_input.content) ?? '');
  const key = deps.key !== undefined ? deps.key : (deps.resolveReceiptKey ? deps.resolveReceiptKey() : null);
  if (SPEC_PATH_RE.test(fp)) {
    const r = deps.sealArtifact({ md: content, verdict, key, judgeMode });
    if (r && r.sealed && deps.persistArtifact) deps.persistArtifact(r.seal);
    return { sealed: !!(r && r.sealed), kind: 'artifact' };
  }
  if (PLAN_PATH_RE.test(fp)) {
    // T6 «зубы» наставника: freeze-gate ПЕРЕД печатью плана. Фикс 3: owner-seal ПЕРЕВЕШИВАЕТ
    // и mentor freeze-gate (владелец продавил судью И наставника) → при ownerSealOpen гейт пропущен.
    if (!ownerSealOpen && typeof deps.mentorGate === 'function') {
      let g;
      try { g = deps.mentorGate({ content }); } catch { g = { pass: false, reason: 'mentor freeze-gate бросил (fail-CLOSE)' }; }
      if (!g || g.pass !== true) return { sealed: false, kind: 'plan', reason: `mentor freeze-gate: ${(g && g.reason) || 'нет pass'}` };
    }
    const cur = deps.loadCurrentArtifact ? deps.loadCurrentArtifact() : null;
    const r = deps.sealPlan({ md: content, currentArtifact: cur, verdict, key, judgeMode });
    if (r && r.sealed && deps.persistPlan) deps.persistPlan(r.seal);
    return { sealed: !!(r && r.sealed), kind: 'plan' };
  }
  return { sealed: false };
}
```

- [ ] **Step 4: vitest GREEN** — `npx vitest run --root app --config vitest.config.tools.mjs seal-override-gate` → PASS (2).

- [ ] **Step 5: Edit `tools/enforce-judge-gate.mjs`** — добавить импорты (прод-проводка) после строки `import { loadTaskId } from './router-task-id.mjs';`:

old:

```javascript
import { loadTaskId } from './router-task-id.mjs';
```

new:

```javascript
import { loadTaskId } from './router-task-id.mjs';
// Фикс 3: owner-seal-override на арбитраже.
import { ownerSealAction } from './seal-override.mjs';
import { escapeGrantOpen, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
```

- [ ] **Step 6: Edit `tools/enforce-judge-gate.mjs`** — в `sealTurnProd` вычислить ownerSealOpen и передать. Заменить:

old:

```javascript
function sealTurnProd(event, verdict, mode) {
  const sessionId = (event && event.session_id) || 'unknown';
  const dir = runtimeDir();
  // Способ B (Фаза 2): судья — хук ПОСЛЕ наставника, поэтому вердикт наставника уже свежий
  // (mentor-GO + персист вердикта для plan_hash). Судья САМ печатает план здесь, через
  // sealOnWiredGo + freeze-gate (verity/VA-9). Прежний «фикс дедлока» (судья сохранял judge-GO,
  // наставник печатал в Post) снят — порядок теперь правильный.
  return sealOnWiredGo({
    event, verdict, judgeMode: mode,
```

new:

```javascript
function sealTurnProd(event, verdict, mode) {
  const sessionId = (event && event.session_id) || 'unknown';
  const dir = runtimeDir();
  // Фикс 3: открыт ли owner-seal владельца для ЭТОГО артефакта (арбитраж «Держусь/Своё»).
  let ownerSealOpen = false;
  try {
    const c = String((event && event.tool_input && event.tool_input.content) ?? '');
    const fp = String((event && event.tool_input && event.tool_input.file_path) || '');
    const hash = SPEC_PATH_RE.test(fp) ? judgedHashOf(sealableArtifact(c)) : planId(sealablePlan(c).steps);
    ownerSealOpen = hash ? escapeGrantOpen(ownerSealAction(hash), loadFloorEscapes(sessionId), loadConsumed(sessionId)) : false;
  } catch { ownerSealOpen = false; }
  // Способ B (Фаза 2): судья — хук ПОСЛЕ наставника, поэтому вердикт наставника уже свежий
  // (mentor-GO + персист вердикта для plan_hash). Судья САМ печатает план здесь, через
  // sealOnWiredGo + freeze-gate (verity/VA-9). Прежний «фикс дедлока» (судья сохранял judge-GO,
  // наставник печатал в Post) снят — порядок теперь правильный.
  return sealOnWiredGo({
    event, verdict, judgeMode: mode, ownerSealOpen,
```

---

### Task 3: Честный текст карточки + sealAction

- [ ] **Step 1: Тест** — `tools/arbitration-card.test.mjs`

```javascript
import { describe, it, expect } from 'vitest';
import { buildArbitrationCard } from './arbitration-card.mjs';

describe('arbitration-card owner-seal', () => {
  it('hold-текст честный (owner-seal, без обещания пропуска ворот) + sealAction', () => {
    const c = buildArbitrationCard({ side: 'judge', level: 'L2', round: 3, objectionVerbatim: 'o', controllerPositionVerbatim: 'p', sealAction: 'owner-seal:h' });
    const hold = c.options.find((o) => o.key === 'hold');
    expect(hold.whatChanges).toContain('owner-seal');
    expect(hold.whatChanges).not.toContain('escape-грант');
    expect(c.sealAction).toBe('owner-seal:h');
  });
});
```

- [ ] **Step 2: vitest RED** — `npx vitest run --root app --config vitest.config.tools.mjs arbitration-card` → FAIL.

- [ ] **Step 3: Edit `tools/arbitration-card.mjs`** — заменить функцию `buildArbitrationCard`:

old:

```javascript
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,
  };
}
```

new:

```javascript
export function buildArbitrationCard({ side, level, round, objectionVerbatim, controllerPositionVerbatim, sealAction = null } = {}) {
  const who = side === 'judge' ? 'судьи' : 'наставника';
  return {
    side, level, round, sealAction,
    title: `Арбитраж (${level}): тупик спора с ${side === 'judge' ? 'судьёй' : 'наставником'} после ${round} кругов`,
    objection: verbatim(objectionVerbatim),
    position: verbatim(controllerPositionVerbatim),
    options: [
      { key: 'hold', label: 'Держусь позиции контроллера', whatChanges: `План/спека опечатывается по твоему решению (owner-seal); замечание ${who} отведено.` },
      { key: 'accept', label: 'Согласиться с замечанием', whatChanges: `Контроллер переделает по замечанию ${who}; счётчик круга сбрасывается.` },
      { key: 'own', label: 'Своё решение', whatChanges: 'Ты вписываешь своё указание; контроллер правит, затем печать по твоему решению (owner-seal).' },
    ],
    explainAffordance: true,
  };
}
```

- [ ] **Step 4: vitest GREEN** — `npx vitest run --root app --config vitest.config.tools.mjs arbitration-card` → PASS (1).

- [ ] **Step 5: Полная регрессия** — `npx vitest run --root app --config vitest.config.tools.mjs` → PASS (0 регрессий).

---

## steps-json

```steps-json
[
  {"op":"Write","object":"tools/seal-override.test.mjs","ref":"D1"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs seal-override.test","ref":"D5"},
  {"op":"Write","object":"tools/seal-override.mjs","ref":"D1"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs seal-override.test","ref":"D5"},
  {"op":"Write","object":"tools/seal-override-gate.test.mjs","ref":"D3"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs seal-override-gate","ref":"D5"},
  {"op":"Edit","object":"tools/enforce-judge-gate.mjs","ref":"D3"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs seal-override-gate","ref":"D5"},
  {"op":"Edit","object":"tools/enforce-judge-gate.mjs","ref":"D3"},
  {"op":"Edit","object":"tools/enforce-judge-gate.mjs","ref":"D3"},
  {"op":"Write","object":"tools/arbitration-card.test.mjs","ref":"D4"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs arbitration-card","ref":"D5"},
  {"op":"Edit","object":"tools/arbitration-card.mjs","ref":"D4"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs arbitration-card","ref":"D5"},
  {"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs","ref":"D5"}
]
```

```verified-context-json
[
  {"id":"D2-seal","kind":"EXTRACTED","ref":"tools/enforce-judge-gate.mjs","anchor":"export function sealOnWiredGo("},
  {"id":"D1-escape","kind":"EXTRACTED","ref":"tools/escape-grant.mjs","anchor":"export function escapeGrantOpen("}
]
```

## Переговоры

### Круг 1

(пусто)
