@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.
- package/CHANGELOG.md +65 -0
- package/README.md +2 -1
- package/SKILL.md +37 -7
- package/bridges/codex-cli-bridge/SKILL.md +81 -28
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +337 -44
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +593 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +279 -19
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +542 -0
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/references/driving-codex.md +67 -40
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +148 -43
- package/bridges/codex-cli-bridge/setup/README.md +6 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/templates/orchestration.json +8 -3
- package/tools/commands.mjs +7 -0
- package/tools/family-registry.mjs +3 -1
- package/tools/inject-methodology.mjs +90 -16
- package/tools/orchestration-config.mjs +297 -0
- package/tools/orchestration-write.mjs +97 -0
- package/tools/procedures.mjs +17 -102
- package/tools/set-recipe.mjs +217 -0
- package/tools/setup-backends.mjs +71 -5
|
@@ -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
|
|
8
|
-
#
|
|
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 (
|
|
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
|
-
|
|
31
|
-
|
|
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.
|
|
69
|
-
|
|
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 [[
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|