From 21be382a682bbb0f5f4bfaacc460080547c8f085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 26 Jun 2026 17:39:59 +0300 Subject: [PATCH] =?UTF-8?q?docs(secretary):=20=D1=81=D0=BF=D0=B5=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=BF=D0=BB=D0=B0=D0=BD=D1=8B=20=D0=B4=D0=BD?= =?UTF-8?q?=D1=8F=20=E2=80=94=20slim,=20=D0=B1=D0=B0=D0=B7=D0=B0=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D0=BD=D0=B8=D0=B9,=207=20=D1=85=D0=BE=D0=B4=D0=BE?= =?UTF-8?q?=D0=B2,=20=D0=B1=D0=BE=D0=B3=D0=B0=D1=82=D1=8B=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2026-06-26-secretary-editor-last7-turns.md | 240 +++++++++ .../2026-06-26-secretary-knowledge-base.md | 504 ++++++++++++++++++ ...-06-26-secretary-roles-feeding-redesign.md | 426 +++++++++++++++ .../2026-06-26-secretary-slim-protocol.md | 386 ++++++++++++++ ...-26-secretary-editor-last7-turns-design.md | 95 ++++ ...6-06-26-secretary-knowledge-base-design.md | 146 +++++ ...secretary-roles-feeding-redesign-design.md | 124 +++++ ...26-06-26-secretary-slim-protocol-design.md | 159 ++++++ 8 files changed, 2080 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-26-secretary-editor-last7-turns.md create mode 100644 docs/superpowers/plans/2026-06-26-secretary-knowledge-base.md create mode 100644 docs/superpowers/plans/2026-06-26-secretary-roles-feeding-redesign.md create mode 100644 docs/superpowers/plans/2026-06-26-secretary-slim-protocol.md create mode 100644 docs/superpowers/specs/2026-06-26-secretary-editor-last7-turns-design.md create mode 100644 docs/superpowers/specs/2026-06-26-secretary-knowledge-base-design.md create mode 100644 docs/superpowers/specs/2026-06-26-secretary-roles-feeding-redesign-design.md create mode 100644 docs/superpowers/specs/2026-06-26-secretary-slim-protocol-design.md diff --git a/docs/superpowers/plans/2026-06-26-secretary-editor-last7-turns.md b/docs/superpowers/plans/2026-06-26-secretary-editor-last7-turns.md new file mode 100644 index 0000000..4a4aaa5 --- /dev/null +++ b/docs/superpowers/plans/2026-06-26-secretary-editor-last7-turns.md @@ -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) " +``` + +--- + +## 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) " +``` + +--- + +## 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`. diff --git a/docs/superpowers/plans/2026-06-26-secretary-knowledge-base.md b/docs/superpowers/plans/2026-06-26-secretary-knowledge-base.md new file mode 100644 index 0000000..c80d8a7 --- /dev/null +++ b/docs/superpowers/plans/2026-06-26-secretary-knowledge-base.md @@ -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) " +``` + +--- + +## 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) " +``` + +--- + +## 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) " +``` + +--- + +## 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) " +``` + +--- + +## 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` (та же тема секретаря) или новой по решению владельца. diff --git a/docs/superpowers/plans/2026-06-26-secretary-roles-feeding-redesign.md b/docs/superpowers/plans/2026-06-26-secretary-roles-feeding-redesign.md new file mode 100644 index 0000000..5a0361a --- /dev/null +++ b/docs/superpowers/plans/2026-06-26-secretary-roles-feeding-redesign.md @@ -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) " +``` + +--- + +## 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) " +``` + +--- + +## 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) " +``` + +--- + +## 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) " +``` + +--- + +## 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`. diff --git a/docs/superpowers/plans/2026-06-26-secretary-slim-protocol.md b/docs/superpowers/plans/2026-06-26-secretary-slim-protocol.md new file mode 100644 index 0000000..ef283f6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-26-secretary-slim-protocol.md @@ -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) " +``` + +--- + +## 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) " +``` + +--- + +## 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) " +``` + +--- + +## 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) " +``` + +--- + +## Заметки для исполнителя + +- **Коммит/пуш — через терминал владельца.** В этом проекте коммиты делает владелец (или по escape-рецепту). Не пушить без проверки-перед-пушем. Шаги «Commit» выше — содержимое сообщения; согласовать момент с владельцем. +- **Границы:** НЕ трогать большой `protocol.json` (формат/накопление мёртвого), `архив.md` и его рендер, editor/reconcile, `.md`-рендеры, очередь/замок/хуки. +- **Проверка эффекта вручную** (опционально, на живом деле): после активации открыть `docs/secretary/<дело>/protocol-slim.json` — в нём не должно быть пунктов со статусом закрыт/отпал/мутировал/повышен. diff --git a/docs/superpowers/specs/2026-06-26-secretary-editor-last7-turns-design.md b/docs/superpowers/specs/2026-06-26-secretary-editor-last7-turns-design.md new file mode 100644 index 0000000..8a95f54 --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-secretary-editor-last7-turns-design.md @@ -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) закрыты владельцем. diff --git a/docs/superpowers/specs/2026-06-26-secretary-knowledge-base-design.md b/docs/superpowers/specs/2026-06-26-secretary-knowledge-base-design.md new file mode 100644 index 0000000..cc9ea69 --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-secretary-knowledge-base-design.md @@ -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)): + +**На вход** — раздел «База знаний» (между темой и решениями) с текущими записями +``, чтобы редактор видел добытое. + +**Новое правило в инструкции** (добавляется к пп.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), активный + рендер воркера): раздел **«## База знаний»** между шапкой/темой и «Решения», + строки `- `. +- **архив.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. рендеры печатают раздел «База знаний» между темой и решениями со строками + ``. + +## 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). diff --git a/docs/superpowers/specs/2026-06-26-secretary-roles-feeding-redesign-design.md b/docs/superpowers/specs/2026-06-26-secretary-roles-feeding-redesign-design.md new file mode 100644 index 0000000..7d87755 --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-secretary-roles-feeding-redesign-design.md @@ -0,0 +1,124 @@ +# Дизайн: переделка подачи всех 5 мозгов секретаря + +**Дата:** 2026-06-26 +**Тема:** секретарь — единый богатый контекст ролям: полный ствол + база + ветки/кандидаты + N ходов целиком + шаги−N +**Статус:** дизайн утверждён владельцем («делай так»), готов к плану + +--- + +## 1. Цель + +Владелец задал точную диету каждого мозга. Перестраиваем подачу: вместо нынешней +разнородной (у каждого свой обрезок) — единый набор «кирпичей», скомбинированный по +ролям. Сознательно УВЕЛИЧИВАЕТ объём подачи (приоритет — качество понимания). + +## 2. Кирпичи (детерминированные рендеры) + +- **ТЕМА** = `proto.subject`. +- **БАЗА** = база знаний: строки `` (поле `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)` — строки `` (или «(пусто)»). + +Переписать 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. diff --git a/docs/superpowers/specs/2026-06-26-secretary-slim-protocol-design.md b/docs/superpowers/specs/2026-06-26-secretary-slim-protocol-design.md new file mode 100644 index 0000000..a298d5a --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-secretary-slim-protocol-design.md @@ -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).