Files
brain/tools/secretary-flag.mjs
T
Дмитрий f8a40da56c feat(secretary): захват выдачи инструмента (N3) + сверка имени дела при включении (N2)
- parseLastExchange привязывает результат инструмента к действию по tool_use_id,
  склеивает text-блоки, усекает до 1200 симв.; [ВЫДАЧА] в Слое 1 теперь наполняется
- resolveCaseActivation: похожее имя дела (опечатка/подстрока) -> переспросить,
  не заводя дело-двойник; хук secretary-prompt-hook выводит подсказку с кандидатами
- TDD: тесты secretary-transcript/flag/prompt-hook; полный свод зелёный

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:21:06 +03:00

57 lines
3.0 KiB
JavaScript

// Детект команды секретаря в тексте промпта. Кавычки/код снимаются до сопоставления,
// чтобы цитирование не срабатывало (приём как в существующих детекторах).
function stripQuoted(text) {
return String(text || '')
.replace(/«[^»]*»/g, ' ')
.replace(/"[^"]*"/g, ' ')
.replace(/`[^`]*`/g, ' ');
}
export function detectSecretaryCommand(promptText) {
const t = stripQuoted(promptText).toLowerCase();
if (/выключи\s+секретар/.test(t)) return 'off';
if (/включи\s+секретар/.test(t)) return 'on';
return null;
}
// Имя файла-флажка ПО СЕССИИ: своя записка у каждого окна, параллельные сессии не топчут
// друг друга (общий флажок раньше перетирался последним «включи»).
export function secretaryModeFileName(session) {
return `secretary-mode-${session || 'unknown'}.json`;
}
// Расстояние Левенштейна (для ловли опечатки в имени дела).
function levenshtein(a, b) {
const m = a.length, n = b.length;
const d = Array.from({ length: m + 1 }, (_, i) => { const row = new Array(n + 1).fill(0); row[0] = i; return row; });
for (let j = 0; j <= n; j++) d[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
}
}
return d[m][n];
}
// «Похоже» = опечатка (правка ≤2 при длине обоих ≥4) ИЛИ сокращение (подстрока, длина короткого ≥3).
function isSimilar(a, b) {
if (a === b) return false;
const short = Math.min(a.length, b.length);
if (short >= 3 && (a.includes(b) || b.includes(a))) return true;
if (a.length >= 4 && b.length >= 4 && levenshtein(a, b) <= 2) return true;
return false;
}
// Сверка введённого имени дела со списком существующих (папки docs/secretary/<дело>).
// Точное совпадение → активировать существующее; похоже, но не точно → переспросить;
// не похоже / список пуст → активировать как новое (имя как ввёл).
export function resolveCaseActivation(requested, existing = []) {
const req = String(requested || '').trim().toLowerCase();
const list = (existing || []).map((e) => String(e || '').trim()).filter(Boolean);
const exact = list.find((e) => e.toLowerCase() === req);
if (exact) return { action: 'activate', work: exact };
const candidates = list.filter((e) => isSimilar(e.toLowerCase(), req));
if (candidates.length > 0) return { action: 'confirm', candidates };
return { action: 'activate', work: String(requested || '').trim() };
}