424 lines
28 KiB
JavaScript
424 lines
28 KiB
JavaScript
// ━━━ ГДЕ ЛЕЖИТ СТЕНОГРАММА РАБОТНИКА ━━━
|
||
//
|
||
// ━━━ БЕДА, РАДИ КОТОРОЙ ЭТОТ ФАЙЛ ЗАВЕДЁН (03.08.2026) ━━━
|
||
// Когда работник умирает молча, затея кладёт владельцу в утреннюю сводку строку вида
|
||
// «Работник кончился, не отдав итога. Что он сказал напоследок: ничего» — и это ВСЁ, что
|
||
// она знает. А полная запись всего, что с работником было, в эту минуту уже лежит на диске:
|
||
// система сама ведёт стенограмму каждого живого работника в домашнем каталоге. Прошлая
|
||
// смена искала в ней причину смерти ВЕСЬ ДЕНЬ, перебирая каталоги руками, — и нашла только
|
||
// вечером. Лежала она там с самого начала.
|
||
//
|
||
// 🔴🔴 ЗАМЕРЕНО НА ДВУХ НАСТОЯЩИХ ПОГИБШИХ/ЗАКОНЧИВШИХ РАБОТНИКАХ (не выдумка, файлы на диске):
|
||
//
|
||
// работник `zhivoe-5/w-1` — УМЕР МОЛЧА (`status: "stalled"`):
|
||
// · своего следа потока у затеи НЕТ ВОВСЕ — `stream-raw.jsonl` не создан,
|
||
// `exit.json` → `result: null`, `supervisor.log` содержит одно ругательство узла
|
||
// про оболочку и ни слова о работнике;
|
||
// · а в стенограмме — причина смерти ПРЯМЫМ ТЕКСТОМ: работнику дошло от поручения
|
||
// одно первое слово («кажется, сообщение оборвалось — пришло только „Ты“»), то есть
|
||
// поручение съела склейка через оболочку. Это ровно тот корень, который затея потом
|
||
// закрыла сторожем `zapretSkleyki` в spawn.mjs.
|
||
//
|
||
// работник `rabota-1/w-1` — ЗАКОНЧИЛ САМ (`status: "extinguished"`):
|
||
// · след потока у затеи ЕСТЬ (`stream-raw.jsonl`, 59 046 байт), `exit.json` несёт
|
||
// полный итог с причиной остановки.
|
||
//
|
||
// ⇒ ОТВЕТ НА ВОПРОС «А НЕ ПИШЕТ ЛИ ЭТУ ЖЕ УЛИКУ КТО-ТО ЕЩЁ, ПОМИМО СТЕНОГРАММЫ?» —
|
||
// ДА, ПИШЕТ, НО РОВНО ТАМ, ГДЕ ОНА НЕ НУЖНА. Затея ведёт свой собственный след потока
|
||
// (`.night/runs/<прогон>/workers/<работник>/stream-raw.jsonl` + `exit.json`), и на живом
|
||
// замере он полон. Но он появляется только тогда, когда работник вправду заговорил: у
|
||
// молча умершего его НЕТ НИ БАЙТА. То есть свой след затеи и стенограмма — не дубликаты,
|
||
// а РОВНО ДОПОЛНЯЮЩИЕ ДРУГ ДРУГА улики, и стенограмма — единственная в том самом случае,
|
||
// ради которого владелец утром и лезет в улики. Дублирования здесь нет, и заводить свой
|
||
// третий след не нужно — нужно уметь НАЙТИ уже существующий.
|
||
//
|
||
// ━━━ 🔴🔴 ГЛАВНОЕ, ЧТО ЗАМЕР ОПРОВЕРГ В САМОЙ ПОСТАНОВКЕ ЗАДАЧИ ━━━
|
||
// Мне было сказано: «номер сессии, который затея записывает у себя, — НЕ ТОТ, каким система
|
||
// называет файл стенограммы». Замерено на обоих живых примерах — правило НЕ ОДНО, их два,
|
||
// и они противоположны:
|
||
//
|
||
// `rabota-1/w-1` : затея записала `c837496c-…`, файл назван `c837496c-….jsonl` → СОВПАЛО
|
||
// `zhivoe-5/w-1` : затея записала `3568d1be-…`, файл назван `3494b548-….jsonl` → РАЗОШЛОСЬ,
|
||
// и заказанного номера внутри файла НЕТ НИ В ОДНОЙ строке (искал — 0 из 15).
|
||
//
|
||
// То есть поиск «по номеру, который затея у себя записала» работает ИНОГДА. И это худший
|
||
// из возможных раскладов: на живом работнике, который дошёл до конца, такой поиск СХОДИТСЯ
|
||
// и тем доказывает сам себе, что он исправен, — а на молча умершем молча промахивается.
|
||
// Ровно на умершем он и нужен.
|
||
//
|
||
// 🔴 ОТЧЕГО ТАК — сказано как замер, а не как диагноз: у `zhivoe-5` поручение до работника
|
||
// не дошло вовсе (склейка через оболочку), а вместе с поручением, судя по всему, не дошёл
|
||
// и ключ `--session-id`, которым затея заказывает номер, — и программа завела свой. Прямым
|
||
// замером ключа я это НЕ проверял, поэтому утверждаю только то, что видно: заказанный номер
|
||
// в стенограмме отсутствует целиком.
|
||
//
|
||
// ⇒ ПОЭТОМУ НОМЕР СЕССИИ ЗДЕСЬ — НЕ ГЛАВНЫЙ КЛЮЧ, А ЛИШЬ БЫСТРАЯ ДОРОЖКА. Главный ключ —
|
||
// РАБОЧИЙ УГОЛ работника: у каждого работника он свой собственный (`night-workspaces/
|
||
// <прогон>/<работник>`), и система заводит под него ОТДЕЛЬНЫЙ каталог стенограмм.
|
||
//
|
||
// ━━━ 🔴 ДОПУЩЕНИЕ, НА КОТОРОМ ВСЁ ЭТО СТОИТ (названо вслух, а не спрятано) ━━━
|
||
// «Один работник — один свой рабочий угол, значит каталог стенограмм этого угла
|
||
// принадлежит ему одному».
|
||
//
|
||
// 🪤 ГДЕ ЭТО ДОПУЩЕНИЕ ОШИБЁТСЯ — тоже названо вслух, и датчик об этом ГОВОРИТ, а не молчит:
|
||
// · Человек (или соседняя смена) открыл своего помощника В ТОМ ЖЕ углу работника — чтобы
|
||
// посмотреть, что тот наделал. В каталоге появится ВТОРАЯ стенограмма, и «взять свежую»
|
||
// дало бы стенограмму ЧЕЛОВЕКА вместо работниковой. Поэтому «свежую» здесь не берут
|
||
// молча: при двух и более находках датчик отдаёт СПИСОК и говорит владельцу, что выбрать
|
||
// за него не может.
|
||
// · Стенограмм НОЛЬ — так бывает, если работник умер раньше, чем программа успела завести
|
||
// файл. Это НЕ «ошибка поиска», и врать про неё нельзя: датчик отвечает «ноль» и всё
|
||
// равно называет каталог, куда смотреть.
|
||
// · Работника перезапустили в том же углу (`--resume`) — стенограмм станет две-три, и
|
||
// это тот же случай «несколько».
|
||
// · Угол переименовали/удалили после прогона — каталог стенограмм останется под СТАРЫМ
|
||
// именем, а по новому пути не найдётся ничего.
|
||
//
|
||
// ━━━ ПРАВИЛО ИМЕНИ КАТАЛОГА — ПРОВЕРЕНО САМ, А НЕ ПРИНЯТО НА ВЕРУ ━━━
|
||
// «Путь угла, где каждый знак, кроме латинских букв и цифр, заменён на дефис». Сверено на
|
||
// четырёх настоящих каталогах из домашней папки (два угла работников + этот рабочий угол +
|
||
// корень хранилища) — сошлось на всех четырёх, включая кириллицу (каждая буква даёт свой
|
||
// дефис), пробел, двоеточие диска, точку в `.claude` и обе косые.
|
||
//
|
||
// 🪤 БУКВА ДИСКА НЕ ПРИВОДИТСЯ К ОДНОМУ РЕГИСТРУ — замерено: в домашней папке живут разом
|
||
// `C---…` и `c---…`, то есть один и тот же каталог, открытый как `C:\` и как `c:\`, даёт
|
||
// ДВА РАЗНЫХ каталога стенограмм. Поэтому ищем по обоим написаниям буквы диска.
|
||
import { readdirSync, statSync, existsSync, readFileSync, openSync, readSync, closeSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { homedir } from 'node:os';
|
||
|
||
// Запас времени по краям окна прогона: стенограмма дописывается не мгновенно, и её время
|
||
// правки может уехать за конец прогона. Пятнадцать минут — с запасом и без риска захватить
|
||
// соседний прогон (между прогонами этой ночью было больше двух часов).
|
||
export const ZAPAS_MS = 15 * 60 * 1000;
|
||
|
||
// ━━━ ИМЯ КАТАЛОГА ПО ПУТИ УГЛА ━━━
|
||
// 🪤 Хвостовые косые срезаются ДО замены: `…\w-1\` иначе дал бы лишний дефис в конце и
|
||
// каталог не нашёлся бы никогда — а путь с хвостовой косой владелец набирает запросто.
|
||
export function imyaKatalogaStenogramm(rabochiyUgol) {
|
||
const s = String(rabochiyUgol ?? '').trim().replace(/[\\/]+$/, '');
|
||
if (!s) return null;
|
||
return s.replace(/[^a-zA-Z0-9]/g, '-');
|
||
}
|
||
|
||
// Оба написания буквы диска — см. ловушку про регистр в шапке. Если путь не начинается
|
||
// с буквы диска (например, это путь Линукса), вариант ровно один.
|
||
export function imenaKatalogovStenogramm(rabochiyUgol) {
|
||
const imya = imyaKatalogaStenogramm(rabochiyUgol);
|
||
if (!imya) return [];
|
||
const pervaya = imya[0];
|
||
if (!/[a-zA-Z]/.test(pervaya)) return [imya];
|
||
const drugoy = pervaya === pervaya.toUpperCase() ? pervaya.toLowerCase() : pervaya.toUpperCase();
|
||
const vtoroy = drugoy + imya.slice(1);
|
||
return vtoroy === imya ? [imya] : [imya, vtoroy];
|
||
}
|
||
|
||
export function katalogiStenogramm(rabochiyUgol, { home = homedir() } = {}) {
|
||
return imenaKatalogovStenogramm(rabochiyUgol).map(imya => join(home, '.claude', 'projects', imya));
|
||
}
|
||
|
||
function spisokStenogramm(katalog) {
|
||
let imena;
|
||
try {
|
||
imena = readdirSync(katalog);
|
||
} catch {
|
||
return null; // каталога нет вовсе — это НЕ то же самое, что «каталог есть и пуст»
|
||
}
|
||
const out = [];
|
||
for (const imya of imena) {
|
||
if (!imya.toLowerCase().endsWith('.jsonl')) continue;
|
||
const put = join(katalog, imya);
|
||
let st;
|
||
try {
|
||
st = statSync(put);
|
||
} catch {
|
||
continue;
|
||
}
|
||
if (!st.isFile()) continue;
|
||
out.push({ put, imya, nomer: imya.slice(0, -'.jsonl'.length), bayt: st.size, izmenen: st.mtimeMs });
|
||
}
|
||
// Свежая первой — но выбор по свежести НИКОГДА не делается молча (см. шапку).
|
||
out.sort((a, b) => b.izmenen - a.izmenen);
|
||
return out;
|
||
}
|
||
|
||
// ━━━ 🔴🔴 А ЭТА СТЕНОГРАММА ВООБЩЕ ЕГО? ━━━
|
||
//
|
||
// Беда, которую эта мелочь закрывает, — самая опасная во всём файле, потому что она врёт
|
||
// НЕ пустотой, а уверенным ответом: если человек заглянул своим помощником в угол умершего
|
||
// работника (посмотреть, что тот наделал), в каталоге окажется ЕГО стенограмма. А если
|
||
// работниковой при этом не завелось вовсе, стенограмма будет ровно одна — и поиск бодро
|
||
// доложит владельцу «Стенограмма работника: …», подсунув запись человека.
|
||
//
|
||
// Сверяем не номера, а СЛОВА: первое сообщение пользователя в стенограмме против поручения
|
||
// из карточки работника. Совпадением считается ПРИСТАВКА в любую сторону — и вот почему
|
||
// именно так, а не «текст равен поручению»:
|
||
// `rabota-1` (дошёл) — в стенограмме лежит поручение ЦЕЛИКОМ;
|
||
// `zhivoe-5` (умер) — в стенограмме лежит «Ты», то есть ПЕРВОЕ СЛОВО поручения и всё
|
||
// (поручение съела склейка через оболочку — это и была причина смерти).
|
||
// Строгое равенство опознало бы только живого и промахнулось бы ровно на мёртвом.
|
||
//
|
||
// 🪤 ЧЕГО ЭТА СВЕРКА НЕ УМЕЕТ, сказано вслух: коротким приставкам верить нельзя — человек
|
||
// тоже мог начать со слова «Ты». Поэтому ответ у неё ТРИ, а не два: «да» · «нет» · «сказать
|
||
// нечем» (сообщения пользователя в стенограмме не нашлось, файл не прочёлся, поручения
|
||
// не дали). «Сказать нечем» НЕ выдаётся за «да» ни при каких условиях.
|
||
// 🪤 Читаем НЕ ВЕСЬ файл, а начало: стенограмма долгого работника вырастает в сотни
|
||
// мегабайт, а первое сообщение пользователя лежит в первых строках. Обрезанная посередине
|
||
// последняя строка не страшна — неразобранные строки и так пропускаются.
|
||
export const CHITAEM_BAYT = 512 * 1024;
|
||
|
||
function pervoePorucheniePolzovatelya(put) {
|
||
let text;
|
||
try {
|
||
const fd = openSync(put, 'r');
|
||
try {
|
||
const buf = Buffer.alloc(CHITAEM_BAYT);
|
||
const skolko = readSync(fd, buf, 0, CHITAEM_BAYT, 0);
|
||
text = buf.slice(0, skolko).toString('utf8');
|
||
} finally {
|
||
closeSync(fd);
|
||
}
|
||
} catch {
|
||
return null;
|
||
}
|
||
for (const line of text.split('\n')) {
|
||
if (!line.trim()) continue;
|
||
let e;
|
||
try { e = JSON.parse(line); } catch { continue; }
|
||
if (e?.type !== 'user') continue;
|
||
const c = e.message?.content;
|
||
const s = typeof c === 'string'
|
||
? c
|
||
: (Array.isArray(c) ? c.map(b => (typeof b?.text === 'string' ? b.text : '')).join('') : '');
|
||
const chisto = String(s).replace(/\s+/g, ' ').trim();
|
||
if (chisto) return chisto;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function pohozhaLiNaRabotnika(put, poruchenie) {
|
||
const zhdyom = String(poruchenie ?? '').replace(/\s+/g, ' ').trim();
|
||
if (!zhdyom) return null; // поручения не дали — сказать нечем
|
||
const bylo = pervoePorucheniePolzovatelya(put);
|
||
if (!bylo) return null; // сообщения пользователя нет — сказать нечем, но НЕ «нет»
|
||
return zhdyom.startsWith(bylo) || bylo.startsWith(zhdyom);
|
||
}
|
||
|
||
// ━━━ ГЛАВНЫЙ ПОИСК ━━━
|
||
//
|
||
// Ответ намеренно НЕ строка с путём, а разбор: сколько нашлось, каким способом, где искали
|
||
// и что из этого следует. Одна строка «вот путь» врала бы в двух из четырёх случаев (ноль
|
||
// находок и несколько находок), а именно в них владельцу и нужна правда.
|
||
export function naytiStenogrammu({
|
||
rabochiyUgol,
|
||
sessionId = null,
|
||
home = homedir(),
|
||
ot = null, // начало прогона, мс — необязательно
|
||
do_ = null, // конец прогона, мс — необязательно
|
||
poruchenie = null, // текст поручения из карточки — им сверяют, ЕГО ли это стенограмма
|
||
} = {}) {
|
||
const katalogi = katalogiStenogramm(rabochiyUgol, { home });
|
||
const pusto = {
|
||
naydeno: 0,
|
||
put: null,
|
||
kandidaty: [],
|
||
katalog: null,
|
||
katalogi_probovali: katalogi,
|
||
sposob: 'угол не назван',
|
||
stroka: 'Стенограмму искать не по чему: рабочий угол работника не назван.',
|
||
};
|
||
if (!katalogi.length) return pusto;
|
||
|
||
// 🪤 ПОЙМАНО СВОИМ ЖЕ СТОРОЖЕМ: сперва здесь стояло «последний найденный побеждает», и на
|
||
// ПУСТОМ каталоге ответ уезжал на второе написание буквы диска — владельцу называли путь,
|
||
// которого он у себя не набирал. Правило теперь: непустой каталог побеждает любой, а среди
|
||
// равных побеждает ПЕРВЫЙ, то есть написанный ровно так, как записан угол работника.
|
||
// 🔴 На Windows файловая система сама не различает регистр, и второе написание почти всегда
|
||
// ведёт в тот же каталог; список из двух заведён ради Линукса, где это два разных каталога.
|
||
let katalog = null;
|
||
let spisok = null;
|
||
for (const k of katalogi) {
|
||
const s = spisokStenogramm(k);
|
||
if (s === null) continue; // такого каталога нет — пробуем второе написание буквы диска
|
||
if (s.length) { katalog = k; spisok = s; break; } // непустой побеждает сразу
|
||
if (spisok === null) { katalog = k; spisok = s; } // пустой запоминаем, но поиск продолжаем
|
||
}
|
||
|
||
if (spisok === null) {
|
||
return {
|
||
naydeno: 0,
|
||
put: null,
|
||
kandidaty: [],
|
||
katalog: null,
|
||
katalogi_probovali: katalogi,
|
||
sposob: 'каталога стенограмм нет',
|
||
stroka: `Стенограммы работника нет: каталога под неё не заведено вовсе. Искали здесь: ${katalogi.join(' и ')}. `
|
||
+ 'Так бывает, когда работник умер раньше, чем программа успела завести файл.',
|
||
};
|
||
}
|
||
|
||
// ━━━ БЫСТРАЯ ДОРОЖКА: файл прямо по заказанному номеру ━━━
|
||
// Она сходится не всегда (замер в шапке: у одного из двух работников — мимо), поэтому
|
||
// она первая, но НЕ единственная: промах по номеру не отменяет поиска по каталогу.
|
||
if (sessionId) {
|
||
const tochno = spisok.find(f => f.nomer === String(sessionId));
|
||
if (tochno) {
|
||
return {
|
||
naydeno: 1,
|
||
put: tochno.put,
|
||
kandidaty: [tochno],
|
||
katalog,
|
||
katalogi_probovali: katalogi,
|
||
sposob: 'по заказанному номеру сессии',
|
||
stroka: `Стенограмма работника: ${tochno.put}`,
|
||
};
|
||
}
|
||
}
|
||
|
||
// ━━━ ОТБОР ПО ВРЕМЕНИ ПРОГОНА ━━━
|
||
// 🔴 Если отбор не оставил НИ ОДНОЙ — он не применяется вовсе, и об этом говорится вслух.
|
||
// Молча отдать ноль там, где файл есть, но со странным временем, — худшее, что можно
|
||
// сделать: владелец решит, что улики нет, и не пойдёт смотреть.
|
||
let otobrannye = spisok;
|
||
let poVremeni = false;
|
||
if (Number.isFinite(ot) || Number.isFinite(do_)) {
|
||
const nizh = Number.isFinite(ot) ? ot - ZAPAS_MS : -Infinity;
|
||
const verh = Number.isFinite(do_) ? do_ + ZAPAS_MS : Infinity;
|
||
const v_okne = spisok.filter(f => f.izmenen >= nizh && f.izmenen <= verh);
|
||
if (v_okne.length) {
|
||
otobrannye = v_okne;
|
||
poVremeni = v_okne.length !== spisok.length;
|
||
}
|
||
}
|
||
|
||
if (!otobrannye.length) {
|
||
return {
|
||
naydeno: 0,
|
||
put: null,
|
||
kandidaty: [],
|
||
katalog,
|
||
katalogi_probovali: katalogi,
|
||
sposob: 'каталог есть, стенограмм в нём нет',
|
||
stroka: `Стенограммы работника нет: каталог под неё заведён, но пуст. Смотреть здесь: ${katalog}`,
|
||
};
|
||
}
|
||
|
||
// ━━━ СВЕРКА ПО СЛОВАМ: чья это стенограмма ━━━
|
||
// Не отсев, а разметка: «нет» никого не выбрасывает молча, оно ГОВОРИТСЯ владельцу.
|
||
// Молча выбросить единственную улику по своей же догадке было бы хуже, чем показать
|
||
// её с оговоркой.
|
||
for (const f of otobrannye) f.pohozhe = poruchenie ? pohozhaLiNaRabotnika(f.put, poruchenie) : null;
|
||
const svoi = otobrannye.filter(f => f.pohozhe === true);
|
||
if (svoi.length === 1 && otobrannye.length > 1) {
|
||
// 🔴 Ровно ради этого сверка и заведена: было несколько, по словам сошлась одна.
|
||
return {
|
||
naydeno: 1,
|
||
put: svoi[0].put,
|
||
kandidaty: otobrannye,
|
||
katalog,
|
||
katalogi_probovali: katalogi,
|
||
sposob: 'опознана по словам поручения',
|
||
stroka: `Стенограмма работника: ${svoi[0].put} (опознана по первым словам его поручения; `
|
||
+ `рядом в том же каталоге лежит ещё ${otobrannye.length - 1} — чужих)`,
|
||
};
|
||
}
|
||
|
||
if (otobrannye.length === 1) {
|
||
const odna = otobrannye[0];
|
||
const sposob = poVremeni ? 'единственная за время прогона' : 'единственная в каталоге работника';
|
||
if (odna.pohozhe === false) {
|
||
// 🔴🔴 САМОЕ ВРЕДНОЕ, ЧТО ЗДЕСЬ МОЖНО СДЕЛАТЬ, — промолчать. Файл ровно один, и
|
||
// соблазн доложить «вот она» велик, но слова в ней не работниковы. Говорим прямо.
|
||
return {
|
||
naydeno: 1,
|
||
put: odna.put,
|
||
kandidaty: otobrannye,
|
||
katalog,
|
||
katalogi_probovali: katalogi,
|
||
sposob: `${sposob}, но по словам НЕ ЕГО`,
|
||
stroka: `🔴 В каталоге работника лежит одна стенограмма — ${odna.put} — но начинается она НЕ `
|
||
+ 'с его поручения. Похоже, это запись человека, заглянувшего в тот же угол, а не работника. '
|
||
+ 'Смотреть можно, но искать в ней причину смерти работника — скорее всего впустую.',
|
||
};
|
||
}
|
||
return {
|
||
naydeno: 1,
|
||
put: odna.put,
|
||
kandidaty: otobrannye,
|
||
katalog,
|
||
katalogi_probovali: katalogi,
|
||
sposob: odna.pohozhe === true ? `${sposob}, слова сошлись` : sposob,
|
||
stroka: `Стенограмма работника: ${odna.put}`,
|
||
};
|
||
}
|
||
|
||
return {
|
||
naydeno: otobrannye.length,
|
||
put: null, // 🔴 выбрать за владельца нельзя — см. ловушку «в углу работал ещё и человек»
|
||
kandidaty: otobrannye,
|
||
katalog,
|
||
katalogi_probovali: katalogi,
|
||
sposob: 'несколько — выбрать нельзя',
|
||
stroka: `Стенограмм у этого работника ${otobrannye.length}, и какая его — отсюда не видно `
|
||
+ '(в том же углу мог работать человек или работника перезапускали). Смотреть все, свежая первой: '
|
||
+ otobrannye.map(f => f.put).join(' · '),
|
||
};
|
||
}
|
||
|
||
// ━━━ ВХОД ДЛЯ ЖИВОГО ХОЗЯЙСТВА: по карточке работника ━━━
|
||
// Карточка `.night/runs/<прогон>/workers/<работник>/worker.json` уже несёт ОБА нужных
|
||
// слова — `workspace` и `session_id`, — и время прогона в придачу. То есть заводить своего
|
||
// нового следа под это не нужно ни одного: всё уже записано, просто никто не складывал
|
||
// одно с другим.
|
||
export function stenogrammaPoKartochke(putKartochki, { home = homedir() } = {}) {
|
||
let card;
|
||
try {
|
||
card = JSON.parse(readFileSync(putKartochki, 'utf8'));
|
||
} catch (e) {
|
||
return {
|
||
naydeno: 0,
|
||
put: null,
|
||
kandidaty: [],
|
||
katalog: null,
|
||
katalogi_probovali: [],
|
||
sposob: 'карточка работника не прочлась',
|
||
stroka: `Карточку работника прочесть не вышло (${putKartochki}): ${e.message}`,
|
||
};
|
||
}
|
||
return naytiStenogrammu({
|
||
rabochiyUgol: card.workspace,
|
||
sessionId: card.session_id ?? null,
|
||
home,
|
||
ot: Number.isFinite(card.started_at) ? card.started_at : null,
|
||
do_: Number.isFinite(card.finished_at) ? card.finished_at : null,
|
||
// 🔴 Поручение — то, чем стенограмма опознаётся как ЕГО, а не человека, заглянувшего
|
||
// в тот же угол. В карточке оно уже лежит; своего нового следа под это не заводим.
|
||
poruchenie: typeof card.prompt === 'string' ? card.prompt : null,
|
||
});
|
||
}
|
||
|
||
// Строка для утренней сводки. Отдельной мелочью — чтобы тот, кто соберёт письмо владельцу,
|
||
// не сочинял свои слова заново и не потерял по дороге случаи «ноль» и «несколько».
|
||
export function strokaVladeltsu(nakhodka) {
|
||
if (!nakhodka) return 'Стенограмму работника не искали.';
|
||
return nakhodka.stroka;
|
||
}
|
||
|
||
// ━━━ РУЧНОЙ ВХОД: `node tools/night/stenogramma.mjs <путь>` ━━━
|
||
// Путь — либо карточка работника `…/worker.json`, либо прямо рабочий угол.
|
||
// Заведён затем, чтобы улика была достижима ПРЯМО СЕЙЧАС, не дожидаясь, пока её приладят
|
||
// в утреннее письмо (см. про последнюю милю в примечании у `isRealSuccess` в spawn.mjs).
|
||
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replace(/\\/g, '/')}`).href) {
|
||
const arg = process.argv[2];
|
||
if (!arg) {
|
||
console.log('Скажите путь: либо карточку работника (…/worker.json), либо его рабочий угол.');
|
||
process.exit(2);
|
||
}
|
||
const nakhodka = arg.toLowerCase().endsWith('.json') && existsSync(arg)
|
||
? stenogrammaPoKartochke(arg)
|
||
: naytiStenogrammu({ rabochiyUgol: arg });
|
||
console.log(nakhodka.stroka);
|
||
console.log(`(способ: ${nakhodka.sposob}; каталог: ${nakhodka.katalog ?? 'не найден'})`);
|
||
process.exit(nakhodka.naydeno === 1 ? 0 : 1);
|
||
}
|