Files
small-scripts/memory/decisions.md
Paul O'Reilly 9e13b7f096 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.
2026-06-12 21:26:07 +12:00

6.2 KiB

Decisions

OpenSpec format for all script specifications

Every script starts with a spec in specs/<name>.spec.md using OpenSpec format: purpose, usage, behaviour, dryrun behaviour, edge cases, and examples. This project is a testbed for agent-driven development where specs drive implementation and testing.

Mandatory --dryrun / -n on every script

All scripts must support --dryrun which previews actions without side effects. This enables the testing strategy: test scripts exercise the main script's dryrun mode to verify behaviour matches the spec without making real changes.

set -uo pipefail without -e for reporting tools

Read-only/reporting scripts use set -uo pipefail instead of set -euo pipefail. The -e flag causes silent failures in complex pipeline/subshell chains. Explicit error handling is more predictable for tools that aggregate data from many sources.

"No remotes" and "detached HEAD" are not dirty states

In git-status-report, repos with no remotes or detached HEAD are considered clean unless they have local changes. "(no remotes)" is only shown as annotation when the repo already has uncommitted changes.

Use python3 for JSON parsing in bash scripts

Scripts that need to read JSON (e.g., .reflection-state.json) use inline python3 rather than jq or fragile bash string parsing. python3 is reliably available on target systems and handles edge cases (nested keys, unicode, null values) that bash alternatives struggle with.

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.

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., sonnetclaude-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. oreillyitoreillyit-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.