Add claude-profile engagement mode picker with statusline and session-start integration
- claude-profile: phase 1-3 picker (profile, mode, launch) with preset support, dryrun, WezTerm theming, and --append-system-prompt mode body injection - 5 mode files (chat/quick/deep/hybrid/orch) with YAML frontmatter + prose body; new escalates_to field drives statusline →Opus arrow for deep and hybrid - statusline.sh reads CLAUDE_CONFIG_DIR/active-mode.env to show [Sonnet→Opus] deep · topic format when launched via claude-profile - Root CLAUDE.md session-start: auto-selects project from cwd or CLAUDE_PROJECT in active-mode.env, skipping the interactive picker when context is clear - Spec, tests (37 assertions, 9 test files, all passing), context docs, and preset example included Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,10 +25,16 @@ small-scripts/
|
|||||||
│ └── <script-name>
|
│ └── <script-name>
|
||||||
├── tests/ # Test scripts (one per main script)
|
├── tests/ # Test scripts (one per main script)
|
||||||
│ └── test-<script-name>.sh
|
│ └── test-<script-name>.sh
|
||||||
|
├── data/ # Read-only data files consumed by scripts
|
||||||
|
│ └── <script-name>/ # One subdirectory per script that needs data
|
||||||
└── memory/ # Tiered memory topic files
|
└── memory/ # Tiered memory topic files
|
||||||
└── log/ # Session logs
|
└── log/ # Session logs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### data/ directory
|
||||||
|
|
||||||
|
Some scripts need static data that is too large or too structured to embed inline (e.g. `claude-profile` reads engagement mode definitions from `data/claude-profile/modes/*.md`). Put such data under `data/<script-name>/`. Treat it as read-only at runtime — user customisation belongs in the user's config directory, not in `data/`. Example files (e.g. `presets.yaml.example`) live alongside the data and are copied by the user into their config dir.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### Adding a new script
|
### Adding a new script
|
||||||
|
|||||||
5
CONTEXT.md
Normal file
5
CONTEXT.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Active Work
|
||||||
|
|
||||||
|
<!-- Thin index — one-line entries linking to context/ detail files -->
|
||||||
|
|
||||||
|
- [claude-profile engagement modes](context/claude-profile-modes.md) — Built and dryrun-tested; needs symlink install + first real `claude` launch + 5 sequenced follow-ups in FUTURE.md
|
||||||
69
FUTURE.md
69
FUTURE.md
@@ -13,3 +13,72 @@
|
|||||||
- **Idea:** Gitea CI action that runs `tests/run-all.sh` on push
|
- **Idea:** Gitea CI action that runs `tests/run-all.sh` on push
|
||||||
- **Open questions:** What runner is available on Gitea? Docker-based?
|
- **Open questions:** What runner is available on Gitea? Docker-based?
|
||||||
- **Depends on:** Gitea CI being available (cluster-bootstrap M10)
|
- **Depends on:** Gitea CI being available (cluster-bootstrap M10)
|
||||||
|
|
||||||
|
## claude-profile engagement modes — follow-up integration
|
||||||
|
|
||||||
|
The initial engagement mode picker (chat / quick / deep / hybrid / orch) ships with `claude-profile` and writes `active-mode.env` into the active profile directory. Several integration pieces are deferred to future work — none block v1 but each unlocks the design fully:
|
||||||
|
|
||||||
|
### CLAUDE.md project picker integration
|
||||||
|
|
||||||
|
- **Problem:** `claude-profile --project <name>` writes `CLAUDE_PROJECT` into `active-mode.env`, but the existing CLAUDE.md session-start project picker still runs and ignores it. The user has to pick the same project twice when launching with a preset.
|
||||||
|
- **Idea:** Update `~/dev/claude/CLAUDE.md` (the top-level session-start instructions) to read `$CLAUDE_CONFIG_DIR/active-mode.env` first; if `CLAUDE_PROJECT` is set, skip the interactive project menu and `cd` straight into that project. Same hook can announce the active mode to satisfy the "first message after `/clear`" templates in each mode file.
|
||||||
|
- **Open questions:** Does the CLAUDE.md flow have a clean place to read shell env files, or does `claude-profile` need to inject the project name into the appended system prompt directly? The latter is simpler but couples the picker to claude-profile.
|
||||||
|
- **Depends on:** Nothing blocking — can be done any time.
|
||||||
|
|
||||||
|
### Status-line script: render the mode tag
|
||||||
|
|
||||||
|
- **Problem:** The mode tag (`chat / quick / deep / hybrid / orch`) is recorded in `active-mode.env` but the status line at `~/.claude/status/statusline.sh` does not currently read it. Humans cannot see the active mode at a glance, which is the whole point of the persistent indicator.
|
||||||
|
- **Idea:** Update the status-line script to read `$CLAUDE_CONFIG_DIR/active-mode.env`, extract `CLAUDE_MODE_TAG` and `CLAUDE_DRIVER`, and prepend them to the existing topic in the format `[Driver] mode-tag · topic | N% ctx`. Modes with named subagent escalation (`deep`, `hybrid`) should render an arrow: `[Sonnet→Opus] deep · topic`. Orchestrator should append a queue-depth segment: `[Sonnet] orch · project · queue:N`.
|
||||||
|
- **Open questions:** Where does the status-line script live in the repo (it's outside small-scripts)? Should the queue-depth read be cached to avoid hammering `.agent-tasks.json` on every status-line render?
|
||||||
|
- **Depends on:** The status-line script is in claude-foundations, not small-scripts — this work happens in that repo.
|
||||||
|
|
||||||
|
### `bg-model-call` wrapper
|
||||||
|
|
||||||
|
- **Problem:** The `deep` and `hybrid` modes both reference `bg-model-call` as the standardised way to fire a third-party model (MiniMax, etc.) in the background and have the result land in a sentinel file. The wrapper does not exist yet — modes currently just describe the pattern.
|
||||||
|
- **Idea:** A new small-script `bg-model-call` that takes a model name, a prompt file, and an output file; runs the appropriate container in the background; writes the result + a sentinel `.done` file when finished. Standardises timeouts, exit codes, and the sentinel format so `ScheduleWakeup` prompts can be self-contained.
|
||||||
|
- **Open questions:** Which third-party models do we actually want to wire up in v1 (MiniMax M2 only, or also others)? Where do API credentials live (secrets folder, with what naming)? Does the wrapper run via Docker on the local host, or via the agent-runtimes control plane?
|
||||||
|
- **Depends on:** Spec for the wrapper. This is its own small-scripts entry.
|
||||||
|
|
||||||
|
### `switch mode` skill (mid-session re-pick)
|
||||||
|
|
||||||
|
- **Problem:** The mode is fixed at launch by `claude-profile`. Switching mid-session currently requires `/clear` + relaunch. For the common case "started in deep, this turned out to be trivial, drop to quick" there is no in-session path.
|
||||||
|
- **Idea:** A `switch-mode` skill (in custom-claude-skills) that re-runs the picker logic by writing a new `active-mode.env`, then asking the harness to apply it. The driver model can't actually swap mid-session (`--model` is launch-only), so the skill works by either (a) instructing the user to relaunch, or (b) updating the active-mode.env so the next `/clear` picks up the new mode without re-prompting. Option (b) is more useful in practice.
|
||||||
|
- **Open questions:** Should the skill be invoked by typing literal "switch mode" (matched by a UserPromptSubmit hook) or by a slash command `/switch-mode`? The mode files all assume the literal phrase.
|
||||||
|
- **Depends on:** No technical blockers; depends on someone actually wanting to switch modes, which may turn out to be rare in practice.
|
||||||
|
|
||||||
|
### MiniMax-driven session with Claude container subagents (inverted architecture)
|
||||||
|
|
||||||
|
- **Problem:** The current `claude-profile` design assumes Claude Code is the main loop and Claude models are the driver. The cheapest available driver is Haiku (`hybrid` mode), which is still Anthropic-priced and still consumes Claude OAuth quota for every conversational turn. For long-running sessions where most turns are mundane routing and only occasional turns need real reasoning, the driver is the dominant cost. A non-Claude driver (MiniMax M2 specifically — comparable agentic ability at a fraction of the per-token cost, on a different vendor and quota pool) would unlock substantially cheaper long sessions while still letting us reach for Opus/Sonnet on demand via container subagents.
|
||||||
|
- **Idea:** A new launcher (working name: `agent-profile` or `minimax-profile`, parallel to `claude-profile`) that runs MiniMax as the conversational driver and treats Claude Code instances running inside containers as named subagents. The container subagents are dispatched via the existing agent-runtimes control plane — same infrastructure the `orch` mode already uses, just invoked from a different driver. Result-passing is file-based (already established as the right pattern in this session): the MiniMax driver writes a prompt file, fires a container, polls for a sentinel, reads the result file. No multi-megabyte tool-call payloads streaming through MiniMax's context window.
|
||||||
|
|
||||||
|
#### Architectural shape
|
||||||
|
1. **Driver layer** — a MiniMax agentic harness (either MiniMax's own CLI if one exists at the time, or a thin custom harness built on the MiniMax SDK with a tool-calling interface). This is **not** Claude Code; the existing CLAUDE.md / skills / hooks / status-line mechanisms do not apply directly. Some equivalents may need to be reimplemented or ported.
|
||||||
|
2. **Subagent dispatch layer** — a small set of tools the MiniMax driver can call: `consult_opus(prompt_file, out_file)`, `consult_sonnet(prompt_file, out_file)`, `consult_minimax_self(...)` (for parallel MiniMax thinking on the same machine). Each tool is a thin shim that submits a task to the agent-runtimes control plane and waits on a sentinel file, identical in shape to the `bg-model-call` wrapper described above.
|
||||||
|
3. **Result passing** — file-based, as in the rest of this design. The MiniMax driver never reads a multi-thousand-token Claude response into its own context unless it explicitly chooses to. Default flow: write prompt → fire container → wait → read summary line → optionally read full file.
|
||||||
|
4. **Engagement modes (parallel set)** — `chat`, `quick`, `deep`, `hybrid`, `orch` may or may not all map cleanly. `hybrid` (cheap driver, premium subagents) is the obvious fit and arguably the *only* mode that makes sense here — if you're not in hybrid you're either using a cheaper driver for trivial work (re-launch in `chat`) or wanting a Claude driver (use `claude-profile`). Worth designing as one mode rather than five.
|
||||||
|
|
||||||
|
#### Credential management — the load-bearing piece
|
||||||
|
|
||||||
|
This is where the user's explicit ask sits, and where it's easy to go wrong:
|
||||||
|
|
||||||
|
- **MiniMax credentials** for the driver: API key, lives at `~/dev/claude/secrets/minimax/api_key` (proposed; user to confirm naming). Read by the MiniMax driver process at launch only. Never written into a container, never logged, never put on a command line (use stdin or env-var injection at exec time).
|
||||||
|
- **Claude credentials** for container subagents: the existing `~/dev/claude/secrets/claude/long_lived_oauth_token` (per cross-project memory) — already used by agent-runtimes containers via `CLAUDE_CODE_OAUTH_TOKEN`. No change needed; the containers already know how to read it.
|
||||||
|
- **Strict separation:** the MiniMax driver process must NOT have access to the Claude OAuth token. The containers must NOT have access to the MiniMax key. Each side reads only its own credential file at point of use. This rules out a "load all secrets into env at launch" pattern.
|
||||||
|
- **No credential crosses to subprocess args.** MiniMax key into the driver via env or stdin only. Claude OAuth into the container via the existing agent-runtimes mechanism (volume mount or env var, set by the control plane, not by the driver process).
|
||||||
|
- **Audit boundary:** if the MiniMax driver ever needs to be told "run a Claude container," it asks the control plane to do it. The driver does not invoke `docker run` directly. This keeps the credential boundary at the control plane, where it already exists, rather than moving it into the driver.
|
||||||
|
- **Cross-project memory feedback already covers this:** "Stream secrets from files" — read tokens from source files at point of use; never reconstruct from context. Apply that same rule to both credential pools.
|
||||||
|
|
||||||
|
#### Open questions
|
||||||
|
1. **Does MiniMax M2 ship a usable agentic CLI/harness in 2026?** If yes, how mature — does it have tool-calling, file editing, persistent context, hooks? If no, the work is "build a minimal agentic harness" first, which is a significant project on its own.
|
||||||
|
2. **Tool-call interop:** MiniMax's function-calling format vs. Claude Code's tool-result format. If we want a Claude container to be able to surface useful structured data back, the driver needs to translate. File-passing avoids most of this — JSON/markdown files are vendor-neutral.
|
||||||
|
3. **Where does this launcher live?** Same `small-scripts` repo, or its own project? Lean toward small-scripts initially; promote to its own project once it has its own modes/specs/tests at a similar scale to claude-profile.
|
||||||
|
4. **Status line / WezTerm theming:** Currently driven by `claude-profile`'s WezTerm escape sequences. The MiniMax driver runs in the same terminal — does it reuse that theming, or pick its own colour to make the inverted-architecture session visually distinct?
|
||||||
|
5. **Does this replace `hybrid` mode entirely, or coexist?** `hybrid` (Claude Haiku driver) is a strict superset functionally — same tools, same Claude harness — but more expensive than MiniMax. Some users may prefer staying inside Claude's harness even at higher cost; others may want the MiniMax surface for cost reasons. Likely coexist.
|
||||||
|
6. **Failure modes when the MiniMax API is degraded:** Claude Code has decent retry/backoff. A custom MiniMax driver needs the same — and a clean way to fall back to a local model or to `claude-profile --mode hybrid` if MiniMax is unreachable for a sustained period.
|
||||||
|
7. **Subagent quota economics:** if every container subagent burns Claude OAuth quota, and the driver burns MiniMax quota, are we actually saving money? Sketch out a typical-session token budget under both architectures (claude-profile hybrid vs. minimax-profile) before committing — this might be the kind of "feels cheaper but isn't" optimisation that wastes a weekend.
|
||||||
|
|
||||||
|
#### Depends on
|
||||||
|
- **MiniMax M2 maturity** — specifically whether an agentic harness exists or needs building. Worth re-checking every few months.
|
||||||
|
- **agent-runtimes control plane** — already exists and is actively used; no new infrastructure required there, but the control plane needs a small documented contract for "submit a Claude consult task and return the result file path."
|
||||||
|
- **The `bg-model-call` wrapper from this same FUTURE.md section** — if that ships first, the subagent dispatch layer here is mostly already built; this entry becomes "new driver harness on top of an existing wrapper."
|
||||||
|
- **A token-budget sketch** — before any code is written, estimate a 4-hour session under both architectures with realistic ratios of routine vs. reasoning turns. If the savings are <30%, this is a research project, not a productivity win.
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -16,10 +16,47 @@ This project uses **spec-driven development** (OpenSpec) as a testbed for agent-
|
|||||||
|--------|---------|--------|
|
|--------|---------|--------|
|
||||||
| `git-status-report` | Recursive git repo status with diff stats | Done |
|
| `git-status-report` | Recursive git repo status with diff stats | Done |
|
||||||
| `md-to-docx` | Markdown to DOCX conversion | Done |
|
| `md-to-docx` | Markdown to DOCX conversion | Done |
|
||||||
| `claude-profile` | Claude Code profile management | Done |
|
| `claude-profile` | Claude Code profile + engagement-mode launcher | Done |
|
||||||
| `unreflected-logs` | Scan projects for unreflected session logs | Done |
|
| `unreflected-logs` | Scan projects for unreflected session logs | Done |
|
||||||
| `validate-skill` | Validate SKILL.md files against known Claude Code restrictions | Done |
|
| `validate-skill` | Validate SKILL.md files against known Claude Code restrictions | Done |
|
||||||
|
|
||||||
|
### Engagement modes (claude-profile)
|
||||||
|
|
||||||
|
`claude-profile` launches Claude Code with both a configuration profile (selecting which `~/.claude-*` directory to use) and an **engagement mode** that bundles a driver model, subagent policy, async tolerance, and workflow stance:
|
||||||
|
|
||||||
|
| Mode | Driver | For |
|
||||||
|
|---|---|---|
|
||||||
|
| `chat` | Haiku | No project, just conversation |
|
||||||
|
| `quick` | Sonnet | Single small focused job, fully synchronous |
|
||||||
|
| `deep` | Sonnet (Opus via named workflows) | Hard design or debugging, plan + spec enforced |
|
||||||
|
| `hybrid` | Haiku (Opus/Sonnet on demand) | Long sessions, escalates only when needed |
|
||||||
|
| `orch` | Sonnet | Decompose work and dispatch container agents |
|
||||||
|
|
||||||
|
Mode definitions live in `data/claude-profile/modes/<name>.md` — YAML frontmatter (machine-parsed) plus a markdown body that becomes part of the system prompt.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude-profile # interactive picker
|
||||||
|
claude-profile oreillyit --mode deep # explicit profile + mode
|
||||||
|
claude-profile --preset deep-cluster # named preset from presets.yaml
|
||||||
|
claude-profile --dryrun [...] # show resolved values, do not launch
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `data/claude-profile/presets.yaml.example` to `$CLAUDE_CONFIG_DIR/presets.yaml` to set up named presets.
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
small-scripts/
|
||||||
|
├── scripts/ # Executable scripts (symlinked into ~/sbin)
|
||||||
|
├── specs/ # OpenSpec files, one per script
|
||||||
|
├── tests/ # Test scripts that exercise --dryrun
|
||||||
|
├── data/ # Read-only data files used by scripts
|
||||||
|
│ └── claude-profile/
|
||||||
|
│ ├── modes/ # Engagement mode definitions
|
||||||
|
│ └── presets.yaml.example
|
||||||
|
└── memory/ # Tiered memory topic files
|
||||||
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
91
context/claude-profile-modes.md
Normal file
91
context/claude-profile-modes.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# claude-profile engagement modes
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Built and tested via dryrun. NOT yet exercised in a real `claude` launch.**
|
||||||
|
|
||||||
|
The feature ships as part of `scripts/claude-profile` (rewritten this session from 104 to ~280 lines). Spec is at `specs/claude-profile.spec.md`. All 37 dryrun assertions pass via `tests/test-claude-profile.sh`. Full test suite (`tests/run-all.sh`) is green: 8 of 8 test files passing.
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
| Component | Path |
|
||||||
|
|---|---|
|
||||||
|
| Spec | `specs/claude-profile.spec.md` |
|
||||||
|
| Picker bash | `scripts/claude-profile` |
|
||||||
|
| 5 mode files | `data/claude-profile/modes/{chat,quick,deep,hybrid,orch}.md` |
|
||||||
|
| Preset example | `data/claude-profile/presets.yaml.example` |
|
||||||
|
| Test script | `tests/test-claude-profile.sh` |
|
||||||
|
|
||||||
|
The five modes:
|
||||||
|
|
||||||
|
| Mode | Driver model | Async | For |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `chat` | Haiku (`claude-haiku-4-5-20251001`) | no | No project, conversation only |
|
||||||
|
| `quick` | Sonnet (`claude-sonnet-4-6`) | no | Single small focused job |
|
||||||
|
| `deep` | Sonnet | yes | Hard design/debugging, plan + spec enforced, Opus via named workflows |
|
||||||
|
| `hybrid` | Haiku | yes | Cheap driver, escalates to Opus/Sonnet subagents on demand |
|
||||||
|
| `orch` | Sonnet | yes | Decompose work, dispatch container agents, auto-fires `/loop 2m /orchestrate` |
|
||||||
|
|
||||||
|
## What is NOT done
|
||||||
|
|
||||||
|
1. **Not symlinked into `~/sbin`.** The new script lives at `~/dev/claude/small-scripts/scripts/claude-profile` but is not yet on PATH. To install:
|
||||||
|
```bash
|
||||||
|
ln -sf "$HOME/dev/claude/small-scripts/scripts/claude-profile" "$HOME/sbin/claude-profile"
|
||||||
|
```
|
||||||
|
2. **Not exercised against real `claude`.** Dryrun confirms the resolved values; an actual launch (e.g. `claude-profile oreillyit --mode quick` or `--mode chat`) is the first thing the next session should try.
|
||||||
|
3. **Presets file not installed.** Example template is at `data/claude-profile/presets.yaml.example`. To use presets, copy to `~/.claude-oreillyit/presets.yaml` and edit.
|
||||||
|
4. **CLAUDE.md does not yet read `active-mode.env`.** When a real launch happens with `--project <name>`, the existing CLAUDE.md project picker will still appear and the user has to confirm the same project. This is the first follow-up listed below.
|
||||||
|
|
||||||
|
## How the runtime contract works
|
||||||
|
|
||||||
|
1. `claude-profile` resolves the profile, mode, and (for some modes) follow-up answers.
|
||||||
|
2. Writes `$CLAUDE_CONFIG_DIR/active-mode.env` with all resolved values (machine-readable, key=value).
|
||||||
|
3. Writes `$CLAUDE_CONFIG_DIR/last-mode` with the mode name (used as picker default next time).
|
||||||
|
4. Reads the mode file's body (everything after the YAML frontmatter), appends it to the context-load output, and passes the combined string via `--append-system-prompt`.
|
||||||
|
5. Resolves the mode's `driver` field through a hardcoded mapping table to a full Claude model ID.
|
||||||
|
6. Executes `claude --model "<full-model-id>" --append-system-prompt "<combined>" "${pass-through-args[@]}"`.
|
||||||
|
|
||||||
|
The mode file's prose body is what tells the driver model how to behave (escalation rules, plan-mode triggers, async policy, status-line format, on-`/clear` behaviour). The frontmatter is parsed by bash for the launcher's own decision-making.
|
||||||
|
|
||||||
|
## Driver mapping (lives in the script as a constant)
|
||||||
|
|
||||||
|
| Frontmatter `driver` | `--model` argument |
|
||||||
|
|---|---|
|
||||||
|
| `haiku` | `claude-haiku-4-5-20251001` |
|
||||||
|
| `sonnet` | `claude-sonnet-4-6` |
|
||||||
|
| `opus` | `claude-opus-4-6` |
|
||||||
|
| `opus-1m` | `claude-opus-4-6[1m]` |
|
||||||
|
|
||||||
|
When new model versions ship, update the `DRIVER_MAP` associative array near the top of `scripts/claude-profile`.
|
||||||
|
|
||||||
|
## Next session — recommended order
|
||||||
|
|
||||||
|
1. **Install the symlink:** `ln -sf "$HOME/dev/claude/small-scripts/scripts/claude-profile" "$HOME/sbin/claude-profile"`
|
||||||
|
2. **Real-launch test:** `claude-profile oreillyit --mode chat` first (lowest blast radius — Haiku driver, no project, no async). Verify the mode body actually appears in the system prompt and the model behaves per the chat mode rules.
|
||||||
|
3. **Then `--mode quick`** with a real project to verify the Sonnet driver and the no-async constraints.
|
||||||
|
4. **Then `--mode deep`** to verify plan-mode auto-engagement and the named-workflow escalation table.
|
||||||
|
5. **If anything feels off,** `tests/test-claude-profile.sh` is the fast feedback loop — run it after any picker-bash changes.
|
||||||
|
6. **Once happy with v1,** start picking up follow-ups from `FUTURE.md` (see "Follow-ups" below).
|
||||||
|
|
||||||
|
## Follow-ups (sequenced)
|
||||||
|
|
||||||
|
All in `FUTURE.md`. Suggested order:
|
||||||
|
|
||||||
|
1. **CLAUDE.md project picker integration** — read `CLAUDE_PROJECT` from `active-mode.env`, skip the duplicate picker. Quick win.
|
||||||
|
2. **Status-line script: render the mode tag** — the user explicitly asked for this; humans need to see the active mode at a glance. Lives in the claude-foundations repo, not small-scripts.
|
||||||
|
3. **`bg-model-call` wrapper** — referenced by the `deep` and `hybrid` modes but not yet built. Own small-scripts entry with its own spec and tests.
|
||||||
|
4. **`switch mode` skill** — mid-session re-pick. Lower priority; depends on whether mode switching turns out to be common in practice.
|
||||||
|
5. **MiniMax-driven inverted architecture** — biggest item, real research project. Token-budget sketch is the precondition before any code; if savings <30% it's not worth pursuing.
|
||||||
|
|
||||||
|
## Files touched this session
|
||||||
|
|
||||||
|
- Created: `specs/claude-profile.spec.md`, `data/claude-profile/modes/{chat,quick,deep,hybrid,orch}.md`, `data/claude-profile/presets.yaml.example`, `tests/test-claude-profile.sh`, `context/claude-profile-modes.md` (this file)
|
||||||
|
- Modified: `scripts/claude-profile` (rewritten), `README.md`, `CLAUDE.md`, `FUTURE.md`
|
||||||
|
- Pruned: 4 reflected session logs from March (`memory/log/2026-03-17.*.md` and `2026-03-23.123551.md`)
|
||||||
|
|
||||||
|
## Key gotchas to remember
|
||||||
|
|
||||||
|
- **`read -rp` under `set -e` exits silently if stdin is not a TTY.** Always guard interactive reads with `[[ -t 0 ]]` and provide a default. Without this, `--dryrun` hangs or silently exits when run from scripts. See `memory/gotchas-bash.md` for the broader pattern.
|
||||||
|
- **`context-load` is on PATH via `~/sbin`, not a sibling of `claude-profile`.** The script uses `command -v context-load` first, with sibling fallback for testing isolation.
|
||||||
|
- **Use full model IDs, not aliases.** The `claude-code-guide` agent only confirmed `sonnet`/`opus` aliases work; `haiku` is unverified. The mapping table uses full IDs throughout for safety and unambiguous version selection.
|
||||||
|
- **`claude --model` is session-scoped.** Switching driver mid-session is impossible — it requires a relaunch. The `switch mode` follow-up works around this by writing a new `active-mode.env` for the next `/clear` to pick up.
|
||||||
67
data/claude-profile/modes/chat.md
Normal file
67
data/claude-profile/modes/chat.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: chat
|
||||||
|
tag: chat
|
||||||
|
driver: haiku
|
||||||
|
async_ok: no
|
||||||
|
autoloop: none
|
||||||
|
plan_mode_auto: no
|
||||||
|
spec_driven: no
|
||||||
|
escalates_to: none
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mode: Chat
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
No project, just conversation. Questions, brainstorming, lookups, advice.
|
||||||
|
NOT for: coding work, file edits, anything touching ~/dev/claude/ projects.
|
||||||
|
|
||||||
|
## Driver constraint
|
||||||
|
You are running as Haiku. Fixed for the session. Do not spawn subagents to "escalate" — if a question genuinely needs Opus, tell the user to relaunch in `deep` or `hybrid` mode rather than burning subagent tokens for chat.
|
||||||
|
|
||||||
|
## Subagent policy
|
||||||
|
- Default subagent model: none
|
||||||
|
- Spawn a subagent when: never in this mode
|
||||||
|
- Do NOT spawn subagents — Chat is a flat conversation
|
||||||
|
|
||||||
|
## Reasoning posture
|
||||||
|
- Default thinking depth: fast
|
||||||
|
- Escalate to "think" only if: the user explicitly asks for a thoughtful answer
|
||||||
|
|
||||||
|
## Async policy
|
||||||
|
- Background bash: disabled
|
||||||
|
- bg-model-call: disabled
|
||||||
|
- ScheduleWakeup: disabled
|
||||||
|
- Container agents: disabled
|
||||||
|
|
||||||
|
Chat is fully synchronous.
|
||||||
|
|
||||||
|
## Workflow stance
|
||||||
|
- Plan mode: never
|
||||||
|
- Spec-driven workflow: ignored
|
||||||
|
- Best-practices topics: do not load any
|
||||||
|
- Memory files: do not read project memory; user-level memory is enough
|
||||||
|
|
||||||
|
## Auto-fire at session start
|
||||||
|
none
|
||||||
|
|
||||||
|
## Status line format
|
||||||
|
`[Haiku] chat | N% ctx`
|
||||||
|
|
||||||
|
## Escalation triggers
|
||||||
|
| Trigger | Action |
|
||||||
|
|---|---|
|
||||||
|
| User starts asking for code edits or project work | Stop. Recommend relaunching in `quick`, `deep`, or `hybrid`. |
|
||||||
|
| User asks a question that genuinely needs deep reasoning | Answer at Haiku's level, then suggest `deep` mode for a deeper take. |
|
||||||
|
| User picks a project from the menu | The project menu should not appear in this mode — if it does, that's a bug. Tell the user. |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
This mode does NOT:
|
||||||
|
- Open or edit files in ~/dev/claude/
|
||||||
|
- Load project CLAUDE.md or memory
|
||||||
|
- Spawn any subagents
|
||||||
|
- Run any background work
|
||||||
|
- Enter plan mode
|
||||||
|
|
||||||
|
## On context wipe (/clear)
|
||||||
|
First message after a context wipe must be:
|
||||||
|
> "Resuming Chat. Say 'switch mode' to change."
|
||||||
83
data/claude-profile/modes/deep.md
Normal file
83
data/claude-profile/modes/deep.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
name: deep
|
||||||
|
tag: deep
|
||||||
|
driver: sonnet
|
||||||
|
async_ok: yes
|
||||||
|
autoloop: none
|
||||||
|
plan_mode_auto: yes
|
||||||
|
spec_driven: yes
|
||||||
|
escalates_to: opus
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mode: Deep Work
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Hard design or debugging problem on a known project. Plan-first, spec-first, willing to spend tokens on quality. Sonnet drives; Opus is consulted only via named workflows.
|
||||||
|
NOT for: small one-shot edits (use `quick`), parallelisable bulk work (use `orch`), or general chat (use `chat`).
|
||||||
|
|
||||||
|
## Driver constraint
|
||||||
|
You are running as Sonnet. Fixed for the session. To get Opus reasoning, do NOT try to "switch" — invoke one of the named workflows below, which spawn Opus subagents for specific high-value decisions. Opus does not enter this mode any other way.
|
||||||
|
|
||||||
|
## Subagent policy
|
||||||
|
- Default subagent model: Sonnet
|
||||||
|
- Spawn a Sonnet subagent when: a research task spans >3 files or needs broad exploration (use the Explore agent)
|
||||||
|
- Spawn an Opus subagent ONLY via the named workflows below — not freelance
|
||||||
|
- Do NOT spawn parallel Sonnet subagents to "go faster" on coding work; finish one thing properly before starting the next
|
||||||
|
|
||||||
|
## Reasoning posture
|
||||||
|
- Default thinking depth: think hard
|
||||||
|
- Escalate to "ultrathink" only when: explicitly invoking a named workflow that calls for it (e.g. plan review, gnarly-bug consult)
|
||||||
|
- Do NOT default to ultrathink on every turn — it bloats context for little gain
|
||||||
|
|
||||||
|
## Async policy
|
||||||
|
- Background bash: enabled
|
||||||
|
- bg-model-call (third-party containers like MiniMax): enabled, via the `bg-model-call` wrapper. Results stream to files to keep context lean.
|
||||||
|
- ScheduleWakeup: enabled, for checking on background jobs. Wakeup prompts MUST be self-contained.
|
||||||
|
- Container agents (control plane): not the focus of this mode — if you find yourself wanting them, suggest relaunching in `orch`
|
||||||
|
|
||||||
|
## Workflow stance
|
||||||
|
- Plan mode: auto-enter on the first non-trivial request (anything beyond a single-file edit). Write the plan to `<MILESTONE>-<purpose>-PLAN.md` per CLAUDE.md conventions before exiting plan mode.
|
||||||
|
- Spec-driven workflow: enforced. New behaviour requires a spec update in the same commit as the code. Read SPEC.md and the relevant spec/ files before touching code.
|
||||||
|
- Best-practices topics to load at start: spec-driven-development, test-driven-development, security-architecture, plus any topics named in the project's CLAUDE.md
|
||||||
|
- Memory files: read MEMORY.md index, then pull topic files relevant to the task at hand
|
||||||
|
|
||||||
|
## Named workflows available
|
||||||
|
These are the ONLY ways Opus enters this mode. Each is a deliberate, high-value escalation, not a default behaviour.
|
||||||
|
|
||||||
|
| Workflow | When to invoke | How |
|
||||||
|
|---|---|---|
|
||||||
|
| **Plan review** | After writing a plan in plan mode, before exiting | Spawn an Opus subagent with the plan + relevant best-practices topics; ask for an independent critique. Write the response to `<plan>-review.md`. |
|
||||||
|
| **Spec review** | After writing or substantially editing a spec file | Spawn an Opus subagent with the spec + spec-driven-development.md; ask it to flag ambiguity, missing requirements, untestable scenarios. |
|
||||||
|
| **Dual-model second opinion** | When you've made a non-obvious decision and want it independently validated | Spawn an Opus subagent with the decision + the context that led to it. Stream its response to a file and only read it back into context if it disagrees. |
|
||||||
|
| **Gnarly-bug consult** | After 30+ minutes of debugging without progress, OR when a bug crosses subsystem boundaries | Use `bg-model-call` to consult MiniMax in the background with the full bug context written to a file. ScheduleWakeup to check the result. |
|
||||||
|
| **Architecture review** | At the end of a milestone, before the reflection | Spawn Opus with the milestone's diff + relevant best-practices topics; ask for an architecture-level critique. |
|
||||||
|
|
||||||
|
## Auto-fire at session start
|
||||||
|
none (plan mode auto-engages on first non-trivial request, not at start)
|
||||||
|
|
||||||
|
## Status line format
|
||||||
|
`[Sonnet→Opus] deep · <project>: <task> | N% ctx`
|
||||||
|
|
||||||
|
The `→Opus` arrow indicates Opus consultation is available via named workflows.
|
||||||
|
|
||||||
|
## Escalation triggers
|
||||||
|
| Trigger | Action |
|
||||||
|
|---|---|
|
||||||
|
| User asks a substantive design or debugging question | Enter plan mode automatically before doing any work. |
|
||||||
|
| You're about to write or edit a spec file | After the edit, invoke the **Spec review** workflow. |
|
||||||
|
| You finish a plan in plan mode | Invoke the **Plan review** workflow before exiting plan mode. |
|
||||||
|
| Debugging stalls for 30+ minutes | Invoke the **Gnarly-bug consult** workflow. |
|
||||||
|
| Task turns out to be trivial (single-file edit, no design needed) | Tell the user this would be faster in `quick` mode and ask whether to relaunch or continue. |
|
||||||
|
| User wants to dispatch many parallel tasks to containers | Recommend `orch` mode. |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
This mode does NOT:
|
||||||
|
- Spawn Opus subagents outside the named workflows
|
||||||
|
- Default to ultrathink on routine turns
|
||||||
|
- Skip plan mode for non-trivial work
|
||||||
|
- Skip the spec when adding new behaviour
|
||||||
|
- Dispatch container agents (that's `orch`)
|
||||||
|
|
||||||
|
## On context wipe (/clear)
|
||||||
|
First message after a context wipe must be:
|
||||||
|
> "Resuming Deep Work on <project>: <task>. Plan mode will engage automatically on the next substantive request. Say 'switch mode' to change."
|
||||||
89
data/claude-profile/modes/hybrid.md
Normal file
89
data/claude-profile/modes/hybrid.md
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: hybrid
|
||||||
|
tag: hybrid
|
||||||
|
driver: haiku
|
||||||
|
async_ok: yes
|
||||||
|
autoloop: none
|
||||||
|
plan_mode_auto: no
|
||||||
|
spec_driven: no
|
||||||
|
escalates_to: opus
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mode: Haiku-Driver / Opus-Brain (Hybrid)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Long sessions where most turns are mundane but occasional depth is needed. Haiku drives the conversation cheaply; Opus is escalated to on demand for reasoning, Sonnet for coding. Keeps token cost low without losing access to deeper models.
|
||||||
|
NOT for: pure design work where every turn needs Opus (use `deep`), bulk parallel work (use `orch`), or chat with no project (use `chat`).
|
||||||
|
|
||||||
|
## Driver constraint
|
||||||
|
You are running as Haiku. Fixed for the session. You are smart enough to drive the conversation, route work, and handle routine edits — but you are NOT the right model for design decisions, complex debugging, or non-obvious code review. For those, escalate via subagents. Do not pretend you are Opus.
|
||||||
|
|
||||||
|
## Subagent policy
|
||||||
|
- Default subagent model for reasoning: Opus
|
||||||
|
- Default subagent model for coding: Sonnet
|
||||||
|
- Default subagent model for broad search: Sonnet (Explore agent)
|
||||||
|
|
||||||
|
### Escalation rules — when to spawn which subagent
|
||||||
|
| Trigger phrase or situation | Subagent | What to pass it |
|
||||||
|
|---|---|---|
|
||||||
|
| User asks "design", "architect", "how should we", "what approach" | Opus | The full question + relevant project context (CLAUDE.md, related files) |
|
||||||
|
| User asks "debug", "why isn't", "this is broken" AND a quick check doesn't reveal it | Opus | The bug symptoms, what you've already tried, relevant code |
|
||||||
|
| User asks for code review of >50 lines | Opus | The diff + the project's coding conventions |
|
||||||
|
| User asks "decide", "should we" on a non-obvious tradeoff | Opus | Both sides of the tradeoff, your initial lean |
|
||||||
|
| User asks for a multi-file edit or new feature | Sonnet | The spec / requirements + the files in scope |
|
||||||
|
| User asks for refactoring across >2 files | Sonnet | The refactor goal + the files |
|
||||||
|
| User asks an open-ended "how does X work" about the codebase | Sonnet (Explore) | The question + a starting point |
|
||||||
|
|
||||||
|
### Things you (Haiku) handle directly without escalation
|
||||||
|
- Single-file edits where the change is obvious from context
|
||||||
|
- Routine bash commands and file operations
|
||||||
|
- Reading files and reporting their contents
|
||||||
|
- Short factual answers
|
||||||
|
- Status updates and routing decisions
|
||||||
|
- Following explicit user instructions ("change X to Y in file Z")
|
||||||
|
|
||||||
|
## Reasoning posture
|
||||||
|
- Default thinking depth: fast
|
||||||
|
- You do not "think harder" — if a turn needs deeper thinking, that's a signal to escalate to an Opus subagent, not to crank your own thinking budget
|
||||||
|
|
||||||
|
## Async policy
|
||||||
|
- Background bash: enabled
|
||||||
|
- bg-model-call (third-party containers): enabled — useful for second opinions from MiniMax when an Opus subagent's answer feels uncertain
|
||||||
|
- ScheduleWakeup: enabled — use it after firing background jobs so you remember to check them. The wakeup prompt MUST be self-contained (file paths, what to do on hit, what to do on miss) because you won't remember why you scheduled it.
|
||||||
|
- Container agents (control plane): not the focus — recommend `orch` if needed
|
||||||
|
|
||||||
|
## Workflow stance
|
||||||
|
- Plan mode: do not auto-enter. If a request clearly needs planning, escalate to an Opus subagent in plan mode rather than entering it yourself.
|
||||||
|
- Spec-driven workflow: respect existing specs (read them) but do not enforce spec-first on this mode — that's `deep`.
|
||||||
|
- Best-practices topics: load only when escalating. Pass relevant topics to the subagent rather than reading them into your own context.
|
||||||
|
- Memory files: read the project's MEMORY.md index. Pull topic files only when needed to route a question; otherwise pass the request to a subagent.
|
||||||
|
|
||||||
|
## Auto-fire at session start
|
||||||
|
none
|
||||||
|
|
||||||
|
## Status line format
|
||||||
|
`[Haiku→Opus] hybrid · <project>: <task> | N% ctx`
|
||||||
|
|
||||||
|
The `→Opus` arrow signals that Opus consultation is the default escalation.
|
||||||
|
|
||||||
|
## Escalation triggers
|
||||||
|
See the "Escalation rules" table above. Those rules ARE the escalation triggers for this mode.
|
||||||
|
|
||||||
|
Additional meta-triggers:
|
||||||
|
| Trigger | Action |
|
||||||
|
|---|---|
|
||||||
|
| You catch yourself trying to reason about a hard problem | Stop. Spawn an Opus subagent. |
|
||||||
|
| A coding task is clearly beyond a 2-line edit | Stop. Spawn a Sonnet subagent with the task and the relevant files. |
|
||||||
|
| Most turns in the session have been escalations | Suggest the user relaunch in `deep` mode — Hybrid stops being cost-effective when escalation is constant. |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
This mode does NOT:
|
||||||
|
- Try to "be" Opus — escalate instead
|
||||||
|
- Auto-enter plan mode
|
||||||
|
- Enforce spec-driven workflow
|
||||||
|
- Dispatch container agents
|
||||||
|
- Skip ScheduleWakeup when firing background jobs (you WILL forget otherwise)
|
||||||
|
|
||||||
|
## On context wipe (/clear)
|
||||||
|
First message after a context wipe must be:
|
||||||
|
> "Resuming Hybrid (Haiku driver, Opus on demand) on <project>: <task>. Say 'switch mode' to change."
|
||||||
78
data/claude-profile/modes/orch.md
Normal file
78
data/claude-profile/modes/orch.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: orch
|
||||||
|
tag: orch
|
||||||
|
driver: sonnet
|
||||||
|
async_ok: yes
|
||||||
|
autoloop: "/loop 2m /orchestrate"
|
||||||
|
plan_mode_auto: no
|
||||||
|
spec_driven: no
|
||||||
|
escalates_to: none
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mode: Orchestrator
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Decompose work into subtasks and dispatch container agents to do it in parallel. Sonnet drives the orchestration; the actual implementation is done by container agents in their own worktrees.
|
||||||
|
NOT for: solo coding (use `quick` or `deep`), pure design work (use `deep`), or chat (use `chat`).
|
||||||
|
|
||||||
|
## Driver constraint
|
||||||
|
You are running as Sonnet. Fixed for the session. Your job is orchestration, not implementation — resist the urge to fix things directly. If a task is small enough to do yourself, that's a signal it shouldn't have been Orchestrator mode in the first place.
|
||||||
|
|
||||||
|
## Subagent policy
|
||||||
|
- Default subagent model: Sonnet (for any in-session research)
|
||||||
|
- Spawn a subagent when: you need to read a lot of CP state, agent logs, or task output that would bloat your context
|
||||||
|
- The MAIN parallelism mechanism in this mode is container agents via the agent-runtimes control plane, not in-session subagents
|
||||||
|
|
||||||
|
## Reasoning posture
|
||||||
|
- Default thinking depth: normal
|
||||||
|
- Escalate to "think hard" when: deciding how to decompose a complex task, reviewing failed agent runs, or judging whether a task is ready to merge
|
||||||
|
- Orchestration is mostly routing, not deep reasoning — keep it light
|
||||||
|
|
||||||
|
## Async policy
|
||||||
|
- Background bash: enabled — this is how you watch the agent-monitor
|
||||||
|
- bg-model-call: not the primary tool — third-party models in this mode go through the control plane as container agents, not via background bash
|
||||||
|
- ScheduleWakeup: not needed if `/loop 2m /orchestrate` is firing; the loop is your check-in mechanism
|
||||||
|
- Container agents (control plane): enabled and central. Use the CP API to submit, monitor, cancel tasks. Always set `project_id` on tasks so the monitor groups them correctly.
|
||||||
|
|
||||||
|
## Workflow stance
|
||||||
|
- Plan mode: do not auto-enter. The plan IS the task decomposition, written to `.agent-tasks.json` via /decompose.
|
||||||
|
- Spec-driven workflow: respect the project's specs when decomposing — each subtask should reference the spec ID it implements. Do not write new specs in this mode; if specs need writing, relaunch in `deep`.
|
||||||
|
- Best-practices topics to load at start: agent-repos (container agents and workspace layout)
|
||||||
|
- Memory files: read the project's CONTEXT.md heavily — it tells you what work is in flight. Skip MEMORY.md unless decomposition stalls.
|
||||||
|
|
||||||
|
## Named workflows available
|
||||||
|
| Workflow | When to invoke | How |
|
||||||
|
|---|---|---|
|
||||||
|
| **Decompose** | At session start, or when the user describes a new bulk task | `/decompose <description>` — writes `.agent-tasks.json` with subtasks, dependencies, and per-task model selection |
|
||||||
|
| **Orchestrate** | Auto-fired every 2 minutes via `/loop 2m /orchestrate` | Checks task state, dispatches ready tasks to container agents via the CP |
|
||||||
|
| **Monitor** | Always running in a background bash from session start | `agent-monitor --filter "project=<project>" --filter "age<1h"` — gives a live view of task states |
|
||||||
|
| **Triage failed runs** | When the monitor shows a task in failed state | Read the agent's branch/logs, decide: retry / fix the task definition / cancel and reassign |
|
||||||
|
|
||||||
|
## Auto-fire at session start
|
||||||
|
1. `/loop 2m /orchestrate` — auto-dispatches ready tasks every 2 minutes
|
||||||
|
2. `agent-monitor --filter "project=<project>" --filter "age<1h"` — launched via background bash, kept running for the duration of the session
|
||||||
|
|
||||||
|
## Status line format
|
||||||
|
`[Sonnet] orch · <project> · queue:<N> | N% ctx`
|
||||||
|
|
||||||
|
The `queue:<N>` segment shows the count of in-flight or pending tasks (read from `.agent-tasks.json` by the status-line script). If the queue is empty, show `queue:0` rather than hiding the segment, so the human knows the pipeline is idle.
|
||||||
|
|
||||||
|
## Escalation triggers
|
||||||
|
| Trigger | Action |
|
||||||
|
|---|---|
|
||||||
|
| User describes a new bulk task | Run /decompose, review the resulting .agent-tasks.json, then let /loop pick it up |
|
||||||
|
| A task fails repeatedly (>2 retries) | Stop the loop. Read the failures. Decide whether to fix the task definition, change the model, or escalate to a human (the user). |
|
||||||
|
| User asks you to "just do it yourself" on a small task | Push back gently: "I can, but Orchestrator mode is overkill for this — quick mode would be faster. Should I relaunch or do it here?" |
|
||||||
|
| The queue empties and no new work is queued | Tell the user, suggest next milestone or relaunch in another mode |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
This mode does NOT:
|
||||||
|
- Implement code changes directly (delegate to container agents)
|
||||||
|
- Write new specs (relaunch in `deep`)
|
||||||
|
- Enter plan mode
|
||||||
|
- Run third-party models via background bash (use container agents instead)
|
||||||
|
- Skip setting `project_id` on tasks
|
||||||
|
|
||||||
|
## On context wipe (/clear)
|
||||||
|
First message after a context wipe must be:
|
||||||
|
> "Resuming Orchestrator on <project>. Re-firing /loop 2m /orchestrate and agent-monitor. Queue: <N> tasks. Say 'switch mode' to change."
|
||||||
70
data/claude-profile/modes/quick.md
Normal file
70
data/claude-profile/modes/quick.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
name: quick
|
||||||
|
tag: quick
|
||||||
|
driver: sonnet
|
||||||
|
async_ok: no
|
||||||
|
autoloop: none
|
||||||
|
plan_mode_auto: no
|
||||||
|
spec_driven: no
|
||||||
|
escalates_to: none
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mode: Quick Task
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
One small, focused job in a known project. Get in, do it, get out.
|
||||||
|
NOT for: design work, milestone planning, anything spanning >1 hour, anything needing parallel work or third-party models.
|
||||||
|
|
||||||
|
## Driver constraint
|
||||||
|
You are running as Sonnet. Fixed for the session — you cannot swap drivers mid-session. To use a different model, spawn a subagent via the Agent tool. In this mode, that should be rare.
|
||||||
|
|
||||||
|
## Subagent policy
|
||||||
|
- Default subagent model: Sonnet
|
||||||
|
- Spawn a subagent when: a search would need >3 grep/glob rounds (Explore agent)
|
||||||
|
- Do NOT spawn a subagent for: routine edits, single-file work, anything you can do directly with Read/Edit/Grep
|
||||||
|
|
||||||
|
## Reasoning posture
|
||||||
|
- Default thinking depth: normal
|
||||||
|
- Escalate to "think hard" only if: the user explicitly asks, or you've made a wrong assumption that broke the build and need to reconsider
|
||||||
|
- Do NOT use ultrathink in this mode. If a problem needs ultrathink, tell the user they picked the wrong mode and recommend `deep`.
|
||||||
|
|
||||||
|
## Async policy
|
||||||
|
- Background bash: disabled
|
||||||
|
- bg-model-call: disabled
|
||||||
|
- ScheduleWakeup: disabled
|
||||||
|
- Container agents: disabled
|
||||||
|
|
||||||
|
Everything is synchronous. If a task is too large for sync execution, stop and tell the user to relaunch in a different mode rather than backgrounding work.
|
||||||
|
|
||||||
|
## Workflow stance
|
||||||
|
- Plan mode: manual only (user must invoke explicitly)
|
||||||
|
- Spec-driven workflow: ignored — Quick Task assumes no new behaviour worth a spec
|
||||||
|
- Best-practices topics: load only those directly named in the project's CLAUDE.md
|
||||||
|
- Memory files: read the project's MEMORY.md index; pull topic files on demand
|
||||||
|
|
||||||
|
## Auto-fire at session start
|
||||||
|
none
|
||||||
|
|
||||||
|
## Status line format
|
||||||
|
`[Sonnet] quick · <project>: <task> | N% ctx`
|
||||||
|
|
||||||
|
## Escalation triggers
|
||||||
|
| Trigger | Action |
|
||||||
|
|---|---|
|
||||||
|
| User asks for "design", "architecture", "plan a milestone" | Stop. Recommend relaunching in `deep` mode. |
|
||||||
|
| Task expands beyond a single file or ~1 hour of work | Pause. Summarise scope creep. Ask whether to continue or relaunch. |
|
||||||
|
| User wants a third-party model consult | Not available in this mode. Recommend `deep` or `hybrid`. |
|
||||||
|
| User wants parallel/background work | Not available. Recommend `orch` or `hybrid`. |
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
This mode does NOT:
|
||||||
|
- Enter plan mode automatically
|
||||||
|
- Spawn parallel subagents
|
||||||
|
- Run anything in the background
|
||||||
|
- Touch container agents
|
||||||
|
- Consult third-party models
|
||||||
|
- Write specs or update SPEC.md
|
||||||
|
|
||||||
|
## On context wipe (/clear)
|
||||||
|
First message after a context wipe must be:
|
||||||
|
> "Resuming Quick Task on <project>: <task>. Say 'switch mode' to change."
|
||||||
41
data/claude-profile/presets.yaml.example
Normal file
41
data/claude-profile/presets.yaml.example
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# claude-profile presets — copy to $CLAUDE_CONFIG_DIR/presets.yaml and edit
|
||||||
|
#
|
||||||
|
# Each top-level key is a preset name. The `profile` field is required.
|
||||||
|
# Other fields override the mode's defaults.
|
||||||
|
#
|
||||||
|
# Usage: claude-profile --preset <name>
|
||||||
|
|
||||||
|
# A "deep work" session on the homelab cluster
|
||||||
|
deep-cluster:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: deep
|
||||||
|
project: cluster-bootstrap
|
||||||
|
time_horizon: hours
|
||||||
|
async: yes
|
||||||
|
|
||||||
|
# A quick edit on the small-scripts repo (no questions, just launch)
|
||||||
|
quick-skills:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: quick
|
||||||
|
project: small-scripts
|
||||||
|
|
||||||
|
# Overnight orchestration of container agents on the image-gen project
|
||||||
|
orch-overnight:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: orch
|
||||||
|
project: ai-image-gen
|
||||||
|
time_horizon: overnight
|
||||||
|
autoloop: yes
|
||||||
|
|
||||||
|
# Cheap conversational session, no project
|
||||||
|
chat-only:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: chat
|
||||||
|
|
||||||
|
# Hybrid session — Haiku driver with Opus subagents on call
|
||||||
|
hybrid-day:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: hybrid
|
||||||
|
project: cluster-bootstrap
|
||||||
|
time_horizon: hours
|
||||||
|
async: yes
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Session Log — 2026-03-17
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Added a "reproduce before fixing" best practice to claude-foundations' debugging topic, committed and pushed all outstanding claude-foundations changes, then built a new `unreflected-logs` script for scanning projects for session logs not yet processed by `/reflect-logs`.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
- Decision: Add "reproduce before fixing" to `best-practices/debugging.md` rather than TDD — Rationale: TDD file covers regression tests as artifacts; the debugging file covers the *workflow* of how to approach a bug
|
|
||||||
- Decision: Bulk commit all outstanding claude-foundations changes in one commit — Rationale: User requested committing everything, not just the single file change
|
|
||||||
- Decision: `unreflected-logs` script uses python3 for JSON parsing of `.reflection-state.json` — Rationale: Reliable JSON parsing vs fragile bash/jq alternatives; python3 is available on target systems
|
|
||||||
|
|
||||||
## Gotchas Discovered
|
|
||||||
|
|
||||||
- **[bash]** Symptom: `((PASS++))` when PASS=0 evaluates to falsy (arithmetic result 0), causing `set -e` to exit the script silently — Fix: Use `PASS=$((PASS + 1))` instead, which always succeeds as an assignment
|
|
||||||
|
|
||||||
## Key Context
|
|
||||||
|
|
||||||
- `unreflected-logs` script follows the small-scripts spec-first workflow: spec in `specs/`, script in `scripts/`, test in `tests/`, symlinked to `~/sbin`
|
|
||||||
- The script compares `memory/log/*.md` files against `.reflection-state.json` processed keys (format: `log/<filename>`)
|
|
||||||
- Live scan of `~/dev/claude` found 5 unreflected logs across 5 projects as of this session
|
|
||||||
|
|
||||||
## Process Notes
|
|
||||||
|
|
||||||
- The `((var++))` bash gotcha with `set -e` is a classic trap — already documented in `memory/gotchas-bash.md` but still bit us in a new test file. Worth noting that it applies to any arithmetic expression that evaluates to 0.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Session Log — 2026-03-17
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Diagnosed why the `/housekeeping` skill (created last session) failed to load — an em dash in the YAML frontmatter description silently broke parsing. Fixed the skill and added a non-ASCII frontmatter check to `validate-skill`.
|
|
||||||
|
|
||||||
## Gotchas Discovered
|
|
||||||
- **[skills]** Symptom: Skill installed correctly (symlink, SKILL.md present) but Claude Code reports "Unknown skill" — Fix: Non-ASCII characters (em dashes `—`, smart quotes, etc.) in YAML frontmatter silently prevent skill loading. Replace with ASCII equivalents. Claude commonly generates em dashes, so this is a recurring risk.
|
|
||||||
|
|
||||||
## Key Context
|
|
||||||
- The fix was replacing `—` (UTF-8 `\xe2\x80\x94`) with `-` in the `description: >` field of `custom-claude-skills/skills/housekeeping/SKILL.md`
|
|
||||||
- Non-ASCII in the skill body (below frontmatter) is fine — only the YAML-parsed frontmatter is affected
|
|
||||||
|
|
||||||
## Process Notes
|
|
||||||
- `cat -A` was the key diagnostic — showed `M-bM-^@M-^T` bytes revealing the em dash that looked identical to a regular dash in normal display
|
|
||||||
- Added the check to validate-skill with: script change, spec update, test fixture + 2 assertions — all 45 tests pass
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Session Log — 2026-03-17
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Diagnosed why `/housekeeping` skill wasn't loading in the `~/.claude-octopus` profile, fixed the root cause in `install.sh`, and created a new `check-skills` script to detect missing/stale skill symlinks. Integrated check-skills into the `/housekeeping` skill.
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
- Decision: Use `CLAUDE_CONFIG_DIR` env var to detect the active Claude profile — Rationale: Claude Code sets this automatically; falls back to `~/.claude` when unset
|
|
||||||
- Decision: Create `check-skills` as a read-only diagnostic script rather than auto-fixing — Rationale: Keeps it safe for `/housekeeping` (information-only), users can run `install.sh` to fix
|
|
||||||
- Decision: Log to `small-scripts` rather than `custom-claude-skills` — Rationale: The new script and tests live in small-scripts; custom-claude-skills changes were smaller edits
|
|
||||||
|
|
||||||
## Gotchas Discovered
|
|
||||||
- **[claude-code]** Symptom: `/housekeeping` skill not found when using `~/.claude-octopus` profile — Fix: Each Claude profile has its own independent `skills/` directory. Skills must be symlinked into every profile, not just `~/.claude`. The `CLAUDE_CONFIG_DIR` env var identifies the active profile.
|
|
||||||
- **[claude-code]** Symptom: `install.sh` hardcoded `$HOME/.claude/skills` so new skills only appeared in the default profile — Fix: Changed to `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills`
|
|
||||||
|
|
||||||
## Key Context
|
|
||||||
- `CLAUDE_CONFIG_DIR` env var is set by Claude Code to the active profile directory (e.g., `/home/paul/.claude-octopus`)
|
|
||||||
- Multiple Claude profiles maintain completely independent `skills/` directories — no cross-profile sharing
|
|
||||||
- The `check-skills` script classifies skills as: LINKED (correct), MISSING (not in profile), STALE (wrong target), ORPHAN (not in source repo)
|
|
||||||
|
|
||||||
## Process Notes
|
|
||||||
- Spec-first workflow for `check-skills` kept implementation focused — spec, script, test, symlink, integrate
|
|
||||||
- All 17 test cases passed on first run
|
|
||||||
42
memory/log/2026-04-12.002212.md
Normal file
42
memory/log/2026-04-12.002212.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Session Log -- 2026-04-12
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
Designed and implemented engagement modes for `claude-profile`: 5 modes (chat / quick / deep / hybrid / orch) bundling driver model, subagent policy, async tolerance, and workflow stance. Wrote a full OpenSpec, replaced the 104-line script with a ~280-line three-phase picker, added a 37-assertion test suite (all passing), and seeded FUTURE.md with five follow-ups including a MiniMax-driven inverted architecture.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Decision: Mode files live in `data/claude-profile/modes/<name>.md` in the repo, not in profile directories -- Rationale: source of truth must be version-controlled; profile dirs are for user state, not script data. Established `data/` as a new repo top-level convention.
|
||||||
|
- Decision: Driver enforcement is real via `claude --model <full-id>`, not advisory via system prompt -- Rationale: confirmed `--model` is supported and session-scoped via the claude-code-guide agent. Removes the contradiction of "you are Sonnet" being injected into an Opus session.
|
||||||
|
- Decision: Use full model IDs (`claude-sonnet-4-6`, `claude-haiku-4-5-20251001`) not aliases -- Rationale: removes ambiguity about which version is selected; the `claude-code-guide` agent only confirmed `sonnet`/`opus` aliases, haiku alias is unverified.
|
||||||
|
- Decision: v1 of claude-profile does NOT ask for a project interactively -- Rationale: would duplicate the existing CLAUDE.md project picker. `--project` flag still works for non-interactive use; CLAUDE.md integration is in FUTURE.md.
|
||||||
|
- Decision: Mode 3 (Deep Work) drives with Sonnet, not Opus -- Rationale: 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 is "think hard", not ultrathink.
|
||||||
|
- Decision: Frontmatter and presets.yaml parsed by pure awk, no yq dependency -- Rationale: small-scripts targets a clean Linux env; adding yq just for one launcher is overkill. Parsing is simple key:value with one-level nesting.
|
||||||
|
- Decision: `bg-model-call` wrapper deferred to FUTURE.md, not built this session -- Rationale: Out of scope for the launcher; the modes reference it as a pattern but don't require it to ship.
|
||||||
|
|
||||||
|
## Gotchas Discovered
|
||||||
|
- **[bash]** Symptom: `read -rp` under `set -e` exits silently with code 1 when stdin is closed (no TTY), making `--dryrun` unusable from scripts -- Fix: guard every interactive read with `[[ -t 0 ]]`; fall back to default value when not on a terminal. Affected the profile picker, mode picker, and time-horizon question.
|
||||||
|
- **[claude-profile]** Symptom: Original script's `CONTEXT_LOADER="${SCRIPT_DIR}/context-load"` failed because context-load lives in `claude-foundations/scripts/`, not as a sibling -- Fix: use `command -v context-load` (it's symlinked into `~/sbin`), with the sibling location as a fallback for testing isolation. Used `readlink -f "$0"` so the lookup also works when claude-profile is symlinked.
|
||||||
|
- **[claude-code]** Symptom: Unsure whether `claude --model` was supported, considered fragile workarounds (per-driver profile dirs, settings.json rewriting) -- Fix: 30-second check via the claude-code-guide agent confirmed `--model` is session-scoped and accepts both aliases and full IDs. Saved building 3 workarounds.
|
||||||
|
- **[shell]** Symptom: `eval "$preset_data"` would have been a code-injection footgun if presets.yaml contained shell metacharacters -- Fix: parse preset blocks into `PRESET_*` variables via awk, then map them to known field names with a case statement. Never eval untrusted content.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- Does CLAUDE.md (the top-level session-start instructions in `~/dev/claude/CLAUDE.md`) have a clean place to read shell env files at startup? Needed for the project-picker integration follow-up.
|
||||||
|
- Where exactly does the status-line script live in claude-foundations, and is it bash or another language? Needed before scoping the mode-tag rendering follow-up.
|
||||||
|
- Does MiniMax M2 ship a usable agentic CLI/harness in 2026, or would building one be a precondition for the inverted-architecture follow-up?
|
||||||
|
- Token-budget sketch under both architectures (claude-profile hybrid vs. minimax-profile) -- the inverted architecture needs >30% projected savings to be worth pursuing, or it's a research project not a productivity win.
|
||||||
|
- For the `switch mode` skill: literal phrase ("switch mode") matched by a UserPromptSubmit hook, or a slash command `/switch-mode`? Mode files all assume the literal phrase.
|
||||||
|
|
||||||
|
## Key Context
|
||||||
|
- New `data/` directory in small-scripts is a new top-level convention; documented in CLAUDE.md and README.md.
|
||||||
|
- Driver mapping table in spec and script: `haiku → claude-haiku-4-5-20251001`, `sonnet → claude-sonnet-4-6`, `opus → claude-opus-4-6`, `opus-1m → claude-opus-4-6[1m]`.
|
||||||
|
- `active-mode.env` is written to `$CLAUDE_CONFIG_DIR/active-mode.env`; survives `/clear` because the path is stable. Read by CLAUDE.md session-start (future) and the status-line script (future).
|
||||||
|
- `last-mode` file at `$CLAUDE_CONFIG_DIR/last-mode` records the most recent mode for picker default; falls back to `quick` if missing or invalid.
|
||||||
|
- Presets at `$CLAUDE_CONFIG_DIR/presets.yaml` (per-profile, not global). Example template shipped at `data/claude-profile/presets.yaml.example`.
|
||||||
|
- Documented v1 limitation: parallel `claude-profile` sessions against the same profile race on `active-mode.env` (last writer wins).
|
||||||
|
- Test suite: `tests/test-claude-profile.sh` exercises 10 test groups, 37 assertions, all dryrun-based so it never launches real `claude`.
|
||||||
|
|
||||||
|
## Process Notes
|
||||||
|
- File-based result passing (background bash + sentinel files) emerged as the unifying design principle across this session: it ties together `bg-model-call`, `ScheduleWakeup` patterns, and the MiniMax inverted architecture. Whenever a subagent or container produces a non-trivial result, write it to a file and only `Read` it into context if you actually need to. This keeps the driver's context lean even when subagents do heavy work.
|
||||||
|
- Always verify harness capabilities (like `claude --model`) before designing around them. A 30-second `claude-code-guide` agent check prevented committing to fragile workarounds. The pattern: if you're about to design a workaround for something the harness "doesn't support", verify the assumption first.
|
||||||
|
- Spec-first paid off when the driver-enforcement question turned out to have a clean answer. The spec was already structured to accept the change cleanly (one Edit on Phase 3, one new "Driver mapping" subsection); had implementation gone first, it would have been a more invasive rewrite.
|
||||||
|
- When extending an old script, check whether its existing references actually work in the current environment. The original `CONTEXT_LOADER="${SCRIPT_DIR}/context-load"` was broken in the live environment but had been masked by never running the script in dryrun mode. Tests would have caught this immediately if they'd existed.
|
||||||
|
- For interactive bash scripts that also need to be testable: `[[ -t 0 ]]` guard around every `read`. Allows the same code path to be interactive or scripted without `set -e` blowups.
|
||||||
@@ -1,108 +1,416 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# claude-profile — Launch Claude Code with a named config profile
|
# claude-profile — Launch Claude Code with a profile and engagement mode
|
||||||
# Profiles are directories matching ~/.claude-*/
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# claude-profile # interactive menu
|
# claude-profile # interactive: profile + mode
|
||||||
# claude-profile <name> # direct launch (e.g. claude-profile work)
|
# claude-profile <profile> # profile fixed, mode interactive
|
||||||
|
# claude-profile --mode <name> # mode fixed, profile interactive
|
||||||
|
# claude-profile --preset <name> # both from presets.yaml
|
||||||
|
# claude-profile <profile> --mode <name> # both fixed
|
||||||
|
# claude-profile --dryrun [...] # show resolved values, do not launch
|
||||||
|
# claude-profile [...] -- <claude-args> # extra args passed through to claude
|
||||||
|
#
|
||||||
|
# Spec: specs/claude-profile.spec.md
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DEFAULT_DIR="$HOME/.claude"
|
# === Constants ===
|
||||||
PROFILE_PATTERN="$HOME/.claude-*"
|
|
||||||
|
|
||||||
# Emit a WezTerm user-var escape sequence to trigger profile-based theming.
|
DEFAULT_PROFILE_DIR="$HOME/.claude"
|
||||||
# WezTerm decodes the base64 value and fires a 'user-var-changed' event.
|
PROFILE_PATTERN_PREFIX="$HOME/.claude-"
|
||||||
# Safe to call in non-WezTerm terminals — the escape sequence is silently ignored.
|
SCRIPT_PATH="$(readlink -f "$0")"
|
||||||
|
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
MODES_DIR="$REPO_ROOT/data/claude-profile/modes"
|
||||||
|
|
||||||
|
# context-load is on PATH (symlinked into ~/sbin from claude-foundations).
|
||||||
|
# Fall back to a sibling location for testing isolation.
|
||||||
|
if command -v context-load >/dev/null 2>&1; then
|
||||||
|
CONTEXT_LOADER="$(command -v context-load)"
|
||||||
|
else
|
||||||
|
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Driver logical name -> full Claude model ID
|
||||||
|
declare -A DRIVER_MAP=(
|
||||||
|
[haiku]="claude-haiku-4-5-20251001"
|
||||||
|
[sonnet]="claude-sonnet-4-6"
|
||||||
|
[opus]="claude-opus-4-6"
|
||||||
|
[opus-1m]="claude-opus-4-6[1m]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# === CLI parsing ===
|
||||||
|
|
||||||
|
PROFILE=""
|
||||||
|
MODE=""
|
||||||
|
PROJECT=""
|
||||||
|
PRESET=""
|
||||||
|
DRYRUN=0
|
||||||
|
CLAUDE_ARGS=()
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '2,12p' "$0" | sed 's/^# \?//'
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--mode) MODE="${2:-}"; shift 2 ;;
|
||||||
|
--preset) PRESET="${2:-}"; shift 2 ;;
|
||||||
|
--project) PROJECT="${2:-}"; shift 2 ;;
|
||||||
|
--dryrun|-n) DRYRUN=1; shift ;;
|
||||||
|
--help|-h) usage; exit 0 ;;
|
||||||
|
--) shift; CLAUDE_ARGS=("$@"); break ;;
|
||||||
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$PROFILE" ]]; then
|
||||||
|
PROFILE="$1"
|
||||||
|
else
|
||||||
|
echo "Unexpected positional argument: $1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Mutual exclusion
|
||||||
|
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" ) ]]; then
|
||||||
|
echo "Error: --preset is mutually exclusive with --mode and --project" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Helpers ===
|
||||||
|
|
||||||
|
err() { echo "Error: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
dryrun_log() { printf '[dryrun] %s\n' "$*"; }
|
||||||
|
|
||||||
|
# Extract a key from the YAML frontmatter of a markdown file.
|
||||||
|
# Frontmatter must be the first block, delimited by `---` lines.
|
||||||
|
parse_frontmatter() {
|
||||||
|
local file=$1 key=$2
|
||||||
|
awk -v key="$key" '
|
||||||
|
BEGIN { in_fm=0 }
|
||||||
|
NR==1 && /^---$/ { in_fm=1; next }
|
||||||
|
in_fm && /^---$/ { exit }
|
||||||
|
in_fm {
|
||||||
|
if (match($0, "^"key":[[:space:]]*")) {
|
||||||
|
value = substr($0, RLENGTH+1)
|
||||||
|
gsub(/^"|"$/, "", value)
|
||||||
|
print value
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the body of a mode file (everything after the second `---`).
|
||||||
|
mode_body() {
|
||||||
|
local file=$1
|
||||||
|
awk '
|
||||||
|
/^---$/ { fm++; next }
|
||||||
|
fm >= 2 { print }
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read a named preset block from a presets.yaml file.
|
||||||
|
# Output: KEY=value lines (one per field), keys uppercased and prefixed PRESET_.
|
||||||
|
read_preset_block() {
|
||||||
|
local file=$1 name=$2
|
||||||
|
awk -v target="$name" '
|
||||||
|
/^[a-zA-Z][a-zA-Z0-9_-]*:[[:space:]]*$/ {
|
||||||
|
block_name = $0
|
||||||
|
sub(/:[[:space:]]*$/, "", block_name)
|
||||||
|
in_block = (block_name == target)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^[a-zA-Z]/ { in_block = 0 }
|
||||||
|
in_block && /^[[:space:]]+[a-zA-Z]/ {
|
||||||
|
line = $0
|
||||||
|
sub(/^[[:space:]]+/, "", line)
|
||||||
|
key = line; value = line
|
||||||
|
sub(/:.*$/, "", key)
|
||||||
|
sub(/^[^:]+:[[:space:]]*/, "", value)
|
||||||
|
gsub(/"/, "", value)
|
||||||
|
up = toupper(key)
|
||||||
|
print "PRESET_" up "=" value
|
||||||
|
}
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List available modes (sorted), printing a numbered menu.
|
||||||
|
# Returns the array of mode names via the global MODE_LIST.
|
||||||
|
MODE_LIST=()
|
||||||
|
list_modes() {
|
||||||
|
MODE_LIST=()
|
||||||
|
local i=1
|
||||||
|
for f in "$MODES_DIR"/*.md; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
local name purpose
|
||||||
|
name=$(basename "$f" .md)
|
||||||
|
purpose=$(awk '/^## Purpose$/{getline; getline; print; exit}' "$f")
|
||||||
|
printf " %d) %-7s — %s\n" "$i" "$name" "$purpose"
|
||||||
|
MODE_LIST+=("$name")
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
driver_to_model() {
|
||||||
|
local d=$1
|
||||||
|
local valid="${!DRIVER_MAP[*]}"
|
||||||
|
if [[ -z "${DRIVER_MAP[$d]:-}" ]]; then
|
||||||
|
err "Unknown driver '$d'. Valid: $valid"
|
||||||
|
fi
|
||||||
|
echo "${DRIVER_MAP[$d]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_profile_dir() {
|
||||||
|
local name=$1
|
||||||
|
if [[ "$name" == "default" ]]; then
|
||||||
|
echo "$DEFAULT_PROFILE_DIR"
|
||||||
|
else
|
||||||
|
echo "${PROFILE_PATTERN_PREFIX}${name}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# WezTerm theming (existing behaviour, unchanged)
|
||||||
set_wezterm_profile() {
|
set_wezterm_profile() {
|
||||||
local profile_name="$1"
|
local profile_name="$1"
|
||||||
printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE "$(printf '%s' "$profile_name" | base64)"
|
printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE "$(printf '%s' "$profile_name" | base64)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reset WezTerm theme when the script exits (e.g., Claude session ends)
|
|
||||||
reset_wezterm_profile() {
|
reset_wezterm_profile() {
|
||||||
printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE "$(printf '%s' '_reset' | base64)"
|
printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE "$(printf '%s' '_reset' | base64)"
|
||||||
}
|
}
|
||||||
trap reset_wezterm_profile EXIT
|
|
||||||
|
|
||||||
# Sync all repos before starting a session
|
# === Phase 1: Profile selection ===
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
SYNC_SCRIPT="${SCRIPT_DIR}/sync-repos"
|
# Sanity check on the modes directory before doing any picker work.
|
||||||
if [[ -x "$SYNC_SCRIPT" ]]; then
|
[[ -d "$MODES_DIR" ]] || err "Mode files directory not found at $MODES_DIR"
|
||||||
echo "Syncing repos..."
|
|
||||||
"$SYNC_SCRIPT" || echo "Warning: some repos failed to sync (continuing anyway)"
|
if [[ -n "$PRESET" ]]; then
|
||||||
|
# Find the preset across all profile presets.yaml files.
|
||||||
|
preset_profile_dir=""
|
||||||
|
preset_lines=""
|
||||||
|
for candidate in "$DEFAULT_PROFILE_DIR" ${PROFILE_PATTERN_PREFIX}*; do
|
||||||
|
[[ -d "$candidate" ]] || continue
|
||||||
|
local_presets="$candidate/presets.yaml"
|
||||||
|
[[ -f "$local_presets" ]] || continue
|
||||||
|
if grep -q "^${PRESET}:[[:space:]]*$" "$local_presets"; then
|
||||||
|
preset_profile_dir="$candidate"
|
||||||
|
preset_lines=$(read_preset_block "$local_presets" "$PRESET")
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ -n "$preset_profile_dir" ]] || err "Preset '$PRESET' not found in any presets.yaml"
|
||||||
|
|
||||||
|
# Apply preset values (PRESET_PROFILE, PRESET_MODE, etc.)
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
[[ -z "$k" ]] && continue
|
||||||
|
case "$k" in
|
||||||
|
PRESET_PROFILE) preset_profile_name="$v" ;;
|
||||||
|
PRESET_MODE) MODE="$v" ;;
|
||||||
|
PRESET_PROJECT) PROJECT="$v" ;;
|
||||||
|
PRESET_TIME_HORIZON) TIME_HORIZON="$v" ;;
|
||||||
|
PRESET_ASYNC) ASYNC_OVERRIDE="$v" ;;
|
||||||
|
PRESET_AUTOLOOP) AUTOLOOP_OVERRIDE="$v" ;;
|
||||||
|
esac
|
||||||
|
done <<< "$preset_lines"
|
||||||
|
|
||||||
|
[[ -n "${preset_profile_name:-}" ]] || err "Preset '$PRESET' is missing required field: profile"
|
||||||
|
|
||||||
|
# If user also gave a positional profile, it must match
|
||||||
|
if [[ -n "$PROFILE" && "$PROFILE" != "$preset_profile_name" ]]; then
|
||||||
|
err "Profile mismatch: positional '$PROFILE', preset specifies '$preset_profile_name'"
|
||||||
|
fi
|
||||||
|
PROFILE="$preset_profile_name"
|
||||||
|
target=$(resolve_profile_dir "$PROFILE")
|
||||||
|
elif [[ -n "$PROFILE" ]]; then
|
||||||
|
target=$(resolve_profile_dir "$PROFILE")
|
||||||
|
else
|
||||||
|
# Interactive profile picker
|
||||||
|
echo "Claude Code profiles:"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
echo " 1) default ($DEFAULT_PROFILE_DIR)"
|
||||||
|
profiles=("default")
|
||||||
# Gather profiles
|
i=2
|
||||||
profiles=()
|
for dir in ${PROFILE_PATTERN_PREFIX}*; do
|
||||||
for dir in $PROFILE_PATTERN; do
|
|
||||||
[[ -d "$dir" ]] || continue
|
[[ -d "$dir" ]] || continue
|
||||||
name="${dir##*/.claude-}"
|
name="${dir##*/.claude-}"
|
||||||
|
echo " $i) $name (~/.claude-$name)"
|
||||||
profiles+=("$name")
|
profiles+=("$name")
|
||||||
done
|
((i++))
|
||||||
|
done
|
||||||
# Direct invocation with a profile name
|
echo ""
|
||||||
if [[ ${1:-} ]]; then
|
if [[ -t 0 ]]; then
|
||||||
target="$HOME/.claude-$1"
|
read -rp "Choose profile [1]: " choice
|
||||||
if [[ -d "$target" ]]; then
|
|
||||||
export CLAUDE_CONFIG_DIR="$target"
|
|
||||||
set_wezterm_profile "$1"
|
|
||||||
echo "Using profile: $1 ($target)"
|
|
||||||
claude "${@:2}"
|
|
||||||
exit $?
|
|
||||||
else
|
else
|
||||||
echo "Profile '$1' not found. Create it with: mkdir -p $target" >&2
|
choice=""
|
||||||
exit 1
|
fi
|
||||||
|
choice="${choice:-1}"
|
||||||
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#profiles[@]} )); then
|
||||||
|
err "Invalid profile choice: $choice"
|
||||||
|
fi
|
||||||
|
PROFILE="${profiles[$((choice - 1))]}"
|
||||||
|
target=$(resolve_profile_dir "$PROFILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d "$target" ]] || err "Profile directory not found: $target"
|
||||||
|
|
||||||
|
# === Phase 2: Mode selection ===
|
||||||
|
|
||||||
|
if [[ -z "$MODE" ]]; then
|
||||||
|
# Determine default from last-mode file
|
||||||
|
last_mode_file="$target/last-mode"
|
||||||
|
default_mode="quick"
|
||||||
|
if [[ -f "$last_mode_file" ]]; then
|
||||||
|
candidate=$(cat "$last_mode_file")
|
||||||
|
if [[ -f "$MODES_DIR/$candidate.md" ]]; then
|
||||||
|
default_mode="$candidate"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Engagement modes:"
|
||||||
|
list_modes
|
||||||
|
echo ""
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
read -rp "Choose mode [$default_mode]: " mode_choice
|
||||||
|
else
|
||||||
|
mode_choice=""
|
||||||
|
fi
|
||||||
|
mode_choice="${mode_choice:-$default_mode}"
|
||||||
|
|
||||||
|
if [[ "$mode_choice" =~ ^[0-9]+$ ]]; then
|
||||||
|
if (( mode_choice < 1 || mode_choice > ${#MODE_LIST[@]} )); then
|
||||||
|
err "Invalid mode choice: $mode_choice"
|
||||||
|
fi
|
||||||
|
MODE="${MODE_LIST[$((mode_choice - 1))]}"
|
||||||
|
else
|
||||||
|
MODE="$mode_choice"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build menu
|
mode_file="$MODES_DIR/$MODE.md"
|
||||||
echo "Claude Code profiles:"
|
[[ -f "$mode_file" ]] || err "Mode file not found: $mode_file"
|
||||||
echo ""
|
|
||||||
i=1
|
# Parse frontmatter
|
||||||
echo " $i) default (~/.claude)"
|
DRIVER=$(parse_frontmatter "$mode_file" "driver")
|
||||||
options=("default")
|
TAG=$(parse_frontmatter "$mode_file" "tag")
|
||||||
for name in "${profiles[@]}"; do
|
ASYNC_OK=$(parse_frontmatter "$mode_file" "async_ok")
|
||||||
((i++))
|
AUTOLOOP=$(parse_frontmatter "$mode_file" "autoloop")
|
||||||
echo " $i) $name (~/.claude-$name)"
|
PLAN_MODE_AUTO=$(parse_frontmatter "$mode_file" "plan_mode_auto")
|
||||||
options+=("$name")
|
SPEC_DRIVEN=$(parse_frontmatter "$mode_file" "spec_driven")
|
||||||
|
ESCALATES_TO=$(parse_frontmatter "$mode_file" "escalates_to")
|
||||||
|
|
||||||
|
# Validate required frontmatter fields
|
||||||
|
for field_name in DRIVER TAG ASYNC_OK; do
|
||||||
|
if [[ -z "${!field_name}" ]]; then
|
||||||
|
err "Mode file $mode_file is missing required frontmatter field: $(echo "$field_name" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo ""
|
# Mode-specific interactive questions (skipped if preset)
|
||||||
read -rp "Choose profile [1]: " choice
|
if [[ -z "$PRESET" ]]; then
|
||||||
choice="${choice:-1}"
|
case "$MODE" in
|
||||||
|
deep|hybrid|orch)
|
||||||
# Validate choice
|
if [[ -z "${TIME_HORIZON:-}" ]]; then
|
||||||
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#options[@]} )); then
|
if [[ -t 0 ]]; then
|
||||||
echo "Invalid choice." >&2
|
read -rp "Time horizon (minutes/hours/overnight) [hours]: " th
|
||||||
exit 1
|
else
|
||||||
|
th=""
|
||||||
|
fi
|
||||||
|
TIME_HORIZON="${th:-hours}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
selected="${options[$((choice - 1))]}"
|
# Apply preset overrides for async/autoloop
|
||||||
|
[[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE"
|
||||||
|
[[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && {
|
||||||
|
if [[ "$AUTOLOOP_OVERRIDE" == "yes" || "$AUTOLOOP_OVERRIDE" == "no" ]]; then
|
||||||
|
: # leave AUTOLOOP as the mode default unless explicitly disabled
|
||||||
|
[[ "$AUTOLOOP_OVERRIDE" == "no" ]] && AUTOLOOP="none"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if [[ "$selected" == "default" ]]; then
|
# === Phase 3: Launch ===
|
||||||
echo "Using default profile ($DEFAULT_DIR)"
|
|
||||||
export CLAUDE_CONFIG_DIR="$DEFAULT_DIR"
|
MODEL_ID=$(driver_to_model "$DRIVER")
|
||||||
set_wezterm_profile "default"
|
|
||||||
else
|
# Validate context-load exists
|
||||||
target="$HOME/.claude-$selected"
|
[[ -x "$CONTEXT_LOADER" ]] || err "context-load not found at $CONTEXT_LOADER"
|
||||||
echo "Using profile: $selected ($target)"
|
|
||||||
export CLAUDE_CONFIG_DIR="$target"
|
# Build active-mode.env content
|
||||||
set_wezterm_profile "$selected"
|
active_env=$(cat <<EOF
|
||||||
|
CLAUDE_MODE=$MODE
|
||||||
|
CLAUDE_MODE_TAG=$TAG
|
||||||
|
CLAUDE_DRIVER=$DRIVER
|
||||||
|
CLAUDE_ESCALATES_TO=${ESCALATES_TO:-none}
|
||||||
|
CLAUDE_PROJECT=${PROJECT:-}
|
||||||
|
CLAUDE_TIME_HORIZON=${TIME_HORIZON:-}
|
||||||
|
CLAUDE_ASYNC_OK=$ASYNC_OK
|
||||||
|
CLAUDE_AUTOLOOP=$AUTOLOOP
|
||||||
|
CLAUDE_PLAN_MODE_AUTO=$PLAN_MODE_AUTO
|
||||||
|
CLAUDE_SPEC_DRIVEN=$SPEC_DRIVEN
|
||||||
|
CLAUDE_MODE_FILE=$mode_file
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mode body (everything after the frontmatter)
|
||||||
|
body=$(mode_body "$mode_file")
|
||||||
|
if [[ -z "$body" ]]; then
|
||||||
|
err "Mode file has no body — nothing to load into the system prompt: $mode_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load the context
|
# Pre-flight context-load (do this even in dryrun so length numbers are accurate,
|
||||||
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
# but suppress side effects — context-load is read-only).
|
||||||
|
context=$("$CONTEXT_LOADER")
|
||||||
|
|
||||||
if [[ ! -x "$CONTEXT_LOADER" ]]; then
|
combined_context="${context}
|
||||||
echo "Error: context-load not found at $CONTEXT_LOADER" >&2
|
|
||||||
exit 1
|
# === Engagement Mode: ${MODE} ===
|
||||||
|
|
||||||
|
${body}"
|
||||||
|
|
||||||
|
if (( DRYRUN )); then
|
||||||
|
dryrun_log "Profile: $PROFILE ($target)"
|
||||||
|
dryrun_log "Mode: $MODE"
|
||||||
|
dryrun_log "Mode file: $mode_file"
|
||||||
|
dryrun_log "Driver: $DRIVER"
|
||||||
|
dryrun_log "Driver model: $MODEL_ID"
|
||||||
|
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 "Autoloop: $AUTOLOOP"
|
||||||
|
dryrun_log "Escalates to: ${ESCALATES_TO:-none}"
|
||||||
|
dryrun_log "Plan mode auto: $PLAN_MODE_AUTO"
|
||||||
|
dryrun_log "Spec driven: $SPEC_DRIVEN"
|
||||||
|
dryrun_log "Would write: $target/active-mode.env"
|
||||||
|
dryrun_log "Would write: $target/last-mode"
|
||||||
|
if (( ${#CLAUDE_ARGS[@]} > 0 )); then
|
||||||
|
dryrun_log "Would execute: claude --model $MODEL_ID --append-system-prompt <context+mode> ${CLAUDE_ARGS[*]}"
|
||||||
|
else
|
||||||
|
dryrun_log "Would execute: claude --model $MODEL_ID --append-system-prompt <context+mode>"
|
||||||
|
fi
|
||||||
|
dryrun_log "Context length: ${#combined_context} chars (${#context} from context-load + ${#body} from mode body)"
|
||||||
|
dryrun_log "No changes made."
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
context="$("$CONTEXT_LOADER")"
|
# === Real launch from here on ===
|
||||||
|
|
||||||
if [[ -n "$context" ]]; then
|
# Set up WezTerm theming and reset trap
|
||||||
claude --append-system-prompt "$context" "$@"
|
trap reset_wezterm_profile EXIT
|
||||||
else
|
export CLAUDE_CONFIG_DIR="$target"
|
||||||
claude "$@"
|
set_wezterm_profile "$PROFILE"
|
||||||
fi
|
|
||||||
|
# Disable adaptive thinking (existing behaviour)
|
||||||
|
export CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1
|
||||||
|
|
||||||
|
# Write state files
|
||||||
|
printf '%s\n' "$active_env" > "$target/active-mode.env"
|
||||||
|
printf '%s\n' "$MODE" > "$target/last-mode"
|
||||||
|
|
||||||
|
echo "Launching: $PROFILE / $MODE${PROJECT:+ / $PROJECT}"
|
||||||
|
exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}"
|
||||||
|
|||||||
281
specs/claude-profile.spec.md
Normal file
281
specs/claude-profile.spec.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# claude-profile
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Launch Claude Code with a named configuration profile and an engagement mode. The profile selects which `~/.claude-*` config directory to use; the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance into a preset that the driver model adopts at session start.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
claude-profile [OPTIONS] [PROFILE] [-- CLAUDE_ARGS...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PROFILE` | (interactive picker) | Name of a profile under `~/.claude-<PROFILE>/`. Pass `default` to use `~/.claude/`. |
|
||||||
|
| `CLAUDE_ARGS` | (none) | Arguments after `--` are passed through to `claude` unchanged. |
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--mode <NAME>` | Skip the mode picker and use the named mode (`chat`, `quick`, `deep`, `hybrid`, `orch`). |
|
||||||
|
| `--preset <NAME>` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, and any preset-defined defaults in one shot. |
|
||||||
|
| `--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`. |
|
||||||
|
| `--help`, `-h` | Show usage. |
|
||||||
|
|
||||||
|
### Mutual exclusion
|
||||||
|
|
||||||
|
- `--preset` is mutually exclusive with `--mode` and `--project` — the preset defines all of those. Passing both is an error.
|
||||||
|
- A positional `PROFILE` combined with `--preset` is allowed only if the preset's `profile` field matches; mismatch is an error.
|
||||||
|
|
||||||
|
## Behaviour
|
||||||
|
|
||||||
|
The script runs in 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
|
||||||
|
|
||||||
|
1. If `--preset <name>` is given, read the preset from the global presets file (see "File contracts" below) and use its `profile` field. Skip the rest of phase 1.
|
||||||
|
2. Else if `PROFILE` is given as a positional argument, use it directly.
|
||||||
|
3. Else (interactive): list profiles by scanning `~/.claude-*` directories, display a numbered menu, prompt for selection. Default selection is `default` (`~/.claude/`).
|
||||||
|
4. Resolve the profile to a directory: `default` → `~/.claude/`; otherwise `~/.claude-<NAME>/`.
|
||||||
|
5. If the directory does not exist, error and exit 1.
|
||||||
|
6. Export `CLAUDE_CONFIG_DIR=<resolved>`.
|
||||||
|
7. Emit the WezTerm theme escape sequence for the profile (existing behaviour, unchanged).
|
||||||
|
|
||||||
|
### Phase 2 — Mode selection
|
||||||
|
|
||||||
|
1. If `--preset` is given, use the preset's `mode` and any other fields (project, time horizon, async, autoloop). Skip the rest of phase 2.
|
||||||
|
2. Else if `--mode <name>` is given, use it and proceed to mode-specific questions (project, async, etc., depending on the mode).
|
||||||
|
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.
|
||||||
|
5. Validate the chosen mode file exists at `<repo>/data/claude-profile/modes/<name>.md`. If not, error and exit 1.
|
||||||
|
6. Read the mode file's frontmatter to extract `driver`, `tag`, `async_ok`, `autoloop`, `plan_mode_auto`, `spec_driven`.
|
||||||
|
|
||||||
|
### Phase 3 — Launch
|
||||||
|
|
||||||
|
1. Write the resolved values to `$CLAUDE_CONFIG_DIR/active-mode.env` (key=value, one per line, see "File contracts").
|
||||||
|
2. Write the chosen mode name to `$CLAUDE_CONFIG_DIR/last-mode`.
|
||||||
|
3. Read the mode file's prose body (everything after the frontmatter) and append it to the context that `context-load` produces. The mode body becomes part of `--append-system-prompt`.
|
||||||
|
4. Set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` (existing behaviour).
|
||||||
|
5. Resolve the mode's `driver` frontmatter field through the **Driver mapping** table below to a concrete model ID.
|
||||||
|
6. Execute `claude --model "<resolved-model-id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`.
|
||||||
|
7. On exit, the existing WezTerm reset trap runs.
|
||||||
|
|
||||||
|
### Driver mapping
|
||||||
|
|
||||||
|
The `driver` field in mode frontmatter uses a logical name; the launcher maps it to the full Claude model ID before passing to `claude --model`. Full IDs are used (not aliases) to remove ambiguity about which model version is selected.
|
||||||
|
|
||||||
|
| Frontmatter `driver` | `--model` argument |
|
||||||
|
|---|---|
|
||||||
|
| `haiku` | `claude-haiku-4-5-20251001` |
|
||||||
|
| `sonnet` | `claude-sonnet-4-6` |
|
||||||
|
| `opus` | `claude-opus-4-6` |
|
||||||
|
| `opus-1m` | `claude-opus-4-6[1m]` |
|
||||||
|
|
||||||
|
If the frontmatter specifies an unknown driver, the launcher errors with the list of valid values and exits 1. The mapping table lives in the launcher script as a constant; updating it is the only change needed when new model versions are released.
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
| **Driver override?** (force a specific Claude model) | quick, deep | mode default | Overrides the `driver` field; resolved through the Driver mapping table and passed to `claude --model` |
|
||||||
|
| **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
|
||||||
|
|
||||||
|
### Mode files — `<repo>/data/claude-profile/modes/<name>.md`
|
||||||
|
|
||||||
|
YAML frontmatter (machine-readable) + markdown body (read by the driver model). Required frontmatter fields:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: <short name, matches filename>
|
||||||
|
tag: <status-line tag, ≤8 chars>
|
||||||
|
driver: <haiku|sonnet|opus|opus-1m>
|
||||||
|
async_ok: <yes|no>
|
||||||
|
autoloop: <none|"/loop 2m /orchestrate"|other slash command>
|
||||||
|
plan_mode_auto: <yes|no>
|
||||||
|
spec_driven: <yes|no>
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Active mode env — `$CLAUDE_CONFIG_DIR/active-mode.env`
|
||||||
|
|
||||||
|
Written on every launch, overwritten each time. Read by:
|
||||||
|
- The status-line script (to render the mode tag)
|
||||||
|
- CLAUDE.md session-start hook (to re-apply the mode after `/clear`)
|
||||||
|
|
||||||
|
```
|
||||||
|
CLAUDE_MODE=deep
|
||||||
|
CLAUDE_MODE_TAG=deep
|
||||||
|
CLAUDE_DRIVER=sonnet
|
||||||
|
CLAUDE_ESCALATES_TO=opus
|
||||||
|
CLAUDE_PROJECT=cluster-bootstrap
|
||||||
|
CLAUDE_TIME_HORIZON=hours
|
||||||
|
CLAUDE_ASYNC_OK=yes
|
||||||
|
CLAUDE_AUTOLOOP=none
|
||||||
|
CLAUDE_PLAN_MODE_AUTO=yes
|
||||||
|
CLAUDE_SPEC_DRIVEN=yes
|
||||||
|
CLAUDE_MODE_FILE=/home/paul/dev/claude/small-scripts/data/claude-profile/modes/deep.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Last mode — `$CLAUDE_CONFIG_DIR/last-mode`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
Per-profile, user-edited. Each preset is a named bundle of answers to the picker:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deep-cluster:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: deep
|
||||||
|
project: cluster-bootstrap
|
||||||
|
time_horizon: hours
|
||||||
|
async: yes
|
||||||
|
|
||||||
|
quick-skills:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: quick
|
||||||
|
project: custom-claude-skills
|
||||||
|
|
||||||
|
orch-overnight:
|
||||||
|
profile: oreillyit
|
||||||
|
mode: orch
|
||||||
|
project: ai-image-gen
|
||||||
|
time_horizon: overnight
|
||||||
|
autoloop: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
The `profile` field is required. Other fields default to the mode's defaults if omitted.
|
||||||
|
|
||||||
|
## Status line integration
|
||||||
|
|
||||||
|
The status-line script (`~/.claude/status/statusline.sh`) reads `$CLAUDE_CONFIG_DIR/active-mode.env` when `CLAUDE_CONFIG_DIR` is set. It uses `CLAUDE_DRIVER` and `CLAUDE_ESCALATES_TO` to build the model bracket, and `CLAUDE_MODE_TAG` to prefix the topic:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Sonnet→Opus] deep · cluster-bootstrap: M10 Gitea CI | 23% ctx
|
||||||
|
```
|
||||||
|
|
||||||
|
- `escalates_to: opus` → `[Sonnet→Opus]` (shows escalation path)
|
||||||
|
- `escalates_to: none` → `[Sonnet]` (no arrow)
|
||||||
|
- Mode tag is prepended to the topic with `·` separator
|
||||||
|
|
||||||
|
If `active-mode.env` is missing or `CLAUDE_CONFIG_DIR` is unset, the status line falls back to the full model display name from the session JSON with no mode tag.
|
||||||
|
|
||||||
|
## Dryrun behaviour
|
||||||
|
|
||||||
|
When `--dryrun` (or `-n`) is passed, claude-profile resolves all values exactly as it would in a real run, then prints them and exits 0 — no `claude` invocation, no file writes.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ claude-profile --dryrun --preset deep-cluster
|
||||||
|
[dryrun] Profile: oreillyit (~/.claude-oreillyit)
|
||||||
|
[dryrun] Mode: deep (Sonnet driver, Opus via named workflows)
|
||||||
|
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md
|
||||||
|
[dryrun] Project: cluster-bootstrap
|
||||||
|
[dryrun] Time horizon: hours
|
||||||
|
[dryrun] Async OK: yes
|
||||||
|
[dryrun] Autoloop: none
|
||||||
|
[dryrun] Would write: ~/.claude-oreillyit/active-mode.env
|
||||||
|
[dryrun] Would write: ~/.claude-oreillyit/last-mode
|
||||||
|
[dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "<context+mode>"
|
||||||
|
[dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode file)
|
||||||
|
[dryrun] No changes made.
|
||||||
|
```
|
||||||
|
|
||||||
|
In dryrun mode the interactive prompts are still shown, so the user can walk through the picker without committing.
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
|
||||||
|
| Case | Handling |
|
||||||
|
|---|---|
|
||||||
|
| `~/dev/claude/small-scripts/data/claude-profile/modes/` does not exist | Error: "Mode files directory not found at <path>. Reinstall claude-profile or check the repo location." Exit 1. |
|
||||||
|
| Mode file referenced by `--mode` does not exist | Error listing the available modes. Exit 1. |
|
||||||
|
| Mode file has malformed frontmatter (missing required field) | Error naming the missing field and the file path. Exit 1. |
|
||||||
|
| `presets.yaml` does not exist and `--preset` was given | Error: "No presets.yaml found at <path>." Exit 1. |
|
||||||
|
| `presets.yaml` exists but the named preset is not in it | Error listing available preset names. Exit 1. |
|
||||||
|
| Preset's `profile` field references a non-existent profile | Error. Exit 1. |
|
||||||
|
| `--preset` and `--mode` both given | Error: "Mutually exclusive: --preset already specifies a mode." 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. |
|
||||||
|
| 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. |
|
||||||
|
| `CLAUDE_CONFIG_DIR` already set in the environment when claude-profile starts | Ignored; claude-profile sets it from the resolved profile, overriding whatever was inherited. |
|
||||||
|
| Mode file body is empty (frontmatter only) | Error: "Mode file has no body — nothing to load into the system prompt." Exit 1. |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Interactive — full picker**
|
||||||
|
```
|
||||||
|
$ claude-profile
|
||||||
|
Claude Code profiles:
|
||||||
|
1) default (~/.claude)
|
||||||
|
2) oreillyit (~/.claude-oreillyit)
|
||||||
|
Choose profile [1]: 2
|
||||||
|
|
||||||
|
Engagement modes:
|
||||||
|
1) chat — No project, just conversation
|
||||||
|
2) quick — One small focused job in a known project
|
||||||
|
3) deep — Hard design or debugging problem
|
||||||
|
4) hybrid — Haiku driver, Opus on demand
|
||||||
|
5) orch — Decompose work and dispatch container agents
|
||||||
|
Choose mode [quick]: 3
|
||||||
|
|
||||||
|
Time horizon? [hours]:
|
||||||
|
Async OK? [yes]:
|
||||||
|
|
||||||
|
Launching: oreillyit / deep
|
||||||
|
[Claude Code starts; the existing CLAUDE.md project picker runs next]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Direct — preset**
|
||||||
|
```
|
||||||
|
$ claude-profile --preset deep-cluster
|
||||||
|
Launching: oreillyit / deep / cluster-bootstrap (from preset deep-cluster)
|
||||||
|
[Claude Code starts]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Direct — profile + mode flag**
|
||||||
|
```
|
||||||
|
$ claude-profile oreillyit --mode quick --project small-scripts
|
||||||
|
Launching: oreillyit / quick / small-scripts
|
||||||
|
[Claude Code starts]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dryrun — verify a preset**
|
||||||
|
```
|
||||||
|
$ claude-profile --dryrun --preset deep-cluster
|
||||||
|
[dryrun] ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pass-through to claude**
|
||||||
|
```
|
||||||
|
$ claude-profile --mode quick --project small-scripts -- --resume
|
||||||
|
[passes --resume to claude]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Out of scope (for v1)
|
||||||
|
|
||||||
|
- Multi-session coordination (parallel claude-profile launches against the same profile)
|
||||||
|
- Mid-session mode switching from inside Claude Code (handled by a separate `switch mode` skill, not claude-profile itself)
|
||||||
|
- Editing mode files in place (mode files are read-only data; customisation happens via `presets.yaml`)
|
||||||
|
- Network/remote profiles (everything is local)
|
||||||
|
- Mid-session driver model swaps (the harness pins the model at launch via `--model`; switching mid-session requires relaunching)
|
||||||
|
- Pinning the model used by named custom subagents at the harness level (those are controlled by the subagent's own definition file under `subagents[name].model`; the Agent tool's per-call `model` parameter is unaffected and remains how mode prose instructs the driver to escalate)
|
||||||
162
tests/test-claude-profile.sh
Executable file
162
tests/test-claude-profile.sh
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test-claude-profile.sh — Verify claude-profile dryrun behaviour against the spec.
|
||||||
|
#
|
||||||
|
# Strategy: every assertion runs claude-profile --dryrun, so nothing is launched
|
||||||
|
# and no real profile state is touched. For preset testing, a fake profile
|
||||||
|
# directory is created under a tempdir-based HOME override.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
CLAUDE_PROFILE="$REPO_ROOT/scripts/claude-profile"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local label=$1 needle=$2 haystack=$3
|
||||||
|
if [[ "$haystack" == *"$needle"* ]]; then
|
||||||
|
printf " ${GREEN}PASS${NC} %s\n" "$label"
|
||||||
|
((PASS++))
|
||||||
|
else
|
||||||
|
printf " ${RED}FAIL${NC} %s\n" "$label"
|
||||||
|
printf " expected to contain: %s\n" "$needle"
|
||||||
|
printf " actual: %s\n" "$haystack"
|
||||||
|
((FAIL++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_exit_code() {
|
||||||
|
local label=$1 expected=$2 actual=$3
|
||||||
|
if [[ "$expected" == "$actual" ]]; then
|
||||||
|
printf " ${GREEN}PASS${NC} %s (exit=%s)\n" "$label" "$actual"
|
||||||
|
((PASS++))
|
||||||
|
else
|
||||||
|
printf " ${RED}FAIL${NC} %s (expected exit=%s, got exit=%s)\n" "$label" "$expected" "$actual"
|
||||||
|
((FAIL++))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
"$CLAUDE_PROFILE" "$@" </dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Pre-flight ===
|
||||||
|
|
||||||
|
if [[ ! -x "$CLAUDE_PROFILE" ]]; then
|
||||||
|
echo "claude-profile not found or not executable at $CLAUDE_PROFILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === Test 1: --help ===
|
||||||
|
echo "Test 1: --help"
|
||||||
|
out=$(run --help)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "--help exits 0" 0 "$rc"
|
||||||
|
assert_contains "--help shows usage" "claude-profile" "$out"
|
||||||
|
assert_contains "--help shows --mode" "--mode" "$out"
|
||||||
|
assert_contains "--help shows --preset" "--preset" "$out"
|
||||||
|
|
||||||
|
# === Test 2: chat dryrun ===
|
||||||
|
echo
|
||||||
|
echo "Test 2: chat mode dryrun"
|
||||||
|
out=$(run --dryrun oreillyit --mode chat)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "chat dryrun exits 0" 0 "$rc"
|
||||||
|
assert_contains "chat shows haiku driver" "Driver: haiku" "$out"
|
||||||
|
assert_contains "chat shows full haiku model ID" "claude-haiku-4-5-20251001" "$out"
|
||||||
|
assert_contains "chat shows mode tag" "Mode: chat" "$out"
|
||||||
|
assert_contains "chat says no changes" "No changes made" "$out"
|
||||||
|
assert_contains "chat does not ask time horizon" "<not asked in this mode>" "$out"
|
||||||
|
|
||||||
|
# === Test 3: deep dryrun with project flag ===
|
||||||
|
echo
|
||||||
|
echo "Test 3: deep mode dryrun"
|
||||||
|
out=$(run --dryrun oreillyit --mode deep --project cluster-bootstrap)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "deep dryrun exits 0" 0 "$rc"
|
||||||
|
assert_contains "deep shows sonnet driver" "Driver: sonnet" "$out"
|
||||||
|
assert_contains "deep shows full sonnet model ID" "claude-sonnet-4-6" "$out"
|
||||||
|
assert_contains "deep shows --model in command" "claude --model claude-sonnet-4-6" "$out"
|
||||||
|
assert_contains "deep records the project" "Project: cluster-bootstrap" "$out"
|
||||||
|
assert_contains "deep defaults time horizon to hours" "Time horizon: hours" "$out"
|
||||||
|
assert_contains "deep says async OK" "Async OK: yes" "$out"
|
||||||
|
assert_contains "deep says plan_mode_auto yes" "Plan mode auto: yes" "$out"
|
||||||
|
assert_contains "deep says spec_driven yes" "Spec driven: yes" "$out"
|
||||||
|
|
||||||
|
# === Test 4: orch dryrun preserves autoloop ===
|
||||||
|
echo
|
||||||
|
echo "Test 4: orch mode dryrun (autoloop preserved)"
|
||||||
|
out=$(run --dryrun oreillyit --mode orch --project ai-image-gen)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "orch dryrun exits 0" 0 "$rc"
|
||||||
|
assert_contains "orch preserves the slash command in autoloop" "/loop 2m /orchestrate" "$out"
|
||||||
|
assert_contains "orch driver is sonnet" "Driver: sonnet" "$out"
|
||||||
|
|
||||||
|
# === Test 5: hybrid dryrun ===
|
||||||
|
echo
|
||||||
|
echo "Test 5: hybrid mode dryrun"
|
||||||
|
out=$(run --dryrun oreillyit --mode hybrid)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "hybrid dryrun exits 0" 0 "$rc"
|
||||||
|
assert_contains "hybrid uses haiku driver" "Driver: haiku" "$out"
|
||||||
|
assert_contains "hybrid allows async" "Async OK: yes" "$out"
|
||||||
|
|
||||||
|
# === Test 6: quick dryrun (no time horizon question) ===
|
||||||
|
echo
|
||||||
|
echo "Test 6: quick mode dryrun"
|
||||||
|
out=$(run --dryrun oreillyit --mode quick)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "quick dryrun exits 0" 0 "$rc"
|
||||||
|
assert_contains "quick uses sonnet driver" "Driver: sonnet" "$out"
|
||||||
|
assert_contains "quick disables async" "Async OK: no" "$out"
|
||||||
|
assert_contains "quick does not ask time horizon" "<not asked in this mode>" "$out"
|
||||||
|
|
||||||
|
# === Test 7: error — unknown mode ===
|
||||||
|
echo
|
||||||
|
echo "Test 7: error path — unknown mode"
|
||||||
|
out=$(run --dryrun oreillyit --mode bogus)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "unknown mode exits 1" 1 "$rc"
|
||||||
|
assert_contains "unknown mode error message" "Mode file not found" "$out"
|
||||||
|
|
||||||
|
# === Test 8: error — --preset and --mode together ===
|
||||||
|
echo
|
||||||
|
echo "Test 8: error path — --preset + --mode mutual exclusion"
|
||||||
|
out=$(run --dryrun --preset whatever --mode quick)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "preset+mode exits 1" 1 "$rc"
|
||||||
|
assert_contains "preset+mode error message" "mutually exclusive" "$out"
|
||||||
|
|
||||||
|
# === Test 9: error — preset not found ===
|
||||||
|
echo
|
||||||
|
echo "Test 9: error path — preset not found"
|
||||||
|
out=$(run --dryrun --preset definitely-not-a-real-preset-name)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "missing preset exits 1" 1 "$rc"
|
||||||
|
assert_contains "missing preset error message" "not found" "$out"
|
||||||
|
|
||||||
|
# === Test 10: pass-through args via -- ===
|
||||||
|
echo
|
||||||
|
echo "Test 10: pass-through args"
|
||||||
|
out=$(run --dryrun oreillyit --mode chat -- --resume some-session)
|
||||||
|
rc=$?
|
||||||
|
assert_exit_code "pass-through exits 0" 0 "$rc"
|
||||||
|
assert_contains "pass-through preserves --resume" "--resume some-session" "$out"
|
||||||
|
|
||||||
|
# === Summary ===
|
||||||
|
echo
|
||||||
|
echo "================================"
|
||||||
|
TOTAL=$((PASS + FAIL))
|
||||||
|
if (( FAIL == 0 )); then
|
||||||
|
printf "${GREEN}All %d tests passed${NC}\n" "$TOTAL"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
printf "${RED}%d of %d tests failed${NC}\n" "$FAIL" "$TOTAL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user