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:
@@ -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"}]
|
||||
```
|
||||
@@ -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';
|
||||
|
||||
@@ -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('баннер с гейтом и дословной причиной', () => {
|
||||
|
||||
Reference in New Issue
Block a user