diff --git a/cspell-words.txt b/cspell-words.txt index 97fbcab4..2d0fcab7 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1191,3 +1191,16 @@ poincaré наслой нормативке рефреш + +# ruflo memory H7 fix + advisory hook design (2026-05-15) — Russian IT-slang inflections +персистит +персистящий +персистят +инжектит +инжектить +инжектящий +флашит +незакоммиченная +бинаря +промпты +свежеустановленный diff --git a/docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md b/docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md new file mode 100644 index 00000000..7024d091 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md @@ -0,0 +1,381 @@ +# Ruflo Memory H7 Fix + Advisory Hook — Design + +**Дата:** 2026-05-15 +**Статус:** Design (согласован в `superpowers:brainstorming`) +**Связанные документы:** `docs/superpowers/specs/2026-05-15-ruflo-integration-design.md` +(ruflo big-bang integration), memory `project_ruflo_integration.md`. + +--- + +## 1. Цель и контекст + +ruflo runtime активирован (daemon под PM2, hive-mind Queen + 9 agents, memory + +локальные embeddings `Xenova/all-MiniLM-L6-v2` 384-dim), но архитектурно остаётся +**parallel subsystem** — не интегрирован в интерактивную Claude Code-сессию: Claude +работает напрямую, ruflo daemon/hive-mind крутятся отдельным процессом рядом. + +Цель — три деливерабла, приближающие ruflo к роли advisory-слоя над сессией: + +- **D1.** Установить standalone `claude` CLI, чтобы `ruflo hive-mind spawn --claude` + мог запускать реальную агентскую работу on-demand. +- **D2.** Починить баг **H7** — `ruflo memory` не персистит данные между + process-invocations. +- **D3.** Создать `UserPromptSubmit` advisory-хук, инжектящий релевантную + ruflo-память в каждый промпт интерактивной сессии. + +### 1.1. Решения пользователя (приняты в brainstorming) + +- Provider-ключ Anthropic в ruflo **не настраиваем** — непрерывный daemon-расход не + нужен. Реальная работа агентов — через `spawn --claude` on-demand, который + использует auth самого `claude` CLI (существующий Claude-план), а не отдельный + ruflo-ключ и не отдельный $-поток. +- H7 чиним подходом **«патч `getBridge()`»** (см. §4) — выбран из 4 вариантов как + единственный, верифицированный по исходнику. +- Порядок: D1 → D2 → D3. + +### 1.2. Не-цели + +- Настройка provider-ключа Anthropic в ruflo. +- Исправление самого AgentDB-v3 bridge или пакета `@claude-flow/memory` — H7 чиним + **обходом** bridge'а, не его починкой. +- Установка native `better-sqlite3` (рассмотрена как альтернатива §4.4, отклонена). +- Авто-store хук (PostToolUse / Stop), наполняющий память автоматически — память + наполняется вручную через `ruflo memory store` по уместности (YAGNI). +- Routing-классификация в хуке (`ruflo route` / Q-Learning) — Q-таблица на свежей + установке не обучена, сигнал пустой; хук инжектит только memory-recall. +- Косметический баг namespace `undefined` (`ctx.flags.namespace` приходит + `undefined` несмотря на `default: 'default'` в `commands/memory.js:38`) — отдельный + дефект, вне scope этого дизайна. +- Превращение ruflo в буквальный entry-point/control-flow над Claude — архитектурно + невозможно (`UserPromptSubmit`-хук умеет только инжектить контекст или блокировать + промпт, не передавать управление). Хук даёт «advisory», не «routing». + +--- + +## 2. H7 — корневая причина (верифицировано по исходнику) + +Симптом: `ruflo memory store -k K -v V` рапортует `[OK] Data stored successfully` +(384-dim вектор, ID), но `.swarm/memory.db` не меняется (mtime/size константны), а +`memory list`/`retrieve`/`search`/`stats` в последующих вызовах возвращают пусто +(`Total Entries: 0`). + +Цепочка (`@claude-flow/cli` в глобальной установке ruflo, `dist/src/`): + +1. `ruflo memory store` → `storeEntry()` — `memory/memory-initializer.js:1777`. +2. `storeEntry` зовёт `getBridge()` — `memory-initializer.js:1779`. `getBridge` + (`memory-initializer.js:71-84`) делает `await import('./memory-bridge.js')` — + это **локальный sibling-модуль, импорт всегда успешен** → `getBridge()` + практически всегда возвращает truthy bridge. +3. → `bridge.bridgeStoreEntry(options)` — `memory-initializer.js:1781`. +4. `bridgeStoreEntry` (`memory/memory-bridge.js:534`) поднимает AgentDB-v3 + `ControllerRegistry` через `import('@claude-flow/memory')` + (`memory-bridge.js:86`); на этой машине загрузка успешна → `bridgeAvailable = true` + (`memory-bridge.js:350`). +5. Вставка строки: `ctx.db.prepare(insertSql).run(...)` — + `memory-bridge.js:593-594`. Код написан под **`better-sqlite3`** (синхронный + `.run()`, комментарий `memory-bridge.js:570`). Native-бинарь `better-sqlite3` на + этой машине не собран (нет build tools) → AgentDB падает на **sql.js (WASM, + in-RAM)** — отсюда строка «✅ Using sql.js» в каждом выводе `memory`-команд. +6. `bridgeStoreEntry` возвращает `{ success: true, id, ... }` + (`memory-bridge.js:602-610`) — **truthy, но нигде не экспортирует in-RAM + WASM-буфер на диск**: ни `db.export()`, ни `writeFileRestricted` в функции нет. + Контраст — raw-fallback `storeEntry` явно делает `const data = db.export(); + writeFileRestricted(...)` на `memory-initializer.js:1858-1859`. +7. Раз `bridgeStoreEntry` вернул truthy — `storeEntry` делает ранний `return + bridgeResult` на `memory-initializer.js:1793`. **Корректный персистящий + raw-sql.js путь (`memory-initializer.js:1796-1875`) недостижим — он «затенён» + bridge'ом.** +8. Процесс CLI завершается → WASM-память освобождается → строка пропадает. + Следующий `ruflo memory ` — новый процесс, новый пустой in-RAM реестр. + +`memory init` работает (создаёт `.swarm/memory.db` 144 KB) потому что +`initializeMemoryDatabase` пишет файл напрямую (`fs.writeFileSync` / +`writeFileRestricted` + `copyFileSync` в `.claude/memory.db`), не через bridge. + +**Итог H7:** AgentDB-v3 bridge перехватывает каждую memory-операцию +(`store`/`get`/`list`/`search`/`delete` — все имеют паттерн `getBridge()`-first), +пишет в in-RAM sql.js и никогда не флашит на диск; рабочий персистящий raw-sql.js +путь затенён. + +### 2.1. Гипотезы (systematic-debugging) — статус + +| Гип. | Формулировка | Статус | +|---|---|---| +| A | `store` пишет в другой файл (stray `.db` / `ruvector.db`) | **Фальсифицирована** — `find` нашёл только `.swarm/memory.db` + `ruvector.db`; `stat` обоих через T0/T1/T2 вокруг `store` — не изменились ни разу | +| B | sql.js не экспортится на диск | **Уточнена и подтверждена** — no-export реален, но в `bridgeStoreEntry`, не в raw-пути (raw-путь экспортит корректно) | +| C | Нужен running memory-сервер | **Фальсифицирована** — `store` делает реальную in-process работу (embedding, ID) | +| D | namespace `undefined` ломает запись | **Фальсифицирована как корень** — отдельный косметический баг; namespace-слепой `list` тоже пуст, т.к. на диск ничего не пишется | +| E | Незакоммиченная транзакция | **Схлопывается в B** — у sql.js «commit» = `db.export()` + запись файла | + +--- + +## 3. D1 — `claude` CLI + spawn-capability + +### 3.1. Проблема + +`ruflo doctor` сообщает «⚠ Claude Code CLI: Not installed». На машине Claude Code +работает через VSCode-расширение; отдельного бинаря `claude` на PATH нет (`claude +--version` → command not found). `ruflo hive-mind spawn --claude` запускает `claude` +Code CLI как подпроцесс (`--claude Launch Claude Code with hive-mind coordination +prompt`) → без бинаря путь «реальной работы агентов» недоступен. + +### 3.2. Решение + +1. `npm i -g @anthropic-ai/claude-code` — установить standalone `claude` CLI. +2. Верификация: `claude --version` возвращает версию; `ruflo doctor` больше не + выдаёт «Claude Code CLI: Not installed». +3. Smoke `spawn`: `ruflo hive-mind spawn --claude --no-auto-permissions` на + тривиальной задаче. Флаг `--no-auto-permissions` обязателен — по умолчанию + `spawn --claude` идёт с `--dangerously-skip-permissions` (default true), + spawned-агент пропускает все permission-промпты (автономно правит репозиторий) — + для CRM-проекта это недопустимо без явного решения. + +### 3.3. Открытые вопросы D1 + +- **Auth `claude` CLI** — не верифицировано, подхватит ли свежеустановленный + standalone CLI существующие credentials из `~/.claude/` (общие с VSCode-расширением) + или потребует интерактивный `claude login`. Проверяется в ходе D1; если нужен + `login` — это user-action (интерактив, не автоматизируется). +- Реальный `spawn --claude` потребляет ёмкость существующего Claude-плана + пользователя — smoke делается на минимальной задаче. + +--- + +## 4. D2 — Фикс H7: патч `getBridge()` + +### 4.1. Суть + +Сделать `getBridge()` в `memory-initializer.js` всегда возвращающим `null`. Тогда +`storeEntry`/`getEntry`/`searchEntries`/`listEntries`/`deleteEntry` пропускают +bridge-ветку и выполняют свои **raw-sql.js fallback-пути**, которые корректно +персистят (`db.export()` + `writeFileRestricted` — например +`memory-initializer.js:1858-1859` для `store`, `:2196-2197` для `get`, +`:2325-2326` для `delete`). + +### 4.2. Патч + +Файл: `/ruflo/node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js`. + +Текущая функция (`:71-84`): + +```js +let _bridge; +async function getBridge() { + if (_bridge === null) + return null; + if (_bridge) + return _bridge; + try { + _bridge = await import('./memory-bridge.js'); + return _bridge; + } + catch { + _bridge = null; + return null; + } +} +``` + +Патч — вставка одной строки первой в тело функции: + +```js +async function getBridge() { + return null; /* LIDERRA-H7-PATCH: bridge writes to in-RAM sql.js, never flushes — force raw persisting path */ + if (_bridge === null) +``` + +Маркер `LIDERRA-H7-PATCH` служит признаком идемпотентности для re-apply-скрипта. +Остальное тело функции остаётся как мёртвый, но безвредный код (не переписываем — +минимизируем diff и риск рассинхрона с upstream при будущих сверках). + +### 4.3. Re-apply-механизм + +Патч лежит в `node_modules` **глобальной** установки ruflo — он не под git проекта +и **молча затирается** при `ruflo update` / `npm i -g ruflo`. Поэтому: + +- Скрипт `tools/ruflo-h7-patch.mjs` (в репо проекта, под git): + - Резолвит путь к `memory-initializer.js`: `npm root -g` → кандидаты + `/ruflo/node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js` + и `/@claude-flow/cli/.../memory-initializer.js` (на случай hoist). + - Читает файл; если содержит маркер `LIDERRA-H7-PATCH` — no-op (идемпотентно). + - Если маркера нет — находит якорь `async function getBridge() {` и вставляет + строку патча сразу за ним; пишет файл обратно. + - Если якорь **не найден и маркера нет** — выходит с ненулевым кодом и сообщением + (upstream изменил функцию — патч пересмотреть вручную, не молчать). + - Флаг `--revert` — снимает патч (удаляет строку с маркером). + - Флаг `--check` — только проверка статуса (exit 0 patched / exit 1 unpatched), + для CI/диагностики. +- Запуск — **вручную** после каждого `ruflo update`. Документируется в memory + `project_ruflo_integration.md` (operational note) и, опционально, в + `Tooling_v8_3.md` §4.10. +- `patch-package` не используется — он рассчитан на проектный `./node_modules`, а не + на глобальную установку. + +### 4.4. Отклонённые альтернативы + +- **better-sqlite3 native** — bridge написан под better-sqlite3 (синхронная запись + на диск); собранный native-бинарь убрал бы H7 без правки исходника. Отклонено: + native-build на Windows + Node 24 невыполнимости не гарантирован (параллель с + сагой sharp/libvips при активации embeddings). +- **MCP-сервер (session-scoped)** — `memory_*` через долгоживущий MCP-процесс. + Отклонено: персист только на время жизни сервера, cross-session нет. +- **Обойти ruflo memory вовсе** — отклонено: пользователь выбрал чинить H7. + +### 4.5. Граница scope D2 + +Патч `getBridge()` в `memory-initializer.js` покрывает **CLI-путь** memory-операций +(`ruflo memory store/get/list/search/delete`), который и использует хук D3. +**Не верифицировано**, разделяют ли MCP-инструменты `memory_*` +(`dist/src/mcp-tools/memory-tools.js`) тот же модуль `memory-initializer.js` или +имеют отдельный bridge-путь. Если в будущем понадобятся MCP memory-инструменты — +проверить и при необходимости расширить патч; в текущем дизайне MCP memory-путь вне +scope. + +--- + +## 5. D3 — Advisory-хук + +### 5.1. Назначение + +`UserPromptSubmit`-хук, который на каждый промпт интерактивной сессии достаёт из +ruflo-памяти релевантные записи (semantic search) и инжектит их в контекст как +`additionalContext`. Это сдвигает ruflo из «parallel» в «integrated advisory» — +Claude видит припомненный контекст, но решение остаётся за Claude (не routing). + +### 5.2. Компоненты + +- **`tools/ruflo-recall-hook.mjs`** (в репо, под git) — Node-скрипт хука. +- Регистрация в проектном **`.claude/settings.json`** → `hooks.UserPromptSubmit` + (не глобально — хук специфичен для этого проекта/ruflo-установки). + +### 5.3. Data flow + +1. Пользователь отправляет промпт → Claude Code запускает `UserPromptSubmit`-хуки. +2. `ruflo-recall-hook.mjs` получает на stdin JSON с полем `prompt`. +3. Хук спавнит `ruflo memory search -q "" --format json -l 3` с таймаутом. +4. После фикса D2 `search` читает `.swarm/memory.db` (персистящий raw-путь) и + возвращает топ-хиты JSON-ом. +5. Хук форматирует хиты в короткий текстовый блок. +6. Хук пишет в stdout: + `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"<блок>"}}`, + exit 0. +7. Claude видит припомненный контекст в этом turn. + +### 5.4. Обработка ошибок — fail-open (жёсткое требование) + +Хук **никогда не ломает сессию**: + +- Таймаут вызова `ruflo` (~3000 мс) — kill подпроцесса, инжект пустой. +- `ruflo` вернул ошибку / невалидный JSON / 0 результатов — инжект пустой. +- Любое исключение в скрипте — поймать, exit 0, инжект пустой. +- Хук **не использует** `decision: "block"` — промпт всегда проходит. + +«Инжект пустой» = либо `additionalContext: ""`, либо вывод без `hookSpecificOutput` +(хук просто завершается exit 0 без JSON). + +### 5.5. Стоимость и ограничения D3 + +- **Латентность:** per-prompt вызов `ruflo` CLI = Node + sql.js WASM init → + ~1-3 с задержки на КАЖДЫЙ промпт. Таймаут ограничивает худший случай. +- **Coexistence:** в глобальном `~/.claude/settings.json` уже есть + `UserPromptSubmit`-хук (economy-mode). Хуки независимы — оба срабатывают, оба + инжектят свой `additionalContext`. Конфликта нет. +- **Ценность растёт с накоплением данных:** на свежей памяти recall пуст; полезным + хук становится по мере наполнения `.swarm/memory.db`. + +### 5.6. Write-сторона + +Память наполняет Claude вручную — вызовом `ruflo memory store -k --value +` (после D2 он персистит), когда в сессии есть что запомнить. Авто-store хук — +не строим (YAGNI; при потребности — отдельная итерация). + +--- + +## 6. Последовательность и зависимости + +``` +D1 (claude CLI) ──independent──┐ +D2 (H7 patch) ───────────────┤ + ▼ +D3 (advisory hook) — зависит от D2 (хук бесполезен без персистящей памяти) +``` + +Порядок исполнения: **D1 → D2 → D3**. D1 первым — это «шаг 1» по постановке +пользователя; D2 строго до D3. + +--- + +## 7. Верификация + +### D1 + +- `claude --version` → версия выведена. +- `ruflo doctor` → нет строки «Claude Code CLI: Not installed». +- `ruflo hive-mind spawn --claude --no-auto-permissions` на тривиальной задаче → + spawned-инстанс отработал (smoke). + +### D2 + +- `tools/ruflo-h7-patch.mjs` — round-trip: + - запуск на чистой установке → патч наложен, маркер присутствует; + - повторный запуск → no-op (идемпотентность); + - `--check` → exit 0; + - `--revert` → маркер удалён, `--check` → exit 1. +- Эффект патча — **cross-process round-trip**: `ruflo memory store -k h7-verify -v + "..."` → (новый процесс) `ruflo memory retrieve -k h7-verify` → запись найдена; + `ruflo memory stats` → `Total Entries ≥ 1`; mtime `.swarm/memory.db` изменилась. +- `ruflo memory delete -k h7-verify` — cleanup verify-записи. + +### D3 + +- Submit промпта → в контексте turn появляется блок recall (когда в памяти есть + релевантные записи). +- **Fail-open тест:** временно сломать `ruflo` (переименовать бинарь / недостижимый + PATH) → submit промпта → промпт проходит, сессия не падает, инжект пустой. +- Таймаут-тест: убедиться, что зависший `ruflo` убивается по таймауту и хук + завершается exit 0. + +### Регрессия + +- ruflo daemon worker-jitter усиливает Pest quirk 72 (memory `feedback_environment.md` + квирк #93) — при прогоне baseline-регрессии `pm2 stop ruflo-daemon`. D1/D2/D3 не + меняют код приложения (`app/`), schema или тесты — отдельный Pest/Vitest-прогон не + требуется; затрагиваются только ruflo-инфраструктура, `.claude/settings.json`, + `tools/`. + +--- + +## 8. Риски и ограничения + +- **Фрагильность патча D2** — патч в глобальном `node_modules` теряется при `ruflo + update`. Митигировано re-apply-скриптом + документированием; остаётся ручной шаг. +- **`claude` CLI auth (D1)** — не верифицировано, нужен ли `claude login` + (см. §3.3). +- **Латентность хука (D3)** — ~1-3 с на каждый промпт (см. §5.5). +- **ruflo alpha** — raw-sql.js путь верифицирован чтением исходника (персистит), но + его runtime-корректность подтверждается только тестом round-trip в D2; возможны + иные alpha-баги в raw-пути, не выявленные чтением. +- **MCP memory-путь** — не верифицирован (см. §4.5). +- **Намеренно не чиним** косметический баг namespace `undefined` (§1.2). + +--- + +## 9. Откат + +- **D1:** `npm uninstall -g @anthropic-ai/claude-code`. +- **D2:** `node tools/ruflo-h7-patch.mjs --revert`, либо `npm i -g ruflo` (чистая + переустановка перетирает патч). +- **D3:** удалить `UserPromptSubmit`-блок из `.claude/settings.json`; удалить + `tools/ruflo-recall-hook.mjs`. + +--- + +## 10. Артефакты + +| Артефакт | Путь | Под git | +|---|---|---| +| Re-apply-скрипт патча H7 | `tools/ruflo-h7-patch.mjs` | да | +| Скрипт advisory-хука | `tools/ruflo-recall-hook.mjs` | да | +| Регистрация хука | `.claude/settings.json` (`hooks.UserPromptSubmit`) | да | +| Патч H7 | `/.../memory-initializer.js` | нет (node_modules) | +| Operational-нота | memory `project_ruflo_integration.md` | нет (личная память) |