diff --git a/docs/superpowers/specs/2026-06-23-secretary-step-formulation-design.md b/docs/superpowers/specs/2026-06-23-secretary-step-formulation-design.md new file mode 100644 index 0000000..29328ba --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-secretary-step-formulation-design.md @@ -0,0 +1,109 @@ +# Секретарь формулирует строку «Хода» (Слой 2 — витрина «Шаги») + +**Дата:** 2026-06-23 · **Статус:** дизайн на ревью · **Автор:** контроллер + владелец + +## Цель + +Сделать раздел **«Шаги (Слой 1)»** протокола читаемой летописью «как мы до этого +дошли», которую можно отдать другому агенту — и он поймёт ход работы, **не ныряя +в Слой 1**. Сейчас строка хода строится детерминированно (`buildStepLine` → +`firstSentence`): для длинных реплик без точек это слепой обрез по 130 знаков +посреди слова (пример — ход 12 дела «протокол»: «…реально скармливаем наставн…», +второй вопрос юзера потерян). Слой 1 (сырьё) остаётся как было — это страховка на +случай сбоя секретаря; меняем только витрину Слоя 2. + +## Решение (суть) + +Строку-суть хода формулирует **тот же секретарь-модель**, что уже пишет протокол +(вызов `callModel` на `SECRETARY_LLM_KEY` в stop-hook). Модель уже получает весь +протокол + текущий обмен — добавляем в её JSON-ответ **одно поле `step`**. Лишнего +вызова и платы нет. Детерминированным остаётся только то, чему модель доверять +нельзя: список реальных инструментов хода и ссылка на Слой 1 — их дописывает хук. + +## Контракты по файлам + +### 1. `tools/secretary-reconcile.mjs` + +- **`buildReconcilePrompt`** — в system добавить правило и попросить поле `step`: + > `step` — суть ТЕКУЩЕГО хода без воды: `{ "user": "<что юзер хотел/спросил>", + > "assistant": "<что ассистент сделал/выяснил/решил/предложил + ключевые + > находки>" }`. Убирай воду, вежливость, повторы; длина по содержанию (короткий + > ход — короче). Факты не выдумывай. Инструменты НЕ перечисляй — их подставит + > система. +- **`parseReconcileResponse`** — читать `parsed.step`; нормализовать в + `{ user: string, assistant: string }` или `null` (если поля нет/кривое). +- **`reconcileTurn`** — приложить `step` к возвращаемому объекту (`stampProvenance` + отдаёт фиксированную форму без `step`, поэтому `{ ...stampProvenance(...), step }`). + При срыве модели (`null`) — `step` отсутствует, дальше работает фолбэк. + +`step` — **транзитное** поле результата reconcile: его потребляет stop-hook для +строки шага и **в `protocol.json` НЕ сохраняет** (хук срезает перед записью). Не +часть схлопываемых корзин; `collapseProtocol` его не трогает. + +### 2. `tools/secretary-layer1.mjs` + +- **`buildStepLine`** — добавить необязательный вход `essence = { user, assistant }`. + Если `essence` задан и непустой — `u = essence.user`, `a = essence.assistant` + (лёгкая чистка пробелов, без `firstSentence`/обреза). Если нет — прежнее + поведение (`firstSentence`/`sysLabel`) как фолбэк. `делал: <инструменты>` + (дедуп реальных tool-имён) и шаблон `Ход N — я: … · ты: … · делал: …` — + общие для обоих путей. +- **`buildStepsFromRaw`** — НЕ менять формулировку (остаётся детерминированной: + это путь восстановления из сырья). Меняется только то, КАК его зовут (см. п. 3). + +### 3. `tools/secretary-stop-hook.mjs` + +- На каждом ходу: если `updated.step` есть — строить шаг через + `buildStepLine({ turn, actions, essence: updated.step })`; иначе — прежний + детерминированный `buildStepLine({ turn, user: ex.user, assistant: ex.assistant, + actions })`. Результат — в `step.text`, дальше как сейчас + (`mergeTurnIntoProtocol` пишет шаг ВСЕГДА — гарантия целостности «Шагов» цела). + +### 4. `tools/secretary-prompt-hook.mjs` (ветка `off`, нарезка) + +- Сейчас: `proto.steps = buildStepsFromRaw(raw, session)` — пересобирает ВСЕ шаги + из сырья, **затирая модельные формулировки**. Цель этого кода — заполнить ходы, + где секретарь был выключен (без пропусков), а не переписать хорошие. +- Меняем на **слияние по ходу**: брать существующий `proto.steps[turn].text`, а из + `buildStepsFromRaw` достраивать ТОЛЬКО ходы, которых в `proto.steps` нет. Модельный + текст переживает выключение/нарезку; ссылки `ходы/turn-N.log` проставляются как + сейчас (`prepareTurnFiles`). + +## Поток данных + +``` +Stop (ход N, секретарь ON) + → reconcileTurn(): модель возвращает протокол + step{user,assistant} + → stop-hook: step есть? buildStepLine(essence=step) ИНАЧЕ buildStepLine(firstSentence) + → + «делал: <реальные tools>» + ссылка Слой 1 (детерминированно, хук) + → mergeTurnIntoProtocol: шаг записан ВСЕГДА + → collapseProtocol → write + +UserPrompt «выключи секретаря» (off) + → слияние: существующие модельные шаги + достройка пропущенных ходов из сырья + → нарезка ходы/turn-N.log + ссылки → write +``` + +## Гарантии и фолбэки + +- **Шаг есть всегда** (сбой/нет ключа/кривой JSON → детерминированная короткая + строка). При мёртвом секретаре протокол и так не ведётся — Слой 1 страхует. +- **Инструменты и ссылка — факт, не модель** (хук, не LLM): нет галлюцинаций tool-имён. +- **Модельный текст переживает выключение/нарезку** (слияние, не перезатир). +- **Слой 1 без изменений** — сырьё пишется как было (страховка/восстановление). + +## Тесты (TDD) + +- `parseReconcileResponse`: читает `step{user,assistant}`; нет/кривой → `step` null. +- `buildStepLine`: с `essence` берёт модельный текст дословно + дописывает + `делал: `; без `essence` — прежний `firstSentence`-фолбэк (старые тесты целы). +- `reconcileTurn`: при ответе модели со `step` — `result.step` проброшен; при срыве — нет. +- Слияние на `off`: существующий модельный `text` сохранён; пропущенный ход достроен + из сырья; ссылки `ходы/turn-N.log` проставлены. + +## Не-цели (YAGNI) + +- Не трогаем Слой 1 (формат сырья, нарезку файлов). +- Не переформулируем задним числом прошлые ходы (модель видит только текущий обмен). +- Не добавляем отдельный вызов/агента — только поле в существующем reconcile-вызове. +- Не меняем 6 корзин / `collapseProtocol` / реестр скрытых вопросов.