feat(secretary): захват выдачи инструмента (N3) + сверка имени дела при включении (N2)

- parseLastExchange привязывает результат инструмента к действию по tool_use_id,
  склеивает text-блоки, усекает до 1200 симв.; [ВЫДАЧА] в Слое 1 теперь наполняется
- resolveCaseActivation: похожее имя дела (опечатка/подстрока) -> переспросить,
  не заводя дело-двойник; хук secretary-prompt-hook выводит подсказку с кандидатами
- TDD: тесты secretary-transcript/flag/prompt-hook; полный свод зелёный

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-22 17:21:06 +03:00
parent 3d6ef98e55
commit f8a40da56c
9 changed files with 365 additions and 14 deletions
@@ -0,0 +1,69 @@
# Спека: секретарь — захват выдачи инструмента + сверка имени дела
## Цель
Закрыть две рекомендации по секретарю:
- **№3** — Слой 1 (сырьё) не сохраняет результаты инструментов: строка `[ВЫДАЧА]` пустая. Решение опиралось на вывод теста/файла — из архива это не восстановить.
- **№2** — имя дела (кодовое слово) при включении пишется во флажок без сверки со списком существующих дел: опечатка или сокращение молча заводит дело-двойник, память дела разрывается.
Обе правки — на чистых функциях с тестами; поведение существующих функций сохраняется (старые тесты зелёные).
## Захват выдачи инструмента в Слой 1 {#D1}
**Контракт.** `parseLastExchange(transcriptText)` дополнительно привязывает результат каждого
действия к нему по идентификатору вызова инструмента.
- Блок `tool_use` несёт `id`; блок `tool_result` (в сообщении `role:user`) несёт `tool_use_id`
и `content`. Совпадение `tool_use.id === tool_result.tool_use_id` → результат привязывается к действию.
- `content` результата: строка берётся как есть; массив блоков → склейка `text`-блоков через `\n`.
- Привязанный результат усекается до предела (константа, ~1200 символов); усечённый
оканчивается маркером `…`.
- **Совместимость:** если у действия нет совпадающего результата (нет `id` либо нет `tool_result`),
объект действия остаётся прежней формы `{tool, input}`**без** ключа `result`. Поле `result`
добавляется ТОЛЬКО при реальном совпадении. Это сохраняет существующие тесты `toEqual([{tool,input}])`.
- Формат записи `[ВЫДАЧА]` в `buildRawRecord` уже печатает `result` при наличии — менять его не нужно.
**Edge-cases.** Нет `tool_result` → действие без `result`. Несколько действий — каждый матчится
по своему `id`. Битые строки стенограммы пропускаются (как сейчас).
## Сверка имени дела при включении {#D2}
**Контракт.** Чистая функция `resolveCaseActivation(requested, existing)` решает, что делать с именем дела:
- `existing` — список имён существующих дел (папки `docs/secretary/<дело>`).
- **Точное совпадение** (без учёта регистра) с существующим → `{ action: 'activate', work: <существующее имя> }`.
- **Похоже, но не точно** на одно/несколько существующих → `{ action: 'confirm', candidates: [<оригинальные имена>] }`.
- **Не похоже ни на что** (или список пуст) → `{ action: 'activate', work: <как ввёл> }`.
**Похоже** = опечатка ИЛИ сокращение:
- подстрока: одно имя содержится в другом, длина короткого ≥ 3;
- опечатка: расстояние Левенштейна ≤ 2 при длине обоих ≥ 4.
**Поведение хука** `secretary-prompt-hook` на `включи`:
- собрать `existing` — имена директорий в `docs/secretary`, исключив `raw` и не-директории;
- `action: 'activate'` → записать флажок `on` с `work` из результата (как сейчас);
- `action: 'confirm'`**флажок НЕ трогать** (секретарь не включается), вывести в stdout
понятную подсказку: имя похоже на существующие `<candidates>`, повтори командой с точным
именем (для существующего) либо с именем, не совпадающим с ними (для нового).
**Edge-cases.** Пустой `existing` → activate. Имя `general` при существующей папке `general`
точное совпадение → activate. `raw` и файлы (`содержание.md`, счётчики) в список дел не попадают.
## Проверка и критерий приёмки {#D3}
- Тесты — `vitest` с `import { describe, it, expect } from 'vitest'` (конвенция репозитория),
файлы `tools/secretary-transcript.test.mjs` и `tools/secretary-flag.test.mjs`.
- TDD: новые тесты пишутся и прогоняются КРАСНЫМИ до реализации, затем зелёными после.
- Зелёность — полный свод через `node tools/produce-verify-receipt.mjs` (гоняет
`tools/*.test.mjs` по `vitest.config.tools.mjs`). **Критерий приёмки:** свод проходит
(вывод `signed GREEN` либо `no-signer-key` — оба означают, что сюита прошла; `suite-not-passed` = провал).
- Покрытие: №3 — привязка по id, склейка text-блоков, усечение, сохранение старой формы без result;
№2 — точное совпадение, нет похожих, опечатка, подстрока, пустой список.
```verified-context-json
[
{"id":"vc-transcript","kind":"EXTRACTED","ref":"tools/secretary-transcript.mjs","anchor":"export function parseLastExchange("},
{"id":"vc-flag","kind":"EXTRACTED","ref":"tools/secretary-flag.mjs","anchor":"export function detectSecretaryCommand("}
]
```