diff --git a/agents/codex.md b/agents/codex.md index 667bc0a..1aa9dd2 100644 --- a/agents/codex.md +++ b/agents/codex.md @@ -25,7 +25,7 @@ It writes to job_dir: `status` (ok|stalled|error), `thread_id`, `last.md` 1. `JOB=$(mktemp -d /tmp/codex-job-XXXXXX)` — remember the path. 2. Write `$JOB/prompt.md`: transport preamble (below) + the task for codex. If the task contains a block between markers `=== TASK FOR CODEX ===` and - `=== END TASK ===` (legacy Russian `=== ЗАДАНИЕ ДЛЯ CODEX ===` / + `=== END TASK ===` (legacy compat: `=== ЗАДАНИЕ ДЛЯ CODEX ===` / `=== КОНЕЦ ЗАДАНИЯ ===` also counts) — only the block content goes into prompt.md (don't copy the markers); text outside the block is instructions for you, codex never sees it. No markers — the orchestrator's whole task @@ -61,36 +61,8 @@ It writes to job_dir: `status` (ok|stalled|error), `thread_id`, `last.md` Do NOT end your turn until status appears — the result must reach the orchestrator in your final message. -## Tools check (before the first launch) - -Task mentions specific tools (`mcp__X__*`, "via MCP X", Claude tool names)? -Check against codex reality BEFORE launching: - -- MCP servers — two layers: - 1. Global: `codex mcp list` (only `~/.codex/config.toml`; project layers - are NOT shown by this command). - 2. Project: grep `[mcp_servers.]` across config layers relative to - cwd — `/config.toml`, `.codex/config.toml` up the tree to the root, - `$(git -C rev-parse --show-toplevel)/.codex/config.toml`. - Found → the server is available (if the project is trusted; codex - disables untrusted layers). - Built-in codex capabilities: shell (always), file editing/apply_patch - (always), browser_use. Unsure about a feature — `codex features list`. -- A mentioned tool is missing but an equivalent exists → replace its mention - in prompt.md with the equivalent, leave the rest of the task untouched: - -| In the task | In codex | -|---|---| -| Bash, shell commands | shell — always available | -| Read / Write / Edit / Grep / Glob | shell + apply_patch — always available | -| WebSearch / WebFetch | browser_use or curl via shell | -| `mcp__X__*` | server `X` from `codex mcp list` | -| Agent / subagents / Task | NOT available in codex — blocker | - -- Tool missing, no replacement, and the task can't be done without it → do - NOT launch codex. Return a blocker: "codex has no ; available: - ; rephrase or provide another path". Let the orchestrator - decide. +## Capabilities +Codex has `shell` and `apply_patch` always, `browser_use` stable. MCP servers if configured (`codex mcp list`). No guard — codex decides how to fulfill the task. ## Collecting the result (`$JOB/status` appeared) @@ -149,6 +121,7 @@ yourself. Common ones: | Reasoning effort | `-c model_reasoning_effort="minimal|low|medium|high|xhigh"` | | Permissions | `-s read-only` / `-s workspace-write` (default: full access from user config) | | Extra write dirs | `--add-dir ` | +| Bypass | `--dangerously-bypass-approvals-and-sandbox` — EXTREMELY DANGEROUS — only in externally sandboxed envs | | Images | `-i ` | | Structured answer | `--output-schema ` | | Any config key | `-c key=value` | @@ -156,3 +129,5 @@ yourself. Common ones: Never: `--ephemeral` (breaks resume). Long task expected — `CODEX_STALL_TIMEOUT=` before the command (default 600 — output stagnation, not total time; touch rarely). + +Flags from `codex options:` are saved to `$JOB/codex-flags` (NUL-delimited) and re-applied on `resume` so permissions persist. diff --git a/scripts/codex-run.sh b/scripts/codex-run.sh index 119c480..86517cc 100755 --- a/scripts/codex-run.sh +++ b/scripts/codex-run.sh @@ -2,9 +2,9 @@ # codex-crew: deterministic wrapper around `codex exec` with stall watchdog. # # Usage: -# codex-run.sh exec [codex-flags...] [prompt] -# codex-run.sh resume [codex-flags...] [prompt] -# codex-run.sh review [--base X|--commit Y|--uncommitted] [codex-flags...] [prompt] +# codex-run.sh exec [codex-flags...] +# codex-run.sh resume [codex-flags...] +# codex-run.sh review [--base X|--commit Y|--uncommitted] [codex-flags...] # # If /prompt.md is non-empty it is always fed via stdin ('-'); codex-flags # stay positional, so options and a file prompt combine freely. @@ -31,14 +31,14 @@ PF=$JOB_DIR/prompt.md STALL=${CODEX_STALL_TIMEOUT:-600} TICK=${CODEX_WATCH_INTERVAL:-15} BIN=${CODEX_BIN:-codex} +FLAGS_FILE="$JOB_DIR/codex-flags" +build_cmd() { local -a f=("$@"); CMD=("$BIN" "${f[@]}" exec --json --skip-git-repo-check -o "$LAST"); } case $MODE in - exec) CMD=("$BIN" exec --json --skip-git-repo-check -o "$LAST") ;; - resume) TID=$1; shift; printf '%s\n' "$TID" > "$JOB_DIR/thread_id" - CMD=("$BIN" exec resume "$TID" --json --skip-git-repo-check -o "$LAST") ;; + exec) if (( $# )); then printf '%s\0' "$@" > "$FLAGS_FILE"; else : > "$FLAGS_FILE"; fi + build_cmd "$@" ;; review) if [[ -s $JOB_DIR/prompt.md ]]; then - # scope flags conflict with a prompt at the CLI level — move scope into text SCOPE_DESC=""; REST=() while [[ $# -gt 0 ]]; do case $1 in @@ -48,22 +48,32 @@ case $MODE in *) REST+=("$1"); shift ;; esac done + if (( ${#REST[@]} )); then printf '%s\0' "${REST[@]}" > "$FLAGS_FILE"; else : > "$FLAGS_FILE"; fi + set -- "${REST[@]}" if [[ -n $SCOPE_DESC ]]; then PF=$JOB_DIR/prompt.final.md { printf 'This is a code review task. %s\n\n' "$SCOPE_DESC"; cat "$JOB_DIR/prompt.md"; } > "$PF" fi - set -- ${REST[@]+"${REST[@]}"} - fi - CMD=("$BIN" exec review --json --skip-git-repo-check -o "$LAST") ;; + CMD=("$BIN" "$@" exec review --json --skip-git-repo-check -o "$LAST") + else + SCOPE_ARGS=(); REST=() + while [[ $# -gt 0 ]]; do case $1 in --base) SCOPE_ARGS=(--base "$2"); shift 2;; --commit) SCOPE_ARGS=(--commit "$2"); shift 2;; --uncommitted) SCOPE_ARGS=(--uncommitted); shift;; *) REST+=("$1"); shift;; esac; done + if (( ${#REST[@]} )); then printf '%s\0' "${REST[@]}" > "$FLAGS_FILE"; else : > "$FLAGS_FILE"; fi + CMD=("$BIN" "${REST[@]}" exec review "${SCOPE_ARGS[@]}" --json --skip-git-repo-check -o "$LAST") + fi ;; + resume) TID=$1; shift + printf '%s\n' "$TID" > "$JOB_DIR/thread_id" + if [[ -s $FLAGS_FILE ]]; then mapfile -d '' -t SAVED < "$FLAGS_FILE"; set -- "${SAVED[@]}" "$@"; fi + CMD=("$BIN" "$@" exec resume "$TID" --json --skip-git-repo-check -o "$LAST") ;; *) echo "usage: codex-run.sh exec|resume|review ..." >&2; exit 2 ;; esac rm -f "$JOB_DIR/status" "$LAST"; : > "$EV"; : > "$ERR" if [[ -s $PF ]]; then - "${CMD[@]}" "$@" - < "$PF" > "$EV" 2> "$ERR" & + "${CMD[@]}" - < "$PF" > "$EV" 2> "$ERR" & else - "${CMD[@]}" "$@" < /dev/null > "$EV" 2> "$ERR" & + "${CMD[@]}" < /dev/null > "$EV" 2> "$ERR" & fi PID=$! diff --git a/tests/test-run.sh b/tests/test-run.sh index c52c5bd..3f05939 100755 --- a/tests/test-run.sh +++ b/tests/test-run.sh @@ -6,14 +6,16 @@ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT # 1. happy path -> status ok, thread_id captured, exit 0 -CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/ok" exec "hello" +mkdir -p "$TMP/ok"; echo "hello" > "$TMP/ok/prompt.md" +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/ok" exec [[ $(cat "$TMP/ok/status") == ok ]] [[ $(cat "$TMP/ok/thread_id") == test-thread-123 ]] -[[ $(cat "$TMP/ok/last.md") == "stub answer|args="*"hello|" ]] +[[ $(cat "$TMP/ok/last.md") == "stub answer|args="*" -|hello" ]] # 2. stall -> watchdog kills, status stalled, exit nonzero +mkdir -p "$TMP/hang"; echo "hello" > "$TMP/hang/prompt.md" if CODEX_BIN=$HERE/stub-hang/codex CODEX_STALL_TIMEOUT=4 CODEX_WATCH_INTERVAL=2 \ - "$RUN" "$TMP/hang" exec "hello"; then + "$RUN" "$TMP/hang" exec; then echo "FAIL: stall run must exit nonzero"; exit 1 fi [[ $(cat "$TMP/hang/status") == stalled ]] @@ -29,7 +31,8 @@ CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/stdin" exec mkdir -p "$TMP/flags"; echo "prompt with flags" > "$TMP/flags/prompt.md" CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/flags" exec -s read-only [[ $(cat "$TMP/flags/status") == ok ]] -[[ $(cat "$TMP/flags/last.md") == "stub answer|args="*"-s read-only -|prompt with flags" ]] +[[ $(cat "$TMP/flags/last.md") == *"-s read-only exec"* ]] +[[ $(cat "$TMP/flags/last.md") == *" -|prompt with flags" ]] # 5. review + scope + prompt.md -> scope translated into prompt text, no scope flag in args mkdir -p "$TMP/rev"; echo "focus: correctness" > "$TMP/rev/prompt.md" @@ -46,7 +49,8 @@ LAST5=$(cat "$TMP/rev/last.md") mkdir -p "$TMP/rev-native" CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/rev-native" review --uncommitted [[ $(cat "$TMP/rev-native/status") == ok ]] -[[ $(cat "$TMP/rev-native/last.md") == "stub answer|args=exec review"*"--uncommitted|" ]] +[[ $(cat "$TMP/rev-native/last.md") == "stub answer|args=exec review"* ]] +[[ $(cat "$TMP/rev-native/last.md") == *"--uncommitted"* ]] # 7. review + prompt.md, no scope -> custom instructions pass through untouched mkdir -p "$TMP/rev-custom"; echo "review whatever you find" > "$TMP/rev-custom/prompt.md" @@ -54,4 +58,50 @@ CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/rev-custom" review [[ $(cat "$TMP/rev-custom/status") == ok ]] [[ $(cat "$TMP/rev-custom/last.md") == "stub answer|args=exec review"*"-|review whatever you find" ]] +# 8. exec -s read-only then resume reuses -s read-only before exec (hoisted) +mkdir -p "$TMP/persist"; echo "task persist" > "$TMP/persist/prompt.md" +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/persist" exec -s read-only +[[ $(cat "$TMP/persist/status") == ok ]] +[[ $(cat "$TMP/persist/last.md") == *"-s read-only exec"* ]] +# resume should hoist saved flags before exec +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/persist" resume "$(cat "$TMP/persist/thread_id")" +[[ $(cat "$TMP/persist/status") == ok ]] +RESUME8=$(cat "$TMP/persist/last.md") +[[ $RESUME8 == *"-s read-only exec resume"* ]] || { echo "FAIL case 8 hoist: $RESUME8"; exit 1; } +[[ $RESUME8 == *" -|task persist"* ]] || { echo "FAIL case 8 prompt: $RESUME8"; exit 1; } + +# 9. exec -m gpt-5.5 --add-dir "/tmp/with spaces" NUL preserves space; resume proves persistence +mkdir -p "$TMP/spaces"; echo "task spaces" > "$TMP/spaces/prompt.md" +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/spaces" exec -m gpt-5.5 --add-dir "/tmp/with spaces" +[[ $(cat "$TMP/spaces/status") == ok ]] +[[ $(cat "$TMP/spaces/last.md") == *"-m gpt-5.5 --add-dir /tmp/with spaces"* ]] +# verify NUL storage preserves space as single entry +mapfile -d '' -t CHK < "$TMP/spaces/codex-flags" +[[ ${#CHK[@]} -eq 4 ]] || { echo "FAIL case 9 NUL count ${#CHK[@]}: ${CHK[*]}"; exit 1; } +[[ ${CHK[0]} == "-m" ]] || { echo "FAIL case 9 flag0 ${CHK[0]}"; exit 1; } +[[ ${CHK[2]} == "--add-dir" ]] || { echo "FAIL case 9 flag2 ${CHK[2]}"; exit 1; } +[[ ${CHK[3]} == "/tmp/with spaces" ]] || { echo "FAIL case 9 space preserved got '${CHK[3]}'"; exit 1; } +# resume should contain same spaced arg as single entry +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/spaces" resume "$(cat "$TMP/spaces/thread_id")" +[[ $(cat "$TMP/spaces/status") == ok ]] +RESUME9=$(cat "$TMP/spaces/last.md") +[[ $RESUME9 == *"-m gpt-5.5 --add-dir /tmp/with spaces exec resume"* ]] || { echo "FAIL case 9 resume hoist: $RESUME9"; exit 1; } + +# 10. review --base main -s read-only then resume keeps -s read-only before exec +mkdir -p "$TMP/rev-persist"; echo "review persist" > "$TMP/rev-persist/prompt.md" +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/rev-persist" review --base main -s read-only +[[ $(cat "$TMP/rev-persist/status") == ok ]] +LAST10=$(cat "$TMP/rev-persist/last.md") +[[ $LAST10 == *"-s read-only exec review"* ]] || { echo "FAIL case 10 exec hoist: $LAST10"; exit 1; } +[[ $LAST10 == *"against base ref 'main'"* ]] || { echo "FAIL case 10 scope text: $LAST10"; exit 1; } +# flags file should contain only -s read-only, scope translated to prompt +mapfile -d '' -t CHK10 < "$TMP/rev-persist/codex-flags" +[[ ${#CHK10[@]} -eq 2 ]] || { echo "FAIL case 10 flags count ${#CHK10[@]}: ${CHK10[*]}"; exit 1; } +[[ ${CHK10[0]} == "-s" && ${CHK10[1]} == "read-only" ]] || { echo "FAIL case 10 flags ${CHK10[*]}"; exit 1; } +# resume keeps flag +CODEX_BIN=$HERE/stub-ok/codex "$RUN" "$TMP/rev-persist" resume "$(cat "$TMP/rev-persist/thread_id")" +[[ $(cat "$TMP/rev-persist/status") == ok ]] +RESUME10=$(cat "$TMP/rev-persist/last.md") +[[ $RESUME10 == *"-s read-only exec resume"* ]] || { echo "FAIL case 10 resume: $RESUME10"; exit 1; } + echo "ALL PASS"