@sabaiway/agent-workflow-kit 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@
4
4
  # Project-agnostic wrapper for the codex-cli-bridge skill. It encodes one fixed,
5
5
  # deterministic execution policy and prepends an ORCHESTRATOR EXECUTION CONTRACT
6
6
  # so codex never wastes a run rediscovering it. Codex reads the TARGET project's
7
- # Hard Constraints itself, from the root AGENTS.md in its working directory
8
- # (codex auto-reads AGENTS.md from cwd) — this wrapper hardcodes no project rules.
7
+ # Hard Constraints from the root AGENTS.md auto-merged into its context (root→cwd,
8
+ # truncated at project_doc_max_bytes) — this wrapper hardcodes no project rules.
9
9
  #
10
10
  # Fixed policy (single source of truth — passed via flags + --ignore-user-config,
11
11
  # so behaviour is deterministic regardless of ~/.codex/config.toml):
@@ -13,7 +13,17 @@
13
13
  # - network access OFF: new dependencies / network installs are done by a human
14
14
  # - approval_policy=never: there is no TTY in exec; anything needing escalation
15
15
  # is refused and reported, then handled by hand
16
- # - strongest model at maximum reasoning effort (override CODEX_MODEL/CODEX_EFFORT)
16
+ # - strongest model at maximum reasoning effort (quality-first — see below)
17
+ # - git WRITES are blocked by a physical shim (codex spawns git via execve, which
18
+ # bypasses shell functions) — the orchestrator owns the commit boundary.
19
+ #
20
+ # Quality-first (hard rule): delegated codex work ALWAYS runs on the frontier
21
+ # model at maximum reasoning effort. The defaults below are pinned and the wrapper
22
+ # REFUSES a non-default CODEX_MODEL/CODEX_EFFORT — knowingly-worse output is never
23
+ # traded for quota. The ONLY exception is a throwaway probe whose result does not
24
+ # depend on effort: set CODEX_PROBE=1 (echoed loudly) to relax the guard. Economy
25
+ # comes from quality-neutral waste removal (clean capture, a hard timeout, a lean
26
+ # prompt, resume instead of re-sending context), never from a downgrade.
17
27
  #
18
28
  # Auth: SUBSCRIPTION ONLY. Uses the cached ChatGPT login under CODEX_HOME
19
29
  # (~/.codex). The wrapper unsets every *_API_KEY plus OPENAI_BASE_URL and passes
@@ -23,14 +33,44 @@
23
33
  # Usage (installed on PATH as `codex-exec`):
24
34
  # codex-exec docs/plans/<slug>.md # drive a plan file
25
35
  # echo "apply review fix: ..." | codex-exec - # ad-hoc instruction (stdin)
26
- # CODEX_MODEL=<slug> codex-exec <file> # override the model
27
36
  # codex-exec <file|-> -- <extra codex flags...> # passthrough codex flags
37
+ # codex-exec --resume-last <file|-> # continue the last session (iterate, no re-send)
38
+ # codex-exec --resume <session-id> <file|-> # continue a specific session
39
+ # CODEX_HARD_TIMEOUT=2h codex-exec <file> # raise the hard wall-clock cap
40
+ # CODEX_PROBE=1 CODEX_MODEL=<slug> codex-exec <file> # throwaway probe (relaxes the guard)
28
41
  set -euo pipefail
29
42
 
30
- CODEX_MODEL="${CODEX_MODEL:-gpt-5.5}" # default coding model (verified locally); override per call
31
- CODEX_EFFORT="${CODEX_EFFORT:-xhigh}" # maximum reasoning effort
43
+ DEFAULT_CODEX_MODEL="gpt-5.5" # frontier coding model (verified locally) pinned
44
+ DEFAULT_CODEX_EFFORT="xhigh" # maximum reasoning effort — pinned
45
+ CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
46
+ CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
47
+ # Generous hard wall-clock cap, sized for a slow xhigh run (subscription latency
48
+ # varies — a trivial reply was observed taking minutes). Raise for a known-healthy
49
+ # long run; lowering it only risks killing real work.
50
+ CODEX_HARD_TIMEOUT="${CODEX_HARD_TIMEOUT:-3600}"
32
51
  CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
33
52
 
53
+ # --- Quality-first guard: refuse a silent model/effort downgrade ---------------
54
+ # Real delegated runs must use the frontier model at max effort. A throwaway probe
55
+ # (effort-independent result) may opt out with CODEX_PROBE=1, announced loudly.
56
+ if [[ "${CODEX_PROBE:-}" == "1" ]]; then
57
+ echo "warning: CODEX_PROBE=1 — THROWAWAY PROBE MODE. Quality guards relaxed; do NOT use this run's" >&2
58
+ echo " output as real delegated work (model='$CODEX_MODEL' effort='$CODEX_EFFORT')." >&2
59
+ else
60
+ if [[ "$CODEX_MODEL" != "$DEFAULT_CODEX_MODEL" ]]; then
61
+ echo "error: CODEX_MODEL='$CODEX_MODEL' is not the pinned frontier model '$DEFAULT_CODEX_MODEL'." >&2
62
+ echo " Delegated codex work must run on the frontier model at max effort (quality-first)." >&2
63
+ echo " For a throwaway probe whose result is effort-independent, set CODEX_PROBE=1." >&2
64
+ exit 2
65
+ fi
66
+ if [[ "$CODEX_EFFORT" != "$DEFAULT_CODEX_EFFORT" ]]; then
67
+ echo "error: CODEX_EFFORT='$CODEX_EFFORT' is not the pinned max effort '$DEFAULT_CODEX_EFFORT'." >&2
68
+ echo " Delegated codex work must run at max reasoning effort (quality-first)." >&2
69
+ echo " For a throwaway probe whose result is effort-independent, set CODEX_PROBE=1." >&2
70
+ exit 2
71
+ fi
72
+ fi
73
+
34
74
  # --- Subscription-only guard -------------------------------------------------
35
75
  # Never let an API key (or a user config) silently switch codex to paid api-key
36
76
  # billing or alternate behaviour. Clear the explicit vars first, then any other
@@ -45,6 +85,22 @@ if ! command -v codex >/dev/null 2>&1; then
45
85
  echo "error: 'codex' (OpenAI Codex CLI) not found on PATH. See this skill's setup/README.md." >&2
46
86
  exit 127
47
87
  fi
88
+ # Resolve the real git to an ABSOLUTE executable, ignoring shell functions/aliases
89
+ # (`type -P` forces a PATH lookup). The shim embeds this path so codex cannot recurse
90
+ # into the shim or delegate to the wrong binary.
91
+ real_git="$(type -P git 2>/dev/null || true)"
92
+ if [[ -z "$real_git" ]]; then
93
+ echo "error: 'git' not found on PATH (needed for the work tree and the git-write boundary shim)." >&2
94
+ exit 127
95
+ fi
96
+ case "$real_git" in
97
+ /*) ;;
98
+ *) real_git="$(cd "$(dirname "$real_git")" 2>/dev/null && pwd)/$(basename "$real_git")" ;;
99
+ esac
100
+ if [[ ! -x "$real_git" ]]; then
101
+ echo "error: resolved git path '$real_git' is not an executable." >&2
102
+ exit 127
103
+ fi
48
104
  if ! codex login status 2>&1 | grep -qF "$CHATGPT_LOGIN_GUARD"; then
49
105
  echo "error: codex is not on a ChatGPT subscription (expected '$CHATGPT_LOGIN_GUARD')." >&2
50
106
  echo " Run 'codex login' once; this skill is subscription-only and won't use api-key billing." >&2
@@ -65,8 +121,8 @@ ORCHESTRATOR EXECUTION CONTRACT — read before the task, follow it exactly:
65
121
  1. Work directly in the current working tree on the current git branch. NEVER run
66
122
  any git write command (no branch, add, commit, stash, reset, checkout, tag, or
67
123
  history rewrite) — the orchestrator commits after review.
68
- 2. Read the target project's root AGENTS.md and obey EVERY Hard Constraint it
69
- declares, plus this task's own "do NOT" / out-of-scope section.
124
+ 2. Obey EVERY Hard Constraint declared in the project's root AGENTS.md (already
125
+ merged into your context) and this task's own "do NOT" / out-of-scope section.
70
126
  3. After implementing, run a SELF-REVIEW pass over your own changes — `git status`
71
127
  for untracked files and `git diff` for tracked ones, reading the contents of
72
128
  any new untracked files — against the task and those Hard Constraints; fix
@@ -81,63 +137,300 @@ ORCHESTRATOR EXECUTION CONTRACT — read before the task, follow it exactly:
81
137
  TASK:
82
138
  DIRECTIVE
83
139
 
140
+ read -r -d '' RESUME_REMINDER <<'REMINDER' || true
141
+ CONTINUE the existing task in the same working tree under the SAME contract: never
142
+ run a git write command (the orchestrator commits), obey the project's root AGENTS.md
143
+ Hard Constraints, run the project's declared gates, do NOT commit, and STOP + report
144
+ any blocker — never guess.
145
+
146
+ NEW INSTRUCTION:
147
+ REMINDER
148
+
84
149
  if [[ $# -lt 1 ]]; then
85
150
  echo "usage: $0 <plan-file|-> [-- extra codex args...]" >&2
151
+ echo " $0 --resume-last <plan-file|->" >&2
152
+ echo " $0 --resume <session-id> <plan-file|->" >&2
86
153
  exit 2
87
154
  fi
88
155
 
156
+ # --- Resume detection (must be the FIRST argument) ---------------------------
157
+ # A dedicated entrypoint for iterating on a session without re-sending context.
158
+ # `codex exec resume` RESETS posture and rejects the -s/--add-dir/-C posture flags
159
+ # (it DOES accept -o/--json on 0.142.3, but we capture stdout directly), so we
160
+ # restate the FULL policy via -c.
161
+ resume_mode=""
162
+ resume_id=""
163
+ case "${1:-}" in
164
+ --resume-last)
165
+ resume_mode="last"; shift
166
+ ;;
167
+ --resume)
168
+ resume_mode="id"; shift
169
+ resume_id="${1:-}"; shift || true
170
+ if [[ -z "$resume_id" || "$resume_id" == "-"* ]]; then
171
+ echo "error: --resume needs a <session-id> argument before the prompt." >&2
172
+ exit 2
173
+ fi
174
+ ;;
175
+ esac
176
+
177
+ if [[ $# -lt 1 ]]; then
178
+ echo "error: missing <plan-file|-> (the instruction to send)." >&2
179
+ exit 2
180
+ fi
89
181
  prompt_src="$1"; shift
90
182
 
91
- # Split off passthrough codex flags after a literal `--`. Extra args WITHOUT the
92
- # `--` separator are a mistake — they would be silently dropped, so fail loudly.
93
183
  passthrough=()
94
- if [[ $# -gt 0 ]]; then
95
- if [[ "$1" == "--" ]]; then
96
- shift
97
- passthrough=("$@")
98
- else
99
- echo "error: unexpected argument '$1'. Pass extra codex flags after a literal '--':" >&2
100
- echo " $0 <plan-file|-> -- <codex flags...>" >&2
184
+ if [[ -n "$resume_mode" ]]; then
185
+ # Resume takes no passthrough the wrapper restates the entire fixed policy.
186
+ if [[ $# -gt 0 ]]; then
187
+ echo "error: resume modes take no extra flags ('$1' …) — the wrapper restates the full policy." >&2
101
188
  exit 2
102
189
  fi
103
- fi
104
-
105
- # This wrapper OWNS the safety policy (sandbox level, approval policy, network
106
- # access, and every -c config override). Reject passthrough flags that would
107
- # defeat it appended flags can otherwise override the fixed ones. Benign flags
108
- # (--add-dir, --cd, --ephemeral, -m, --image, ...) still pass through.
109
- if [[ ${#passthrough[@]} -gt 0 ]]; then
110
- for _arg in "${passthrough[@]}"; do
111
- case "$_arg" in
112
- -c*|--config*|-s*|--sandbox*|--dangerously-bypass-approvals-and-sandbox|--dangerously-bypass-hook-trust|--full-auto)
113
- echo "error: passthrough flag '$_arg' is not allowed — this wrapper fixes the sandbox / approval / network / config policy." >&2
114
- echo " Drop it, or invoke 'codex' directly if you truly need a different policy." >&2
115
- exit 2
116
- ;;
117
- esac
118
- done
190
+ if [[ "$resume_mode" == "last" ]]; then
191
+ sidecar="${CODEX_SESSION_FILE:-$PWD/.codex-last-session}"
192
+ if [[ ! -f "$sidecar" ]]; then
193
+ echo "error: --resume-last found no session sidecar at '$sidecar'." >&2
194
+ echo " Run a normal 'codex-exec' once (it records the session id there) before resuming." >&2
195
+ exit 2
196
+ fi
197
+ resume_id="$(head -n1 -- "$sidecar" | tr -d '[:space:]')"
198
+ if [[ -z "$resume_id" ]]; then
199
+ echo "error: the session sidecar '$sidecar' is empty — no session id to resume." >&2
200
+ exit 2
201
+ fi
202
+ fi
203
+ else
204
+ # Normal mode: split off passthrough codex flags after a literal `--`. Extra args
205
+ # WITHOUT the `--` separator are a mistake — fail loudly rather than drop them.
206
+ if [[ $# -gt 0 ]]; then
207
+ if [[ "$1" == "--" ]]; then
208
+ shift
209
+ passthrough=("$@")
210
+ else
211
+ echo "error: unexpected argument '$1'. Pass extra codex flags after a literal '--':" >&2
212
+ echo " $0 <plan-file|-> -- <codex flags...>" >&2
213
+ exit 2
214
+ fi
215
+ fi
216
+ # This wrapper OWNS the safety + quality policy. Reject passthrough flags in two
217
+ # tiers:
218
+ # (1) ALWAYS rejected — they would defeat the subscription / sandbox / approval /
219
+ # config-isolation policy (-c/-s/--full-auto/bypass), switch the provider off
220
+ # the subscription (--oss/--local-provider), load alternate config (-p/--profile),
221
+ # override the pinned frontier model (-m), or break the wrapper-owned clean
222
+ # output / session capture (-o/--json/--color/--output-schema/--ephemeral).
223
+ # CODEX_PROBE=1 NEVER relaxes these: a probe still runs on the subscription, in
224
+ # the sandbox, with clean capture; its model is chosen via CODEX_MODEL, not -m.
225
+ # (2) Probe-relaxable — context/discovery knobs the wrapper otherwise pins; a
226
+ # throwaway probe (CODEX_PROBE=1) may pass them. Need more? invoke `codex` direct.
227
+ if [[ ${#passthrough[@]} -gt 0 ]]; then
228
+ for _arg in "${passthrough[@]}"; do
229
+ case "$_arg" in
230
+ -c*|--config*|-s*|--sandbox*|--dangerously-bypass-approvals-and-sandbox|--dangerously-bypass-hook-trust|--full-auto|--oss|--local-provider*|-p*|--profile*|-m*|--model*|-o*|--output-last-message*|--json*|--color*|--output-schema*|--ephemeral*)
231
+ echo "error: passthrough flag '$_arg' is not allowed — it would defeat the subscription / sandbox /" >&2
232
+ echo " approval / config-isolation policy, the pinned frontier model, or the clean output/session" >&2
233
+ echo " capture. It stays blocked even under CODEX_PROBE=1. Invoke 'codex' directly if you must." >&2
234
+ exit 2
235
+ ;;
236
+ --add-dir*|-C*|--cd*|--skip-git-repo-check|--ignore-rules|--enable*|--disable*)
237
+ if [[ "${CODEX_PROBE:-}" != "1" ]]; then
238
+ echo "error: passthrough flag '$_arg' is not allowed — this wrapper pins the model & context." >&2
239
+ echo " Set CODEX_PROBE=1 for a throwaway probe, or invoke 'codex' directly." >&2
240
+ exit 2
241
+ fi
242
+ ;;
243
+ esac
244
+ done
245
+ fi
119
246
  fi
120
247
 
121
248
  if [[ "$prompt_src" == "-" ]]; then
122
249
  task="$(cat)"
123
250
  elif [[ -f "$prompt_src" ]]; then
124
- task="$(cat "$prompt_src")"
251
+ task="$(cat -- "$prompt_src")"
125
252
  else
126
253
  echo "error: '$prompt_src' is not a file (use '-' to read the prompt from stdin)" >&2
127
254
  exit 2
128
255
  fi
129
256
 
130
257
  if [[ -z "${task//[[:space:]]/}" ]]; then
131
- echo "error: empty plan/instruction" >&2
258
+ echo "error: empty ${resume_mode:+resumed }plan/instruction" >&2
132
259
  exit 2
133
260
  fi
134
261
 
135
- printf '%s\n\n%s' "$ORCHESTRATOR_DIRECTIVE" "$task" | codex exec \
136
- --ignore-user-config \
137
- --sandbox workspace-write \
138
- -c approval_policy="never" \
139
- -c sandbox_workspace_write.network_access=false \
140
- -c model_reasoning_effort="$CODEX_EFFORT" \
141
- -m "$CODEX_MODEL" \
142
- "${passthrough[@]+"${passthrough[@]}"}" \
143
- -
262
+ # --- Enforced git-write boundary (physical shim file) ------------------------
263
+ # codex spawns `git` via execve, which BYPASSES exported shell functions — so the
264
+ # boundary MUST be a physical executable on PATH. We write a `git` shim into a temp
265
+ # dir and prepend it to PATH for the codex subprocess ONLY. It passes read-only
266
+ # verbs through to the real git and blocks every write (add/commit/reset/…). The
267
+ # real git path is BAKED INTO the shim (not an env var) so codex can't read it to
268
+ # bypass the shim. This is best-effort defence-in-depth beside the prompt contract
269
+ # (codex could still call git by an absolute path — the contract + review are the
270
+ # real guard); it removes the trivial PATH-level write vector.
271
+ shim_dir=""
272
+ out=""
273
+ trace=""
274
+ trap 'rm -rf "$shim_dir" 2>/dev/null; rm -f "$out" "$trace" 2>/dev/null; true' EXIT
275
+ shim_dir="$(mktemp -d)"
276
+ out="$(mktemp)"
277
+ trace="$(mktemp)"
278
+ {
279
+ printf '#!/usr/bin/env bash\n'
280
+ printf 'set -u\n'
281
+ printf 'real=%q\n' "$real_git"
282
+ cat <<'SHIM'
283
+ # git-write boundary shim — read-only verbs pass through to the embedded real git;
284
+ # every write/unknown verb is blocked by default.
285
+ args=("$@")
286
+ i=0; n=${#args[@]}
287
+ # Walk past git's leading global options (value-taking ones consume the next token).
288
+ while [[ $i -lt $n ]]; do
289
+ case "${args[$i]}" in
290
+ -C|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path|--config-env|-c) i=$((i+2)); continue ;;
291
+ -*) i=$((i+1)); continue ;;
292
+ *) break ;;
293
+ esac
294
+ done
295
+ verb="${args[$i]:-}"
296
+ rest=("${args[@]:$((i+1))}")
297
+ case "$verb" in
298
+ ""|version|status|diff|show|log|ls-files|ls-tree|rev-parse|rev-list|merge-base|cat-file|\
299
+ describe|for-each-ref|name-rev|blame|grep|shortlog|annotate|whatchanged|count-objects|var|\
300
+ check-ignore|check-attr|show-ref|show-branch|verify-commit|verify-tag|cherry)
301
+ exec "$real" "$@" ;;
302
+ config)
303
+ # Reads only: block on any write/action flag, or a `<name> <value>` set form
304
+ # (>= 2 non-option args). Permits --get*/--list and a bare `git config <name>`.
305
+ positionals=0
306
+ for a in ${rest[@]+"${rest[@]}"}; do
307
+ case "$a" in
308
+ --add|--unset|--unset-all|--replace-all|--remove-section|--rename-section|--edit|-e|--unset-pattern|--fixed-value|--set)
309
+ echo "git-write-shim: 'git config' write is blocked (read-only boundary)." >&2; exit 13 ;;
310
+ -*) : ;;
311
+ *) positionals=$((positionals+1)) ;;
312
+ esac
313
+ done
314
+ if [[ $positionals -ge 2 ]]; then
315
+ echo "git-write-shim: 'git config <name> <value>' write is blocked." >&2; exit 13
316
+ fi
317
+ exec "$real" "$@" ;;
318
+ *)
319
+ echo "git-write-shim: 'git ${verb}' is blocked — codex must not write git state; the orchestrator commits after review." >&2
320
+ exit 13 ;;
321
+ esac
322
+ SHIM
323
+ } >"$shim_dir/git"
324
+ chmod 755 "$shim_dir/git"
325
+
326
+ # --- Build the codex invocation + the prompt ---------------------------------
327
+ if [[ -n "$resume_mode" ]]; then
328
+ # Resume RESETS posture and rejects the -s/--add-dir/-C posture flags, so restate
329
+ # the entire policy via -c. We deliberately pass no -o/--json (resume DOES accept
330
+ # them) — codex prints the final message to stdout, which we capture into $out.
331
+ codex_cmd=(codex exec resume "$resume_id"
332
+ --ignore-user-config
333
+ -m "$CODEX_MODEL"
334
+ -c model_reasoning_effort="$CODEX_EFFORT"
335
+ -c sandbox_mode=workspace-write
336
+ -c approval_policy=never
337
+ -c sandbox_workspace_write.network_access=false
338
+ -c hide_agent_reasoning=true
339
+ -c model_reasoning_summary=none
340
+ -)
341
+ full_prompt="$RESUME_REMINDER"$'\n\n'"$task"
342
+ else
343
+ # `-o` writes ONLY codex's final message; `--json` streams structured events
344
+ # (thread.started carries the session id). CoT is dropped and colour disabled, so
345
+ # the captured surfaces stay clean. Reasoning still runs at xhigh — quality is
346
+ # unchanged; we only stop printing the noise.
347
+ codex_cmd=(codex exec
348
+ --ignore-user-config
349
+ --sandbox workspace-write
350
+ -c approval_policy="never"
351
+ -c sandbox_workspace_write.network_access=false
352
+ -c model_reasoning_effort="$CODEX_EFFORT"
353
+ -c hide_agent_reasoning=true
354
+ -c model_reasoning_summary=none
355
+ --color never
356
+ -o "$out"
357
+ --json
358
+ -m "$CODEX_MODEL"
359
+ "${passthrough[@]+"${passthrough[@]}"}"
360
+ -)
361
+ full_prompt="$ORCHESTRATOR_DIRECTIVE"$'\n\n'"$task"
362
+ fi
363
+
364
+ # Env for the codex subprocess: the git-write shim FIRST on PATH. The real git path
365
+ # is baked into the shim itself — never exposed to codex as an env var.
366
+ run_env=(env "PATH=$shim_dir:$PATH")
367
+
368
+ # --- Hard wall-clock cap via timeout(1) (gtimeout on macOS) -------------------
369
+ # A backgrounded, hung codex run survives otherwise. --kill-after SIGKILLs 15s
370
+ # after the initial TERM if codex ignores it (a live probe confirmed plain
371
+ # `timeout` reaps the whole codex child tree — no --foreground needed). If neither
372
+ # binary exists we warn loudly and run uncapped rather than fail silently.
373
+ timeout_bin=""
374
+ if command -v timeout >/dev/null 2>&1; then
375
+ timeout_bin="timeout"
376
+ elif command -v gtimeout >/dev/null 2>&1; then
377
+ timeout_bin="gtimeout"
378
+ fi
379
+ if [[ -z "$timeout_bin" ]]; then
380
+ echo "warning: no 'timeout'/'gtimeout' on PATH — running codex WITHOUT a hard wall-clock cap" >&2
381
+ echo " (install coreutils to enable CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT)." >&2
382
+ fi
383
+
384
+ # Normal mode: -o writes $out, the JSON stream + logs go to $trace. Resume mode: the
385
+ # final message is codex's stdout → $out, logs → $trace. Either way the final lands
386
+ # in $out and diagnostics in $trace, so the post-processing below is shared.
387
+ set +e
388
+ if [[ -n "$resume_mode" ]]; then
389
+ if [[ -n "$timeout_bin" ]]; then
390
+ printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$out" 2>"$trace"
391
+ else
392
+ printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$out" 2>"$trace"
393
+ fi
394
+ rc=$?
395
+ else
396
+ if [[ -n "$timeout_bin" ]]; then
397
+ printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=15s "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
398
+ else
399
+ printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
400
+ fi
401
+ rc=$?
402
+ fi
403
+ set -e
404
+
405
+ if [[ $rc -eq 124 || $rc -eq 137 ]]; then
406
+ echo "error: codex exec exceeded the hard cap CODEX_HARD_TIMEOUT=${CODEX_HARD_TIMEOUT}s and was terminated." >&2
407
+ echo " Raise CODEX_HARD_TIMEOUT for a known-healthy slow run, or narrow the task, then re-dispatch." >&2
408
+ exit $rc
409
+ fi
410
+ if [[ $rc -ne 0 ]]; then
411
+ echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
412
+ tail -n 40 "$trace" >&2
413
+ exit $rc
414
+ fi
415
+
416
+ # Success: capture the session id (NORMAL mode only — it carries thread.started in
417
+ # the JSON trace; a resume continues the same session) BEFORE the trap removes the
418
+ # trace, so an iterative resume (codex-exec --resume-last) can find it.
419
+ if [[ -z "$resume_mode" ]]; then
420
+ session_id="$(grep -m1 '"type":"thread.started"' "$trace" 2>/dev/null \
421
+ | grep -o '"thread_id":"[^"]*"' | cut -d'"' -f4 || true)"
422
+ if [[ -n "$session_id" ]]; then
423
+ sidecar="${CODEX_SESSION_FILE:-$PWD/.codex-last-session}"
424
+ if ! printf '%s\n' "$session_id" >"$sidecar" 2>/dev/null; then
425
+ echo "warning: could not write the session sidecar '$sidecar' — 'codex-exec --resume-last' won't find this id." >&2
426
+ fi
427
+ echo "session: $session_id" >&2
428
+ fi
429
+ fi
430
+
431
+ if [[ -f "$out" && -s "$out" ]]; then
432
+ cat "$out"
433
+ else
434
+ echo "warning: codex produced no final-message file — printing the run-trace tail instead." >&2
435
+ tail -n 40 "$trace"
436
+ fi