From 4f5c9287965130b211d815d5f114070c3d48a12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Wed, 17 Jun 2026 05:43:18 +0300 Subject: [PATCH] fix: classifyJudgeOutcome skip vs degraded plus bug notes and SP2a design Co-Authored-By: Claude Opus 4.8 --- ...oy-commit-not-executable-under-wall-bug.md | 89 +++++++++++++++++++ ...6-06-16-mentor-empty-recommendation-bug.md | 69 ++++++++++++++ .../2026-06-16-sp2a-round-memory-design.md | 63 +++++++++++++ tools/verdict-outcome-line.mjs | 1 + tools/verdict-outcome-line.test.mjs | 4 + 5 files changed, 226 insertions(+) create mode 100644 bags/2026-06-16-deploy-commit-not-executable-under-wall-bug.md create mode 100644 bags/2026-06-16-mentor-empty-recommendation-bug.md create mode 100644 docs/superpowers/specs/2026-06-16-sp2a-round-memory-design.md diff --git a/bags/2026-06-16-deploy-commit-not-executable-under-wall-bug.md b/bags/2026-06-16-deploy-commit-not-executable-under-wall-bug.md new file mode 100644 index 0000000..fa40c8a --- /dev/null +++ b/bags/2026-06-16-deploy-commit-not-executable-under-wall-bug.md @@ -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:`. Застрявший неисполнимый план блокирует и последующую обычную работу. + +## Где именно ломается (точки для 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`. diff --git a/bags/2026-06-16-mentor-empty-recommendation-bug.md b/bags/2026-06-16-mentor-empty-recommendation-bug.md new file mode 100644 index 0000000..5fdb8b1 --- /dev/null +++ b/bags/2026-06-16-mentor-empty-recommendation-bug.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. diff --git a/docs/superpowers/specs/2026-06-16-sp2a-round-memory-design.md b/docs/superpowers/specs/2026-06-16-sp2a-round-memory-design.md new file mode 100644 index 0000000..8579e99 --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-sp2a-round-memory-design.md @@ -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"}] +``` diff --git a/tools/verdict-outcome-line.mjs b/tools/verdict-outcome-line.mjs index 46d3293..e601dcc 100644 --- a/tools/verdict-outcome-line.mjs +++ b/tools/verdict-outcome-line.mjs @@ -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'; diff --git a/tools/verdict-outcome-line.test.mjs b/tools/verdict-outcome-line.test.mjs index cf45bb6..1456c72 100644 --- a/tools/verdict-outcome-line.test.mjs +++ b/tools/verdict-outcome-line.test.mjs @@ -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('баннер с гейтом и дословной причиной', () => {