# Sealed-Plan Production Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (inline — субагенты ЗАПРЕЩЕНЫ владельцем, handoff §5). Steps use checkbox (`- [ ]`) syntax.

**Goal:** Построить рантайм-производство двух печатей (артефакт-решение + план-шаги), чтобы стена М2 имела что матчить — код-предусловие флипа Фазы 8.

**Architecture:** Два чистых парсера (спека→`{sections}`, план→`[{op,object,ref}]`) + seal-оркестрация, которую хук судьи зовёт на РЕАЛЬНОМ GO (spec-Write → печать артефакта; plan-Write → печать плана со штампом `artifact_id`). Печать привязана к `judged_hash` судимого объекта (одно представление течёт судья→печать). Персист атомарен, артефакт ДО плана. Закрыты 18 находок двух адверсариальных разборов.

**Tech Stack:** Node ESM, vitest (tools-only config), переиспользование `plan-lock.mjs` / `judge-seal-channel.mjs` / `receipt-sign.mjs` / `path-normalization.mjs` / `receipt-key-config.mjs`.

**Источник истины:** [docs/superpowers/specs/2026-06-09-sealed-plan-production-design.md](../specs/2026-06-09-sealed-plan-production-design.md) (ОДОБРЕНА). При расхождении — приоритет у спеки.

**Квирки (handoff §5/§7):** vitest через Bash одной абс.командой `node "<repo>\app\node_modules\vitest\vitest.mjs" run --root "<repo>\app" --config "<repo>\app\vitest.config.tools.mjs" <фильтр> --reporter dot`; тест-файлы **for-of + it()**, НЕ `it.each`; enforce-tdd-gate требует правки `*.test.mjs` ПЕРЕД правкой prod `*.mjs` в том же ходе (RED первым); коммит-не-пуш, git через PowerShell.

---

## File Structure

| Файл | Ответственность | Действие |
|---|---|---|
| `tools/artifact-from-spec.mjs` | спека markdown → `{sections, source_sha}` по якорям `{#id}` (P2-2) | Create |
| `tools/plan-steps-parse.mjs` | план markdown → `[{op,object,ref}]`; валидация fail-CLOSE; reject `op:'Task'` (VA-4); канон `object` (SE-5) | Create |
| `tools/seal-orchestration.mjs` | sealable-объекты + `judgedHashOf` (SD-1) + `sealArtifact`/`sealPlan` (real-GO, TOCTOU, carve-out, штамп `artifact_id`, fail-CLOSE) | Create |
| `tools/plan-lock.mjs` | +`judgeMode` в подписанную базу `freezePlan`; атомарный персист `saveFrozen*` (SE-4/VA-2/VA-3) | Modify |
| `tools/enforce-judge-gate.mjs` | в `runJudgeTurn`: spec-Write GO → `sealArtifact`; plan-Write GO → `sealPlan`; анти-инъекция продукта (P2-3) | Modify |
| `tools/*.test.mjs` (зеркальные) | тесты каждого модуля | Create |

`<repo>` = `c:\моя\проекты\портал crm\Документация`.

---

## Task 1: C1 — парсер артефакта из спеки (`artifact-from-spec.mjs`)

**Files:**

- Create: `tools/artifact-from-spec.mjs`
- Test: `tools/artifact-from-spec.test.mjs`

- [ ] **Step 1: Write the failing test**

```javascript
import { describe, it, expect } from 'vitest';
import { parseAnchoredSections, buildArtifact } from './artifact-from-spec.mjs';

describe('artifact-from-spec', () => {
  const md = [
    '# Spec',
    '## Решение А {#dec-a}',
    'Текст А.',
    '## Решение Б {#dec-b}',
    'Текст Б.',
  ].join('\n');

  for (const [name, fn] of [['parses anchored sections', () => {
    const { sections } = parseAnchoredSections(md);
    expect(Object.keys(sections).sort()).toEqual(['dec-a', 'dec-b']);
    expect(sections['dec-a']).toContain('Текст А.');
  }]]) it(name, fn);

  it('heading WITHOUT anchor is not a section key', () => {
    const { sections } = parseAnchoredSections('## Без якоря\nx');
    expect(Object.keys(sections)).toEqual([]);
  });

  it('empty-content section is dropped (fail-CLOSE)', () => {
    const { sections } = parseAnchoredSections('## Пусто {#e}\n\n## Полно {#f}\ny');
    expect(Object.keys(sections)).toEqual(['f']);
  });

  it('buildArtifact returns {sections, source_sha} deterministically', () => {
    const a1 = buildArtifact(md); const a2 = buildArtifact(md);
    expect(a1.source_sha).toEqual(a2.source_sha);
    expect(typeof a1.source_sha).toBe('string');
    expect(a1.sections['dec-a']).toBeTruthy();
  });
});
```

- [ ] **Step 2: Run test → FAIL**

Run: `node "<repo>\app\node_modules\vitest\vitest.mjs" run --root "<repo>\app" --config "<repo>\app\vitest.config.tools.mjs" artifact-from-spec --reporter dot`
Expected: FAIL (module not found / not a function).

- [ ] **Step 3: Implement**

```javascript
#!/usr/bin/env node
/**
 * artifact-from-spec (C1, §5) — спека markdown → {sections, source_sha}.
 * ref = ЯВНЫЙ якорь `## ... {#id}` (P2-2, НЕ слаг заголовка). Пустой раздел отброшен
 * (fail-CLOSE). source_sha — sha256 исходного текста (входит в sealable-объект, SD-1).
 */
import { createHash } from 'node:crypto';

const ANCHOR_RE = /^#{1,6}\s+.*\{#([a-zA-Z0-9._-]+)\}\s*$/;

/** markdown → { sections: { <anchor>: <содержимое раздела> } }. */
export function parseAnchoredSections(md) {
  const lines = String(md == null ? '' : md).split(/\r?\n/);
  const sections = {};
  let curKey = null; let buf = [];
  const flush = () => {
    if (curKey) { const body = buf.join('\n').trim(); if (body) sections[curKey] = body; }
    buf = [];
  };
  for (const line of lines) {
    const m = line.match(ANCHOR_RE);
    const isHeading = /^#{1,6}\s+/.test(line);
    if (m) { flush(); curKey = m[1]; }
    else if (isHeading) { flush(); curKey = null; }   // заголовок без якоря закрывает раздел
    else if (curKey) buf.push(line);
  }
  flush();
  return { sections };
}

/** sealable-объект артефакта: { sections, source_sha }. */
export function buildArtifact(md) {
  const { sections } = parseAnchoredSections(md);
  const source_sha = createHash('sha256').update(String(md == null ? '' : md)).digest('hex');
  return { sections, source_sha };
}
```

- [ ] **Step 4: Run test → PASS**

Run: тот же vitest-фильтр `artifact-from-spec`. Expected: PASS.

- [ ] **Step 5: Commit**

```
# PowerShell, из <repo>
git add tools/artifact-from-spec.mjs tools/artifact-from-spec.test.mjs
git commit -F .scratch/c1.txt   # msg: "feat(seal): C1 artifact-from-spec parser (anchored sections, P2-2)"
```

---

## Task 2: C2 — парсер шагов плана (`plan-steps-parse.mjs`)

**Files:**

- Create: `tools/plan-steps-parse.mjs`
- Test: `tools/plan-steps-parse.test.mjs`

Контракт блока в плане: огороженный ` ```steps-json ` блок с JSON-массивом `[{op,object,ref}]`.

- [ ] **Step 1: Write the failing test**

```javascript
import { describe, it, expect } from 'vitest';
import { extractStepsBlock, parsePlanSteps } from './plan-steps-parse.mjs';

const good = [
  '# Plan', '```steps-json',
  '[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"},',
  ' {"op":"Bash","object":"npm run build","ref":"dec-b"}]',
  '```',
].join('\n');

describe('plan-steps-parse', () => {
  it('extracts and parses valid steps', () => {
    const steps = parsePlanSteps(good);
    expect(steps).toHaveLength(2);
    expect(steps[0]).toMatchObject({ op: 'Edit', ref: 'dec-a' });
  });
  it('canonicalizes file object to repo-relative POSIX (SE-5)', () => {
    const md = '```steps-json\n[{"op":"Write","object":"app\\\\Bar.php","ref":"r"}]\n```';
    expect(parsePlanSteps(md)[0].object).toBe('app/Bar.php');
  });
  it('rejects step without ref (SE-1) → throws', () => {
    const md = '```steps-json\n[{"op":"Edit","object":"x"}]\n```';
    expect(() => parsePlanSteps(md)).toThrow(/ref/);
  });
  it('rejects op:Task (VA-4, субагенты запрещены) → throws', () => {
    const md = '```steps-json\n[{"op":"Task","object":"coder","ref":"r"}]\n```';
    expect(() => parsePlanSteps(md)).toThrow(/Task/);
  });
  it('missing block → throws (fail-CLOSE)', () => {
    expect(() => parsePlanSteps('# Plan no block')).toThrow();
  });
  it('non-JSON block → throws', () => {
    expect(() => parsePlanSteps('```steps-json\nnot json\n```')).toThrow();
  });
});
```

- [ ] **Step 2: Run test → FAIL**

Run: vitest-фильтр `plan-steps-parse`. Expected: FAIL.

- [ ] **Step 3: Implement**

```javascript
#!/usr/bin/env node
/**
 * plan-steps-parse (C2, §5) — план markdown → [{op,object,ref}]. fail-CLOSE на любой брак.
 * Reject op:'Task' (VA-4). Канон object: файлы → repo-relative POSIX (SE-5). ref обязателен (SE-1).
 */
import { pathNormalize } from './path-normalization.mjs';

const BLOCK_RE = /```steps-json\s*\n([\s\S]*?)\n```/;
const FILE_OPS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);

/** Достать содержимое блока ```steps-json``` или null. */
export function extractStepsBlock(md) {
  const m = String(md == null ? '' : md).match(BLOCK_RE);
  return m ? m[1] : null;
}

function canonObject(op, object) {
  const s = String(object ?? '');
  if (FILE_OPS.has(op)) { try { return pathNormalize(s).replace(/\\/g, '/'); } catch { return s.replace(/\\/g, '/'); } }
  return s;
}

/** Распарсить + провалидировать шаги. Бросает на брак (fail-CLOSE). */
export function parsePlanSteps(md) {
  const raw = extractStepsBlock(md);
  if (raw == null) throw new Error('plan-steps-parse: нет блока ```steps-json``` (fail-CLOSE)');
  let arr;
  try { arr = JSON.parse(raw); } catch { throw new Error('plan-steps-parse: блок не валидный JSON (fail-CLOSE)'); }
  if (!Array.isArray(arr) || arr.length === 0) throw new Error('plan-steps-parse: ожидался непустой массив');
  return arr.map((s, i) => {
    if (!s || typeof s !== 'object') throw new Error(`plan-steps-parse: шаг ${i} не объект`);
    const op = String(s.op || '');
    const object = String(s.object ?? '');
    const ref = String(s.ref ?? '');
    if (!op) throw new Error(`plan-steps-parse: шаг ${i} без op`);
    if (op === 'Task') throw new Error('plan-steps-parse: op:Task запрещён (субагенты, VA-4)');
    if (!object.trim()) throw new Error(`plan-steps-parse: шаг ${i} без object`);
    if (!ref.trim()) throw new Error(`plan-steps-parse: шаг ${i} без ref (closed-door, SE-1)`);
    return { op, object: canonObject(op, object), ref };
  });
}
```

- [ ] **Step 4: Run test → PASS** (vitest-фильтр `plan-steps-parse`).

- [ ] **Step 5: Commit**

```
git add tools/plan-steps-parse.mjs tools/plan-steps-parse.test.mjs
git commit -F .scratch/c2.txt   # "feat(seal): C2 plan-steps parser (fail-CLOSE, no-Task VA-4, canon SE-5)"
```

---

## Task 3: plan-lock — judge_mode в подписи плана + атомарный персист

**Files:**

- Modify: `tools/plan-lock.mjs` (`freezePlan`, `saveFrozenArtifact`, `saveFrozenPlan`)
- Test: `tools/plan-lock.test.mjs` (добавить кейсы — Read файла перед правкой существующего теста, harness)

- [ ] **Step 1: Write the failing test (добавить в plan-lock.test.mjs)**

```javascript
it('freezePlan carries judge_mode in signed base (VA-2)', () => {
  const key = 'k';
  const p = freezePlan({ steps: [{ op: 'Edit', object: 'a', ref: 'r' }], artifactId: 'aid', judgeMode: 'shadow', key });
  expect(p.judge_mode).toBe('shadow');
  expect(verifyFrozenPlan(p, key)).toBe(true);
  const tampered = { ...p, judge_mode: 'live-block' };
  expect(verifyFrozenPlan(tampered, key)).toBe(false); // judge_mode в подписи
});
it('saveFrozenPlan writes atomically via temp+rename (SE-4)', () => {
  const calls = [];
  const fsImpl = { writeFileSync: (p, d) => calls.push(['write', p]), renameSync: (a, b) => calls.push(['rename', a, b]) };
  saveFrozenPlan({ plan: { plan_id: 'x' }, sessionId: 's', runtimeDir: '/r', fsImpl });
  expect(calls.some(c => c[0] === 'rename')).toBe(true); // прошёл через rename
});
```

- [ ] **Step 2: Run → FAIL** (vitest-фильтр `plan-lock`). Expected: FAIL (judge_mode/rename отсутствуют).

- [ ] **Step 3: Implement (правки в plan-lock.mjs)**

`freezePlan` — добавить `judgeMode`:

```javascript
export function freezePlan({ steps, artifactId = null, judgeMode = null, key, nowMs }) {
  const sealedSteps = withCriterionIds(steps);
  const id = planId(sealedSteps);
  const base = { plan_id: id, artifact_id: artifactId, judge_mode: judgeMode,
    frozen_at: typeof nowMs === 'number' ? nowMs : Date.now(), steps: sealedSteps };
  return { ...base, sig: signPayload(base, key, RECEIPT_DOMAINS.FROZEN_PLAN) };
}
```

Атомарный персист — общий помощник + замена в обоих `save*`:

```javascript
function writeAtomic(path, data, fsImpl) {
  const tmp = `${path}.tmp-${planId([path, data.length])}`;
  fsImpl.writeFileSync(tmp, data);
  fsImpl.renameSync(tmp, path);
}
export function saveFrozenPlan({ plan, sessionId, runtimeDir, fsImpl = fsDefault }) {
  writeAtomic(planPath(runtimeDir, sessionId), JSON.stringify(plan), fsImpl);
}
export function saveFrozenArtifact({ artifact, sessionId, runtimeDir, fsImpl = fsDefault }) {
  writeAtomic(artifactPath(runtimeDir, sessionId), JSON.stringify(artifact), fsImpl);
}
```

(NB: `verifyFrozenPlan` не трогаем — `verifyReceipt` пересчитывает base из всех полей кроме sig, judge_mode попадёт автоматически. Существующие печати без judge_mode: base без поля → verify по-прежнему ок.)

- [ ] **Step 4: Run → PASS** (vitest-фильтр `plan-lock`, регрессия существующих кейсов зелёная).

- [ ] **Step 5: Commit**

```
git add tools/plan-lock.mjs tools/plan-lock.test.mjs
git commit -F .scratch/t3.txt   # "feat(seal): plan-lock judge_mode-in-sig (VA-2) + atomic persist (SE-4)"
```

---

## Task 4: seal-orchestration — sealable + judged_hash + sealArtifact/sealPlan

**Files:**

- Create: `tools/seal-orchestration.mjs`
- Test: `tools/seal-orchestration.test.mjs`

- [ ] **Step 1: Write the failing test**

```javascript
import { describe, it, expect } from 'vitest';
import { sealableArtifact, sealablePlan, judgedHashOf, sealArtifact, sealPlan } from './seal-orchestration.mjs';
import { contentHash } from './judge-seal-channel.mjs';

const specMd = '## Реш {#dec-a}\nтекст';
const planMd = '```steps-json\n[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"}]\n```';
const KEY = 'k';
const goVerdict = (obj) => ({ wired: true, decision: 'GO', judged_hash: judgedHashOf(obj), verdict_id: 'v1' });

describe('seal-orchestration', () => {
  it('judgedHashOf == contentHash of same sealable object (SD-1)', () => {
    const a = sealableArtifact(specMd);
    expect(judgedHashOf(a)).toBe(contentHash(a));
  });
  it('sealArtifact on real GO → sealed, verifiable', () => {
    const a = sealableArtifact(specMd);
    const r = sealArtifact({ md: specMd, verdict: goVerdict(a), key: KEY, judgeMode: 'live-block' });
    expect(r.sealed).toBe(true);
    expect(r.seal.sections['dec-a']).toBeTruthy();
    expect(r.seal.judge_mode).toBe('live-block');
  });
  it('sealArtifact NOT sealed on degraded GO (wired:false, SE-3)', () => {
    const a = sealableArtifact(specMd);
    const r = sealArtifact({ md: specMd, verdict: { wired: false, decision: 'GO' }, key: KEY, judgeMode: 'shadow' });
    expect(r.sealed).toBe(false);
  });
  it('sealArtifact NOT sealed on judged_hash mismatch (SD-1/TOCTOU)', () => {
    const r = sealArtifact({ md: specMd, verdict: { wired: true, decision: 'GO', judged_hash: 'WRONG' }, key: KEY, judgeMode: 'live-block' });
    expect(r.sealed).toBe(false);
  });
  it('sealPlan stamps artifact_id from current artifact (SD-3)', () => {
    const planObj = sealablePlan(planMd);
    const r = sealPlan({ md: planMd, currentArtifact: { artifact_id: 'AID' }, verdict: goVerdict(planObj), key: KEY, judgeMode: 'live-block' });
    expect(r.sealed).toBe(true);
    expect(r.seal.artifact_id).toBe('AID');
    expect(r.seal.judge_mode).toBe('live-block');
  });
  it('sealPlan fail-CLOSE without current artifact (VA-1/SD-3)', () => {
    const planObj = sealablePlan(planMd);
    const r = sealPlan({ md: planMd, currentArtifact: null, verdict: goVerdict(planObj), key: KEY, judgeMode: 'live-block' });
    expect(r.sealed).toBe(false);
  });
});
```

- [ ] **Step 2: Run → FAIL** (vitest-фильтр `seal-orchestration`).

- [ ] **Step 3: Implement**

```javascript
#!/usr/bin/env node
/**
 * seal-orchestration (C3-C6, §5) — собрать sealable-объект, сверить judged_hash (SD-1),
 * поставить печать ТОЛЬКО на реальном GO (SE-3), со штампом artifact_id (SD-3) и judge_mode.
 * Печать-движок инъектируется (в проде freezeArtifact/freezePlan Машины 2).
 */
import { buildArtifact } from './artifact-from-spec.mjs';
import { parsePlanSteps } from './plan-steps-parse.mjs';
import { contentHash, sealOnApproval } from './judge-seal-channel.mjs';
import { freezeArtifact, freezePlan } from './plan-lock.mjs';

export function sealableArtifact(md) { return buildArtifact(md); }              // {sections, source_sha}
export function sealablePlan(md) { return { steps: parsePlanSteps(md) }; }      // {steps:[{op,object,ref}]}
export function judgedHashOf(obj) { return contentHash(obj); }

function isRealGo(v) { return !!(v && v.wired === true && v.decision === 'GO'); }

/** Печать артефакта на реальном GO (carve-out деньги/тупик — через sealOnApproval). */
export function sealArtifact({ md, verdict, key, judgeMode, nowMs, freezeImpl = freezeArtifact }) {
  if (!isRealGo(verdict)) return { sealed: false, reason: 'нет реального GO (SE-3)' };
  const artifact = sealableArtifact(md);
  if (verdict.judged_hash !== judgedHashOf(artifact)) return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' };
  return sealOnApproval({ artifact: { ...artifact, judge_mode: judgeMode }, verdict, key, nowMs, freezeImpl });
}

/** Печать плана: штамп artifact_id текущего артефакта (SD-3); нет артефакта → fail-CLOSE (VA-1). */
export function sealPlan({ md, currentArtifact, verdict, key, judgeMode, nowMs, freezeImpl = freezePlan }) {
  if (!isRealGo(verdict)) return { sealed: false, reason: 'нет реального GO (SE-3)' };
  if (!currentArtifact || !currentArtifact.artifact_id) return { sealed: false, reason: 'нет текущего артефакта (VA-1/SD-3)' };
  let planObj;
  try { planObj = sealablePlan(md); } catch (e) { return { sealed: false, reason: `парс плана fail-CLOSE: ${e.message}` }; }
  if (verdict.judged_hash !== judgedHashOf(planObj)) return { sealed: false, reason: 'judged_hash mismatch (SD-1/TOCTOU)' };
  const seal = freezeImpl({ steps: planObj.steps, artifactId: currentArtifact.artifact_id, judgeMode, key, nowMs });
  return { sealed: true, seal };
}
```

- [ ] **Step 4: Run → PASS** (vitest-фильтр `seal-orchestration`).

- [ ] **Step 5: Commit**

```
git add tools/seal-orchestration.mjs tools/seal-orchestration.test.mjs
git commit -F .scratch/t4.txt   # "feat(seal): orchestration sealArtifact/sealPlan (SD-1/SD-3/SE-3/VA-1)"
```

---

## Task 5: Wire в enforce-judge-gate — печать на реальном GO + анти-инъекция (P2-3)

**Files:**

- Modify: `tools/enforce-judge-gate.mjs` (новый `SPEC_PATH_RE`, ветвление продукта; в `runJudgeTurn` — печать на `wired` GO)
- Test: `tools/enforce-judge-gate.test.mjs` (Read перед правкой существующего теста, harness)

**Принцип P2-3:** продукт на суд (`extractGate2Product` / новый `extractSpecProduct`) подаётся через существующую анти-инъекцию судьи (делимитеры/pre-filter из `judge-engine`/`buildJudgePrompt`); `parseJudgeResponse` уже fail-closed. Печать ставится из чистой seal-оркестрации (Task 4), НЕ из текста контроллера.

- [ ] **Step 1: Write the failing test (добавить)**

```javascript
import { sealOnWiredGo, SPEC_PATH_RE } from './enforce-judge-gate.mjs';

it('SPEC_PATH_RE matches docs/superpowers/specs/*.md', () => {
  expect(SPEC_PATH_RE.test('docs/superpowers/specs/2026-06-09-x-design.md')).toBe(true);
  expect(SPEC_PATH_RE.test('docs/superpowers/plans/x.md')).toBe(false);
});
it('sealOnWiredGo: spec-Write + real GO → sealArtifact invoked', () => {
  const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: '## R {#r}\nt' } };
  const calls = [];
  const r = sealOnWiredGo({ event, verdict: { wired: true, decision: 'GO' }, judgeMode: 'shadow',
    deps: { sealArtifact: (a) => { calls.push(['artifact', a.judgeMode]); return { sealed: true }; }, persistArtifact: () => {} } });
  expect(calls[0]).toEqual(['artifact', 'shadow']);
});
it('sealOnWiredGo: degraded GO (wired:false) → no seal call', () => {
  const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: 'x' } };
  const calls = [];
  sealOnWiredGo({ event, verdict: { wired: false, decision: 'GO' }, judgeMode: 'shadow',
    deps: { sealArtifact: () => { calls.push('a'); return {}; } } });
  expect(calls).toEqual([]);
});
```

- [ ] **Step 2: Run → FAIL** (vitest-фильтр `enforce-judge-gate`).

- [ ] **Step 3: Implement (в enforce-judge-gate.mjs)**

```javascript
export const SPEC_PATH_RE = /(^|[/\\])docs[/\\]superpowers[/\\]specs[/\\][^/\\]+\.md$/i;

/**
 * Печать на ВЫЧИСЛЕННОМ реальном GO (wired===true). spec-Write → sealArtifact;
 * plan-Write → sealPlan (currentArtifact из загрузки). Persist инъектируется.
 * Чистая (без stdin/exit) — для теста. Возвращает {sealed?, kind?}.
 */
export function sealOnWiredGo({ event, verdict, judgeMode, deps = {} }) {
  if (!(verdict && verdict.wired === true && verdict.decision === 'GO')) return { sealed: false };
  const fp = String(event?.tool_input?.file_path || '');
  const content = String(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.sealed && deps.persistArtifact) deps.persistArtifact(r.seal);
    return { sealed: r.sealed, kind: 'artifact' };
  }
  if (PLAN_PATH_RE.test(fp)) {
    const cur = deps.loadCurrentArtifact ? deps.loadCurrentArtifact() : null;
    const r = (deps.sealPlan)({ md: content, currentArtifact: cur, verdict, key, judgeMode });
    if (r.sealed && deps.persistPlan) deps.persistPlan(r.seal);
    return { sealed: r.sealed, kind: 'plan' };
  }
  return { sealed: false };
}
```

В `runJudgeTurn` после `verdict = await runJudgeGate(...)`, если `verdict.wired` — вызвать `sealOnWiredGo({ event, verdict, judgeMode: mode, deps: <прод-deps> })` (прод-deps: `sealArtifact`/`sealPlan` из seal-orchestration; `resolveReceiptKey`; `loadFrozenArtifact`; `saveFrozenArtifact`/`saveFrozenPlan` атомарные — артефакт ДО плана по порядку событий, VA-3). Печать best-effort, НЕ влияет на block-решение (печать = одобрение, не энфорсмент).

**judged_hash в вердикте (C5):** в `runJudgeGate`/`callJudgeModel` продукт, отданный судье, = sealable-объект (`sealableArtifact`/`sealablePlan`), и `verdict.judged_hash = judgedHashOf(тот же объект)` (одно представление, SD-1). Добавить в `runJudgeGate` присвоение `judged_hash` перед возвратом.

- [ ] **Step 4: Run → PASS** (vitest-фильтр `enforce-judge-gate`, регрессия зелёная).

- [ ] **Step 5: Commit**

```
git add tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit -F .scratch/t5.txt   # "feat(seal): wire seal-on-wired-GO into judge-gate (P2-3 anti-inject, SD-1 judged_hash)"
```

---

## Task 6: Интеграционный тест — печать → стена пускает/блокирует/closed-door

**Files:**

- Test: `tools/seal-to-wall.integration.test.mjs` (Create)

- [ ] **Step 1: Write the failing test**

```javascript
import { describe, it, expect } from 'vitest';
import { sealArtifact, sealPlan, judgedHashOf, sealableArtifact, sealablePlan } from './seal-orchestration.mjs';
import { decideMode } from './enforce-supreme-gate.mjs';

const KEY = 'k';
const specMd = '## Реш {#dec-a}\nтекст';
const planMd = '```steps-json\n[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"}]\n```';

describe('seal → wall integration', () => {
  it('matching edit allowed; non-matching blocked; bad ref blocked (closed-door)', () => {
    const aObj = sealableArtifact(specMd);
    const aRes = sealArtifact({ md: specMd, verdict: { wired: true, decision: 'GO', judged_hash: judgedHashOf(aObj), verdict_id: 'v' }, key: KEY, judgeMode: 'live-block' });
    const pObj = sealablePlan(planMd);
    const pRes = sealPlan({ md: planMd, currentArtifact: aRes.seal, verdict: { wired: true, decision: 'GO', judged_hash: judgedHashOf(pObj), verdict_id: 'v' }, key: KEY, judgeMode: 'live-block' });

    const base = { frozenPlan: pRes.seal, frozenArtifact: aRes.seal, key: KEY, stepPtr: 0 };
    // совпавшая правка
    expect(decideMode({ ...base, toolUse: { name: 'Edit', input: { file_path: 'app/Foo.php' } } }).decision).toBe('allow');
    // несовпавшая
    expect(decideMode({ ...base, toolUse: { name: 'Edit', input: { file_path: 'app/Other.php' } } }).decision).toBe('block');
  });
});
```

- [ ] **Step 2: Run → FAIL/наблюдать** — убедиться, что тест краснеет до согласования форм (если стена ждёт `step.ref` резолюцию, шаг несёт `ref:'dec-a'`, артефакт имеет `sections['dec-a']` → closed-door проходит). Подогнать формы при необходимости.

- [ ] **Step 3: Зафиксировать форму** — если decideMode требует `frozenArtifact.sections`/`artifact_id` совпадений, проверить, что `aRes.seal.artifact_id` === `pRes.seal.artifact_id`. Доработок prod-кода быть не должно (Task 1-5 уже дают формы); если тест вскрыл рассинхрон — это находка, чинить по systematic-debugging.

- [ ] **Step 4: Run → PASS**.

- [ ] **Step 5: Commit**

```
git add tools/seal-to-wall.integration.test.mjs
git commit -F .scratch/t6.txt   # "test(seal): integration seal → supreme-gate allow/block/closed-door"
```

---

## Task 7: Гейт закрытия — регрессия + финал

- [ ] **Step 1:** Полная регрессия tools-only:
Run: `node "<repo>\app\node_modules\vitest\vitest.mjs" run --root "<repo>\app" --config "<repo>\app\vitest.config.tools.mjs" --reporter dot`
Expected: ≥ baseline (текущий main baseline ~3398) + новые кейсы Task 1-6, **0 регрессий**.

- [ ] **Step 2:** `verification-before-completion` — сверить, что каждый компонент C1-C6 спеки реализован и каждый инвариант §11 покрыт тестом.

- [ ] **Step 3:** Финальный коммит-сводка (если нужен) + обновить §13 спеки (реализация done). **Push — НЕ делать** (только по слову «пуш»).

---

## Self-Review (план против спеки)

- **Покрытие спеки:** C1→Task1, C2→Task2, C3/C4/C5/C6→Task3-5, closed-door→Task2(ref)+Task6, инварианты §11 (SE-1..SD-3, P2-1..3)→Task2-6. P2-1 (несущая зависимость runtime-страж) — проверяется на Фазе 8 (порядок регистрации), не код-задача здесь; P2-3 анти-инъекция — Task5 (переиспользование судьи).
- **Плейсхолдеры:** нет — реальный код в каждом шаге.
- **Согласованность типов:** `judgedHashOf`/`sealable*` (Task4) ↔ `judged_hash` в вердикте (Task5); `freezePlan({steps,artifactId,judgeMode,key})` (Task3) ↔ вызов в `sealPlan` (Task4) ↔ интеграция (Task6) — совпадают.
- **NB scope:** анти-инъекция (P2-3) опирается на существующий слой судьи — Task5 переиспользует, не строит заново; если слоя нет в коде судьи — отдельная под-задача (вскроется на Task5 audit-context-building).
