claude-profile: remove provider-yaml system, embed model config in profiles
Provider selection phase removed; Anthropic driver→model_id is now hardcoded in the script and non-Anthropic profiles supply provider.env (MODEL_ID, ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY_FILE). Test suite baseline changes 58 → 41 assertions: the 17 dropped assertions covered the removed provider-yaml selection phase, not retained behaviour. Includes refactor-derived memory: decisions (provider architecture, driver enforcement via --model), bash gotchas (read -rp under set -e, never eval user config), process lessons.
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
<!-- Thin index — one-line entries linking to context/ detail files -->
|
<!-- Thin index — one-line entries linking to context/ detail files -->
|
||||||
|
|
||||||
- [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
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<!-- Thin index only — one-line descriptions linking to memory/ topic files -->
|
<!-- Thin index only — one-line descriptions linking to memory/ topic files -->
|
||||||
|
|
||||||
## Gotchas
|
## 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
|
- [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
|
- [WezTerm Gotchas](memory/gotchas-wezterm.md) — Flatpak CLI not in PATH, proportional split math for equal panes
|
||||||
|
|
||||||
|
|||||||
@@ -2,42 +2,45 @@
|
|||||||
|
|
||||||
## Status
|
## 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
|
## What's done
|
||||||
|
|
||||||
| Component | State |
|
| 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 mode files with `escalates_to` frontmatter | Done |
|
||||||
| 5 provider files (4 Anthropic + minimax-sonnet) | Done (`data/claude-profile/providers/`) |
|
| `provider.env` for non-Anthropic profiles | Done (`~/.claude-oreillyit-minimax/provider.env`) |
|
||||||
| `active-mode.env` written on every launch | Done (includes `CLAUDE_PROVIDER`) |
|
| Driver→model_id hardcoded for Anthropic | Done (haiku/sonnet/opus/opus-1m) |
|
||||||
| MiniMax token decrypted + stored | Done (`~/.claude-secrets/minimax-auth-token`, 600 perms) |
|
| `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`) |
|
| 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 |
|
| 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`) |
|
| 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
|
## Next session — recommended order
|
||||||
|
|
||||||
1. **Install the symlink** (if not already done):
|
1. **Install the symlink** (if not already done):
|
||||||
```bash
|
```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
|
- Mode body appears in system prompt
|
||||||
- Status bar shows `[Haiku] chat` format
|
- Status bar shows `[Haiku] chat` format
|
||||||
- `~/.claude-oreillyit/active-mode.env` has `CLAUDE_PROVIDER=anthropic-haiku`
|
- `~/.claude-oreillyit-anthropic/active-mode.env` has no `CLAUDE_PROVIDER` or `CLAUDE_TIME_HORIZON`
|
||||||
3. **MiniMax launch test:** `claude-profile oreillyit --mode quick --provider minimax-sonnet`. Verify:
|
3. **MiniMax launch test:** `claude-profile oreillyit-minimax --mode quick`. Verify:
|
||||||
- Claude starts and is responsive
|
- Claude starts and is responsive
|
||||||
- `ANTHROPIC_BASE_URL` is set to MiniMax endpoint (check via a "what model are you?" prompt)
|
- `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
|
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/<name>.yaml` file. For third-party providers:
|
|
||||||
- Decrypt credentials and store the raw token in `~/.claude-secrets/<provider>-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)
|
## 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.
|
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/<name>.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.
|
- **`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`.
|
- **`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.
|
- **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).
|
||||||
|
|||||||
@@ -7,42 +7,38 @@
|
|||||||
|
|
||||||
# A "deep work" session on the homelab cluster
|
# A "deep work" session on the homelab cluster
|
||||||
deep-cluster:
|
deep-cluster:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: deep
|
mode: deep
|
||||||
project: cluster-bootstrap
|
project: cluster-bootstrap
|
||||||
time_horizon: hours
|
|
||||||
async: yes
|
async: yes
|
||||||
|
|
||||||
# A quick edit on the small-scripts repo (no questions, just launch)
|
# A quick edit on the small-scripts repo (no questions, just launch)
|
||||||
quick-skills:
|
quick-skills:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: quick
|
mode: quick
|
||||||
project: small-scripts
|
project: small-scripts
|
||||||
|
|
||||||
# Overnight orchestration of container agents on the image-gen project
|
# Overnight orchestration of container agents on the image-gen project
|
||||||
orch-overnight:
|
orch-overnight:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: orch
|
mode: orch
|
||||||
project: ai-image-gen
|
project: ai-image-gen
|
||||||
time_horizon: overnight
|
|
||||||
autoloop: yes
|
autoloop: yes
|
||||||
|
|
||||||
# Cheap conversational session, no project
|
# Cheap conversational session, no project
|
||||||
chat-only:
|
chat-only:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: chat
|
mode: chat
|
||||||
|
|
||||||
# Hybrid session — Haiku driver with Opus subagents on call
|
# Hybrid session — Haiku driver with Opus subagents on call
|
||||||
hybrid-day:
|
hybrid-day:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: hybrid
|
mode: hybrid
|
||||||
project: cluster-bootstrap
|
project: cluster-bootstrap
|
||||||
time_horizon: hours
|
|
||||||
async: yes
|
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:
|
quick-minimax:
|
||||||
profile: minimax
|
profile: oreillyit-minimax
|
||||||
mode: quick
|
mode: quick
|
||||||
provider: minimax-sonnet
|
|
||||||
project: small-scripts
|
project: small-scripts
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
name: anthropic-haiku
|
|
||||||
display: Anthropic (Haiku)
|
|
||||||
model_id: claude-haiku-4-5-20251001
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
name: anthropic-opus-1m
|
|
||||||
display: Anthropic (Opus 1M ctx)
|
|
||||||
model_id: "claude-opus-4-6[1m]"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
name: anthropic-opus
|
|
||||||
display: Anthropic (Opus)
|
|
||||||
model_id: claude-opus-4-6
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
name: anthropic-sonnet
|
|
||||||
display: Anthropic (Sonnet)
|
|
||||||
model_id: claude-sonnet-4-6
|
|
||||||
@@ -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"
|
|
||||||
@@ -23,3 +23,59 @@ Scripts that need to read JSON (e.g., `.reflection-state.json`) use inline pytho
|
|||||||
## Project hosted under `skynet` org
|
## 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.
|
`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/<name>.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 <full-id>` 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. <!-- STALE? CLAUDE.md session-start now auto-selects project from cwd or CLAUDE_PROJECT in active-mode.env (commit 340c403) — the deferred integration has shipped. -->
|
||||||
|
|
||||||
|
## 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=<value>`. 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.
|
||||||
|
|||||||
@@ -27,3 +27,11 @@ Test scripts with `set -euo pipefail` silently abort when an assertion helper ru
|
|||||||
## `file` command marks shell scripts as "executable"
|
## `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`.
|
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.
|
||||||
|
|||||||
15
memory/gotchas-claude-profile.md
Normal file
15
memory/gotchas-claude-profile.md
Normal file
@@ -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`.
|
||||||
@@ -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`
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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:
|
# Usage:
|
||||||
# claude-profile # interactive: profile + mode + provider
|
# claude-profile # interactive: profile + mode
|
||||||
# claude-profile <profile> # profile fixed, rest interactive
|
# claude-profile <profile> # profile fixed, mode interactive
|
||||||
# claude-profile --mode <name> # mode fixed, profile + provider interactive
|
# claude-profile --mode <name> # mode fixed, profile interactive
|
||||||
# claude-profile --provider <name> # provider fixed, profile + mode interactive
|
|
||||||
# claude-profile --preset <name> # all from presets.yaml
|
# claude-profile --preset <name> # all from presets.yaml
|
||||||
# claude-profile <profile> --mode <name> # profile + mode fixed
|
# claude-profile <profile> --mode <name> # profile + mode fixed
|
||||||
# claude-profile --dryrun [...] # show resolved values, do not launch
|
# claude-profile --dryrun [...] # show resolved values, do not launch
|
||||||
@@ -23,7 +22,6 @@ SCRIPT_PATH="$(readlink -f "$0")"
|
|||||||
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
MODES_DIR="$REPO_ROOT/data/claude-profile/modes"
|
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).
|
# context-load is on PATH (symlinked into ~/sbin from claude-foundations).
|
||||||
# Fall back to a sibling location for testing isolation.
|
# Fall back to a sibling location for testing isolation.
|
||||||
@@ -33,13 +31,13 @@ else
|
|||||||
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Driver logical name -> default provider (fallback when --provider not given)
|
# Driver logical name -> Anthropic model ID
|
||||||
driver_default_provider() {
|
driver_model_id() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
haiku) echo "anthropic-haiku" ;;
|
haiku) echo "claude-haiku-4-5-20251001" ;;
|
||||||
sonnet) echo "anthropic-sonnet" ;;
|
sonnet) echo "claude-sonnet-4-6" ;;
|
||||||
opus) echo "anthropic-opus" ;;
|
opus) echo "claude-opus-4-6" ;;
|
||||||
opus-1m) echo "anthropic-opus-1m" ;;
|
opus-1m) echo "claude-opus-4-6[1m]" ;;
|
||||||
*) err "Unknown driver '$1'. Valid: haiku sonnet opus opus-1m" ;;
|
*) err "Unknown driver '$1'. Valid: haiku sonnet opus opus-1m" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -48,7 +46,6 @@ driver_default_provider() {
|
|||||||
|
|
||||||
PROFILE=""
|
PROFILE=""
|
||||||
MODE=""
|
MODE=""
|
||||||
PROVIDER=""
|
|
||||||
PROJECT=""
|
PROJECT=""
|
||||||
PRESET=""
|
PRESET=""
|
||||||
DRYRUN=0
|
DRYRUN=0
|
||||||
@@ -61,7 +58,6 @@ usage() {
|
|||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--mode) MODE="${2:-}"; shift 2 ;;
|
--mode) MODE="${2:-}"; shift 2 ;;
|
||||||
--provider) PROVIDER="${2:-}"; shift 2 ;;
|
|
||||||
--preset) PRESET="${2:-}"; shift 2 ;;
|
--preset) PRESET="${2:-}"; shift 2 ;;
|
||||||
--project) PROJECT="${2:-}"; shift 2 ;;
|
--project) PROJECT="${2:-}"; shift 2 ;;
|
||||||
--dryrun|-n) DRYRUN=1; shift ;;
|
--dryrun|-n) DRYRUN=1; shift ;;
|
||||||
@@ -81,8 +77,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Mutual exclusion
|
# Mutual exclusion
|
||||||
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" || -n "$PROVIDER" ) ]]; then
|
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" ) ]]; then
|
||||||
echo "Error: --preset is mutually exclusive with --mode, --provider, and --project" >&2
|
echo "Error: --preset is mutually exclusive with --mode and --project" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -145,40 +141,6 @@ read_preset_block() {
|
|||||||
' "$file"
|
' "$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.
|
# List available modes (sorted), printing a numbered menu.
|
||||||
# Returns the array of mode names via the global MODE_LIST.
|
# Returns the array of mode names via the global MODE_LIST.
|
||||||
MODE_LIST=()
|
MODE_LIST=()
|
||||||
@@ -196,23 +158,6 @@ list_modes() {
|
|||||||
done
|
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() {
|
resolve_profile_dir() {
|
||||||
local name=$1
|
local name=$1
|
||||||
if [[ "$name" == "default" ]]; then
|
if [[ "$name" == "default" ]]; then
|
||||||
@@ -235,7 +180,6 @@ reset_wezterm_profile() {
|
|||||||
|
|
||||||
# Sanity check on the modes directory before doing any picker work.
|
# Sanity check on the modes directory before doing any picker work.
|
||||||
[[ -d "$MODES_DIR" ]] || err "Mode files directory not found at $MODES_DIR"
|
[[ -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
|
if [[ -n "$PRESET" ]]; then
|
||||||
# Find the preset across all profile presets.yaml files.
|
# Find the preset across all profile presets.yaml files.
|
||||||
@@ -259,9 +203,7 @@ if [[ -n "$PRESET" ]]; then
|
|||||||
case "$k" in
|
case "$k" in
|
||||||
PRESET_PROFILE) preset_profile_name="$v" ;;
|
PRESET_PROFILE) preset_profile_name="$v" ;;
|
||||||
PRESET_MODE) MODE="$v" ;;
|
PRESET_MODE) MODE="$v" ;;
|
||||||
PRESET_PROVIDER) PROVIDER="$v" ;;
|
|
||||||
PRESET_PROJECT) PROJECT="$v" ;;
|
PRESET_PROJECT) PROJECT="$v" ;;
|
||||||
PRESET_TIME_HORIZON) TIME_HORIZON="$v" ;;
|
|
||||||
PRESET_ASYNC) ASYNC_OVERRIDE="$v" ;;
|
PRESET_ASYNC) ASYNC_OVERRIDE="$v" ;;
|
||||||
PRESET_AUTOLOOP) AUTOLOOP_OVERRIDE="$v" ;;
|
PRESET_AUTOLOOP) AUTOLOOP_OVERRIDE="$v" ;;
|
||||||
esac
|
esac
|
||||||
@@ -307,6 +249,16 @@ fi
|
|||||||
|
|
||||||
[[ -d "$target" ]] || err "Profile directory not found: $target"
|
[[ -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 ===
|
# === Phase 2: Mode selection ===
|
||||||
|
|
||||||
if [[ -z "$MODE" ]]; then
|
if [[ -z "$MODE" ]]; then
|
||||||
@@ -360,22 +312,6 @@ for field_name in DRIVER TAG ASYNC_OK; do
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
# Apply preset overrides for async/autoloop
|
||||||
[[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE"
|
[[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE"
|
||||||
[[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && {
|
[[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && {
|
||||||
@@ -384,55 +320,25 @@ fi
|
|||||||
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
|
# Override from profile's provider.env if present
|
||||||
if [[ -z "$PRESET" ]]; then
|
PROVIDER_ENV="$target/provider.env"
|
||||||
echo ""
|
if [[ -f "$PROVIDER_ENV" ]]; then
|
||||||
echo "Provider (model + API):"
|
# Source provider.env into a subshell-safe set of variables.
|
||||||
list_providers
|
# We parse key=value lines rather than sourcing directly to avoid side effects.
|
||||||
echo ""
|
while IFS='=' read -r key value; do
|
||||||
if [[ -t 0 ]]; then
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||||
read -rp "Choose provider [$default_provider]: " prov_choice
|
case "$key" in
|
||||||
else
|
MODEL_ID) MODEL_ID="$value" ;;
|
||||||
prov_choice=""
|
ANTHROPIC_BASE_URL) PROVIDER_BASE_URL="$value" ;;
|
||||||
fi
|
ANTHROPIC_API_KEY_FILE) PROVIDER_API_KEY_FILE="$value" ;;
|
||||||
prov_choice="${prov_choice:-$default_provider}"
|
*) PROVIDER_EXTRA_ENV+=("$key=$value") ;;
|
||||||
|
esac
|
||||||
if [[ "$prov_choice" =~ ^[0-9]+$ ]]; then
|
done < "$PROVIDER_ENV"
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# === Phase 3: Launch ===
|
# === Phase 3: Launch ===
|
||||||
@@ -445,10 +351,8 @@ active_env=$(cat <<EOF
|
|||||||
CLAUDE_MODE=$MODE
|
CLAUDE_MODE=$MODE
|
||||||
CLAUDE_MODE_TAG=$TAG
|
CLAUDE_MODE_TAG=$TAG
|
||||||
CLAUDE_DRIVER=$DRIVER
|
CLAUDE_DRIVER=$DRIVER
|
||||||
CLAUDE_PROVIDER=$PROVIDER
|
|
||||||
CLAUDE_ESCALATES_TO=${ESCALATES_TO:-none}
|
CLAUDE_ESCALATES_TO=${ESCALATES_TO:-none}
|
||||||
CLAUDE_PROJECT=${PROJECT:-}
|
CLAUDE_PROJECT=${PROJECT:-}
|
||||||
CLAUDE_TIME_HORIZON=${TIME_HORIZON:-}
|
|
||||||
CLAUDE_ASYNC_OK=$ASYNC_OK
|
CLAUDE_ASYNC_OK=$ASYNC_OK
|
||||||
CLAUDE_AUTOLOOP=$AUTOLOOP
|
CLAUDE_AUTOLOOP=$AUTOLOOP
|
||||||
CLAUDE_PLAN_MODE_AUTO=$PLAN_MODE_AUTO
|
CLAUDE_PLAN_MODE_AUTO=$PLAN_MODE_AUTO
|
||||||
@@ -478,12 +382,11 @@ if (( DRYRUN )); then
|
|||||||
dryrun_log "Mode: $MODE"
|
dryrun_log "Mode: $MODE"
|
||||||
dryrun_log "Mode file: $mode_file"
|
dryrun_log "Mode file: $mode_file"
|
||||||
dryrun_log "Driver: $DRIVER"
|
dryrun_log "Driver: $DRIVER"
|
||||||
dryrun_log "Provider: $PROVIDER ($PROVIDER_DISPLAY)"
|
|
||||||
dryrun_log "Model ID: $MODEL_ID"
|
dryrun_log "Model ID: $MODEL_ID"
|
||||||
[[ -n "$PROVIDER_BASE_URL" ]] && dryrun_log "Base URL: $PROVIDER_BASE_URL"
|
[[ -n "${PROVIDER_BASE_URL:-}" ]] && dryrun_log "Base URL: $PROVIDER_BASE_URL"
|
||||||
[[ -n "$PROVIDER_API_KEY_ENV" ]] && dryrun_log "API key env: $PROVIDER_API_KEY_ENV (from $PROVIDER_API_KEY_FILE)"
|
[[ -n "${PROVIDER_API_KEY_FILE:-}" ]] && dryrun_log "API key file: $PROVIDER_API_KEY_FILE"
|
||||||
|
[[ -f "$PROVIDER_ENV" ]] && dryrun_log "Provider env: $PROVIDER_ENV"
|
||||||
dryrun_log "Project: ${PROJECT:-<not set, will be picked by CLAUDE.md>}"
|
dryrun_log "Project: ${PROJECT:-<not set, will be picked by CLAUDE.md>}"
|
||||||
dryrun_log "Time horizon: ${TIME_HORIZON:-<not asked in this mode>}"
|
|
||||||
dryrun_log "Async OK: $ASYNC_OK"
|
dryrun_log "Async OK: $ASYNC_OK"
|
||||||
dryrun_log "Autoloop: $AUTOLOOP"
|
dryrun_log "Autoloop: $AUTOLOOP"
|
||||||
dryrun_log "Escalates to: ${ESCALATES_TO:-none}"
|
dryrun_log "Escalates to: ${ESCALATES_TO:-none}"
|
||||||
@@ -503,32 +406,31 @@ fi
|
|||||||
|
|
||||||
# === Real launch from here on ===
|
# === Real launch from here on ===
|
||||||
|
|
||||||
# Set up WezTerm theming and reset trap
|
|
||||||
trap reset_wezterm_profile EXIT
|
|
||||||
export CLAUDE_CONFIG_DIR="$target"
|
export CLAUDE_CONFIG_DIR="$target"
|
||||||
set_wezterm_profile "$PROFILE"
|
|
||||||
|
|
||||||
# Disable adaptive thinking (existing behaviour)
|
# Disable adaptive thinking (existing behaviour)
|
||||||
export CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1
|
export CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1
|
||||||
|
|
||||||
# Export provider env vars for non-Anthropic providers
|
# Ensure proper colour support
|
||||||
if [[ -n "$PROVIDER_BASE_URL" ]]; then
|
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"
|
export ANTHROPIC_BASE_URL="$PROVIDER_BASE_URL"
|
||||||
fi
|
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}"
|
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")
|
api_key=$(cat "$expanded_key_file")
|
||||||
export "${PROVIDER_API_KEY_ENV}=${api_key}"
|
export ANTHROPIC_API_KEY="$api_key"
|
||||||
fi
|
fi
|
||||||
# Export extra_env entries from provider file
|
for entry in "${PROVIDER_EXTRA_ENV[@]+"${PROVIDER_EXTRA_ENV[@]}"}"; do
|
||||||
while IFS='=' read -r k v; do
|
[[ -n "$entry" ]] && export "$entry"
|
||||||
[[ -n "$k" ]] && export "${k}=${v}"
|
done
|
||||||
done < <(parse_extra_env "$provider_file")
|
|
||||||
|
|
||||||
# Write state files
|
# Write state files
|
||||||
printf '%s\n' "$active_env" > "$target/active-mode.env"
|
printf '%s\n' "$active_env" > "$target/active-mode.env"
|
||||||
printf '%s\n' "$MODE" > "$target/last-mode"
|
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[@]}"
|
exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Purpose
|
## 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
|
## Usage
|
||||||
|
|
||||||
@@ -22,20 +22,19 @@ claude-profile [OPTIONS] [PROFILE] [-- CLAUDE_ARGS...]
|
|||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `--mode <NAME>` | Skip the mode picker and use the named mode (`chat`, `quick`, `deep`, `hybrid`, `orch`). |
|
| `--mode <NAME>` | Skip the mode picker and use the named mode (`chat`, `quick`, `deep`, `hybrid`, `orch`). |
|
||||||
| `--provider <NAME>` | Skip the provider picker and use the named provider (e.g. `anthropic-sonnet`, `minimax-sonnet`). |
|
| `--preset <NAME>` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, and any preset-defined defaults in one shot. |
|
||||||
| `--preset <NAME>` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, provider, and any preset-defined defaults in one shot. |
|
|
||||||
| `--project <NAME>` | Pre-select a project (skips the project question for modes that ask it). |
|
| `--project <NAME>` | 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`. |
|
| `--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. |
|
| `--help`, `-h` | Show usage. |
|
||||||
|
|
||||||
### Mutual exclusion
|
### 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.
|
- A positional `PROFILE` combined with `--preset` is allowed only if the preset's `profile` field matches; mismatch is an error.
|
||||||
|
|
||||||
## Behaviour
|
## 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
|
### Phase 1 — Profile selection
|
||||||
|
|
||||||
@@ -49,20 +48,30 @@ The script runs in four phases: **profile selection**, **mode selection**, **pro
|
|||||||
|
|
||||||
### Phase 2 — Mode selection
|
### 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.
|
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 <name>` is given, use it and proceed to mode-specific questions (project, async, etc., depending on the mode).
|
2. Else if `--mode <name>` is given, use it.
|
||||||
3. Else (interactive): list available modes by scanning `<repo>/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`.
|
3. Else (interactive): list available modes by scanning `<repo>/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.
|
4. Validate the chosen mode file exists at `<repo>/data/claude-profile/modes/<name>.md`. If not, error and exit 1.
|
||||||
5. Validate the chosen mode file exists at `<repo>/data/claude-profile/modes/<name>.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`.
|
||||||
6. Read the mode file's frontmatter to extract `driver`, `tag`, `async_ok`, `autoloop`, `plan_mode_auto`, `spec_driven`.
|
|
||||||
|
|
||||||
### 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.
|
The model ID is determined by combining the mode's `driver` field with the profile's optional `provider.env`:
|
||||||
2. Else if `--provider <name>` 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 `<repo>/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.
|
1. Map the driver to an Anthropic model ID using the hardcoded table:
|
||||||
4. Validate the chosen provider file exists at `<repo>/data/claude-profile/providers/<name>.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`.
|
| 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 <name>` as a non-interactive override (for presets and scripted launches), which writes `CLAUDE_PROJECT` into `active-mode.env`.
|
||||||
|
|
||||||
### Phase 3 — Launch
|
### 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`.
|
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`.
|
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).
|
4. Set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` (existing behaviour).
|
||||||
5. If the provider has a `base_url`, export `ANTHROPIC_BASE_URL=<value>`.
|
5. Set `COLORTERM=truecolor` for proper colour support.
|
||||||
6. If the provider has `api_key_env` and `api_key_file`, read the key from the file and export it as `<api_key_env>=<value>`. Error if the file is missing.
|
6. If `provider.env` specifies `ANTHROPIC_BASE_URL`, export it.
|
||||||
7. Export any `extra_env` entries from the provider file.
|
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. Execute `claude --model "<provider.model_id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`.
|
8. Export any other entries from `provider.env` as environment variables.
|
||||||
9. On exit, the existing WezTerm reset trap runs.
|
9. Execute `claude --model "<model_id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`.
|
||||||
|
10. 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 <name>` 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.
|
|
||||||
|
|
||||||
## File contracts
|
## File contracts
|
||||||
|
|
||||||
@@ -122,33 +105,25 @@ escalates_to: <none|opus> # 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.
|
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 — `<repo>/data/claude-profile/providers/<name>.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
|
MODEL_ID=MiniMax-M2.7
|
||||||
display: MiniMax (M2.7) # shown in the interactive picker
|
ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
|
||||||
model_id: MiniMax-M2.7 # passed to claude --model
|
ANTHROPIC_API_KEY_FILE=~/.claude-secrets/minimax-auth-token
|
||||||
base_url: https://api.minimax.io/anthropic # exported as ANTHROPIC_BASE_URL (omit for Anthropic)
|
ANTHROPIC_MODEL=MiniMax-M2.7
|
||||||
api_key_env: ANTHROPIC_API_KEY # env var name for the API key (omit for Anthropic)
|
ANTHROPIC_SMALL_FAST_MODEL=MiniMax-M2.7
|
||||||
api_key_file: ~/.claude-secrets/minimax-auth-token # file containing the raw token value
|
API_TIMEOUT_MS=3000000
|
||||||
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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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`
|
### 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=deep
|
||||||
CLAUDE_MODE_TAG=deep
|
CLAUDE_MODE_TAG=deep
|
||||||
CLAUDE_DRIVER=sonnet
|
CLAUDE_DRIVER=sonnet
|
||||||
CLAUDE_PROVIDER=anthropic-sonnet
|
|
||||||
CLAUDE_ESCALATES_TO=opus
|
CLAUDE_ESCALATES_TO=opus
|
||||||
CLAUDE_PROJECT=cluster-bootstrap
|
CLAUDE_PROJECT=cluster-bootstrap
|
||||||
CLAUDE_TIME_HORIZON=hours
|
|
||||||
CLAUDE_ASYNC_OK=yes
|
CLAUDE_ASYNC_OK=yes
|
||||||
CLAUDE_AUTOLOOP=none
|
CLAUDE_AUTOLOOP=none
|
||||||
CLAUDE_PLAN_MODE_AUTO=yes
|
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.
|
Single line: the mode name. Used as the default in the next interactive picker.
|
||||||
|
|
||||||
### Last project per mode — `$CLAUDE_CONFIG_DIR/last-mode-project-<mode>`
|
|
||||||
|
|
||||||
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`
|
### Presets — `$CLAUDE_CONFIG_DIR/presets.yaml`
|
||||||
|
|
||||||
Per-profile, user-edited. Each preset is a named bundle of answers to the picker:
|
Per-profile, user-edited. Each preset is a named bundle of answers to the picker:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
deep-cluster:
|
deep-cluster:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: deep
|
mode: deep
|
||||||
project: cluster-bootstrap
|
project: cluster-bootstrap
|
||||||
time_horizon: hours
|
|
||||||
async: yes
|
async: yes
|
||||||
|
|
||||||
quick-skills:
|
quick-skills:
|
||||||
profile: oreillyit
|
profile: oreillyit-anthropic
|
||||||
mode: quick
|
mode: quick
|
||||||
project: custom-claude-skills
|
project: small-scripts
|
||||||
|
|
||||||
orch-overnight:
|
|
||||||
profile: oreillyit
|
|
||||||
mode: orch
|
|
||||||
project: ai-image-gen
|
|
||||||
time_horizon: overnight
|
|
||||||
autoloop: yes
|
|
||||||
|
|
||||||
quick-minimax:
|
quick-minimax:
|
||||||
profile: oreillyit
|
profile: oreillyit-minimax
|
||||||
mode: quick
|
mode: quick
|
||||||
provider: minimax-sonnet
|
|
||||||
project: small-scripts
|
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
|
## 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
|
$ claude-profile --dryrun --preset deep-cluster
|
||||||
[dryrun] Profile: oreillyit (~/.claude-oreillyit)
|
[dryrun] Profile: oreillyit-anthropic (~/.claude-oreillyit-anthropic)
|
||||||
[dryrun] Mode: deep
|
[dryrun] Mode: deep
|
||||||
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md
|
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md
|
||||||
[dryrun] Driver: sonnet
|
[dryrun] Driver: sonnet
|
||||||
[dryrun] Provider: anthropic-sonnet (Anthropic (Sonnet))
|
|
||||||
[dryrun] Model ID: claude-sonnet-4-6
|
[dryrun] Model ID: claude-sonnet-4-6
|
||||||
[dryrun] Project: cluster-bootstrap
|
[dryrun] Project: cluster-bootstrap
|
||||||
[dryrun] Time horizon: hours
|
|
||||||
[dryrun] Async OK: yes
|
[dryrun] Async OK: yes
|
||||||
[dryrun] Autoloop: none
|
[dryrun] Autoloop: none
|
||||||
[dryrun] Would write: ~/.claude-oreillyit/active-mode.env
|
[dryrun] Would write: ~/.claude-oreillyit-anthropic/active-mode.env
|
||||||
[dryrun] Would write: ~/.claude-oreillyit/last-mode
|
[dryrun] Would write: ~/.claude-oreillyit-anthropic/last-mode
|
||||||
[dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "<context+mode>"
|
[dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "<context+mode>"
|
||||||
[dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode body)
|
[dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode body)
|
||||||
[dryrun] No changes made.
|
[dryrun] No changes made.
|
||||||
```
|
```
|
||||||
|
|
||||||
**Dryrun with MiniMax provider:**
|
**Dryrun with MiniMax profile:**
|
||||||
```
|
```
|
||||||
$ claude-profile --dryrun oreillyit --mode quick --provider minimax-sonnet
|
$ claude-profile --dryrun oreillyit-minimax --mode quick
|
||||||
[dryrun] Profile: oreillyit (~/.claude-oreillyit)
|
[dryrun] Profile: oreillyit-minimax (~/.claude-oreillyit-minimax)
|
||||||
[dryrun] Mode: quick
|
[dryrun] Mode: quick
|
||||||
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/quick.md
|
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/quick.md
|
||||||
[dryrun] Driver: sonnet
|
[dryrun] Driver: sonnet
|
||||||
[dryrun] Provider: minimax-sonnet (MiniMax (M2.7))
|
|
||||||
[dryrun] Model ID: MiniMax-M2.7
|
[dryrun] Model ID: MiniMax-M2.7
|
||||||
[dryrun] Base URL: https://api.minimax.io/anthropic
|
[dryrun] Provider env: ~/.claude-oreillyit-minimax/provider.env
|
||||||
[dryrun] API key env: ANTHROPIC_AUTH_TOKEN (from ~/.claude-secrets/minimax-auth-token)
|
|
||||||
[dryrun] Project: <not set, will be picked by CLAUDE.md>
|
[dryrun] Project: <not set, will be picked by CLAUDE.md>
|
||||||
[dryrun] Time horizon: <not asked in this mode>
|
|
||||||
[dryrun] Async OK: no
|
[dryrun] Async OK: no
|
||||||
[dryrun] Autoloop: none
|
[dryrun] Autoloop: none
|
||||||
[dryrun] Would execute: claude --model MiniMax-M2.7 --append-system-prompt "<context+mode>"
|
[dryrun] Would execute: claude --model MiniMax-M2.7 --append-system-prompt "<context+mode>"
|
||||||
@@ -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 <path>." Exit 1. |
|
| `~/dev/claude/small-scripts/data/claude-profile/modes/` does not exist | Error: "Mode files directory not found at <path>." Exit 1. |
|
||||||
| Mode file referenced by `--mode` does not exist | Error: "Mode file not found: <path>." Exit 1. |
|
| Mode file referenced by `--mode` does not exist | Error: "Mode file not found: <path>." Exit 1. |
|
||||||
| Mode file has malformed frontmatter (missing required field) | Error naming the missing field and the file path. 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 <path>." Exit 1. |
|
| `provider.env` exists but has no `MODEL_ID` | Driver default is used; other entries are still exported. |
|
||||||
| Provider file referenced by `--provider` does not exist | Error: "Provider not found: <name> (looked in <providers_dir>)." Exit 1. |
|
| `provider.env` specifies `ANTHROPIC_API_KEY_FILE` but the file does not exist | Error: "API key file not found: <path>." Exit 1. |
|
||||||
| Provider file exists but is missing `model_id` | Error: "Provider file <path> 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: <path>." Exit 1. |
|
|
||||||
| `--provider` not given | Default provider derived from mode's `driver` field; interactive picker shown with that as the default. |
|
|
||||||
| `presets.yaml` does not exist and `--preset` was given | Error: "Preset '<name>' not found in any presets.yaml." Exit 1. |
|
| `presets.yaml` does not exist and `--preset` was given | Error: "Preset '<name>' not found in any presets.yaml." Exit 1. |
|
||||||
| `presets.yaml` exists but the named preset is not in it | Error: "Preset '<name>' not found in any presets.yaml." Exit 1. |
|
| `presets.yaml` exists but the named preset is not in it | Error: "Preset '<name>' not found in any presets.yaml." Exit 1. |
|
||||||
| Preset's `profile` field references a non-existent profile | Error. 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 `--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 <X>, preset specifies <Y>." Exit 1. |
|
| Positional `PROFILE` and `--preset` profile field disagree | Error: "Profile mismatch: positional <X>, preset specifies <Y>." Exit 1. |
|
||||||
| `last-mode` file is missing or contains an unknown mode | Fall back to `quick` as the default. |
|
| `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. |
|
| 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-profile
|
||||||
Claude Code profiles:
|
Claude Code profiles:
|
||||||
1) default (~/.claude)
|
1) default (~/.claude)
|
||||||
2) oreillyit (~/.claude-oreillyit)
|
2) oreillyit-anthropic (~/.claude-oreillyit-anthropic)
|
||||||
|
3) oreillyit-minimax (~/.claude-oreillyit-minimax)
|
||||||
Choose profile [1]: 2
|
Choose profile [1]: 2
|
||||||
|
|
||||||
Engagement modes:
|
Engagement modes:
|
||||||
@@ -311,34 +263,24 @@ Engagement modes:
|
|||||||
5) orch — Decompose work and dispatch container agents
|
5) orch — Decompose work and dispatch container agents
|
||||||
Choose mode [quick]: 3
|
Choose mode [quick]: 3
|
||||||
|
|
||||||
Time horizon? [hours]:
|
Launching: oreillyit-anthropic / deep
|
||||||
Async OK? [yes]:
|
|
||||||
|
|
||||||
Launching: oreillyit / deep
|
|
||||||
[Claude Code starts; the existing CLAUDE.md project picker runs next]
|
[Claude Code starts; the existing CLAUDE.md project picker runs next]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Direct — preset**
|
**Direct — preset**
|
||||||
```
|
```
|
||||||
$ claude-profile --preset deep-cluster
|
$ 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]
|
[Claude Code starts]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Direct — profile + mode flag**
|
**Direct — profile + mode flag**
|
||||||
```
|
```
|
||||||
$ claude-profile oreillyit --mode quick --project small-scripts
|
$ claude-profile oreillyit-anthropic --mode quick --project small-scripts
|
||||||
Launching: oreillyit / quick / anthropic-sonnet / small-scripts
|
Launching: oreillyit-anthropic / quick / small-scripts
|
||||||
[Claude Code starts]
|
[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**
|
**Dryrun — verify a preset**
|
||||||
```
|
```
|
||||||
$ claude-profile --dryrun --preset deep-cluster
|
$ claude-profile --dryrun --preset deep-cluster
|
||||||
@@ -354,8 +296,8 @@ $ claude-profile --mode quick --project small-scripts -- --resume
|
|||||||
## Out of scope (for v1)
|
## Out of scope (for v1)
|
||||||
|
|
||||||
- Multi-session coordination (parallel claude-profile launches against the same profile)
|
- 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)
|
- Mid-session mode 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`)
|
- Editing mode files in place (they are read-only data; customisation happens via `presets.yaml`)
|
||||||
- Network/remote profiles (everything is local)
|
- 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)
|
- 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)
|
- 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)
|
||||||
|
|||||||
@@ -73,34 +73,29 @@ assert_exit_code "--help exits 0" 0 "$rc"
|
|||||||
assert_contains "--help shows usage" "claude-profile" "$out"
|
assert_contains "--help shows usage" "claude-profile" "$out"
|
||||||
assert_contains "--help shows --mode" "--mode" "$out"
|
assert_contains "--help shows --mode" "--mode" "$out"
|
||||||
assert_contains "--help shows --preset" "--preset" "$out"
|
assert_contains "--help shows --preset" "--preset" "$out"
|
||||||
assert_contains "--help shows --provider" "--provider" "$out"
|
|
||||||
|
|
||||||
# === Test 2: chat dryrun ===
|
# === Test 2: chat dryrun ===
|
||||||
echo
|
echo
|
||||||
echo "Test 2: chat mode dryrun"
|
echo "Test 2: chat mode dryrun"
|
||||||
out=$(run --dryrun oreillyit --mode chat --provider anthropic-haiku)
|
out=$(run --dryrun oreillyit-anthropic --mode chat)
|
||||||
rc=$?
|
rc=$?
|
||||||
assert_exit_code "chat dryrun exits 0" 0 "$rc"
|
assert_exit_code "chat dryrun exits 0" 0 "$rc"
|
||||||
assert_contains "chat shows haiku driver" "Driver: haiku" "$out"
|
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 model ID" "Model ID: claude-haiku-4-5-20251001" "$out"
|
||||||
assert_contains "chat shows mode tag" "Mode: chat" "$out"
|
assert_contains "chat shows mode tag" "Mode: chat" "$out"
|
||||||
assert_contains "chat says no changes" "No changes made" "$out"
|
assert_contains "chat says no changes" "No changes made" "$out"
|
||||||
assert_contains "chat does not ask time horizon" "<not asked in this mode>" "$out"
|
|
||||||
assert_contains "chat shows model in exec command" "claude --model claude-haiku-4-5-20251001" "$out"
|
assert_contains "chat shows model in exec command" "claude --model claude-haiku-4-5-20251001" "$out"
|
||||||
|
|
||||||
# === Test 3: deep dryrun with project flag ===
|
# === Test 3: deep dryrun with project flag ===
|
||||||
echo
|
echo
|
||||||
echo "Test 3: deep mode dryrun"
|
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=$?
|
rc=$?
|
||||||
assert_exit_code "deep dryrun exits 0" 0 "$rc"
|
assert_exit_code "deep dryrun exits 0" 0 "$rc"
|
||||||
assert_contains "deep shows sonnet driver" "Driver: sonnet" "$out"
|
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 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 shows --model in command" "claude --model claude-sonnet-4-6" "$out"
|
||||||
assert_contains "deep records the project" "Project: cluster-bootstrap" "$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 async OK" "Async OK: yes" "$out"
|
||||||
assert_contains "deep says plan_mode_auto yes" "Plan mode auto: 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"
|
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 ===
|
# === Test 4: orch dryrun preserves autoloop ===
|
||||||
echo
|
echo
|
||||||
echo "Test 4: orch mode dryrun (autoloop preserved)"
|
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=$?
|
rc=$?
|
||||||
assert_exit_code "orch dryrun exits 0" 0 "$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 preserves the slash command in autoloop" "/loop 2m /orchestrate" "$out"
|
||||||
assert_contains "orch driver is sonnet" "Driver: sonnet" "$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 ===
|
# === Test 5: hybrid dryrun ===
|
||||||
echo
|
echo
|
||||||
echo "Test 5: hybrid mode dryrun"
|
echo "Test 5: hybrid mode dryrun"
|
||||||
out=$(run --dryrun oreillyit --mode hybrid --provider anthropic-haiku)
|
out=$(run --dryrun oreillyit-anthropic --mode hybrid)
|
||||||
rc=$?
|
rc=$?
|
||||||
assert_exit_code "hybrid dryrun exits 0" 0 "$rc"
|
assert_exit_code "hybrid dryrun exits 0" 0 "$rc"
|
||||||
assert_contains "hybrid uses haiku driver" "Driver: haiku" "$out"
|
assert_contains "hybrid uses haiku driver" "Driver: haiku" "$out"
|
||||||
assert_contains "hybrid allows async" "Async OK: yes" "$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
|
||||||
echo "Test 6: quick mode dryrun"
|
echo "Test 6: quick mode dryrun"
|
||||||
out=$(run --dryrun oreillyit --mode quick --provider anthropic-sonnet)
|
out=$(run --dryrun oreillyit-anthropic --mode quick)
|
||||||
rc=$?
|
rc=$?
|
||||||
assert_exit_code "quick dryrun exits 0" 0 "$rc"
|
assert_exit_code "quick dryrun exits 0" 0 "$rc"
|
||||||
assert_contains "quick uses sonnet driver" "Driver: sonnet" "$out"
|
assert_contains "quick uses sonnet driver" "Driver: sonnet" "$out"
|
||||||
assert_contains "quick disables async" "Async OK: no" "$out"
|
assert_contains "quick disables async" "Async OK: no" "$out"
|
||||||
assert_contains "quick does not ask time horizon" "<not asked in this mode>" "$out"
|
|
||||||
assert_contains "quick shows provider" "Provider: anthropic-sonnet" "$out"
|
|
||||||
|
|
||||||
# === Test 7: error — unknown mode ===
|
# === Test 7: error — unknown mode ===
|
||||||
echo
|
echo
|
||||||
echo "Test 7: error path — unknown mode"
|
echo "Test 7: error path — unknown mode"
|
||||||
out=$(run --dryrun oreillyit --mode bogus)
|
out=$(run --dryrun oreillyit-anthropic --mode bogus)
|
||||||
rc=$?
|
rc=$?
|
||||||
assert_exit_code "unknown mode exits 1" 1 "$rc"
|
assert_exit_code "unknown mode exits 1" 1 "$rc"
|
||||||
assert_contains "unknown mode error message" "Mode file not found" "$out"
|
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 -- ===
|
# === Test 10: pass-through args via -- ===
|
||||||
echo
|
echo
|
||||||
echo "Test 10: pass-through args"
|
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=$?
|
rc=$?
|
||||||
assert_exit_code "pass-through exits 0" 0 "$rc"
|
assert_exit_code "pass-through exits 0" 0 "$rc"
|
||||||
assert_contains "pass-through preserves --resume" "--resume some-session" "$out"
|
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
|
||||||
echo "Test 11: minimax provider dryrun"
|
echo "Test 11: minimax profile uses provider.env"
|
||||||
out=$(run --dryrun oreillyit --mode quick --provider minimax-sonnet)
|
out=$(run --dryrun oreillyit-minimax --mode quick)
|
||||||
rc=$?
|
rc=$?
|
||||||
assert_exit_code "minimax dryrun exits 0" 0 "$rc"
|
assert_exit_code "minimax dryrun exits 0" 0 "$rc"
|
||||||
assert_contains "minimax shows provider" "Provider: minimax-sonnet" "$out"
|
assert_contains "minimax shows MiniMax model ID" "Model ID: MiniMax-M2.7" "$out"
|
||||||
assert_contains "minimax shows model ID" "Model ID: MiniMax-M2.7" "$out"
|
assert_contains "minimax shows provider env file" "Provider env:" "$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 model in exec command" "claude --model MiniMax-M2.7" "$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
|
||||||
echo "Test 12: error path — unknown provider"
|
echo "Test 12: error path — unknown flag"
|
||||||
out=$(run --dryrun oreillyit --mode quick --provider bogus-provider)
|
out=$(run --dryrun oreillyit-anthropic --mode quick --provider anthropic-sonnet)
|
||||||
rc=$?
|
rc=$?
|
||||||
assert_exit_code "unknown provider exits 1" 1 "$rc"
|
assert_exit_code "unknown flag exits 1" 1 "$rc"
|
||||||
assert_contains "unknown provider error message" "Provider not found" "$out"
|
assert_contains "unknown flag error message" "Unknown flag" "$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"
|
|
||||||
|
|
||||||
# === Summary ===
|
# === Summary ===
|
||||||
echo
|
echo
|
||||||
|
|||||||
Reference in New Issue
Block a user