docs(secretary): спека + план ядра секретаря протокола работ

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-22 04:30:30 +03:00
parent 2a6caeca4a
commit c963142c27
2 changed files with 602 additions and 0 deletions
@@ -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). Ключ — <slug>/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).
@@ -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/<session>.log`**сырой журнал Слоя 1** (§D3), append-only резерв.
- `<дело>` — стабильный kebab-slug; формируется при первом появлении дела.
**Edge-cases.** Папки создаются лениво при первой записи. Отсутствие `содержание.md` =
пустой индекс (не ошибка). Имя дела не меняется ретроактивно (slug стабилен).
**Критерий.** Любую запись протокола можно отнести к ровно одному делу; индекс находит
протокол каждого дела по ссылке.
## Слой 1 — постоянный сырой журнал {#D3}
**Контракт.**
- Слой 1 — **всегда включён**, независимо от состояния секретаря. Тупой Stop-хук, без
вызова LLM, без суждений.
- На каждом завершённом ходе дописывает в `raw/<session>.log` одну запись: текст
пользователя, текст ассистента, **все действия** (вызовы инструментов) и **их выдачи**
(включая ответы MCP), дословно.
- Каждая запись несёт стабильный ключ `turn=N` (порядковый номер хода в сессии), отметку
времени и `session`.
- ПДн вырезаются **до** записи (переиспользуется фильтр наблюдателя).
- Падение хука — **громкое**: пишется маркер ошибки, не молчаливый пропуск.
**Формат записи (литерал).**
```
=== ХОД turn=<N> · <ISO-time> · session=<id> ===
[ЮЗЕР]
<текст>
[АССИСТЕНТ]
<текст>
[ДЕЙСТВИЕ] <tool> in=<json-аргументов>
[ВЫДАЧА] <tool>
<результат как есть>
=== КОНЕЦ ХОДА ===
```
**Edge-cases.** Гигантская выдача (например, веб-скрейп) пишется целиком — Слой 1 не
сжимает (полнота важнее объёма; сжатие — забота выжимки и файлов-ходов). Источник данных
для записи — стенограмма сессии на диске (полная, не подрезанная контекстом).
**Критерий.** После любого хода в `raw/<session>.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"
}
]
```