- 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>
282 lines
14 KiB
Markdown
282 lines
14 KiB
Markdown
# 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)
|