diff --git a/docs/superpowers/plans/2026-06-17-mentor-silent-swallow-fix-impl-r2.md b/docs/superpowers/plans/2026-06-17-mentor-silent-swallow-fix-impl-r2.md new file mode 100644 index 0000000..c27d67e --- /dev/null +++ b/docs/superpowers/plans/2026-06-17-mentor-silent-swallow-fix-impl-r2.md @@ -0,0 +1,96 @@ +# Наставник-хук: фикс silent-swallow → видимый degraded (re-trigger) Implementation Plan + +**Goal:** Обернуть регион производства вердикта наставника (память кругов + `onPlanWrite`/`onSpecWrite`) +в `try/catch`, чтобы throw в нём возвращал видимый `{ran:true, wired:false}` (degraded) вместо +молчаливого пробрасывания в `catch` main() — план/спека больше не зависают без сигнала. + +**Architecture:** Правка только в `tools/enforce-mentor-on-plan-write.mjs` (обе ветки — план и спека). +`planHash`/`specHash` поднимаются выше региона (нужны в `catch` для binding снимка). Успешный путь +байт-в-байт прежний; degraded-контракт (`wired:false`) уже обрабатывается main() штатно (снимок +degraded + decideMentorObjection degraded-блок). Тесты — инъекцией `roundMemoryImpl`, который бросает. + +**Tech Stack:** Node ESM, vitest (config `vitest.config.tools.mjs`). TDD. + +## Цель + +Превратить молчаливый срыв наставника в видимый degraded-вердикт (Уроки №7), чтобы зависший +наставник был ВИДЕН в снимке и перезапускался, а не выглядел вечным «считает». Обе ветки (план и +спека) симметрично. + +## Структура файлов + +- Изменить (тест): `tools/enforce-mentor-on-plan-write.test.mjs` — 2 теста (план/спека: throw в + регионе → ran:true, wired:false). +- Изменить: `tools/enforce-mentor-on-plan-write.mjs` — guard план-ветки и спека-ветки. + +```skills-json +["test-driven-development"] +``` + +```steps-json +[ + {"op":"Edit","object":"tools/enforce-mentor-on-plan-write.test.mjs","ref":"m4"}, + {"op":"Bash","object":"npx vitest run tools/enforce-mentor-on-plan-write.test.mjs --config vitest.config.tools.mjs --no-file-parallelism","ref":"m4"}, + {"op":"Edit","object":"tools/enforce-mentor-on-plan-write.mjs","ref":"m1"}, + {"op":"Bash","object":"npx vitest run tools/enforce-mentor-on-plan-write.test.mjs --config vitest.config.tools.mjs --no-file-parallelism","ref":"m4"}, + {"op":"Edit","object":"tools/enforce-mentor-on-plan-write.mjs","ref":"m1"}, + {"op":"Bash","object":"npx vitest run tools/enforce-mentor-on-plan-write.test.mjs --config vitest.config.tools.mjs --no-file-parallelism","ref":"m4"}, + {"op":"Bash","object":"npx vitest run --config vitest.config.tools.mjs --no-file-parallelism","ref":"m4"} +] +``` + +```verified-context-json +[{"id":"mpc2","kind":"EXTRACTED","ref":"tools/enforce-mentor-on-plan-write.mjs","anchor":"export async function runMentorOnPlanWrite("}] +``` + +--- + +### Task 1: тесты «throw в регионе → видимый degraded» (§m4) + +- [ ] **Step 1 (Edit `tools/enforce-mentor-on-plan-write.test.mjs`): 2 падающих теста** + +В конец describe `runMentorOnPlanWrite` (перед его закрывающим `});`) дописать: план с +`roundMemoryImpl`, который бросает → `{ran:true, wired:false}` + непустой `planHash` + `reason`/срыв; +то же для `specEvent`. + +- [ ] **Step 2 (Bash): прогон — тесты падают (RED)** + +Run: `npx vitest run tools/enforce-mentor-on-plan-write.test.mjs --config vitest.config.tools.mjs --no-file-parallelism` +Expected: FAIL — сейчас throw в регионе реджектит промис. (Под Claude harness-collapse; авторитет — терминал владельца.) + +### Task 2: guard обеих веток (§m1) + +- [ ] **Step 3 (Edit `tools/enforce-mentor-on-plan-write.mjs`): guard план-ветки** + +`planHash` поднять выше региона; обернуть `roundMemoryP` + `onPlanWrite` в `try/catch`; на throw — +вернуть `{ran:true, ok:false, wired:false, reason:'наставник-путь сорвался: ', taskId:taskIdForPrompt, planHash, verdict:null, routerClassification:null}`. + +- [ ] **Step 4 (Bash): прогон — план-тест зелёный, спека-тест ещё красный** + +Run: `npx vitest run tools/enforce-mentor-on-plan-write.test.mjs --config vitest.config.tools.mjs --no-file-parallelism` +Expected: план-ветка проходит, спека-ветка ещё падает. (Авторитет — терминал владельца.) + +- [ ] **Step 5 (Edit `tools/enforce-mentor-on-plan-write.mjs`): guard спека-ветки** + +Обернуть `roundMemoryS` + `onSpecWrite` в `try/catch`; на throw — вернуть `{ran:true, ok:false, wired:false, reason:'наставник-путь (спека) сорвался: ', taskId:taskIdForPromptS, planHash:specHash, verdict:null}`. + +- [ ] **Step 6 (Bash): прогон — оба теста зелёные (GREEN)** + +Run: `npx vitest run tools/enforce-mentor-on-plan-write.test.mjs --config vitest.config.tools.mjs --no-file-parallelism` +Expected: PASS — оба новых теста + прежние тесты файла без регрессий. (Авторитет — терминал владельца.) + +- [ ] **Step 7 (Bash): полная регрессия tools** + +Run: `npx vitest run --config vitest.config.tools.mjs --no-file-parallelism` +Expected: PASS — база + новые тесты, 0 регрессий. (Под Claude harness-collapse; авторитетный свод — терминал владельца.) + +## Self-Review (план против спеки) + +- **§m1 guard** — Task 2: обе ветки обёрнуты, `planHash`/`specHash` выше региона для binding в catch, + degraded-возврат `wired:false`. ✓ +- **§m2 крайние случаи** — throw в `roundMemoryImpl` и внутри `onPlanWrite`/`onSpecWrite` ловится; + транспортный сбой по-прежнему через `runMentorVerdict` (не трогаем). ✓ +- **§m4 критерий** — RED (throw реджектит) → GREEN (degraded) для плана и спеки; прежние тесты целы. ✓ +- **DR-1** — мутирующие шаги 1,3,5 сопровождены Bash (2,4,6); шаги 3,5 один файл, между ними Bash (4); + шаг 7 — отдельная полная регрессия. ✓ +- **Конвенция §m3** — аддитивный точечный diff, успешный путь не меняется, без новых файлов. ✓ diff --git a/docs/superpowers/router-mentor-wall-GUIDE.md b/docs/superpowers/router-mentor-wall-GUIDE.md index 2d251c2..f287d8e 100644 --- a/docs/superpowers/router-mentor-wall-GUIDE.md +++ b/docs/superpowers/router-mentor-wall-GUIDE.md @@ -267,3 +267,58 @@ Verify-шаги под стеной сдвигают указатель, но GR (`claude-sonnet-4-6`→`deepseek-v4-pro`, `HEAVY_LLM_TIMEOUT_MS` 90000→300000) ломает 3 теста (`router-config`, `enforce-mentor-on-plan-write` timeout, `enforce-judge-gate` parse `plan_soundness`) — это пре-существующий дрейф, НЕ регресс твоей задачи. Не путать со своими провалами. + +## Уроки сессии №6 (2026-06-17) — escape-рецепт + verify-gate + +- **Метка escape — в LABEL опции, НЕ в описании.** `enforce-askuser-answer-parser` подписывает + ВЫБРАННЫЙ ответ = текст label. Маркер `FLOOR-ESCAPE: ` в `description` НЕ подписывается + (проверено вживую: Edit блокнут «разговорный режим», грант не записан). Клади ТОЧНУЮ каноническую + строку прямо в `label`. +- **Один маркер на вопрос; батч = несколько ВОПРОСОВ.** Две метки в одном label (через перенос + строки) НЕ подписываются. «Разные файлы в одном AskUser» = массив `questions[]` (до 4), каждый + вопрос = один грант. Один файл несколько раз = РАЗНЫЕ раунды/вопросы. +- **Грант одноразовый — ВСЕ правки файла ОДНИМ Edit.** `enforce-floor-escape-consume` гасит грант на + первом УСПЕШНОМ Edit; вторая правка того же файла под тем же грантом блокнётся. Делай широкий + `old_string`, охватывающий все изменения, либо запрашивай отдельный грант. Упавший Edit грант НЕ + гасит (можно повторить). Read-перед-Edit обязателен и тут — Grep чтение не засчитывается. +- **`enforce-verify-gate` ≠ escape/override.** На `git commit`/`push` через Claude требует ПОДПИСАННУЮ + `~/.claude/runtime/verify-receipt.json` (fingerprint == staged-diff) от `node tools/produce-verify-receipt.mjs`. + Ни `floor_escape`, ни `ремонт инфраструктуры` его не снимают. А продюсер в claude-brain СЛОМАН (смотрит + на `app/vitest.config.tools.mjs` после сплита → always suite-not-passed, см. `bags/2026-06-17-produce-verify-receipt-app-path-bug.md`) + → Claude-коммиты тут тупиковы, коммить в терминале владельца (verify-gate — Claude-PreToolUse-хук). +- **`node tools/verdict-wait.mjs` разрешён в стене** как read-only сторож видимости вердиктов + (`isObserveOnly` regex без цепочек/редиректов) — для по-этапного ожидания вердиктов под стеной. + +## Уроки сессии №7 (2026-06-17) — диагностика «наставник не вернулся» (молчаливый срыв) + +**Симптом.** Написал план, печать не встаёт, режим разговорный. В снимке вердиктов нет записи по +плану (нет 4-го `hash`), наставник/судья по плану не появляются — будто «ещё считает» бесконечно. + +**Причина (корневой дефект машинерии).** `main()` в `tools/enforce-mentor-on-plan-write.mjs` +обёрнут в `catch { /* производитель никогда не блокирует */ }`. Любой throw МЕЖДУ вызовом роутера +(`classifyImpl`/`classify`) и LLM-вердиктом наставника проглатывается МОЛЧА — нет вердикта, нет +записи в снимок, нет печати, нет ошибки. Незащищённое место: `renderSkillContext` +([tools/on-plan-write.mjs](../../tools/on-plan-write.mjs) ~стр.71) НЕ в `try`; ранний throw там или +в `runMentorVerdict` до транспорта → всплывает в молчаливый catch. В снимке +**«упало-и-проглотилось» НЕОТЛИЧИМО от «ещё считает»** — в обоих случаев записи просто нет. + +**Чем срыв отличается от медленной модели.** В логах вызовов владельца (внешняя панель провайдера): +роутер по плану отработал (есть строка `Agent Router` / `recommend`), а **второго вызова +`Agent mentor` после него НЕТ**. Это срыв, а не латентность. Если доступа к логам нет — считать +«вердикта нет дольше 1-2 циклов ПОСЛЕ того как роутер точно отработал» = вероятный молчаливый срыв. + +**Рецепт (что делать — НЕ повторять прокол сессии №7):** + +1. **НЕ опрашивать снимок по кругу и НЕ гадать «медленная модель».** «Нет 4-го hash» — это НЕ + диагноз (срыв и счёт выглядят одинаково). +2. **Сразу читать оркестратор** (`enforce-mentor-on-plan-write.mjs` + `on-plan-write.mjs`) — чтение + в разговорном режиме свободно. Помнить про молчаливый catch: он прячет срыв. +3. **Лечение — перезапуск плана НОВЫМ именем файла** (свежий `plan_id` фитилит наставника заново; + `planId` от steps, поэтому при идентичных шагах слегка варьируй один шаг, напр. `--reporter dot`). + Транзиент обычно не повторяется — r2 печатается чисто. Это быстрее, чем ждать/спрашивать владельца. +4. **Опрос снимка полезен ТОЛЬКО чтобы поймать момент печати** (Grep `verdict-snapshot` блокируется + стеной = план опечатан = режим исполнения), но НЕ как диагноз зависания. + +**Мета-урок.** При «застряло» — сначала прочитать код пути (средства под рукой в разговорном +режиме), потом действовать. НЕ скатываться в «ждать или спросить владельца», когда можешь сам +диагностировать чтением. Owner видит вердикты в логах — но это не повод не открыть код самому. diff --git a/docs/superpowers/specs/2026-06-17-mentor-silent-swallow-fix-design.md b/docs/superpowers/specs/2026-06-17-mentor-silent-swallow-fix-design.md new file mode 100644 index 0000000..ba7df14 --- /dev/null +++ b/docs/superpowers/specs/2026-06-17-mentor-silent-swallow-fix-design.md @@ -0,0 +1,59 @@ +# Наставник-хук: молчаливый срыв в видимый degraded (фикс silent-swallow) + +**Дата:** 2026-06-17 · **Статус:** дизайн под реализацию (TDD). Багфикс машинерии переговоров. + +## Цель + +`main()` в `tools/enforce-mentor-on-plan-write.mjs` обёрнут в `catch {}` («производитель никогда не +блокирует»). Побочка: любой throw в регионе производства вердикта наставника — МЕЖДУ вызовом роутера +и LLM-вердиктом (например `renderSkillContext`, загрузчик памяти кругов, сборка промпта) — +проглатывается молча. Итог: ни вердикта, ни записи в снимок, ни печати, ни сигнала; план тихо не +печатается, а в снимке «упало-и-проглотилось» НЕОТЛИЧИМО от «ещё считает». Цель — превратить такой +срыв в **видимый degraded-вердикт** (`wired:false`), который проходит штатным каналом видимости +(снимок `mentor:plan`/`mentor:spec` = degraded) и блокировки (degraded-сообщение контроллеру), чтобы +зависший наставник был ВИДЕН и перезапускался, а не выглядел вечным «считает». + +## Контракт guard'а {#m1} + +В `runMentorOnPlanWrite` регион производства вердикта (загрузка памяти кругов + вызов +`onPlanWrite`/`onSpecWrite`) оборачивается в `try/catch`: + +- успех — поведение прежнее (возврат `{ran:true, ok, wired, ...}` без изменений); +- throw в регионе — НЕ пробрасывается наружу (раньше всплывал в молчаливый `catch` main()), а + возвращается как `{ran:true, ok:false, wired:false, reason:'наставник-путь сорвался: ', + taskId, planHash|specHash, verdict:null}`. + +`wired:false` — это ровно тот degraded-контракт, что уже умеет `main()`: снимок пишет стадию +`degraded`, `decideMentorObjection` отдаёт degraded-блок (recordMentorGo:false, escalation не растёт). +`planHash`/`specHash` доступны в `catch` (считаются ДО региона), поэтому снимок несёт binding. + +## Крайние случаи {#m2} + +- throw в загрузчике памяти кругов (`roundMemoryImpl`) — degraded, не молчаливый throw. +- throw внутри `onPlanWrite`/`onSpecWrite` до транспорта (`renderSkillContext` и пр.) — degraded. +- сбой ТРАНСПОРТА LLM (сеть/таймаут) — уже ловится внутри `runMentorVerdict` → `wired:false` штатно; + этот путь не меняется (остаётся degraded, как был). +- оба пути (план и спека) симметричны — guard ставится в обеих ветках. + +## Конвенция {#m3} + +Минимальный аддитивный diff: обернуть существующий регион в `try/catch`, `planHash`/`specHash` +поднять выше региона (нужны в `catch`). Успешный путь байт-в-байт прежний. Без новых файлов и +зависимостей. Прочая логика `main()` (снимок, счётчик, decideMentorObjection) переиспользуется как +есть — она уже корректно обрабатывает `wired:false`. + +## Критерий приёмки {#m4} + +TDD-тесты в `tools/enforce-mentor-on-plan-write.test.mjs` (инъекция `roundMemoryImpl`, который +бросает — доступная точка throw в защищаемом регионе): + +- план: `roundMemoryImpl` бросает → `runMentorOnPlanWrite` НЕ реджектит, а возвращает + `{ran:true, wired:false}` с непустым `planHash` и `reason` про срыв; +- спека: то же для spec-пути (`specEvent`) → `{ran:true, wired:false}`; +- существующие тесты (сбой транспорта, GO, NO-GO, видимость) — без регрессий. + +Полная регрессия tools зелёная: `npx vitest run --config vitest.config.tools.mjs --no-file-parallelism`. + +```verified-context-json +[{"id":"mc1","kind":"EXTRACTED","ref":"tools/enforce-mentor-on-plan-write.mjs","anchor":"производитель никогда не блокирует"}] +``` diff --git a/tools/enforce-mentor-on-plan-write.mjs b/tools/enforce-mentor-on-plan-write.mjs index 5fecfda..29ed1a6 100644 --- a/tools/enforce-mentor-on-plan-write.mjs +++ b/tools/enforce-mentor-on-plan-write.mjs @@ -135,22 +135,29 @@ export async function runMentorOnPlanWrite(event, { let graphSectionS = null; try { graphSectionS = graphSectionImpl(); } catch { graphSectionS = null; } const verifiedContextS = parseVerifiedContext(content); - // SP2c-2: память кругов M-side спеки (свои замечания + M-доводы + diff + замечание судьи). - const roundMemoryS = roundMemoryImpl ? roundMemoryImpl({ stage: 'spec', content, taskId: taskIdForPromptS }) : {}; - const rs = await onSpecWrite({ - specContent: content, - specHash, - existingTaskId: loadTaskIdImpl(), - persistTaskIdImpl, - llmCall, - journalEntries: journalS.entries, - journalKey, - nowMs, - verifiedContext: verifiedContextS, - negotiationLog: negotiationLogS, - graphSection: graphSectionS, - roundMemory: roundMemoryS, - }); + // Фикс silent-swallow (Уроки №7): симметрично план-ветке — throw в регионе спеки → видимый + // degraded (wired:false), не молчаливый реджект в catch main(). + let rs; + try { + // SP2c-2: память кругов M-side спеки (свои замечания + M-доводы + diff + замечание судьи). + const roundMemoryS = roundMemoryImpl ? roundMemoryImpl({ stage: 'spec', content, taskId: taskIdForPromptS }) : {}; + rs = await onSpecWrite({ + specContent: content, + specHash, + existingTaskId: loadTaskIdImpl(), + persistTaskIdImpl, + llmCall, + journalEntries: journalS.entries, + journalKey, + nowMs, + verifiedContext: verifiedContextS, + negotiationLog: negotiationLogS, + graphSection: graphSectionS, + roundMemory: roundMemoryS, + }); + } catch (e) { + return { ran: true, ok: false, wired: false, reason: `наставник-путь (спека) сорвался: ${e && e.message ? e.message : e}`, taskId: taskIdForPromptS, planHash: specHash, verdict: null }; + } try { persistVerdictImpl({ ok: rs.ok, wired: rs.wired, reason: rs.reason ?? null, planHash: specHash, verdict: rs.verdict }); } catch { /* best-effort */ } if (rs.journalOk && rs.journal) { try { persistJournalImpl(rs.journal); } catch { /* best-effort (SE10) */ } } return { ran: true, ok: rs.ok, wired: rs.wired, reason: rs.reason, taskId: rs.taskId, planHash: specHash, verdict: rs.verdict }; @@ -177,26 +184,35 @@ export async function runMentorOnPlanWrite(event, { const planGoal = extractPlanGoal(content); let registry = null; try { registry = typeof registryImpl === 'function' ? registryImpl() : null; } catch { registry = null; } - // SP2c-2: память кругов M-side плана (свои замечания + M-доводы + diff + замечание судьи). - const roundMemoryP = roundMemoryImpl ? roundMemoryImpl({ stage: 'plan', content, taskId: taskIdForPrompt }) : {}; - const r = await onPlanWrite({ - planSteps: steps, - existingTaskId: loadTaskIdImpl(), - persistTaskIdImpl, - llmCall, - journalEntries: journal0.entries, - journalKey, - nowMs, - verifiedContext, - negotiationLog, - graphSection, - classifyImpl, - registry, - declaredSkills, - planGoal, - roundMemory: roundMemoryP, - }); const planHash = planId(steps); + // Фикс silent-swallow (Уроки №7): срыв В РЕГИОНЕ наставника (renderSkillContext в on-plan-write.mjs + // и пр.) раньше всплывал в молчаливый catch main() → ни вердикта, ни снимка, ни печати, ни сигнала + // (в снимке «упало» неотличимо от «считает»). Теперь любой throw → ВИДИМЫЙ degraded (wired:false): + // снимок mentor:plan=degraded + блок «наставник сорвался» → контроллер видит и перезапускает. + let r; + try { + // SP2c-2: память кругов M-side плана (свои замечания + M-доводы + diff + замечание судьи). + const roundMemoryP = roundMemoryImpl ? roundMemoryImpl({ stage: 'plan', content, taskId: taskIdForPrompt }) : {}; + r = await onPlanWrite({ + planSteps: steps, + existingTaskId: loadTaskIdImpl(), + persistTaskIdImpl, + llmCall, + journalEntries: journal0.entries, + journalKey, + nowMs, + verifiedContext, + negotiationLog, + graphSection, + classifyImpl, + registry, + declaredSkills, + planGoal, + roundMemory: roundMemoryP, + }); + } catch (e) { + return { ran: true, ok: false, wired: false, reason: `наставник-путь сорвался: ${e && e.message ? e.message : e}`, taskId: taskIdForPrompt, planHash, verdict: null, routerClassification: null }; + } try { persistVerdictImpl({ ok: r.ok, wired: r.wired, reason: r.reason ?? null, planHash, verdict: r.verdict }); } catch { /* best-effort */ } if (r.journalOk && r.journal) { try { persistJournalImpl(r.journal); } catch { /* best-effort (SE10) */ } } return { ran: true, ok: r.ok, wired: r.wired, reason: r.reason, taskId: r.taskId, planHash, verdict: r.verdict, routerClassification: r.routerClassification ?? null }; diff --git a/tools/enforce-mentor-on-plan-write.test.mjs b/tools/enforce-mentor-on-plan-write.test.mjs index 62803f7..d495f36 100644 --- a/tools/enforce-mentor-on-plan-write.test.mjs +++ b/tools/enforce-mentor-on-plan-write.test.mjs @@ -145,6 +145,26 @@ describe('runMentorOnPlanWrite (обёртка-производитель W7)', const r = await runMentorOnPlanWrite(planEvent, d); expect(r.routerClassification).toEqual({ recommended_chain: ['systematic-debugging'] }); }); + + // Фикс silent-swallow (Уроки №7): срыв В РЕГИОНЕ наставника (между роутером и LLM-вердиктом — + // renderSkillContext, загрузчик памяти кругов и пр.) раньше реджектил промис → молчаливый catch + // main() → ни вердикта, ни снимка, ни печати. Теперь → видимый degraded (ran:true, wired:false). + it('срыв в регионе наставника (план, roundMemoryImpl бросил) → ran:true, wired:false (видимый degraded, не реджект)', async () => { + const d = deps({ roundMemoryImpl: () => { throw new Error('boom-в-регионе'); } }); + const r = await runMentorOnPlanWrite(planEvent, d); + expect(r.ran).toBe(true); + expect(r.wired).toBe(false); + expect(r.reason).toMatch(/сорвал/i); + expect(typeof r.planHash).toBe('string'); + expect(r.planHash.length).toBeGreaterThan(0); + }); + it('срыв в регионе наставника (спека, roundMemoryImpl бросил) → ran:true, wired:false', async () => { + const d = deps({ roundMemoryImpl: () => { throw new Error('boom-спека'); } }); + const r = await runMentorOnPlanWrite(specEvent, d); + expect(r.ran).toBe(true); + expect(r.wired).toBe(false); + expect(r.reason).toMatch(/сорвал/i); + }); }); describe('decideMentorObjection (Фаза 1 — канал замечаний наставника контроллеру)', () => {