diff --git a/docs/superpowers/plans/2026-06-21-secretary-protocol-plan-v2.md b/docs/superpowers/plans/2026-06-21-secretary-protocol-plan-v2.md new file mode 100644 index 0000000..7ecad81 --- /dev/null +++ b/docs/superpowers/plans/2026-06-21-secretary-protocol-plan-v2.md @@ -0,0 +1,320 @@ +# Секретарь протокола работ — план реализации (v2) + +> **Для агентных исполнителей:** ОБЯЗАТЕЛЬНАЯ СУБ-СКИЛ: используйте superpowers:executing-plans. +> Шаги — чекбоксами (`- [ ]`). + +**Goal:** собрать чистые модули `tools/secretary-*.mjs` (детект команды, сверка протокола, +нарезка периода, оглавление) с unit-тестами, без правки `.claude/settings.json` (§D12 спеки). + +**Architecture:** секретарь — набор хуков поверх мотора LLM и PII-фильтра наблюдателя. Этот +план строит чистую тестируемую логику четырёх узлов. Модуль `layer1` (D3, сырой журнал) +**отложен** отдельным планом — наставник (NO-GO v1) попросил сперва привязать источник +стенограммы; источник известен (`transcript_path` Stop-события, см. `observer-stop-hook.mjs`), +но требует уточнения опечатанной спеки с владельцем. Контракт — спека +`docs/superpowers/specs/2026-06-21-secretary-protocol-design.md`. + +**Tech Stack:** Node ESM (`.mjs`), vitest (`import { describe, it, expect } from 'vitest'`). + +## Цель + +Реализовать чистую логику секретаря четырьмя модулями с тестами (детект команды, сверка +протокола, нарезка периода, оглавление), без правки `settings.json` и без модуля `layer1`. + +## Карта файлов + +- Create `tools/secretary-flag.mjs` + `tools/secretary-flag.test.mjs` — детект «включи/выключи» (§D4). +- Create `tools/secretary-protocol.mjs` + `tools/secretary-protocol.test.mjs` — 9-пунктов + сверка (§D5/§D7). +- Create `tools/secretary-slice.mjs` + `tools/secretary-slice.test.mjs` — нарезка периода (§D6). +- Create `tools/secretary-index.mjs` + `tools/secretary-index.test.mjs` — апсерт оглавления (§D8). + +```skills-json +["test-driven-development"] +``` + +```steps-json +[ + {"op":"Write","object":"tools/secretary-flag.test.mjs","ref":"D4"}, + {"op":"Bash","object":"npx vitest run tools/secretary-flag.test.mjs --reporter dot","ref":"D4"}, + {"op":"Write","object":"tools/secretary-flag.mjs","ref":"D4"}, + {"op":"Bash","object":"npx vitest run tools/secretary-flag.test.mjs --reporter basic","ref":"D4"}, + {"op":"Write","object":"tools/secretary-protocol.test.mjs","ref":"D7"}, + {"op":"Bash","object":"npx vitest run tools/secretary-protocol.test.mjs --reporter dot","ref":"D7"}, + {"op":"Write","object":"tools/secretary-protocol.mjs","ref":"D5"}, + {"op":"Bash","object":"npx vitest run tools/secretary-protocol.test.mjs --reporter basic","ref":"D5"}, + {"op":"Write","object":"tools/secretary-slice.test.mjs","ref":"D6"}, + {"op":"Bash","object":"npx vitest run tools/secretary-slice.test.mjs --reporter dot","ref":"D6"}, + {"op":"Write","object":"tools/secretary-slice.mjs","ref":"D6"}, + {"op":"Bash","object":"npx vitest run tools/secretary-slice.test.mjs --reporter basic","ref":"D6"}, + {"op":"Write","object":"tools/secretary-index.test.mjs","ref":"D8"}, + {"op":"Bash","object":"npx vitest run tools/secretary-index.test.mjs --reporter dot","ref":"D8"}, + {"op":"Write","object":"tools/secretary-index.mjs","ref":"D8"}, + {"op":"Bash","object":"npx vitest run tools/secretary-index.test.mjs --reporter basic","ref":"D8"} +] +``` + +```verified-context-json +[ + {"id":"vc-motor","kind":"EXTRACTED","ref":"tools/observer-self-assessment-api.mjs","anchor":"callSelfAssessmentApi"}, + {"id":"vc-flag","kind":"EXTRACTED","ref":"tools/observer-self-assessment-api.mjs","anchor":"readRuntimeFlag"}, + {"id":"vc-append","kind":"EXTRACTED","ref":"tools/observer-stop-hook.mjs","anchor":"appendEpisode"} +] +``` + +--- + +### Task 1: secretary-flag — детект команды (§D4) + +**Files:** Create `tools/secretary-flag.mjs`, Test `tools/secretary-flag.test.mjs`. + +- [ ] **Step 1: тест (RED)** — `tools/secretary-flag.test.mjs`: + +```js +import { describe, it, expect } from 'vitest'; +import { detectSecretaryCommand } from './secretary-flag.mjs'; + +describe('detectSecretaryCommand', () => { + it('распознаёт включение', () => { + expect(detectSecretaryCommand('включи секретаря пожалуйста')).toBe('on'); + }); + it('распознаёт выключение', () => { + expect(detectSecretaryCommand('всё, выключи секретаря')).toBe('off'); + }); + it('нет команды — null', () => { + expect(detectSecretaryCommand('давай продолжим работу')).toBeNull(); + }); + it('цитата в кавычках не срабатывает', () => { + expect(detectSecretaryCommand('фраза «включи секретаря» это команда')).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: RED-прогон** — `npx vitest run tools/secretary-flag.test.mjs --reporter dot` → FAIL (нет модуля). +- [ ] **Step 3: импл (GREEN)** — `tools/secretary-flag.mjs`: + +```js +// Детект команды секретаря в тексте промпта. Кавычки/код снимаются до сопоставления, +// чтобы цитирование не срабатывало (приём как в существующих детекторах). +function stripQuoted(text) { + return String(text || '') + .replace(/«[^»]*»/g, ' ') + .replace(/"[^"]*"/g, ' ') + .replace(/`[^`]*`/g, ' '); +} + +export function detectSecretaryCommand(promptText) { + const t = stripQuoted(promptText).toLowerCase(); + if (/выключи\s+секретар/.test(t)) return 'off'; + if (/включи\s+секретар/.test(t)) return 'on'; + return null; +} +``` + +- [ ] **Step 4: GREEN-прогон** — `npx vitest run tools/secretary-flag.test.mjs --reporter basic` → PASS. + +--- + +### Task 2: secretary-protocol — структура и сверка (§D5/§D7) + +**Files:** Create `tools/secretary-protocol.mjs`, Test `tools/secretary-protocol.test.mjs`. + +- [ ] **Step 1: тест (RED)** — `tools/secretary-protocol.test.mjs`: + +```js +import { describe, it, expect } from 'vitest'; +import { applyExtraction, renderProtocol, EMPTY_PROTOCOL } from './secretary-protocol.mjs'; + +describe('secretary-protocol', () => { + it('добавляет решение с провенансом', () => { + const p = applyExtraction(EMPTY_PROTOCOL(), { + decisions: [{ text: 'единица = дело', why: 'тянется через сессии', turns: [7] }], + }); + const md = renderProtocol(p); + expect(md).toContain('единица = дело'); + expect(md).toContain('[→7]'); + }); + it('сверка зачёркивает, не удаляет', () => { + let p = applyExtraction(EMPTY_PROTOCOL(), { decisions: [{ text: 'A', turns: [1] }] }); + p = applyExtraction(p, { supersede: [{ oldText: 'A', newText: 'B', turns: [2] }] }); + const md = renderProtocol(p); + expect(md).toContain('~~A~~'); + expect(md).toContain('B'); + }); +}); +``` + +- [ ] **Step 2: RED-прогон** — `npx vitest run tools/secretary-protocol.test.mjs --reporter dot` → FAIL. +- [ ] **Step 3: импл (GREEN)** — `tools/secretary-protocol.mjs`: + +```js +// Структура и сверка короткого протокола (§D5/§D7). Отменённое зачёркивается, не удаляется. +export function EMPTY_PROTOCOL() { + return { decisions: [], will: [], open: [], doneNext: [], history: [] }; +} + +function prov(turns) { + return Array.isArray(turns) && turns.length ? ` [${turns.map((t) => `→${t}`).join(', ')}]` : ''; +} + +export function applyExtraction(protocol, extraction = {}) { + const p = { + decisions: [...protocol.decisions], will: [...protocol.will], open: [...protocol.open], + doneNext: [...protocol.doneNext], history: [...protocol.history], + }; + for (const d of extraction.decisions || []) { + p.decisions.push({ text: d.text, why: d.why || null, turns: d.turns || [], struck: false }); + } + for (const s of extraction.supersede || []) { + const old = p.decisions.find((d) => d.text === s.oldText && !d.struck); + if (old) old.struck = true; + p.decisions.push({ text: s.newText, why: s.why || null, turns: s.turns || [], struck: false }); + p.history.push({ oldText: s.oldText, newText: s.newText, turns: s.turns || [] }); + } + for (const w of extraction.will || []) p.will.push({ text: w.text, turns: w.turns || [] }); + for (const o of extraction.open || []) p.open.push({ text: o.text, turns: o.turns || [] }); + for (const s of extraction.doneNext || []) p.doneNext.push({ text: s.text, done: !!s.done, turns: s.turns || [] }); + return p; +} + +export function renderProtocol(protocol) { + const L = []; + L.push('## Решения'); + for (const d of protocol.decisions) { + const body = d.struck ? `~~${d.text}~~` : d.text; + const why = d.why ? ` — ${d.why}` : ''; + L.push(`- ${body}${why}${prov(d.turns)}`); + } + L.push('', '## Твоя воля / запреты'); + for (const w of protocol.will) L.push(`- ${w.text}${prov(w.turns)}`); + L.push('', '## Открытые вопросы'); + for (const o of protocol.open) L.push(`- ${o.text}${prov(o.turns)}`); + L.push('', '## Сделано / дальше'); + for (const s of protocol.doneNext) L.push(`- [${s.done ? 'x' : ' '}] ${s.text}${prov(s.turns)}`); + L.push('', '## История (заменено, не стёрто)'); + for (const h of protocol.history) L.push(`- ~~${h.oldText}~~ → ${h.newText}${prov(h.turns)}`); + return L.join('\n'); +} +``` + +- [ ] **Step 4: GREEN-прогон** — `npx vitest run tools/secretary-protocol.test.mjs --reporter basic` → PASS. + +--- + +### Task 3: secretary-slice — нарезка периода (§D6) + +**Files:** Create `tools/secretary-slice.mjs`, Test `tools/secretary-slice.test.mjs`. + +- [ ] **Step 1: тест (RED)** — `tools/secretary-slice.test.mjs`: + +```js +import { describe, it, expect } from 'vitest'; +import { sliceTurns } from './secretary-slice.mjs'; + +describe('sliceTurns', () => { + it('режет журнал на ходы по диапазону', () => { + const raw = [ + '=== ХОД turn=5 · t · session=a ===\nx\n=== КОНЕЦ ХОДА ===', + '=== ХОД turn=6 · t · session=a ===\ny\n=== КОНЕЦ ХОДА ===', + '=== ХОД turn=7 · t · session=a ===\nz\n=== КОНЕЦ ХОДА ===', + ].join('\n'); + const out = sliceTurns(raw, 6, 7); + expect(out.map((o) => o.turn)).toEqual([6, 7]); + expect(out[0].content).toContain('y'); + }); +}); +``` + +- [ ] **Step 2: RED-прогон** — `npx vitest run tools/secretary-slice.test.mjs --reporter dot` → FAIL. +- [ ] **Step 3: импл (GREEN)** — `tools/secretary-slice.mjs`: + +```js +// Нарезка сырого журнала на ходы по диапазону [from,to] (§D6). Идемпотентность по turn — +// забота писателя файлов (вызывающего хука). +export function sliceTurns(rawLog, from, to) { + const out = []; + const re = /=== ХОД turn=(\d+)[^\n]*===([\s\S]*?)=== КОНЕЦ ХОДА ===/g; + let m; + while ((m = re.exec(String(rawLog || ''))) !== null) { + const turn = Number(m[1]); + if (turn >= from && turn <= to) { + out.push({ turn, content: m[0].trim() }); + } + } + return out; +} +``` + +- [ ] **Step 4: GREEN-прогон** — `npx vitest run tools/secretary-slice.test.mjs --reporter basic` → PASS. + +--- + +### Task 4: secretary-index — апсерт оглавления (§D8) + +**Files:** Create `tools/secretary-index.mjs`, Test `tools/secretary-index.test.mjs`. + +- [ ] **Step 1: тест (RED)** — `tools/secretary-index.test.mjs`: + +```js +import { describe, it, expect } from 'vitest'; +import { upsertIndexEntry } from './secretary-index.mjs'; + +describe('upsertIndexEntry', () => { + it('добавляет новое дело', () => { + const md = upsertIndexEntry('', { slug: 'sec', title: 'Секретарь', goal: 'память сути', status: 'открыто', date: '2026-06-21' }); + expect(md).toContain('[Секретарь](sec/protocol.md)'); + expect(md).toContain('открыто'); + }); + it('обновляет существующее дело без дубля', () => { + const first = upsertIndexEntry('', { slug: 'sec', title: 'Секретарь', goal: 'g', status: 'открыто', date: '2026-06-21' }); + const upd = upsertIndexEntry(first, { slug: 'sec', title: 'Секретарь', goal: 'g', status: 'закрыто', date: '2026-06-22' }); + expect(upd.match(/sec\/protocol\.md/g).length).toBe(1); + expect(upd).toContain('закрыто'); + }); +}); +``` + +- [ ] **Step 2: RED-прогон** — `npx vitest run tools/secretary-index.test.mjs --reporter dot` → FAIL. +- [ ] **Step 3: импл (GREEN)** — `tools/secretary-index.mjs`: + +```js +// Апсерт строки дела в оглавление (§D8). Ключ — /protocol.md. +export function upsertIndexEntry(indexMd, { slug, title, goal, status, date }) { + const line = `- [${title}](${slug}/protocol.md) — ${goal} · ${status} · ${date}`; + const key = `(${slug}/protocol.md)`; + const lines = String(indexMd || '').split('\n').filter((l) => l.length > 0); + const idx = lines.findIndex((l) => l.includes(key)); + if (idx >= 0) lines[idx] = line; + else lines.push(line); + return lines.join('\n'); +} +``` + +- [ ] **Step 4: GREEN-прогон** — `npx vitest run tools/secretary-index.test.mjs --reporter basic` → PASS. + +--- + +## Отложено отдельным планом (по NO-GO наставника v1) + +- **Модуль `layer1` (D3, сырой журнал).** Источник записи — `transcript_path` из Stop-события + (читается как в `observer-stop-hook.mjs`); требует уточнения опечатанной спеки D3 (ссылка на + механизм) с владельцем, затем реализуется отдельным планом. +- **CLI-обёртки хуков** (`secretary-stop-hook.mjs`, `secretary-prompt-hook.mjs`, + `secretary-sessionstart-hook.mjs`) и **регистрация в `.claude/settings.json`** — ручной шаг владельца (§D12). + +## Self-Review + +- **Покрытие:** D4→Task1, D5/D7→Task2, D6→Task3, D8→Task4. D3 отложен (по требованию наставника). +- **Плейсхолдеров нет:** весь код тестов и реализаций приведён дословно. +- **Согласованность имён:** `detectSecretaryCommand`; `EMPTY_PROTOCOL`/`applyExtraction`/`renderProtocol`; + `sliceTurns`; `upsertIndexEntry` — едины между тестом и реализацией. + +## Переговоры + +### Круг 1 + +(пусто) + +### Круг 2 + +- **На NO-GO v1 (D3-шаги):** уступаю — модуль `layer1`/D3 удалён из плана и отложен отдельным + планом до уточнения источника стенограммы в спеке. В этом плане D3-шагов нет; остаются только + одобренные наставником узлы (D4/D5/D7/D6/D8). diff --git a/docs/superpowers/specs/2026-06-21-secretary-protocol-design.md b/docs/superpowers/specs/2026-06-21-secretary-protocol-design.md new file mode 100644 index 0000000..45fbf86 --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-secretary-protocol-design.md @@ -0,0 +1,282 @@ +# Спецификация: Секретарь протокола работ (след рассуждения) + +Контракт фонового «секретаря», который ведёт по каждому **делу** протокол рассуждения, +чтобы при сборке спеки/решения ничего из обсуждения не терялось. Реализация — набор +хуков Claude Code (`tools/*.mjs`), переиспользующих существующий мотор LLM и PII-фильтр +наблюдателя. Регистрация хуков в `.claude/settings.json` — **вне** этой спеки (ручной шаг +владельца). + +## Цель + +Дать управляющему слою механизм, при котором **полный ход рассуждения по делу +сохраняется автоматически и не теряется** между ходами и сессиями, а его выжимка +доступна как короткий, самодостаточный протокол. Назначение — не «память ради памяти», а +**не потерять рассуждение, чтобы потом собрать спеку правильно**. Это базовый слой; его +потребители за пределами авторской сборки спеки (наставник, совет директоров) — отдельные +будущие задачи и в этот контракт не входят. + +## Назначение и границы {#D1} + +**Контракт.** + +- Секретарь решает **одну** задачу: не потерять суть рассуждения по делу ради корректной + последующей сборки спеки. «Память между сессиями» — побочный эффект, не цель. +- **В контуре этой спеки:** Слой 1 (сырой журнал), команды включения/выключения, онлайн- + ведение короткого протокола, сборка файлов-ходов на выключении, схема извлечения, + провенанс, авто-показ оглавления на старте. +- **Вне контура (отдельные задачи, здесь не проектируются):** передача протокола + наставнику для оценки качества спеки; «совет директоров»; установка стороннего + `claude-mem`. Дизайн не должен мешать этим будущим слоям — полный архив остаётся + доступным. +- `claude-mem` рассматривается как **дополнение**, не замена: собственный секретарь — + основной механизм. + +**Критерий приёмки.** В спеке явно перечислено, что входит и что вынесено; ни один +вынесенный пункт не является скрытой зависимостью реализации входящих пунктов. + +## Единица памяти и артефакты {#D2} + +**Контракт.** + +- Единица — **дело** (work): связный кусок работы, который может тянуться через много + сессий. В одной сессии может затрагиваться несколько дел. +- Артефакты на диске (база `docs/secretary/`): + - `docs/secretary/содержание.md` — **оглавление**: по строке на дело (название, цель + одной строкой, статус, ссылка на протокол). Единый индекс всех дел. + - `docs/secretary/<дело>/protocol.md` — **короткий протокол** одного дела (выжимка по + схеме §D7, со ссылками `[→N]`). + - `docs/secretary/<дело>/steps/turn-N.md` — **файл на ход**: один ход периода (реплики + + действия + результаты), нарезается на выключении (§D6). + - `docs/secretary/raw/.log` — **сырой журнал Слоя 1** (§D3), append-only резерв. +- `<дело>` — стабильный kebab-slug; формируется при первом появлении дела. + +**Edge-cases.** Папки создаются лениво при первой записи. Отсутствие `содержание.md` = +пустой индекс (не ошибка). Имя дела не меняется ретроактивно (slug стабилен). + +**Критерий.** Любую запись протокола можно отнести к ровно одному делу; индекс находит +протокол каждого дела по ссылке. + +## Слой 1 — постоянный сырой журнал {#D3} + +**Контракт.** + +- Слой 1 — **всегда включён**, независимо от состояния секретаря. Тупой Stop-хук, без + вызова LLM, без суждений. +- На каждом завершённом ходе дописывает в `raw/.log` одну запись: текст + пользователя, текст ассистента, **все действия** (вызовы инструментов) и **их выдачи** + (включая ответы MCP), дословно. +- Каждая запись несёт стабильный ключ `turn=N` (порядковый номер хода в сессии), отметку + времени и `session`. +- ПДн вырезаются **до** записи (переиспользуется фильтр наблюдателя). +- Падение хука — **громкое**: пишется маркер ошибки, не молчаливый пропуск. + +**Формат записи (литерал).** + +``` +=== ХОД turn= · · session= === +[ЮЗЕР] +<текст> +[АССИСТЕНТ] +<текст> +[ДЕЙСТВИЕ] in= +[ВЫДАЧА] +<результат как есть> +=== КОНЕЦ ХОДА === +``` + +**Edge-cases.** Гигантская выдача (например, веб-скрейп) пишется целиком — Слой 1 не +сжимает (полнота важнее объёма; сжатие — забота выжимки и файлов-ходов). Источник данных +для записи — стенограмма сессии на диске (полная, не подрезанная контекстом). + +**Критерий.** После любого хода в `raw/.log` присутствует запись с этим `turn=N`, +содержащая реплики и результаты действий хода; при внутреннем сбое присутствует маркер +ошибки. + +## Включение и выключение секретаря {#D4} + +**Контракт.** + +- Владелец управляет секретарём **словом**: фраза «включи секретаря» → состояние ВКЛ; + «выключи секретаря» → состояние ВЫКЛ. +- Состояние хранится в файле-флажке `~/.claude/runtime/secretary-mode.json` (поле `mode`: + `on`/`off`), читается тем же способом, что и прочие рантайм-флаги. +- Фразу распознаёт хук на ввод пользователя (точное вхождение подстроки, без LLM). +- Состояние сбрасывается в ВЫКЛ при старте сессии (включать заново осознанно). + +**Edge-cases.** Повторное «включи» при уже включённом — идемпотентно. «Выключи» при уже +выключенном — без действия (нечего собирать). Цитирование фразы в коде/кавычках не должно +ложно срабатывать (снятие цитат до сопоставления, как в существующих детекторах). + +**Критерий.** После «включи секретаря» флаг = `on`; после «выключи секретаря» флаг = `off` +и запускается сборка периода (§D6). + +## Онлайн-ведение короткого протокола {#D5} + +**Контракт.** + +- Пока флаг = `on`, на каждом завершённом ходе короткий протокол текущего дела + обновляется выжимкой **последнего обмена** по схеме §D7. +- Выжимку делает **хук, вызывающий мотор LLM** (не tool-call ассистента): хук читает + последний обмен из стенограммы, отправляет его мотору, получает структурированную + выжимку, сам записывает файл протокола. Это сохраняет интеллект выжимки, не завися от + ручной записи ассистентом. +- Ведение — **сверка (reconcile), не дозапись**: новое решение добавляется; решение, + отменяющее прежнее, помечает прежнее зачёркнутым с пометкой «заменено» и добавляет новое; + отвеченный открытый вопрос переносится в «решено»; выполненный шаг отмечается. +- **Отменённое НИКОГДА не удаляется** — только зачёркивается с пометкой. + +**Edge-cases.** Ход без сути (механический шаг, «ок») → протокол не меняется. Сбой мотора +(таймаут/сеть) → ход пропускается без записи, но сырьё уже в Слое 1 (восстановимо); +протокол не повреждается. Принадлежность хода делу определяется по контексту обмена и +оглавлению; при неуверенности запись помечается «разобрать» (не теряется). + +**Критерий.** Содержательный ход при ВКЛ отражается в `protocol.md`; отмена прежнего +решения видна как зачёркивание, не как удаление. + +## Сборка файлов-ходов на выключении {#D6} + +**Контракт.** + +- На «выключи секретаря» хук берёт из Слоя 1 **только ходы периода** «вкл→выкл» и + раскладывает их на отдельные файлы `steps/turn-N.md` в папке дела (каждый файл = один + ход: реплики + действия + результаты). +- После нарезки выполняется **проверка**: файлы в кодировке UTF-8 (без BOM), не пусты, + ссылки `[→N]` из протокола резолвятся в существующие `turn-N.md`. +- Проверку кодировки/целостности и простановку ссылок `[→N]` в коротком протоколе делает + агентная часть (мотор/проверочный хук); чистая нарезка — механическая часть хука. + +**Edge-cases.** Сессия рухнула до «выключи» → файлы не нарезаны в тот момент, но сырьё +в Слое 1 на диске → нарезку можно повторить позже (потери нет). Повторная сборка того же +периода не должна дублировать файлы (нарезка идемпотентна по `turn=N`). + +**Критерий.** После «выключи» в `steps/` лежит по файлу на каждый ход периода; каждая +ссылка `[→N]` короткого протокола указывает на существующий `turn-N.md` с тем же номером. + +## Схема извлечения — 9 пунктов {#D7} + +**Контракт.** Выжимка по делу обязана проверяться по девяти категориям (не каждая запись +содержит все, но проверяются все): + +1. **Дело** — к какому делу, дата, статус (открыто/закрыто/заменено). +2. **Решение** — что решено, чёткой повелительной формулировкой. +3. **Почему** — обоснование, что двигало выбором. +4. **Альтернативы** — что ещё рассматривали и почему отвергли. +5. **Последствия / цена** — что меняется, риски, что усложняется. +6. **Воля / запреты** — пожелания и запреты владельца, кто хозяин решения. +7. **Открытые вопросы** — что ещё не решено (закрывается только словом владельца). +8. **Сделано / дальше** — выполненные и следующие шаги, признак готовности. +9. **История** — что чем заменено (зачёркнуто, не стёрто), связанные дела. + +**Структура `protocol.md`** — шапка (дело, статус, тронуто, хозяин, цель), затем разделы +по корзинам 2–9 выше. + +**Критерий.** Выжимка строится по этим девяти категориям; «почему» обязательно для каждого +нетривиального решения. + +## Провенанс и привязка к сырью {#D8} + +**Контракт.** + +- Каждая значимая запись протокола несёт ссылку `[→N]` — на ход номер N. +- **Номер N выдаёт хук** (порядковый номер хода из стенограммы), а не выбирается выжимкой + произвольно; выжимка лишь проставляет выданный номер. Поэтому `[→N]` протокола и `turn=N` + файла/журнала **совпадают механически** (один ключ с двух сторон) — привязка надёжна без + понимания смысла хуком. +- Оглавление `содержание.md` — строки вида `- [<дело>](<дело>/protocol.md) — <цель> · + <статус> · <дата>`. + +**Edge-cases.** Запись может ссылаться на несколько ходов (`[→7, →12]`). Ошибочный +смысловой тег — не потеря данных: сырьё всех ходов на диске под теми же номерами, +тег исправим сверкой. + +**Критерий.** Любую ссылку `[→N]` протокола можно разрешить в запись `turn=N` журнала/файла +того же номера. + +## Самодостаточность и навигация {#D9} + +**Контракт.** + +- Короткий протокол **самодостаточен** для обычной работы: решение + «почему» + воля + + статус содержатся прямо в нём. Чтение только короткого протокола безопасно для типовых + задач. +- Тяжёлое сырьё (файлы-ходы, журнал) **не грузится целиком** — извлекается точечно по + номеру хода. Разделение «лёгкий протокол ↔ тяжёлый архив» обязательно (короткий файл + держится в контексте, архив читается выборочно). +- На старте сессии хук показывает **оглавление** (`содержание.md`) как контекст — карту + всех дел и их статусов, без подгрузки самих протоколов. + +**Критерий.** Короткий протокол читаем и осмыслен без открытия архива для типовой задачи; +оглавление доступно на старте сессии. + +## Мотор LLM и переиспользование {#D10} + +**Контракт.** + +- Выжимка и сборка используют существующий мотор вызова модели (паттерн + `callSelfAssessmentApi`): базовый URL и ключ из окружения, таймаут с гонкой, **тихий + отказ** (ошибка → null, никогда не throw в проде). +- Состояние флага читается существующим читателем рантайм-флагов. +- ПДн вырезаются существующим фильтром наблюдателя до любой записи. +- Новые скрипты — отдельные файлы `tools/secretary-*.mjs` с чистыми тестируемыми функциями + и тонкой CLI-обёрткой, по образцу существующих хуков наблюдателя. + +**Критерий.** Ни один новый скрипт не дублирует мотор/флаг-ридер/PII-фильтр — они +импортируются из существующих модулей. + +## Надёжность и честная оценка потерь {#D11} + +**Контракт (что гарантируется и что нет).** + +- **Сырьё практически не теряется:** Слой 1 пишет каждый ход на диск, источник — + непод­резаемая стенограмма; плюс резерв в журнале. +- **Остаточные риски потери (named):** (а) падение самого хука Слоя 1 на ходу — смягчается + громким маркером ошибки; (б) то, чего не было в сессии (невысказанные мысли, работа в + другом окне) — принципиальный предел; (в) сознательное обрезание огромных выдач — в этом + контракте **не делается** (пишем целиком). +- **Выжимка может быть неточной** (живой мотор), но это **не потеря**: полный обмен в Слое + 1, запись восстановима/исправима сверкой. + +**Критерий.** Каждый названный риск потери имеет либо смягчение, либо явный предел; «суть» +восстановима из Слоя 1 при любой ошибке выжимки. + +## Границы реализации {#D12} + +**Контракт.** + +- **Регистрация хуков в `.claude/settings.json` — вне автоматической реализации.** Её + выполняет владелец вручную; спека/план поставляют готовые скрипты и точную инструкцию, + какие хуки на какие события добавить. +- Файлы протокола, оглавления и файлы-ходы — `docs/*.md` (документные изменения). +- Установка `claude-mem` — отдельная задача владельца, с проверкой совместимости. + +**Критерий.** Реализация не требует изменения `settings.json` для прохождения тестов; +ручной шаг владельца описан явным списком «событие → скрипт». + +```verified-context-json +[ + { + "id": "ctx-motor", + "kind": "EXTRACTED", + "ref": "tools/observer-self-assessment-api.mjs", + "anchor": "callSelfAssessmentApi" + }, + { + "id": "ctx-flag", + "kind": "EXTRACTED", + "ref": "tools/observer-self-assessment-api.mjs", + "anchor": "readRuntimeFlag" + }, + { + "id": "ctx-append", + "kind": "EXTRACTED", + "ref": "tools/observer-stop-hook.mjs", + "anchor": "appendEpisode" + }, + { + "id": "ctx-pii", + "kind": "EXTRACTED", + "ref": "tools/observer-stop-hook.mjs", + "anchor": "observer-pii-filter" + } +] +```