- Зачем:
- сторож был зашит на 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>
223 lines
19 KiB
Markdown
223 lines
19 KiB
Markdown
# 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](prompt-templates.md)).
|
||
|
||
## Сквозные правила (любая ветка)
|
||
|
||
- **Промпт — файлом, шум — в файлы.** Промпт подавай через stdin (`-`): вход
|
||
этого вызова занят промптом и должен оставаться открытым — правило
|
||
`< /dev/null` для фоновых команд сюда не относится (закроешь — получишь
|
||
`No prompt provided via stdin`). stdout/stderr — редиректами в `$SCRATCH`.
|
||
В контекст оркестратора читается ровно один файл — финальный отчёт из
|
||
`-o`. Промпт **собирай конкатенацией, а не переписывай**: скелеты — в
|
||
[prompt-templates.md](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](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-*`): подставь их в
|
||
первую строку, иначе сторож проверит чужой отчёт, найдёт его на месте и
|
||
выйдет, не сделав ни одного тика.
|
||
|
||
```bash
|
||
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`). Обход без sudo
|
||
`use_legacy_landlock=true` чинит только read-only, write-режим с ним
|
||
паникует; полный фикс — AppArmor-профиль, и он под sudo: сам не применяй,
|
||
дай пользователю готовые команды из
|
||
[codex-sandbox-apparmor.md](codex-sandbox-apparmor.md) (`!`-префикс в чат),
|
||
после применения профиля напомни убрать `use_legacy_landlock`. Перед первым
|
||
прогоном конвейера на новой Linux-машине прогони оттуда же диагностику
|
||
превентивно.
|
||
- **Блок классификатора — сначала сузь песочницу, потом зови пользователя.**
|
||
Первая проверка: нет ли в команде `-s danger-full-access` — классификатор
|
||
его блокирует, и он не нужен (почему — в ветке исполнителя). Если блок не
|
||
из-за песочницы — дай пользователю готовую команду с префиксом `!` в чат
|
||
или попроси сменить режим разрешений. Сработавший путь — прямой exec;
|
||
companion с `--write` блокируется и напрямую, и через раннера.
|
||
|
||
## Ветка «исполнитель» (пишет код)
|
||
|
||
```bash
|
||
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](../handoffs/20260710-codex-revyuer-write-i-sliyanie-handoffov.md)).
|
||
- Промпт: путь к файлу задачи; мандат — в этапе 3 SKILL.md.
|
||
- До старта сними baseline `git status --porcelain`; после — сверь отчёт
|
||
Кодекса с деревом: правок, которых он не называл, быть не должно.
|
||
|
||
## Ветка «ревьюер» (свежий тред, мандат промптом)
|
||
|
||
```bash
|
||
# линия дефектов этапа 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](prompts/review-boilerplate.md) (сборка —
|
||
[prompt-templates.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-промпт конкатенацией, читай его один раз для синтеза и не пересказывай.
|
||
|
||
## Ветка «триаж/починка» (тем же тредом, что писал код)
|
||
|
||
```bash
|
||
# 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) — живучесть даёт
|
||
именно фоновый запуск.
|