- Зачем:
- сторож был зашит на 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>
19 KiB
Codex через прямой codex exec: рецепты и ловушки
Проверено 2026-07-09 (0.142.5, Windows + Git Bash) и 2026-07-12 (0.144.1,
WSL2 Ubuntu: exec/resume/-o/фоновый Bash — весь конвейер); ключи exec
и resume сверены 2026-08-03 на 0.145.0.
Конвейер ходит прямым exec, не плагином-обёрткой: у обёртки глючит
фоновая машина — молчаливая подмена анализа Кодекса своим (issue #158,
ломает слепое ревью) и зависшие статусы, — а её --write-запуски режет
классификатор авторежима. Сама обёртка (codex-companion.mjs в кэше
плагинов) остаётся для ручного интерактива: status, result <job-id>.
Далее $D = подпапка задачи в обменном каталоге прогона (определение и
конвенция имён — prompt-templates.md).
Сквозные правила (любая ветка)
-
Промпт — файлом, шум — в файлы. Промпт подавай через stdin (
-): вход этого вызова занят промптом и должен оставаться открытым — правило< /dev/nullдля фоновых команд сюда не относится (закроешь — получишьNo prompt provided via stdin). stdout/stderr — редиректами в$SCRATCH. В контекст оркестратора читается ровно один файл — финальный отчёт из-o. Промпт собирай конкатенацией, а не переписывай: скелеты — в prompt-templates.md, Write'ом пиши только параметрический блок задачи; чужой контент (вердикты ревью, отчёты) в промпт попадает черезcat a.md b.md > prompt.md— дословно и без повторного прохода через твой контекст (2026-07-10: переписывание гоняло находки через оркестратора до шести раз). -
Модель и глубину рассуждения задавай явно (
-m,-c 'model_reasoning_effort="..."'): дефолты берутся из~/.codex/config.tomlи плавают за последней интерактивной сессией пользователя — из конфига не наследовать, при расхождении с правилом — спроси пользователя. Боевая модель —gpt-5.6-sol(решение пользователя 2026-07-12), она хороша и для анализа/ревью, не только для кода. Ступень — по цене тихой ошибки этапа (model-matrix.md), не по сложности:medium— дефолт для обычных задач (типовая реализация, механика, узкие починки по готовому диагнозу);high— ревью кода, диагноз, нетривиальная или рискованная реализация;xhighне дефолтить: расход удваивается, а выигрыш мал — только по явному запросу пользователя. Реализация по отревьюированному самодостаточному тикету идёт наmediumи при нетривиальной задаче — просадку ловит конвейер (решение пользователя 2026-07-21); но экономию считай по конвейеру целиком, а не по этапу: триаж лишних находок съедает её без остатка. Оверрайды проверены под--strict-config. -
Запуск — собственным фоновым Bash оркестратора (
run_in_background): фоновый процесс детачится, живёт сквозь ходы и будит оркестратора по выходу. Потолок 10 минут — только у фронтграундного Bash; фоновому таймаут не нужен,timeout NNNвокруг codex не ставь.&внутрь команды не добавляй — фон даёт харнесс; с&уведомление придёт от обёртки при живом codex (2026-07-12). Осиротевший процесс подбирает ожидатель:while kill -0 <pid>; do sleep 10; doneподrun_in_backgroundбудит по его реальному выходу. -
О зависании фоновый запуск молчит: он будит по выходу, а повисший тред не выходит — живой процесс и работающий процесс снаружи неразличимы. Признак жизни — рост JSONL-лога, а не статус (2026-07-07). Долгий прогон страхуй сторожем — вторым фоновым Bash, который выходит либо когда появился
-o-отчёт, либо когда лог перестал расти. Сторож ходит по артефактам, а не по pid: фоновый Bash отдаёт id задачи харнесса, а pid процесса тебе неоткуда взять. Пара файлов — от той ветки, которую страхуешь (у ревьюreview-impl-*, у починкиfix-*): подставь их в первую строку, иначе сторож проверит чужой отчёт, найдёт его на месте и выйдет, не сделав ни одного тика.REPORT="$D/task-result.md"; LOG="$D/task-stdout.jsonl" # ветка исполнителя prev=-1 while [ ! -s "$REPORT" ]; do sleep 300 [ -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в части дистрибутивов) ругается в stderr, а в подстановку команды отдаёт пустой stdout — ошибка проглотится, и сторож закричит на первом же тике.NOLOG— путь неверен или запуск не состоялся: редирект создаёт лог сразу, так что через тик его не быть не может.STALLED— повод посмотреть, а не вердикт: длинная команда внутри песочницы (сборка, тесты) событий не пишет. Разбудил — прочти последнюю строку JSONL и решай; решил, что жив, — перезапусти сторож: выходом из цикла он страховку снял. -
thread_id — в первой строке stdout-JSONL (
{"type":"thread.started",...}). Забирай сразу после старта: это страховка — если процесс умер или его прибил хост, тред жив:codex exec resume <id> «дай финальный отчёт». -
resume — только по явному id. Безадресный resume цепляет последний тред сессии — при параллельных тредах подхватит чужой (например, ревью-тред вместо треда реализации).
-
Кросс-платформенно. Рецепты — POSIX под Git Bash (2026-07-09:
codex,jq, редиректы под Windows работают).$SCRATCHзадавай forward-slash-путём (/c/Users/...), неC:\...; пути в аргументах кавычь; в выводах под Windows возможны\r. -
Linux: sandbox может молча сломаться обновлением Codex. Сигнатура: web_search работает, а
pwdвнутри песочницы — нет (bwrap: loopback: Failed RTM_NEWADDR). Обход без sudouse_legacy_landlock=trueчинит только read-only, write-режим с ним паникует; полный фикс — AppArmor-профиль, и он под sudo: сам не применяй, дай пользователю готовые команды из codex-sandbox-apparmor.md (!-префикс в чат), после применения профиля напомни убратьuse_legacy_landlock. Перед первым прогоном конвейера на новой Linux-машине прогони оттуда же диагностику превентивно. -
Блок классификатора — сначала сузь песочницу, потом зови пользователя. Первая проверка: нет ли в команде
-s danger-full-access— классификатор его блокирует, и он не нужен (почему — в ветке исполнителя). Если блок не из-за песочницы — дай пользователю готовую команду с префиксом!в чат или попроси сменить режим разрешений. Сработавший путь — прямой exec; companion с--writeблокируется и напрямую, и через раннера.
Ветка «исполнитель» (пишет код)
cat "$D/task-prompt.md" | codex exec --json --strict-config \
-s workspace-write -m gpt-5.6-sol \
-c 'model_reasoning_effort="high"' \
-c 'approval_policy="on-request"' -c 'approvals_reviewer="auto_review"' \
-o "$D/task-result.md" - \
> "$D/task-stdout.jsonl" 2> "$D/task-stderr.txt"
- Эффорт в примере —
high(сложная задача); обычной задаче ставьmediumпо сквозному правилу выше. --strict-config— не про песочницу, а про опечатку. С ним нераспознанный-c-ключ валит запуск громко; без него он проглатывается молча, и ты едешь на дефолте вместо заданной ступени. Заодно strict-config принимаетapprovals_reviewer, которого нет в--help: связка сapproval_policy="on-request"отработала боевой прогон 2026-07-09.- Право записи + авто-ревьюер аппрувов. Скоуп ограничивай промптом («коммит не делай», границы из файла задачи), не песочницей — жёсткий read-only лишает исполнителя тестов и поиска.
- Шире тоже не бери.
-s danger-full-accessблокируется классификатором авторежима и не нужен: под штатной связкой из рецепта у Codex прошли и сеть (docker manifest inspect), и docker-сокет (make up,docker compose down) — эскалацией через авто-аппрув (боевой прогон 2026-07-30, тикет #12 v2). Отдельных событий эскалации в--json-логе при этом нет: успешные команды выглядят обычнымиcompleted— не ищи их там (история вопроса — ../handoffs/20260710-codex-revyuer-write-i-sliyanie-handoffov.md). - Промпт: путь к файлу задачи; мандат — в этапе 3 SKILL.md.
- До старта сними baseline
git status --porcelain; после — сверь отчёт Кодекса с деревом: правок, которых он не называл, быть не должно.
Ветка «ревьюер» (свежий тред, мандат промптом)
# линия дефектов этапа 4; линия уместности и этап 2 — тот же вызов,
# другой промпт и другой вердикт (имена — prompt-templates.md)
cat "$D/review-prompt-code.md" | codex exec --json --strict-config \
-s workspace-write -m gpt-5.6-sol \
-c 'model_reasoning_effort="high"' \
-c 'approval_policy="on-request"' -c 'approvals_reviewer="auto_review"' \
-o "$D/review-impl-codex.md" - \
> "$D/review-impl-stdout.jsonl" 2> "$D/review-impl-stderr.txt"
- Свежим тредом, не тем, что писал код. Вызов тот же, что у исполнителя: запись + авто-аппрув.
- Эффорт
high— дефолт ревью; ниже опускай только по явной просьбе пользователя. - Read-only-песочницей ревьюера не режь: она отнимает у него руки — тесты,
проверку гипотез, поиск, документацию, первоисточники. Защита — мандат в
промпте, готовым файлом
prompts/review-boilerplate.md (сборка —
prompt-templates.md); Codex держит его надёжно
(практика пользователя, 2026-07-10). Дешёвая подстраховка:
git status --porcelainдо ревью и сверка после — дерево должно остаться нетронутым. - Вердикт без находок — проверь лог: работа или отписка. Дёшево:
grep -c '"command_execution"' "$D/review-impl-stdout.jsonl"(счётчик выполненных команд) или промежуточные сообщения —jq -r 'select(.item.type=="agent_message") | .item.text' "$D/review-impl-stdout.jsonl". Одна строка APPROVED при пустом счётчике — отписка, перезапускай. -o-файл вердикта — и есть обменный артефакт: на триаже он уходит в fix-промпт конкатенацией, читай его один раз для синтеза и не пересказывай.
Ветка «триаж/починка» (тем же тредом, что писал код)
# resume продолжает тред: среда (-s, -C) задана при создании, не повторяй.
cat "$D/fix-prompt.md" | codex exec resume --json --strict-config <thread-id> \
-o "$D/fix-result.md" - \
> "$D/fix-stdout.jsonl" 2> "$D/fix-stderr.txt"
resumeпродолжает тред, а не заводит новый — среда принадлежит треду, не ходу. Песочница и CWD заданы при создании и на продолжении не переуказываются: ключей-sи-Cуresumeпросто нет, дописанные по образцуexecони даютexit 2, unexpected argument. Write-тред остаётся write. Ручки хода остаются:-m(модель),-c model_reasoning_effort=…,-o,--json,--strict-config.-c sandbox_mode=…разбирается без ошибки, но среду продолжения не меняет — не рассчитывай на него. Ключи сверены 2026-08-03 на codex-cli 0.145.0.- Смена эффорта внутри треда кэш не сбрасывает (доля кэша держалась
~93–94% при high→medium, прогоны 06/03/04) — дорогое ревью на
highи дешёвая перепроверка наmediumв одном треде работают как задумано. - Промпт — индекс находок дословно + «закрой перечисленное, больше ничего не меняй; отклонение — только аргументированно».
Раннер-субагент: когда результат надо переварить
Для запуска раннер не нужен (см. сквозные правила) — он нужен, когда сырой
результат шумный и его стоит провалидировать или сжать до индекса, прежде
чем показывать оркестратору. Тогда раннеру (fast-worker; нет в
репозитории — general-purpose) отдай готовую команду и правила
разбора, не постановку задачи: тонкий агент, получив постановку, реализует
её сам (казус форвардера, lessons-archive 2026-07-06).
Свой 10-минутный потолок на синхронный Bash раннер обходит тем же приёмом,
что и оркестратор, — отслеживаемым ожиданием: команда запускается его
собственным фоновым Bash, а ждёт он процессом-ожидателем
(until grep -q '<маркер>' лог; do sleep 30; done под run_in_background),
и харнесс переинвоцирует его по выходу процесса. «Жду уведомления» без
отслеживаемого процесса не будит никогда, а голый поллер вне
run_in_background умирает при смене хода (2026-07-07) — живучесть даёт
именно фоновый запуск.