feat(secretary): buildStepLine принимает готовую суть (essence)

Task 2/5 плана. Если передан essence{user,assistant} — берём его дословно
(+ чистка пробелов); иначе прежний фолбэк firstSentence. «делал: <tools>»
остаётся детерминированным. Свод секретаря 110/110.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-23 09:25:55 +03:00
parent 503ddaeab2
commit ff16d05ec3
2 changed files with 17 additions and 3 deletions
+6 -3
View File
@@ -61,7 +61,7 @@ export function buildStepsFromRaw(rawText, session) {
// Человекочитаемая строка шага для раздела «Шаги (Слой 1)»: «Ход N — я: … · ты: … · делал: …».
// Суть — первая фраза реплики; служебные строки (экономия/coverage/вердикт) отброшены;
// «делал» — имена инструментов из действий хода. Название файла полного хода добавляет рендер.
export function buildStepLine({ turn, user, assistant, actions = [] } = {}) {
export function buildStepLine({ turn, user, assistant, actions = [], essence = null } = {}) {
// Содержательная фраза: убираем ведущую нумерацию списка («1.»/«2)»), копим до ≥25 симв.,
// чтобы не выдать обрывок «Стоп.»; длинное усекаем.
const firstSentence = (s) => {
@@ -82,8 +82,11 @@ export function buildStepLine({ turn, user, assistant, actions = [] } = {}) {
};
const cleanA = String(assistant ?? '').split('\n')
.filter((l) => !/^\s*(экономия:|coverage:|вердикт:)/i.test(l)).join(' ');
const u = sysLabel(user) || firstSentence(user) || '(без вопроса)';
const a = firstSentence(cleanA) || '(без ответа)';
const clean1 = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
const eU = essence && clean1(essence.user);
const eA = essence && clean1(essence.assistant);
const u = eU || sysLabel(user) || firstSentence(user) || '(без вопроса)';
const a = eA || firstSentence(cleanA) || '(без ответа)';
const did = [...new Set((actions || []).map((t) => String(t).trim()).filter(Boolean))].join(', ') || '—';
return `Ход ${turn} — я: ${u} · ты: ${a} · делал: ${did}`;
}
+11
View File
@@ -99,6 +99,17 @@ describe('buildStepLine', () => {
expect(buildStepLine({ turn: 1, user: 'Stop hook feedback: coverage missing', assistant: '' })).toContain('я: (гейт проверки)');
expect(buildStepLine({ turn: 2, user: 'Base directory for this skill: C:\\x\\skills\\writing-plans\\SKILL.md', assistant: 'x.' })).toContain('я: (навык: writing-plans)');
});
it('essence: берёт модельную суть дословно + детерминированный «делал»', () => {
const s = buildStepLine({ turn: 12, user: 'длинная вода без точек '.repeat(10),
assistant: 'вода', actions: ['Read', 'Read', 'Grep'],
essence: { user: 'промпт не логируется?', assistant: 'достать можно: поймать или пересобрать' } });
expect(s).toBe('Ход 12 — я: промпт не логируется? · ты: достать можно: поймать или пересобрать · делал: Read, Grep');
});
it('без essence — прежний фолбэк (firstSentence)', () => {
const s = buildStepLine({ turn: 2, user: 'сделай флажок.', assistant: 'Готово.', essence: null });
expect(s).toContain('я: сделай флажок');
expect(s).toContain('ты: Готово');
});
});
describe('writeFileAtomic — запись через temp + rename (защита от полузаписи при параллельных сессиях)', () => {