fix: classifyJudgeOutcome skip vs degraded plus bug notes and SP2a design

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-17 05:43:18 +03:00
parent c4774c55fb
commit 4f5c928796
5 changed files with 226 additions and 0 deletions
@@ -0,0 +1,89 @@
# Баг: прод-деплой и коммит невозможно выполнить шагами опечатанного плана под стеной — вся механика падает на хозяина-непрограммиста
> Дата: 16.06.2026. Для проекта **claude-brain**. Кодовая фраза «роутер-наставник».
> Тип: bug-report (управляющий слой стены). Выявлено на живом деплое F-1 (CVE vendor-апдейт).
## Краткая суть
Под стеной «роутер-наставник» легитимный план прод-деплоя, **прошедший наставника и судью**
(✅ GO, опечатан), всё равно **не может быть исполнен самим контроллером**. Его шаги — `ssh`,
`scp`, `composer install`, `systemctl`, `php artisan down/up` — это floor-опасные команды
(класс install / cloud-CLI / ssh / redirect). Пол (M5) рубит их безусловно, стена не двигает
указатель → опечатанный план встаёт колом. Аналогично git-коммит упирается в частокол
router-gate (git-approval) + criterion-gate + verify-gate.
В итоге **вся механика деплоя и коммита перекладывается на хозяина**: он вручную копипастит
~18 команд в свой терминал, по его собственным словам — «тупо сижу выполняю механическую работу
без осознания».
## Почему это баг, а не «так задумано»
Стена строилась, чтобы правки кода шли через осознанный sealed-plan + ревью наставника/судьи.
Но для **прод-ops** она даёт ОБРАТНЫЙ эффект по безопасности:
- осознанный агент, прошедший ревью, **отстранён** от исполнения;
- исполняет **человек-непрограммист**, который не понимает содержимого команды;
- он не может санитарно проверить команду, заметить опечатку, среагировать на нештатный вывод;
- для **НЕОБРАТИМОГО** действия на боевом сервере это **повышает** риск, а не снижает.
То есть для класса задач «прод-ops/коммит» стена работает против собственной цели.
## Доказательства из этой сессии (живой деплой F-1)
1. **Опасный инцидент в окне обслуживания.** Сайт был под `artisan down` (HTTP 503). В терминал
**случайно вставился текст из соседней сессии** (claude-brain): PowerShell ответил
`Unexpected token 'enforce-mentor-then-judge' …`, `'memory' is not recognized`. Мои
smoke-команды НЕ выполнились, сайт оставался в обслуживании дольше нужного. Будь в буфере
деструктивная команда — это ушло бы на живой прод. Корень — человек как «реле» без понимания
того, что он вставляет.
2. **Повторные сбои кавычек.** `grep -iE "a|b"` через цепочку PowerShell → ssh терял двойные
кавычки → `b: command not found` (трижды за сессию: `phone_ranges|region`,
`queue|horizon|liderra`, `Cache|Config|Route|View`). Каждый — лишний round-trip, потому что
человек не мог сам отладить квотинг.
3. **Объём ручного труда.** Pre-flight + доставка + бэкап + down + install + smoke + кэши +
коммит = ~18 копипаст-итераций туда-обратно. Всё это осознанный агент сделал бы за один
проход с само-проверкой вывода.
4. **Баг подтвердил сам себя.** Попытка контроллера записать ЭТОТ баг-файл была заблокирована
стеной: `действие не в плане (ожидался шаг … ssh liderra-prod 'df -h /')`. Опечатанный план
деплоя так и висел на шаге 1, потому что его шаги исполнял человек в терминале (вне стены), а
указатель плана у стены не двигался. Запись удалось сделать только через owner-escape
`write:<path>`. Застрявший неисполнимый план блокирует и последующую обычную работу.
## Где именно ломается (точки для brain)
- **Floor M5** (`enforce-floor-*`): классы install / cloud-CLI / ssh / redirect рубятся
безусловно. Нет понятия «благословлённый ops-шаг опечатанного плана».
- **supreme-gate**: при floor-блоке со-хука указатель плана **не откатывается** (десинк F-J) —
значит даже теоретически собранный sealed-ops-план повис бы на первом ssh-шаге. Вдобавок
план, чьи шаги физически не исполнимы под стеной, навсегда застревает на шаге 1 и блокирует
дальнейшую работу (см. доказательство №4).
- **Коммит**: router-gate + criterion-gate + verify-gate рассчитаны на **код-PR**, не на
docs/ops-коммит. Чистый путь — терминал хозяина `LEFTHOOK=0`, т.е. снова руками.
## Чего хотелось бы (направления; решает brain, это не предписание)
1. **Канал «опечатанный ops-runbook».** Если план прошёл наставника+судью И помечен как
ops/deploy — пол должен **пускать его `ssh`/`scp`/`systemctl`/`composer`-шаги к исполнению
контроллером** строго по белому списку из самого плана, с полным журналом. Тогда необратимое
делает осознанный агент, а человек только один раз говорит «деплоим».
2. **Либо эргономика escape под блок ops-команд:** один owner-grant на весь благословлённый
runbook (а не `FLOOR-ESCAPE` на каждую из ~15 команд), окно шире, привязка к hash плана.
3. **Минимум:** floor должен отличать «ssh-шаг благословлённого плана» от «произвольной
floor-опасной команды»; и стена не должна оставлять указатель на неисполнимом шаге —
план, шаги которого floor-блокируются, должен авто-завершаться или явно помечаться
«исполняется вне стены», а не висеть колом (фикс десинка F-J + застревания из доказательства №4).
4. **Коммит:** docs/ops-коммит из-под опечатанного плана не должен требовать criterion/verify-gate
(там нет кода и тестов) — отдельная ветка гейта по типу изменения.
## Влияние
Пока не починено: каждый прод-деплой и коммит идут через ручной копипаст хозяина-непрограммиста —
**медленно** (раздутый round-trip) и **рискованно** (инцидент №1 в окне обслуживания живого
прода). Это прямо противоречит цели стены: для прод-ops она безопасность снижает.
## Связанное
- Деплой, на котором выявлено: `docs/superpowers/plans/2026-06-16-f1-cve-deploy-plan.md` +
`docs/superpowers/specs/2026-06-16-f1-cve-deploy-spec-v3.md` (опечатаны, GO).
- Соседний баг машинерии: `docs/superpowers/2026-06-16-mentor-empty-recommendation-bug.md`.
- Десинк указателя F-J — описан в `docs/superpowers/router-mentor-wall-GUIDE.md`.
@@ -0,0 +1,69 @@
# Баг: наставник GO с пустым `recommendation` → судья не вызывается (`no_mentor_go`)
**Дата:** 16.06.2026. **Где:** машинерия стены «роутер-наставник», оркестратор
`enforce-mentor-then-judge` (наставник → судья). **Severity:** блокирующий печать — план с
чистым GO не опечатывается. **Куда:** проект **claude-brain** (управляющий слой).
## Симптом
При записи опечатываемого плана наставник (Agent mentor, модель `deepseek-v4-pro`)
возвращает чистый GO без замечаний:
```json
{ "plan_points_addressed": [...], "reasoning": "...", "recommendation": "", "confidence": 0.95, "decision": "GO" }
```
Оркестратор применяет проверку-содержательности вердикта, видит **пустой слот
`recommendation`** и помечает вердикт несодержательным → `no_mentor_go` → **судья НЕ
вызывается** → план не печатается. В логе:
`несодержательный вердикт: пустые слоты [recommendation]` + `⏭ пропуск (no_mentor_go)`.
## Корень
Проверка-содержательности (введена M4-аудитом: «слот вердикта обязан быть непустой
строкой, `{}` больше не содержателен») применяется **одинаково** к GO и NO-GO. Для NO-GO
непустой `recommendation`/objection логичен. Для **GO** пустой `recommendation` — нормальное
состояние (плану нечего рекомендовать), но проверка его отбраковывает → валидный GO теряется
→ судья пропускается → печать не встаёт. Дефект делает невозможной печать «идеального» плана,
к которому у наставника нет замечаний.
## Воспроизведение
1. Записать чистый, корректный опечатываемый план (без спорных мест).
2. Наставник возвращает `decision:GO` с `recommendation:""`.
3. Печать не встаёт; в логе `no_mentor_go`, судья не вызывался.
**Обход (подтверждён 16.06):** добавить в `## Переговоры` плана круг, прямо приглашающий
наставника записать forward-`recommendation` (напр. «при одобрении зафиксируй в
recommendation указание исполнителю …»). Тогда слот непуст → судья вызывается → печать встаёт.
Это костыль уровня контроллера, не лечит корень.
## Предлагаемый фикс (корень)
Сделать проверку-содержательности **decision-aware**:
- `decision === 'GO'` → пустой `recommendation` **допустим** (достаточно непустого `reasoning`
и/или `plan_points_addressed`); вердикт содержателен.
- `decision === 'NO-GO'` → требовать непустой `recommendation`/objection (как сейчас).
## Где искать в коде
Слой, считающий `no_mentor_go` и проверяющий содержательность вердикта наставника:
`mentor-verdict.mjs` / `judge-orchestrator.mjs` / `enforce-mentor-then-judge.mjs`. Искать
проверку пустых слотов вердикта (`пустые слоты`, `no_mentor_go`).
## Доказательство (лог AITUNNEL)
16.06.2026 14:17:32, эндпоинт `/v1/messages`, модель `deepseek-v4-pro`, ключ «Agent mentor»,
ответ ASSISTANT: `"decision": "GO"`, `"confidence": 0.95`, `"recommendation": ""`,
`plan_points_addressed` заполнен (обе претензии сняты) → оркестратор: `no_mentor_go`, судья не
вызван. На следующей итерации плана с Переговоры-кругом, приглашающим recommendation, наставник
вернул GO с непустым `recommendation` → судья вызвался → план опечатан.
## Связанная находка (для того же разбора)
Десинк **criterion-gate**: `enforce-supreme-gate` удаляет frozen-plan на совпадении
**последнего** шага плана раньше, чем `enforce-criterion-gate` (со-хук) проверит
`frozenPlanValid` → ложный блок git push «кодовое изменение без валидного запечатанного
плана». Лечится не-терминальным мутирующим шагом, но корень — порядок хуков (supreme-gate не
должен удалять печать, если со-хук в той же цепочке блокнёт действие). Тот же класс, что F-J.
@@ -0,0 +1,63 @@
# SP2a — round-memory-store + version-diff (дизайн)
Часть эпика «полный единый цикл переговоров». Слой данных памяти кругов: версии артефакта,
дословные замечания сторон, доводы контроллера раздельно по адресату, и построчный показ
изменений между версиями. Потребители (блоки промптов, оркестрация стадий, арбитраж) —
отдельные под-спеки и здесь не реализуются.
## Цель
Дать машинерии переговоров склад памяти: хранить версии спеки и плана, дословные замечания
наставника и судьи, доводы контроллера раздельно по адресату (наставнику / судье), и показывать
изменения между соседними версиями построчным diff. Без этого склада память кругов, две дорожки
доводов и возврат «наставник после судьи» нереализуемы.
## Контракт API {#ct1}
Модуль `tools/round-memory-store.mjs` (I/O, fail-quiet) экспортирует:
- `recordVersion(taskId, stage, text)` / `getVersions(taskId, stage)` — снимок и список версий.
- `diffVersions(taskId, stage)` — построчный diff двух последних версий (пусто, если версий <2).
- `recordObjection(taskId, stage, side, text)` / `getObjections(taskId, stage, side)` — замечания
дословно; `side``mentor | judge`.
- `recordArg(taskId, stage, track, text)` / `getArgs(taskId, stage, track)` — доводы контроллера
дословно; `track``M | J`.
- `clearRoundMemory(taskId)` — стереть память задачи (вызовут при печати/обрыве в оркестрации).
Ключ — пара `(taskId, stage)`, `stage``spec | plan` (спека и план хранятся раздельно — цикл
прогоняется дважды). Любая ошибка I/O → безопасный no-op (запись) или пустое значение (чтение).
Модуль `tools/version-diff.mjs` (чистый) экспортирует `diffLines(oldText, newText)` → строковый
блок изменений.
## Алгоритм diff {#al1}
`diffLines` сравнивает тексты построчно и возвращает читаемый блок: неизменные строки опускаются
или помечаются нейтрально, удалённые — префиксом `- `, добавленные — префиксом `+ `. Простой
проход по строкам (общий префикс/суффикс + середина), без внешних зависимостей. `diffVersions`
берёт две последние версии стадии и зовёт `diffLines(пред, тек)`.
## Крайние случаи {#ed1}
- Версий нет или одна → `diffVersions` возвращает пусто. Идентичные тексты → пустой diff.
- Битый baseDir / неразборный JSON → fail-quiet: запись возвращает `false`, чтение — `[]`/`null`.
- Отсутствующий `taskId` → ключ `unknown` (как в существующих транзиентных сторах).
- Неизвестные `stage`/`side`/`track` — не валидируются жёстко; пишутся как есть (потребитель
отвечает за словарь значений). Пустой `text` допустим.
## Конвенция именования {#nm1}
Файлы: `tools/round-memory-store.mjs`, `tools/version-diff.mjs` (+ парные `*.test.mjs`). Стиль
каталога `tools/`: ESM `.mjs`, fail-quiet I/O, один JSON на задачу в state-каталоге runtime. Зеркало
существующего транзиентного стора (`verdict-surface-store`) по структуре и обработке ошибок.
## Критерий приёмки {#ac1}
Покрытие vitest: `version-diff` (добавление / удаление / идентичные / пустой вход) и
`round-memory-store` (record→get round-trip; версии→diff; изоляция по `(taskId, stage)`; раздельность
`side`/`track`; `clearRoundMemory`; fail-quiet на битом baseDir). Достоверный полный свод — терминал
владельца (harness-collapse под фоновым прогоном).
```verified-context-json
[{"id":"vx1","kind":"EXTRACTED","ref":"tools/verdict-surface-store.mjs","anchor":"export function pushVerdict"}]
```
+1
View File
@@ -7,6 +7,7 @@ const BAR = '━'.repeat(36);
/** Сырой вердикт судьи/наставника → исход. wired:false = degraded; иначе по decision. */
export function classifyJudgeOutcome(verdict) {
if (!verdict || typeof verdict !== 'object') return 'skip';
if (verdict.skip) return 'skip';
if (verdict.wired === false) return 'degraded';
const d = String(verdict.decision || '').toUpperCase().replace(/\s+/g, '');
if (d === 'GO') return 'GO';
+4
View File
@@ -8,6 +8,10 @@ describe('classifyJudgeOutcome', () => {
expect(classifyJudgeOutcome({ wired: false })).toBe('degraded');
expect(classifyJudgeOutcome(null)).toBe('skip');
});
it('skip-вердикт (судья не судил) → skip, не degraded', () => {
expect(classifyJudgeOutcome({ wired: false, skip: 'no_mentor_go' })).toBe('skip');
expect(classifyJudgeOutcome({ wired: false, unavailable: true })).toBe('degraded');
});
});
describe('buildVerdictBanner', () => {
it('баннер с гейтом и дословной причиной', () => {