Files
small-scripts/memory/decisions.md

90 lines
7.2 KiB
Markdown

# 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. <!-- 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.
## Driver→model_id mapping and context-window facts
Reference facts baked into `claude-profile`: `haiku → claude-haiku-4-5-20251001`, `sonnet → claude-sonnet-4-6`, `opus → claude-opus-4-6`, `opus-1m → claude-opus-4-6[1m]`. Sonnet 4.6 has a fixed 200k context window with no extended-context option; `opus-1m` denotes Opus running with the 1M-token window. <!-- STALE? Model IDs will age out as new model versions ship; verify against live model list before relying on these. -->
## claude-profile state files live under `$CLAUDE_CONFIG_DIR`
`active-mode.env` (current mode/driver), `last-mode` (picker default, falls back to `quick`), and `presets.yaml` (per-profile) all live under `$CLAUDE_CONFIG_DIR`. The path is stable across `/clear`, so `active-mode.env` survives and is read by both the CLAUDE.md session-start (project auto-select) and `statusline.sh`. Known v1 limitation: parallel sessions against the same profile race on `active-mode.env` (last writer wins).