docs: SP1 видимость вердикта — дизайн+план + handoff №7 arbitration epic
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
# SP1 — громкая видимость вердикта — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (ИНЛАЙН — субагенты стена
|
||||
> запрещает, VA-4). Steps — checkbox (`- [ ]`). Под стеной H4: новые файлы — escape+override; врезки —
|
||||
> терминал владельца (F-K). Полный свод + коммит — терминал владельца.
|
||||
|
||||
**Goal:** Громко показывать контроллеру ВСЕ исходы вердикта судьи/наставника (GO/NO-GO/degraded/skip) на
|
||||
следующем ходе — баннер в UserPromptSubmit.additionalContext + обязательное подтверждение `вердикт:` +
|
||||
Stop-страж, ловящий пропуск.
|
||||
|
||||
**Architecture:** 4 новых файла `tools/` (чистые verdict-line + I/O стор + UPS-хук показа + Stop-хук
|
||||
подтверждения) + врезки `pushVerdict` в существующие судью/наставника. Паттерны brain: Stop-хук —
|
||||
`exitDisciplineDecision`+чистая `decide` (как `enforce-coverage-verify`); эмиссия —
|
||||
`hookSpecificOutput`; стор — `~/.claude/runtime` пер-сессия, fail-quiet.
|
||||
|
||||
**Tech Stack:** Node ESM (`.mjs`, стиль `tools/`), vitest. Без внешних зависимостей.
|
||||
|
||||
**Спек:** `docs/superpowers/specs/2026-06-16-verdict-visibility-sp1-design.md`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create `tools/verdict-outcome-line.mjs` — чистые: `classifyJudgeOutcome`, `buildVerdictBanner`, `parseVerdictAck`.
|
||||
- Create `tools/verdict-surface-store.mjs` — I/O: `pushVerdict`/`drainVerdicts` + `markSurfaced`/`readPendingAck`/`clearPendingAck`.
|
||||
- Create `tools/enforce-verdict-surface.mjs` — UPS-хук: `buildSurfaceOutput` (чистая) + `main`.
|
||||
- Create `tools/enforce-verdict-ack.mjs` — Stop-хук: `decide` (чистая) + `main`.
|
||||
- Create соответствующие `.test.mjs` к каждому.
|
||||
- **Терминал (F-K):** врезки `pushVerdict` в `enforce-judge-gate.mjs` (`runJudgeGate`) + mentor-пути; регистрация хуков в `settings.json`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `verdict-outcome-line.mjs` — чистые verdict-line функции
|
||||
|
||||
**Files:** Create `tools/verdict-outcome-line.mjs`, `tools/verdict-outcome-line.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Failing tests**
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { classifyJudgeOutcome, buildVerdictBanner, parseVerdictAck } from './verdict-outcome-line.mjs';
|
||||
|
||||
describe('classifyJudgeOutcome', () => {
|
||||
it('GO / NO-GO / degraded / skip', () => {
|
||||
expect(classifyJudgeOutcome({ wired: true, decision: 'GO' })).toBe('GO');
|
||||
expect(classifyJudgeOutcome({ wired: true, decision: 'NO-GO' })).toBe('NO-GO');
|
||||
expect(classifyJudgeOutcome({ wired: false })).toBe('degraded');
|
||||
expect(classifyJudgeOutcome(null)).toBe('skip');
|
||||
});
|
||||
});
|
||||
describe('buildVerdictBanner', () => {
|
||||
it('баннер с гейтом и дословной причиной', () => {
|
||||
const b = buildVerdictBanner({ outcome: 'NO-GO', gate: 'judge', round: 1, version: 1, reason: 'шаг 3 не проверяем' });
|
||||
expect(b).toContain('🚫 NO-GO [judge] круг 1 v1');
|
||||
expect(b).toContain('причина (дословно): шаг 3 не проверяем');
|
||||
});
|
||||
it('опускает круг/версию если не заданы', () => {
|
||||
const b = buildVerdictBanner({ outcome: 'GO', gate: 'mentor' });
|
||||
expect(b).toContain('✅ GO [mentor]');
|
||||
expect(b).not.toContain('круг');
|
||||
});
|
||||
});
|
||||
describe('parseVerdictAck', () => {
|
||||
it('ловит `вердикт:` первой строкой', () => {
|
||||
expect(parseVerdictAck('вердикт: NO-GO\nдалее...')).toBe(true);
|
||||
expect(parseVerdictAck('обычный ответ')).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — verify FAIL**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/verdict-outcome-line.test.mjs`
|
||||
Expected: FAIL «is not a function».
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/** verdict-outcome-line — чистые функции отображения вердикта (SP1, спек 2026-06-16-verdict-visibility). */
|
||||
|
||||
const EMOJI = { 'GO': '✅', 'NO-GO': '🚫', 'degraded': '⚠', 'skip': '⤳' };
|
||||
const BAR = '━'.repeat(36);
|
||||
|
||||
/** Сырой вердикт судьи/наставника → исход. wired:false = degraded; иначе по decision. */
|
||||
export function classifyJudgeOutcome(verdict) {
|
||||
if (!verdict || typeof verdict !== 'object') return 'skip';
|
||||
if (verdict.wired === false) return 'degraded';
|
||||
const d = String(verdict.decision || '').toUpperCase().replace(/\s+/g, '');
|
||||
if (d === 'GO') return 'GO';
|
||||
if (d === 'NO-GO' || d === 'NOGO') return 'NO-GO';
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
/** Баннер. round/version опускаются при null/undefined (SP1 до памяти кругов). reason — дословно. */
|
||||
export function buildVerdictBanner({ outcome, gate, round, version, reason } = {}) {
|
||||
const oc = EMOJI[outcome] ? outcome : 'skip';
|
||||
const head = [
|
||||
`${EMOJI[oc]} ${oc}`,
|
||||
gate ? `[${gate}]` : '',
|
||||
round != null ? `круг ${round}` : '',
|
||||
version != null ? `v${version}` : '',
|
||||
].filter(Boolean).join(' ');
|
||||
const lines = [BAR, head];
|
||||
if (reason) lines.push(`причина (дословно): ${reason}`);
|
||||
lines.push(BAR);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** true, если в первых строках текста есть подтверждение `вердикт: <outcome>`. */
|
||||
export function parseVerdictAck(text) {
|
||||
return /^\s*вердикт:\s*(GO|NO-GO|NOGO|degraded|skip)\b/im.test(String(text || ''));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run — verify PASS**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/verdict-outcome-line.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit (терминал владельца, формат-сохраняющий)**
|
||||
|
||||
```bash
|
||||
git add tools/verdict-outcome-line.mjs tools/verdict-outcome-line.test.mjs
|
||||
git commit -m "feat(verdict-surface): чистые verdict-line функции (SP1 Task 1)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `verdict-surface-store.mjs` — пер-сессионный стор + pending-ack
|
||||
|
||||
**Files:** Create `tools/verdict-surface-store.mjs`, `tools/verdict-surface-store.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Failing tests** (стор + pending-ack, через временный baseDir)
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { pushVerdict, drainVerdicts, markSurfaced, readPendingAck, clearPendingAck } from './verdict-surface-store.mjs';
|
||||
|
||||
const dir = () => mkdtempSync(join(tmpdir(), 'vsurf-'));
|
||||
|
||||
describe('verdict-surface-store', () => {
|
||||
it('push → drain one-shot (drain чистит)', () => {
|
||||
const d = dir();
|
||||
pushVerdict('s1', { outcome: 'NO-GO', gate: 'judge' }, d);
|
||||
expect(drainVerdicts('s1', d)).toHaveLength(1);
|
||||
expect(drainVerdicts('s1', d)).toHaveLength(0);
|
||||
});
|
||||
it('пер-сессия: чужие не видны', () => {
|
||||
const d = dir();
|
||||
pushVerdict('s1', { outcome: 'GO' }, d);
|
||||
expect(drainVerdicts('s2', d)).toHaveLength(0);
|
||||
});
|
||||
it('pending-ack: mark → read → clear', () => {
|
||||
const d = dir();
|
||||
markSurfaced('s1', ['NO-GO'], d);
|
||||
expect(readPendingAck('s1', d)).toEqual(['NO-GO']);
|
||||
clearPendingAck('s1', d);
|
||||
expect(readPendingAck('s1', d)).toBeNull();
|
||||
});
|
||||
it('fail-quiet: битый baseDir не кидает', () => {
|
||||
expect(() => drainVerdicts('s1', '\0bad')).not.toThrow();
|
||||
expect(drainVerdicts('s1', '\0bad')).toEqual([]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — verify FAIL**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/verdict-surface-store.test.mjs`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/** verdict-surface-store — пер-сессионный транзиентный стор вердиктов + pending-ack маркер (SP1).
|
||||
* Fail-quiet: любая ошибка → безопасный no-op/пусто. Дефолт baseDir = ~/.claude/runtime. */
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
|
||||
function baseOf(baseDir) { return baseDir || join(homedir(), '.claude', 'runtime'); }
|
||||
function queuePath(sid, baseDir) { return join(baseOf(baseDir), `verdict-surface-${sid || 'unknown'}.json`); }
|
||||
function ackPath(sid, baseDir) { return join(baseOf(baseDir), `verdict-pending-ack-${sid || 'unknown'}.json`); }
|
||||
function readJson(p) { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; } }
|
||||
function ensureDir(p) { try { mkdirSync(baseOf(p), { recursive: true }); } catch {} }
|
||||
|
||||
export function pushVerdict(sid, obj, baseDir) {
|
||||
try {
|
||||
ensureDir(baseDir);
|
||||
const p = queuePath(sid, baseDir);
|
||||
let arr = existsSync(p) ? readJson(p) : [];
|
||||
if (!Array.isArray(arr)) arr = [];
|
||||
arr.push(obj);
|
||||
writeFileSync(p, JSON.stringify(arr));
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
export function drainVerdicts(sid, baseDir) {
|
||||
try {
|
||||
const p = queuePath(sid, baseDir);
|
||||
if (!existsSync(p)) return [];
|
||||
const arr = readJson(p);
|
||||
try { writeFileSync(p, '[]'); } catch {}
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
export function markSurfaced(sid, outcomes, baseDir) {
|
||||
try { ensureDir(baseDir); writeFileSync(ackPath(sid, baseDir), JSON.stringify(outcomes || [])); return true; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
export function readPendingAck(sid, baseDir) {
|
||||
try {
|
||||
const p = ackPath(sid, baseDir);
|
||||
if (!existsSync(p)) return null;
|
||||
const v = readJson(p);
|
||||
return Array.isArray(v) ? v : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function clearPendingAck(sid, baseDir) {
|
||||
try { const p = ackPath(sid, baseDir); if (existsSync(p)) writeFileSync(p, 'null'); return true; }
|
||||
catch { return false; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run — verify PASS**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/verdict-surface-store.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit (терминал)**
|
||||
|
||||
```bash
|
||||
git add tools/verdict-surface-store.mjs tools/verdict-surface-store.test.mjs
|
||||
git commit -m "feat(verdict-surface): пер-сессионный стор + pending-ack (SP1 Task 2)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `enforce-verdict-surface.mjs` — UPS-хук показа
|
||||
|
||||
**Files:** Create `tools/enforce-verdict-surface.mjs`, `tools/enforce-verdict-surface.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Failing test** (чистая `buildSurfaceOutput`)
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildSurfaceOutput } from './enforce-verdict-surface.mjs';
|
||||
|
||||
describe('buildSurfaceOutput', () => {
|
||||
it('пусто → null (тихо)', () => {
|
||||
expect(buildSurfaceOutput([])).toBeNull();
|
||||
});
|
||||
it('баннер(ы) + императив подтверждения', () => {
|
||||
const out = buildSurfaceOutput([{ outcome: 'NO-GO', gate: 'judge', reason: 'x' }]);
|
||||
expect(out).toContain('🚫 NO-GO [judge]');
|
||||
expect(out).toContain('ПЕРВОЙ строкой ответа подтверди');
|
||||
expect(out).toContain('`вердикт:');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — verify FAIL**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/enforce-verdict-surface.test.mjs`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement** (чистая + main, эмиссия как subagent-prompt-prefix)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/** enforce-verdict-surface — UserPromptSubmit. Дренит вердикты сессии → громкий баннер +
|
||||
* императив подтверждения в additionalContext. Помечает pending-ack для Stop-стража.
|
||||
* Fail-quiet: ошибка → пустой вывод, никогда не роняет ход. (SP1) */
|
||||
import { drainVerdicts, markSurfaced } from './verdict-surface-store.mjs';
|
||||
import { buildVerdictBanner } from './verdict-outcome-line.mjs';
|
||||
|
||||
const IMPERATIVE = 'ПЕРВОЙ строкой ответа подтверди: `вердикт: <outcome>` — иначе считается пропущенным.';
|
||||
|
||||
/** Чистая: список вердиктов → текст additionalContext, либо null если пусто. */
|
||||
export function buildSurfaceOutput(verdicts) {
|
||||
if (!Array.isArray(verdicts) || verdicts.length === 0) return null;
|
||||
return verdicts.map(buildVerdictBanner).join('\n') + '\n' + IMPERATIVE;
|
||||
}
|
||||
|
||||
async function readStdin() { let b = ''; for await (const c of process.stdin) b += c; return b; }
|
||||
function emitNone() { process.stdout.write('{}'); }
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const ev = JSON.parse(await readStdin());
|
||||
const sid = ev.session_id || ev.sessionId || 'unknown';
|
||||
const verdicts = drainVerdicts(sid);
|
||||
const out = buildSurfaceOutput(verdicts);
|
||||
if (!out) return emitNone();
|
||||
markSurfaced(sid, verdicts.map((v) => v.outcome));
|
||||
process.stdout.write(JSON.stringify({
|
||||
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: out },
|
||||
}));
|
||||
} catch { emitNone(); }
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-verdict-surface.mjs');
|
||||
if (isCli) main();
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run — verify PASS**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/enforce-verdict-surface.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit (терминал)**
|
||||
|
||||
```bash
|
||||
git add tools/enforce-verdict-surface.mjs tools/enforce-verdict-surface.test.mjs
|
||||
git commit -m "feat(verdict-surface): UPS-хук показа баннера + императив (SP1 Task 3)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: `enforce-verdict-ack.mjs` — Stop-хук подтверждения
|
||||
|
||||
**Files:** Create `tools/enforce-verdict-ack.mjs`, `tools/enforce-verdict-ack.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Failing test** (чистая `decide`)
|
||||
|
||||
```javascript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { decide } from './enforce-verdict-ack.mjs';
|
||||
|
||||
describe('enforce-verdict-ack decide', () => {
|
||||
it('есть pending-ack + нет `вердикт:` → block', () => {
|
||||
const r = decide({ pendingAck: ['NO-GO'], assistantText: 'обычный ответ' });
|
||||
expect(r.block).toBe(true);
|
||||
expect(r.message).toContain('вердикт');
|
||||
});
|
||||
it('есть pending-ack + есть `вердикт:` → ok', () => {
|
||||
expect(decide({ pendingAck: ['NO-GO'], assistantText: 'вердикт: NO-GO\n...' }).block).toBe(false);
|
||||
});
|
||||
it('нет pending-ack → тихо', () => {
|
||||
expect(decide({ pendingAck: null, assistantText: 'что угодно' }).block).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run — verify FAIL**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/enforce-verdict-ack.test.mjs`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement** (паттерн `enforce-coverage-verify`: exitDisciplineDecision + decide; маркер чистится при ack)
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
/** enforce-verdict-ack — Stop-хук. Если на ходе был surfaced-вердикт (pending-ack), а в ответе нет
|
||||
* строки `вердикт:` — громкая претензия (маркер остаётся → нагнетает до подтверждения). При ack —
|
||||
* чистит маркер. Fail-quiet через exitDisciplineDecision. (SP1) */
|
||||
import { readStdin, parseEventJson, readTranscript, lastAssistantText, exitDisciplineDecision } from './enforce-hook-helpers.mjs';
|
||||
import { parseVerdictAck } from './verdict-outcome-line.mjs';
|
||||
import { readPendingAck, clearPendingAck } from './verdict-surface-store.mjs';
|
||||
|
||||
/** Чистая: решение по наличию pending-ack и факту подтверждения в тексте. */
|
||||
export function decide({ pendingAck, assistantText }) {
|
||||
if (!pendingAck || pendingAck.length === 0) return { block: false };
|
||||
if (parseVerdictAck(assistantText)) return { block: false, acked: true };
|
||||
return {
|
||||
block: true,
|
||||
message: [
|
||||
`[enforce-verdict-ack] на ходе был показан вердикт (${pendingAck.join(', ')}), но ответ его не подтвердил.`,
|
||||
'ПЕРВОЙ строкой следующего ответа: `вердикт: <outcome>` (повтори исход из баннера).',
|
||||
].join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ev = parseEventJson(await readStdin());
|
||||
const sid = ev.session_id || ev.sessionId || 'unknown';
|
||||
await exitDisciplineDecision(
|
||||
() => {
|
||||
const transcript = readTranscript(ev.transcript_path);
|
||||
const assistantText = lastAssistantText(transcript);
|
||||
const pendingAck = readPendingAck(sid);
|
||||
const r = decide({ pendingAck, assistantText });
|
||||
if (r.acked) clearPendingAck(sid);
|
||||
return r;
|
||||
},
|
||||
{ label: 'enforce-verdict-ack' },
|
||||
);
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-verdict-ack.mjs');
|
||||
if (isCli) main();
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run — verify PASS**
|
||||
|
||||
Run: `npx vitest run --config vitest.config.tools.mjs tools/enforce-verdict-ack.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit (терминал)**
|
||||
|
||||
```bash
|
||||
git add tools/enforce-verdict-ack.mjs tools/enforce-verdict-ack.test.mjs
|
||||
git commit -m "feat(verdict-surface): Stop-страж подтверждения вердикта (SP1 Task 4)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Врезки + регистрация хуков — ТЕРМИНАЛ ВЛАДЕЛЬЦА (F-K)
|
||||
|
||||
**Files (терминал, не под стеной):** Modify `tools/enforce-judge-gate.mjs`, mentor-пути (`tools/mentor-verdict.mjs`/`tools/enforce-mentor-on-plan-write.mjs`), `.claude/settings.json`
|
||||
|
||||
> **Канал — терминал (F-K):** правка discipline-source (`enforce-judge-/mentor-`) под стеной рубится
|
||||
> `enforce-normative-content-rules §6` и floor-escape не зачитывается (Лидерра, 3 провала). Применять
|
||||
> node string-replace по якорям + **сразу коммит** (иначе git restore откатит — инцидент Лидерры).
|
||||
|
||||
- [ ] **Step 1: Врезка в судью.** В `enforce-judge-gate.mjs`, в точке, где вычислен `verdict` (рядом с
|
||||
`sealOnWiredGo`/возврат вердикта в `runJudgeGate`), добавить:
|
||||
|
||||
```javascript
|
||||
import { classifyJudgeOutcome } from './verdict-outcome-line.mjs';
|
||||
import { pushVerdict } from './verdict-surface-store.mjs';
|
||||
// ...после вычисления verdict, перед возвратом:
|
||||
try {
|
||||
pushVerdict(sessionId, {
|
||||
outcome: classifyJudgeOutcome(verdict),
|
||||
gate: 'judge',
|
||||
round: verdict.round ?? null, // SP2 наполнит; SP1 → null
|
||||
version: verdict.version ?? null,
|
||||
reason: verdict.reason || verdict.objection || '',
|
||||
});
|
||||
} catch { /* fail-quiet */ }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Врезка в наставника** (симметрично, `gate: 'mentor'`) в mentor-verdict/onPlanWrite/onSpecWrite пути.
|
||||
|
||||
- [ ] **Step 3: Регистрация хуков в `.claude/settings.json`** — `enforce-verdict-surface` на `UserPromptSubmit`, `enforce-verdict-ack` на `Stop`. Команды `node tools/enforce-verdict-surface.mjs` / `node tools/enforce-verdict-ack.mjs`. (В Фазе 2 плагина → перенос в `hooks/hooks.json` с `${CLAUDE_PLUGIN_ROOT}`.)
|
||||
|
||||
- [ ] **Step 4: Полный свод + интеграционная проверка живым** (терминал): `npx vitest run --config vitest.config.tools.mjs`; затем спровоцировать вердикт (план/спека) → на следующем ходе должен всплыть баннер + требование `вердикт:`.
|
||||
|
||||
- [ ] **Step 5: Commit (терминал)**
|
||||
|
||||
```bash
|
||||
git add tools/enforce-judge-gate.mjs tools/mentor-verdict.mjs tools/enforce-mentor-on-plan-write.mjs .claude/settings.json
|
||||
git commit -m "feat(verdict-surface): врезки judge/mentor + регистрация хуков (SP1 Task 5)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review (выполнено при написании)
|
||||
|
||||
- **Покрытие спека:** §2.1 classifyJudgeOutcome → T1; §2.2 баннер → T1; §2.3 стор → T2; §2.4 UPS-показ → T3;
|
||||
§2.5 Stop-страж → T4; врезки §2.6 → T5; громкость (баннер+императив+страж) §4 → T1/T3/T4; все исходы §1 →
|
||||
T1 классификатор; verbatim причина §4 → buildVerdictBanner; SP2-ready round/version → T1/T5 (null в SP1);
|
||||
fail-safe §5 → fail-quiet во всех I/O; F-K §7 → T5 терминал.
|
||||
- **Заглушки:** реальный код во всех new-file шагах; врезки — точный сниппет + точка (T5 owner-terminal,
|
||||
без точных номеров строк — discipline-source правит владелец по якорю).
|
||||
- **Согласованность имён:** `pushVerdict`/`drainVerdicts`/`markSurfaced`/`readPendingAck`/`clearPendingAck`/
|
||||
`classifyJudgeOutcome`/`buildVerdictBanner`/`parseVerdictAck`/`buildSurfaceOutput`/`decide` — едины T1-T5;
|
||||
маркер `вердикт:` един (surface императив / ack parser / страж message).
|
||||
- **Граница:** round/version проброшены, но в SP1 = null (память кругов — SP2).
|
||||
|
||||
## Execution Handoff (под стеной)
|
||||
|
||||
Субагенты запрещены (VA-4) → только ИНЛАЙН через `executing-plans`. T1-T4 — новые файлы: тесты `.test.mjs`
|
||||
(floor-escape), production `.mjs` (override `ремонт инфраструктуры` + floor-escape per-file). T5 — терминал
|
||||
владельца (F-K). Полный свод + формат-сохраняющие коммиты — терминал владельца.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Brain-as-plugin + arbitration-machinery — handoff сессии №7 (закрытие 2026-06-16)
|
||||
|
||||
**Кодовая фраза стены:** «роутер-наставник». **Каноны:** `2026-06-15-brain-as-plugin-design-v6.md` (§14 фазы);
|
||||
полный цикл переговоров — `2026-06-16-full-negotiation-cycle-design-for-brain.md`; журнал Лидерры —
|
||||
`2026-06-16-arbitration-fixes-worklog-for-brain.md`. Предыдущий handoff: №6.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что сделано (сессия №7)
|
||||
|
||||
| # | Что | Коммит |
|
||||
|---|---|---|
|
||||
| #3-observer | `classifyFilePath(path, normativeStems)` config-driven (закрыл весь greenfield #3) | `318add8` (свод 224 passed) |
|
||||
| Фаза 2 Спек 1 | `.claude-plugin/plugin.json` + `marketplace.json` + `agents/reviewer-agent.md` (neutral) + GUIDE-cleaning | `29d2dd3` |
|
||||
| Память | `project-brain-plugin-phase1-progress` обновлена (Фаза 1 закрыта, Фаза 2 старт) | — |
|
||||
| SP1 дизайн+план | спек+план видимости вердикта (НЕ исполнено) | этот коммит |
|
||||
|
||||
**Коммиты через Claude** в claude-brain работают: floor-escape `bash:<git>`, НО сообщение **paren-free**
|
||||
(floor режет `()` → `feat:` вместо `feat(scope):`), явные пути `git commit -- <пути>`. Формат-сохраняющий
|
||||
коммит — терминал владельца.
|
||||
|
||||
## 2. НОВЫЙ ЭПИК — машинерия переговоров/арбитража в плагин
|
||||
|
||||
Владелец: «реализовать ВСЁ в плагине» (машинерия стены = ядро движка, слой 1). Brain на **до-фиксовом
|
||||
базовом** состоянии — отсутствуют `round-memory-store`/`seal-override`/`verdict-surface-*`/`controller-negotiation`,
|
||||
нет priorObjections-врезок. Нужно перенести/построить целиком. **Декомпозиция (согласована):**
|
||||
|
||||
- **SP1 — громкая видимость вердикта** (дизайн+план готовы, см. §4). Standalone, первый.
|
||||
- **SP2 — полный цикл переговоров** (`full-negotiation-cycle-design`): M/J-дорожки, версии+diff, M-после-J
|
||||
возврат, потолки ≤3, **симметрия спека/план вкл. спека-путь наставника** (тот «не реализованный момент»).
|
||||
- **SP3 — Ф3 owner-seal арбитраж** (`seal-override` + `sealOnWiredGo` owner-ветка + arbitration-card AskUser + выход на любом круге).
|
||||
- **SP4 — баги/enablers:** F-J (рассинхрон указателя supreme-gate при со-хук-блоке), F-K (правка
|
||||
discipline-source под стеной недостижима + helper `print-canonical-action`).
|
||||
|
||||
## 3. Уроки стены №7
|
||||
|
||||
1. **Коммит через Claude в claude-brain доступен** floor-escape `bash:<git>` (router-gate не подключён,
|
||||
criterion-gate чтит escape) — но **paren-free** сообщение. Формат `feat(scope):` сохраняет только терминал.
|
||||
2. **F-K: правку discipline-source (`enforce-judge-/mentor-/judge-engine`) под стеной не сделать** —
|
||||
рубит `enforce-normative-content-rules §6`, floor-escape `write:` не зачитывается (Лидерра, 3 провала).
|
||||
→ врезки в машинерию = **терминал владельца** (node string-replace по якорю + СРАЗУ коммит, иначе git restore откатит).
|
||||
3. **plugin-dev/executing-plans грузятся через `skill:`-escape**; brainstorming/writing-plans/TDD — свободно.
|
||||
4. Схема плагина: компоненты — папки авто-дискавери; минимальный `plugin.json` = `name`; docs едут файлами неявно.
|
||||
|
||||
## 4. Остаток (порядок)
|
||||
|
||||
1. **SP1 исполнить** (план `plans/2026-06-16-verdict-visibility-sp1-plan.md`): T1-T4 под стеной (новые файлы —
|
||||
override `ремонт инфраструктуры` + floor-escape per-file, TDD), **T5 врезки+регистрация — терминал (F-K)**.
|
||||
5 узлов: `verdict-outcome-line` (classifyJudgeOutcome/buildVerdictBanner/parseVerdictAck) +
|
||||
`verdict-surface-store` (push/drain + pending-ack) + `enforce-verdict-surface` (UPS) + `enforce-verdict-ack` (Stop) + врезки.
|
||||
2. **SP2 → SP3 → SP4** (каждый: brainstorming-адопция дизайна Лидерры → план → исполнение; машинерия — терминал).
|
||||
3. **Фаза 2 Спек 2/2B** плагина (провод компонентов, группы §6, `${CLAUDE_PLUGIN_ROOT}`, инлайн-хуки, `/brain-migrate`).
|
||||
4. Фазы 3-5 (persist улик / dogfood / greenfield).
|
||||
|
||||
## 5. Промт для сессии №8 (копировать целиком)
|
||||
|
||||
```
|
||||
Продолжаем claude-brain. Кодовая фраза стены — «роутер-наставник», ПО ПРАВИЛАМ (authoring-канал для новых .md в docs/superpowers/{specs,plans}; escape/память — только с моего разрешения; субагенты под стеной запрещены — инлайн; коммит paren-free через floor-escape ИЛИ формат-сохраняющий в моём терминале; полный свод — в моём терминале).
|
||||
|
||||
Прочитай (разговорный режим): handoff №7 (specs/2026-06-16-brain-as-plugin-session-handoff-7.md), память project-brain-plugin-phase1-progress, дизайн+план SP1 (specs/2026-06-16-verdict-visibility-sp1-design.md + plans/2026-06-16-verdict-visibility-sp1-plan.md), full-negotiation-cycle-design + arbitration-fixes-worklog, router-mentor-wall-GUIDE.
|
||||
|
||||
Контекст: новый эпик — машинерия переговоров/арбитража в плагин (brain на до-фиксовом состоянии). Декомпозиция SP1 видимость / SP2 полный цикл / SP3 owner-seal / SP4 F-J/F-K. SP1 дизайн+план готовы.
|
||||
|
||||
ВАЖНО F-K: правка discipline-source (enforce-judge-/mentor-/judge-engine) под стеной недостижима (§6 не чтит escape) → врезки = ТЕРМИНАЛ. Новые файлы tools/*.mjs — под стеной (override `ремонт инфраструктуры` + floor-escape per-file, vitest-RED недостижим). Тесты .test.mjs — floor-escape.
|
||||
|
||||
Сделай в начале: спроси канал памяти (escape/claude-md-management) и берём ли SP1 исполнять (T1-T4 под стеной, T5 терминал) или иное.
|
||||
|
||||
Не пломбируй/не коммить без клика. Находки — в authoring-доки.
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# Дизайн: SP1 — громкая видимость вердикта судьи/наставника (для brain-плагина)
|
||||
|
||||
**Дата:** 2026-06-16
|
||||
**Статус:** Approved (brainstorming, владелец одобрил с уточнением «громко, чтобы не пропустить»)
|
||||
**Эпик:** перенос/реализация машинерии переговоров в brain-плагин. Под-проект **SP1** (декомпозиция: SP1 видимость / SP2 полный цикл / SP3 owner-seal / SP4 F-J/F-K).
|
||||
**Источник:** Лидерра-Фикс 1 (worklog `2026-06-16-arbitration-fixes-worklog-for-brain.md`, проверен живым) + дизайн полного цикла `2026-06-16-full-negotiation-cycle-design-for-brain.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель и контракт
|
||||
|
||||
Контроллер сейчас **слеп** к вердиктам судьи/наставника на исходах GO/skip/degraded — текст всплывает только при block (exit2); allow→`{}`, GO/skip/degraded тихие (worklog F-G: `exitDecision` отдаёт текст только при block). Из-за этого «почему печать не встала» (degraded/NO-GO наставника) контроллеру не видно.
|
||||
|
||||
**Контракт:** ВСЕ исходы вердикта (`GO | NO-GO | degraded | skip`) показываются контроллеру на следующем ходе **громко** — баннер в `UserPromptSubmit.additionalContext` + **обязательное подтверждение первой строкой** + **Stop-хук-страж**, ловящий пропуск. Канал brain уже задействован (экономия/coverage инжектятся так же — проверенный, надёжный). Механизм fail-quiet: никогда не роняет UPS/Stop.
|
||||
|
||||
**Граница SP1/SP2:** SP1 показывает ОДИН вердикт за раз (текущий механизм без кругов памяти). Поля `round`/`version` в строке проброшены (дефолт 1/null), наполнятся в SP2 (память кругов). SP1 НЕ трогает память/диалог/оркестрацию — только делает громким то, что уже считается.
|
||||
|
||||
## 2. Архитектура — 5 узлов + врезки
|
||||
|
||||
| # | Узел | Тип | Ответственность |
|
||||
|---|---|---|---|
|
||||
| 1 | `classifyJudgeOutcome(verdict)` | чистая | сырой вердикт судьи/наставника → enum `GO\|NO-GO\|degraded\|skip` |
|
||||
| 2 | `verdict-outcome-line.mjs` `buildVerdictBanner({outcome,gate,round,version,reason})` | чистая | баннер (см. §4); `round`/`version` SP2-ready (дефолт 1/null → опускаются) |
|
||||
| 3 | `verdict-surface-store.mjs` | I/O | пер-сессионный транзиентный стор: `pushVerdict(sessionId,obj)` + `drainVerdicts(sessionId)` (читает+чистит, one-shot) |
|
||||
| 4 | `enforce-verdict-surface.mjs` | UPS-хук | дренит вердикты сессии → баннер + императив подтверждения в additionalContext |
|
||||
| 5 | `enforce-verdict-ack.mjs` | Stop-хук | если на ходе был свежий вердикт, а в ответе нет строки `вердикт:` → громкая претензия на следующем ходе (паттерн `enforce-coverage-verify`) |
|
||||
| — | **врезки** в `enforce-judge-gate` (`runJudgeGate`) + mentor-пути (`mentor-verdict`/`enforce-mentor-on-plan-write`) | правка | после вычисления вердикта зовут `classifyJudgeOutcome` + `pushVerdict`. ⚠️ **discipline-source → F-K** (см. §7) |
|
||||
|
||||
**Изоляция:** узлы 1-2 чистые (без I/O, тривиально тестируются); 3 — единственный, кто знает про файловую систему стора; 4-5 — хук-обёртки (читают стор, не считают вердикт). Врезки минимальны: одна строка `pushVerdict(...)` на точку вычисления вердикта.
|
||||
|
||||
## 3. Поток данных
|
||||
|
||||
```
|
||||
судья/наставник считает вердикт
|
||||
→ врезка: classifyJudgeOutcome(verdict) → pushVerdict(sessionId, {outcome,gate,round,version,reason})
|
||||
→ [стор: runtime, пер-сессия, gitignore]
|
||||
следующий user-prompt
|
||||
→ enforce-verdict-surface (UPS): drainVerdicts(sessionId) → баннер(ы) + императив в additionalContext
|
||||
→ контроллер ВИДИТ баннер, ОБЯЗАН первой строкой: `вердикт: <outcome>`
|
||||
конец хода
|
||||
→ enforce-verdict-ack (Stop): был свежий вердикт без `вердикт:`-подтверждения? → претензия
|
||||
```
|
||||
|
||||
## 4. Формат баннера + подтверждение + страж
|
||||
|
||||
**Баннер** (узел 2) — не строчка, а заметный блок:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🚫 NO-GO [judge] круг 1 v1
|
||||
причина (дословно): <замечание судьи verbatim>
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
Эмодзи по исходу: `✅ GO` / `🚫 NO-GO` / `⚠ degraded` / `⤳ skip`. `причина` — **дословно** (verbatim, без пересказа — принцип доков §2/§3). `круг N vV` опускается, если round/version неизвестны (SP1 до памяти кругов).
|
||||
|
||||
**Императив** (узел 4 добавляет к баннеру): «ПЕРВОЙ строкой ответа подтверди: `вердикт: <outcome>` — иначе считается пропущенным».
|
||||
|
||||
**Страж** (узел 5): на Stop — если за ход был свежий surfaced-вердикт (по стору/маркеру), а в ответе контроллера нет строки `вердикт:` → на следующем ходе громкая претензия (как `enforce-coverage-verify`). Делает пропуск невозможным молча.
|
||||
|
||||
## 5. Жизненный цикл и fail-safe
|
||||
|
||||
- **One-shot:** `drainVerdicts` читает И чистит — баннер не повторяется.
|
||||
- **Пер-сессия:** ключ — sessionId; чужие вердикты не видны.
|
||||
- **Транзиент:** стор в state-dir brain (`loadConfig().state_dir`, дефолт), gitignore — потеря безопасна.
|
||||
- **Fail-quiet:** любой сбой стора/хука → пустой additionalContext, НИКОГДА не роняет UPS/Stop (exit 0). Нет вердикта → нет баннера → нет требования подтверждения (страж молчит).
|
||||
- **Ack-маркер:** факт surfacing фиксируется (для стража), подтверждение `вердикт:` гасит требование.
|
||||
|
||||
## 6. Тестирование (TDD)
|
||||
|
||||
Чистые узлы — RED→GREEN: `classifyJudgeOutcome` (каждый исход + неизвестный→safe); `buildVerdictBanner` (формат, verbatim-причина, опускание round/version). Стор: `pushVerdict`/`drainVerdicts` (one-shot clear, пер-сессия, fail-quiet). Хуки: `enforce-verdict-surface` (дрен→additionalContext, императив, пусто→тихо); `enforce-verdict-ack` (есть вердикт без ack→претензия; с ack→тихо; нет вердикта→тихо). Врезки — интеграционно (judge/mentor пушат верный outcome). Полный свод — терминал владельца (harness-collapse).
|
||||
|
||||
## 7. Исполнение под стеной (F-K)
|
||||
|
||||
Узлы 1-5 — **новые файлы** (`tools/*.mjs`); production-код → `enforce-tdd-gate` (нужен owner-override `ремонт инфраструктуры`) + floor-escape per-file. Но **врезки (узел 6)** правят **существующий discipline-source** (`enforce-judge-gate`/mentor) → их рубит `enforce-normative-content-rules §6` (ЗАКОН), и floor-escape `write:` этим гейтом НЕ зачитывается (Лидерра F-K, 3 провала) → под стеной недостижимо. **Канал врезок — терминал владельца** (как делала Лидерра: node string-replace по якорям + сразу коммит, иначе git restore откатит). Регистрация хуков 4-5 в settings.json — шаг владельца. Полный план разнесёт: новые файлы (под стеной, escape+override) vs врезки/регистрация (терминал). F-K как код-фикс — отдельный SP4 (опционально, чтобы врезки делались под стеной).
|
||||
|
||||
## 8. Self-review (выполнено при написании)
|
||||
|
||||
- **Покрытие:** все исходы (§1 контракт) → классификатор §2.1 + баннер §4; громкость → императив+страж §4; не-пропуск → узел 5. Verbatim-причина → §4. SP2-готовность → round/version проброшены §2.2.
|
||||
- **Заглушки:** компоненты конкретны (имена/сигнатуры/ответственность); формат баннера показан; точки врезки названы.
|
||||
- **Согласованность:** `pushVerdict`/`drainVerdicts`/`classifyJudgeOutcome`/`buildVerdictBanner` — единые имена; `вердикт:` — единый маркер подтверждения (узлы 4/5).
|
||||
- **Граница:** SP1 не лезет в память кругов (SP2); round/version — проброшены, не наполняются.
|
||||
Reference in New Issue
Block a user