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:
Paul O'Reilly
2026-06-12 21:26:07 +12:00
parent c0a2110ee6
commit 9e13b7f096
16 changed files with 271 additions and 398 deletions

View File

@@ -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/<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.

View File

@@ -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.

View 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`.

View File

@@ -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.