diff --git a/CONTEXT.md b/CONTEXT.md index ff58fd5..d8319b2 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,4 +2,4 @@ -- [claude-profile engagement modes](context/claude-profile-modes.md) — Core done (picker, statusline integration, session-start auto-select); needs symlink install + first real `claude` launch test before v1 is declared complete +- [claude-profile engagement modes](context/claude-profile-modes.md) — Provider selection removed, model config embedded in profiles. Needs symlink install + first real `claude` launch test (both Anthropic and MiniMax profiles) before v1 is declared complete diff --git a/MEMORY.md b/MEMORY.md index 0e84ccf..09c9044 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -3,7 +3,8 @@ ## Gotchas -- [Bash Gotchas](memory/gotchas-bash.md) — `set -e` silent failures, `((var++))` with zero, grep flag parsing, `local` scope, binary detection with `file` command, test harness `set -e` abort +- [Bash Gotchas](memory/gotchas-bash.md) — `set -e` silent failures, `((var++))` with zero, grep flag parsing, `local` scope, binary detection with `file` command, test harness `set -e` abort, `read -rp` TTY exit, never eval user config +- [Claude-Profile Gotchas](memory/gotchas-claude-profile.md) — context-loader path resolution, `--model` flag verification pattern - [Skills Gotchas](memory/gotchas-skills.md) — Non-ASCII frontmatter silently breaks loading, per-profile independent skills directories - [WezTerm Gotchas](memory/gotchas-wezterm.md) — Flatpak CLI not in PATH, proportional split math for equal panes diff --git a/context/claude-profile-modes.md b/context/claude-profile-modes.md index 487c1ec..98e0f38 100644 --- a/context/claude-profile-modes.md +++ b/context/claude-profile-modes.md @@ -2,42 +2,45 @@ ## Status -**Provider support added. 58 dryrun assertions passing. MiniMax token provisioned. NOT yet exercised in a real `claude` launch.** +**Provider selection removed. Model config embedded in profiles via `provider.env`. 41 dryrun assertions passing.** ## What's done | Component | State | |---|---| -| Profile + mode + provider picker (phases 1-3) | Done, 58 dryrun assertions passing | +| Profile + mode picker (phases 1-2) | Done, 41 dryrun assertions passing | | 5 mode files with `escalates_to` frontmatter | Done | -| 5 provider files (4 Anthropic + minimax-sonnet) | Done (`data/claude-profile/providers/`) | -| `active-mode.env` written on every launch | Done (includes `CLAUDE_PROVIDER`) | -| MiniMax token decrypted + stored | Done (`~/.claude-secrets/minimax-auth-token`, 600 perms) | +| `provider.env` for non-Anthropic profiles | Done (`~/.claude-oreillyit-minimax/provider.env`) | +| Driver→model_id hardcoded for Anthropic | Done (haiku/sonnet/opus/opus-1m) | +| `active-mode.env` written on every launch | Done (no longer includes `CLAUDE_PROVIDER` or `CLAUDE_TIME_HORIZON`) | | statusline.sh — `[Sonnet→Opus] deep · topic` format | Done (reads `CLAUDE_CONFIG_DIR/active-mode.env`) | | Root CLAUDE.md — auto-select project from cwd or `CLAUDE_PROJECT` | Done | | Preset support (`presets.yaml`) | Done (example at `data/claude-profile/presets.yaml.example`) | +| `COLORTERM=truecolor` exported before launch | Done | + +## Architecture change: providers → profiles + +Previously, provider selection was a separate interactive phase (profile → mode → provider). Now: + +- **Anthropic profiles** need no extra config — the driver (from mode frontmatter) maps directly to an Anthropic model ID +- **Non-Anthropic profiles** put a `provider.env` file in their profile directory with `MODEL_ID`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY_FILE`, and any extra env vars +- **Different providers = different profiles** (e.g., `oreillyit-anthropic` vs `oreillyit-minimax`) ## Next session — recommended order 1. **Install the symlink** (if not already done): ```bash - ln -sf "$HOME/dev/claude/small-scripts/scripts/claude-profile" "$HOME/sbin/claude-profile" + ln -sf "$HOME/dev/claude/projects/small-scripts/scripts/claude-profile" "$HOME/sbin/claude-profile" ``` -2. **Real-launch test — Anthropic first:** `claude-profile oreillyit --mode chat` (press Enter to accept default anthropic-haiku). Verify: +2. **Real-launch test — Anthropic:** `claude-profile oreillyit-anthropic --mode chat`. Verify: - Mode body appears in system prompt - Status bar shows `[Haiku] chat` format - - `~/.claude-oreillyit/active-mode.env` has `CLAUDE_PROVIDER=anthropic-haiku` -3. **MiniMax launch test:** `claude-profile oreillyit --mode quick --provider minimax-sonnet`. Verify: + - `~/.claude-oreillyit-anthropic/active-mode.env` has no `CLAUDE_PROVIDER` or `CLAUDE_TIME_HORIZON` +3. **MiniMax launch test:** `claude-profile oreillyit-minimax --mode quick`. Verify: - Claude starts and is responsive - `ANTHROPIC_BASE_URL` is set to MiniMax endpoint (check via a "what model are you?" prompt) 4. **If anything's off**, `tests/test-claude-profile.sh` is the fast feedback loop -## Adding new providers - -Drop a new `data/claude-profile/providers/.yaml` file. For third-party providers: -- Decrypt credentials and store the raw token in `~/.claude-secrets/-auth-token` -- Provider YAML needs: `base_url`, `api_key_env`, `api_key_file`, and any `extra_env` flags the provider requires - ## Remaining follow-ups (from FUTURE.md) 1. **`bg-model-call` wrapper** — `deep` and `hybrid` modes reference it but it doesn't exist yet. Own spec + script entry. @@ -50,3 +53,4 @@ Drop a new `data/claude-profile/providers/.yaml` file. For third-party pro - **`CLAUDE_CONFIG_DIR` must be exported** by claude-profile before `exec claude`. The statusline hook inherits it from the claude process environment. - **`read -rp` under `set -e` exits silently if stdin is not a TTY.** The picker guards all interactive reads with `[[ -t 0 ]]`. See `memory/gotchas-bash.md`. - **MiniMax token is in `~/.claude-secrets/`** — gitignored, plaintext, 600 perms. The SOPS-encrypted version in agent-runtimes is for container agents only. +- **`provider.env` uses `ANTHROPIC_API_KEY_FILE`** — the script reads the file and exports `ANTHROPIC_API_KEY`. The env var name is always `ANTHROPIC_API_KEY` (not configurable per-provider). diff --git a/data/claude-profile/presets.yaml.example b/data/claude-profile/presets.yaml.example index aefe88f..d1706a9 100644 --- a/data/claude-profile/presets.yaml.example +++ b/data/claude-profile/presets.yaml.example @@ -7,42 +7,38 @@ # A "deep work" session on the homelab cluster deep-cluster: - profile: oreillyit + profile: oreillyit-anthropic mode: deep project: cluster-bootstrap - time_horizon: hours async: yes # A quick edit on the small-scripts repo (no questions, just launch) quick-skills: - profile: oreillyit + profile: oreillyit-anthropic mode: quick project: small-scripts # Overnight orchestration of container agents on the image-gen project orch-overnight: - profile: oreillyit + profile: oreillyit-anthropic mode: orch project: ai-image-gen - time_horizon: overnight autoloop: yes # Cheap conversational session, no project chat-only: - profile: oreillyit + profile: oreillyit-anthropic mode: chat # Hybrid session — Haiku driver with Opus subagents on call hybrid-day: - profile: oreillyit + profile: oreillyit-anthropic mode: hybrid project: cluster-bootstrap - time_horizon: hours async: yes -# Quick session via MiniMax — uses the minimax profile (no OAuth credentials) +# Quick session via MiniMax — uses the minimax profile (provider.env embedded) quick-minimax: - profile: minimax + profile: oreillyit-minimax mode: quick - provider: minimax-sonnet project: small-scripts diff --git a/data/claude-profile/providers/anthropic-haiku.yaml b/data/claude-profile/providers/anthropic-haiku.yaml deleted file mode 100644 index 9df8e0d..0000000 --- a/data/claude-profile/providers/anthropic-haiku.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: anthropic-haiku -display: Anthropic (Haiku) -model_id: claude-haiku-4-5-20251001 diff --git a/data/claude-profile/providers/anthropic-opus-1m.yaml b/data/claude-profile/providers/anthropic-opus-1m.yaml deleted file mode 100644 index aa44316..0000000 --- a/data/claude-profile/providers/anthropic-opus-1m.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: anthropic-opus-1m -display: Anthropic (Opus 1M ctx) -model_id: "claude-opus-4-6[1m]" diff --git a/data/claude-profile/providers/anthropic-opus.yaml b/data/claude-profile/providers/anthropic-opus.yaml deleted file mode 100644 index e62b4a0..0000000 --- a/data/claude-profile/providers/anthropic-opus.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: anthropic-opus -display: Anthropic (Opus) -model_id: claude-opus-4-6 diff --git a/data/claude-profile/providers/anthropic-sonnet.yaml b/data/claude-profile/providers/anthropic-sonnet.yaml deleted file mode 100644 index bd2c378..0000000 --- a/data/claude-profile/providers/anthropic-sonnet.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: anthropic-sonnet -display: Anthropic (Sonnet) -model_id: claude-sonnet-4-6 diff --git a/data/claude-profile/providers/minimax-sonnet.yaml b/data/claude-profile/providers/minimax-sonnet.yaml deleted file mode 100644 index 7be6653..0000000 --- a/data/claude-profile/providers/minimax-sonnet.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: minimax-sonnet -display: MiniMax (M2.7) -model_id: MiniMax-M2.7 -base_url: https://api.minimax.io/anthropic -api_key_env: ANTHROPIC_API_KEY -api_key_file: ~/.claude-secrets/minimax-auth-token -extra_env: - ANTHROPIC_MODEL: MiniMax-M2.7 - ANTHROPIC_SMALL_FAST_MODEL: MiniMax-M2.7 - ANTHROPIC_DEFAULT_SONNET_MODEL: MiniMax-M2.7 - ANTHROPIC_DEFAULT_OPUS_MODEL: MiniMax-M2.7 - ANTHROPIC_DEFAULT_HAIKU_MODEL: MiniMax-M2.7 - API_TIMEOUT_MS: "3000000" - CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" - DISABLE_PROMPT_CACHING: "1" - CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" diff --git a/memory/decisions.md b/memory/decisions.md index 74c8669..44906e9 100644 --- a/memory/decisions.md +++ b/memory/decisions.md @@ -23,3 +23,59 @@ Scripts that need to read JSON (e.g., `.reflection-state.json`) use inline pytho ## Project hosted under `skynet` org `small-scripts` lives in the `skynet` org on Gitea (`gitea.oreillyit.nz`) as an AI-focused project — specifically a testbed for agent-driven, spec-first development workflows. + +## Mode files in `data/claude-profile/modes/` not profile directories + +Engagement mode definitions live in `data/claude-profile/modes/.md` in the repo, not in profile directories. Source of truth must be version-controlled; profile dirs are for user state, not script data. This established `data/` as a new repo top-level convention. + +## Driver enforcement via `claude --model`, not system prompt + +Driver model is set via `claude --model ` which is session-scoped. This is real enforcement, not advisory injection of "you are Sonnet" into an Opus session. Confirmed via the `claude-code-guide` agent. + +## Full model IDs not aliases + +Use `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`, `claude-opus-4-6` instead of `sonnet`/`opus`/`haiku` aliases. Removes ambiguity about which version is selected; haiku alias was unverified. + +## v1 claude-profile skips interactive project picker + +v1 does not ask for a project interactively because that would duplicate the existing CLAUDE.md session-start project picker. The `--project` flag works for non-interactive use; CLAUDE.md integration is deferred to FUTURE.md. + +## Deep Work mode drives with Sonnet, not Opus + +Sonnet is the workhorse; Opus is the consultant. Opus only enters via named workflows (plan review, spec review, dual-model second opinion, gnarly-bug consult, architecture review). Default reasoning depth is "think hard", not ultrathink. + +## Pure awk for frontmatter and presets parsing + +Frontmatter and `presets.yaml` are parsed by pure awk, not yq. small-scripts targets a clean Linux env; adding yq for one launcher is overkill. Parsing is simple key:value with one-level nesting. + +## `bg-model-call` wrapper deferred + +The background model call wrapper is referenced by modes as a pattern but not required to ship. Deferred to FUTURE.md as out of scope for the launcher. + +## `escalates_to` field in mode frontmatter, not hardcoded in statusline + +Which modes display the `[Sonnet->Opus]` arrow is declared per-mode via an `escalates_to` frontmatter field, not hardcoded in `statusline.sh`. Keeps the status line generic; any future mode can declare its own escalation target without touching statusline code. + +## statusline reads `CLAUDE_DRIVER` short name from active-mode.env, not session JSON + +`statusline.sh` reads `CLAUDE_DRIVER` (short name like "Sonnet") from `CLAUDE_CONFIG_DIR/active-mode.env` rather than using `model.display_name` from the session JSON. Session JSON returns the full name ("Claude Sonnet 4.6") which doesn't fit the concise status format, and using the profile-provided short name also future-proofs against model renames. + +## Parse `active-mode.env` with `grep | cut`, never `source` + +`statusline.sh` reads `CLAUDE_CONFIG_DIR/active-mode.env` using `grep | cut` rather than `source`. Sourcing arbitrary env files is unsafe (code-injection) and, combined with `set -euo pipefail`, any error in the sourced file would abort the status line. The grep/cut approach is inert and bounded. + +## Provider config lives in profile directories via `provider.env` + +Provider choice (Anthropic, MiniMax, etc.) was previously an interactive phase in `claude-profile`. It is now embedded per-profile via `provider.env` in the profile directory. Different providers = different profiles, which is already the natural isolation boundary. Eliminates an entire interactive phase, and avoids the dead-code `CLAUDE_PROVIDER` env var. + +## Hardcode Anthropic driver→model_id, `provider.env` only for non-Anthropic + +Anthropic is the default path and needs zero config. The driver→model_id mapping for Anthropic (e.g., `sonnet` → `claude-sonnet-4-6`) is hardcoded in `claude-profile`. Only non-Anthropic profiles ship a `provider.env` file. Keeps the common case friction-free. + +## `ANTHROPIC_API_KEY` is the env var name for all provider keys + +Claude Code reads `ANTHROPIC_API_KEY` for its credential regardless of provider. `provider.env` therefore does not need a configurable key-name field — it always exports `ANTHROPIC_API_KEY=`. Simplifies provider.env to a flat value list. + +## Decouple WezTerm theme name from profile name via `wezterm-theme` file + +Profile names changed (e.g. `oreillyit` → `oreillyit-anthropic`) but the WezTerm theme table still keys on the old short names. Each profile dir ships a one-line `wezterm-theme` file that maps the new profile name to the theme key, so WezTerm theme emission can fire immediately after profile selection without renaming themes. diff --git a/memory/gotchas-bash.md b/memory/gotchas-bash.md index 3926e11..2108535 100644 --- a/memory/gotchas-bash.md +++ b/memory/gotchas-bash.md @@ -27,3 +27,11 @@ Test scripts with `set -euo pipefail` silently abort when an assertion helper ru ## `file` command marks shell scripts as "executable" The `file` command returns strings like "Bourne-Again shell script, Unicode text, UTF-8 text executable" for shell scripts. Grepping for `executable` to detect binaries will false-positive on text scripts. Instead, grep for `binary|image|archive` and additionally check that the output does NOT contain `text`. + +## `read -rp` under `set -e` exits silently when stdin is not a TTY + +`read -rp "prompt: " var` returns exit code 1 when stdin is closed (no TTY), which `set -e` treats as a failure and silently kills the script. This makes `--dryrun` unusable from non-interactive contexts (pipes, CI, test harnesses). Fix: guard every interactive `read` with `[[ -t 0 ]]` and fall back to a default value when not on a terminal. + +## Never `eval` user-controlled data from config files + +`eval "$preset_data"` is a code-injection footgun if the config file (e.g., `presets.yaml`) contains shell metacharacters. Instead, parse structured data with `awk` into known variable names and map them with a `case` statement. Never eval content that originates from user-editable files. diff --git a/memory/gotchas-claude-profile.md b/memory/gotchas-claude-profile.md new file mode 100644 index 0000000..3a04ea8 --- /dev/null +++ b/memory/gotchas-claude-profile.md @@ -0,0 +1,15 @@ +# Claude-Profile Gotchas + +## Context-loader path assumes sibling location + +The original `claude-profile` script set `CONTEXT_LOADER="${SCRIPT_DIR}/context-load"`, but `context-load` lives in `claude-foundations/scripts/`, not as a sibling of `claude-profile`. Fix: use `command -v context-load` (it's symlinked into `~/sbin`) as the primary lookup, with the sibling path as a fallback for testing isolation. Use `readlink -f "$0"` so the lookup works when `claude-profile` itself is symlinked. + +## Verify `claude --model` support before designing workarounds + +Before designing workarounds for harness limitations (per-driver profile dirs, settings.json rewriting), verify the actual capability. A 30-second check via the `claude-code-guide` agent confirmed `claude --model` is session-scoped and accepts both aliases (`sonnet`, `opus`) and full model IDs. This prevented building 3 unnecessary workarounds. + +## Dead env vars accumulate in `active-mode.env` if not audited + +Symptom: `CLAUDE_TIME_HORIZON` was written to `active-mode.env` and asked interactively at every launch, but no downstream tool ever consumed it. `CLAUDE_PROVIDER` was only read by the statusline for non-Anthropic short-name display — redundant once provider was promoted to a per-profile concern. + +Fix: when changing an interactive prompt or a launcher field, grep all consumers (`grep -r CLAUDE_TIME_HORIZON ~`) before keeping it. Removed both vars entirely; updated `custom-claude-skills/skills/switch-mode/SKILL.md` to drop them from its `active-mode.env` template, and `~/.claude/status/statusline.sh` to stop parsing `CLAUDE_PROVIDER`. diff --git a/memory/process-lessons.md b/memory/process-lessons.md index 27d792d..42fd89f 100644 --- a/memory/process-lessons.md +++ b/memory/process-lessons.md @@ -15,3 +15,27 @@ When a file looks correct but tooling rejects it, `cat -A` reveals non-printing ## Use `git diff --numstat` for binary detection instead of `file` The `file` command is unreliable for distinguishing binary from text files (marks shell scripts as "executable"). `git diff --numstat` shows `-` for binary files and is more reliable since git already has its own binary detection heuristics. + +## Verify harness capabilities before designing workarounds + +Before committing to workarounds for something the harness "doesn't support", verify the assumption. A 30-second check via a guide agent can prevent building multiple unnecessary workarounds. Pattern: if you're about to design around a limitation, confirm it's actually a limitation first. + +## Check existing references in old scripts before extending + +When extending an existing script, verify that its current references actually work in the live environment. Broken references can be masked by never running certain code paths. Tests catch these immediately. + +## Guard interactive `read` with `[[ -t 0 ]]` for testable scripts + +For bash scripts that are both interactive and testable, wrap every `read` call with `[[ -t 0 ]]` and provide a default value for non-TTY contexts. This allows the same code path to work interactively or in dryrun/test mode without `set -e` blowups. + +## File-based result passing keeps driver context lean + +When subagents or background processes produce non-trivial results, write them to files and only `Read` into context when actually needed. This unifying pattern (sentinel files, `bg-model-call`, container agent outputs) prevents the driver's context from bloating with data it may never use. + +## Always `Read` a file with the Read tool before calling `Edit` + +The Edit tool tracks which files have been explicitly read via the Read tool — viewing a file via Bash (`cat`, etc.) does not count. An Edit attempt on a file seen only through Bash output is rejected. When planning multiple edits, batch parallel `Read` calls first, then parallel `Edit` calls. + +## `/context` "Memory files" aggregates two distinct systems + +The "Memory files" category reported by `/context` combines project `CLAUDE.md` instructions and auto-memory `MEMORY.md`. They have different purposes (project conventions vs cross-session learnings) and live in different places. When auditing context size, check both rather than assuming a single source. diff --git a/scripts/claude-profile b/scripts/claude-profile index 117d9b2..923a54a 100755 --- a/scripts/claude-profile +++ b/scripts/claude-profile @@ -1,11 +1,10 @@ #!/usr/bin/env bash -# claude-profile — Launch Claude Code with a profile, engagement mode, and provider +# claude-profile — Launch Claude Code with a profile and engagement mode # # Usage: -# claude-profile # interactive: profile + mode + provider -# claude-profile # profile fixed, rest interactive -# claude-profile --mode # mode fixed, profile + provider interactive -# claude-profile --provider # provider fixed, profile + mode interactive +# claude-profile # interactive: profile + mode +# claude-profile # profile fixed, mode interactive +# claude-profile --mode # mode fixed, profile interactive # claude-profile --preset # all from presets.yaml # claude-profile --mode # profile + mode fixed # claude-profile --dryrun [...] # show resolved values, do not launch @@ -23,7 +22,6 @@ SCRIPT_PATH="$(readlink -f "$0")" SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MODES_DIR="$REPO_ROOT/data/claude-profile/modes" -PROVIDERS_DIR="$REPO_ROOT/data/claude-profile/providers" # context-load is on PATH (symlinked into ~/sbin from claude-foundations). # Fall back to a sibling location for testing isolation. @@ -33,13 +31,13 @@ else CONTEXT_LOADER="${SCRIPT_DIR}/context-load" fi -# Driver logical name -> default provider (fallback when --provider not given) -driver_default_provider() { +# Driver logical name -> Anthropic model ID +driver_model_id() { case "$1" in - haiku) echo "anthropic-haiku" ;; - sonnet) echo "anthropic-sonnet" ;; - opus) echo "anthropic-opus" ;; - opus-1m) echo "anthropic-opus-1m" ;; + haiku) echo "claude-haiku-4-5-20251001" ;; + sonnet) echo "claude-sonnet-4-6" ;; + opus) echo "claude-opus-4-6" ;; + opus-1m) echo "claude-opus-4-6[1m]" ;; *) err "Unknown driver '$1'. Valid: haiku sonnet opus opus-1m" ;; esac } @@ -48,7 +46,6 @@ driver_default_provider() { PROFILE="" MODE="" -PROVIDER="" PROJECT="" PRESET="" DRYRUN=0 @@ -61,7 +58,6 @@ usage() { while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="${2:-}"; shift 2 ;; - --provider) PROVIDER="${2:-}"; shift 2 ;; --preset) PRESET="${2:-}"; shift 2 ;; --project) PROJECT="${2:-}"; shift 2 ;; --dryrun|-n) DRYRUN=1; shift ;; @@ -81,8 +77,8 @@ while [[ $# -gt 0 ]]; do done # Mutual exclusion -if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" || -n "$PROVIDER" ) ]]; then - echo "Error: --preset is mutually exclusive with --mode, --provider, and --project" >&2 +if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" ) ]]; then + echo "Error: --preset is mutually exclusive with --mode and --project" >&2 exit 1 fi @@ -145,40 +141,6 @@ read_preset_block() { ' "$file" } -# Extract a scalar field from a provider YAML file (non-indented key: value). -parse_provider_field() { - local file=$1 key=$2 - awk -v key="$key" ' - /^extra_env:[[:space:]]*$/ { in_extra=1; next } - /^[a-zA-Z_]/ { in_extra=0 } - !in_extra { - if (match($0, "^"key":[[:space:]]*")) { - val = substr($0, RLENGTH+1) - gsub(/^[[:space:]]*"?/, "", val) - gsub(/"?[[:space:]]*$/, "", val) - print val; exit - } - } - ' "$file" -} - -# Output KEY=value lines for the extra_env block in a provider YAML file. -parse_extra_env() { - local file=$1 - awk ' - /^extra_env:[[:space:]]*$/ { in_extra=1; next } - /^[a-zA-Z_]/ { in_extra=0 } - in_extra && /^[[:space:]]+[A-Z_]/ { - line = $0; sub(/^[[:space:]]+/, "", line) - key = line; val = line - sub(/:.*$/, "", key) - sub(/^[^:]+:[[:space:]]*/, "", val) - gsub(/^[[:space:]]*"?/, "", val); gsub(/"?[[:space:]]*$/, "", val) - print key "=" val - } - ' "$file" -} - # List available modes (sorted), printing a numbered menu. # Returns the array of mode names via the global MODE_LIST. MODE_LIST=() @@ -196,23 +158,6 @@ list_modes() { done } -# List available providers, printing a numbered menu. -# Returns the array of provider names via the global PROVIDER_LIST. -PROVIDER_LIST=() -list_providers() { - PROVIDER_LIST=() - local i=1 - for f in "$PROVIDERS_DIR"/*.yaml; do - [[ -f "$f" ]] || continue - local name display - name=$(parse_provider_field "$f" name) - display=$(parse_provider_field "$f" display) - printf " %d) %-22s — %s\n" "$i" "$name" "$display" - PROVIDER_LIST+=("$name") - i=$((i + 1)) - done -} - resolve_profile_dir() { local name=$1 if [[ "$name" == "default" ]]; then @@ -235,7 +180,6 @@ reset_wezterm_profile() { # Sanity check on the modes directory before doing any picker work. [[ -d "$MODES_DIR" ]] || err "Mode files directory not found at $MODES_DIR" -[[ -d "$PROVIDERS_DIR" ]] || err "Providers directory not found at $PROVIDERS_DIR" if [[ -n "$PRESET" ]]; then # Find the preset across all profile presets.yaml files. @@ -259,9 +203,7 @@ if [[ -n "$PRESET" ]]; then case "$k" in PRESET_PROFILE) preset_profile_name="$v" ;; PRESET_MODE) MODE="$v" ;; - PRESET_PROVIDER) PROVIDER="$v" ;; PRESET_PROJECT) PROJECT="$v" ;; - PRESET_TIME_HORIZON) TIME_HORIZON="$v" ;; PRESET_ASYNC) ASYNC_OVERRIDE="$v" ;; PRESET_AUTOLOOP) AUTOLOOP_OVERRIDE="$v" ;; esac @@ -307,6 +249,16 @@ fi [[ -d "$target" ]] || err "Profile directory not found: $target" +# Apply WezTerm theme immediately so the terminal changes during the mode picker. +# Read theme name from profile's wezterm-theme file, falling back to the profile name. +if [[ -f "$target/wezterm-theme" ]]; then + WEZTERM_THEME=$(cat "$target/wezterm-theme") +else + WEZTERM_THEME="$PROFILE" +fi +trap reset_wezterm_profile EXIT +set_wezterm_profile "$WEZTERM_THEME" + # === Phase 2: Mode selection === if [[ -z "$MODE" ]]; then @@ -360,22 +312,6 @@ for field_name in DRIVER TAG ASYNC_OK; do fi done -# Mode-specific interactive questions (skipped if preset) -if [[ -z "$PRESET" ]]; then - case "$MODE" in - deep|hybrid|orch) - if [[ -z "${TIME_HORIZON:-}" ]]; then - if [[ -t 0 ]]; then - read -rp "Time horizon (minutes/hours/overnight) [hours]: " th - else - th="" - fi - TIME_HORIZON="${th:-hours}" - fi - ;; - esac -fi - # Apply preset overrides for async/autoloop [[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE" [[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && { @@ -384,55 +320,25 @@ fi fi } -# === Phase 2.5: Provider selection === +# === Phase 2.5: Resolve model ID === -default_provider=$(driver_default_provider "$DRIVER") +# Default: Anthropic model from driver +MODEL_ID=$(driver_model_id "$DRIVER") -if [[ -z "$PROVIDER" ]]; then - if [[ -z "$PRESET" ]]; then - echo "" - echo "Provider (model + API):" - list_providers - echo "" - if [[ -t 0 ]]; then - read -rp "Choose provider [$default_provider]: " prov_choice - else - prov_choice="" - fi - prov_choice="${prov_choice:-$default_provider}" - - if [[ "$prov_choice" =~ ^[0-9]+$ ]]; then - if (( prov_choice < 1 || prov_choice > ${#PROVIDER_LIST[@]} )); then - err "Invalid provider choice: $prov_choice" - fi - PROVIDER="${PROVIDER_LIST[$((prov_choice - 1))]}" - else - PROVIDER="$prov_choice" - fi - else - PROVIDER="$default_provider" - fi -fi - -provider_file="$PROVIDERS_DIR/$PROVIDER.yaml" -[[ -f "$provider_file" ]] || err "Provider not found: $PROVIDER (looked in $PROVIDERS_DIR)" - -PROVIDER_DISPLAY=$(parse_provider_field "$provider_file" display) -MODEL_ID=$(parse_provider_field "$provider_file" model_id) -PROVIDER_BASE_URL=$(parse_provider_field "$provider_file" base_url) -PROVIDER_API_KEY_ENV=$(parse_provider_field "$provider_file" api_key_env) -PROVIDER_API_KEY_FILE=$(parse_provider_field "$provider_file" api_key_file) - -[[ -n "$MODEL_ID" ]] || err "Provider file $provider_file is missing required field: model_id" - -# Warn when a non-Anthropic provider is selected but the profile has stored OAuth credentials. -# Claude Code uses .credentials.json for auth before checking ANTHROPIC_API_KEY, which -# routes requests to Anthropic regardless of ANTHROPIC_BASE_URL. -if [[ -n "$PROVIDER_BASE_URL" && -f "$target/.credentials.json" ]]; then - echo "Warning: profile '$PROFILE' has stored OAuth credentials ($target/.credentials.json)." >&2 - echo " These take precedence over ANTHROPIC_API_KEY and will route requests to Anthropic," >&2 - echo " not $PROVIDER. Use a profile without .credentials.json for non-Anthropic providers." >&2 - echo " See: claude-profile --help" >&2 +# Override from profile's provider.env if present +PROVIDER_ENV="$target/provider.env" +if [[ -f "$PROVIDER_ENV" ]]; then + # Source provider.env into a subshell-safe set of variables. + # We parse key=value lines rather than sourcing directly to avoid side effects. + while IFS='=' read -r key value; do + [[ -z "$key" || "$key" == \#* ]] && continue + case "$key" in + MODEL_ID) MODEL_ID="$value" ;; + ANTHROPIC_BASE_URL) PROVIDER_BASE_URL="$value" ;; + ANTHROPIC_API_KEY_FILE) PROVIDER_API_KEY_FILE="$value" ;; + *) PROVIDER_EXTRA_ENV+=("$key=$value") ;; + esac + done < "$PROVIDER_ENV" fi # === Phase 3: Launch === @@ -445,10 +351,8 @@ active_env=$(cat <}" - dryrun_log "Time horizon: ${TIME_HORIZON:-}" dryrun_log "Async OK: $ASYNC_OK" dryrun_log "Autoloop: $AUTOLOOP" dryrun_log "Escalates to: ${ESCALATES_TO:-none}" @@ -503,32 +406,31 @@ fi # === Real launch from here on === -# Set up WezTerm theming and reset trap -trap reset_wezterm_profile EXIT export CLAUDE_CONFIG_DIR="$target" -set_wezterm_profile "$PROFILE" # Disable adaptive thinking (existing behaviour) export CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1 -# Export provider env vars for non-Anthropic providers -if [[ -n "$PROVIDER_BASE_URL" ]]; then +# Ensure proper colour support +export COLORTERM=truecolor + +# Export provider env vars from profile's provider.env +if [[ -n "${PROVIDER_BASE_URL:-}" ]]; then export ANTHROPIC_BASE_URL="$PROVIDER_BASE_URL" fi -if [[ -n "$PROVIDER_API_KEY_ENV" && -n "$PROVIDER_API_KEY_FILE" ]]; then +if [[ -n "${PROVIDER_API_KEY_FILE:-}" ]]; then expanded_key_file="${PROVIDER_API_KEY_FILE/#\~/$HOME}" - [[ -f "$expanded_key_file" ]] || err "Provider API key file not found: $expanded_key_file" + [[ -f "$expanded_key_file" ]] || err "API key file not found: $expanded_key_file" api_key=$(cat "$expanded_key_file") - export "${PROVIDER_API_KEY_ENV}=${api_key}" + export ANTHROPIC_API_KEY="$api_key" fi -# Export extra_env entries from provider file -while IFS='=' read -r k v; do - [[ -n "$k" ]] && export "${k}=${v}" -done < <(parse_extra_env "$provider_file") +for entry in "${PROVIDER_EXTRA_ENV[@]+"${PROVIDER_EXTRA_ENV[@]}"}"; do + [[ -n "$entry" ]] && export "$entry" +done # Write state files printf '%s\n' "$active_env" > "$target/active-mode.env" printf '%s\n' "$MODE" > "$target/last-mode" -echo "Launching: $PROFILE / $MODE / $PROVIDER${PROJECT:+ / $PROJECT}" +echo "Launching: $PROFILE / $MODE${PROJECT:+ / $PROJECT}" exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}" diff --git a/specs/claude-profile.spec.md b/specs/claude-profile.spec.md index 342a799..48ac863 100644 --- a/specs/claude-profile.spec.md +++ b/specs/claude-profile.spec.md @@ -2,7 +2,7 @@ ## Purpose -Launch Claude Code with a named configuration profile, an engagement mode, and a provider. The profile selects which `~/.claude-*` config directory to use; the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance; the provider determines the model ID, API endpoint, and credentials. +Launch Claude Code with a named configuration profile and an engagement mode. The profile selects which `~/.claude-*` config directory to use (and optionally embeds non-Anthropic provider config); the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance. ## Usage @@ -22,20 +22,19 @@ claude-profile [OPTIONS] [PROFILE] [-- CLAUDE_ARGS...] | Flag | Description | |------|-------------| | `--mode ` | Skip the mode picker and use the named mode (`chat`, `quick`, `deep`, `hybrid`, `orch`). | -| `--provider ` | Skip the provider picker and use the named provider (e.g. `anthropic-sonnet`, `minimax-sonnet`). | -| `--preset ` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, provider, and any preset-defined defaults in one shot. | +| `--preset ` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, and any preset-defined defaults in one shot. | | `--project ` | Pre-select a project (skips the project question for modes that ask it). | | `--dryrun`, `-n` | Print all resolved values and the command that would be executed; do not launch `claude`, do not write `active-mode.env` or `last-mode`. | | `--help`, `-h` | Show usage. | ### Mutual exclusion -- `--preset` is mutually exclusive with `--mode`, `--provider`, and `--project` — the preset defines all of those. Passing both is an error. +- `--preset` is mutually exclusive with `--mode` and `--project` — the preset defines all of those. Passing both is an error. - A positional `PROFILE` combined with `--preset` is allowed only if the preset's `profile` field matches; mismatch is an error. ## Behaviour -The script runs in four phases: **profile selection**, **mode selection**, **provider selection**, **launch**. Each phase has an interactive path (prompt the user) and a non-interactive path (CLI flag or config file). +The script runs in three phases: **profile selection**, **mode selection**, **launch**. Each phase has an interactive path (prompt the user) and a non-interactive path (CLI flag or config file). ### Phase 1 — Profile selection @@ -49,20 +48,30 @@ The script runs in four phases: **profile selection**, **mode selection**, **pro ### Phase 2 — Mode selection -1. If `--preset` is given, use the preset's `mode` and any other fields (project, time horizon, async, autoloop). Skip the rest of phase 2. -2. Else if `--mode ` is given, use it and proceed to mode-specific questions (project, async, etc., depending on the mode). +1. If `--preset` is given, use the preset's `mode` and any other fields (project, async, autoloop). Skip the rest of phase 2. +2. Else if `--mode ` is given, use it. 3. Else (interactive): list available modes by scanning `/data/claude-profile/modes/*.md`, display a numbered menu with each mode's `tag` and the first sentence of its `## Purpose` section. Default selection is the contents of `$CLAUDE_CONFIG_DIR/last-mode` (if present), else `quick`. -4. After the mode is chosen, ask only the questions relevant to that mode (see "Mode follow-up questions" below). Skip questions already answered by CLI flags. -5. Validate the chosen mode file exists at `/data/claude-profile/modes/.md`. If not, error and exit 1. -6. Read the mode file's frontmatter to extract `driver`, `tag`, `async_ok`, `autoloop`, `plan_mode_auto`, `spec_driven`. +4. Validate the chosen mode file exists at `/data/claude-profile/modes/.md`. If not, error and exit 1. +5. Read the mode file's frontmatter to extract `driver`, `tag`, `async_ok`, `autoloop`, `plan_mode_auto`, `spec_driven`, `escalates_to`. -### Phase 2.5 — Provider selection +### Model ID resolution -1. If `--preset` is given, use the preset's `provider` field (if present) or derive the default from the mode's `driver` field. Skip the rest of phase 2.5. -2. Else if `--provider ` is given, use it. -3. Else (interactive): derive the default provider from the mode's `driver` field (see "Driver-to-provider defaults" below). List available providers from `/data/claude-profile/providers/*.yaml`, display a numbered menu, prompt for selection. Default is the driver-derived provider, so pressing Enter preserves existing Anthropic behaviour. -4. Validate the chosen provider file exists at `/data/claude-profile/providers/.yaml`. If not, error and exit 1. -5. Read the provider file to extract `model_id`, `base_url`, `api_key_env`, `api_key_file`, and `extra_env`. +The model ID is determined by combining the mode's `driver` field with the profile's optional `provider.env`: + +1. Map the driver to an Anthropic model ID using the hardcoded table: + +| Driver | Model ID | +|---|---| +| `haiku` | `claude-haiku-4-5-20251001` | +| `sonnet` | `claude-sonnet-4-6` | +| `opus` | `claude-opus-4-6` | +| `opus-1m` | `claude-opus-4-6[1m]` | + +2. If `$CLAUDE_CONFIG_DIR/provider.env` exists, parse it. If it contains a `MODEL_ID` entry, use that instead of the hardcoded default. + +This means Anthropic profiles need no extra config. Non-Anthropic profiles embed their model and API config in `provider.env`. + +**Project selection is intentionally NOT asked by claude-profile.** The existing CLAUDE.md session-start flow handles project selection once `claude` is running. claude-profile accepts `--project ` as a non-interactive override (for presets and scripted launches), which writes `CLAUDE_PROJECT` into `active-mode.env`. ### Phase 3 — Launch @@ -70,38 +79,12 @@ The script runs in four phases: **profile selection**, **mode selection**, **pro 2. Write the chosen mode name to `$CLAUDE_CONFIG_DIR/last-mode`. 3. Read the mode file's prose body (everything after the frontmatter) and append it to the context that `context-load` produces. The mode body becomes part of `--append-system-prompt`. 4. Set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` (existing behaviour). -5. If the provider has a `base_url`, export `ANTHROPIC_BASE_URL=`. -6. If the provider has `api_key_env` and `api_key_file`, read the key from the file and export it as `=`. Error if the file is missing. -7. Export any `extra_env` entries from the provider file. -8. Execute `claude --model "" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`. -9. On exit, the existing WezTerm reset trap runs. - -### Driver-to-provider defaults - -When no `--provider` is given, the default provider is derived from the mode's `driver` field: - -| Mode `driver` | Default provider | -|---|---| -| `haiku` | `anthropic-haiku` | -| `sonnet` | `anthropic-sonnet` | -| `opus` | `anthropic-opus` | -| `opus-1m` | `anthropic-opus-1m` | - -This preserves backwards-compatible behaviour: launching without `--provider` behaves exactly as before. - -### Mode follow-up questions - -Each mode declares which questions are relevant. Asked in order; only those not already supplied via CLI flags are prompted for. - -| Question | Asked in modes | Default | Drives | -|---|---|---|---| -| **Time horizon?** (minutes / hours / overnight) | deep, hybrid, orch | hours | Recorded in active-mode.env; the model adapts ScheduleWakeup cadence | -| **Async OK?** (yes / no) | deep, hybrid | mode default (yes for both) | Overrides `async_ok` | -| **Auto-loops?** (yes / no, confirms `/loop 2m /orchestrate`) | orch | yes | Overrides `autoloop` | - -`chat` and `quick` ask no questions interactively. - -**Project selection is intentionally NOT asked by claude-profile.** The existing CLAUDE.md session-start flow handles project selection once `claude` is running. claude-profile accepts `--project ` as a non-interactive override (for presets and scripted launches), which writes `CLAUDE_PROJECT` into `active-mode.env`. A future CLAUDE.md update will read that field and skip its own picker when set; until then, supplying `--project` will result in the CLAUDE.md picker still appearing — the user can confirm the same project. This is a documented v1 limitation. +5. Set `COLORTERM=truecolor` for proper colour support. +6. If `provider.env` specifies `ANTHROPIC_BASE_URL`, export it. +7. If `provider.env` specifies `ANTHROPIC_API_KEY_FILE`, read the key from the file and export it as `ANTHROPIC_API_KEY`. Error if the file is missing. +8. Export any other entries from `provider.env` as environment variables. +9. Execute `claude --model "" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`. +10. On exit, the existing WezTerm reset trap runs. ## File contracts @@ -122,33 +105,25 @@ escalates_to: # used by statusline to render →Opus arrow The body must include sections (in order): `## Purpose`, `## Driver constraint`, `## Subagent policy`, `## Reasoning posture`, `## Async policy`, `## Workflow stance`, `## Auto-fire at session start`, `## Status line format`, `## Escalation triggers`, `## Out of scope`, `## On context wipe (/clear)`. `## Named workflows available` is optional. -### Provider files — `/data/claude-profile/providers/.yaml` +### Profile provider config — `$CLAUDE_CONFIG_DIR/provider.env` -Plain YAML (no frontmatter). Required field: `model_id`. All other fields are optional. +Optional. Only needed for non-Anthropic profiles. Plain key=value format (no quoting, no shell expansion): -```yaml -name: minimax-sonnet # unique identifier, matches filename -display: MiniMax (M2.7) # shown in the interactive picker -model_id: MiniMax-M2.7 # passed to claude --model -base_url: https://api.minimax.io/anthropic # exported as ANTHROPIC_BASE_URL (omit for Anthropic) -api_key_env: ANTHROPIC_API_KEY # env var name for the API key (omit for Anthropic) -api_key_file: ~/.claude-secrets/minimax-auth-token # file containing the raw token value -extra_env: - # Claude Code uses separate tier vars; all must point to the same model to prevent Anthropic fallback - ANTHROPIC_MODEL: MiniMax-M2.7 - ANTHROPIC_SMALL_FAST_MODEL: MiniMax-M2.7 - ANTHROPIC_DEFAULT_SONNET_MODEL: MiniMax-M2.7 - ANTHROPIC_DEFAULT_OPUS_MODEL: MiniMax-M2.7 - ANTHROPIC_DEFAULT_HAIKU_MODEL: MiniMax-M2.7 - API_TIMEOUT_MS: "3000000" - CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1" - DISABLE_PROMPT_CACHING: "1" - CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" +``` +MODEL_ID=MiniMax-M2.7 +ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic +ANTHROPIC_API_KEY_FILE=~/.claude-secrets/minimax-auth-token +ANTHROPIC_MODEL=MiniMax-M2.7 +ANTHROPIC_SMALL_FAST_MODEL=MiniMax-M2.7 +API_TIMEOUT_MS=3000000 ``` -Anthropic providers need only `name`, `display`, and `model_id` — Claude Code handles Anthropic auth via its own config. +Reserved keys with special handling: +- `MODEL_ID` — overrides the driver→model_id mapping; passed to `claude --model` +- `ANTHROPIC_BASE_URL` — exported as-is +- `ANTHROPIC_API_KEY_FILE` — file is read and its contents exported as `ANTHROPIC_API_KEY` -Adding a new provider = one new YAML file in `providers/`. No script changes required. +All other keys are exported as environment variables unchanged. ### Active mode env — `$CLAUDE_CONFIG_DIR/active-mode.env` @@ -160,10 +135,8 @@ Written on every launch, overwritten each time. Read by: CLAUDE_MODE=deep CLAUDE_MODE_TAG=deep CLAUDE_DRIVER=sonnet -CLAUDE_PROVIDER=anthropic-sonnet CLAUDE_ESCALATES_TO=opus CLAUDE_PROJECT=cluster-bootstrap -CLAUDE_TIME_HORIZON=hours CLAUDE_ASYNC_OK=yes CLAUDE_AUTOLOOP=none CLAUDE_PLAN_MODE_AUTO=yes @@ -175,42 +148,29 @@ CLAUDE_MODE_FILE=/home/paul/dev/claude/small-scripts/data/claude-profile/modes/d Single line: the mode name. Used as the default in the next interactive picker. -### Last project per mode — `$CLAUDE_CONFIG_DIR/last-mode-project-` - -Single line: the project name last used with this mode. Per-mode so switching modes doesn't lose the previous mode's project context. - ### Presets — `$CLAUDE_CONFIG_DIR/presets.yaml` Per-profile, user-edited. Each preset is a named bundle of answers to the picker: ```yaml deep-cluster: - profile: oreillyit + profile: oreillyit-anthropic mode: deep project: cluster-bootstrap - time_horizon: hours async: yes quick-skills: - profile: oreillyit + profile: oreillyit-anthropic mode: quick - project: custom-claude-skills - -orch-overnight: - profile: oreillyit - mode: orch - project: ai-image-gen - time_horizon: overnight - autoloop: yes + project: small-scripts quick-minimax: - profile: oreillyit + profile: oreillyit-minimax mode: quick - provider: minimax-sonnet project: small-scripts ``` -The `profile` field is required. `provider` defaults to the driver-mapped Anthropic provider if omitted. See `data/claude-profile/presets.yaml.example` for a full example. +The `profile` field is required. See `data/claude-profile/presets.yaml.example` for a full example. ## Status line integration @@ -232,36 +192,31 @@ When `--dryrun` (or `-n`) is passed, claude-profile resolves all values exactly ``` $ claude-profile --dryrun --preset deep-cluster -[dryrun] Profile: oreillyit (~/.claude-oreillyit) +[dryrun] Profile: oreillyit-anthropic (~/.claude-oreillyit-anthropic) [dryrun] Mode: deep [dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md [dryrun] Driver: sonnet -[dryrun] Provider: anthropic-sonnet (Anthropic (Sonnet)) [dryrun] Model ID: claude-sonnet-4-6 [dryrun] Project: cluster-bootstrap -[dryrun] Time horizon: hours [dryrun] Async OK: yes [dryrun] Autoloop: none -[dryrun] Would write: ~/.claude-oreillyit/active-mode.env -[dryrun] Would write: ~/.claude-oreillyit/last-mode +[dryrun] Would write: ~/.claude-oreillyit-anthropic/active-mode.env +[dryrun] Would write: ~/.claude-oreillyit-anthropic/last-mode [dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "" [dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode body) [dryrun] No changes made. ``` -**Dryrun with MiniMax provider:** +**Dryrun with MiniMax profile:** ``` -$ claude-profile --dryrun oreillyit --mode quick --provider minimax-sonnet -[dryrun] Profile: oreillyit (~/.claude-oreillyit) +$ claude-profile --dryrun oreillyit-minimax --mode quick +[dryrun] Profile: oreillyit-minimax (~/.claude-oreillyit-minimax) [dryrun] Mode: quick [dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/quick.md [dryrun] Driver: sonnet -[dryrun] Provider: minimax-sonnet (MiniMax (M2.7)) [dryrun] Model ID: MiniMax-M2.7 -[dryrun] Base URL: https://api.minimax.io/anthropic -[dryrun] API key env: ANTHROPIC_AUTH_TOKEN (from ~/.claude-secrets/minimax-auth-token) +[dryrun] Provider env: ~/.claude-oreillyit-minimax/provider.env [dryrun] Project: -[dryrun] Time horizon: [dryrun] Async OK: no [dryrun] Autoloop: none [dryrun] Would execute: claude --model MiniMax-M2.7 --append-system-prompt "" @@ -277,16 +232,12 @@ In dryrun mode the interactive prompts are still shown, so the user can walk thr | `~/dev/claude/small-scripts/data/claude-profile/modes/` does not exist | Error: "Mode files directory not found at ." Exit 1. | | Mode file referenced by `--mode` does not exist | Error: "Mode file not found: ." Exit 1. | | Mode file has malformed frontmatter (missing required field) | Error naming the missing field and the file path. Exit 1. | -| `data/claude-profile/providers/` does not exist | Error: "Providers directory not found at ." Exit 1. | -| Provider file referenced by `--provider` does not exist | Error: "Provider not found: (looked in )." Exit 1. | -| Provider file exists but is missing `model_id` | Error: "Provider file is missing required field: model_id." Exit 1. | -| Provider has `api_key_file` but the file does not exist at launch | Error: "Provider API key file not found: ." Exit 1. | -| `--provider` not given | Default provider derived from mode's `driver` field; interactive picker shown with that as the default. | +| `provider.env` exists but has no `MODEL_ID` | Driver default is used; other entries are still exported. | +| `provider.env` specifies `ANTHROPIC_API_KEY_FILE` but the file does not exist | Error: "API key file not found: ." Exit 1. | | `presets.yaml` does not exist and `--preset` was given | Error: "Preset '' not found in any presets.yaml." Exit 1. | | `presets.yaml` exists but the named preset is not in it | Error: "Preset '' not found in any presets.yaml." Exit 1. | | Preset's `profile` field references a non-existent profile | Error. Exit 1. | | `--preset` and `--mode` both given | Error: "mutually exclusive". Exit 1. | -| `--preset` and `--provider` both given | Error: "mutually exclusive". Exit 1. | | Positional `PROFILE` and `--preset` profile field disagree | Error: "Profile mismatch: positional , preset specifies ." Exit 1. | | `last-mode` file is missing or contains an unknown mode | Fall back to `quick` as the default. | | Two parallel sessions launched against the same profile | Both write to the same `active-mode.env` — last writer wins. Documented limitation; not addressed in v1. | @@ -300,7 +251,8 @@ In dryrun mode the interactive prompts are still shown, so the user can walk thr $ claude-profile Claude Code profiles: 1) default (~/.claude) - 2) oreillyit (~/.claude-oreillyit) + 2) oreillyit-anthropic (~/.claude-oreillyit-anthropic) + 3) oreillyit-minimax (~/.claude-oreillyit-minimax) Choose profile [1]: 2 Engagement modes: @@ -311,34 +263,24 @@ Engagement modes: 5) orch — Decompose work and dispatch container agents Choose mode [quick]: 3 -Time horizon? [hours]: -Async OK? [yes]: - -Launching: oreillyit / deep +Launching: oreillyit-anthropic / deep [Claude Code starts; the existing CLAUDE.md project picker runs next] ``` **Direct — preset** ``` $ claude-profile --preset deep-cluster -Launching: oreillyit / deep / cluster-bootstrap (from preset deep-cluster) +Launching: oreillyit-anthropic / deep / cluster-bootstrap (from preset deep-cluster) [Claude Code starts] ``` **Direct — profile + mode flag** ``` -$ claude-profile oreillyit --mode quick --project small-scripts -Launching: oreillyit / quick / anthropic-sonnet / small-scripts +$ claude-profile oreillyit-anthropic --mode quick --project small-scripts +Launching: oreillyit-anthropic / quick / small-scripts [Claude Code starts] ``` -**Direct — MiniMax provider** -``` -$ claude-profile oreillyit --mode quick --provider minimax-sonnet -Launching: oreillyit / quick / minimax-sonnet -[Claude Code starts with ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN set] -``` - **Dryrun — verify a preset** ``` $ claude-profile --dryrun --preset deep-cluster @@ -354,8 +296,8 @@ $ claude-profile --mode quick --project small-scripts -- --resume ## Out of scope (for v1) - Multi-session coordination (parallel claude-profile launches against the same profile) -- Mid-session mode or provider switching from inside Claude Code (handled by a separate `switch mode` skill; requires relaunching) -- Editing mode or provider files in place (they are read-only data; customisation happens via `presets.yaml`) +- Mid-session mode switching from inside Claude Code (handled by a separate `switch mode` skill; requires relaunching) +- Editing mode files in place (they are read-only data; customisation happens via `presets.yaml`) - Network/remote profiles (everything is local) - SOPS-encrypted provider credentials at launch time (credentials are stored as plaintext in `~/.claude-secrets/`; SOPS decryption is handled by the agent-runtimes container harness for headless agents) - Pinning the model used by named custom subagents at the harness level (those are controlled by the subagent's own definition file; the Agent tool's per-call `model` parameter is unaffected) diff --git a/tests/test-claude-profile.sh b/tests/test-claude-profile.sh index 6dc99d2..5e5a469 100755 --- a/tests/test-claude-profile.sh +++ b/tests/test-claude-profile.sh @@ -73,34 +73,29 @@ assert_exit_code "--help exits 0" 0 "$rc" assert_contains "--help shows usage" "claude-profile" "$out" assert_contains "--help shows --mode" "--mode" "$out" assert_contains "--help shows --preset" "--preset" "$out" -assert_contains "--help shows --provider" "--provider" "$out" # === Test 2: chat dryrun === echo echo "Test 2: chat mode dryrun" -out=$(run --dryrun oreillyit --mode chat --provider anthropic-haiku) +out=$(run --dryrun oreillyit-anthropic --mode chat) rc=$? assert_exit_code "chat dryrun exits 0" 0 "$rc" assert_contains "chat shows haiku driver" "Driver: haiku" "$out" -assert_contains "chat shows provider" "Provider: anthropic-haiku" "$out" assert_contains "chat shows model ID" "Model ID: claude-haiku-4-5-20251001" "$out" assert_contains "chat shows mode tag" "Mode: chat" "$out" assert_contains "chat says no changes" "No changes made" "$out" -assert_contains "chat does not ask time horizon" "" "$out" assert_contains "chat shows model in exec command" "claude --model claude-haiku-4-5-20251001" "$out" # === Test 3: deep dryrun with project flag === echo echo "Test 3: deep mode dryrun" -out=$(run --dryrun oreillyit --mode deep --project cluster-bootstrap --provider anthropic-sonnet) +out=$(run --dryrun oreillyit-anthropic --mode deep --project cluster-bootstrap) rc=$? assert_exit_code "deep dryrun exits 0" 0 "$rc" assert_contains "deep shows sonnet driver" "Driver: sonnet" "$out" -assert_contains "deep shows provider" "Provider: anthropic-sonnet" "$out" assert_contains "deep shows model ID" "Model ID: claude-sonnet-4-6" "$out" assert_contains "deep shows --model in command" "claude --model claude-sonnet-4-6" "$out" assert_contains "deep records the project" "Project: cluster-bootstrap" "$out" -assert_contains "deep defaults time horizon to hours" "Time horizon: hours" "$out" assert_contains "deep says async OK" "Async OK: yes" "$out" assert_contains "deep says plan_mode_auto yes" "Plan mode auto: yes" "$out" assert_contains "deep says spec_driven yes" "Spec driven: yes" "$out" @@ -108,38 +103,34 @@ assert_contains "deep says spec_driven yes" "Spec driven: yes" "$out" # === Test 4: orch dryrun preserves autoloop === echo echo "Test 4: orch mode dryrun (autoloop preserved)" -out=$(run --dryrun oreillyit --mode orch --project ai-image-gen --provider anthropic-sonnet) +out=$(run --dryrun oreillyit-anthropic --mode orch --project ai-image-gen) rc=$? assert_exit_code "orch dryrun exits 0" 0 "$rc" assert_contains "orch preserves the slash command in autoloop" "/loop 2m /orchestrate" "$out" assert_contains "orch driver is sonnet" "Driver: sonnet" "$out" -assert_contains "orch provider is anthropic-sonnet" "Provider: anthropic-sonnet" "$out" # === Test 5: hybrid dryrun === echo echo "Test 5: hybrid mode dryrun" -out=$(run --dryrun oreillyit --mode hybrid --provider anthropic-haiku) +out=$(run --dryrun oreillyit-anthropic --mode hybrid) rc=$? assert_exit_code "hybrid dryrun exits 0" 0 "$rc" assert_contains "hybrid uses haiku driver" "Driver: haiku" "$out" assert_contains "hybrid allows async" "Async OK: yes" "$out" -assert_contains "hybrid shows provider" "Provider: anthropic-haiku" "$out" -# === Test 6: quick dryrun (no time horizon question) === +# === Test 6: quick dryrun === echo echo "Test 6: quick mode dryrun" -out=$(run --dryrun oreillyit --mode quick --provider anthropic-sonnet) +out=$(run --dryrun oreillyit-anthropic --mode quick) rc=$? assert_exit_code "quick dryrun exits 0" 0 "$rc" assert_contains "quick uses sonnet driver" "Driver: sonnet" "$out" assert_contains "quick disables async" "Async OK: no" "$out" -assert_contains "quick does not ask time horizon" "" "$out" -assert_contains "quick shows provider" "Provider: anthropic-sonnet" "$out" # === Test 7: error — unknown mode === echo echo "Test 7: error path — unknown mode" -out=$(run --dryrun oreillyit --mode bogus) +out=$(run --dryrun oreillyit-anthropic --mode bogus) rc=$? assert_exit_code "unknown mode exits 1" 1 "$rc" assert_contains "unknown mode error message" "Mode file not found" "$out" @@ -163,66 +154,28 @@ assert_contains "missing preset error message" "not found" "$out" # === Test 10: pass-through args via -- === echo echo "Test 10: pass-through args" -out=$(run --dryrun oreillyit --mode chat --provider anthropic-haiku -- --resume some-session) +out=$(run --dryrun oreillyit-anthropic --mode chat -- --resume some-session) rc=$? assert_exit_code "pass-through exits 0" 0 "$rc" assert_contains "pass-through preserves --resume" "--resume some-session" "$out" -# === Test 11: minimax provider dryrun === +# === Test 11: minimax profile picks up provider.env === echo -echo "Test 11: minimax provider dryrun" -out=$(run --dryrun oreillyit --mode quick --provider minimax-sonnet) +echo "Test 11: minimax profile uses provider.env" +out=$(run --dryrun oreillyit-minimax --mode quick) rc=$? assert_exit_code "minimax dryrun exits 0" 0 "$rc" -assert_contains "minimax shows provider" "Provider: minimax-sonnet" "$out" -assert_contains "minimax shows model ID" "Model ID: MiniMax-M2.7" "$out" -assert_contains "minimax shows base URL" "Base URL: https://api.minimax.io/anthropic" "$out" -assert_contains "minimax shows API key env" "API key env: ANTHROPIC_API_KEY" "$out" -assert_contains "minimax references key file" "minimax-auth-token" "$out" +assert_contains "minimax shows MiniMax model ID" "Model ID: MiniMax-M2.7" "$out" +assert_contains "minimax shows provider env file" "Provider env:" "$out" assert_contains "minimax shows model in exec command" "claude --model MiniMax-M2.7" "$out" -# === Test 12: error — unknown provider === +# === Test 12: error — unknown flag (--provider no longer valid) === echo -echo "Test 12: error path — unknown provider" -out=$(run --dryrun oreillyit --mode quick --provider bogus-provider) +echo "Test 12: error path — unknown flag" +out=$(run --dryrun oreillyit-anthropic --mode quick --provider anthropic-sonnet) rc=$? -assert_exit_code "unknown provider exits 1" 1 "$rc" -assert_contains "unknown provider error message" "Provider not found" "$out" - -# === Test 13: default provider from driver (no --provider flag) === -echo -echo "Test 13: default provider selected from driver" -out=$(run --dryrun oreillyit --mode chat) -rc=$? -assert_exit_code "default provider dryrun exits 0" 0 "$rc" -assert_contains "chat default provider is anthropic-haiku" "Provider: anthropic-haiku" "$out" -assert_contains "chat default model ID is haiku" "Model ID: claude-haiku-4-5-20251001" "$out" - -# === Test 14: --preset + --provider mutual exclusion === - -echo -echo "Test 14: error path — --preset + --provider mutual exclusion" -out=$(run --dryrun --preset whatever --provider minimax-sonnet) -rc=$? -assert_exit_code "preset+provider exits 1" 1 "$rc" -assert_contains "preset+provider error message" "mutually exclusive" "$out" - -# === Test 15: OAuth credentials warning fires for non-Anthropic provider === -echo -echo "Test 15: OAuth credentials warning" -out=$(run --dryrun oreillyit --mode quick --provider minimax-sonnet) -rc=$? -assert_exit_code "credentials warning dryrun exits 0" 0 "$rc" -assert_contains "warning mentions stored credentials" ".credentials.json" "$out" -assert_contains "warning names the provider" "minimax-sonnet" "$out" - -# === Test 16: No warning when profile has no OAuth credentials === -echo -echo "Test 16: no warning for credentials-free profile" -out=$(run --dryrun minimax --mode quick --provider minimax-sonnet) -rc=$? -assert_exit_code "no-credentials dryrun exits 0" 0 "$rc" -assert_not_contains "no credentials warning" ".credentials.json" "$out" +assert_exit_code "unknown flag exits 1" 1 "$rc" +assert_contains "unknown flag error message" "Unknown flag" "$out" # === Summary === echo