#!/usr/bin/env bash set -euo pipefail # ======================================== # Source Shared Libraries # ======================================== 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/../hooks/lib/project-root.sh" PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(resolve_project_root)" || { echo "Error: Cannot determine project root." >&2 echo " Set CLAUDE_PROJECT_DIR or run inside a git repository." >&2 exit 1 } MERGED_CONFIG="$(load_merged_config "$PLUGIN_ROOT" "$PROJECT_ROOT")" BITLESSON_MODEL="$(get_config_value "$MERGED_CONFIG" "bitlesson_model")" BITLESSON_MODEL="${BITLESSON_MODEL:-haiku}" CODEX_FALLBACK_MODEL="$(get_config_value "$MERGED_CONFIG" "codex_model")" CODEX_FALLBACK_MODEL="${CODEX_FALLBACK_MODEL:-$DEFAULT_CODEX_MODEL}" PROVIDER_MODE="$(get_config_value "$MERGED_CONFIG" "provider_mode")" PROVIDER_MODE="${PROVIDER_MODE:-auto}" # Source portable timeout wrapper source "$SCRIPT_DIR/portable-timeout.sh" # Source shared loop library (kept for consistency with ask-codex.sh) HOOKS_LIB_DIR="$(cd "$SCRIPT_DIR/../hooks/lib" && pwd)" source "$HOOKS_LIB_DIR/loop-common.sh" usage() { cat <<'USAGE_EOF' >&2 Usage: bitlesson-select.sh --task --paths --bitlesson-file Output (exactly): LESSON_IDS: RATIONALE: USAGE_EOF } TASK="" PATHS="" BITLESSON_FILE="" while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; --task) TASK="${2:-}" shift 2 ;; --paths) PATHS="${2:-}" shift 2 ;; --bitlesson-file) BITLESSON_FILE="${2:-}" shift 2 ;; *) echo "Error: Unknown argument: $1" >&2 usage exit 1 ;; esac done if [[ -z "$TASK" ]]; then echo "Error: --task is required and must be non-empty" >&2 usage exit 1 fi if [[ -z "$PATHS" ]]; then echo "Error: --paths is required and must be non-empty" >&2 usage exit 1 fi if [[ -z "$BITLESSON_FILE" ]]; then echo "Error: --bitlesson-file is required" >&2 usage exit 1 fi if [[ ! -f "$BITLESSON_FILE" ]]; then echo "Error: BitLesson file not found: $BITLESSON_FILE" >&2 exit 1 fi BITLESSON_CONTENT="$(cat "$BITLESSON_FILE")" if [[ -z "$(printf '%s' "$BITLESSON_CONTENT" | tr -d ' \t\n\r')" ]]; then echo "Error: BitLesson file is empty (whitespace only): $BITLESSON_FILE" >&2 exit 1 fi if ! printf '%s\n' "$BITLESSON_CONTENT" | grep -Eq '^[[:space:]]*##[[:space:]]+Lesson:'; then printf 'LESSON_IDS: NONE\n' printf 'RATIONALE: The BitLesson file has no recorded lessons yet.\n' exit 0 fi # ======================================== # Determine Provider from BITLESSON_MODEL # ======================================== BITLESSON_PROVIDER="$(detect_provider "$BITLESSON_MODEL")" if [[ "$PROVIDER_MODE" == "codex-only" ]] && [[ "$BITLESSON_PROVIDER" == "claude" ]]; then BITLESSON_MODEL="$CODEX_FALLBACK_MODEL" BITLESSON_PROVIDER="codex" fi # ======================================== # Conditional Dependency Check (with fallback) # ======================================== if ! check_provider_dependency "$BITLESSON_PROVIDER" 2>/dev/null; then # Fall back to codex provider when the configured provider binary is missing BITLESSON_MODEL="$DEFAULT_CODEX_MODEL" BITLESSON_PROVIDER="codex" check_provider_dependency "$BITLESSON_PROVIDER" fi # ======================================== # Detect Project Root (for -C) # ======================================== BITLESSON_DIR="$(cd "$(dirname "$BITLESSON_FILE")" && pwd -P)" if git -C "$BITLESSON_DIR" rev-parse --show-toplevel &>/dev/null; then CODEX_PROJECT_ROOT="$(git -C "$BITLESSON_DIR" rev-parse --show-toplevel)" else CODEX_PROJECT_ROOT="$BITLESSON_DIR" fi # ======================================== # Build Selector Prompt # ======================================== PROMPT="$(cat < RATIONALE: EOF )" # ======================================== # Run Selector (Codex or Claude) # ======================================== SELECTOR_TIMEOUT=120 run_selector() { local provider="$1" local model="$2" if [[ "$provider" == "codex" ]]; then 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" "hooks") fi # Probe for --skip-git-repo-check and --ephemeral support if codex exec --help 2>&1 | grep -q -- '--skip-git-repo-check'; then codex_exec_args+=("--skip-git-repo-check") fi if codex exec --help 2>&1 | grep -q -- '--ephemeral'; then codex_exec_args+=("--ephemeral") fi codex_exec_args+=( "-s" "read-only" "-m" "$model" "-c" "model_reasoning_effort=low" "-C" "$CODEX_PROJECT_ROOT" ) printf '%s' "$PROMPT" | run_with_timeout "$SELECTOR_TIMEOUT" codex exec "${codex_exec_args[@]}" - return $? fi if [[ "$provider" == "claude" ]]; then printf '%s' "$PROMPT" | run_with_timeout "$SELECTOR_TIMEOUT" claude --print --model "$model" - return $? fi echo "Error: Unsupported BitLesson provider '$provider'" >&2 return 1 } CODEX_EXIT_CODE=0 RAW_OUTPUT="$(run_selector "$BITLESSON_PROVIDER" "$BITLESSON_MODEL" 2>&1)" || CODEX_EXIT_CODE=$? if [[ $CODEX_EXIT_CODE -eq 124 ]]; then echo "Error: BitLesson selector timed out after ${SELECTOR_TIMEOUT} seconds" >&2 exit 124 fi if [[ $CODEX_EXIT_CODE -ne 0 ]]; then echo "Error: BitLesson selector failed (exit code $CODEX_EXIT_CODE)" >&2 printf '%s\n' "$RAW_OUTPUT" >&2 exit "$CODEX_EXIT_CODE" fi # ======================================== # Enforce Stable Output Format # ======================================== LESSON_IDS_VALUE="$( printf '%s\n' "$RAW_OUTPUT" \ | sed -n 's/^[[:space:]]*LESSON_IDS:[[:space:]]*//p' \ | head -n 1 \ | tr -d '\r' \ | sed 's/[[:space:]]*$//' )" RATIONALE_VALUE="$( printf '%s\n' "$RAW_OUTPUT" \ | sed -n 's/^[[:space:]]*RATIONALE:[[:space:]]*//p' \ | head -n 1 \ | tr -d '\r' \ | sed 's/[[:space:]]*$//' )" if [[ -z "$LESSON_IDS_VALUE" || -z "$RATIONALE_VALUE" ]]; then echo "Error: Unexpected selector output format (expected LESSON_IDS and RATIONALE lines)" >&2 echo "" >&2 echo "Raw output:" >&2 printf '%s\n' "$RAW_OUTPUT" >&2 exit 1 fi printf 'LESSON_IDS: %s\n' "$LESSON_IDS_VALUE" printf 'RATIONALE: %s\n' "$RATIONALE_VALUE"