voyage/docs/profiles.md

229 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Profile system — voyage v4.1
This document describes the model profile system: built-in tiers,
lookup precedence, custom-profile authoring, drift detection, and
cost estimation (with disclaimer).
## Built-in profiles
Four pre-defined tiers ship with the plugin (fable added in v5.9), located at
`lib/profiles/{economy,balanced,premium,fable}.yaml`.
| Profile | Brief | Research | Plan | Execute | Review | Continue | Use case |
|---------|-------|----------|------|---------|--------|----------|----------|
| `economy` | sonnet | sonnet | sonnet | sonnet | sonnet | sonnet | ⚠ **Experimental** (uncalibrated Jaccard floor) — lowest cost; small-scope tasks where you have high confidence the brief is right |
| `balanced` | sonnet | sonnet | opus | sonnet | opus | sonnet | Mixed — opus where reasoning depth pays off (plan synthesis + adversarial review); opt-in via `--profile balanced` |
| `premium` (default) | opus | opus | opus | opus | opus | opus | Maximum quality — Opus on every phase + external research on (default since the 2026-05-13 operator decision) |
| `fable` | fable | fable | fable | fable | fable | fable | Max quality — Fable 5 (Mythos-class, above Opus) on every phase; opt-in via `--profile fable`; reasoning effort inherits from the session (see Model & effort axes) |
`premium` is the default tier — set by the 2026-05-13 operator decision and
matched by the hardcoded resolver default in `lib/profiles/resolver.mjs`. It
runs opus on every phase and turns external research on: maximum quality, at
roughly 5× the sub-agent cost of an all-sonnet run, accepted as a deliberate
trade-off. Drop to `--profile balanced` (opus only on the two phases where
quality matters most — Plan synthesis + Review — and sonnet everywhere else)
or `--profile economy` (sonnet everywhere) when cost or latency matters more
than depth. Step up to `--profile fable` (Fable 5 on every phase) when
maximum quality is wanted end-to-end and cost is not a constraint.
`economy` is *strictly experimental* in v4.1, and says so in the profile
data itself: `lib/profiles/economy.yaml` carries `experimental: true`. The
cross-tier Jaccard floor (0.55) is grounded in parked-synthetic fixtures, not
empirical runs (Step 17 calibration was deferred — see
`tests/synthetic/profile-jaccard-calibration.md`). The flag is pinned: it must
stay `true` while the calibration status is `parked-synthetic`, and must be
dropped in the same change that lands empirical calibration
(`status: empirical`). If you observe economy-plan quality regressions, fall
back to `balanced`.
## Model & effort axes
`opus`, `sonnet`, and `fable` are model **aliases**, not pinned ids. As of
Claude Code 2.1.154 the `opus` alias resolves to **Opus 4.8**, whose default
reasoning effort is **`high`**; `sonnet` resolves to Sonnet 4.6; `fable`
resolves to **Fable 5** (Mythos-class, positioned above Opus), whose default
reasoning effort is also `high`. The profile table above selects *which
alias* runs each phase — it does not touch reasoning effort.
**Reasoning effort inherits from the session.** Voyage effort (orchestration
shape — which agents/passes run) and model reasoning effort are different
axes. Fable 5's default reasoning effort is `high`, NOT xhigh, and switching
model resets effort to the model default — xhigh does not follow the model.
To run the fable tier at xhigh, set it at session level: `/effort xhigh`, the
`effortLevel` setting, or `CLAUDE_CODE_EFFORT_LEVEL`.
Two different things share the word "effort" in Voyage. They are **orthogonal
axes** — same name, different mechanism:
| | **Orchestration effort** | **Reasoning effort (native)** |
|---|---|---|
| Where | brief `phase_signals.effort`, consumed by command prose | native Claude Code `effort:` in agent frontmatter |
| Values | `low` / `standard` / `high` | `low` / `medium` / `high` / `xhigh` / `max` |
| Controls | *which agents/passes/gates run* (swarm cardinality, gate strictness, sequential-vs-parallel) | *per-spawn reasoning-token budget* the harness gives one agent |
| `low` means | "run fewer agents" (the `--quick`-equivalent code-path) | "think with a smaller budget" |
| Applied by | Voyage command logic | the harness, at spawn time |
The `phase-signal-resolver.mjs` helper only reads the **orchestration** axis
(`phase_signals.effort`, gated against `low/standard/high`) plus the optional
per-phase `model` (gated against `['sonnet','opus','fable']`). It never emits
native `effort:`.
**Native `effort:` on agents.** Voyage sets the reasoning axis statically on
selected agents, additively over the Opus-4.8 default:
- **Retrieval agents → `medium`:** `task-finder`, `git-historian`,
`dependency-tracer`, `architecture-mapper` (structured discovery, not deep
multi-step reasoning).
- **Adversarial-reasoning agents → `high`:** `plan-critic`, `risk-assessor`,
`contrarian-researcher`, `review-coordinator` (synthesis and stress-testing
where reasoning depth pays off).
- **All other agents:** unset → inherit the model default (Opus 4.8 = `high`).
Native-effort precedence (harness): env `CLAUDE_CODE_EFFORT_LEVEL` > frontmatter
`effort:` > session setting > model default. `MAX_THINKING_TOKENS=0` (or
`--thinking disabled`) overrides effort entirely. Enterprise `availableModels`
constrains the *model* alias only — it does **not** bound effort.
## Decision tree
```
Are you uncertain whether the brief is correctly framed?
└── Yes → premium (opus on brief + plan + review)
└── No → continue
Is the change small (≤ 5 steps in the plan)?
└── Yes → economy (sonnet everywhere)
└── No → balanced (opus on plan + review)
Special cases:
- Critical-infrastructure plan → premium
- Migration with rollback risk → premium
- Research-heavy task (≥ 4 dimensions) → balanced (research-stage benefits)
- Bug fix with clear reproducer → economy
- Documentation-only PR → economy
```
## Lookup order
Voyage resolves the profile in this priority order:
1. **Explicit `--profile <name>` flag** — passed to the command
2. **Plan-file frontmatter `profile:`** — when resuming via
`/trekexecute --resume` or `/trekcontinue`
3. **`VOYAGE_PROFILE` environment variable** — useful for headless CI
4. **Default `premium`** — final fallback
The resolved value is recorded in two places:
- Plan-file frontmatter `profile: <name>` and `phase_models: [...]`
- Stats stream `${CLAUDE_PLUGIN_DATA}/trek*-stats.jsonl`
`profile`, `profile_source`, `phase_models`, `model_used`,
`phase_models_resolved` fields
`profile_source` distinguishes how the profile was resolved (`flag` /
`plan_frontmatter` / `env` / `default`), so dashboards can surface
unexpected env-var inheritance in CI.
## Custom profiles
Drop a YAML file at `lib/profiles/<name>.yaml` to define a new tier.
The validator (`lib/validators/profile-validator.mjs`) enforces:
- Every `phase_models[].phase` must be a known phase enum:
`brief` / `research` / `plan` / `execute` / `review` / `continue`
- Every `phase_models[].model` must exactly match an entry in
`BASE_ALLOWED_MODELS` (`['sonnet', 'opus', 'fable']` in
`lib/validators/profile-validator.mjs`; `haiku` only with
`VOYAGE_ALLOW_HAIKU=1`)
- All six phases must be present (no partial profiles)
The four built-in names (`economy`, `balanced`, `premium`, `fable`) resolve to their
bundled yaml first — `findProfilePath()` returns the built-in before consulting
`voyage-profiles/`, so a same-named custom file is ignored and cannot shadow a
built-in. To customize, give your profile a new name and reference it via
`--profile <new-name>` or `VOYAGE_PROFILE=<new-name>`.
### Example custom profile
```yaml
# lib/profiles/critical.yaml — opus everywhere except continue
phase_models:
- phase: brief
model: opus
- phase: research
model: opus
- phase: plan
model: opus
- phase: execute
model: opus
- phase: review
model: opus
- phase: continue
model: sonnet
```
Validate with: `node lib/validators/profile-validator.mjs --json lib/profiles/critical.yaml`
## Drift detection
In `--strict` mode, `plan-validator.mjs` emits a `MANIFEST_PROFILE_DRIFT`
warning when the plan-level `profile:` differs from any step manifest's
`profile_used`. The warning is a *signal*, not a failure — the plan
remains `valid: true`. This catches:
- Manual edits where an operator changed a single step's profile
- Resume from a partial run where the previous session used a different
tier
- Copy-paste errors when stitching plan fragments
To suppress the warning intentionally (e.g. when a critical step
genuinely needs a higher tier), document the override in the step's
prose and re-run with `--soft` to validate without strict-mode warnings.
## Cost estimation
> **Disclaimer:** the table below is an *anslag*, not a contractual
> SLA. Real cost depends on context size, agent-swarm cardinality,
> tool-use density, and Claude Code billing schedule. Treat these as
> rough order-of-magnitude.
| Profile | Brief | Research | Plan | Execute | Review | Total |
|---------|-------|----------|------|---------|--------|-------|
| `economy` | $0.100.50 | $0.502.00 | $0.502.00 | $1.005.00 | $0.201.00 | **$210** |
| `balanced` | $0.100.50 | $0.502.00 | $1.004.00 | $1.005.00 | $0.502.00 | **$314** |
| `premium` | $0.502.00 | $0.502.00 | $1.004.00 | $1.005.00 | $0.502.00 | **$415** |
Numbers are per *full pipeline run* (brief + research + plan +
execute + review) on a moderate-complexity task. Numbers scale roughly
linearly with the size of the resulting plan (10 steps ≈ baseline; 30
steps ≈ 3× the execute column).
Per-profile actuals are emitted to JSONL stats — pipe them through the
OTel export (`docs/observability.md`) to get real cost-attribution
graphs in Grafana. Replace the table above with your own measured
numbers after ≥ 3 runs of each profile.
## Deferred to v4.2
- **`balanced.external_research_enabled` operator-override** —
v4.1 omits this per scope-guardian SG2. v4.2 may add an opt-in
flag to enable external research agents in the balanced tier
without forcing premium.
- **Empirical Jaccard re-calibration** — parked-synthetic fixtures
in v4.1 use a 0.55 conservative starting threshold. v4.2 plans an
empirical re-run with $60-120 LLM budget to derive a calibrated
threshold from real economy-vs-premium plan pairs.
- **ROUGE-L + char-4gram MinHash** as primary/secondary cross-tier
gates per research/02 Recommendation #7. Jaccard remains the gate
in v4.1; v4.2 may layer ROUGE-L on top.
## See also
- [`README.md` § Profile system](../README.md) — top-level overview
- [`CLAUDE.md` § Profile system](../CLAUDE.md) — internal reference
- [`docs/observability.md`](observability.md) — JSONL → OTel pipeline
- [`tests/synthetic/profile-jaccard-calibration.md`](../tests/synthetic/profile-jaccard-calibration.md)
— calibration status and threshold rationale
- [`lib/profiles/`](../lib/profiles/) — built-in profile YAMLs
- [`lib/validators/profile-validator.mjs`](../lib/validators/profile-validator.mjs)
— schema validator with CLI shim