Skip to content

Commit 4d54140

Browse files
authored
Merge pull request #134 from VitalyAnkh/fix/codex-hooks-feature-rename
fix(codex): use renamed hooks feature
2 parents f4e5721 + af0083d commit 4d54140

10 files changed

Lines changed: 278 additions & 97 deletions

docs/install-for-codex.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ This will:
2626
- Sync `humanize`, `humanize-gen-plan`, `humanize-refine-plan`, and `humanize-rlcr` into `${CODEX_HOME:-~/.codex}/skills`
2727
- Copy runtime dependencies into `${CODEX_HOME:-~/.codex}/skills/humanize`
2828
- Install/update native Humanize Stop hooks in `${CODEX_HOME:-~/.codex}/hooks.json`
29-
- Enable the experimental `codex_hooks` feature in `${CODEX_HOME:-~/.codex}/config.toml` when `codex` is available
29+
- Enable the native `hooks` feature in `${CODEX_HOME:-~/.codex}/config.toml` when `codex` is available
3030
- Seed `~/.config/humanize/config.json` with a Codex/OpenAI `bitlesson_model` when that key is not already set
3131
- Mark the install as `provider_mode: "codex-only"` when using `--target codex`
3232
- Use RLCR defaults: `codex exec` with `gpt-5.5:high`, `codex review` with `gpt-5.5:high`
3333

34-
Requires Codex CLI `0.114.0` or newer for native hooks. Older Codex builds are not supported by the Codex install path.
34+
Requires Codex CLI `0.114.0` or newer for native hooks. The hooks feature was renamed to `hooks`; older Codex builds that still expose `codex_hooks` are not supported by the Codex install path.
3535

3636
## Verify
3737

@@ -70,12 +70,12 @@ Installed files/directories:
7070
Verify native hooks:
7171

7272
```bash
73-
codex features list | rg codex_hooks
73+
codex features list | rg '^hooks\s'
7474
sed -n '1,220p' "${CODEX_HOME:-$HOME/.codex}/hooks.json"
7575
```
7676

7777
Expected:
78-
- `codex_hooks` is `true`
78+
- `hooks` is present in `codex features list`
7979
- `hooks.json` contains `loop-codex-stop-hook.sh`
8080
- `${XDG_CONFIG_HOME:-~/.config}/humanize/config.json` contains `bitlesson_model` set to a Codex/OpenAI model such as `gpt-5.5`
8181
- for `--target codex`, `${XDG_CONFIG_HOME:-~/.config}/humanize/config.json` also contains `provider_mode: "codex-only"`
@@ -110,6 +110,8 @@ ls -la "${CODEX_HOME:-$HOME/.codex}/skills/humanize/scripts"
110110
If native exit gating does not trigger:
111111

112112
```bash
113-
codex features enable codex_hooks
113+
codex features enable hooks
114114
sed -n '1,220p' "${CODEX_HOME:-$HOME/.codex}/hooks.json"
115115
```
116+
117+
If the installer reports that your config or installed Codex still uses `codex_hooks`, upgrade Codex first or change `${CODEX_HOME:-~/.codex}/config.toml` to `[features]\nhooks = true`.

hooks/loop-codex-stop-hook.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,9 +1172,9 @@ mkdir -p "$CACHE_DIR"
11721172
CODEX_DISABLE_HOOKS_ARGS=()
11731173
_CODEX_FEATURE_CACHE="$CACHE_DIR/.codex-disable-hooks-supported"
11741174
if [[ -f "$_CODEX_FEATURE_CACHE" ]]; then
1175-
[[ "$(cat "$_CODEX_FEATURE_CACHE")" == "yes" ]] && CODEX_DISABLE_HOOKS_ARGS=(--disable codex_hooks)
1175+
[[ "$(cat "$_CODEX_FEATURE_CACHE")" == "yes" ]] && CODEX_DISABLE_HOOKS_ARGS=(--disable hooks)
11761176
elif codex --help 2>&1 | grep -q -- '--disable'; then
1177-
CODEX_DISABLE_HOOKS_ARGS=(--disable codex_hooks)
1177+
CODEX_DISABLE_HOOKS_ARGS=(--disable hooks)
11781178
echo "yes" > "$_CODEX_FEATURE_CACHE" 2>/dev/null
11791179
else
11801180
echo "no" > "$_CODEX_FEATURE_CACHE" 2>/dev/null

scripts/bitlesson-select.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ run_selector() {
193193
local codex_exec_args=()
194194
# Probe whether the installed Codex CLI supports --disable flag
195195
if codex --help 2>&1 | grep -q -- '--disable'; then
196-
codex_exec_args+=("--disable" "codex_hooks")
196+
codex_exec_args+=("--disable" "hooks")
197197
fi
198198
# Probe for --skip-git-repo-check and --ephemeral support
199199
if codex exec --help 2>&1 | grep -q -- '--skip-git-repo-check'; then

scripts/install-codex-hooks.sh

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ RUNTIME_ROOT="$CODEX_CONFIG_DIR/skills/humanize"
1212
DRY_RUN="false"
1313
ENABLE_FEATURE="true"
1414
HOOKS_TEMPLATE="$REPO_ROOT/config/codex-hooks.json"
15+
HOOK_FEATURE_ENABLED=""
1516

1617
usage() {
1718
cat <<'EOF'
@@ -23,7 +24,7 @@ Usage:
2324
Options:
2425
--codex-config-dir PATH Codex config dir (default: ${CODEX_HOME:-~/.codex})
2526
--runtime-root PATH Installed Humanize runtime root (default: <codex-config-dir>/skills/humanize)
26-
--skip-enable-feature Do not run `codex features enable codex_hooks`
27+
--skip-enable-feature Do not run `codex features enable hooks`
2728
--dry-run Print actions without writing
2829
-h, --help Show help
2930
EOF
@@ -72,14 +73,40 @@ done
7273

7374
HOOKS_FILE="$CODEX_CONFIG_DIR/hooks.json"
7475

75-
require_codex_hooks_support() {
76+
config_uses_legacy_codex_hooks() {
77+
local config_file="$CODEX_CONFIG_DIR/config.toml"
78+
79+
[[ -f "$config_file" ]] || return 1
80+
81+
grep -Eq '^[[:space:]]*(features\.)?codex_hooks[[:space:]]*=' "$config_file"
82+
}
83+
84+
require_native_hooks_support() {
7685
if ! command -v codex >/dev/null 2>&1; then
7786
die "Codex CLI with native hooks support is required. Install Codex 0.114.0+ first."
7887
fi
7988

80-
if ! codex features list 2>/dev/null | grep -qE '^codex_hooks[[:space:]]'; then
81-
die "Installed Codex CLI does not expose the codex_hooks feature. Humanize Codex install requires Codex 0.114.0+."
89+
if config_uses_legacy_codex_hooks; then
90+
die "Codex config uses the legacy feature key 'codex_hooks'. Current Codex uses 'hooks'. Update $CODEX_CONFIG_DIR/config.toml to use 'hooks = true' under [features], or upgrade Codex if 'codex features list' does not show 'hooks'."
91+
fi
92+
93+
local features
94+
local line
95+
features="$(CODEX_HOME="$CODEX_CONFIG_DIR" codex features list 2>/dev/null)" || {
96+
die "failed to inspect Codex features. Humanize Codex install requires the native 'hooks' feature."
97+
}
98+
99+
line="$(printf '%s\n' "$features" | awk '$1 == "hooks" { print; exit }')"
100+
if [[ -n "$line" ]]; then
101+
HOOK_FEATURE_ENABLED="$(awk '{ print $NF }' <<<"$line")"
102+
return 0
103+
fi
104+
105+
if printf '%s\n' "$features" | awk '$1 == "codex_hooks" { found = 1 } END { exit found ? 0 : 1 }'; then
106+
die "Installed Codex exposes only the legacy 'codex_hooks' feature. Humanize now requires the renamed 'hooks' feature. Upgrade Codex, then rerun the installer."
82107
fi
108+
109+
die "Installed Codex CLI does not expose the native 'hooks' feature. Upgrade Codex, then rerun the installer."
83110
}
84111

85112
merge_hooks_json() {
@@ -177,23 +204,28 @@ enable_feature() {
177204

178205
[[ "$ENABLE_FEATURE" == "true" ]] || return 0
179206

180-
if CODEX_HOME="$config_dir" codex features enable codex_hooks >/dev/null 2>&1; then
181-
log "enabled codex_hooks feature in $config_dir/config.toml"
207+
if [[ "$HOOK_FEATURE_ENABLED" == "true" ]]; then
208+
log "native hooks feature already enabled in $config_dir/config.toml"
209+
return 0
210+
fi
211+
212+
if CODEX_HOME="$config_dir" codex features enable hooks >/dev/null 2>&1; then
213+
log "enabled hooks feature in $config_dir/config.toml"
182214
else
183-
die "failed to enable codex_hooks feature automatically in $config_dir/config.toml"
215+
die "failed to enable hooks feature automatically in $config_dir/config.toml"
184216
fi
185217
}
186218

187219
log "codex config dir: $CODEX_CONFIG_DIR"
188220
log "runtime root: $RUNTIME_ROOT"
189221
log "hooks file: $HOOKS_FILE"
190222

191-
require_codex_hooks_support
223+
require_native_hooks_support
192224

193225
if [[ "$DRY_RUN" == "true" ]]; then
194226
log "DRY-RUN merge $HOOKS_TEMPLATE -> $HOOKS_FILE"
195227
if [[ "$ENABLE_FEATURE" == "true" ]]; then
196-
log "DRY-RUN enable codex_hooks feature in $CODEX_CONFIG_DIR/config.toml"
228+
log "DRY-RUN enable hooks feature in $CODEX_CONFIG_DIR/config.toml"
197229
fi
198230
exit 0
199231
fi

tests/run-all-tests.sh

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ ZSH_TESTS=(
133133
"test-zsh-monitor-safety.sh"
134134
)
135135

136+
# Signal-heavy runtime tests are more stable when they run after the
137+
# parallel batch finishes.
138+
SERIAL_TESTS=(
139+
"test-monitor-runtime.sh"
140+
)
141+
136142
# Temp directory for per-suite output files
137143
OUTPUT_DIR=$(mktemp -d)
138144
trap "rm -rf $OUTPUT_DIR" EXIT
@@ -161,6 +167,16 @@ needs_zsh() {
161167
return 1
162168
}
163169

170+
needs_serial() {
171+
local suite="$1"
172+
for serial_test in "${SERIAL_TESTS[@]}"; do
173+
if [[ "$suite" == "$serial_test" ]]; then
174+
return 0
175+
fi
176+
done
177+
return 1
178+
}
179+
164180
# Format milliseconds as human-readable duration
165181
format_ms() {
166182
local ms="$1"
@@ -169,10 +185,79 @@ format_ms() {
169185
echo "${s}.${frac}s"
170186
}
171187

172-
# Launch all test suites in parallel
188+
run_suite_capture() {
189+
local suite="$1"
190+
local out_file="$2"
191+
local exit_file="$3"
192+
local time_file="$4"
193+
local suite_path="$SCRIPT_DIR/$suite"
194+
195+
if needs_zsh "$suite"; then
196+
(
197+
t_start=$(date +%s%3N)
198+
zsh "$suite_path" >"$out_file" 2>&1
199+
echo $? >"$exit_file"
200+
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
201+
)
202+
else
203+
(
204+
t_start=$(date +%s%3N)
205+
"$suite_path" >"$out_file" 2>&1
206+
echo $? >"$exit_file"
207+
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
208+
)
209+
fi
210+
}
211+
212+
collect_suite_result() {
213+
local suite="$1"
214+
local safe_name="$2"
215+
local out_file="$3"
216+
local exit_file="$4"
217+
local time_file="$5"
218+
local exit_code
219+
local output
220+
local elapsed_ms
221+
local elapsed_display
222+
local output_stripped
223+
local passed
224+
local failed
225+
local line
226+
local zsh_label
227+
228+
exit_code=$(cat "$exit_file" 2>/dev/null || echo "1")
229+
output=$(cat "$out_file" 2>/dev/null || echo "")
230+
elapsed_ms=$(cat "$time_file" 2>/dev/null || echo "0")
231+
elapsed_display=$(format_ms "$elapsed_ms")
232+
233+
# Strip ANSI escape codes and extract pass/fail counts
234+
output_stripped=$(echo "$output" | sed "s/${esc}\\[[0-9;]*m//g")
235+
passed=$(echo "$output_stripped" | grep -oE 'Passed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")
236+
failed=$(echo "$output_stripped" | grep -oE 'Failed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")
237+
238+
TOTAL_PASSED=$((TOTAL_PASSED + passed))
239+
TOTAL_FAILED=$((TOTAL_FAILED + failed))
240+
241+
if [[ $exit_code -ne 0 ]] || [[ "$failed" -gt 0 ]]; then
242+
FAILED_SUITES+=("$suite")
243+
line=$(echo -e "${RED}FAILED${NC}: $suite (exit code: $exit_code, failed: $failed, ${elapsed_display})")
244+
printf '%d\t%s\n' "$elapsed_ms" "$line" >> "$SORT_FILE"
245+
# Preserve the full suite log so CI surfaces the exact failing assertion.
246+
printf '%s\n' "$output" > "$OUTPUT_DIR/${safe_name}.detail"
247+
else
248+
zsh_label=""
249+
needs_zsh "$suite" && zsh_label=" (zsh)"
250+
line=$(echo -e "${GREEN}PASSED${NC}: $suite${zsh_label} ($passed tests, ${elapsed_display})")
251+
printf '%d\t%s\n' "$elapsed_ms" "$line" >> "$SORT_FILE"
252+
fi
253+
}
254+
255+
# Launch all test suites in parallel, except signal-heavy runtime tests which
256+
# run serially after the parallel batch finishes.
173257
declare -A PIDS # suite -> PID
174258
declare -A SKIPPED # suite -> reason
175259
ACTIVE_PIDS=()
260+
SERIAL_SUITES=()
176261

177262
for suite in "${TEST_SUITES[@]}"; do
178263
suite_path="$SCRIPT_DIR/$suite"
@@ -186,25 +271,21 @@ for suite in "${TEST_SUITES[@]}"; do
186271
continue
187272
fi
188273

274+
if needs_serial "$suite"; then
275+
SERIAL_SUITES+=("$suite")
276+
continue
277+
fi
278+
189279
if needs_zsh "$suite"; then
190280
if ! command -v zsh &>/dev/null; then
191281
SKIPPED["$suite"]="zsh not available"
192282
continue
193283
fi
194-
(
195-
t_start=$(date +%s%3N)
196-
zsh "$suite_path" >"$out_file" 2>&1
197-
echo $? >"$exit_file"
198-
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
199-
) &
200-
else
201-
(
202-
t_start=$(date +%s%3N)
203-
"$suite_path" >"$out_file" 2>&1
204-
echo $? >"$exit_file"
205-
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
206-
) &
207284
fi
285+
286+
(
287+
run_suite_capture "$suite" "$out_file" "$exit_file" "$time_file"
288+
) &
208289
PIDS["$suite"]=$!
209290
ACTIVE_PIDS+=("${PIDS[$suite]}")
210291

@@ -228,7 +309,7 @@ for suite in "${TEST_SUITES[@]}"; do
228309
done
229310
done
230311

231-
# Wait for all and collect results
312+
# Wait for parallel suites and collect results.
232313
TOTAL_PASSED=0
233314
TOTAL_FAILED=0
234315
FAILED_SUITES=()
@@ -239,6 +320,7 @@ SORT_FILE="$OUTPUT_DIR/sortable.txt"
239320
esc=$'\033'
240321
for suite in "${TEST_SUITES[@]}"; do
241322
[[ -n "${SKIPPED[$suite]+x}" ]] && continue
323+
[[ " ${SERIAL_SUITES[*]} " == *" $suite "* ]] && continue
242324

243325
pid="${PIDS[$suite]}"
244326
wait "$pid" 2>/dev/null
@@ -247,32 +329,18 @@ for suite in "${TEST_SUITES[@]}"; do
247329
out_file="$OUTPUT_DIR/${safe_name}.out"
248330
exit_file="$OUTPUT_DIR/${safe_name}.exit"
249331
time_file="$OUTPUT_DIR/${safe_name}.time"
332+
collect_suite_result "$suite" "$safe_name" "$out_file" "$exit_file" "$time_file"
333+
done
250334

251-
exit_code=$(cat "$exit_file" 2>/dev/null || echo "1")
252-
output=$(cat "$out_file" 2>/dev/null || echo "")
253-
elapsed_ms=$(cat "$time_file" 2>/dev/null || echo "0")
254-
elapsed_display=$(format_ms "$elapsed_ms")
255-
256-
# Strip ANSI escape codes and extract pass/fail counts
257-
output_stripped=$(echo "$output" | sed "s/${esc}\\[[0-9;]*m//g")
258-
passed=$(echo "$output_stripped" | grep -oE 'Passed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")
259-
failed=$(echo "$output_stripped" | grep -oE 'Failed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")
260-
261-
TOTAL_PASSED=$((TOTAL_PASSED + passed))
262-
TOTAL_FAILED=$((TOTAL_FAILED + failed))
335+
# Run serial suites after the parallel batch finishes.
336+
for suite in "${SERIAL_SUITES[@]}"; do
337+
safe_name="$(echo "$suite" | tr '/' '_')"
338+
out_file="$OUTPUT_DIR/${safe_name}.out"
339+
exit_file="$OUTPUT_DIR/${safe_name}.exit"
340+
time_file="$OUTPUT_DIR/${safe_name}.time"
263341

264-
if [[ $exit_code -ne 0 ]] || [[ "$failed" -gt 0 ]]; then
265-
FAILED_SUITES+=("$suite")
266-
line=$(echo -e "${RED}FAILED${NC}: $suite (exit code: $exit_code, failed: $failed, ${elapsed_display})")
267-
printf '%d\t%s\n' "$elapsed_ms" "$line" >> "$SORT_FILE"
268-
# Preserve the full suite log so CI surfaces the exact failing assertion.
269-
printf '%s\n' "$output" > "$OUTPUT_DIR/${safe_name}.detail"
270-
else
271-
zsh_label=""
272-
needs_zsh "$suite" && zsh_label=" (zsh)"
273-
line=$(echo -e "${GREEN}PASSED${NC}: $suite${zsh_label} ($passed tests, ${elapsed_display})")
274-
printf '%d\t%s\n' "$elapsed_ms" "$line" >> "$SORT_FILE"
275-
fi
342+
run_suite_capture "$suite" "$out_file" "$exit_file" "$time_file"
343+
collect_suite_result "$suite" "$safe_name" "$out_file" "$exit_file" "$time_file"
276344
done
277345

278346
# Print skipped suites first

tests/test-bitlesson-select-routing.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ source "$SCRIPT_DIR/test-helpers.sh"
88
BITLESSON_SELECT="$PROJECT_ROOT/scripts/bitlesson-select.sh"
99
# Keep PATH isolation strict in missing-binary tests to avoid picking up
1010
# real codex/claude from user-local directories (e.g. ~/.nvm, ~/.local/bin).
11-
SAFE_BASE_PATH="/usr/bin:/bin:/usr/sbin:/sbin"
11+
# On NixOS, the shell toolchain itself lives under /run/current-system/sw/bin.
12+
SAFE_BASE_PATH="/run/current-system/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin"
1213

1314
echo "=========================================="
1415
echo "Bitlesson Select Routing Tests"
@@ -481,7 +482,7 @@ captured_args="$(cat "$CAPTURE_ARGS")"
481482
if [[ $exit_code -eq 0 ]] \
482483
&& echo "$stdout_out" | grep -q "BL-20260315-tracker-drift" \
483484
&& echo "$captured_args" | grep -q -- '--disable' \
484-
&& echo "$captured_args" | grep -q -- 'codex_hooks' \
485+
&& echo "$captured_args" | grep -q -- 'hooks' \
485486
&& echo "$captured_args" | grep -q -- '--skip-git-repo-check' \
486487
&& echo "$captured_args" | grep -q -- '--ephemeral' \
487488
&& echo "$captured_args" | grep -q -- 'read-only' \

0 commit comments

Comments
 (0)