# Ruflo Memory H7 Fix + Advisory Hook — 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:** Установить standalone `claude` CLI, починить баг персистентности ruflo
memory (H7) и добавить `UserPromptSubmit` advisory-хук, инжектящий ruflo-память в
каждый промпт.

**Architecture:** Три деливерабла, исполняются строго по порядку D1 → D2 → D3. D1 —
операционная установка пакета. D2 — два Node-ESM-скрипта (`tools/`): идемпотентный
re-apply-патч для глобального `@claude-flow/cli` + его юнит-тесты. D3 — Node-ESM
скрипт хука с чистым ядром + юнит-тесты + регистрация в `.claude/settings.json`.
Скрипты `tools/*.mjs` тестируются встроенным раннером `node --test` (Node 24),
независимо от Vitest приложения.

**Tech Stack:** Node.js 24 (ESM, `node:test`, `node:assert`), ruflo CLI
v3.7.0-alpha.38, Claude Code hooks (`UserPromptSubmit`), Windows Server 2022 +
PowerShell 5.1 / Git Bash.

**Спека:** `docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md`
(commit `a6649e4`).

**Ветка:** `main` (согласно норме проекта — спека закоммичена в `main` коммитом
`a6649e4`; деливераблы — мелкие infra-файлы). Push в `origin/main` — финальный шаг
Task 8 (разрешён settings rule `Bash(git push origin main:*)`).

---

## File Structure

| Файл | Ответственность | Действие |
|---|---|---|
| `tools/ruflo-h7-patch.mjs` | Re-apply-скрипт H7-патча: чистые функции `isPatched`/`applyPatch`/`revertPatch`/`resolveTargetPath` + CLI `main()` | Create (Task 4) |
| `tools/ruflo-h7-patch.test.mjs` | Юнит-тесты чистых функций патч-скрипта (`node --test`) | Create (Task 4) |
| `tools/ruflo-recall-hook.mjs` | `UserPromptSubmit`-хук: чистые функции `parsePrompt`/`extractJson`/`formatRecall`/`buildHookOutput`/`rufloBinCandidates` + `main()` | Create (Task 6) |
| `tools/ruflo-recall-hook.test.mjs` | Юнит-тесты чистых функций хука (`node --test`) | Create (Task 6) |
| `.claude/settings.json` | +блок `hooks.UserPromptSubmit` | Modify (Task 7) |
| `<npm-root-g>/ruflo/node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js` | H7-патч (вне git, накладывается скриптом) | Patch (Task 5) |

`<npm-root-g>` = `C:\Users\Administrator\AppData\Roaming\npm\node_modules` (verified
`npm root -g`).

---

## D1 — Standalone `claude` CLI

### Task 1: Установить standalone `claude` CLI

**Files:** нет (глобальная npm-установка, не репозиторий).

- [ ] **Step 1: Установить пакет**

```bash
npm i -g @anthropic-ai/claude-code
```

Expected: установка завершается без ошибок (выводит установленную версию).

- [ ] **Step 2: Проверить, что бинарь на PATH**

```bash
claude --version
```

Expected: выводится номер версии (например `2.x.x`). Если `command not found` —
перезапустить shell (PATH глобальных npm-bin обновляется при новом shell) и повторить.

**Commit:** нет — Task 1 не меняет файлы репозитория.

---

### Task 2: Проверить `claude` CLI и статус auth

**Files:** нет.

- [ ] **Step 1: Проверить, что ruflo doctor видит CLI**

```bash
ruflo doctor 2>&1 | grep -i "claude code cli"
```

Expected: строка вида `✓ Claude Code CLI: ...` (НЕ `⚠ ... Not installed`).
Если всё ещё `Not installed` — `claude` не на PATH; вернуться к Task 1 Step 2.

- [ ] **Step 2: Проверить статус авторизации CLI**

```bash
claude --version && echo "---" && claude -p "Reply with exactly: AUTHOK" 2>&1 | head -20
```

Expected — одно из двух:

- Вывод содержит `AUTHOK` → CLI авторизован (подхватил credentials из `~/.claude/`),
  D1 можно завершать.
- Вывод требует логина (`Please run claude login` / приглашение войти) → **остановиться
  и сообщить пользователю**: нужен интерактивный `claude login` (это user-action,
  не автоматизируется). Без авторизации `hive-mind spawn --claude` (Task 3) работать
  не будет.

**Commit:** нет.

---

### Task 3: Smoke-проверка `hive-mind spawn --claude`

**Files:** нет.

- [ ] **Step 1: Уточнить флаг отключения auto-permissions**

```bash
ruflo hive-mind spawn --help 2>&1
```

Expected: в выводе есть флаг `--claude` и флаг по умолчанию
`--dangerously-skip-permissions [default: true]`. Найти точное имя флага, который
ОТКЛЮЧАЕТ авто-пропуск permission-промптов (ожидается `--no-auto-permissions`;
использовать ровно то имя, что в `--help`). Записать его как `<NO_AUTOPERM_FLAG>`.

- [ ] **Step 2: Smoke-spawn на тривиальной задаче**

```bash
ruflo hive-mind spawn --claude <NO_AUTOPERM_FLAG> -o "Reply with the single word READY and make no file changes."
```

Expected: spawned-инстанс `claude` Code запускается, отрабатывает и завершается без
ошибки запуска подпроцесса. (Эта операция потребляет ёмкость существующего
Claude-плана пользователя — задача намеренно минимальная.)
Если падает с «claude: not found» / ошибкой запуска подпроцесса — вернуться к Task 1.
Если падает на auth — вернуться к Task 2 Step 2.

**Commit:** нет — D1 завершён, репозиторий не изменён.

---

## D2 — Фикс H7: патч `getBridge()`

### Task 4: Создать `tools/ruflo-h7-patch.mjs` (TDD)

**Files:**

- Create: `tools/ruflo-h7-patch.mjs`
- Create: `tools/ruflo-h7-patch.test.mjs`

- [ ] **Step 1: Написать падающий тест**

Создать `tools/ruflo-h7-patch.test.mjs`:

```js
import test from 'node:test';
import assert from 'node:assert/strict';
import {
  isPatched, applyPatch, revertPatch, resolveTargetPath, PATCH_MARKER,
} from './ruflo-h7-patch.mjs';

// Фикстура — фактический getBridge() из memory-initializer.js:70-84.
const FIXTURE = `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;
    }
}
`;

test('isPatched: false on clean content', () => {
  assert.equal(isPatched(FIXTURE), false);
});

test('applyPatch: inserts patch line right after the anchor', () => {
  const { content, changed } = applyPatch(FIXTURE);
  assert.equal(changed, true);
  assert.ok(content.includes(PATCH_MARKER));
  const lines = content.split('\n');
  const anchorIdx = lines.findIndex((l) => l.includes('async function getBridge()'));
  assert.ok(lines[anchorIdx + 1].includes(PATCH_MARKER));
});

test('applyPatch: idempotent — second apply is a no-op', () => {
  const once = applyPatch(FIXTURE).content;
  const twice = applyPatch(once);
  assert.equal(twice.changed, false);
  assert.equal(twice.content, once);
});

test('isPatched: true after applyPatch', () => {
  assert.equal(isPatched(applyPatch(FIXTURE).content), true);
});

test('applyPatch: throws when the anchor is absent', () => {
  assert.throws(() => applyPatch('function other() {}\n'), /anchor .* not found/);
});

test('revertPatch: removes the patch line and restores the original', () => {
  const patched = applyPatch(FIXTURE).content;
  const { content, changed } = revertPatch(patched);
  assert.equal(changed, true);
  assert.equal(isPatched(content), false);
  assert.equal(content, FIXTURE);
});

test('revertPatch: no-op on unpatched content', () => {
  assert.equal(revertPatch(FIXTURE).changed, false);
});

test('resolveTargetPath: null when no candidate file exists', () => {
  assert.equal(resolveTargetPath('/nonexistent/npm/root/zzz'), null);
});
```

- [ ] **Step 2: Запустить тест — убедиться, что падает**

```bash
node --test tools/ruflo-h7-patch.test.mjs
```

Expected: FAIL — `Cannot find module ... ruflo-h7-patch.mjs` (скрипт ещё не создан).

- [ ] **Step 3: Реализовать `tools/ruflo-h7-patch.mjs`**

Создать `tools/ruflo-h7-patch.mjs`:

```js
#!/usr/bin/env node
/**
 * ruflo H7 patch re-apply tool.
 *
 * H7: `ruflo memory store` routes through the AgentDB-v3 bridge
 * (memory-bridge.js:bridgeStoreEntry) which inserts into an in-RAM sql.js DB
 * and never flushes to disk — entries are lost between CLI invocations.
 *
 * Fix: force getBridge() in @claude-flow/cli's memory-initializer.js to
 * return null, so memory ops fall through to the raw sql.js path that DOES
 * persist (db.export() + writeFileRestricted).
 *
 * The patch lives in the GLOBAL npm install and is overwritten by
 * `ruflo update` — re-run this script after any ruflo upgrade.
 *
 * Usage:
 *   node tools/ruflo-h7-patch.mjs            apply the patch (idempotent)
 *   node tools/ruflo-h7-patch.mjs --check    exit 0 if patched, 1 if not
 *   node tools/ruflo-h7-patch.mjs --revert   remove the patch
 *
 * See docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md §4
 */
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

export const PATCH_MARKER = 'LIDERRA-H7-PATCH';
export const PATCH_LINE =
  '    return null; /* LIDERRA-H7-PATCH: AgentDB bridge writes in-RAM sql.js, never flushes — force raw persisting path */';
const ANCHOR = 'async function getBridge() {';

/** True if the file content already contains the patch. */
export function isPatched(content) {
  return content.includes(PATCH_MARKER);
}

/**
 * Insert the patch line as the first statement of getBridge().
 * Idempotent. Throws if the anchor is absent (ruflo upstream changed).
 */
export function applyPatch(content) {
  if (isPatched(content)) return { content, changed: false };
  const idx = content.indexOf(ANCHOR);
  if (idx === -1) {
    throw new Error(
      `anchor "${ANCHOR}" not found — ruflo upstream changed getBridge(); revise this patch manually`,
    );
  }
  const insertAt = idx + ANCHOR.length;
  const patched = content.slice(0, insertAt) + '\n' + PATCH_LINE + content.slice(insertAt);
  return { content: patched, changed: true };
}

/** Remove the patch line (the line carrying PATCH_MARKER). */
export function revertPatch(content) {
  if (!isPatched(content)) return { content, changed: false };
  const lines = content.split('\n');
  const kept = lines.filter((l) => !l.includes(PATCH_MARKER));
  return { content: kept.join('\n'), changed: true };
}

/**
 * Resolve the path to @claude-flow/cli's memory-initializer.js inside the
 * global npm tree. Handles both nested (under ruflo/) and hoisted layouts.
 */
export function resolveTargetPath(npmRoot) {
  const tail = ['@claude-flow', 'cli', 'dist', 'src', 'memory', 'memory-initializer.js'];
  const candidates = [
    join(npmRoot, 'ruflo', 'node_modules', ...tail),
    join(npmRoot, ...tail),
  ];
  return candidates.find((c) => existsSync(c)) ?? null;
}

function main() {
  const mode = process.argv[2];
  let npmRoot;
  try {
    npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
  } catch (e) {
    console.error('[ruflo-h7-patch] failed to run `npm root -g`: ' + e.message);
    process.exit(2);
  }
  const target = resolveTargetPath(npmRoot);
  if (!target) {
    console.error('[ruflo-h7-patch] memory-initializer.js not found under ' + npmRoot);
    process.exit(2);
  }
  const content = readFileSync(target, 'utf8');

  if (mode === '--check') {
    const patched = isPatched(content);
    console.log(`[ruflo-h7-patch] ${target}\n  ${patched ? 'PATCHED' : 'UNPATCHED'}`);
    process.exit(patched ? 0 : 1);
  }
  if (mode === '--revert') {
    const { content: out, changed } = revertPatch(content);
    if (changed) writeFileSync(target, out);
    console.log(`[ruflo-h7-patch] revert: ${changed ? 'patch removed' : 'was not patched'}`);
    process.exit(0);
  }
  try {
    const { content: out, changed } = applyPatch(content);
    if (changed) writeFileSync(target, out);
    console.log(`[ruflo-h7-patch] apply: ${changed ? 'patched' : 'already patched'}`);
    process.exit(0);
  } catch (e) {
    console.error('[ruflo-h7-patch] ' + e.message);
    process.exit(3);
  }
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  main();
}
```

- [ ] **Step 4: Запустить тест — убедиться, что проходит**

```bash
node --test tools/ruflo-h7-patch.test.mjs
```

Expected: PASS — 8 тестов, `# pass 8`, `# fail 0`.

- [ ] **Step 5: Коммит**

```bash
git add tools/ruflo-h7-patch.mjs tools/ruflo-h7-patch.test.mjs
git commit -m "feat(ruflo): H7 patch re-apply script (getBridge -> null)

Idempotent script that forces @claude-flow/cli getBridge() to return null
so ruflo memory ops use the persisting raw sql.js path. Pure-function core
unit-tested via node --test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

Expected: pre-commit зелёный (gitleaks/eslint-vue skip — `.mjs` в `tools/` не под
ESLint-конфигом приложения; gitleaks проходит).

---

### Task 5: Наложить H7-патч и проверить cross-process персистентность

**Files:** патчит (вне git) `<npm-root-g>/ruflo/node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js`.

- [ ] **Step 1: Проверить, что патч ещё не наложен**

```bash
node tools/ruflo-h7-patch.mjs --check
```

Expected: вывод `UNPATCHED`, exit code 1.

- [ ] **Step 2: Наложить патч**

```bash
node tools/ruflo-h7-patch.mjs
```

Expected: `[ruflo-h7-patch] apply: patched`.
Если вывод `anchor ... not found` (exit 3) — **остановиться**: ruflo upstream
изменил `getBridge()`; патч пересмотреть вручную против актуального исходника.

- [ ] **Step 3: Подтвердить статус патча**

```bash
node tools/ruflo-h7-patch.mjs --check
```

Expected: `PATCHED`, exit code 0.

- [ ] **Step 4: Перезапустить daemon, чтобы он загрузил пропатченный модуль**

```bash
pm2 restart ruflo-daemon && pm2 save
```

Expected: `ruflo-daemon` перезапущен, статус `online`. (Уже запущенный daemon держал
старый модуль в памяти; перезапуск подхватывает патч.)

- [ ] **Step 5: Cross-process round-trip — доказать персистентность**

```bash
echo "=== mtime before ==="; stat -c '%Y' .swarm/memory.db
ruflo memory store -k h7-verify --value "ROUNDTRIP-OK liderra 2026-05-15"
echo "=== mtime after store ==="; stat -c '%Y' .swarm/memory.db
ruflo memory retrieve -k h7-verify
ruflo memory stats 2>&1 | grep -E "Total Entries"
```

Expected:

- mtime `.swarm/memory.db` ПОСЛЕ store **больше**, чем ДО (файл записан);
- `memory retrieve -k h7-verify` → запись найдена, value содержит `ROUNDTRIP-OK`;
- `memory stats` → `Total Entries` ≥ 1.

Если retrieve не находит запись или mtime не изменилась — патч не возымел эффекта;
**остановиться** и диагностировать через `superpowers:systematic-debugging` (проверить:
действительно ли пропатчен файл; не кэширует ли что-то модуль; не лежит ли реальная
БД по иному пути из `getMemoryRoot()`).

- [ ] **Step 6: Очистить verify-запись**

```bash
ruflo memory delete -k h7-verify
ruflo memory retrieve -k h7-verify 2>&1 | grep -i "not found"
```

Expected: `delete` сообщает об удалении; повторный `retrieve` → `Key not found`.

**Commit:** нет — патч в `node_modules` (вне git); скрипт уже закоммичен в Task 4.

---

## D3 — Advisory-хук

### Task 6: Создать `tools/ruflo-recall-hook.mjs` (TDD)

**Files:**

- Create: `tools/ruflo-recall-hook.mjs`
- Create: `tools/ruflo-recall-hook.test.mjs`

- [ ] **Step 1: Написать падающий тест**

Создать `tools/ruflo-recall-hook.test.mjs`:

```js
import test from 'node:test';
import assert from 'node:assert/strict';
import {
  parsePrompt, extractJson, formatRecall, buildHookOutput, rufloBinCandidates,
} from './ruflo-recall-hook.mjs';

test('parsePrompt: extracts the prompt field', () => {
  assert.equal(parsePrompt('{"prompt":"hello world"}'), 'hello world');
});
test('parsePrompt: empty string when field is missing', () => {
  assert.equal(parsePrompt('{"other":1}'), '');
});
test('parsePrompt: empty string on invalid JSON (fail-open)', () => {
  assert.equal(parsePrompt('not json at all'), '');
});
test('parsePrompt: empty string when prompt is not a string', () => {
  assert.equal(parsePrompt('{"prompt":123}'), '');
});

test('extractJson: pulls the JSON object out of noisy ruflo stdout', () => {
  const stdout =
    '[INFO] Searching: "x" (semantic)\n\n' +
    '✅ Using sql.js\n' +
    '{\n  "query": "x",\n  "results": []\n}\n';
  const obj = extractJson(stdout);
  assert.equal(obj.query, 'x');
  assert.deepEqual(obj.results, []);
});
test('extractJson: null when there is no JSON', () => {
  assert.equal(extractJson('[INFO] only noise, no braces'), null);
});
test('extractJson: null on malformed JSON (fail-open)', () => {
  assert.equal(extractJson('noise {not: valid'), null);
});

test('formatRecall: empty string when there are no results', () => {
  assert.equal(formatRecall({ results: [] }), '');
  assert.equal(formatRecall(null), '');
  assert.equal(formatRecall({}), '');
});
test('formatRecall: formats at most 3 results and tags the block', () => {
  const out = formatRecall({
    results: [
      { key: 'k1', preview: 'p1' },
      { key: 'k2', preview: 'p2' },
      { key: 'k3', preview: 'p3' },
      { key: 'k4', preview: 'p4' },
    ],
  });
  assert.ok(out.includes('k1') && out.includes('k3'));
  assert.ok(!out.includes('k4'));
  assert.ok(out.includes('ruflo memory recall'));
});

test('buildHookOutput: correct UserPromptSubmit payload shape', () => {
  const o = buildHookOutput('some context');
  assert.equal(o.hookSpecificOutput.hookEventName, 'UserPromptSubmit');
  assert.equal(o.hookSpecificOutput.additionalContext, 'some context');
});

test('rufloBinCandidates: RUFLO_RECALL_BIN override wins', () => {
  assert.deepEqual(rufloBinCandidates({ RUFLO_RECALL_BIN: 'X:/r.js' }), ['X:/r.js']);
});
test('rufloBinCandidates: builds an APPDATA-based candidate', () => {
  const c = rufloBinCandidates({ APPDATA: 'C:\\\\Users\\\\X\\\\AppData\\\\Roaming' });
  assert.equal(c.length, 1);
  assert.ok(c[0].includes('ruflo') && c[0].endsWith('ruflo.js'));
});
test('rufloBinCandidates: empty array when no env hints', () => {
  assert.deepEqual(rufloBinCandidates({}), []);
});
```

- [ ] **Step 2: Запустить тест — убедиться, что падает**

```bash
node --test tools/ruflo-recall-hook.test.mjs
```

Expected: FAIL — `Cannot find module ... ruflo-recall-hook.mjs`.

- [ ] **Step 3: Реализовать `tools/ruflo-recall-hook.mjs`**

Создать `tools/ruflo-recall-hook.mjs`:

```js
#!/usr/bin/env node
/**
 * UserPromptSubmit hook — ruflo memory advisory recall.
 *
 * On each prompt: runs `ruflo memory search` and injects the top matches as
 * additionalContext so Claude sees relevant recalled memory.
 *
 * FAIL-OPEN: any error / timeout / no results -> empty injection, exit 0.
 * The hook NEVER blocks a prompt.
 *
 * Requires the H7 patch (tools/ruflo-h7-patch.mjs) — otherwise ruflo memory
 * does not persist and search always returns nothing.
 *
 * See docs/superpowers/specs/2026-05-15-ruflo-memory-h7-fix-and-advisory-hook-design.md §5
 */
import { execFile } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

const SEARCH_TIMEOUT_MS = 3500;
const MAX_RESULTS = 3;
const MAX_QUERY_CHARS = 400;

/** Extract the `prompt` string from the UserPromptSubmit hook stdin JSON. */
export function parsePrompt(stdinRaw) {
  try {
    const obj = JSON.parse(stdinRaw);
    return typeof obj.prompt === 'string' ? obj.prompt : '';
  } catch {
    return '';
  }
}

/**
 * Extract the JSON object from `ruflo memory search --format json` stdout.
 * stdout is noisy ([INFO]..., "Using sql.js"...) followed by a pretty-printed
 * JSON object — take from the first '{' to the last '}'.
 */
export function extractJson(stdout) {
  const start = stdout.indexOf('{');
  const end = stdout.lastIndexOf('}');
  if (start === -1 || end === -1 || end < start) return null;
  try {
    return JSON.parse(stdout.slice(start, end + 1));
  } catch {
    return null;
  }
}

/**
 * Format the search result object into an additionalContext block.
 * Returns '' when there are no usable results.
 */
export function formatRecall(searchObj) {
  if (!searchObj || !Array.isArray(searchObj.results) || searchObj.results.length === 0) {
    return '';
  }
  const lines = searchObj.results.slice(0, MAX_RESULTS).map((r) => {
    const key = typeof r.key === 'string' ? r.key : '(no-key)';
    const preview = typeof r.preview === 'string' ? r.preview : '';
    return `- [${key}] ${preview}`;
  });
  return ['[ruflo memory recall — релевантное из прошлых сессий]', ...lines].join('\n');
}

/** Build the UserPromptSubmit hook stdout payload. */
export function buildHookOutput(additionalContext) {
  return {
    hookSpecificOutput: {
      hookEventName: 'UserPromptSubmit',
      additionalContext,
    },
  };
}

/** Candidate paths for the ruflo bin JS. RUFLO_RECALL_BIN overrides all. */
export function rufloBinCandidates(env) {
  if (env.RUFLO_RECALL_BIN) return [env.RUFLO_RECALL_BIN];
  const out = [];
  if (env.APPDATA) {
    out.push(join(env.APPDATA, 'npm', 'node_modules', 'ruflo', 'bin', 'ruflo.js'));
  }
  return out;
}

function resolveRufloBin(env) {
  return rufloBinCandidates(env).find((p) => existsSync(p)) ?? null;
}

function readStdin() {
  return new Promise((resolve) => {
    let data = '';
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', (c) => { data += c; });
    process.stdin.on('end', () => resolve(data));
    process.stdin.on('error', () => resolve(data));
  });
}

function runSearch(rufloBin, query) {
  return new Promise((resolve) => {
    execFile(
      process.execPath,
      [rufloBin, 'memory', 'search', '-q', query, '--format', 'json', '-l', String(MAX_RESULTS)],
      { timeout: SEARCH_TIMEOUT_MS, encoding: 'utf8' },
      (_err, stdout) => resolve(typeof stdout === 'string' ? stdout : ''),
    );
  });
}

async function main() {
  let additionalContext = '';
  try {
    const stdinRaw = await readStdin();
    const prompt = parsePrompt(stdinRaw);
    const rufloBin = resolveRufloBin(process.env);
    if (prompt.trim().length > 0 && rufloBin) {
      const stdout = await runSearch(rufloBin, prompt.slice(0, MAX_QUERY_CHARS));
      additionalContext = formatRecall(extractJson(stdout));
    }
  } catch {
    additionalContext = '';
  }
  if (additionalContext) {
    process.stdout.write(JSON.stringify(buildHookOutput(additionalContext)));
  }
  process.exit(0);
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  main();
}
```

- [ ] **Step 4: Запустить тест — убедиться, что проходит**

```bash
node --test tools/ruflo-recall-hook.test.mjs
```

Expected: PASS — 13 тестов, `# pass 13`, `# fail 0`.

- [ ] **Step 5: Интеграционная проверка скрипта (fail-open при отсутствии ruflo)**

```bash
echo '{"prompt":"anything"}' | RUFLO_RECALL_BIN=/nonexistent/zzz.js node tools/ruflo-recall-hook.mjs; echo "exit=$?"
```

Expected: `exit=0`, вывода нет (ruflo-бинарь не найден → пустой инжект, fail-open).

- [ ] **Step 6: Коммит**

```bash
git add tools/ruflo-recall-hook.mjs tools/ruflo-recall-hook.test.mjs
git commit -m "feat(ruflo): UserPromptSubmit advisory recall hook

Hook script that runs ruflo memory search per prompt and injects top
matches as additionalContext. Fail-open (error/timeout -> empty inject,
exit 0, never blocks). Pure-function core unit-tested via node --test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

Expected: pre-commit зелёный.

---

### Task 7: Зарегистрировать хук в `.claude/settings.json` и проверить

**Files:**

- Modify: `.claude/settings.json` (добавить `hooks.UserPromptSubmit`)

- [ ] **Step 1: Добавить блок `UserPromptSubmit` в `hooks`**

В `.claude/settings.json` заменить:

```json
  "hooks": {
    "PreToolUse": [
```

на:

```json
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"$CLAUDE_PROJECT_DIR/tools/ruflo-recall-hook.mjs\""
          }
        ]
      }
    ],
    "PreToolUse": [
```

- [ ] **Step 2: Проверить, что `settings.json` остался валидным JSON**

```bash
node -e "JSON.parse(require('fs').readFileSync('.claude/settings.json','utf8')); console.log('settings.json: valid JSON')"
```

Expected: `settings.json: valid JSON`.

- [ ] **Step 3: End-to-end проверка хука с реальной памятью**

```bash
ruflo memory store -k hook-e2e --value "Лидерра advisory hook end-to-end probe entry"
echo '{"prompt":"advisory hook probe"}' | node tools/ruflo-recall-hook.mjs; echo "exit=$?"
ruflo memory delete -k hook-e2e
```

Expected: вывод хука — JSON, содержащий `"hookEventName":"UserPromptSubmit"` и
`"additionalContext"` с упоминанием `hook-e2e` (запись припомнена); `exit=0`.
Если `additionalContext` пуст — проверить, что патч H7 наложен (`node
tools/ruflo-h7-patch.mjs --check` → `PATCHED`) и что store/search работают (Task 5).

- [ ] **Step 4: Измерить латентность хука**

В PowerShell:

```powershell
Measure-Command { echo '{"prompt":"latency probe"}' | node tools/ruflo-recall-hook.mjs }
```

Expected: `TotalSeconds` записать. Если > 3.5 c — хук упирается в таймаут
`SEARCH_TIMEOUT_MS`; это допустимо (fail-open), но если латентность стабильно высокая,
поднять `SEARCH_TIMEOUT_MS` в `tools/ruflo-recall-hook.mjs` (отдельным шагом + повторный
`node --test`) или принять как known cost. Зафиксировать измеренное значение в коммите.

- [ ] **Step 5: Коммит**

```bash
git add .claude/settings.json
git commit -m "feat(ruflo): register UserPromptSubmit advisory recall hook

Wire tools/ruflo-recall-hook.mjs into .claude/settings.json so ruflo
memory recall is injected per prompt. Project-scoped, fail-open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

Expected: pre-commit зелёный.

---

## Task 8: Closeout — память + push

**Files:** memory `project_ruflo_integration.md` (вне git), push коммитов.

- [ ] **Step 1: Обновить memory `project_ruflo_integration.md`**

В `C:\Users\Administrator\.claude\projects\c---------------------crm-------------\memory\project_ruflo_integration.md`:

- Исправить раздел про H7: баг **исправлен** патчем `getBridge()` (раздел «Runtime
  state» / «Open questions» / квирк H7).
- Добавить operational-ноту: **после каждого `ruflo update` запускать `node
  tools/ruflo-h7-patch.mjs`** (патч в global node_modules затирается обновлением).
- Зафиксировать: `claude` CLI установлен (D1), advisory-хук активен (D3).

- [ ] **Step 2: Финальная верификация перед push**

```bash
node --test tools/ruflo-h7-patch.test.mjs tools/ruflo-recall-hook.test.mjs
node tools/ruflo-h7-patch.mjs --check
git log --oneline -4
```

Expected: все тесты PASS (`# fail 0`); патч `PATCHED`; в логе — 3 новых коммита
(Task 4, 6, 7) поверх `a6649e4`.

- [ ] **Step 3: Push в origin/main**

```bash
git push origin main
```

Expected: pre-push gauntlet (gitleaks-full-history + lychee) зелёный; коммиты
запушены. Push в `main` разрешён settings rule `Bash(git push origin main:*)`.

---

## Verification (end-to-end)

После Task 8:

- **D1:** `claude --version` → версия; `ruflo doctor` без «Claude Code CLI: Not installed».
- **D2:** `node tools/ruflo-h7-patch.mjs --check` → `PATCHED`; cross-process
  round-trip (store → новый процесс retrieve) находит запись; `node --test
  tools/ruflo-h7-patch.test.mjs` → 8 pass.
- **D3:** `node --test tools/ruflo-recall-hook.test.mjs` → 14 pass; `echo
  '{"prompt":"..."}' | node tools/ruflo-recall-hook.mjs` на непустой памяти выдаёт
  `UserPromptSubmit`-payload; `.claude/settings.json` — валидный JSON с блоком
  `UserPromptSubmit`.
- Регрессия приложения не затрагивается (D1-D3 меняют только ruflo-инфраструктуру,
  `.claude/settings.json`, `tools/` — не `app/`, не schema, не тесты приложения).

## Rollback

- **D1:** `npm uninstall -g @anthropic-ai/claude-code`.
- **D2:** `node tools/ruflo-h7-patch.mjs --revert` (или `npm i -g ruflo` —
  переустановка перетирает патч); удалить `tools/ruflo-h7-patch.*`.
- **D3:** удалить блок `UserPromptSubmit` из `.claude/settings.json`; удалить
  `tools/ruflo-recall-hook.*`.
- Откат коммитов: `git revert` коммитов Task 4/6/7.

## Notes

- `tools/*.mjs` намеренно вне Vitest приложения (Vitest сконфигурирован на
  `app/resources/js/**`) — тестируются `node --test`, без правок конфигов проекта.
- Косметический баг namespace `undefined` (`commands/memory.js:38`) — вне scope
  (спека §1.2). На round-trip не влияет: и `storeEntry`, и `getEntry` дефолтят
  namespace в `'default'` внутри себя.
- Авто-store хук, routing-классификация в хуке, provider-ключ Anthropic — не-цели
  (спека §1.2).

---

## Implementation addendum (2026-05-15)

Исполнено через `superpowers:subagent-driven-development`. Ключевое отклонение от
плана как написан:

**D2 — фикс H7 потребовал второй операции (dedupe `onnxruntime-node`).** Round-trip
верификация Task 5 показала: один патч `getBridge()` приводит к крашу `ruflo memory`
(`ERR_DLOPEN_FAILED`). Raw sql.js путь генерирует embeddings через код-путь,
тянущий `onnxruntime-node`, а дерево зависимостей ruflo несёт **две несовместимые
версии** `onnxruntime-node` — `1.24.3` (top-level) и `1.14.0` (вложенная под
`@xenova/transformers`). Обе несут `onnxruntime.dll`; загрузка обеих в один процесс
конфликтует по имени DLL → load failure. Корень подтверждён через
`superpowers:systematic-debugging` (контролируемый эксперимент: одна версия
изолированно — грузится; обе вместе — падает).

Фикс: отключить вложенный дубликат (`@xenova/transformers/node_modules/onnxruntime-node`
→ `.h7disabled`), чтобы грузилась только hoisted-версия 1.24.3. Скрипт
`tools/ruflo-h7-patch.mjs` расширен (commit `ef71bce`) — теперь делает **обе**
операции (getBridge-патч + dedupe); `apply`/`--revert`/`--check` покрывают весь
фикс. Верифицировано end-to-end: store → retrieve в новом процессе → найдено;
semantic-поиск score 0.76. Это уточняет спеку §4 (там фикс описан как один
getBridge-патч — Option 1; фактически нужно patch + dedupe).

**Остаточные ruflo-alpha-квирки** (не блокеры, задокументированы):

- `ruflo memory store` периодически логирует `TypeError: Tensor.location must be a
  string` — JS-level API-рассинхрон @xenova/transformers v2 ↔ onnxruntime 1.24,
  вскрытый dedupe. Не фатально: запись всё равно персистит и находится (поиск
  деградирует к keyword-матчингу).
- Удалённые записи остаются «призраками» в HNSW-индексе (ruflo #1122) — `memory
  delete` убирает строку, но не вектор; поиск их ещё возвращает.
- `ruflo memory retrieve` иногда печатает libuv-assertion при выходе процесса
  (`UV_HANDLE_CLOSING`) — косметика, после реального вывода команды.

**Обе операции живут в global npm-установке** и затираются `ruflo update` — после
любого апгрейда ruflo перезапустить `node tools/ruflo-h7-patch.mjs`.

**Коммиты:** `1052ddf` + `be755dd` (D2 Task 4 — скрипт + harden), `ef71bce`
(D2 — скрипт +dedupe), `08d5ff1` (D3 Task 6 — хук), `cd04cd6` (D3 Task 7 —
регистрация хука). D1 (claude CLI) — операционный, без коммитов в репозиторий.
