From 5bd63e9be5c8da9ef0cc2948cf25b1ea28b37a03 Mon Sep 17 00:00:00 2001 From: Inference_ <68734681+Inference1@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:17:44 +0800 Subject: [PATCH] fix: support renamed Codex hooks feature --- docs/install-for-codex.md | 9 +-- hooks/loop-codex-stop-hook.sh | 29 +++++--- scripts/bitlesson-select.sh | 7 +- scripts/install-codex-hooks.sh | 19 +++--- scripts/lib/codex-hooks-feature.sh | 22 ++++++ tests/run-all-tests.sh | 1 + tests/test-bitlesson-select-routing.sh | 10 +-- tests/test-codex-hook-install.sh | 16 ++--- tests/test-codex-hooks-feature.sh | 87 ++++++++++++++++++++++++ tests/test-disable-nested-codex-hooks.sh | 21 +++--- 10 files changed, 177 insertions(+), 44 deletions(-) create mode 100644 scripts/lib/codex-hooks-feature.sh create mode 100755 tests/test-codex-hooks-feature.sh diff --git a/docs/install-for-codex.md b/docs/install-for-codex.md index 2c70a1cc..f727b9a5 100644 --- a/docs/install-for-codex.md +++ b/docs/install-for-codex.md @@ -26,7 +26,7 @@ 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 (`codex_hooks` on older supported releases) - 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` @@ -70,12 +70,12 @@ Installed files/directories: Verify native hooks: ```bash -codex features list | rg codex_hooks +codex features list | rg '^(hooks|codex_hooks)\s' sed -n '1,220p' "${CODEX_HOME:-$HOME/.codex}/hooks.json" ``` Expected: -- `codex_hooks` is `true` +- `hooks` is `true` (`codex_hooks` on older supported releases) - `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"` @@ -110,6 +110,7 @@ ls -la "${CODEX_HOME:-$HOME/.codex}/skills/humanize/scripts" If native exit gating does not trigger: ```bash -codex features enable codex_hooks +feature_name="$(codex features list | awk '$1 == "hooks" { print "hooks"; found=1; exit } $1 == "codex_hooks" { legacy="codex_hooks" } END { if (!found && legacy) print legacy }')" +codex features enable "$feature_name" sed -n '1,220p' "${CODEX_HOME:-$HOME/.codex}/hooks.json" ``` diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 0c191d4c..22302304 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -49,6 +49,7 @@ LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr" # Source portable timeout wrapper for git operations PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" source "$PLUGIN_ROOT/scripts/portable-timeout.sh" +source "$PLUGIN_ROOT/scripts/lib/codex-hooks-feature.sh" # Source methodology analysis library source "$SCRIPT_DIR/lib/methodology-analysis.sh" @@ -1171,13 +1172,21 @@ mkdir -p "$CACHE_DIR" # so older builds do not fail with an unknown-argument error. CODEX_DISABLE_HOOKS_ARGS=() _CODEX_FEATURE_CACHE="$CACHE_DIR/.codex-disable-hooks-supported" +_CODEX_HOOKS_FEATURE="" if [[ -f "$_CODEX_FEATURE_CACHE" ]]; then - [[ "$(cat "$_CODEX_FEATURE_CACHE")" == "yes" ]] && CODEX_DISABLE_HOOKS_ARGS=(--disable codex_hooks) -elif codex --help 2>&1 | grep -q -- '--disable'; then - CODEX_DISABLE_HOOKS_ARGS=(--disable codex_hooks) - echo "yes" > "$_CODEX_FEATURE_CACHE" 2>/dev/null -else - echo "no" > "$_CODEX_FEATURE_CACHE" 2>/dev/null + _CODEX_HOOKS_FEATURE="$(cat "$_CODEX_FEATURE_CACHE")" + case "$_CODEX_HOOKS_FEATURE" in + hooks|codex_hooks) ;; + *) _CODEX_HOOKS_FEATURE="" ;; + esac +fi +if [[ -z "$_CODEX_HOOKS_FEATURE" ]] \ + && codex --help 2>&1 | grep -q -- '--disable'; then + _CODEX_HOOKS_FEATURE="$(detect_codex_hooks_feature || true)" + printf '%s\n' "${_CODEX_HOOKS_FEATURE:-none}" > "$_CODEX_FEATURE_CACHE" 2>/dev/null +fi +if [[ -n "$_CODEX_HOOKS_FEATURE" ]]; then + CODEX_DISABLE_HOOKS_ARGS=(--disable "$_CODEX_HOOKS_FEATURE") fi # Build command arguments for summary review (codex exec) @@ -1256,14 +1265,14 @@ Provider: codex echo "# Review base ($review_base_type): $review_base" echo "# Timeout: $CODEX_TIMEOUT seconds" echo "" - echo "codex review ${CODEX_DISABLE_HOOKS_ARGS[*]} --base $review_base ${CODEX_REVIEW_ARGS[*]}" + echo "codex review ${CODEX_DISABLE_HOOKS_ARGS[*]:-} --base $review_base ${CODEX_REVIEW_ARGS[*]}" } > "$CODEX_REVIEW_CMD_FILE" echo "Code review command saved to: $CODEX_REVIEW_CMD_FILE" >&2 echo "Running codex review with timeout ${CODEX_TIMEOUT}s in $PROJECT_ROOT (base: $review_base)..." >&2 CODEX_REVIEW_EXIT_CODE=0 - (cd "$PROJECT_ROOT" && run_with_timeout "$CODEX_TIMEOUT" codex review "${CODEX_DISABLE_HOOKS_ARGS[@]}" --base "$review_base" "${CODEX_REVIEW_ARGS[@]}") \ + (cd "$PROJECT_ROOT" && run_with_timeout "$CODEX_TIMEOUT" codex review ${CODEX_DISABLE_HOOKS_ARGS[@]+"${CODEX_DISABLE_HOOKS_ARGS[@]}"} --base "$review_base" "${CODEX_REVIEW_ARGS[@]}") \ > "$CODEX_REVIEW_LOG_FILE" 2>&1 || CODEX_REVIEW_EXIT_CODE=$? echo "Code review exit code: $CODEX_REVIEW_EXIT_CODE" >&2 @@ -1682,7 +1691,7 @@ CODEX_PROMPT_CONTENT=$(cat "$REVIEW_PROMPT_FILE") echo "# Working directory: $PROJECT_ROOT" echo "# Timeout: $CODEX_TIMEOUT seconds" echo "" - echo "codex exec ${CODEX_DISABLE_HOOKS_ARGS[*]} ${CODEX_EXEC_ARGS[*]} \"\"" + echo "codex exec ${CODEX_DISABLE_HOOKS_ARGS[*]:-} ${CODEX_EXEC_ARGS[*]} \"\"" echo "" echo "# Prompt content:" echo "$CODEX_PROMPT_CONTENT" @@ -1692,7 +1701,7 @@ echo "Codex command saved to: $CODEX_CMD_FILE" >&2 echo "Running summary review with timeout ${CODEX_TIMEOUT}s..." >&2 CODEX_EXIT_CODE=0 -printf '%s' "$CODEX_PROMPT_CONTENT" | run_with_timeout "$CODEX_TIMEOUT" codex exec "${CODEX_DISABLE_HOOKS_ARGS[@]}" "${CODEX_EXEC_ARGS[@]}" - \ +printf '%s' "$CODEX_PROMPT_CONTENT" | run_with_timeout "$CODEX_TIMEOUT" codex exec ${CODEX_DISABLE_HOOKS_ARGS[@]+"${CODEX_DISABLE_HOOKS_ARGS[@]}"} "${CODEX_EXEC_ARGS[@]}" - \ > "$CODEX_STDOUT_FILE" 2> "$CODEX_STDERR_FILE" || CODEX_EXIT_CODE=$? echo "Codex exit code: $CODEX_EXIT_CODE" >&2 diff --git a/scripts/bitlesson-select.sh b/scripts/bitlesson-select.sh index fd19a445..dd3989d6 100755 --- a/scripts/bitlesson-select.sh +++ b/scripts/bitlesson-select.sh @@ -9,6 +9,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" source "$SCRIPT_DIR/lib/config-loader.sh" source "$SCRIPT_DIR/lib/model-router.sh" +source "$SCRIPT_DIR/lib/codex-hooks-feature.sh" source "$SCRIPT_DIR/../hooks/lib/project-root.sh" PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -191,9 +192,11 @@ run_selector() { if [[ "$provider" == "codex" ]]; then local codex_exec_args=() + local hooks_feature="" # Probe whether the installed Codex CLI supports --disable flag - if codex --help 2>&1 | grep -q -- '--disable'; then - codex_exec_args+=("--disable" "codex_hooks") + if codex --help 2>&1 | grep -q -- '--disable' \ + && hooks_feature="$(detect_codex_hooks_feature)"; then + codex_exec_args+=("--disable" "$hooks_feature") fi # Probe for --skip-git-repo-check and --ephemeral support if codex exec --help 2>&1 | grep -q -- '--skip-git-repo-check'; then diff --git a/scripts/install-codex-hooks.sh b/scripts/install-codex-hooks.sh index 407fe668..b17ce4a4 100755 --- a/scripts/install-codex-hooks.sh +++ b/scripts/install-codex-hooks.sh @@ -7,11 +7,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$SCRIPT_DIR/lib/codex-hooks-feature.sh" CODEX_CONFIG_DIR="${CODEX_HOME:-${HOME}/.codex}" RUNTIME_ROOT="$CODEX_CONFIG_DIR/skills/humanize" DRY_RUN="false" ENABLE_FEATURE="true" HOOKS_TEMPLATE="$REPO_ROOT/config/codex-hooks.json" +CODEX_HOOKS_FEATURE="" usage() { cat <<'EOF' @@ -23,7 +25,7 @@ Usage: Options: --codex-config-dir PATH Codex config dir (default: ${CODEX_HOME:-~/.codex}) --runtime-root PATH Installed Humanize runtime root (default: /skills/humanize) - --skip-enable-feature Do not run `codex features enable codex_hooks` + --skip-enable-feature Do not enable the native hooks feature --dry-run Print actions without writing -h, --help Show help EOF @@ -77,8 +79,8 @@ require_codex_hooks_support() { 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 ! CODEX_HOOKS_FEATURE="$(detect_codex_hooks_feature)"; then + die "Installed Codex CLI does not expose the hooks feature (legacy name: codex_hooks). Humanize Codex install requires Codex 0.114.0+." fi } @@ -174,13 +176,14 @@ PY enable_feature() { local config_dir="$1" + local feature_name="$2" [[ "$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 CODEX_HOME="$config_dir" codex features enable "$feature_name" >/dev/null 2>&1; then + log "enabled $feature_name feature in $config_dir/config.toml" else - die "failed to enable codex_hooks feature automatically in $config_dir/config.toml" + die "failed to enable $feature_name feature automatically in $config_dir/config.toml" fi } @@ -193,13 +196,13 @@ require_codex_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 $CODEX_HOOKS_FEATURE feature in $CODEX_CONFIG_DIR/config.toml" fi exit 0 fi merge_hooks_json "$HOOKS_FILE" "$HOOKS_TEMPLATE" "$RUNTIME_ROOT" -enable_feature "$CODEX_CONFIG_DIR" +enable_feature "$CODEX_CONFIG_DIR" "$CODEX_HOOKS_FEATURE" cat < "$CAPTURE_BIN/codex" <<'EOF' #!/usr/bin/env bash +if [[ "${1:-}" == "features" && "${2:-}" == "list" ]]; then + echo "hooks stable true" + exit 0 +fi # Respond to help probes with supported flags for arg in "$@"; do if [[ "$arg" == "--help" ]]; then - echo " --disable Disable a feature" - echo " --skip-git-repo-check Skip git repo check" - echo " --ephemeral Ephemeral mode" + echo " --disable --skip-git-repo-check --ephemeral" exit 0 fi done @@ -481,7 +483,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' \ diff --git a/tests/test-codex-hook-install.sh b/tests/test-codex-hook-install.sh index da20fb96..ff623a56 100755 --- a/tests/test-codex-hook-install.sh +++ b/tests/test-codex-hook-install.sh @@ -43,12 +43,12 @@ set -euo pipefail if [[ "${1:-}" == "features" && "${2:-}" == "list" ]]; then cat <<'LIST' -codex_hooks under development false +hooks stable true LIST exit 0 fi -if [[ "${1:-}" == "features" && "${2:-}" == "enable" && "${3:-}" == "codex_hooks" ]]; then +if [[ "${1:-}" == "features" && "${2:-}" == "enable" && "${3:-}" == "hooks" ]]; then printf 'CODEX_HOME=%s\n' "${CODEX_HOME:-}" >> "${TEST_CODEX_FEATURE_LOG:?}" mkdir -p "${CODEX_HOME:?}" : > "${CODEX_HOME}/.codex-hooks-enabled" @@ -134,9 +134,9 @@ else fi if [[ -f "$CODEX_HOME_DIR/.codex-hooks-enabled" ]]; then - pass "Codex install enables codex_hooks feature" + pass "Codex install enables current hooks feature" else - fail "Codex install enables codex_hooks feature" ".codex-hooks-enabled marker exists" "missing" + fail "Codex install enables current hooks feature" ".codex-hooks-enabled marker exists" "missing" fi if [[ -f "$HUMANIZE_USER_CONFIG" ]]; then @@ -323,11 +323,11 @@ else fail "Codex install rejects builds without native hooks support" "non-zero exit" "exit 0" fi -if grep -q "codex_hooks feature" "$TEST_DIR/install-unsupported.log"; then - pass "Unsupported Codex failure explains missing codex_hooks feature" +if grep -q "hooks feature" "$TEST_DIR/install-unsupported.log"; then + pass "Unsupported Codex failure explains missing hooks feature" else - fail "Unsupported Codex failure explains missing codex_hooks feature" \ - "error mentioning codex_hooks feature" \ + fail "Unsupported Codex failure explains missing hooks feature" \ + "error mentioning hooks feature" \ "$(cat "$TEST_DIR/install-unsupported.log")" fi diff --git a/tests/test-codex-hooks-feature.sh b/tests/test-codex-hooks-feature.sh new file mode 100755 index 00000000..21279f39 --- /dev/null +++ b/tests/test-codex-hooks-feature.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$SCRIPT_DIR/test-helpers.sh" + +FEATURE_HELPER="$PROJECT_ROOT/scripts/lib/codex-hooks-feature.sh" + +echo "==========================================" +echo "Codex Hooks Feature Detection Tests" +echo "==========================================" +echo "" + +setup_test_dir +FAKE_BIN="$TEST_DIR/bin" +mkdir -p "$FAKE_BIN" + +cat > "$FAKE_BIN/codex" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +[[ "${1:-}" == "features" && "${2:-}" == "list" ]] || exit 2 + +case "${TEST_FEATURE_SET:?}" in + current) + printf 'hooks stable true\n' + for i in $(seq 1 500); do + printf 'unrelated_feature_%s stable true\n' "$i" + done + ;; + legacy) + printf 'codex_hooks under development true\n' + ;; + both) + printf 'codex_hooks under development true\n' + printf 'hooks stable true\n' + ;; + unsupported) + printf 'apply_patch_freeform stable true\n' + ;; + failure) + exit 1 + ;; +esac +EOF +chmod +x "$FAKE_BIN/codex" + +if [[ ! -f "$FEATURE_HELPER" ]]; then + fail "shared Codex hooks feature detector exists" "$FEATURE_HELPER" "missing" + print_test_summary "Codex Hooks Feature Detection Tests" + exit $? +fi + +source "$FEATURE_HELPER" + +assert_detected_feature() { + local feature_set="$1" + local expected="$2" + local actual="" + + actual="$(PATH="$FAKE_BIN:$PATH" TEST_FEATURE_SET="$feature_set" detect_codex_hooks_feature)" + if [[ "$actual" == "$expected" ]]; then + pass "$feature_set Codex feature set resolves to $expected" + else + fail "$feature_set Codex feature set resolves to $expected" "$expected" "${actual:-}" + fi +} + +assert_detected_feature current hooks +assert_detected_feature legacy codex_hooks +assert_detected_feature both hooks + +for feature_set in unsupported failure; do + detected="" + exit_code=0 + detected="$(PATH="$FAKE_BIN:$PATH" TEST_FEATURE_SET="$feature_set" detect_codex_hooks_feature)" || exit_code=$? + if [[ $exit_code -ne 0 && -z "$detected" ]]; then + pass "$feature_set Codex feature set is rejected" + else + fail "$feature_set Codex feature set is rejected" "non-zero exit and no output" \ + "exit=$exit_code output=${detected:-}" + fi +done + +print_test_summary "Codex Hooks Feature Detection Tests" diff --git a/tests/test-disable-nested-codex-hooks.sh b/tests/test-disable-nested-codex-hooks.sh index c240ad65..5ec20b7b 100755 --- a/tests/test-disable-nested-codex-hooks.sh +++ b/tests/test-disable-nested-codex-hooks.sh @@ -83,6 +83,11 @@ HELP exit 0 fi +if [[ "\${1:-}" == "features" && "\${2:-}" == "list" ]]; then + echo "hooks stable true" + exit 0 +fi + printf '%s\n' "\$*" > "$args_file" subcommand="" @@ -188,22 +193,22 @@ REPO_IMPL="$TEST_DIR/repo-impl" setup_repo "$REPO_IMPL" run_loop_hook "$REPO_IMPL" "$TEST_DIR/impl.args" "false" -if grep -q -- 'exec --disable codex_hooks' "$TEST_DIR/impl.args"; then - pass "implementation-phase stop hook disables codex_hooks for codex exec" +if grep -q -- 'exec --disable hooks' "$TEST_DIR/impl.args"; then + pass "implementation-phase stop hook disables hooks for codex exec" else - fail "implementation-phase stop hook disables codex_hooks for codex exec" \ - "exec --disable codex_hooks" "$(cat "$TEST_DIR/impl.args" 2>/dev/null || echo missing)" + fail "implementation-phase stop hook disables hooks for codex exec" \ + "exec --disable hooks" "$(cat "$TEST_DIR/impl.args" 2>/dev/null || echo missing)" fi REPO_REVIEW="$TEST_DIR/repo-review" setup_repo "$REPO_REVIEW" run_loop_hook "$REPO_REVIEW" "$TEST_DIR/review.args" "true" -if grep -q -- 'review --disable codex_hooks' "$TEST_DIR/review.args"; then - pass "review-phase stop hook disables codex_hooks for codex review" +if grep -q -- 'review --disable hooks' "$TEST_DIR/review.args"; then + pass "review-phase stop hook disables hooks for codex review" else - fail "review-phase stop hook disables codex_hooks for codex review" \ - "review --disable codex_hooks" "$(cat "$TEST_DIR/review.args" 2>/dev/null || echo missing)" + fail "review-phase stop hook disables hooks for codex review" \ + "review --disable hooks" "$(cat "$TEST_DIR/review.args" 2>/dev/null || echo missing)" fi echo ""