From 41105b615e96c4bbb3680588a37b833dd645acd7 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: Tue, 23 Jun 2026 09:09:09 +0300 Subject: [PATCH] =?UTF-8?q?docs(secretary):=20=D1=81=D0=BF=D0=B5=D0=BA?= =?UTF-8?q?=D0=B0=20=E2=80=94=20=D1=81=D0=B5=D0=BA=D1=80=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D1=8C=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D0=B5=D1=82=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D1=83=20?= =?UTF-8?q?=C2=AB=D0=A5=D0=BE=D0=B4=D0=B0=C2=BB=20(=D0=A1=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Витрина «Шаги» сейчас режется детерминированно (firstSentence: слепой обрез 130 знаков посреди слова, пример — ход 12 «протокола»). Дизайн: суть хода формулирует тот же секретарь-модель — одно поле step{user,assistant} в том же reconcile-вызове (без лишней платы). Инструменты и ссылка на Слой 1 остаются детерминированными (хук, не LLM). Фолбэк на сбой; модельный текст переживает выключение (слияние, не перезатир buildStepsFromRaw). Слой 1 не трогаем. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...06-23-secretary-step-formulation-design.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-23-secretary-step-formulation-design.md 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` / реестр скрытых вопросов.