diff --git a/DreamcoderShell/.bashrc b/DreamcoderShell/.bashrc index 9d97f5ba..e2a17d0a 100644 --- a/DreamcoderShell/.bashrc +++ b/DreamcoderShell/.bashrc @@ -1,5 +1,6 @@ # shellcheck shell=bash disable=SC1090,SC1091 -set -euo pipefail +# Dreamcoder interactive Bash. No errexit/nounset/pipefail here: in an +# interactive shell they close the terminal on the first failing command. [[ -z "${TERM:-}" || "${TERM}" == "dumb" ]] && export TERM="xterm-256color" export COLORTERM="${COLORTERM:-truecolor}" [[ "${-}" != *i* ]] && return @@ -16,6 +17,13 @@ BUN_COMPLETION="${HOME}/.bun/_bun" [[ -f "${BUN_COMPLETION}" ]] && source "${BUN_COMPLETION}" export BUN_INSTALL="${HOME}/.bun" [[ -d "${BUN_INSTALL}/bin" ]] && export PATH="${BUN_INSTALL}/bin:${PATH}" +# gga reviews: Codex provider with the pinned model shim first (docs/configuration/gga.md). +_dc_gga_bin="${XDG_CONFIG_HOME:-${HOME}/.config}/gga/bin" +if [[ -d "${_dc_gga_bin}" ]]; then + export GGA_PROVIDER="codex" + [[ ":${PATH}:" == *":${_dc_gga_bin}:"* ]] || export PATH="${_dc_gga_bin}:${PATH}" +fi +unset _dc_gga_bin SHELL_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/shell" for group in core aliases functions; do for file in "${SHELL_DIR}/${group}"/*.sh; do [[ -f "${file}" ]] && source "${file}"; done @@ -27,7 +35,7 @@ if command -v starship >/dev/null; then fi command -v zoxide >/dev/null && eval "$(zoxide init bash)" [[ -f "${HOME}/.cargo/env" ]] && source "${HOME}/.cargo/env" -unset PATH_DIRS dir group file BUN_COMPLETION SHELL_DIR -. "$HOME/.cargo/env" -export PATH="$HOME/.local/share/fnm:$PATH" -eval "$(fnm env --use-on-cd --shell bash)" +_dc_fnm_dir="${HOME}/.local/share/fnm" +[[ -d "${_dc_fnm_dir}" && ":${PATH}:" != *":${_dc_fnm_dir}:"* ]] && export PATH="${_dc_fnm_dir}:${PATH}" +command -v fnm >/dev/null && eval "$(fnm env --use-on-cd --shell bash)" +unset PATH_DIRS dir group file BUN_COMPLETION SHELL_DIR _dc_fnm_dir diff --git a/DreamcoderShell/.config/fish/conf.d/27-dreamcoder-gga-pin.fish b/DreamcoderShell/.config/fish/conf.d/27-dreamcoder-gga-pin.fish new file mode 100644 index 00000000..40f541ec --- /dev/null +++ b/DreamcoderShell/.config/fish/conf.d/27-dreamcoder-gga-pin.fish @@ -0,0 +1,9 @@ +# gga reviews: Codex provider with the pinned model shim first on PATH. +# The shim only changes `codex exec` calls made by gga (docs/configuration/gga.md). +set -l _dc_config_home $HOME/.config +set -q XDG_CONFIG_HOME[1]; and test -n "$XDG_CONFIG_HOME"; and set _dc_config_home $XDG_CONFIG_HOME +set -l _dc_gga_bin $_dc_config_home/gga/bin +if test -d $_dc_gga_bin + set -gx GGA_PROVIDER codex + fish_add_path --global --move --path $_dc_gga_bin +end diff --git a/DreamcoderShell/.zshrc b/DreamcoderShell/.zshrc index 38de89a7..abbaf5d5 100644 --- a/DreamcoderShell/.zshrc +++ b/DreamcoderShell/.zshrc @@ -10,6 +10,13 @@ BUN_INSTALL="${HOME}/.bun" [[ -s "${BUN_INSTALL}/_bun" ]] && source "${BUN_INSTALL}/_bun" [[ -d "${BUN_INSTALL}/bin" ]] && path=("${BUN_INSTALL}/bin" "${path[@]}") export BUN_INSTALL +# gga reviews: Codex provider with the pinned model shim first (docs/configuration/gga.md). +_dc_gga_bin="${XDG_CONFIG_HOME:-${HOME}/.config}/gga/bin" +if [[ -d "${_dc_gga_bin}" ]]; then + export GGA_PROVIDER="codex" + path=("${_dc_gga_bin}" "${path[@]}") +fi +unset _dc_gga_bin # ── Dreamcoder Theme Hooks ───────────────────────────────────── # Source shell-level theme snippets based on current mode. @@ -19,7 +26,9 @@ _dc_mode_cache="${XDG_CACHE_HOME:-${HOME}/.cache}/dreamcoder/cursor-cli.env" _dc_mode="$([[ -r "${_dc_mode_cache}" ]] && sed -nE 's/^(export )?DREAMCODER_THEME_MODE="?(light|dark)"?$/\2/p' "${_dc_mode_cache}" | tail -n1)" _dc_mode="${_dc_mode:-${DREAMCODER_THEME_MODE:-dark}}" export DREAMCODER_THEME_MODE="${_dc_mode}" -_dc_theme_dir="${DREAMCODER_DOTS_DIR:-${HOME}/Documents/PROYECTOS/dreamcoder-dots}/themes/dreamcoder" +# Without DREAMCODER_DOTS_DIR, resolve the repo through the stowed ~/.zshrc link +# (/DreamcoderShell/.zshrc). +_dc_theme_dir="${DREAMCODER_DOTS_DIR:-${${:-${HOME}/.zshrc}:A:h:h}}/themes/dreamcoder" [[ -f "${_dc_theme_dir}/ls-colors-dreamcoder-${_dc_mode}.sh" ]] && source "${_dc_theme_dir}/ls-colors-dreamcoder-${_dc_mode}.sh" [[ -f "${_dc_theme_dir}/bat-dreamcoder-${_dc_mode}.sh" ]] && source "${_dc_theme_dir}/bat-dreamcoder-${_dc_mode}.sh" diff --git a/docs/README.md b/docs/README.md index a9573038..406b9b0c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -66,6 +66,7 @@ Configuration guides for individual components. | [Multiplexer](configuration/multiplexer-config.md) | Tmux, Zellij | | [ML4W Integration](configuration/ml4w.md) | Profile-driven keybindings, native dispatchers, file layout | | [Theme System](configuration/theme-system.md) | Theme engine architecture, tokens, variants | +| [gga Model Pin](configuration/gga.md) | Gentleman Guardian Angel reviews pinned to Codex `gpt-6.1-sol` (medium) | --- diff --git a/docs/configuration/gga.md b/docs/configuration/gga.md new file mode 100644 index 00000000..5f2c3940 --- /dev/null +++ b/docs/configuration/gga.md @@ -0,0 +1,47 @@ +# Gentleman Guardian Angel (gga) Model Pin + +← Back to [docs/README.md](../README.md) + +Every gga review, in every repository, runs on the OpenAI Codex provider with +`gpt-6.1-sol` at `medium` reasoning effort. + +## What is pinned and where + +| File | Role | +|------|------| +| `~/.config/gga/bin/codex` | Shim (source: `scripts/gga-codex-shim.sh`). Injects `-m -c model_reasoning_effort=` into `codex exec` only when a `gga` process is an ancestor and no `-m`/`--model` is given. Everything else passes through to the real `codex`. | +| `~/.config/gga/pin.env` | `GGA_PIN_MODEL` and `GGA_PIN_EFFORT`. Created once; never overwritten. | +| `~/.config/gga/config` | Marked block at the end (`# >>> dreamcoder gga pin >>>`): `PROVIDER`/`GGA_PROVIDER="codex"` and the shim dir first on `PATH`. | +| `~/.config/environment.d/50-gga-pin.conf` | Same provider and `PATH` for desktop-launched programs (IDE git hooks). | +| Fish, Zsh, Bash startup | Export `GGA_PROVIDER=codex` and put the shim dir first on `PATH` when it exists. | + +## Why a shim + +gga runs `codex exec ""` without model options, so it would use the model +that `~/.codex/config.toml` selects for interactive work. The shim pins the review +model without touching that file, and interactive `codex` stays unchanged. + +`GGA_PROVIDER` is applied by gga after the global config and the project `.gga`, +so it beats a project's own `PROVIDER`. + +## Change the model or effort + +Edit `~/.config/gga/pin.env`: + +```bash +GGA_PIN_MODEL="gpt-6.1-sol" +GGA_PIN_EFFORT="high" +``` + +The next review uses it; nothing needs re-installing. + +## After `gentle-ai sync` or `gentle-ai install` + +Both rewrite the whole `~/.config/gga/config` and drop the pin block. The shim, +`pin.env`, the shell exports and `environment.d` survive, so reviews keep the +pinned model; re-apply the config block with either command: + +```bash +./scripts/dreamcoder repair # runs the installer when gga is on PATH +./scripts/install-gga-pin.sh # idempotent; --dry-run shows pending changes +``` diff --git a/scripts/dreamcoder-lib.sh b/scripts/dreamcoder-lib.sh index 45311ad0..7a9418c4 100644 --- a/scripts/dreamcoder-lib.sh +++ b/scripts/dreamcoder-lib.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -euo pipefail -# used by sourcing scripts (dreamcoder.sh, dreamcoder-maintenance.sh) +# Sourced by dreamcoder-maintenance.sh, which sets strict mode itself; a sourced +# library must not change its caller's shell options. # Stow packages owned by install/repair; keep in sync with # src/dreamcoder_theme/installer.py (installer_plan()["modules"]). DREAMCODER_MODULES=(DreamcoderShell DreamcoderKitty DreamcoderGhostty DreamcoderFastfetch DreamcoderWarp DreamcoderBat DreamcoderSystemd) @@ -8,7 +8,13 @@ DREAMCODER_TARGETS=("${CONFIG_HOME}/kitty" "${CONFIG_HOME}/ghostty" "${CONFIG_HO dreamcoder_control() { PYTHONPATH="${DREAMCODER_DOTS_DIR}/src${PYTHONPATH:+:${PYTHONPATH}}" python3 -m dreamcoder_theme.control "$@"; } dreamcoder_json_get() { python3 -c 'import json,sys; print(json.load(sys.stdin)[sys.argv[1]])' "$1"; } dreamcoder_backup() { dreamcoder_control backup create "${DREAMCODER_TARGETS[@]}" --reason "${1}" --json; } -dreamcoder_apply_hooks() { "${DREAMCODER_DOTS_DIR}/scripts/apply-ml4w-hooks.sh"; "${DREAMCODER_DOTS_DIR}/scripts/apply-cli-env-hooks.sh"; "${DREAMCODER_DOTS_DIR}/scripts/apply-fastfetch-assets.sh"; } +dreamcoder_apply_hooks() { + "${DREAMCODER_DOTS_DIR}/scripts/apply-ml4w-hooks.sh" + "${DREAMCODER_DOTS_DIR}/scripts/apply-cli-env-hooks.sh" + "${DREAMCODER_DOTS_DIR}/scripts/apply-fastfetch-assets.sh" + # Re-apply the gga model pin that `gentle-ai sync` drops from ~/.config/gga/config. + if command -v gga >/dev/null; then "${DREAMCODER_DOTS_DIR}/scripts/install-gga-pin.sh"; fi +} dreamcoder_enable_timer() { command -v systemctl >/dev/null || return 0; systemctl --user daemon-reload || true; systemctl --user enable --now dreamcoder-theme-auto.timer || true; } # Print every target (relative to $HOME) that blocks stowing the modules, diff --git a/scripts/gga-codex-shim.sh b/scripts/gga-codex-shim.sh new file mode 100755 index 00000000..202cfe50 --- /dev/null +++ b/scripts/gga-codex-shim.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# GGA-only model pin for the Codex CLI, installed as ~/.config/gga/bin/codex. +# +# Gentleman Guardian Angel (gga) runs `codex exec ""` with no model +# options, so reviews would follow whatever ~/.codex/config.toml selects for +# interactive work. This shim sits first on PATH and, only for `codex exec` +# calls made by a gga process, injects the pinned model and reasoning effort. +# Every other invocation (interactive codex, other tools) passes through +# untouched, and an explicit -m/--model always wins. +# +# The pin lives in ~/.config/gga/pin.env (GGA_PIN_MODEL, GGA_PIN_EFFORT) and +# never in ~/.config/gga/config, which `gentle-ai sync` rewrites wholesale. +# Installed by scripts/install-gga-pin.sh; see docs/configuration/gga.md. +set -euo pipefail + +readonly DEFAULT_MODEL="gpt-6.1-sol" +readonly DEFAULT_EFFORT="medium" +readonly MAX_ANCESTORS=10 +# Linux procfs; without it the shim never pins and passes every call through. +readonly PROC_ROOT="${GGA_SHIM_PROC_ROOT:-/proc}" + +self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" + +find_real_codex() { + local candidate + while IFS= read -r candidate; do + [[ "$(cd "$(dirname "${candidate}")" && pwd -P)" == "${self_dir}" ]] && continue + printf '%s\n' "${candidate}" + return 0 + done < <(type -ap codex) + return 1 +} + +# True when one of the first MAX_ANCESTORS ancestors is a gga process: its +# comm is `gga` (the kernel names a script after its file) or it runs an +# interpreter whose script argument is named gga. +has_gga_ancestor() { + local pid="${PPID}" depth comm ppid arg + local -a argv + [[ -d "${PROC_ROOT}/self" ]] || return 1 + for ((depth = 0; depth < MAX_ANCESTORS; depth++)); do + [[ "${pid}" =~ ^[0-9]+$ && "${pid}" -gt 1 && -r "${PROC_ROOT}/${pid}/status" ]] || return 1 + comm="$(cat "${PROC_ROOT}/${pid}/comm" 2>/dev/null)" || return 1 + [[ "${comm}" == "gga" ]] && return 0 + argv=() + while IFS= read -r -d '' arg; do argv+=("${arg}"); done <"${PROC_ROOT}/${pid}/cmdline" 2>/dev/null || true + for arg in "${argv[@]:0:2}"; do + [[ "${arg##*/}" == "gga" ]] && return 0 + done + ppid="$(sed -n 's/^PPid:[[:space:]]*//p' "${PROC_ROOT}/${pid}/status" 2>/dev/null)" || return 1 + pid="${ppid}" + done + return 1 +} + +has_explicit_model() { + local arg + for arg in "$@"; do + case "${arg}" in -m | --model | --model=* | -m?*) return 0 ;; esac + done + return 1 +} + +# Read GGA_PIN_MODEL / GGA_PIN_EFFORT from pin.env without executing it. +read_pin() { + local key="$1" fallback="$2" file line value="" + file="${XDG_CONFIG_HOME:-${HOME}/.config}/gga/pin.env" + if [[ -r "${file}" ]]; then + while IFS= read -r line || [[ -n "${line}" ]]; do + line="${line#export }" + [[ "${line}" == "${key}="* ]] || continue + value="${line#"${key}"=}" + value="${value#[\"\']}" + value="${value%[\"\']}" + done <"${file}" + fi + printf '%s\n' "${value:-${fallback}}" +} + +real="$(find_real_codex)" || { + echo "gga codex shim: the real codex CLI was not found on PATH" >&2 + exit 127 +} + +if [[ "${1:-}" == "exec" ]] && ! has_explicit_model "$@" && has_gga_ancestor; then + shift + model="$(read_pin GGA_PIN_MODEL "${DEFAULT_MODEL}")" + effort="$(read_pin GGA_PIN_EFFORT "${DEFAULT_EFFORT}")" + exec "${real}" exec -m "${model}" -c "model_reasoning_effort=${effort}" "$@" +fi +exec "${real}" "$@" diff --git a/scripts/install-gga-pin.sh b/scripts/install-gga-pin.sh new file mode 100755 index 00000000..468c42e8 --- /dev/null +++ b/scripts/install-gga-pin.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# Pin Gentleman Guardian Angel (gga) reviews to the Codex provider and model. +# +# `gentle-ai sync` / `gentle-ai install` rewrite the whole gga config, so the pin +# is kept outside it where possible and re-applied here idempotently: +# 1. /bin/codex shim that injects the model for gga's `codex exec` +# 2. /pin.env model and effort (created once, never overwritten) +# 3. /config marked block at the end: PROVIDER/GGA_PROVIDER=codex +# and the shim dir first on PATH +# 4. environment.d/50-gga-pin.conf the same for desktop-launched programs +# One line is printed per changed file; nothing when everything is current. +# +# Usage: install-gga-pin.sh [--dry-run] +set -euo pipefail + +readonly BLOCK_BEGIN="# >>> dreamcoder gga pin >>>" +readonly BLOCK_END="# <<< dreamcoder gga pin <<<" + +dry_run=0 +case "${1:-}" in + "") ;; + --dry-run) dry_run=1 ;; + -h | --help) + sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + printf 'install-gga-pin: unknown option: %s\n' "$1" >&2 + exit 2 + ;; +esac + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +config_home="${XDG_CONFIG_HOME:-${HOME}/.config}" +gga_dir="${config_home}/gga" +shim_dir="${gga_dir}/bin" +shim_src="${script_dir}/gga-codex-shim.sh" +shim_dst="${shim_dir}/codex" +pin_file="${gga_dir}/pin.env" +config_file="${gga_dir}/config" +env_file="${config_home}/environment.d/50-gga-pin.conf" + +# The shim dir is written unescaped into shell code (gga config) and into +# environment.d, so only plain path characters are accepted. +if [[ ! "${shim_dir}" =~ ^/[A-Za-z0-9._@+/-]+$ ]]; then + printf 'install-gga-pin: unsupported characters in %s; set a plain HOME/XDG_CONFIG_HOME\n' "${shim_dir}" >&2 + exit 1 +fi + +[[ -r "${shim_src}" ]] || { + printf 'install-gga-pin: shim source not found: %s\n' "${shim_src}" >&2 + exit 1 +} + +# Write stdin to $1 when its content differs; report what changed. +write_if_changed() { + local target="$1" label="$2" tmp + tmp="$(mktemp)" + cat >"${tmp}" + if [[ -f "${target}" ]] && cmp -s "${tmp}" "${target}"; then + rm -f "${tmp}" + return 0 + fi + if ((dry_run)); then + printf '→ Would update %s (%s)\n' "${target}" "${label}" + else + mkdir -p "$(dirname "${target}")" + # cat (not mv) keeps an existing file's inode, mode and symlink target. + cat "${tmp}" >"${target}" + printf '✓ %s: %s\n' "${label}" "${target}" + fi + rm -f "${tmp}" +} + +install_shim() { + write_if_changed "${shim_dst}" "gga codex shim" <"${shim_src}" + if ((!dry_run)) && [[ -f "${shim_dst}" && ! -x "${shim_dst}" ]]; then + chmod 0755 "${shim_dst}" + fi +} + +install_pin_env() { + [[ -e "${pin_file}" ]] && return 0 + write_if_changed "${pin_file}" "gga pin model/effort" <<'EOF' +# Model used for every gga review; read by ~/.config/gga/bin/codex. +# Edit freely: scripts/install-gga-pin.sh never overwrites this file. +GGA_PIN_MODEL="gpt-6.1-sol" +GGA_PIN_EFFORT="medium" +EOF +} + +pin_block() { + cat <&2 + return 1 + fi + rest="$(awk -v begin="${BLOCK_BEGIN}" -v end="${BLOCK_END}" ' + $0 == begin { skip = 1; held = 0; next } + skip { if ($0 == end) skip = 0; next } + { + if (held) print "" + held = 0 + if ($0 == "") held = 1; else print + } + END { if (held) print "" } + ' "${config_file}")" + fi + if [[ -n "${rest}" ]]; then + printf '%s\n\n' "${rest}" + fi + pin_block +} + +install_config_block() { + local rendered + rendered="$(render_config)" + printf '%s\n' "${rendered}" | write_if_changed "${config_file}" "gga config pin block" +} + +install_environment_d() { + write_if_changed "${env_file}" "gga pin for desktop sessions" <"${REAL_BIN}/codex" + # Fake gga that calls codex as a child process, like gga's execute_codex. + printf '#!/usr/bin/env bash\ncodex "$@"\n' >"${HOME}/fake-gga/gga" + chmod +x "${REAL_BIN}/codex" "${HOME}/fake-gga/gga" +} + +gentle_ai_config() { + mkdir -p "${GGA_DIR}" + printf '%s\n' '# Gentleman Guardian Angel Configuration' '# Generated by gentle-ai' '' \ + 'PROVIDER="claude"' '' 'RULES_FILE="AGENTS.md"' >"${CONFIG}" +} + +snapshot() { find "${HOME}/.config" -type f -exec sha256sum {} + 2>/dev/null | sort; } + +# Run the installed shim with the fake codex behind it. +shim() { PATH="${SHIM_DIR}:${REAL_BIN}:${PATH}" "$@"; } + +# ── installer ──────────────────────────────────────────────────────── + +@test "installer creates shim, pin.env, config block and environment.d file" { + gga_setup + run installer + [ "$status" -eq 0 ] + [ -x "${SHIM_DIR}/codex" ] + cmp -s "${SHIM_DIR}/codex" "${DREAMCODER_DOTS_DIR}/scripts/gga-codex-shim.sh" + grep -qx 'GGA_PIN_MODEL="gpt-6.1-sol"' "${PIN}" + grep -qx 'GGA_PIN_EFFORT="medium"' "${PIN}" + grep -qx '# >>> dreamcoder gga pin >>>' "${CONFIG}" + grep -qx 'GGA_PROVIDER=codex' "${ENV_D}" + grep -qx "PATH=${SHIM_DIR}:\${PATH}" "${ENV_D}" + [ "$(grep -c '^✓' <<<"$output")" -eq 4 ] +} + +@test "installer is idempotent: second run prints nothing and changes nothing" { + gga_setup + gentle_ai_config + installer >/dev/null + before="$(snapshot)" + run installer + [ "$status" -eq 0 ] + [ -z "$output" ] + [ "$(snapshot)" = "${before}" ] +} + +@test "installer replaces an existing block instead of duplicating it" { + gga_setup + gentle_ai_config + printf '\n%s\n%s\n%s\n' '# >>> dreamcoder gga pin >>>' 'PROVIDER="stale"' '# <<< dreamcoder gga pin <<<' >>"${CONFIG}" + printf 'TIMEOUT="300"\n' >>"${CONFIG}" + installer >/dev/null + [ "$(grep -c '^# >>> dreamcoder gga pin >>>$' "${CONFIG}")" -eq 1 ] + run grep -q 'stale' "${CONFIG}" + [ "$status" -ne 0 ] + # The block moves to the end and the user's lines survive. + [ "$(tail -n1 "${CONFIG}")" = '# <<< dreamcoder gga pin <<<' ] + grep -qx 'TIMEOUT="300"' "${CONFIG}" +} + +@test "a gentle-ai rewritten config gets the block and resolves to codex" { + gga_setup + gentle_ai_config + installer >/dev/null + head -n2 "${CONFIG}" | grep -qx '# Generated by gentle-ai' + grep -qx 'RULES_FILE="AGENTS.md"' "${CONFIG}" + run env -u GGA_PROVIDER bash -c 'source "$1"; printf "%s %s %s\n" "$PROVIDER" "$GGA_PROVIDER" "${PATH%%:*}"' _ "${CONFIG}" + [ "$status" -eq 0 ] + [ "$output" = "codex codex ${SHIM_DIR}" ] +} + +@test "sourcing the block twice does not duplicate the shim dir on PATH" { + gga_setup + installer >/dev/null + run bash -c 'source "$1"; source "$1"; tr ":" "\n" <<<"$PATH" | grep -cx "$2"' _ "${CONFIG}" "${SHIM_DIR}" + [ "$output" = "1" ] +} + +@test "installer never overwrites a user-edited pin.env" { + gga_setup + mkdir -p "${GGA_DIR}" + printf 'GGA_PIN_MODEL="custom-model"\n' >"${PIN}" + installer >/dev/null + [ "$(cat "${PIN}")" = 'GGA_PIN_MODEL="custom-model"' ] +} + +@test "installer --dry-run writes nothing" { + gga_setup + gentle_ai_config + before="$(snapshot)" + run installer --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"Would update"* ]] + [ "$(snapshot)" = "${before}" ] + [ ! -e "${SHIM_DIR}/codex" ] +} + +@test "installer refuses an unterminated pin block" { + gga_setup + gentle_ai_config + printf '# >>> dreamcoder gga pin >>>\n' >>"${CONFIG}" + run installer + [ "$status" -ne 0 ] + grep -qx 'PROVIDER="claude"' "${CONFIG}" +} + +@test "installer refuses a config dir that is unsafe to embed in shell code" { + gga_setup + export XDG_CONFIG_HOME="${HOME}/cfg\$(touch pwned)" + run installer + [ "$status" -ne 0 ] + [[ "$output" == *"unsupported characters"* ]] + [ ! -e "${XDG_CONFIG_HOME}" ] +} + +# ── shim ───────────────────────────────────────────────────────────── + +@test "shim injects the pinned model when gga runs codex exec" { + gga_setup + installer >/dev/null + run shim "${HOME}/fake-gga/gga" exec "review this" + [ "$status" -eq 0 ] + [ "$output" = "$(printf '%s\n' exec -m gpt-6.1-sol -c model_reasoning_effort=medium 'review this')" ] +} + +@test "shim also detects gga through an intermediate process" { + gga_setup + installer >/dev/null + printf '#!/usr/bin/env bash\nbash -c '\''codex "$@"'\'' _ "$@"\n' >"${HOME}/fake-gga/gga" + run shim "${HOME}/fake-gga/gga" exec "review this" + [ "${lines[1]}" = "-m" ] + [ "${lines[2]}" = "gpt-6.1-sol" ] +} + +@test "shim passes codex exec through untouched without a gga ancestor" { + gga_setup + installer >/dev/null + run shim bash -c 'codex exec "hello"' + [ "$status" -eq 0 ] + [ "$output" = "$(printf '%s\n' exec hello)" ] +} + +@test "shim keeps an explicit model from the caller" { + gga_setup + installer >/dev/null + run shim "${HOME}/fake-gga/gga" exec -m other-model "review" + [ "$output" = "$(printf '%s\n' exec -m other-model review)" ] + run shim "${HOME}/fake-gga/gga" exec --model=other-model "review" + [ "$output" = "$(printf '%s\n' exec --model=other-model review)" ] +} + +@test "shim leaves non-exec subcommands untouched under gga" { + gga_setup + installer >/dev/null + run shim "${HOME}/fake-gga/gga" login --status + [ "$output" = "$(printf '%s\n' login --status)" ] +} + +@test "pin.env overrides model and effort" { + gga_setup + installer >/dev/null + printf 'GGA_PIN_MODEL="gpt-x"\nexport GGA_PIN_EFFORT=high\n' >"${PIN}" + run shim "${HOME}/fake-gga/gga" exec "review" + [ "$output" = "$(printf '%s\n' exec -m gpt-x -c model_reasoning_effort=high review)" ] +} + +@test "shim fails clearly when no real codex exists" { + gga_setup + installer >/dev/null + run -127 env PATH="${SHIM_DIR}:/usr/bin:/bin" codex exec x + [[ "$output" == *"real codex CLI was not found"* ]] +}