4253cd7114
Корень бага: в формате Anthropic tool_result — сообщения role:user; parseLastExchange брал их вместо настоящего промпта, теряя текст юзера и действия. + хук форсит реальный turn (Хайку его не знает) + work-slug принимает кириллицу. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
4.6 KiB
JavaScript
92 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
// Stop-переходник секретаря: ВСЕГДА пишет сырьё (Слой 1); если секретарь включён —
|
|
// онлайн-выжимка в протокол дела через НОВЫЙ мотор (SECRETARY_LLM_KEY).
|
|
// Тонкий shell над чистыми parseLastExchange / buildRawRecord / buildExtractionPrompt /
|
|
// parseExtractionResponse / applyExtraction / renderProtocol / upsertIndexEntry.
|
|
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
import { parseLastExchange } from './secretary-transcript.mjs';
|
|
import { buildRawRecord } from './secretary-layer1.mjs';
|
|
import { buildExtractionPrompt, parseExtractionResponse } from './secretary-extract.mjs';
|
|
import { applyExtraction, renderProtocol, EMPTY_PROTOCOL } from './secretary-protocol.mjs';
|
|
import { upsertIndexEntry } from './secretary-index.mjs';
|
|
import { sanitize } from './observer-pii-filter.mjs';
|
|
import { callAnthropicAPI } from './router-classifier.mjs';
|
|
|
|
const FLAG = join(homedir(), '.claude', 'runtime', 'secretary-mode.json');
|
|
|
|
function readStdin() { try { return readFileSync(0, 'utf-8'); } catch { return ''; } }
|
|
function readFlag() { try { return JSON.parse(readFileSync(FLAG, 'utf-8')); } catch { return { mode: 'off' }; } }
|
|
function turnCount(rawFile) {
|
|
if (!existsSync(rawFile)) return 0;
|
|
try { return (readFileSync(rawFile, 'utf-8').match(/=== ХОД turn=/g) || []).length; } catch { return 0; }
|
|
}
|
|
|
|
async function main() {
|
|
let ev = {};
|
|
try { ev = JSON.parse(readStdin() || '{}'); } catch { ev = {}; }
|
|
const session = ev.session_id || ev.sessionId || 'unknown';
|
|
const tp = ev.transcript_path || ev.transcriptPath;
|
|
let transcript = '';
|
|
try { if (tp && existsSync(tp)) transcript = readFileSync(tp, 'utf-8'); } catch { transcript = ''; }
|
|
|
|
const secdir = join(process.cwd(), 'docs', 'secretary');
|
|
const rawFile = join(secdir, 'raw', `${session}.log`);
|
|
const ex = parseLastExchange(transcript);
|
|
const turn = turnCount(rawFile) + 1;
|
|
|
|
// Слой 1: всегда пишем сырьё (PII вырезается перед записью).
|
|
try {
|
|
const rec = sanitize(buildRawRecord({
|
|
turn, time: new Date().toISOString(), session,
|
|
user: ex.user, assistant: ex.assistant, actions: ex.actions,
|
|
}));
|
|
mkdirSync(join(secdir, 'raw'), { recursive: true });
|
|
appendFileSync(rawFile, rec + '\n', 'utf-8');
|
|
} catch { /* fail-quiet */ }
|
|
|
|
// Онлайн-выжимка только если секретарь включён и есть НОВЫЙ ключ.
|
|
const flag = readFlag();
|
|
const apiKey = process.env.SECRETARY_LLM_KEY;
|
|
if (flag.mode !== 'on' || !apiKey) { process.exit(0); }
|
|
|
|
const work = flag.work || 'general';
|
|
try {
|
|
const { system, user } = buildExtractionPrompt({ lastExchange: ex, worksIndex: [] });
|
|
const text = await callAnthropicAPI({ system, user }, {
|
|
apiKey,
|
|
baseUrl: process.env.SECRETARY_LLM_BASE_URL || undefined,
|
|
model: process.env.SECRETARY_LLM_MODEL || undefined,
|
|
});
|
|
const extraction = parseExtractionResponse(typeof text === 'string' ? text : '');
|
|
if (extraction) {
|
|
// Номер хода знает только хук — форсим реальный turn на все записи (Хайку его не знает).
|
|
for (const arr of [extraction.decisions, extraction.will, extraction.open, extraction.doneNext, extraction.supersede]) {
|
|
for (const e of (arr || [])) { e.turns = [turn]; }
|
|
}
|
|
const workDir = join(secdir, work);
|
|
const protoJson = join(workDir, 'protocol.json');
|
|
let proto = EMPTY_PROTOCOL();
|
|
try { if (existsSync(protoJson)) proto = JSON.parse(readFileSync(protoJson, 'utf-8')); } catch { proto = EMPTY_PROTOCOL(); }
|
|
proto = applyExtraction(proto, extraction);
|
|
mkdirSync(workDir, { recursive: true });
|
|
writeFileSync(protoJson, JSON.stringify(proto, null, 2), 'utf-8');
|
|
writeFileSync(join(workDir, 'protocol.md'), renderProtocol(proto), 'utf-8');
|
|
|
|
const idxFile = join(secdir, 'содержание.md');
|
|
let idxMd = '';
|
|
try { if (existsSync(idxFile)) idxMd = readFileSync(idxFile, 'utf-8'); } catch { idxMd = ''; }
|
|
const updated = upsertIndexEntry(idxMd, {
|
|
slug: work, title: work, goal: '(дело)', status: 'открыто',
|
|
date: new Date().toISOString().slice(0, 10),
|
|
});
|
|
writeFileSync(idxFile, updated, 'utf-8');
|
|
}
|
|
} catch { /* fail-quiet: сырьё уже записано */ }
|
|
process.exit(0);
|
|
}
|
|
|
|
const isCli = (process.argv[1] || '').replace(/\\/g, '/').endsWith('/secretary-stop-hook.mjs');
|
|
if (isCli) main();
|