diff --git a/tools/secretary-prompt-hook.mjs b/tools/secretary-prompt-hook.mjs index aa30de5..8dc7787 100644 --- a/tools/secretary-prompt-hook.mjs +++ b/tools/secretary-prompt-hook.mjs @@ -29,7 +29,7 @@ function main() { try { mkdirSync(dirname(FLAG), { recursive: true }); } catch { /* ignore */ } if (cmd === 'on') { - const m = prompt.match(/секретар[а-я]*\s+(?:для\s+|по\s+)?([a-zA-Z0-9-]{2,})/); + const m = prompt.match(/секретар[а-я]*\s+(?:для\s+|по\s+)?([a-zA-Zа-яёА-ЯЁ0-9-]{2,})/); const work = (m && m[1]) || 'general'; try { writeFileSync(FLAG, JSON.stringify({ mode: 'on', startedAtTurn: turnCount(rawFile), work, session })); } catch { /* ignore */ } } else if (cmd === 'off') { diff --git a/tools/secretary-stop-hook.mjs b/tools/secretary-stop-hook.mjs index 061057d..ac0f8c1 100644 --- a/tools/secretary-stop-hook.mjs +++ b/tools/secretary-stop-hook.mjs @@ -61,7 +61,10 @@ async function main() { }); const extraction = parseExtractionResponse(typeof text === 'string' ? text : ''); if (extraction) { - for (const d of extraction.decisions) { if (!Array.isArray(d.turns) || !d.turns.length) d.turns = [turn]; } + // Номер хода знает только хук — форсим реальный 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(); diff --git a/tools/secretary-transcript.mjs b/tools/secretary-transcript.mjs index ca834d0..44c1c29 100644 --- a/tools/secretary-transcript.mjs +++ b/tools/secretary-transcript.mjs @@ -12,12 +12,23 @@ function parseLines(text) { return entries; } +// Настоящий промпт пользователя (НЕ tool_result): content — строка или массив с text-блоком. +// В формате Anthropic tool_result — это сообщения role:user, их пропускаем, иначе теряются +// и настоящий промпт, и все действия ассистента до него. +function isRealUserPrompt(msg) { + if (!msg || msg.role !== 'user') return false; + const c = msg.content; + if (typeof c === 'string') return true; + if (Array.isArray(c)) return c.some((b) => b && b.type === 'text'); + return false; +} + /** Последний обмен из стенограммы: { user, assistant, actions:[{tool,input}] }. */ export function parseLastExchange(transcriptText) { const entries = parseLines(transcriptText); let u = -1; for (let i = entries.length - 1; i >= 0; i--) { - if (entries[i] && entries[i].message && entries[i].message.role === 'user') { u = i; break; } + if (entries[i] && isRealUserPrompt(entries[i].message)) { u = i; break; } } const userContent = u >= 0 ? entries[u].message.content : ''; const user = typeof userContent === 'string' diff --git a/tools/secretary-transcript.test.mjs b/tools/secretary-transcript.test.mjs index 3ff2e1b..790b40d 100644 --- a/tools/secretary-transcript.test.mjs +++ b/tools/secretary-transcript.test.mjs @@ -24,4 +24,18 @@ describe('parseLastExchange', () => { expect(ex.assistant).toBe('а'); expect(ex.actions).toEqual([]); }); + it('пропускает tool_result (role:user) — берёт настоящий промпт + все действия', () => { + const t = [ + JSON.stringify({ message: { role: 'user', content: 'настоящий вопрос' } }), + JSON.stringify({ message: { role: 'assistant', content: [ + { type: 'text', text: 'думаю' }, { type: 'tool_use', name: 'Read', input: { f: 'a' } }] } }), + JSON.stringify({ message: { role: 'user', content: [{ type: 'tool_result', content: 'результат' }] } }), + JSON.stringify({ message: { role: 'assistant', content: [{ type: 'text', text: 'готово' }] } }), + ].join('\n'); + const ex = parseLastExchange(t); + expect(ex.user).toBe('настоящий вопрос'); + expect(ex.assistant).toContain('думаю'); + expect(ex.assistant).toContain('готово'); + expect(ex.actions).toEqual([{ tool: 'Read', input: '{"f":"a"}' }]); + }); });