docs(secretary): спеки и планы дня — slim, база знаний, 7 ходов, богатый контекст ролям

This commit is contained in:
Дмитрий
2026-06-26 17:39:59 +03:00
parent af262fffbb
commit 21be382a68
8 changed files with 2080 additions and 0 deletions
@@ -0,0 +1,240 @@
# Редактору — последние 7 ходов целиком — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Обмен, подаваемый редактору (`buildReconcilePrompt`), = последние 7 ходов спана, каждый целиком (юзер + ассистент + ВСЕ действия); убрать у редактора урезку «7 действий». 4 роли не трогаем.
**Architecture:** `assembleSpan` дополнительно отдаёт по-ходовый список `turns`. `buildReconcilePrompt` рендерит обмен из `turns` (последние 7 ходов), с фолбэком на единый блок, если `turns` нет.
**Tech Stack:** Node ESM (`.mjs`), vitest.
**Spec:** [docs/superpowers/specs/2026-06-26-secretary-editor-last7-turns-design.md](../specs/2026-06-26-secretary-editor-last7-turns-design.md)
---
## Файловая карта
| Файл | Изменение |
|---|---|
| `tools/secretary-span.mjs` | `assembleSpan` +`turns` (по-ходовый список) |
| `tools/secretary-reconcile.mjs` | обмен из `turns` (7 ходов целиком); `MAX_EXCHANGE_ACTIONS``MAX_EXCHANGE_TURNS` |
| `tools/secretary-span.test.mjs`, `tools/secretary-reconcile.test.mjs` | кейсы |
---
## Task 1: `assembleSpan` отдаёт по-ходовый `turns`
**Files:**
- Modify: `tools/secretary-span.mjs` (`assembleSpan`)
- Test: `tools/secretary-span.test.mjs`
- [ ] **Step 1: Write the failing test**
Append inside the existing `describe('assembleSpan', ...)` block in `tools/secretary-span.test.mjs` (before its closing `});`):
```javascript
it('возвращает по-ходовый список turns (для редактора)', () => {
const ex = assembleSpan(raw, { start: 3, end: 4 });
expect(ex.turns).toEqual([
{ turn: 3, user: 'настоящий промпт', assistant: 'первый ответ', actions: [{ tool: 'Read', input: 'a', result: 'r1' }] },
{ turn: 4, user: 'Stop hook feedback: x', assistant: 'второй ответ', actions: [{ tool: 'Grep', input: 'b', result: 'r2' }] },
]);
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npx vitest run tools/secretary-span.test.mjs -t "по-ходовый список turns"`
Expected: FAIL — `ex.turns` is undefined.
- [ ] **Step 3: Implement**
In `tools/secretary-span.mjs`, replace the `assembleSpan` function:
```javascript
export function assembleSpan(rawText, { start, end }) {
const blocks = splitRawIntoTurns(rawText).filter((p) => p.turn >= start && p.turn <= end);
const parsed = blocks.map((p) => parseTurnBlock(p.block));
const startTurn = parsed.find((p) => p.turn === start) || parsed[0] || {};
const assistant = parsed.map((p) => p.assistant).filter(Boolean).join('\n');
const actions = parsed.flatMap((p) => p.actions);
return { user: startTurn.user || '', assistant, actions };
}
```
with (добавить `turns` — склейка сохраняется для buildStepLine/ролей):
```javascript
export function assembleSpan(rawText, { start, end }) {
const blocks = splitRawIntoTurns(rawText).filter((p) => p.turn >= start && p.turn <= end);
const parsed = blocks.map((p) => parseTurnBlock(p.block));
const startTurn = parsed.find((p) => p.turn === start) || parsed[0] || {};
const assistant = parsed.map((p) => p.assistant).filter(Boolean).join('\n');
const actions = parsed.flatMap((p) => p.actions);
const turns = parsed.map((p) => ({ turn: p.turn, user: p.user, assistant: p.assistant, actions: p.actions }));
return { user: startTurn.user || '', assistant, actions, turns };
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `npx vitest run tools/secretary-span.test.mjs`
Expected: PASS (все прежние + новый).
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-span.mjs tools/secretary-span.test.mjs
git commit -m "feat(secretary): assembleSpan отдаёт по-ходовый список turns
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 2: редактор рендерит последние 7 ходов целиком
**Files:**
- Modify: `tools/secretary-reconcile.mjs` (`buildReconcilePrompt` + const)
- Test: `tools/secretary-reconcile.test.mjs`
- [ ] **Step 1: Write the failing tests**
In `tools/secretary-reconcile.test.mjs`, REPLACE the existing test (около строки 128):
```javascript
it('Последний обмен — только последние 7 действий с пометкой', () => {
const actions = Array.from({ length: 10 }, (_, i) => ({ tool: 'Read', input: `f${i}`, result: `R${i}` }));
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: { user: 'u', assistant: 'a', actions } });
expect(user).toContain('f9');
expect(user).toContain('f3');
expect(user).not.toContain('f2');
expect(user).toMatch(/последние 7 из 10/);
});
```
with (теперь редактор режет по ХОДАМ, действия не урезает):
```javascript
it('обмен редактора — последние 7 ходов целиком, действия не урезаны', () => {
const turns = Array.from({ length: 9 }, (_, i) => ({
turn: i + 1, user: `u${i + 1}`, assistant: `a${i + 1}`,
actions: [{ tool: 'Read', input: `fA${i + 1}`, result: 'r' }, { tool: 'Read', input: `fB${i + 1}`, result: 'r' }],
}));
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: { turns } });
expect(user).toMatch(/последние 7 из 9 ходов/);
expect(user).toContain('[ХОД 9]');
expect(user).toContain('[ХОД 3]'); // граница последних 7 (ходы 3..9)
expect(user).not.toContain('[ХОД 2]'); // 8-й с конца отброшен
expect(user).toContain('fA9'); // ОБА действия хода 9 целиком
expect(user).toContain('fB9');
});
it('обмен редактора — фолбэк на единый блок, если turns нет', () => {
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: { user: 'спросил', assistant: 'ответил', actions: [{ tool: 'Read', input: 'x', result: 'СОДЕРЖ' }] } });
expect(user).toContain('спросил');
expect(user).toContain('ответил');
expect(user).toContain('СОДЕРЖ');
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npx vitest run tools/secretary-reconcile.test.mjs -t "обмен редактора"`
Expected: FAIL — нет `[ХОД N]`-рендера (сейчас режется по действиям).
- [ ] **Step 3: Replace the const**
In `tools/secretary-reconcile.mjs`, find:
```javascript
// Сколько ПОСЛЕДНИХ действий хода подавать редактору (анти-раздувание: длинный ход с десятками
// Read не возим целиком). Держим в синхроне с MAX_EXCHANGE_ACTIONS из secretary-harvest.mjs.
const MAX_EXCHANGE_ACTIONS = 7;
```
change to:
```javascript
// Сколько ПОСЛЕДНИХ ходов спана подавать редактору целиком (юзер+ассистент+ВСЕ действия хода).
const MAX_EXCHANGE_TURNS = 7;
```
- [ ] **Step 4: Replace the exchange rendering**
In `tools/secretary-reconcile.mjs` `buildReconcilePrompt`, find:
```javascript
const allActs = lastExchange.actions || [];
const shownActs = allActs.slice(-MAX_EXCHANGE_ACTIONS);
const actsHead = allActs.length > shownActs.length ? ` (показаны последние ${shownActs.length} из ${allActs.length} действий)\n` : '';
const acts = actsHead + ((shownActs.map((a) =>
`${a.tool} in=${a.input ?? ''}${a.result != null ? `\n${String(a.result).replace(/\n/g, '\n ')}` : ''}`).join('\n')) || '—');
```
change to:
```javascript
const renderActs = (acts) => ((acts || []).map((a) =>
`${a.tool} in=${a.input ?? ''}${a.result != null ? `\n${String(a.result).replace(/\n/g, '\n ')}` : ''}`).join('\n')) || ' —';
let exchangeText;
if (Array.isArray(lastExchange.turns) && lastExchange.turns.length) {
const all = lastExchange.turns;
const shown = all.slice(-MAX_EXCHANGE_TURNS);
const head = all.length > shown.length ? `(показаны последние ${shown.length} из ${all.length} ходов)\n\n` : '';
exchangeText = head + shown.map((t) =>
`[ХОД ${t.turn}]\n[ЮЗЕР]: ${t.user || ''}\n[АССИСТЕНТ]: ${t.assistant || ''}\n[ДЕЙСТВИЯ]:\n${renderActs(t.actions)}`).join('\n\n');
} else {
exchangeText = `[ЮЗЕР]: ${lastExchange.user || ''}\n[АССИСТЕНТ]: ${lastExchange.assistant || ''}\n[ДЕЙСТВИЯ]:\n${renderActs(lastExchange.actions)}`;
}
```
- [ ] **Step 5: Use `exchangeText` in the user array**
In `tools/secretary-reconcile.mjs` `buildReconcilePrompt`, find:
```javascript
'', 'Последний обмен:',
`[ЮЗЕР]: ${lastExchange.user || ''}`,
`[АССИСТЕНТ]: ${lastExchange.assistant || ''}`,
`Действия (с содержимым):\n${acts}`,
```
change to:
```javascript
'', 'Последние ходы (обмен):',
exchangeText,
```
- [ ] **Step 6: Run tests to verify they pass**
Run: `npx vitest run tools/secretary-reconcile.test.mjs`
Expected: PASS. Проверь, что прежние кейсы «в user — текущий протокол и обмен» и «подаёт действия с содержимым» зелёные (они без `turns` → фолбэк, контент на месте).
- [ ] **Step 7: Commit**
```bash
git add tools/secretary-reconcile.mjs tools/secretary-reconcile.test.mjs
git commit -m "feat(secretary): редактору обмен = последние 7 ходов целиком (не 7 действий)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 3: полный регресс секретаря
**Files:** нет (проверка)
- [ ] **Step 1: Прогнать все secretary-тесты**
Run: `npx vitest run tools/secretary-*.test.mjs`
Expected: PASS, exit 0. Особое внимание: `secretary-span`, `secretary-reconcile`, `secretary-harvest` (роли — без изменений, «7 действий» у них остаётся).
---
## Заметки для исполнителя
- **Коммит/пуш — через терминал владельца** (verify-receipt + один Bash, цепочки режет пол).
- **Границы:** НЕ трогать `renderExchangeText` / `MAX_EXCHANGE_ACTIONS` в `secretary-harvest.mjs` (4 роли остаются на «7 действий»); не трогать склейку `{user,assistant,actions}` в `assembleSpan` (нужна `buildStepLine`/ролям) — только ДОБАВИЛИ `turns`.
- **Ветка** — продолжаем на `feat/secretary-slim-protocol`.
@@ -0,0 +1,504 @@
# «База знаний» в протоколе секретаря — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Между темой и решениями протокол ведёт «базу знаний» — факты из изучения с источником в конце; наполняет редактор; копим (дубль по источнику уточняем), не удаляем.
**Architecture:** Новое поле `knowledge: [{ref, text}]` (ключ — `ref`). Редактор (`reconcile`) пишет его из действий хода; парсер читает; детерминированный `preserveKnowledge` гарантирует «не удаляем» даже при сбое модели; рендеры `protocol.md`/`архив.md` печатают раздел; slim его сохраняет.
**Tech Stack:** Node ESM (`.mjs`), vitest.
**Spec:** [docs/superpowers/specs/2026-06-26-secretary-knowledge-base-design.md](../specs/2026-06-26-secretary-knowledge-base-design.md)
---
## Файловая карта
| Файл | Изменение |
|---|---|
| `tools/secretary-protocol.mjs` | `EMPTY_PROTOCOL` +`knowledge: []` |
| `tools/secretary-reconcile.mjs` | парсер +knowledge; `preserveKnowledge`; вызов в `reconcileTurn`; промпт (поле+правило+вход) |
| `tools/secretary-render-fluffy.mjs` | раздел «🧠 База знаний» в protocol.md |
| `tools/secretary-render-archive.mjs` | раздел «База знаний» в архив.md |
| `tools/secretary-*.test.mjs` | кейсы |
---
## Task 1: данные — `knowledge` в EMPTY_PROTOCOL и парсере
**Files:**
- Modify: `tools/secretary-protocol.mjs` (EMPTY_PROTOCOL)
- Modify: `tools/secretary-reconcile.mjs` (parseReconcileResponse)
- Test: `tools/secretary-protocol.test.mjs`, `tools/secretary-reconcile.test.mjs`
- [ ] **Step 1: Write the failing tests**
Append to `tools/secretary-protocol.test.mjs` (внутри файла, новый describe):
```javascript
describe('EMPTY_PROTOCOL — база знаний', () => {
it('инициализирует knowledge пустым массивом', () => {
expect(EMPTY_PROTOCOL().knowledge).toEqual([]);
});
});
```
Append to `tools/secretary-reconcile.test.mjs` (новый describe в конце):
```javascript
describe('parseReconcileResponse — knowledge', () => {
it('читает knowledge [{ref,text}] и отсеивает записи без ref', () => {
const out = parseReconcileResponse(JSON.stringify({
subject: 'S',
knowledge: [
{ text: 'функция Y делает Z', ref: 'a.mjs:10' },
{ text: 'без источника — не знание' },
{ text: '', ref: 'b.mjs:1' },
],
}));
expect(out.knowledge).toEqual([{ ref: 'a.mjs:10', text: 'функция Y делает Z' }]);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npx vitest run tools/secretary-protocol.test.mjs tools/secretary-reconcile.test.mjs -t "knowledge"`
Expected: FAIL — `EMPTY_PROTOCOL().knowledge` is undefined; `out.knowledge` is undefined.
- [ ] **Step 3: Implement EMPTY_PROTOCOL**
In `tools/secretary-protocol.mjs`, find:
```javascript
return {
subject: '', status: 'открыто',
decisions: [], alternatives: [], consequences: [],
```
change to:
```javascript
return {
subject: '', status: 'открыто',
knowledge: [],
decisions: [], alternatives: [], consequences: [],
```
- [ ] **Step 4: Implement parser**
In `tools/secretary-reconcile.mjs`, inside `parseReconcileResponse`'s return object, find:
```javascript
return {
subject: typeof parsed.subject === 'string' ? parsed.subject.trim() : '',
decisions: list(parsed.decisions).map((e) => ({ ...ent(e), why: (e && e.why) || null })),
```
change to (insert `knowledge` between `subject` and `decisions`):
```javascript
return {
subject: typeof parsed.subject === 'string' ? parsed.subject.trim() : '',
knowledge: list(parsed.knowledge)
.map((e) => ({ ref: String((e && e.ref) || '').trim(), text: String((e && e.text) || '').trim() }))
.filter((e) => e.ref && e.text),
decisions: list(parsed.decisions).map((e) => ({ ...ent(e), why: (e && e.why) || null })),
```
- [ ] **Step 5: Run tests to verify they pass**
Run: `npx vitest run tools/secretary-protocol.test.mjs tools/secretary-reconcile.test.mjs`
Expected: PASS (все прежние + 2 новых).
- [ ] **Step 6: Commit**
```bash
git add tools/secretary-protocol.mjs tools/secretary-reconcile.mjs tools/secretary-protocol.test.mjs tools/secretary-reconcile.test.mjs
git commit -m "feat(secretary): поле knowledge в EMPTY_PROTOCOL и парсере reconcile
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 2: `preserveKnowledge` + сохранность в `reconcileTurn`
**Files:**
- Modify: `tools/secretary-reconcile.mjs` (новый export `preserveKnowledge`; вызов в `reconcileTurn`)
- Test: `tools/secretary-reconcile.test.mjs`
- [ ] **Step 1: Write the failing tests**
Append to `tools/secretary-reconcile.test.mjs`. First add imports — change the existing import line:
```javascript
import { parseReconcileResponse, reconcileGuard, buildGuardRemark, stampProvenance, buildReconcilePrompt, reconcileTurn } from './secretary-reconcile.mjs';
```
to:
```javascript
import { parseReconcileResponse, reconcileGuard, buildGuardRemark, stampProvenance, buildReconcilePrompt, reconcileTurn, preserveKnowledge } from './secretary-reconcile.mjs';
import { EMPTY_PROTOCOL } from './secretary-protocol.mjs';
```
Then append:
```javascript
describe('preserveKnowledge — копим, дубль уточняем, не удаляем', () => {
it('общий ref берёт новый текст (уточнение)', () => {
expect(preserveKnowledge([{ ref: 'x', text: 'old' }], [{ ref: 'x', text: 'new' }]))
.toEqual([{ ref: 'x', text: 'new' }]);
});
it('выпавший ref восстанавливается из старого', () => {
expect(preserveKnowledge([{ ref: 'x', text: 'old' }], []))
.toEqual([{ ref: 'x', text: 'old' }]);
});
it('новый ref добавляется после старых', () => {
expect(preserveKnowledge([{ ref: 'x', text: 'a' }], [{ ref: 'y', text: 'b' }]))
.toEqual([{ ref: 'x', text: 'a' }, { ref: 'y', text: 'b' }]);
});
});
describe('reconcileTurn — база знаний переживает ход', () => {
it('запись, выпавшую из ответа модели, восстанавливает', async () => {
const proto = { ...EMPTY_PROTOCOL(), knowledge: [{ ref: 'a.mjs:1', text: 'старое знание' }] };
const callModel = async () => JSON.stringify({
subject: 'd', decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [],
knowledge: [], step: { user: 'u', assistant: 'a' },
});
const out = await reconcileTurn({ proto, ex: { user: 'u', assistant: 'a', actions: [] }, turn: 2, session: 's', callModel });
expect(out.knowledge.map((k) => k.ref)).toContain('a.mjs:1');
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npx vitest run tools/secretary-reconcile.test.mjs -t "knowledge|preserveKnowledge|базу знаний"`
Expected: FAIL — `preserveKnowledge` is not exported; `out.knowledge` loses `a.mjs:1`.
- [ ] **Step 3: Implement `preserveKnowledge`**
In `tools/secretary-reconcile.mjs`, add this exported function near the other reconcile helpers (e.g. right after `restoreLostLines`):
```javascript
/** Слить базы знаний по ключу ref: старое не теряем, общий ref берёт новый текст, новые добавляем.
* Реализует «копим; дубль уточняем/расширяем; не удаляем» (knowledge не в SECTIONS — своя защита). */
export function preserveKnowledge(oldK = [], newK = []) {
const norm = (e) => ({ ref: String((e && e.ref) || '').trim(), text: String((e && e.text) || '').trim() });
const old = (Array.isArray(oldK) ? oldK : []).map(norm).filter((e) => e.ref);
const fresh = (Array.isArray(newK) ? newK : []).map(norm).filter((e) => e.ref);
const freshByRef = new Map(fresh.map((e) => [e.ref, e]));
const out = [];
const seen = new Set();
for (const e of old) { out.push(freshByRef.get(e.ref) || e); seen.add(e.ref); } // старое сохранено; общий ref → новый текст
for (const e of fresh) if (!seen.has(e.ref)) { out.push(e); seen.add(e.ref); } // новые ref после старых
return out;
}
```
- [ ] **Step 4: Wire into `reconcileTurn`**
In `tools/secretary-reconcile.mjs`, inside `reconcileTurn`, find:
```javascript
const returned = collapseProtocol(parsed);
const guard = reconcileGuard(clean, returned);
```
change to:
```javascript
const returned = collapseProtocol(parsed);
returned.knowledge = preserveKnowledge(clean.knowledge, returned.knowledge); // «не удаляем»: выпавший ref вернётся
const guard = reconcileGuard(clean, returned);
```
- [ ] **Step 4b: Carry `knowledge` through `stampProvenance`**
ВАЖНО: `stampProvenance` возвращает СВЕЖИЙ объект и переносит только subject/status/6 корзин/history/steps — без явного поля `knowledge` оно потеряется. In `tools/secretary-reconcile.mjs` `stampProvenance`, find its return object:
```javascript
history: [...legacy, ...toggleMap.values()],
steps: Array.isArray(oldProtocol.steps) ? oldProtocol.steps : [],
};
}
```
change to (добавить `knowledge` — provenance ему не нужен, несём как есть):
```javascript
history: [...legacy, ...toggleMap.values()],
steps: Array.isArray(oldProtocol.steps) ? oldProtocol.steps : [],
knowledge: Array.isArray(returned.knowledge) ? returned.knowledge : [],
};
}
```
- [ ] **Step 5: Run tests to verify they pass**
Run: `npx vitest run tools/secretary-reconcile.test.mjs`
Expected: PASS (все прежние + новые).
- [ ] **Step 6: Commit**
```bash
git add tools/secretary-reconcile.mjs tools/secretary-reconcile.test.mjs
git commit -m "feat(secretary): preserveKnowledge — база знаний не теряется при сбое редактора
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 3: промпт редактора — поле, правило, подача базы на вход
**Files:**
- Modify: `tools/secretary-reconcile.mjs` (`buildReconcilePrompt`)
- Test: `tools/secretary-reconcile.test.mjs`
- [ ] **Step 1: Write the failing test**
Append to `tools/secretary-reconcile.test.mjs`:
```javascript
describe('buildReconcilePrompt — база знаний', () => {
it('система просит поле knowledge и правило источника', () => {
const { system } = buildReconcilePrompt({ protocol: {}, lastExchange: {} });
expect(system).toMatch(/knowledge/);
expect(system).toMatch(/источник/i);
});
it('на вход подаёт текущую базу знаний (между темой и решениями)', () => {
const { user } = buildReconcilePrompt({
protocol: { subject: 'тема', knowledge: [{ ref: 'a.mjs:5', text: 'факт A' }], decisions: [] },
lastExchange: {},
});
expect(user).toContain('факт A — a.mjs:5');
expect(user.indexOf('База знаний')).toBeGreaterThan(user.indexOf('Тема дела'));
expect(user.indexOf('База знаний')).toBeLessThan(user.indexOf('Решения'));
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npx vitest run tools/secretary-reconcile.test.mjs -t "buildReconcilePrompt — база знаний"`
Expected: FAIL — нет `knowledge` в системе и нет блока «База знаний» в user.
- [ ] **Step 3: Implement — поле в списке возврата + правило**
In `tools/secretary-reconcile.mjs` `buildReconcilePrompt`, find:
```javascript
'will[{text,struck}], open[{text,struck}], doneNext[{text,done,struck}]).',
'ПРАВИЛА:',
```
change to:
```javascript
'will[{text,struck}], open[{text,struck}], doneNext[{text,done,struck}], knowledge[{text,ref}]).',
'ПРАВИЛА:',
```
Then find the last system rule (point 9) ending block:
```javascript
' к тебе — в "will") ОТДЕЛЬНО; ещё не решённую часть (формат/способ/детали) положи в "open".',
' НЕ прячь уже принятое решение целиком в "open" или "doneNext".',
].join('\n');
```
change to (add point 10):
```javascript
' к тебе — в "will") ОТДЕЛЬНО; ещё не решённую часть (формат/способ/детали) положи в "open".',
' НЕ прячь уже принятое решение целиком в "open" или "doneNext".',
'10. БАЗА ЗНАНИЙ ("knowledge"): из действий хода (чтения файлов/спек/url с содержимым) вынеси',
' ДОБЫТЫЕ ФАКТЫ — каждый как {"text":"<что узнал>","ref":"<источник: файл:строка/url/раздел>"}.',
' Источник ОБЯЗАТЕЛЕН (нет источника → это НЕ знание, не пиши; вывод/решение → "decisions").',
' Знание ВПЕРЁД, источник В КОНЦЕ. Если про этот источник запись уже есть — УТОЧНИ/РАСШИРЬ её',
' (тот же "ref"), не дублируй. НИЧЕГО из базы не удаляй и не сокращай.',
].join('\n');
```
- [ ] **Step 4: Implement — база знаний на вход (между темой и решениями)**
In `tools/secretary-reconcile.mjs` `buildReconcilePrompt`, find:
```javascript
const user = [
`Тема дела: ${protocol.subject || '(нет)'}`,
sec('Решения', protocol.decisions), sec('Альтернативы', protocol.alternatives),
```
change to (insert «База знаний» строкой между темой и решениями):
```javascript
const knowLines = ((protocol.knowledge || []).map((e) => ` - ${e.text}${e.ref ? ' — ' + e.ref : ''}`).join('\n')) || ' (пусто)';
const user = [
`Тема дела: ${protocol.subject || '(нет)'}`,
`База знаний (что уже добыто — НЕ удаляй, дубль по источнику уточняй):\n${knowLines}`,
sec('Решения', protocol.decisions), sec('Альтернативы', protocol.alternatives),
```
- [ ] **Step 5: Run test to verify it passes**
Run: `npx vitest run tools/secretary-reconcile.test.mjs`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add tools/secretary-reconcile.mjs tools/secretary-reconcile.test.mjs
git commit -m "feat(secretary): редактор ведёт базу знаний (правило + подача на вход)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 4: показы (protocol.md + архив.md) и сохранность в slim
**Files:**
- Modify: `tools/secretary-render-fluffy.mjs`, `tools/secretary-render-archive.mjs`
- Test: `tools/secretary-render-fluffy.test.mjs`, `tools/secretary-render-archive.test.mjs`, `tools/secretary-slim.test.mjs`
- [ ] **Step 1: Write the failing tests**
Append to `tools/secretary-render-fluffy.test.mjs`:
```javascript
describe('renderFluffy — база знаний', () => {
it('печатает раздел между темой и стволом', () => {
const md = renderFluffy({ ...base(), subject: 'тема', knowledge: [{ ref: 'f.mjs:10', text: 'факт' }] });
expect(md).toContain('## 🧠 База знаний');
expect(md).toContain('факт — f.mjs:10');
expect(md.indexOf('База знаний')).toBeLessThan(md.indexOf('🌳 Ствол'));
});
});
```
Append to `tools/secretary-render-archive.test.mjs`:
```javascript
describe('renderArchive — база знаний', () => {
it('печатает раздел со строкой знание — источник', () => {
const md = renderArchive(base({ knowledge: [{ ref: 'f.mjs:10', text: 'факт' }] }));
expect(md).toContain('## База знаний');
expect(md).toContain('факт — f.mjs:10');
});
});
```
Append to `tools/secretary-slim.test.mjs` (внутри describe('slimProtocol', ...) или новым):
```javascript
describe('slimProtocol — knowledge', () => {
it('сохраняет базу знаний целиком', () => {
const s = slimProtocol({ knowledge: [{ ref: 'a', text: 'b' }], hidden: [], candidates: [] });
expect(s.knowledge).toEqual([{ ref: 'a', text: 'b' }]);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npx vitest run tools/secretary-render-fluffy.test.mjs tools/secretary-render-archive.test.mjs tools/secretary-slim.test.mjs -t "база знаний|knowledge"`
Expected: FAIL — рендеры не печатают раздел (slim-тест пройдёт сразу, т.к. spread уже сохраняет — это страховочный кейс).
- [ ] **Step 3: Implement renderFluffy**
In `tools/secretary-render-fluffy.mjs`, find:
```javascript
L.push(`*статус: ${p.status || 'открыто'}${opts.date ? ' · ' + opts.date : ''} · каждая строка тянется до ходы/turn-N.log*`, '');
// 🌳 СТВОЛ — живое наверху, зачёркнутое собираем в свёрнутый блок
L.push('## 🌳 Ствол', '');
```
change to (вставить «База знаний» между шапкой и Стволом):
```javascript
L.push(`*статус: ${p.status || 'открыто'}${opts.date ? ' · ' + opts.date : ''} · каждая строка тянется до ходы/turn-N.log*`, '');
// 🧠 БАЗА ЗНАНИЙ — добытые факты с источником в конце (между темой и стволом)
const know = (p.knowledge || []).filter((e) => e && e.text);
if (know.length) {
L.push('## 🧠 База знаний', '');
for (const e of know) L.push(`- ${e.text}${e.ref ? `${e.ref}` : ''}`);
L.push('');
}
// 🌳 СТВОЛ — живое наверху, зачёркнутое собираем в свёрнутый блок
L.push('## 🌳 Ствол', '');
```
- [ ] **Step 4: Implement renderArchive**
In `tools/secretary-render-archive.mjs`, find:
```javascript
for (const [k, t] of [['decisions', 'Решения'], ['will', 'Воля'], ['open', 'Открытые'], ['consequences', 'Последствия'], ['doneNext', 'Сделано/дальше']])
L.push(`**${t}**`, ...((proto[k] || []).map((e) => `- ${e.struck ? '~~' + e.text + '~~' : e.text}`)), '');
// Все ветки и кандидаты — живые и мёртвые, со статусом, наследником, историей по шагам и родословной.
```
change to (вставить раздел между Стволом и ветками):
```javascript
for (const [k, t] of [['decisions', 'Решения'], ['will', 'Воля'], ['open', 'Открытые'], ['consequences', 'Последствия'], ['doneNext', 'Сделано/дальше']])
L.push(`**${t}**`, ...((proto[k] || []).map((e) => `- ${e.struck ? '~~' + e.text + '~~' : e.text}`)), '');
// База знаний — добытые факты с источником в конце.
const know = (proto.knowledge || []).filter((e) => e && e.text);
L.push('## База знаний');
if (!know.length) L.push('(пусто)');
for (const e of know) L.push(`- ${e.text}${e.ref ? `${e.ref}` : ''}`);
L.push('');
// Все ветки и кандидаты — живые и мёртвые, со статусом, наследником, историей по шагам и родословной.
```
- [ ] **Step 5: Run tests to verify they pass**
Run: `npx vitest run tools/secretary-render-fluffy.test.mjs tools/secretary-render-archive.test.mjs tools/secretary-slim.test.mjs`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add tools/secretary-render-fluffy.mjs tools/secretary-render-archive.mjs tools/secretary-render-fluffy.test.mjs tools/secretary-render-archive.test.mjs tools/secretary-slim.test.mjs
git commit -m "feat(secretary): показ База знаний в protocol.md и архив.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 5: полный регресс секретаря
**Files:** нет (проверка)
- [ ] **Step 1: Прогнать все secretary-тесты**
Run: `npx vitest run tools/secretary-*.test.mjs`
Expected: PASS, exit 0. Ни один прежний кейс не сломан.
- [ ] **Step 2: Если красное — чинить точечно**
По superpowers:systematic-debugging. Ожидаемые расширения схемы (НЕ дефекты логики — обновить ожидания тестов):
- тест EMPTY_PROTOCOL «все 9 разделов» мог считать ключи — теперь +`knowledge`;
- тест, сверяющий точную форму вывода `stampProvenance` через `toEqual`, теперь увидит лишний ключ `knowledge: []` — добавить его в ожидаемый объект.
---
## Заметки для исполнителя
- **Коммит/пуш — через терминал владельца** (verify-receipt + один Bash, цепочки режет пол). Согласовать момент.
- **Границы:** не подавать базу 4 ролям (diagnostic/catcher/brainstorm/gardener); не вводить `struck`/удаление/окно у knowledge; не трогать `renderProtocol` (off-path), очередь/хуки/воркер-IO.
- **Ветка** — продолжаем на `feat/secretary-slim-protocol` (та же тема секретаря) или новой по решению владельца.
@@ -0,0 +1,426 @@
# Переделка подачи всех 5 мозгов секретаря — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Единый богатый контекст ролям: ТЕМА·БАЗА·СТВОЛ·ВЕТКИ·КАНДИДАТЫ·N ходов целиком·шаги−N. editor/gardener — 7 ходов, diagnostic/catcher/brainstorm — 4.
**Architecture:** Новые детерминированные хелперы в harvest (renderTurns/stepsMinusLast/fullTrunk/knowledgeBlock). USER-сборщики переписаны (берут `spanEx`, рендерят N ходов сами). distill передаёт `spanEx`. editor +шаги−7. diffTrunk +база знаний.
**Tech Stack:** Node ESM (`.mjs`), vitest.
**Spec:** [docs/superpowers/specs/2026-06-26-secretary-roles-feeding-redesign-design.md](../specs/2026-06-26-secretary-roles-feeding-redesign-design.md)
---
## Task 1: хелперы harvest (renderTurns/stepsMinusLast/fullTrunk/knowledgeBlock)
**Files:**
- Modify: `tools/secretary-harvest.mjs`
- Test: `tools/secretary-harvest.test.mjs`
- [ ] **Step 1: Write failing tests** — append to `tools/secretary-harvest.test.mjs`:
```javascript
import { renderTurns, stepsMinusLast, fullTrunk, knowledgeBlock } from './secretary-harvest.mjs';
describe('harvest — новые кирпичи подачи', () => {
const turns = (n) => Array.from({ length: n }, (_, i) => ({ turn: i + 1, user: `u${i + 1}`, assistant: `a${i + 1}`, actions: [{ tool: 'Read', input: `f${i + 1}`, result: 'r' }] }));
it('renderTurns — последние n ходов целиком, с пометкой если больше', () => {
const t = renderTurns(turns(6), 4);
expect(t).toContain('[ХОД 6]');
expect(t).toContain('[ХОД 3]');
expect(t).not.toContain('[ХОД 2]');
expect(t).toContain('• Read in=f6');
expect(t).toMatch(/последние 4 из 6 ходов/);
});
it('renderTurns — ≤n ходов: без пометки, все', () => {
const t = renderTurns(turns(2), 4);
expect(t).toContain('[ХОД 1]');
expect(t).not.toMatch(/последние \d+ из/);
});
it('stepsMinusLast — все шаги минус последние n', () => {
const p = { steps: Array.from({ length: 6 }, (_, i) => ({ turn: i + 1, text: `шаг-${i + 1}` })) };
const s = stepsMinusLast(p, 4);
expect(s).toContain('шаг-1');
expect(s).toContain('шаг-2');
expect(s).not.toContain('шаг-3');
expect(s).not.toContain('шаг-6');
});
it('fullTrunk — все 6 разделов, зачёркнутые помечены', () => {
const p = { decisions: [{ text: 'жив-реш' }, { text: 'мёртв-реш', struck: true }], will: [{ text: 'воля' }] };
const t = fullTrunk(p);
expect(t).toContain('Решения:');
expect(t).toContain('жив-реш');
expect(t).toContain('[зачёркнуто] мёртв-реш');
expect(t).toContain('Альтернативы:');
expect(t).toContain('Сделано:');
});
it('knowledgeBlock — строки знание — источник; пусто → (пусто)', () => {
expect(knowledgeBlock({ knowledge: [{ ref: 'a.mjs:1', text: 'факт' }] })).toContain('факт — a.mjs:1');
expect(knowledgeBlock({})).toContain('(пусто)');
});
});
```
- [ ] **Step 2: Run to verify fail** — Run: `npx vitest run tools/secretary-harvest.test.mjs -t "новые кирпичи"` — Expected: FAIL (helpers not exported).
- [ ] **Step 3: Implement** — in `tools/secretary-harvest.mjs`, add after the `clamp` function (right before the `MAX_EXCHANGE_ACTIONS` block):
```javascript
// Сколько последних ходов спана подавать целиком: роли — 4, садовник/редактор — 7.
export const TURNS_ROLE = 4;
export const TURNS_GARDENER = 7;
/** Последние n ходов спана целиком: [ХОД K] юзер+ассистент+ВСЕ действия. Пометка если ходов больше. */
export function renderTurns(turns, n) {
const all = Array.isArray(turns) ? turns : [];
const shown = all.slice(-n);
const head = all.length > shown.length ? `(показаны последние ${shown.length} из ${all.length} ходов)\n\n` : '';
const acts = (list) => ((list || []).map((a) => `${a.tool} in=${a.input ?? ''}${a.result != null ? `${clamp(String(a.result))}` : ''}`).join('\n')) || ' —';
return head + shown.map((t) =>
`[ХОД ${t.turn}]\n[ЮЗЕР]: ${t.user || ''}\n[АССИСТЕНТ]: ${t.assistant || ''}\n[ДЕЙСТВИЯ]:\n${acts(t.actions)}`).join('\n\n');
}
/** Краткий лог ВСЕХ ходов минус последние n (они показаны целиком — не дублируем). */
export function stepsMinusLast(proto, n) {
const all = (proto.steps || []).slice().sort((a, b) => (a.turn || 0) - (b.turn || 0));
const keep = n > 0 ? all.slice(0, Math.max(0, all.length - n)) : all;
return keep.map((s) => `ход ${s.turn}: ${s.text}`).join('\n') || '(пока нет)';
}
/** Полный ствол: все 6 разделов, зачёркнутые помечены. */
export function fullTrunk(proto) {
const sec = (name, arr) => `${name}:\n` + (((arr || []).map((e) => ` - ${e.struck ? '[зачёркнуто] ' : ''}${e.text}${e.why ? ' — ' + e.why : ''}`).join('\n')) || ' (пусто)');
return [
sec('Решения', proto.decisions), sec('Альтернативы', proto.alternatives),
sec('Последствия', proto.consequences), sec('Воля', proto.will),
sec('Открытые', proto.open), sec('Сделано', proto.doneNext),
].join('\n');
}
/** База знаний: строки «знание — источник». */
export function knowledgeBlock(proto) {
return ((proto.knowledge || []).map((e) => ` - ${e.text}${e.ref ? ' — ' + e.ref : ''}`).join('\n')) || ' (пусто)';
}
```
- [ ] **Step 4: Run to verify pass** — Run: `npx vitest run tools/secretary-harvest.test.mjs` — Expected: PASS (прежние + 5 новых).
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-harvest.mjs tools/secretary-harvest.test.mjs
git commit -m "feat(secretary): хелперы подачи — renderTurns/stepsMinusLast/fullTrunk/knowledgeBlock
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 2: переписать USER-сборщики + distill + убрать renderExchangeText
**Files:**
- Modify: `tools/secretary-harvest.mjs` (builders, удалить renderExchangeText/MAX_EXCHANGE_ACTIONS)
- Modify: `tools/secretary-distill.mjs` (передавать spanEx)
- Test: `tools/secretary-harvest.test.mjs`, `tools/secretary-distill.test.mjs`
- [ ] **Step 1: Rewrite builders** — in `tools/secretary-harvest.mjs`, replace the whole `// ── USER-сборщики` block (functions `user11`, `user12`, `user13`, `userG`) with:
```javascript
// ── USER-сборщики (богатый контекст: тема+база+ствол+ветки+кандидаты+N ходов+шаги−N) ──
/** Общий контекст для diagnostic/catcher/brainstorm: n=TURNS_ROLE ходов целиком. */
function roleContext(proto, spanEx, n) {
return `ТЕМА: ${proto.subject || '(нет)'}\n\n`
+ `БАЗА ЗНАНИЙ:\n${knowledgeBlock(proto)}\n\n`
+ `СТВОЛ:\n${fullTrunk(proto)}\n\n`
+ `${registry(proto)}\n\nЖИВЫЕ КАНДИДАТЫ:\n${candidatesForPrompt(proto)}${THEMES_BLOCK(proto)}\n\n`
+ `=== ПОСЛЕДНИЕ ХОДЫ (целиком) ===\n${renderTurns(spanEx && spanEx.turns, n)}\n\n`
+ `КРАТКИЙ ЛОГ ОСТАЛЬНЫХ ХОДОВ:\n${stepsMinusLast(proto, n)}`;
}
export function user11(proto, spanEx) { return roleContext(proto, spanEx, TURNS_ROLE); }
export function user12(proto, spanEx) { return roleContext(proto, spanEx, TURNS_ROLE); }
export function user13(proto, spanEx, found11, found12) {
return `${roleContext(proto, spanEx, TURNS_ROLE)}`
+ `\n\n=== УЖЕ НАЙДЕНО ЭТОТ ХОД (не повторяй) ===\nДиагностика: ${found11}\nБрошенное: ${found12}`;
}
export function userG(proto, spanEx, diff) {
return `ЧТО ИЗМЕНИЛОСЬ В СТВОЛЕ ЭТОТ ХОД (дифф редактора):\n${diff}\n\n`
+ `${registry(proto)}\nЖИВЫЕ КАНДИДАТЫ (для моста «повысить»):\n${candidatesForPrompt(proto)}${THEMES_BLOCK(proto)}\n\n`
+ `=== ПОСЛЕДНИЕ ХОДЫ (целиком) ===\n${renderTurns(spanEx && spanEx.turns, TURNS_GARDENER)}\n\n`
+ `КРАТКИЙ ЛОГ ОСТАЛЬНЫХ ХОДОВ:\n${stepsMinusLast(proto, TURNS_GARDENER)}`;
}
```
- [ ] **Step 2: Remove dead code** — in `tools/secretary-harvest.mjs`, delete the `MAX_EXCHANGE_ACTIONS` const + comment AND the entire `renderExchangeText` function (they are no longer used). Find and delete:
```javascript
// Сколько ПОСЛЕДНИХ действий хода показывать ролям/редактору: длинный ход с десятками Read
// не возим целиком (анти-раздувание). Суть хода всё равно фиксирует «step» редактора.
export const MAX_EXCHANGE_ACTIONS = 7;
/** Текст обмена из спана {user, assistant, actions}. Показываем последние MAX_EXCHANGE_ACTIONS действий. */
export function renderExchangeText(spanEx) {
const all = spanEx.actions || [];
const shown = all.slice(-MAX_EXCHANGE_ACTIONS);
const head = all.length > shown.length ? ` (показаны последние ${shown.length} из ${all.length} действий)\n` : '';
const acts = (shown.map((a) => `${a.tool} in=${a.input ?? ''}${a.result != null ? `${clamp(String(a.result))}` : ''}`).join('\n')) || '—';
return `[ЮЗЕР]: ${spanEx.user || ''}\n[АССИСТЕНТ]: ${spanEx.assistant || ''}\n[ДЕЙСТВИЯ]:\n${head}${acts}`;
}
```
(Keep `clamp`, `recentSteps`, `trunkForCatcher``clamp` is used by `renderTurns`; the other two are harmless legacy with their own tests.)
- [ ] **Step 3: Update distill** — in `tools/secretary-distill.mjs`:
(a) In the import from `./secretary-harvest.mjs`, remove `renderExchangeText` from the list.
(b) In the `fluffy` block, find:
```javascript
const spanText = renderExchangeText(spanEx);
const txt = (r) => (typeof r === 'string' ? r : (r && r.text) || '');
const call = (role, system, user) => callModel({ system, user, model: resolveModel(role) });
// ПОДАЧА ролям — из live-only проекции (надгробия не возим): мёртвое отсеяно, мутации ниже идут по полному proto.
const view = slimProtocol(toWrite);
// сбор: 1.1 диагностика ∥ 1.2 ловец, затем 1.3 брейншторм (после них)
const [r11, r12] = await Promise.all([
call('diagnostic', SYS_11, user11(view, spanText, { start, end })),
call('catcher', SYS_12, user12(view, spanText)),
]);
const d11 = diagFindings(parseLoose(txt(r11), 'lenses'));
const d12 = parseLoose(txt(r12), 'dropped');
const r13 = await call('brainstorm', SYS_13, user13(view, spanText, { start, end }, JSON.stringify(d11.new), JSON.stringify(d12.dropped)));
const d13 = parseLoose(txt(r13), 'forks');
// ЭТАП 1: урожай (новые ветки + рождение/заточка/полнота кандидатов) — садовник увидит свежую грядку
const withHarvest = applyHarvest(toWrite, start, d11, d12, d13);
// ЭТАП 2: садовник — подача из СВЕЖЕЙ live-проекции (видит свежие живые ветки и кандидаты), без надгробий
const rG = await call('gardener', SYS_G, userG(slimProtocol(withHarvest), spanText, diff));
```
replace with (передаём `spanEx`, числа ходов — внутри сборщиков):
```javascript
const txt = (r) => (typeof r === 'string' ? r : (r && r.text) || '');
const call = (role, system, user) => callModel({ system, user, model: resolveModel(role) });
// ПОДАЧА ролям — из live-only проекции (надгробия не возим). spanEx несёт turns — сборщики рендерят N ходов сами.
const view = slimProtocol(toWrite);
// сбор: 1.1 диагностика ∥ 1.2 ловец, затем 1.3 брейншторм (после них)
const [r11, r12] = await Promise.all([
call('diagnostic', SYS_11, user11(view, spanEx)),
call('catcher', SYS_12, user12(view, spanEx)),
]);
const d11 = diagFindings(parseLoose(txt(r11), 'lenses'));
const d12 = parseLoose(txt(r12), 'dropped');
const r13 = await call('brainstorm', SYS_13, user13(view, spanEx, JSON.stringify(d11.new), JSON.stringify(d12.dropped)));
const d13 = parseLoose(txt(r13), 'forks');
// ЭТАП 1: урожай (новые ветки + рождение/заточка/полнота кандидатов) — садовник увидит свежую грядку
const withHarvest = applyHarvest(toWrite, start, d11, d12, d13);
// ЭТАП 2: садовник — подача из СВЕЖЕЙ live-проекции (видит свежие живые ветки и кандидаты), без надгробий
const rG = await call('gardener', SYS_G, userG(slimProtocol(withHarvest), spanEx, diff));
```
- [ ] **Step 4: Fix harvest tests** — in `tools/secretary-harvest.test.mjs`:
(a) Change the first import line — remove `renderExchangeText` from it:
```javascript
import { SYS_11, SYS_12, SYS_13, diagFindings, user11, user12, user13, userG, themesInUse, fullSteps, tombstones } from './secretary-harvest.mjs';
```
(b) DELETE these now-obsolete describe blocks / tests entirely:
- the `it('renderExchangeText форматирует обмен с действиями', ...)` test inside `describe('USER-сборщики', ...)`;
- the three old builder tests inside `describe('USER-сборщики', ...)` (`user12 даёт ловцу ствол…`, `user11 несёт реестр…`, `user13 добавляет уже найденное`);
- the whole `describe('renderExchangeText input cap', ...)` block;
- the whole `describe('renderExchangeText — последние 7 действий', ...)` block;
- the `it('user13 включает шаги, темы и живых кандидатов', ...)` test (inside «брейншторм v2»);
- the `it('user12 включает шаги, волю и реестр живых веток', ...)` test (inside «ловец прозрел»).
(c) ADD a new describe block at the end:
```javascript
describe('USER-сборщики (богатый контекст)', () => {
const spanEx = (n = 2) => ({ turns: Array.from({ length: n }, (_, i) => ({ turn: i + 1, user: `u${i + 1}`, assistant: `a${i + 1}`, actions: [{ tool: 'Read', input: `f${i + 1}`, result: 'r' }] })) });
const proto = () => ({
subject: 'дело', knowledge: [{ ref: 'a.mjs:1', text: 'факт' }],
decisions: [{ text: 'решено-X' }], alternatives: [], consequences: [], will: [{ text: 'воля-один' }], open: [{ text: 'открыто-Y' }], doneNext: [],
hidden: [{ id: 'СВ-1', lens: 'Л1', status: 'открыт', text: 'ветка' }],
candidates: [{ id: 'КД-1', branch: 'идея-кандидат', status: 'жив', тема: 't' }],
steps: Array.from({ length: 6 }, (_, i) => ({ turn: i + 1, text: `шаг-${i + 1}` })),
});
it('user11/user12 несут тему, базу, ствол, ветки, кандидатов, 4 хода, шаги−4', () => {
for (const build of [user11, user12]) {
const u = build(proto(), spanEx(6));
expect(u).toContain('дело');
expect(u).toContain('факт — a.mjs:1');
expect(u).toContain('решено-X');
expect(u).toContain('воля-один');
expect(u).toContain('СВ-1');
expect(u).toContain('КД-1');
expect(u).toContain('[ХОД 6]');
expect(u).toMatch(/последние 4 из 6 ходов/);
expect(u).toContain('шаг-1');
expect(u).not.toContain('шаг-6');
}
});
it('user13 = контекст + хвост «уже найдено»', () => {
const u = user13(proto(), spanEx(2), '["находка"]', '["брошено"]');
expect(u).toContain('СВ-1');
expect(u).toContain('УЖЕ НАЙДЕНО');
expect(u).toContain('находка');
});
it('userG = дифф + ветки + кандидаты + 7 ходов + шаги−7', () => {
const u = userG(proto(), spanEx(2), 'ДИФФ-СТРОКА');
expect(u).toContain('ДИФФ-СТРОКА');
expect(u).toContain('СВ-1');
expect(u).toContain('КД-1');
expect(u).toContain('[ХОД 2]');
});
});
```
- [ ] **Step 5: Fix distill tests** — in `tools/secretary-distill.test.mjs`, the fluffy tests pass a `spanEx` with `{ user, assistant, actions }` but no `turns`. They assert the SADOVNIK sees a fresh candidate by name. That assertion still holds (candidate name comes from the protocol, not the turns). No change needed UNLESS a test asserts old `renderExchangeText` output — it does not. Run the suite; if a fluffy test fails because it asserted span TEXT in a prompt, add `turns: [{ turn: 1, user: spanEx.user, assistant: spanEx.assistant, actions: spanEx.actions }]` to that test's spanEx. (Most should pass unchanged.)
- [ ] **Step 6: Run** — Run: `npx vitest run tools/secretary-harvest.test.mjs tools/secretary-distill.test.mjs` — Expected: PASS. If a distill fluffy test fails on a missing prompt substring, apply the `turns` fix from Step 5.
- [ ] **Step 7: Commit**
```bash
git add tools/secretary-harvest.mjs tools/secretary-distill.mjs tools/secretary-harvest.test.mjs tools/secretary-distill.test.mjs
git commit -m "feat(secretary): роли — единый богатый контекст (ствол+база+ветки+кандидаты+N ходов+шаги−N)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 3: editor — добавить шаги−7
**Files:**
- Modify: `tools/secretary-reconcile.mjs` (import + user-промпт)
- Test: `tools/secretary-reconcile.test.mjs`
- [ ] **Step 1: Write failing test** — append to `tools/secretary-reconcile.test.mjs`:
```javascript
describe('buildReconcilePrompt — шаги−7', () => {
it('добавляет краткий лог всех ходов минус последние 7', () => {
const steps = Array.from({ length: 9 }, (_, i) => ({ turn: i + 1, text: `шаг-${i + 1}` }));
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [], steps }, lastExchange: { turns: [] } });
expect(user).toContain('шаг-1');
expect(user).toContain('шаг-2');
expect(user).not.toContain('шаг-3');
});
});
```
- [ ] **Step 2: Run to verify fail** — Run: `npx vitest run tools/secretary-reconcile.test.mjs -t "шаги−7"` — Expected: FAIL.
- [ ] **Step 3: Implement** — in `tools/secretary-reconcile.mjs`:
(a) Add import at the top (after the opening comment, before `const MAX_EXCHANGE_TURNS`):
```javascript
import { stepsMinusLast } from './secretary-harvest.mjs';
```
(b) In `buildReconcilePrompt`, find in the `user` array:
```javascript
'', 'Последние ходы (обмен):',
exchangeText,
```
change to:
```javascript
'', 'Краткий лог остальных ходов:',
stepsMinusLast(protocol, MAX_EXCHANGE_TURNS),
'', 'Последние ходы (обмен):',
exchangeText,
```
- [ ] **Step 4: Run to verify pass** — Run: `npx vitest run tools/secretary-reconcile.test.mjs` — Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-reconcile.mjs tools/secretary-reconcile.test.mjs
git commit -m "feat(secretary): редактору добавить краткий лог ходов (шаги−7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 4: diffTrunk — добавить дифф базы знаний
**Files:**
- Modify: `tools/secretary-trunkdiff.mjs`
- Test: `tools/secretary-trunkdiff.test.mjs`
- [ ] **Step 1: Write failing test** — append to `tools/secretary-trunkdiff.test.mjs` (если файла нет — создать с импортом `import { describe, it, expect } from 'vitest'; import { diffTrunk } from './secretary-trunkdiff.mjs';`):
```javascript
describe('diffTrunk — база знаний', () => {
it('показывает добавленное и уточнённое знание', () => {
const oldP = { knowledge: [{ ref: 'a.mjs:1', text: 'старое' }] };
const newP = { knowledge: [{ ref: 'a.mjs:1', text: 'уточнённое' }, { ref: 'b.mjs:2', text: 'новое' }] };
const d = diffTrunk(oldP, newP);
expect(d).toContain('ЗНАНИЕ уточнено');
expect(d).toContain('a.mjs:1');
expect(d).toContain('ЗНАНИЕ добавлено');
expect(d).toContain('b.mjs:2');
});
});
```
- [ ] **Step 2: Run to verify fail** — Run: `npx vitest run tools/secretary-trunkdiff.test.mjs -t "база знаний"` — Expected: FAIL.
- [ ] **Step 3: Implement** — in `tools/secretary-trunkdiff.mjs`, find:
```javascript
return lines.length ? lines.join('\n') : '(ствол не двигался этот ход)';
}
```
change to (добавить дифф знаний перед возвратом):
```javascript
const oldK = new Map(((oldP.knowledge) || []).map((e) => [String(e.ref || '').trim(), String(e.text || '').trim()]));
for (const e of (newP.knowledge) || []) {
const ref = String(e.ref || '').trim();
const text = String(e.text || '').trim();
if (!ref) continue;
if (!oldK.has(ref)) lines.push(`ЗНАНИЕ добавлено: «${text}${ref}»`);
else if (oldK.get(ref) !== text) lines.push(`ЗНАНИЕ уточнено: «${text}${ref}»`);
}
return lines.length ? lines.join('\n') : '(ствол не двигался этот ход)';
}
```
- [ ] **Step 4: Run to verify pass** — Run: `npx vitest run tools/secretary-trunkdiff.test.mjs` — Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-trunkdiff.mjs tools/secretary-trunkdiff.test.mjs
git commit -m "feat(secretary): diffTrunk показывает дифф базы знаний (для садовника)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 5: полный регресс
- [ ] **Step 1: Прогнать все** — Run: `npx vitest run tools/secretary-*.test.mjs` — Expected: PASS, exit 0.
- [ ] **Step 2: Если красное — чинить точечно** (superpowers:systematic-debugging). Вероятный риск: distill fluffy-тест ждал подстроку обмена → добавить `turns` в его spanEx (Task 2 Step 5).
---
## Заметки для исполнителя
- **Коммит/пуш — терминал владельца** (verify-receipt + один Bash, цепочки режет пол).
- **Границы:** системные промпты SYS_11/12/13/SYS_G НЕ трогать; нарезку спанов/очередь/воркер НЕ трогать.
- **Ветка** — `feat/secretary-slim-protocol`.
@@ -0,0 +1,386 @@
# Маленький протокол для агента (slim working JSON) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Перестать слать «надгробия» (мёртвые ветки/кандидаты) в LLM — кормить 4 пушистые роли копией протокола только из живого; большой протокол и архив не трогать.
**Architecture:** Чистая функция `slimProtocol(proto)` отдаёт live-only копию. В `secretary-distill.mjs` (ветка fluffy) промпты для diagnostic/catcher/brainstorm/gardener строятся из slim; editor — из полного. Воркер пишет видимый `protocol-slim.json`. Решения агента применяет детерминированный код к полному протоколу — мёртвое копится там.
**Tech Stack:** Node ESM (`.mjs`), vitest (`import { describe, it, expect } from 'vitest'`).
**Spec:** [docs/superpowers/specs/2026-06-26-secretary-slim-protocol-design.md](../specs/2026-06-26-secretary-slim-protocol-design.md)
---
## Файловая карта
| Файл | Ответственность |
|---|---|
| `tools/secretary-slim.mjs` | **новый**`slimProtocol(proto)` + предикат «жив» |
| `tools/secretary-slim.test.mjs` | **новый** — юниты slim |
| `tools/secretary-distill.mjs` | подача 4 ролей из slim (editor — нет) |
| `tools/secretary-distill.test.mjs` | кейс: в подаче catcher/gardener нет надгробий |
| `tools/secretary-worker.mjs` | запись `protocol-slim.json` |
| `tools/secretary-worker.test.mjs` | кейс: `protocol-slim.json` = live-only |
---
## Task 1: `slimProtocol(proto)` — чистая live-only проекция
**Files:**
- Create: `tools/secretary-slim.mjs`
- Test: `tools/secretary-slim.test.mjs`
- [ ] **Step 1: Write the failing test**
Create `tools/secretary-slim.test.mjs`:
```javascript
import { describe, it, expect } from 'vitest';
import { slimProtocol, isLiveBranch, isLiveCandidate } from './secretary-slim.mjs';
const proto = () => ({
subject: 'дело', status: 'открыто', nextSvId: 3, nextKdId: 4,
decisions: [{ text: 'D1', struck: false }],
open: [{ text: 'O1' }], will: [], consequences: [], doneNext: [], history: [],
steps: [{ turn: 1, text: 'шаг 1' }],
hidden: [
{ id: 'СВ-1', lens: 'Л1', status: 'открыт', text: 'живая ветка' },
{ id: 'СВ-2', lens: 'Л4', status: 'закрыт', text: 'мёртвая (закрыт)' },
{ id: 'СВ-3', lens: 'Л6', status: 'отпал', text: 'мёртвая (отпал)' },
{ id: 'СВ-4', lens: 'Л7', status: 'мутировал', text: 'мёртвая (мутировал)' },
{ id: 'СВ-5', lens: 'Л8', text: 'живая без статуса' },
],
candidates: [
{ id: 'КД-1', status: 'жив', branch: 'живой кандидат' },
{ id: 'КД-2', branch: 'живой без статуса' },
{ id: 'КД-3', status: 'отпал', branch: 'мёртвый (отпал)' },
{ id: 'КД-4', status: 'мутировал', branch: 'мёртвый (мутировал)' },
{ id: 'КД-5', status: 'закрыт', branch: 'мёртвый (закрыт)' },
{ id: 'КД-6', status: 'повышен', branch: 'мёртвый (повышен)' },
],
extraNewField: 'forward-safe',
});
describe('slimProtocol', () => {
it('оставляет только живые ветки (выкидывает закрыт/отпал/мутировал)', () => {
const s = slimProtocol(proto());
expect(s.hidden.map((h) => h.id)).toEqual(['СВ-1', 'СВ-5']);
});
it('оставляет только живых кандидатов (нет статуса или жив)', () => {
const s = slimProtocol(proto());
expect(s.candidates.map((c) => c.id)).toEqual(['КД-1', 'КД-2']);
});
it('переносит ствол/шаги/счётчики/новые поля как есть', () => {
const s = slimProtocol(proto());
expect(s.decisions).toEqual([{ text: 'D1', struck: false }]);
expect(s.steps).toEqual([{ turn: 1, text: 'шаг 1' }]);
expect(s.nextSvId).toBe(3);
expect(s.nextKdId).toBe(4);
expect(s.subject).toBe('дело');
expect(s.extraNewField).toBe('forward-safe');
});
it('не мутирует вход (мёртвые остаются в исходном proto)', () => {
const p = proto();
slimProtocol(p);
expect(p.hidden).toHaveLength(5);
expect(p.candidates).toHaveLength(6);
});
it('терпит пустой/кривой вход', () => {
expect(slimProtocol({}).hidden).toEqual([]);
expect(slimProtocol({}).candidates).toEqual([]);
expect(slimProtocol(null)).toEqual(null);
});
it('предикаты живости', () => {
expect(isLiveBranch({ status: 'открыт' })).toBe(true);
expect(isLiveBranch({})).toBe(true);
expect(isLiveBranch({ status: 'закрыт' })).toBe(false);
expect(isLiveCandidate({ status: 'жив' })).toBe(true);
expect(isLiveCandidate({})).toBe(true);
expect(isLiveCandidate({ status: 'повышен' })).toBe(false);
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npx vitest run tools/secretary-slim.test.mjs`
Expected: FAIL — `Cannot find module './secretary-slim.mjs'`.
- [ ] **Step 3: Write minimal implementation**
Create `tools/secretary-slim.mjs`:
```javascript
// tools/secretary-slim.mjs
// Live-only проекция протокола для подачи в LLM: мёртвые ветки/кандидаты («надгробия»)
// в неё не попадают совсем. Чистая, не мутирует вход. Полный протокол (источник истины)
// и архив строятся отдельно — здесь только то, что видит агент.
// Терминальные статусы = пункт мёртв, в подачу не идёт.
const DEAD_BRANCH = new Set(['закрыт', 'отпал', 'мутировал']);
const DEAD_CANDIDATE = new Set(['закрыт', 'отпал', 'мутировал', 'повышен']);
/** Ветка жива, если статус не терминальный (нет статуса/«открыт» — живые). */
export function isLiveBranch(h) { return !DEAD_BRANCH.has(String((h && h.status) || '')); }
/** Кандидат жив, если без статуса или «жив» (то же, что фильтрует candidatesForPrompt). */
export function isLiveCandidate(c) { const s = String((c && c.status) || ''); return s === '' || s === 'жив'; }
/** Копия протокола только из живого. Вход не мутируется. null/не-объект — как есть. */
export function slimProtocol(proto) {
if (!proto || typeof proto !== 'object') return proto;
return {
...proto,
hidden: (proto.hidden || []).filter(isLiveBranch),
candidates: (proto.candidates || []).filter(isLiveCandidate),
};
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `npx vitest run tools/secretary-slim.test.mjs`
Expected: PASS (6 tests).
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-slim.mjs tools/secretary-slim.test.mjs
git commit -m "feat(secretary): slimProtocol — live-only проекция протокола
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 2: кормить 4 пушистые роли из slim (editor — из полного)
**Files:**
- Modify: `tools/secretary-distill.mjs` (ветка `fluffy`, строки ~44-65)
- Test: `tools/secretary-distill.test.mjs`
- [ ] **Step 1: Write the failing test**
Append to `tools/secretary-distill.test.mjs` (внутри файла, новый `describe`):
```javascript
import { slimProtocol } from './secretary-slim.mjs';
describe('distillSpan — подача ролей без надгробий (slim)', () => {
it('catcher и gardener НЕ видят мёртвые ветки/кандидаты, editor видит полный', async () => {
const proto = {
...EMPTY_PROTOCOL(),
hidden: [
{ id: 'СВ-1', lens: 'Л1', status: 'открыт', text: 'ЖИВАЯ_ВЕТКА' },
{ id: 'СВ-2', lens: 'Л4', status: 'закрыт', text: 'МЁРТВАЯ_ВЕТКА' },
],
candidates: [
{ id: 'КД-1', status: 'жив', branch: 'ЖИВОЙ_КАНД' },
{ id: 'КД-2', status: 'отпал', branch: 'МЁРТВЫЙ_КАНД' },
],
};
const seen = { editor: '', catcher: '', gardener: '' };
const callModel = async ({ system, user }) => {
if (/секретарь-редактор/.test(system)) { seen.editor = user; return JSON.stringify({ subject: 'd', decisions: [], alternatives: [], consequences: [], will: [], open: [], doneNext: [], step: { user: 'u', assistant: 'a' } }); }
if (/сборщик скрытых веток/.test(system)) return '{"lenses":[]}';
if (/ловец/.test(system)) { seen.catcher = user; return '{"dropped":[]}'; }
if (/брейншторма/.test(system)) return '{"forks":[]}';
if (/САДОВНИК/.test(system)) { seen.gardener = user; return '{"tend":[]}'; }
return '{}';
};
await distillSpan(proto, { user: 'просьба достаточно длинная', assistant: 'ок', actions: [] },
{ start: 1, end: 1 }, { callModel, session: 's', flags: { fluffy: true } });
// мёртвое не уехало в LLM
expect(seen.catcher).not.toContain('МЁРТВАЯ_ВЕТКА');
expect(seen.catcher).not.toContain('МЁРТВЫЙ_КАНД');
expect(seen.catcher).not.toContain('НАДГРОБИЯ');
expect(seen.gardener).not.toContain('МЁРТВАЯ_ВЕТКА');
expect(seen.gardener).not.toContain('МЁРТВЫЙ_КАНД');
// живое осталось
expect(seen.catcher).toContain('ЖИВАЯ_ВЕТКА');
expect(seen.gardener).toContain('ЖИВОЙ_КАНД');
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npx vitest run tools/secretary-distill.test.mjs -t "без надгробий"`
Expected: FAIL — `seen.catcher` содержит `МЁРТВАЯ_ВЕТКА`/`НАДГРОБИЯ` (сейчас роли получают полный proto).
- [ ] **Step 3: Write minimal implementation**
In `tools/secretary-distill.mjs`, add import near the top (after the `secretary-gardener` import):
```javascript
import { slimProtocol } from './secretary-slim.mjs';
```
Then in the `if (callModel && fluffy)` block, replace the prompt-building calls so the 4 fluffy roles receive a slim view. The current block:
```javascript
const diff = diffTrunk(proto, toWrite); // движение ствола до/после редактора
const spanText = renderExchangeText(spanEx);
const txt = (r) => (typeof r === 'string' ? r : (r && r.text) || '');
const call = (role, system, user) => callModel({ system, user, model: resolveModel(role) });
// сбор: 1.1 диагностика ∥ 1.2 ловец, затем 1.3 брейншторм (после них)
const [r11, r12] = await Promise.all([
call('diagnostic', SYS_11, user11(toWrite, spanText, { start, end })),
call('catcher', SYS_12, user12(toWrite, spanText)),
]);
const d11 = diagFindings(parseLoose(txt(r11), 'lenses'));
const d12 = parseLoose(txt(r12), 'dropped');
const r13 = await call('brainstorm', SYS_13, user13(toWrite, spanText, { start, end }, JSON.stringify(d11.new), JSON.stringify(d12.dropped)));
const d13 = parseLoose(txt(r13), 'forks');
// ЭТАП 1: урожай (новые ветки + рождение/заточка/полнота кандидатов) — садовник увидит свежую грядку
const withHarvest = applyHarvest(toWrite, start, d11, d12, d13);
// ЭТАП 2: садовник на СВЕЖЕМ proto (видит дифф ствола + свежие ветки И кандидаты для моста «повысить»)
const rG = await call('gardener', SYS_G, userG(withHarvest, spanText, diff));
```
becomes (slim-view для подачи, мутации/урожай — на полном toWrite/withHarvest):
```javascript
const diff = diffTrunk(proto, toWrite); // движение ствола до/после редактора
const spanText = renderExchangeText(spanEx);
const txt = (r) => (typeof r === 'string' ? r : (r && r.text) || '');
const call = (role, system, user) => callModel({ system, user, model: resolveModel(role) });
// ПОДАЧА ролям — из live-only проекции (надгробия не возим): мёртвое отсеяно, мутации ниже идут по полному proto.
const view = slimProtocol(toWrite);
// сбор: 1.1 диагностика ∥ 1.2 ловец, затем 1.3 брейншторм (после них)
const [r11, r12] = await Promise.all([
call('diagnostic', SYS_11, user11(view, spanText, { start, end })),
call('catcher', SYS_12, user12(view, spanText)),
]);
const d11 = diagFindings(parseLoose(txt(r11), 'lenses'));
const d12 = parseLoose(txt(r12), 'dropped');
const r13 = await call('brainstorm', SYS_13, user13(view, spanText, { start, end }, JSON.stringify(d11.new), JSON.stringify(d12.dropped)));
const d13 = parseLoose(txt(r13), 'forks');
// ЭТАП 1: урожай (новые ветки + рождение/заточка/полнота кандидатов) — садовник увидит свежую грядку
const withHarvest = applyHarvest(toWrite, start, d11, d12, d13);
// ЭТАП 2: садовник — подача из СВЕЖЕЙ live-проекции (видит свежие живые ветки и кандидаты), без надгробий
const rG = await call('gardener', SYS_G, userG(slimProtocol(withHarvest), spanText, diff));
```
Note: `applyHarvest(toWrite, …)` и `applyGardener(withHarvest, …)` остаются на ПОЛНОМ протоколе — менять их нельзя.
- [ ] **Step 4: Run the new test + existing distill suite**
Run: `npx vitest run tools/secretary-distill.test.mjs`
Expected: PASS (все прежние кейсы + новый «без надгробий»).
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-distill.mjs tools/secretary-distill.test.mjs
git commit -m "feat(secretary): кормить 4 пушистые роли из slim-протокола (без надгробий)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 3: воркер пишет видимый `protocol-slim.json`
**Files:**
- Modify: `tools/secretary-worker.mjs` (deps-деструктуризация + запись после protocol.json)
- Test: `tools/secretary-worker.test.mjs`
- [ ] **Step 1: Write the failing test**
Append to `tools/secretary-worker.test.mjs` (внутри `describe('runWorker', …)`):
```javascript
it('пишет protocol-slim.json только из живого; protocol.json — полный', async () => {
enqueueSpan(workDir, { session: 's', span: { start: 1, end: 1, index: 0, note: '' }, kind: 'span' });
const distill = async (proto) => ({
...proto, subject: 'd',
hidden: [{ id: 'СВ-1', status: 'открыт', text: 'жив' }, { id: 'СВ-2', status: 'закрыт', text: 'мёртв' }],
candidates: [{ id: 'КД-1', status: 'жив', branch: 'жив' }, { id: 'КД-2', status: 'отпал', branch: 'мёртв' }],
});
await runWorker(workDir, deps({ distill }));
const full = JSON.parse(readFileSync(join(workDir, 'protocol.json'), 'utf-8'));
const slim = JSON.parse(readFileSync(join(workDir, 'protocol-slim.json'), 'utf-8'));
expect(full.hidden).toHaveLength(2); // полный хранит мёртвое
expect(full.candidates).toHaveLength(2);
expect(slim.hidden.map((h) => h.id)).toEqual(['СВ-1']); // slim — только живое
expect(slim.candidates.map((c) => c.id)).toEqual(['КД-1']);
});
```
- [ ] **Step 2: Run test to verify it fails**
Run: `npx vitest run tools/secretary-worker.test.mjs -t "protocol-slim"`
Expected: FAIL — `ENOENT … protocol-slim.json` (файл ещё не пишется).
- [ ] **Step 3: Write minimal implementation**
In `tools/secretary-worker.mjs`:
Add import (after the `secretary-protocol` import):
```javascript
import { slimProtocol } from './secretary-slim.mjs';
```
Add `slim` to the `runWorker` deps destructuring (alongside `render`, `archive`, etc.):
```javascript
slim = (p) => slimProtocol(p),
```
After the `protocol.json` write line, add the slim write:
```javascript
writeFileAtomic(join(workDir, 'protocol.json'), JSON.stringify(proto, null, 2));
writeFileAtomic(join(workDir, 'protocol-slim.json'), JSON.stringify(slim(proto), null, 2)); // видимая live-проекция (что видит агент)
```
- [ ] **Step 4: Run the new test + existing worker suite**
Run: `npx vitest run tools/secretary-worker.test.mjs`
Expected: PASS (все прежние кейсы + новый «protocol-slim»).
- [ ] **Step 5: Commit**
```bash
git add tools/secretary-worker.mjs tools/secretary-worker.test.mjs
git commit -m "feat(secretary): воркер пишет видимый protocol-slim.json (live-only)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Task 4: полный прогон секретарских тестов (регресс)
**Files:** нет (проверка)
- [ ] **Step 1: Прогнать все secretary-тесты**
Run: `npx vitest run tools/secretary-*.test.mjs`
Expected: PASS, exit code 0. Ни один прежний кейс не сломан (особенно `secretary-distill`, `secretary-worker`, `secretary-harvest`, `secretary-gardener`).
- [ ] **Step 2: Если что-то красное — чинить точечно**
Разобрать падение по superpowers:systematic-debugging. Типовые риски:
- роль читает поле, которого нет в slim (его быть не должно — slim переносит всё, кроме мёртвых hidden/candidates);
- тест ожидал надгробия в подаче catcher/gardener (такой тест надо обновить — это и есть смысл правки).
- [ ] **Step 3: Финальный коммит-метка (если были правки сверх Task 1-3)**
```bash
git add -A
git commit -m "test(secretary): зелёный свод после slim-протокола
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```
---
## Заметки для исполнителя
- **Коммит/пуш — через терминал владельца.** В этом проекте коммиты делает владелец (или по escape-рецепту). Не пушить без проверки-перед-пушем. Шаги «Commit» выше — содержимое сообщения; согласовать момент с владельцем.
- **Границы:** НЕ трогать большой `protocol.json` (формат/накопление мёртвого), `архив.md` и его рендер, editor/reconcile, `.md`-рендеры, очередь/замок/хуки.
- **Проверка эффекта вручную** (опционально, на живом деле): после активации открыть `docs/secretary/<дело>/protocol-slim.json` — в нём не должно быть пунктов со статусом закрыт/отпал/мутировал/повышен.
@@ -0,0 +1,95 @@
# Дизайн: редактору — последние 7 ходов целиком (вместо 7 действий)
**Дата:** 2026-06-26
**Тема:** секретарь — «обмен» для редактора = последние 7 ходов спана, каждый целиком
**Статус:** дизайн утверждён владельцем (пример + «пока только первому»), готов к плану
---
## 1. Что меняем и почему
Утром ввели «последние 7 **действий**» в обмене (и редактору, и 4 ролям). Владелец
уточнил: для **редактора** нужно иначе — обмен = последние **7 ходов** спана, каждый
**целиком** (юзер + ассистент + ВСЕ его действия), показанные **по-ходово**, а не
склеенные в один блок с урезкой действий.
**Scope: только редактор** (`buildReconcilePrompt`). 4 роли (diagnostic/catcher/
brainstorm/gardener через `renderExchangeText`) пока **не трогаем**у них остаётся
«последние 7 действий».
## 2. Механика (по коду)
Спан = отрезок ходов `[start..end]` сырья ([secretary-span.mjs](../../../tools/secretary-span.mjs)).
`assembleSpan` сейчас СКЛЕИВАЕТ ходы спана в один `{user, assistant, actions}` (actions
со всех ходов). Длинный спан (владелец дал промпт, ассистент работал 12 ходов) = десятки
действий в одном блоке.
Новое:
1. **`assembleSpan`** дополнительно возвращает **`turns`** — список по-ходовых блоков
`[{turn, user, assistant, actions}]` (все ходы спана). Существующие поля
`{user, assistant, actions}` (склейка) **сохраняются** — их используют `buildStepLine`
(строка-шаг) и `renderExchangeText` (4 роли).
2. **`buildReconcilePrompt`** рендерит обмен из `lastExchange.turns`: **последние 7 ходов**,
каждый блоком `[ХОД N] / [ЮЗЕР] / [АССИСТЕНТ] / [ДЕЙСТВИЯ] (все целиком)`. Если ходов >7 —
пометка `(показаны последние 7 из N ходов)`. Если `turns` нет (юнит-тесты со старым
`{user,assistant,actions}`) — **фолбэк** на прежний единый блок (без урезки действий).
3. **Урезка действий у редактора снимается**`MAX_EXCHANGE_ACTIONS` в
[secretary-reconcile.mjs](../../../tools/secretary-reconcile.mjs) больше не нужен (удалить);
вместо него `MAX_EXCHANGE_TURNS = 7`.
`renderExchangeText` и его `MAX_EXCHANGE_ACTIONS` в
[secretary-harvest.mjs](../../../tools/secretary-harvest.mjs) — **без изменений** (4 роли).
## 3. Формат обмена редактору (пример)
```
Последние ходы (показаны последние 7 из 12 ходов):
[ХОД 6]
[ЮЗЕР]: добавь поле «важность»
[АССИСТЕНТ]: смотрю схему...
[ДЕЙСТВИЯ]:
• Read in=schema.sql
→ <содержимое>
• Read in=Card.vue
→ <содержимое>
[ХОД 7]
...
```
Юзер и ассистент каждого хода — целиком; действия каждого хода — **все** (не урезаем).
## 4. Тесты (TDD)
В `tools/secretary-span.test.mjs`:
1. `assembleSpan` возвращает `turns` — по-ходовый список с `{turn,user,assistant,actions}`
для всех ходов спана; склейка `{user,assistant,actions}` сохранена.
В `tools/secretary-reconcile.test.mjs`:
2. `buildReconcilePrompt` с `lastExchange.turns` из 9 ходов рендерит последние 7 ходов,
каждый с меткой `[ХОД N]`, действия НЕ урезаны (все на месте), пометка «7 из 9 ходов».
3. ход за пределами последних 7 (например первый) — отсутствует.
4. фолбэк: `lastExchange` без `turns` (только `{user,assistant,actions}`) — обмен
по-прежнему содержит юзера/ассистента/действия (единый блок).
5. **заменить** прежний тест «Последний обмен — только последние 7 действий» (редактор
больше так не делает) на проверку по-ходового рендера.
## 5. Что НЕ делаем
- Не трогаем 4 роли (`renderExchangeText` / `MAX_EXCHANGE_ACTIONS` в harvest) — у них
остаётся «последние 7 действий».
- Не трогаем `assembleSpan`-склейку (нужна `buildStepLine`/ролям) — только ДОБАВЛЯЕМ `turns`.
- Не меняем нарезку спанов / очередь / воркер.
## 6. Файлы
| Файл | Изменение |
|---|---|
| `tools/secretary-span.mjs` | `assembleSpan` +`turns` |
| `tools/secretary-reconcile.mjs` | обмен из `turns` (7 ходов целиком); убрать `MAX_EXCHANGE_ACTIONS`, ввести `MAX_EXCHANGE_TURNS` |
| `tools/secretary-span.test.mjs`, `tools/secretary-reconcile.test.mjs` | кейсы 1-5 |
## 7. Открытые вопросы
Нет. Scope (только редактор) и единица (ход, последние 7) закрыты владельцем.
@@ -0,0 +1,146 @@
# Дизайн: «база знаний» в протоколе секретаря
**Дата:** 2026-06-26
**Тема:** секретарь — копить факты, добытые при изучении, со ссылкой на источник
**Статус:** дизайн утверждён владельцем (куски 1-3), готов к плану
---
## 1. Идея
Между **темой** и **решениями** протокол ведёт **базу знаний** — факты, добытые при
изучении (чтение файлов / спек / url с содержимым), каждый со **ссылкой на источник
в конце**. И на выходе (редактор пишет), и на входе (редактор видит уже добытое и не
дублирует). Накопление монотонное: **добавляем; дубль по источнику — уточняем/расширяем;
ничего не удаляем и не сокращаем.**
Решения владельца (брейншторм 26.06.2026):
1. **Кто пишет:** редактор (`reconcile`) — он уже видит действия хода (чтения с
содержимым), без лишних LLM-вызовов.
2. **Накопление:** копить; дубли уточнять/расширять; не уменьшать и не удалять.
3. **Ключ записи:** `ref` (источник) — на один источник одна запись.
## 2. Данные
Новое поле протокола **`knowledge: [{ ref, text }]`**:
- **`text`** — само знание («функция Y в файле X делает Z»; «спека требует A»);
- **`ref`** — источник: `файл:строка` / `url` / раздел спеки. **Обязателен.**
Ключ записи — **`ref`**. Один источник → одна запись. Несколько фактов из одного
источника редактор сводит/расширяет в одной записи (владелец: «уточнять/расширять»).
Позиция — **между `subject` и `decisions`**`EMPTY_PROTOCOL`, в парсере, во всех
показах). `knowledge` НЕ входит в `SECTIONS` reconcile (его не схлопывают и не сторожат
как ствол) — у него своя защита (§5).
`EMPTY_PROTOCOL` ([secretary-protocol.mjs](../../../tools/secretary-protocol.mjs)) получает
`knowledge: []` — поле первоклассное, всегда инициализировано.
## 3. Кто и как наполняет — редактор
В промпт редактора ([buildReconcilePrompt](../../../tools/secretary-reconcile.mjs)):
**На вход** — раздел «База знаний» (между темой и решениями) с текущими записями
`<text> — <ref>`, чтобы редактор видел добытое.
**Новое правило в инструкции** (добавляется к пп.1-9):
- из действий хода (чтения файлов/спек/url с содержимым) вытаскивай ФАКТЫ — что
конкретно узнал;
- каждая запись: **знание, потом источник** (`ref`); **источник обязателен** — нет
источника → это не знание (вывод/решение идёт в `decisions`, не сюда);
- если запись про этот источник **уже есть** — уточни/расширь её (тот же `ref`,
обновлённый `text`), не дублируй;
- **ничего из базы не удаляй и не сокращай.**
**На выход** — редактор возвращает `knowledge: [{ref, text}]` вместе со стволом.
## 4. Парсер
[parseReconcileResponse](../../../tools/secretary-reconcile.mjs) учится читать
`knowledge`: `list(parsed.knowledge)``{ ref: String(ref).trim(), text: String(text).trim() }`,
**отсев записей с пустым `ref`** (нет источника → не знание). Возвращается в объекте
парсера рядом с `subject`/`decisions`/…/`step`.
## 5. Сохранность (no-delete) — `preserveKnowledge`
Редактор — LLM, может случайно уронить/смержить записи. Гарантия «не удаляем»
держится детерминированно:
Новый helper **`preserveKnowledge(oldKnowledge, newKnowledge)`** в
[secretary-reconcile.mjs](../../../tools/secretary-reconcile.mjs) — возвращает слитый
массив:
- ключ — `ref`;
- `ref`, что был в старом, но пропал в новом — **восстанавливается** (старая запись);
- `ref` в обоих — берётся версия из нового (это и есть «уточнение/расширение» —
новый `text` проходит);
- новые `ref` — добавляются (порядок: старые сохранённые, затем новые).
**Точка вызова — внутри `reconcileTurn`**, сразу после `const returned =
collapseProtocol(parsed)`:
`returned.knowledge = preserveKnowledge(clean.knowledge, returned.knowledge)`.
`returned` — свежий объект, обе ветки сторожа (`guard.ok` и `restoreLostLines`) уносят
его `knowledge` дальше через spread.
**`mergeTurnIntoProtocol` НЕ меняем** — он делает `{ ...base, steps }`, где `base =
updated`, поэтому `knowledge` переносится сам. **На сбое редактора** (`reconcileTurn`
вернул `null`) `base = proto` (старый) — старая база знаний остаётся целой.
**`preserveRegistry`** в distill `knowledge` не трогает (только hidden/candidates).
`collapseProtocol` `knowledge` не трогает (не в `SECTIONS`) — записи переживают
схлопывание как есть.
## 6. slim и показы
- **slim** ([secretary-slim.mjs](../../../tools/secretary-slim.mjs)): `knowledge`
остаётся **целиком** — это ствол, ценное, мёртвого в нём нет; `slimProtocol`
пропускает его через spread без фильтра. (Подтвердить тестом, что slim не теряет
knowledge.)
- **protocol.md** ([render-fluffy](../../../tools/secretary-render-fluffy.mjs), активный
рендер воркера): раздел **«## База знаний»** между шапкой/темой и «Решения»,
строки `- <text> — <ref>`.
- **архив.md** ([render-archive](../../../tools/secretary-render-archive.mjs)): тот же
раздел «База знаний» в полном леджере (между стволом-шапкой и ветками).
## 7. Что НЕ делаем (границы, YAGNI)
- База **не подаётся 4 ролям** (diagnostic/catcher/brainstorm/gardener) — им не нужна,
лишний вес ни к чему. Только редактор.
- У знаний **нет `struck`/удаления** — только добавление и уточнение.
- **Нет окна/обрезки** базы — копим полностью (выбор владельца). (Если когда-нибудь
разрастётся — окно на вход добавим отдельной задачей; сейчас YAGNI.)
- Не трогаем `slimProtocol`-фильтры (knowledge просто проходит), очередь/хуки/воркер-IO.
## 8. Тестирование (TDD)
В `tools/secretary-reconcile.test.mjs`:
1. парсер читает `knowledge: [{ref, text}]`; запись без `ref` отсеивается.
2. промпт редактора содержит раздел «База знаний» с текущими записями + правило
(источник в конце, не удалять).
3. `preserveKnowledge`: `ref`, выпавший из ответа, восстанавливается; общий `ref` берёт
новый `text` (уточнение); новый `ref` добавляется.
4. `reconcileTurn`: после ответа с урезанной базой выпавшие записи восстановлены.
В `tools/secretary-protocol.test.mjs` (или где тестируется EMPTY_PROTOCOL):
5. `EMPTY_PROTOCOL().knowledge` === `[]`.
В `tools/secretary-slim.test.mjs`:
6. `slimProtocol` сохраняет `knowledge` целиком.
В `tools/secretary-render-fluffy.test.mjs` и `tools/secretary-render-archive.test.mjs`:
7. рендеры печатают раздел «База знаний» между темой и решениями со строками
`<text> — <ref>`.
## 9. Файлы
| Файл | Изменение |
|---|---|
| `tools/secretary-protocol.mjs` | `EMPTY_PROTOCOL` +`knowledge: []` |
| `tools/secretary-reconcile.mjs` | промпт (вход+правило), парсер, `preserveKnowledge`, вызов в `reconcileTurn` |
| `tools/secretary-render-fluffy.mjs` | раздел «База знаний» в protocol.md |
| `tools/secretary-render-archive.mjs` | раздел «База знаний» в архив.md |
| `tools/secretary-slim.mjs` | без изменений кода; тест на сохранность knowledge |
| соответствующие `*.test.mjs` | кейсы 1-7 |
## 10. Открытые вопросы
Нет. Producer, накопление и ключ закрыты владельцем (§1).
@@ -0,0 +1,124 @@
# Дизайн: переделка подачи всех 5 мозгов секретаря
**Дата:** 2026-06-26
**Тема:** секретарь — единый богатый контекст ролям: полный ствол + база + ветки/кандидаты + N ходов целиком + шаги−N
**Статус:** дизайн утверждён владельцем («делай так»), готов к плану
---
## 1. Цель
Владелец задал точную диету каждого мозга. Перестраиваем подачу: вместо нынешней
разнородной (у каждого свой обрезок) — единый набор «кирпичей», скомбинированный по
ролям. Сознательно УВЕЛИЧИВАЕТ объём подачи (приоритет — качество понимания).
## 2. Кирпичи (детерминированные рендеры)
- **ТЕМА** = `proto.subject`.
- **БАЗА** = база знаний: строки `<text> — <ref>` (поле `knowledge`).
- **СТВОЛ** = все 6 разделов (Решения · Альтернативы · Последствия · Воля · Открытые ·
Сделано), зачёркнутые показаны как `[зачёркнуто] …`.
- **ВЕТКИ** = живые скрытые ветки (`registry`, slim — мёртвых нет).
- **КАНДИДАТЫ** = живые кандидаты (`candidatesForPrompt`) + список тем (`THEMES_BLOCK`).
- **N ХОДОВ** = последние N ходов спана целиком: на каждый ход блок
`[ХОД K]\n[ЮЗЕР]: …\n[АССИСТЕНТ]: …\n[ДЕЙСТВИЯ]:\n• tool in=… → результат`. Если ходов
спана > N — пометка `(показаны последние N из M ходов)`. Источник — `spanEx.turns`
(добавлен в `assembleSpan`).
- **ШАГИ−N** = краткий лог ВСЕХ ходов (`fullSteps`) МИНУС последние N записей (они уже
показаны целиком — не дублируем).
- **ДИФФ** = `diffTrunk(до, после редактора)`, расширенный диффом базы знаний.
## 3. Диета по ролям
| # | Мозг | Подача |
|---|---|---|
| 1 | **editor** | ТЕМА · БАЗА · СТВОЛ · **7 ХОДОВ** · **ШАГИ−7** |
| 2 | **diagnostic** | ТЕМА · БАЗА · СТВОЛ · ВЕТКИ · КАНДИДАТЫ · **4 ХОДА** · **ШАГИ−4** |
| 3 | **catcher** | то же, что #2 |
| 4 | **brainstorm** | то же, что #2 · + хвост «уже найдено этот ход» (см. §5) |
| 5 | **gardener** | ДИФФ · ВЕТКИ · КАНДИДАТЫ · **7 ХОДОВ** · **ШАГИ−7** |
Числа: editor/gardener — **7**, diagnostic/catcher/brainstorm — **4**.
## 4. Изменения по файлам
### `tools/secretary-span.mjs`
Без изменений — `turns` уже добавлен (предыдущая задача).
### `tools/secretary-harvest.mjs` — основной объём
Новые/изменённые детерминированные хелперы:
- `renderTurns(turns, n)` — последние n ходов целиком (формат блока выше, с пометкой).
- `stepsMinusLast(proto, n)``fullSteps` минус последние n записей (по `turn`).
- `fullTrunk(proto)` — рендер всех 6 разделов ствола со зачёркнутыми.
- `knowledgeBlock(proto)` — строки `<text> — <ref>` (или «(пусто)»).
Переписать USER-сборщики (берут `spanEx` вместо готового `spanText`, число ходов
внутри):
- `user11(proto, spanEx, span)` (diagnostic) = ТЕМА + БАЗА + СТВОЛ + `registry` + `candidatesForPrompt`+`THEMES_BLOCK` + `renderTurns(spanEx.turns, 4)` + `stepsMinusLast(proto, 4)`.
- `user12(proto, spanEx)` (catcher) = то же содержимое, что user11 (по таблице — идентично).
- `user13(proto, spanEx, span, found11, found12)` (brainstorm) = то же, что user11, + хвост
«=== УЖЕ НАЙДЕНО ЭТОТ ХОД ===» (found11/found12) — см. §5.
- `userG(proto, spanEx, diff)` (gardener) = ДИФФ + `registry` + `candidatesForPrompt`+`THEMES_BLOCK` + `renderTurns(spanEx.turns, 7)` + `stepsMinusLast(proto, 7)`.
Константы числа ходов: `TURNS_ROLE = 4`, `TURNS_GARDENER = 7` (явные имена).
`renderExchangeText` (старый «последние 7 действий») и `MAX_EXCHANGE_ACTIONS`
**удаляются** (роли больше не используют действия-обрезку; всё через `renderTurns`).
### `tools/secretary-distill.mjs`
В fluffy-ветке: передавать в сборщики `spanEx` вместо `renderExchangeText(spanEx)`.
Убрать `const spanText = renderExchangeText(spanEx)`. Вызовы:
- `user11(view, spanEx, {start,end})`, `user12(view, spanEx)`,
`user13(view, spanEx, {start,end}, JSON.stringify(d11.new), JSON.stringify(d12.dropped))`,
`userG(slimProtocol(withHarvest), spanEx, diff)`.
### `tools/secretary-reconcile.mjs` (editor)
Добавить **ШАГИ−7** в user-промпт (после блока «Последние ходы»): краткий лог всех ходов
минус последние 7. Нужен доступ к `proto.steps` — он уже есть в `protocol`. Добавить
локальный рендер «шаги минус N» (или импортировать `stepsMinusLast` из harvest —
предпочесть импорт, чтобы не дублировать). editor уже имеет ТЕМА·БАЗА·СТВОЛ·7 ХОДОВ.
### `tools/secretary-trunkdiff.mjs` (для gardener)
Расширить `diffTrunk` диффом базы знаний: новый `ref` → «ЗНАНИЕ добавлено: «text — ref»»;
тот же `ref` с изменённым `text` → «ЗНАНИЕ уточнено: «text — ref»». («в полном объёме» =
весь дифф ствола + знания, без обрезки.)
## 5. Нюанс: «уже найдено этот ход» у brainstorm
Таблица для #4 = «то же, что #2». Но brainstorm исторически получает хвост
«уже найдено этот ход» (находки diagnostic/catcher), чтобы НЕ плодить дубль. Это
координация прогона (brainstorm идёт после них), а не «контекст дела». **Решение:**
оставить этот хвост у brainstorm (иначе регресс — дубли). В остальном контекст #4 = #2.
Зафиксировано как осознанное отклонение от буквального списка.
## 6. Что НЕ делаем
- Не трогаем нарезку спанов, очередь, воркер-IO, slim-фильтры.
- Не меняем СИСТЕМНЫЕ промпты ролей (SYS_11/12/13/SYS_G) — их работа прежняя; меняется
только USER-контекст (данные).
- Склейку `{user,assistant,actions}` в `assembleSpan` оставляем (нужна `buildStepLine`).
## 7. Тесты (TDD)
`tools/secretary-harvest.test.mjs`:
1. `renderTurns(turns, 4)` — последние 4 хода, каждый `[ХОД K]` целиком; >4 → пометка.
2. `stepsMinusLast(proto, 4)` — без последних 4 шагов; ≤4 → пусто.
3. `fullTrunk` — все 6 разделов, зачёркнутые помечены.
4. `knowledgeBlock` — строки `text — ref`; пусто → «(пусто)».
5. `user11`/`user12` содержат ТЕМА+БАЗА+СТВОЛ+ВЕТКИ+КАНДИДАТЫ+4 хода+шаги−4; НЕ содержат
5-й-с-конца ход целиком.
6. `user13` дополнительно содержит «УЖЕ НАЙДЕНО ЭТОТ ХОД».
7. `userG` содержит ДИФФ+ВЕТКИ+КАНДИДАТЫ+7 ходов+шаги−7.
`tools/secretary-trunkdiff.test.mjs`:
8. `diffTrunk` показывает «ЗНАНИЕ добавлено/уточнено».
`tools/secretary-reconcile.test.mjs`:
9. editor-промпт содержит шаги−7 (краткий лог всех минус последние 7).
`tools/secretary-distill.test.mjs`:
10. существующие fluffy-кейсы зелёные (садовник видит свежего кандидата и т.п.) после
смены сигнатур (spanEx вместо spanText).
## 8. Открытые вопросы
Нет. Диета закрыта владельцем (§3, 4 пункта подтверждены). Нюанс brainstorm — §5.
@@ -0,0 +1,159 @@
# Дизайн: маленький протокол для агента (slim working JSON)
**Дата:** 2026-06-26
**Тема:** секретарь — убрать груз «надгробий» из подачи в LLM
**Статус:** дизайн утверждён владельцем (по кускам 1-3), готов к плану
---
## 1. Проблема
Секретарь на каждый завершённый кусок разговора («спан») делает 5 вызовов LLM
(editor + diagnostic + catcher + brainstorm + gardener — см.
[secretary-distill.mjs](../../../tools/secretary-distill.mjs)). Вход каждого вызова
собирается из текущего `protocol.json`.
`protocol.json` копит **все** ветки и кандидаты навсегда, в том числе **мёртвые**
(статусы `закрыт` / `отпал` / `мутировал`). Закрытие пункта ставит только
`struck=true` + статус — из массива пункт **не удаляется**
([collapseProtocol](../../../tools/secretary-reconcile.mjs): «hidden/… — как есть»).
Мёртвые пункты («надгробия») **каждый ход снова грузятся в LLM** — в роли
**catcher** и **gardener** ([tombstones](../../../tools/secretary-harvest.mjs)),
чтобы роли не «воскрешали» уже отброшенное.
**Замер на живом деле** `выпонение-задач-секретаря` (`protocol.json` ≈ 194 000 знаков JSON):
| Поле | Всего | Живых | Мёртвых | Вес мёртвых |
|---|---|---|---|---|
| candidates | 154 | 58 | 96 | ~95 500 знаков |
| hidden | 58 | 40 | 18 | ~16 800 знаков |
Итог: **114 надгробий ≈ 112 КБ** возятся в catcher+gardener **каждый ход**. Чем
дольше живёт дело — тем дороже и медленнее каждый ход. Это и есть наблюдаемый
владельцем рост цены/времени.
## 2. Решения владельца (закреплено в брейншторме 2026-06-26)
1. **Воскрешение:** агент мёртвое **вообще не видит**. Самый лёгкий протокол.
Случайное повторение убитой идеи ловит садовник/владелец; полная запись — в
`архив.md` и большом `protocol.json`.
2. **Хранение:** большой `protocol.json` **остаётся как есть** (полная запись,
кормит `архив.md`). **Рядом** появляется маленький производный JSON только из
живого; его читает агент.
## 3. Что добавляем
### 3.1 `tools/secretary-slim.mjs` — чистая функция `slimProtocol(proto)`
Возвращает **копию** протокола **только из живого**, не мутируя вход:
- `hidden` → только живые: терминальные статусы **`закрыт` / `отпал` / `мутировал`**
выкидываются (остаются `открыт` / без статуса);
- `candidates` → только живые: остаются без статуса или `жив`; выкидываются
`закрыт` / `отпал` / `мутировал` / `повышен` (то же, что уже фильтрует
`candidatesForPrompt`);
- ствол (`decisions`/`open`/`will`/`consequences`/`doneNext`/`history`), `steps`,
`subject`, `status`, `nextSvId`, `nextKdId` — переносятся как есть (они малы и
нужны ролям/нумерации);
- всё прочее поле — копируется как есть (forward-safe: новые поля не теряются).
**Почему предикат задаём явно.** В существующем коде определения «мёртвого»
слегка разъезжаются: `tombstones` считает мёртвой веткой только `закрыт`/`отпал`
(пропускает `мутировал`), а `registry` рендерит **все** ветки без фильтра. Slim
задаёт **единый строгий** предикат «терминальный статус = мёртв» и пинит его
тестом, чтобы в подачу не протекала ни одна завершённая ветка/кандидат. Предикат
живёт в `secretary-slim.mjs` в одном месте.
### 3.2 Подача 4 «пушистым» ролям — из slim
В [secretary-distill.mjs](../../../tools/secretary-distill.mjs), ветка `fluffy`:
прежде чем строить промпты для **diagnostic (1.1)**, **catcher (1.2)**,
**brainstorm (1.3)**, **gardener (2)** — собрать `view = slimProtocol(proto-на-этот-момент)`
и передавать `view` в `user11/user12/user13/userG` вместо полного протокола.
Следствие (проверено): на slim-протоколе (мёртвых нет в принципе) подача
очищается **сама собой**, без правок в [secretary-harvest.mjs](../../../tools/secretary-harvest.mjs):
- `tombstones(view)` → пустая строка (нет мёртвых);
- `registry(view)` → только живые ветки (сейчас registry рендерит **все**, включая
мёртвые — slim дополнительно срезает их, на нашем деле это ещё ~18 веток);
- `candidatesForPrompt(view)` → как и раньше только живые кандидаты.
**Тонкость по этапам fluffy:** в распиле есть две точки протокола — `toWrite`
(после editor) и `withHarvest` (после урожая, для садовника). Slim берётся от
актуального протокола на каждом шаге (`slimProtocol(toWrite)` для 1.1-1.3,
`slimProtocol(withHarvest)` для gardener), чтобы садовник видел свежие живые ветки
и кандидаты.
### 3.3 editor (reconcile) — остаётся на полном протоколе
Роль editor ведёт ствол и опирается на сторожа потерянных строк
([restoreLostLines](../../../tools/secretary-reconcile.mjs)) — ей нужны и
зачёркнутые строки ствола. Ствол мал (~6 КБ), цена не здесь. **editor slim НЕ
получает.**
### 3.4 `protocol-slim.json` на диске (видимость)
Воркер ([secretary-worker.mjs](../../../tools/secretary-worker.mjs)) после записи
`protocol.json` и `архив.md` пишет рядом `protocol-slim.json = slimProtocol(proto)`
— ровно то, что видит агент. Назначение: владелец/разработчик открывает файл и
видит глазами, что реально уходит в LLM (прямой инструмент против раздувания).
Файл **производный** (можно удалить — пересоберётся), источник истины — большой
`protocol.json`.
## 4. Сохранность данных (почему ничего не теряется)
Агент только **читает** slim и возвращает решения (id веток/кандидатов, действия,
тексты). Применяют эти решения **детерминированные** `applyHarvest` / `applyGardener`
к **полному** `toWrite`/`withHarvest`. Поэтому:
- мёртвое корректно **копится** в большом `protocol.json`;
- `архив.md` рисуется из полного протокола — **не меняется**;
- slim — read-only проекция на момент построения промпта, никуда не пишется как
источник истины (кроме видимого `protocol-slim.json`, тоже производного).
## 5. Что НЕ делаем (границы, YAGNI)
- Не трогаем большой `protocol.json` (формат, накопление мёртвого) — он SoT.
- Не трогаем `архив.md` и его рендер.
- Не трогаем editor/reconcile.
- Не трогаем `.md`-рендеры (`protocol.md`), очередь, замок, хуки, курсор.
- Не вводим окна/обрезку живых веток — живое нужно ролям целиком.
- Не вводим «список не воскрешать» — владелец выбрал «совсем не слать мёртвое».
## 6. Тестирование (TDD)
Новый юнит-набор `tools/secretary-slim.test.mjs`:
1. `slimProtocol` выкидывает мёртвые `hidden` (закрыт/отпал) и мёртвые `candidates`
(отпал/мутировал/закрыт), сохраняет живые.
2. `slimProtocol` не мутирует вход (вход остаётся с мёртвыми).
3. ствол/шаги/`nextSvId`/`nextKdId`/новые поля — переносятся как есть.
В `tools/secretary-distill.test.mjs` (или новый кейс):
4. при `fluffy` подача 4 ролей **не содержит** блок «НАДГРОБИЯ» (catcher/gardener),
при наличии мёртвых в исходном proto.
5. editor по-прежнему получает полный proto (со struck-стволом).
6. решения агента (close/отпал/повысить) применяются к **полному** proto —
мёртвое накапливается, живое корректно переходит в мёртвое.
В `tools/secretary-worker.test.mjs`:
7. после прогона воркер пишет `protocol-slim.json`, и он равен `slimProtocol(proto)`
(без мёртвых), а `protocol.json` — полный.
## 7. Файлы
| Файл | Изменение |
|---|---|
| `tools/secretary-slim.mjs` | **новый**`slimProtocol(proto)` |
| `tools/secretary-slim.test.mjs` | **новый** — юниты slim |
| `tools/secretary-distill.mjs` | подача 4 ролей из slim (editor — нет) |
| `tools/secretary-worker.mjs` | запись `protocol-slim.json` |
| `tools/secretary-distill.test.mjs` | кейсы 4-6 |
| `tools/secretary-worker.test.mjs` | кейс 7 |
## 8. Открытые вопросы
Нет. Развилки воскрешения и хранения закрыты владельцем (§2).