Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/install-for-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ This will:
- Sync `humanize`, `humanize-gen-plan`, `humanize-refine-plan`, and `humanize-rlcr` into `${CODEX_HOME:-~/.codex}/skills`
- Copy runtime dependencies into `${CODEX_HOME:-~/.codex}/skills/humanize`
- Install/update native Humanize Stop hooks in `${CODEX_HOME:-~/.codex}/hooks.json`
- Enable the experimental `codex_hooks` feature in `${CODEX_HOME:-~/.codex}/config.toml` when `codex` is available
- Enable the native `hooks` feature in `${CODEX_HOME:-~/.codex}/config.toml` when `codex` is available
- Seed `~/.config/humanize/config.json` with a Codex/OpenAI `bitlesson_model` when that key is not already set
- Mark the install as `provider_mode: "codex-only"` when using `--target codex`
- Use RLCR defaults: `codex exec` with `gpt-5.5:high`, `codex review` with `gpt-5.5:high`

Requires Codex CLI `0.114.0` or newer for native hooks. Older Codex builds are not supported by the Codex install path.
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.

## Verify

Expand Down Expand Up @@ -70,12 +70,12 @@ Installed files/directories:
Verify native hooks:

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

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

```bash
codex features enable codex_hooks
codex features enable hooks
sed -n '1,220p' "${CODEX_HOME:-$HOME/.codex}/hooks.json"
```

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`.
4 changes: 2 additions & 2 deletions hooks/loop-codex-stop-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1172,9 +1172,9 @@ mkdir -p "$CACHE_DIR"
CODEX_DISABLE_HOOKS_ARGS=()
_CODEX_FEATURE_CACHE="$CACHE_DIR/.codex-disable-hooks-supported"
if [[ -f "$_CODEX_FEATURE_CACHE" ]]; then
[[ "$(cat "$_CODEX_FEATURE_CACHE")" == "yes" ]] && CODEX_DISABLE_HOOKS_ARGS=(--disable codex_hooks)
[[ "$(cat "$_CODEX_FEATURE_CACHE")" == "yes" ]] && CODEX_DISABLE_HOOKS_ARGS=(--disable hooks)
elif codex --help 2>&1 | grep -q -- '--disable'; then
CODEX_DISABLE_HOOKS_ARGS=(--disable codex_hooks)
CODEX_DISABLE_HOOKS_ARGS=(--disable hooks)
echo "yes" > "$_CODEX_FEATURE_CACHE" 2>/dev/null
else
echo "no" > "$_CODEX_FEATURE_CACHE" 2>/dev/null
Expand Down
2 changes: 1 addition & 1 deletion scripts/bitlesson-select.sh
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ run_selector() {
local codex_exec_args=()
# Probe whether the installed Codex CLI supports --disable flag
if codex --help 2>&1 | grep -q -- '--disable'; then
codex_exec_args+=("--disable" "codex_hooks")
codex_exec_args+=("--disable" "hooks")
fi
# Probe for --skip-git-repo-check and --ephemeral support
if codex exec --help 2>&1 | grep -q -- '--skip-git-repo-check'; then
Expand Down
50 changes: 41 additions & 9 deletions scripts/install-codex-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ RUNTIME_ROOT="$CODEX_CONFIG_DIR/skills/humanize"
DRY_RUN="false"
ENABLE_FEATURE="true"
HOOKS_TEMPLATE="$REPO_ROOT/config/codex-hooks.json"
HOOK_FEATURE_ENABLED=""

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

HOOKS_FILE="$CODEX_CONFIG_DIR/hooks.json"

require_codex_hooks_support() {
config_uses_legacy_codex_hooks() {
local config_file="$CODEX_CONFIG_DIR/config.toml"

[[ -f "$config_file" ]] || return 1

grep -Eq '^[[:space:]]*(features\.)?codex_hooks[[:space:]]*=' "$config_file"
}

require_native_hooks_support() {
if ! command -v codex >/dev/null 2>&1; then
die "Codex CLI with native hooks support is required. Install Codex 0.114.0+ first."
fi

if ! codex features list 2>/dev/null | grep -qE '^codex_hooks[[:space:]]'; then
die "Installed Codex CLI does not expose the codex_hooks feature. Humanize Codex install requires Codex 0.114.0+."
if config_uses_legacy_codex_hooks; then
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'."
fi

local features
local line
features="$(CODEX_HOME="$CODEX_CONFIG_DIR" codex features list 2>/dev/null)" || {
die "failed to inspect Codex features. Humanize Codex install requires the native 'hooks' feature."
}

line="$(printf '%s\n' "$features" | awk '$1 == "hooks" { print; exit }')"
if [[ -n "$line" ]]; then
HOOK_FEATURE_ENABLED="$(awk '{ print $NF }' <<<"$line")"
return 0
fi

if printf '%s\n' "$features" | awk '$1 == "codex_hooks" { found = 1 } END { exit found ? 0 : 1 }'; then
die "Installed Codex exposes only the legacy 'codex_hooks' feature. Humanize now requires the renamed 'hooks' feature. Upgrade Codex, then rerun the installer."
fi

die "Installed Codex CLI does not expose the native 'hooks' feature. Upgrade Codex, then rerun the installer."
}

merge_hooks_json() {
Expand Down Expand Up @@ -177,23 +204,28 @@ enable_feature() {

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

if CODEX_HOME="$config_dir" codex features enable codex_hooks >/dev/null 2>&1; then
log "enabled codex_hooks feature in $config_dir/config.toml"
if [[ "$HOOK_FEATURE_ENABLED" == "true" ]]; then
log "native hooks feature already enabled in $config_dir/config.toml"
return 0
fi

if CODEX_HOME="$config_dir" codex features enable hooks >/dev/null 2>&1; then
log "enabled hooks feature in $config_dir/config.toml"
else
die "failed to enable codex_hooks feature automatically in $config_dir/config.toml"
die "failed to enable hooks feature automatically in $config_dir/config.toml"
fi
}

log "codex config dir: $CODEX_CONFIG_DIR"
log "runtime root: $RUNTIME_ROOT"
log "hooks file: $HOOKS_FILE"

require_codex_hooks_support
require_native_hooks_support

if [[ "$DRY_RUN" == "true" ]]; then
log "DRY-RUN merge $HOOKS_TEMPLATE -> $HOOKS_FILE"
if [[ "$ENABLE_FEATURE" == "true" ]]; then
log "DRY-RUN enable codex_hooks feature in $CODEX_CONFIG_DIR/config.toml"
log "DRY-RUN enable hooks feature in $CODEX_CONFIG_DIR/config.toml"
fi
exit 0
fi
Expand Down
146 changes: 107 additions & 39 deletions tests/run-all-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ ZSH_TESTS=(
"test-zsh-monitor-safety.sh"
)

# Signal-heavy runtime tests are more stable when they run after the
# parallel batch finishes.
SERIAL_TESTS=(
"test-monitor-runtime.sh"
)

# Temp directory for per-suite output files
OUTPUT_DIR=$(mktemp -d)
trap "rm -rf $OUTPUT_DIR" EXIT
Expand Down Expand Up @@ -161,6 +167,16 @@ needs_zsh() {
return 1
}

needs_serial() {
local suite="$1"
for serial_test in "${SERIAL_TESTS[@]}"; do
if [[ "$suite" == "$serial_test" ]]; then
return 0
fi
done
return 1
}

# Format milliseconds as human-readable duration
format_ms() {
local ms="$1"
Expand All @@ -169,10 +185,79 @@ format_ms() {
echo "${s}.${frac}s"
}

# Launch all test suites in parallel
run_suite_capture() {
local suite="$1"
local out_file="$2"
local exit_file="$3"
local time_file="$4"
local suite_path="$SCRIPT_DIR/$suite"

if needs_zsh "$suite"; then
(
t_start=$(date +%s%3N)
zsh "$suite_path" >"$out_file" 2>&1
echo $? >"$exit_file"
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
)
else
(
t_start=$(date +%s%3N)
"$suite_path" >"$out_file" 2>&1
echo $? >"$exit_file"
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
)
fi
}

collect_suite_result() {
local suite="$1"
local safe_name="$2"
local out_file="$3"
local exit_file="$4"
local time_file="$5"
local exit_code
local output
local elapsed_ms
local elapsed_display
local output_stripped
local passed
local failed
local line
local zsh_label

exit_code=$(cat "$exit_file" 2>/dev/null || echo "1")
output=$(cat "$out_file" 2>/dev/null || echo "")
elapsed_ms=$(cat "$time_file" 2>/dev/null || echo "0")
elapsed_display=$(format_ms "$elapsed_ms")

# Strip ANSI escape codes and extract pass/fail counts
output_stripped=$(echo "$output" | sed "s/${esc}\\[[0-9;]*m//g")
passed=$(echo "$output_stripped" | grep -oE 'Passed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")
failed=$(echo "$output_stripped" | grep -oE 'Failed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")

TOTAL_PASSED=$((TOTAL_PASSED + passed))
TOTAL_FAILED=$((TOTAL_FAILED + failed))

if [[ $exit_code -ne 0 ]] || [[ "$failed" -gt 0 ]]; then
FAILED_SUITES+=("$suite")
line=$(echo -e "${RED}FAILED${NC}: $suite (exit code: $exit_code, failed: $failed, ${elapsed_display})")
printf '%d\t%s\n' "$elapsed_ms" "$line" >> "$SORT_FILE"
# Preserve the full suite log so CI surfaces the exact failing assertion.
printf '%s\n' "$output" > "$OUTPUT_DIR/${safe_name}.detail"
else
zsh_label=""
needs_zsh "$suite" && zsh_label=" (zsh)"
line=$(echo -e "${GREEN}PASSED${NC}: $suite${zsh_label} ($passed tests, ${elapsed_display})")
printf '%d\t%s\n' "$elapsed_ms" "$line" >> "$SORT_FILE"
fi
}

# Launch all test suites in parallel, except signal-heavy runtime tests which
# run serially after the parallel batch finishes.
declare -A PIDS # suite -> PID
declare -A SKIPPED # suite -> reason
ACTIVE_PIDS=()
SERIAL_SUITES=()

for suite in "${TEST_SUITES[@]}"; do
suite_path="$SCRIPT_DIR/$suite"
Expand All @@ -186,25 +271,21 @@ for suite in "${TEST_SUITES[@]}"; do
continue
fi

if needs_serial "$suite"; then
SERIAL_SUITES+=("$suite")
continue
fi

if needs_zsh "$suite"; then
if ! command -v zsh &>/dev/null; then
SKIPPED["$suite"]="zsh not available"
continue
fi
(
t_start=$(date +%s%3N)
zsh "$suite_path" >"$out_file" 2>&1
echo $? >"$exit_file"
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
) &
else
(
t_start=$(date +%s%3N)
"$suite_path" >"$out_file" 2>&1
echo $? >"$exit_file"
echo $(( $(date +%s%3N) - t_start )) >"$time_file"
) &
fi

(
run_suite_capture "$suite" "$out_file" "$exit_file" "$time_file"
) &
PIDS["$suite"]=$!
ACTIVE_PIDS+=("${PIDS[$suite]}")

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

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

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

exit_code=$(cat "$exit_file" 2>/dev/null || echo "1")
output=$(cat "$out_file" 2>/dev/null || echo "")
elapsed_ms=$(cat "$time_file" 2>/dev/null || echo "0")
elapsed_display=$(format_ms "$elapsed_ms")

# Strip ANSI escape codes and extract pass/fail counts
output_stripped=$(echo "$output" | sed "s/${esc}\\[[0-9;]*m//g")
passed=$(echo "$output_stripped" | grep -oE 'Passed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")
failed=$(echo "$output_stripped" | grep -oE 'Failed:[[:space:]]*[0-9]+' | grep -oE '[0-9]+$' | tail -1 || echo "0")

TOTAL_PASSED=$((TOTAL_PASSED + passed))
TOTAL_FAILED=$((TOTAL_FAILED + failed))
# Run serial suites after the parallel batch finishes.
for suite in "${SERIAL_SUITES[@]}"; do
safe_name="$(echo "$suite" | tr '/' '_')"
out_file="$OUTPUT_DIR/${safe_name}.out"
exit_file="$OUTPUT_DIR/${safe_name}.exit"
time_file="$OUTPUT_DIR/${safe_name}.time"

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

# Print skipped suites first
Expand Down
5 changes: 3 additions & 2 deletions tests/test-bitlesson-select-routing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ source "$SCRIPT_DIR/test-helpers.sh"
BITLESSON_SELECT="$PROJECT_ROOT/scripts/bitlesson-select.sh"
# Keep PATH isolation strict in missing-binary tests to avoid picking up
# real codex/claude from user-local directories (e.g. ~/.nvm, ~/.local/bin).
SAFE_BASE_PATH="/usr/bin:/bin:/usr/sbin:/sbin"
# On NixOS, the shell toolchain itself lives under /run/current-system/sw/bin.
SAFE_BASE_PATH="/run/current-system/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin"

echo "=========================================="
echo "Bitlesson Select Routing Tests"
Expand Down Expand Up @@ -481,7 +482,7 @@ captured_args="$(cat "$CAPTURE_ARGS")"
if [[ $exit_code -eq 0 ]] \
&& echo "$stdout_out" | grep -q "BL-20260315-tracker-drift" \
&& echo "$captured_args" | grep -q -- '--disable' \
&& echo "$captured_args" | grep -q -- 'codex_hooks' \
&& echo "$captured_args" | grep -q -- 'hooks' \
&& echo "$captured_args" | grep -q -- '--skip-git-repo-check' \
&& echo "$captured_args" | grep -q -- '--ephemeral' \
&& echo "$captured_args" | grep -q -- 'read-only' \
Expand Down
Loading