docs(secretary): спека + план ядра секретаря протокола работ
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
```
|
||||
Reference in New Issue
Block a user