fix(skills): сторож зависшего треда развязан с веткой исполнителя

- Зачем:
  - сторож был зашит на task-result.md и task-stdout.jsonl, то есть на
    артефакты этапа 3. На ревью и на починке отчёт исполнителя уже лежит
    рядом, цикл не делал ни одного тика и выходил молча — страховка
    отсутствовала ровно там, где прогон длиннее всего.
  - отсутствующий лог был неотличим от неростущего: `|| echo 0` выдавал
    ноль два тика подряд, и неверный путь читался как застой.
- Что:
  - пара файлов вынесена в переменные REPORT и LOG первой строкой, с
    указанием подставлять их от страхуемой ветки.
  - отсутствие лога стало отдельным сигналом NOLOG (редирект создаёт лог
    сразу, так что через тик его не быть не может).
  - дописано, что выход из цикла снимает страховку: после STALLED сторож
    перезапускают руками.
  - поправлено описание ловушки bfs: он ругается в stderr, а пустым
    отдаёт только stdout — «молча» было неверно.
- Проверка:
  - симуляция четырёх исходов: штатное завершение, зависание, неверный
    путь к логу (NOLOG) и ветка ревью при лежащем рядом отчёте этапа 3 —
    последняя как раз и не работала до правки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dmitry DementievandClaude Opus 5 committed 2026-08-04 00:14:23 +03:00
1 parent 05336325db
commit 78ee8bfa48
1 file changed
+22 -15
@@ -52,33 +52,40 @@ WSL2 Ubuntu: exec/resume/`-o`/фоновый Bash — весь конвейер)
его реальному выходу.
- **О зависании фоновый запуск молчит**: он будит по выходу, а повисший тред
не выходит — живой процесс и работающий процесс снаружи неразличимы.
Признак жизни — рост `stdout.jsonl`, а не статус (2026-07-07). Долгий
прогон страхуй сторожем — вторым фоновым Bash, который выходит либо когда
Признак жизни — рост JSONL-лога, а не статус (2026-07-07). Долгий прогон
страхуй сторожем — вторым фоновым Bash, который выходит либо когда
появился `-o`-отчёт, либо когда лог перестал расти. Сторож ходит по
артефактам, а не по pid: фоновый Bash отдаёт id задачи харнесса, а pid
процесса тебе неоткуда взять.
процесса тебе неоткуда взять. Пара файлов — **от той ветки, которую
страхуешь** (у ревью `review-impl-*`, у починки `fix-*`): подставь их в
первую строку, иначе сторож проверит чужой отчёт, найдёт его на месте и
выйдет, не сделав ни одного тика.
```bash
REPORT="$D/task-result.md"; LOG="$D/task-stdout.jsonl" # ветка исполнителя
prev=-1
while [ ! -s "$D/task-result.md" ]; do
while [ ! -s "$REPORT" ]; do
sleep 300
[ -s "$D/task-result.md" ] && break
cur=$(wc -c < "$D/task-stdout.jsonl" 2>/dev/null || echo 0)
[ -s "$REPORT" ] && break
[ -e "$LOG" ] || { echo NOLOG; break; }
cur=$(wc -c < "$LOG")
[ "$cur" = "$prev" ] && { echo STALLED; break; }
prev=$cur
done
```
Две ловушки, обе проверены на симуляции 2026-08-03. Выход проверяй **и
после `sleep`** — иначе сторож объявит застой ровно в тот тик, когда
Кодекс дописывал отчёт. И не переводи признак жизни на относительные
метки времени: `find -newermt '-10 minutes'` под `bfs` (подмена `find` в
части дистрибутивов) молча отдаёт пустой stdout — сторож закричит на
первом же тике.
Проверено симуляцией 2026-08-03: выход проверяй **и после `sleep`**, иначе
сторож объявит застой в тот самый тик, когда Кодекс дописывал отчёт. И не
переводи признак жизни на относительные метки времени:
`find -newermt '-10 minutes'` под `bfs` (подмена `find` в части
дистрибутивов) ругается в stderr, а в подстановку команды отдаёт пустой
stdout — ошибка проглотится, и сторож закричит на первом же тике.
`STALLED` — повод посмотреть, а не вердикт: длинная команда внутри
песочницы (сборка, тесты) событий не пишет. Разбудил — прочти последнюю
строку JSONL и решай сам.
`NOLOG` — путь неверен или запуск не состоялся: редирект создаёт лог сразу,
так что через тик его не быть не может. `STALLED` — повод посмотреть, а не
вердикт: длинная команда внутри песочницы (сборка, тесты) событий не пишет.
Разбудил — прочти последнюю строку JSONL и решай; решил, что жив, —
перезапусти сторож: выходом из цикла он страховку снял.
- **thread_id — в первой строке stdout-JSONL** (`{"type":"thread.started",...}`).
Забирай сразу после старта: это страховка — если процесс умер или его
прибил хост, тред жив: `codex exec resume <id> «дай финальный отчёт»`.