diff --git a/.squad/agents/eecom/history.md b/.squad/agents/eecom/history.md index b4007c402..7fce94c9c 100644 --- a/.squad/agents/eecom/history.md +++ b/.squad/agents/eecom/history.md @@ -103,3 +103,22 @@ CLI completeness audit (2026-03-08) confirmed: 26 primary commands routed in cli Implemented Flight's privacy-first adoption monitoring strategy on PR #326 branch. Moved `.squad/adoption/` β†’ `.github/adoption/` for better GitHub integration. Stripped tracking.md to aggregate-only metrics (removed all individual repo names/URLs). Updated GitHub Action workflow (adoption-report.yml) and monitoring script (scripts/adoption-monitor.mjs) to write reports to `.github/adoption/reports/`. Removed "Built with Squad" showcase link from README.md (deferred to Tier 2 opt-in feature). This honors the principle: collect aggregate metrics via public APIs, but never publish individual repo lists without explicit consent. Test discipline: verified npm run build passes; docs-build.test.ts passed structure tests (Astro build failure unrelated to changes). Committed with clear message explaining privacy rationale. πŸ“Œ **Team update (2026-03-10T12-55-49Z):** Adoption tracking Tier 1 complete and merged to decisions.md. Privacy-first architecture confirmed: aggregate metrics only, opt-in for individual repos, public showcase only when 5+ projects opt in. Append-only file governance enforced (no deletions in history.md or decisions.md). Microsoft ampersand style guide adopted for documentation. + +### Economy Mode Implementation (#500) (2026-03-20) + +**Context:** Issue #500 requested economy mode β€” a session-level and persistent modifier that shifts model selection to cheaper alternatives. + +**Architecture decision:** Economy mode is a Layer 3/4 modifier only. Layers 0–2 (explicit user preferences: config.json, session directive, charter) are never downgraded. This preserves user intent while enabling cost savings on auto-selected tasks. + +**Implementation:** +1. `ECONOMY_MODEL_MAP` + `applyEconomyMode()` in `config/models.ts` β€” pure mapping function for premiumβ†’standard and standardβ†’fast downgrades +2. `readEconomyMode()` + `writeEconomyMode()` β€” config.json read/write functions (same merge-without-clobber pattern as `writeModelPreference()`) +3. `resolveModel()` in `config/models.ts` updated with `economyMode?: boolean` option; falls back to reading from `squadDir` if not provided +4. `resolveModel()` in `agents/model-selector.ts` updated with `economyMode?: boolean` β€” both SDK resolvers are economy-aware +5. `squad economy [on|off]` command in CLI for persistent toggle +6. `--economy` global flag in `cli-entry.ts` sets `SQUAD_ECONOMY_MODE=1` env var for session scope +7. 34 new tests in `test/economy-mode.test.ts` β€” all pass + +**Key pattern:** Both resolveModel implementations follow identical principle: explicit overrides (user choice) are sacred; economy only affects computed auto-selection. + +**PR:** #500 branch `squad/500-economy-mode` diff --git a/.squad/agents/procedures/history.md b/.squad/agents/procedures/history.md index 2390dfc70..ea52095bf 100644 --- a/.squad/agents/procedures/history.md +++ b/.squad/agents/procedures/history.md @@ -80,3 +80,25 @@ **Pattern:** NEVER/ALWAYS sections in charters compress well β€” fold bullet lists into single-paragraph summaries. Essential workflow details (Scribe's commit steps) should stay verbose. +### 2026-03-22: Economy mode skill and personal squad governance (#500, #344) + +**Task:** Two governance tasks β€” economy mode skill design and personal squad coordinator awareness. + +**Economy mode (SKILL.md):** +- Created `.squad/skills/economy-mode/SKILL.md` as a Layer 3 modifier, not a new resolution layer +- Key design decision: economy mode ONLY affects Layer 3 auto-selection β€” Layer 0/1/2 (user intent) always wins +- `πŸ’°` indicator in spawn acknowledgments keeps it transparent +- Activation via session phrase, persistent config (`economyMode: true` in config.json), or CLI flag +- Architecture trips shift from opus β†’ sonnet; code tasks shift from sonnet β†’ gpt-4.1/gpt-5-mini +- Confidence: `low` β€” first implementation, not yet validated + +**Personal squad governance (proposals):** +- Gap analysis: coordinator has no consult mode awareness despite full SDK implementation +- Five gaps identified: Init Mode missing personal squad resolution, no consult mode detection, TEAM_ROOT has no personal-squad semantics, charter templates lack consult-mode patterns, no consult-mode skill +- Proposed `CONSULT_MODE: true` as spawn prompt signal, `🧳 consult` in acknowledgments +- Proposed new consult-mode skill (after governance approval β€” skill after governance, not before) + +**Governance workflow pattern:** When proposals touch squad.agent.md (governance territory), write to `decisions/inbox/` for Flight review. Don't directly edit squad.agent.md β€” Flight reviews governance changes. + +**Catalog audit finding:** `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex` appear in model-selection SKILL.md fallback chains but are absent from squad.agent.md's "Valid models" catalog. Documented in economy-mode governance proposal for Flight to address. + diff --git a/.squad/decisions/inbox/eecom-economy-mode.md b/.squad/decisions/inbox/eecom-economy-mode.md new file mode 100644 index 000000000..d534aa5d6 --- /dev/null +++ b/.squad/decisions/inbox/eecom-economy-mode.md @@ -0,0 +1,37 @@ +# Economy Mode Design β€” #500 + +**Date:** 2026-03-20 +**Author:** EECOM +**Issue:** #500 + +## Decision + +Economy mode is implemented as a modifier that shifts model selection at Layer 3 (task-aware auto) and Layer 4 (default fallback) only. Layers 0–2 (explicit user preferences) are never downgraded. + +## Model Map + +| Normal | Economy | Use case | +|--------|---------|----------| +| `claude-opus-4.6` | `claude-sonnet-4.5` | Architecture, review | +| `claude-sonnet-4.6` | `gpt-4.1` | Code writing | +| `claude-sonnet-4.5` | `gpt-4.1` | Code writing | +| `claude-haiku-4.5` | `gpt-4.1` | Docs, planning, mechanical | + +## Activation + +1. **Persistent:** `"economyMode": true` in `.squad/config.json` (survives sessions) +2. **Session:** `--economy` CLI flag (sets `SQUAD_ECONOMY_MODE=1` env var, current session only) +3. **Toggle command:** `squad economy on|off` writes to config.json + +## Hierarchy Integration + +Economy mode is a Layer 3/4 modifier β€” it does NOT override explicit preferences (Layers 0–2). This is intentional: if a user said "always use opus", economy mode respects that choice. + +## Implementation Points + +- `ECONOMY_MODEL_MAP` + `applyEconomyMode()` in `packages/squad-sdk/src/config/models.ts` +- `readEconomyMode()` + `writeEconomyMode()` in `packages/squad-sdk/src/config/models.ts` +- `resolveModel()` in `config/models.ts` accepts `economyMode?: boolean` option; reads from config if not provided +- `resolveModel()` in `agents/model-selector.ts` also supports `economyMode?: boolean` +- `squad economy [on|off]` command in `packages/squad-cli/src/cli/commands/economy.ts` +- `--economy` global flag in `cli-entry.ts` diff --git a/.squad/decisions/inbox/procedures-economy-mode-governance.md b/.squad/decisions/inbox/procedures-economy-mode-governance.md new file mode 100644 index 000000000..4561be940 --- /dev/null +++ b/.squad/decisions/inbox/procedures-economy-mode-governance.md @@ -0,0 +1,126 @@ +# Proposal: Economy Mode Integration in squad.agent.md + +**By:** Procedures (Prompt Engineer) +**Date:** 2026-03-22 +**Issues:** #500 +**Status:** DRAFT β€” for Flight review before merging to squad.agent.md + +--- + +## Summary + +Economy mode is a new session/persistent modifier that shifts Layer 3 (Task-Aware Auto-Selection) to cost-optimized alternatives. This proposal documents the governance additions needed in `squad.agent.md`. + +**Note to Flight:** Procedures owns the skill design. Squad.agent.md is governance β€” Flight reviews before commit. + +--- + +## 1. New Paragraph After Layer 0 (Per-Agent Model Selection section) + +Insert after the existing Layer 0 bullet points and before "**Layer 1 β€” Session Directive**": + +--- + +**Economy Mode β€” Cost Modifier (Layer 3 override):** Economy mode shifts all Layer 3 auto-selection to cost-optimized alternatives. It does NOT override Layer 0 (persistent config), Layer 1 (explicit session directive), or Layer 2 (charter preference) β€” user intent always wins. + +- **Activation (session):** User says "use economy mode", "save costs", "go cheap" β†’ activate for this session only. +- **Activation (persistent):** User says "always use economy mode" OR `"economyMode": true` in `.squad/config.json` β†’ persists across sessions. +- **Deactivation:** "turn off economy mode" or remove `economyMode` from `config.json`. +- **On session start:** Read `.squad/config.json`. If `economyMode: true`, activate economy mode before any spawns. + +--- + +## 2. Economy Model Selection Table + +Add after Layer 3 normal table: + +--- + +**Economy Mode Layer 3 Table** (active when economy mode is on): + +| Task Output | Normal Mode | Economy Mode | +|-------------|-------------|--------------| +| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | +| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | + +Prefer `gpt-4.1` over `gpt-5-mini` for structured output or tool use. Prefer `gpt-5-mini` for pure text generation. + +--- + +## 3. Spawn Acknowledgment Convention + +Add to the spawn acknowledgment format guidance: + +--- + +When economy mode is active, include `πŸ’° economy` after the model name in spawn acknowledgments: + +``` +πŸ”§ Fenster (gpt-4.1 Β· πŸ’° economy) β€” fixing auth bug +πŸ“‹ Scribe (gpt-4.1 Β· πŸ’° economy) β€” logging decision +``` + +This gives the user instant visibility that cost-optimized models are in use. + +--- + +## 4. Valid Models Catalog Audit + +Current "Valid models" section lists: + +``` +Premium: claude-opus-4.6, claude-opus-4.6-fast, claude-opus-4.5 +Standard: claude-sonnet-4.5, claude-sonnet-4, gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1, gpt-5, gemini-3-pro-preview +Fast/Cheap: claude-haiku-4.5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 +``` + +**Audit findings:** +- `claude-opus-4.6` and `claude-opus-4.6-fast` are listed but not used in the Layer 3 table (table uses `claude-opus-4.5`). The Layer 3 table should reference `claude-opus-4.6` as the premium default for consistency with the catalog. +- `claude-sonnet-4.6` appears in the model-selection SKILL.md but is absent from the valid models list in squad.agent.md. Add it under Standard. +- Economy mode introduces `gpt-4.1` and `gpt-5-mini` as primary alternatives β€” both are already in the Fast/Cheap catalog. No additions needed. + +**Proposed updated catalog:** + +``` +Premium: claude-opus-4.6, claude-opus-4.6-fast, claude-opus-4.5 +Standard: claude-sonnet-4.6, claude-sonnet-4.5, claude-sonnet-4, gpt-5.4, gpt-5.3-codex, gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1, gpt-5, gemini-3-pro-preview +Fast/Cheap: claude-haiku-4.5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 +``` + +(Added `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex` which appear in the model-selection SKILL.md fallback chains but are missing from squad.agent.md's catalog.) + +--- + +## 5. Config Schema Addition + +Add `economyMode` to the config schema reference in squad.agent.md (wherever `defaultModel` is documented): + +```json +{ + "version": 1, + "defaultModel": "claude-sonnet-4.6", + "economyMode": true, + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6" + } +} +``` + +--- + +## Rationale + +Economy mode solves a real user need: "I want all agents to run cheaper, but I don't want to set each one individually." It's a session-level modifier that works orthogonally to the existing hierarchy β€” no layer gets changed, only Layer 3's lookup table swaps. The `πŸ’°` indicator keeps it transparent. + +The skill (`economy-mode/SKILL.md`) covers the coordinator behavior in detail. This proposal is the governance side β€” ensuring squad.agent.md is the authoritative source for the feature. + +--- + +## References + +- Skill: `.squad/skills/economy-mode/SKILL.md` +- Issue: #500 +- Model selection skill: `.squad/skills/model-selection/SKILL.md` diff --git a/.squad/decisions/inbox/procedures-personal-squad-governance.md b/.squad/decisions/inbox/procedures-personal-squad-governance.md new file mode 100644 index 000000000..b46bb17de --- /dev/null +++ b/.squad/decisions/inbox/procedures-personal-squad-governance.md @@ -0,0 +1,158 @@ +# Proposal: Personal Squad Governance Awareness in squad.agent.md + +**By:** Procedures (Prompt Engineer) +**Date:** 2026-03-22 +**Issues:** #344 +**Status:** DRAFT β€” for Flight review before merging to squad.agent.md + +--- + +## Summary + +Squad has a consult mode (implemented, per `prd-consult-mode.md`) and personal squad semantics (via `resolveGlobalSquadPath()`), but `squad.agent.md` doesn't tell the coordinator how to reason about either. This proposal documents the gaps and the governance additions needed. + +--- + +## Gap Analysis + +### Gap 1: Init Mode references `--global` without explaining personal squad resolution + +Current Init Mode says "run `squad init --global` for a personal squad" (implied by CLI docs) but squad.agent.md doesn't explain what a personal squad IS or how the coordinator should detect it. + +**What agents need to know:** +- Personal squad = a squad at the global path (resolved via `resolveGlobalSquadPath()`) + - Linux/macOS: `~/.config/squad/.squad` + - macOS (alt): `~/Library/Application Support/squad/.squad` + - Windows: `%APPDATA%\squad\.squad` +- If `.squad/config.json` contains `"consult": true`, the coordinator is working inside a consult session +- `sourceSquad` in `config.json` points to the original personal squad (for Scribe extraction context) + +### Gap 2: No coordinator guidance for consult mode + +`squad.agent.md` mentions nothing about consult mode. The coordinator doesn't know: +- How to recognize it's in a consult session +- That writes go to the project `.squad/` (isolated copy) β€” NOT the personal squad +- That Scribe's charter is patched with extraction instructions +- That `.squad/extract/` is a staging area for generic learnings + +### Gap 3: TEAM_ROOT works, but personal squad semantics are absent + +The coordinator resolves `TEAM_ROOT` correctly (Worktree Awareness section), but: +- No distinction between "project squad" vs "personal squad copy in consult mode" +- No guidance on what to tell agents about their squad context when in consult mode + +### Gap 4: Charter templates have no personal-squad-aware patterns + +Agent charters have no concept of: +- Consult mode restrictions (agents shouldn't commit to project, shouldn't pollute personal squad) +- Extraction tagging (Scribe needs to flag decisions as generic vs project-specific) + +### Gap 5: No skill for consult-mode behavior + +There is no skill for consult-mode coordinator behavior, even though consult mode has distinct patterns (invisibility, extraction, isolation). + +--- + +## Proposed squad.agent.md Additions + +### Addition 1: Consult Mode Detection (in Team Mode β†’ On Session Start) + +After "resolve the team root" and before Issue Awareness, add: + +--- + +**Consult Mode Detection:** After resolving team root, check `.squad/config.json` for `"consult": true`. + +- If `consult: true` β†’ **Consult mode is active.** This is a personal squad consulting on a project. + - The `.squad/` directory is an isolated copy of the user's personal squad. + - `sourceSquad` in `config.json` contains the path to the original personal squad. + - Do NOT read or write to `sourceSquad` β€” it's out of scope. Only operate within TEAM_ROOT. + - Scribe's charter is already patched with extraction instructions β€” no coordinator action needed. + - Include `🧳 consult` in your session acknowledgment: `Squad v{version} (🧳 consult β€” {projectName})` + - Remind agents: decisions they make here are project-isolated until explicitly extracted. +- If `consult: false` or absent β†’ Normal mode. Team root is authoritative. + +--- + +### Addition 2: Personal Squad Path Reference + +Add a new subsection under "Worktree Awareness": + +--- + +**Personal Squad Paths:** The global squad path is resolved by `resolveGlobalSquadPath()`: + +| Platform | Path | +|----------|------| +| Linux | `~/.config/squad/.squad` | +| macOS | `~/Library/Application Support/squad/.squad` | +| Windows | `%APPDATA%\squad\.squad` | + +The coordinator should NEVER hard-code these paths. Use `squad --global` or `resolveGlobalSquadPath()` to resolve. Only relevant in consult mode (to understand the `sourceSquad` field) β€” the coordinator does NOT read the personal squad directly during a session. + +--- + +### Addition 3: Consult Mode Spawn Guidance + +Add to the spawn template section: + +--- + +**In consult mode:** Pass `CONSULT_MODE: true` and `PROJECT_NAME: {projectName}` in spawn prompts alongside `TEAM_ROOT`. This lets agents know: +1. Their decisions will be reviewed for extraction β€” keep project-specific and generic reasoning separate +2. They should NOT reference personal squad paths or personal squad agent names +3. Scribe will classify their decisions β€” agents should write clear, extractable decision rationale + +--- + +### Addition 4: Consult Mode Acknowledgment Format + +Add to spawn acknowledgment conventions: + +``` +🧳 consult mode active β€” Fenster (claude-sonnet-4.5) β€” refactoring auth module + ↳ decisions staged in .squad/extract/ for review before extraction +``` + +--- + +## Proposed New Skill + +**Skill needed:** `.squad/skills/consult-mode/SKILL.md` + +Should cover: +- Detecting consult mode from config.json +- Coordinator behavior changes (CONSULT_MODE in spawn prompts) +- Scribe extraction workflow (already documented in prd-consult-mode.md β€” condense into skill) +- Acknowledgment format conventions +- STOP: extraction is always user-driven via `squad extract` β€” coordinator never auto-extracts + +This skill should be authored after this governance proposal is approved, to avoid the skill getting ahead of the governance. + +--- + +## Charter Template Additions + +All agent charter templates should include a note in "How I Work": + +```markdown +**Consult Mode Awareness:** If `CONSULT_MODE: true` is in my spawn prompt, I'm working on a project outside my home squad. My decisions here are project-isolated. Write extractable rationale so Scribe can classify them for `squad extract` review. +``` + +This should be added to `.squad/templates/charter.md` (if it exists) and `.squad/agents/scribe/charter.md` (Scribe already has extraction logic, but clarifying the classification responsibility is valuable). + +--- + +## Rationale + +Consult mode is fully implemented at the SDK level (`prd-consult-mode.md`, `squad consult` command) but the coordinator has no awareness of it. The result: agents running in a consult session have no context that they're in a temporary, isolated copy of a personal squad. They might make decisions as if they're permanent, or reference the project in ways that pollute the personal squad on extraction. + +These governance additions close the loop between the implementation (CLI + SDK) and the runtime behavior (coordinator + agents). + +--- + +## References + +- Consult mode PRD: `.squad/identity/prd-consult-mode.md` +- Issue: #344 +- Flight ambient personal squad note: `.squad/decisions/inbox/flight-ambient-personal-squad.md` diff --git a/.squad/skills/economy-mode/SKILL.md b/.squad/skills/economy-mode/SKILL.md new file mode 100644 index 000000000..696e778c4 --- /dev/null +++ b/.squad/skills/economy-mode/SKILL.md @@ -0,0 +1,114 @@ +--- +name: "economy-mode" +description: "Shifts Layer 3 model selection to cost-optimized alternatives when economy mode is active." +domain: "model-selection" +confidence: "low" +source: "manual" +--- + +## SCOPE + +βœ… THIS SKILL PRODUCES: +- A modified Layer 3 model selection table applied when economy mode is active +- `economyMode: true` written to `.squad/config.json` when activated persistently +- Spawn acknowledgments with `πŸ’°` indicator when economy mode is active + +❌ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Cost reports or billing artifacts +- Changes to Layer 0, Layer 1, or Layer 2 resolution (user intent always wins) + +## Context + +Economy mode shifts Layer 3 (Task-Aware Auto-Selection) to lower-cost alternatives. It does NOT override persistent config (`defaultModel`, `agentModelOverrides`) or per-agent charter preferences β€” those represent explicit user intent and always take priority. + +Use this skill when the user wants to reduce costs across an entire session or permanently, without manually specifying models for each agent. + +## Activation Methods + +| Method | How | +|--------|-----| +| Session phrase | "use economy mode", "save costs", "go cheap", "reduce costs" | +| Persistent config | `"economyMode": true` in `.squad/config.json` | +| CLI flag | `squad --economy` | + +**Deactivation:** "turn off economy mode", "disable economy mode", or remove `economyMode` from `config.json`. + +## Economy Model Selection Table + +When economy mode is **active**, Layer 3 auto-selection uses this table instead of the normal defaults: + +| Task Output | Normal Mode | Economy Mode | +|-------------|-------------|--------------| +| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | +| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | + +**Prefer `gpt-4.1` over `gpt-5-mini`** when the task involves structured output or agentic tool use. Prefer `gpt-5-mini` for pure text generation tasks where latency matters. + +## AGENT WORKFLOW + +### On Session Start + +1. READ `.squad/config.json` +2. CHECK for `economyMode: true` β€” if present, activate economy mode for the session +3. STORE economy mode state in session context + +### On User Phrase Trigger + +**Session-only (no config change):** "use economy mode", "save costs", "go cheap" + +1. SET economy mode active for this session +2. ACKNOWLEDGE: `βœ… Economy mode active β€” using cost-optimized models this session. (Layer 0 and Layer 2 preferences still apply)` + +**Persistent:** "always use economy mode", "save economy mode" + +1. WRITE `economyMode: true` to `.squad/config.json` (merge, don't overwrite other fields) +2. ACKNOWLEDGE: `βœ… Economy mode saved β€” cost-optimized models will be used until disabled.` + +### On Every Agent Spawn (Economy Mode Active) + +1. CHECK Layer 0a/0b first (agentModelOverrides, defaultModel) β€” if set, use that. Economy mode does NOT override Layer 0. +2. CHECK Layer 1 (session directive for a specific model) β€” if set, use that. Economy mode does NOT override explicit session directives. +3. CHECK Layer 2 (charter preference) β€” if set, use that. Economy mode does NOT override charter preferences. +4. APPLY economy table at Layer 3 instead of normal table. +5. INCLUDE `πŸ’°` in spawn acknowledgment: `πŸ”§ {Name} ({model} Β· πŸ’° economy) β€” {task}` + +### On Deactivation + +**Trigger phrases:** "turn off economy mode", "disable economy mode", "use normal models" + +1. REMOVE `economyMode` from `.squad/config.json` (if it was persisted) +2. CLEAR session economy mode state +3. ACKNOWLEDGE: `βœ… Economy mode disabled β€” returning to standard model selection.` + +### STOP + +After updating economy mode state and including the `πŸ’°` indicator in spawn acknowledgments, this skill is done. Do NOT: +- Change Layer 0, Layer 1, or Layer 2 model choices +- Override charter-specified models +- Generate cost reports or comparisons +- Fall back to premium models via economy mode (economy mode never bumps UP) + +## Config Schema + +`.squad/config.json` economy-related fields: + +```json +{ + "version": 1, + "economyMode": true +} +``` + +- `economyMode` β€” when `true`, Layer 3 uses the economy table. Optional; absent = economy mode off. +- Combines with `defaultModel` and `agentModelOverrides` β€” Layer 0 always wins. + +## Anti-Patterns + +- **Don't override Layer 0 in economy mode.** If the user set `defaultModel: "claude-opus-4.6"`, they want quality. Economy mode only affects Layer 3 auto-selection. +- **Don't silently apply economy mode.** Always acknowledge when activated or deactivated. +- **Don't treat economy mode as permanent by default.** Session phrases activate session-only; only "always" or `config.json` persist it. +- **Don't bump premium tasks down too far.** Architecture and security reviews shift from opus to sonnet in economy mode β€” they do NOT go to fast/cheap models. diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index da8ce8b56..6a121f853 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -132,6 +132,11 @@ async function checkNodeSqlite(): Promise { async function main(): Promise { const args = process.argv.slice(2); const hasGlobal = args.includes('--global'); + // --economy activates economy mode for this session (sets env var for spawner) + const hasEconomy = args.includes('--economy'); + if (hasEconomy) { + process.env['SQUAD_ECONOMY_MODE'] = '1'; + } const rawCmd = args[0]; const cmd = rawCmd?.trim() || ''; @@ -225,12 +230,15 @@ async function main(): Promise { console.log(` upstream remove `); console.log(` upstream list`); console.log(` upstream sync [name]`); + console.log(` ${BOLD}economy${RESET} Toggle economy mode (cost-conscious model selection)`); + console.log(` Usage: economy [on|off]`); console.log(` ${BOLD}help${RESET} Show this help message`); console.log(`\nFlags:`); console.log(` ${BOLD}--version, -v${RESET} Print version`); console.log(` ${BOLD}--help, -h${RESET} Show help`); console.log(` ${BOLD}--global${RESET} Use personal (global) squad path (for init, upgrade)`); + console.log(` ${BOLD}--economy${RESET} Activate economy mode for this session (cheaper models)`); console.log(`\nInstallation:`); console.log(` npm install --save-dev @bradygaster/squad-cli`); console.log(`\nInsider channel:`); @@ -605,6 +613,12 @@ async function main(): Promise { return; } + if (cmd === 'economy') { + const { runEconomy } = await import('./cli/commands/economy.js'); + await runEconomy(process.cwd(), args.slice(1)); + return; + } + // Unknown command fatal(`Unknown command: ${cmd}\n Run 'squad doctor' to check your setup, or 'squad help' for usage information.`); } diff --git a/packages/squad-cli/src/cli/commands/economy.ts b/packages/squad-cli/src/cli/commands/economy.ts new file mode 100644 index 000000000..3043d03a2 --- /dev/null +++ b/packages/squad-cli/src/cli/commands/economy.ts @@ -0,0 +1,68 @@ +/** + * Economy mode command β€” toggle cost-conscious model selection. + * + * Usage: + * squad economy on β€” enable economy mode (persisted to config.json) + * squad economy off β€” disable economy mode + * squad economy β€” show current status + */ + +import { join } from 'node:path'; +import { existsSync, readFileSync } from 'node:fs'; +import { writeEconomyMode, readEconomyMode } from '@bradygaster/squad-sdk/config'; +import { fatal } from '../core/errors.js'; +import { BOLD, RESET, GREEN, DIM } from '../core/output.js'; + +function resolveSquadDir(cwd: string): string | null { + // Walk up to find .squad/ + let dir = cwd; + for (let i = 0; i < 10; i++) { + const candidate = join(dir, '.squad'); + if (existsSync(candidate)) { + return candidate; + } + const parent = join(dir, '..'); + if (parent === dir) break; + dir = parent; + } + return null; +} + +export async function runEconomy(cwd: string, subArgs: string[]): Promise { + const squadDir = resolveSquadDir(cwd); + if (!squadDir) { + fatal('No squad found. Run "squad init" first.'); + return; + } + + const sub = subArgs[0]?.toLowerCase(); + + if (sub === 'on') { + writeEconomyMode(squadDir, true); + console.log(`${GREEN}βœ“${RESET} Economy mode ${BOLD}enabled${RESET} β€” cheaper models will be used for auto-selection.`); + console.log(` Saved to: ${DIM}${join(squadDir, 'config.json')}${RESET}`); + return; + } + + if (sub === 'off') { + writeEconomyMode(squadDir, false); + console.log(`${GREEN}βœ“${RESET} Economy mode ${BOLD}disabled${RESET} β€” returning to standard model selection.`); + console.log(` Saved to: ${DIM}${join(squadDir, 'config.json')}${RESET}`); + return; + } + + // Status display + const enabled = readEconomyMode(squadDir); + console.log(`\n${BOLD}Economy Mode${RESET}\n`); + console.log(` Status: ${enabled ? `${GREEN}enabled${RESET}` : `${DIM}disabled${RESET}`}`); + console.log(` Config: ${DIM}${join(squadDir, 'config.json')}${RESET}\n`); + if (enabled) { + console.log(` When active, auto-selected models are downgraded:`); + console.log(` ${DIM}claude-opus-4.6 β†’ claude-sonnet-4.5 (architecture/review)${RESET}`); + console.log(` ${DIM}claude-sonnet-4.6 β†’ gpt-4.1 (code writing)${RESET}`); + console.log(` ${DIM}claude-haiku-4.5 β†’ gpt-4.1 (docs/mechanical)${RESET}`); + console.log(` Explicit overrides (config.json, charter) are never changed.\n`); + } else { + console.log(` Usage: squad economy on | off\n`); + } +} diff --git a/packages/squad-sdk/src/agents/model-selector.ts b/packages/squad-sdk/src/agents/model-selector.ts index 2e2eee305..0fa0bfacb 100644 --- a/packages/squad-sdk/src/agents/model-selector.ts +++ b/packages/squad-sdk/src/agents/model-selector.ts @@ -3,6 +3,7 @@ */ import { MODELS } from '../runtime/constants.js'; +import { applyEconomyMode } from '../config/models.js'; import type { EventBus } from '../runtime/event-bus.js'; /** @@ -32,6 +33,8 @@ export interface ModelResolutionOptions { taskType: TaskType; /** Agent role (for context) */ agentRole?: string; + /** When true, apply economy mode substitution at Layer 3/4 */ + economyMode?: boolean; } /** @@ -55,9 +58,9 @@ export interface ResolvedModel { * @returns Resolved model with tier and fallback chain */ export function resolveModel(options: ModelResolutionOptions): ResolvedModel { - const { userOverride, charterPreference, taskType } = options; + const { userOverride, charterPreference, taskType, economyMode } = options; - // Layer 1: User Override + // Layer 1: User Override (explicit β€” economy does not apply) if (userOverride && userOverride.trim().length > 0) { const tier = inferTierFromModel(userOverride); return { @@ -68,7 +71,7 @@ export function resolveModel(options: ModelResolutionOptions): ResolvedModel { }; } - // Layer 2: Charter Preference + // Layer 2: Charter Preference (explicit β€” economy does not apply) if (charterPreference && charterPreference.trim().length > 0 && charterPreference !== 'auto') { const tier = inferTierFromModel(charterPreference); return { @@ -79,66 +82,70 @@ export function resolveModel(options: ModelResolutionOptions): ResolvedModel { }; } - // Layer 3: Task-Aware Auto-Selection - const autoSelected = selectModelForTask(taskType); + // Layer 3: Task-Aware Auto-Selection (economy mode applies) + const autoSelected = selectModelForTask(taskType, economyMode); if (autoSelected) { return autoSelected; } - // Layer 4: Default + // Layer 4: Default (economy mode applies) + const defaultModel = economyMode + ? applyEconomyMode(MODELS.SELECTOR_DEFAULT) + : MODELS.SELECTOR_DEFAULT; + const defaultTier = inferTierFromModel(defaultModel); return { - model: MODELS.SELECTOR_DEFAULT, - tier: MODELS.SELECTOR_DEFAULT_TIER, + model: defaultModel, + tier: defaultTier, source: 'default', - fallbackChain: [...MODELS.FALLBACK_CHAINS[MODELS.SELECTOR_DEFAULT_TIER]], + fallbackChain: [...MODELS.FALLBACK_CHAINS[defaultTier]], }; } /** - * Select model based on task type. + * Select model based on task type, with optional economy mode substitution. * * @param taskType - Type of task being performed + * @param economyMode - When true, downgrade model to cheaper alternative * @returns Resolved model or undefined if no match */ -function selectModelForTask(taskType: TaskType): ResolvedModel | undefined { +function selectModelForTask(taskType: TaskType, economyMode?: boolean): ResolvedModel | undefined { + let model: string | undefined; + let tier: ModelTier | undefined; + switch (taskType) { case 'code': - return { - model: 'claude-sonnet-4.6', - tier: 'standard', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.standard], - }; - + model = 'claude-sonnet-4.6'; + tier = 'standard'; + break; case 'prompt': - return { - model: 'claude-sonnet-4.6', - tier: 'standard', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.standard], - }; - + model = 'claude-sonnet-4.6'; + tier = 'standard'; + break; case 'visual': - return { - model: 'claude-opus-4.6', - tier: 'premium', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.premium], - }; - + model = 'claude-opus-4.6'; + tier = 'premium'; + break; case 'docs': case 'planning': case 'mechanical': - return { - model: 'claude-haiku-4.5', - tier: 'fast', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.fast], - }; - + model = 'claude-haiku-4.5'; + tier = 'fast'; + break; default: return undefined; } + + if (economyMode) { + model = applyEconomyMode(model); + tier = inferTierFromModel(model); + } + + return { + model, + tier, + source: 'task-auto', + fallbackChain: [...MODELS.FALLBACK_CHAINS[tier]], + }; } export function inferTierFromModel(model: string): ModelTier { diff --git a/packages/squad-sdk/src/config/models.ts b/packages/squad-sdk/src/config/models.ts index 370040bc6..cac32e4a8 100644 --- a/packages/squad-sdk/src/config/models.ts +++ b/packages/squad-sdk/src/config/models.ts @@ -511,12 +511,93 @@ export function estimateCost(model: string, inputTokens: number, outputTokens: n // Persistent Model Preference (Layer 0) // ============================================================================ +/** + * Economy mode model map: normal model β†’ cheaper alternative. + * + * Applied at Layer 3 (task-aware auto) and Layer 4 (default) when + * economy mode is active. Layers 0–2 (explicit preferences) are + * never substituted β€” the user's explicit choice always wins. + * + * Table source: issue #500 + */ +export const ECONOMY_MODEL_MAP: Record = { + // Premium β†’ standard downgrade (architecture/review tasks) + 'claude-opus-4.6': 'claude-sonnet-4.5', + 'claude-opus-4.6-fast': 'claude-sonnet-4.5', + 'claude-opus-4.5': 'claude-sonnet-4.5', + // Standard β†’ fast downgrade (code writing, docs, planning, triage) + 'claude-sonnet-4.6': 'gpt-4.1', + 'claude-sonnet-4.5': 'gpt-4.1', + // Fast β†’ cheapest fast (scribe/mechanical, docs) + 'claude-haiku-4.5': 'gpt-4.1', +}; + +/** + * Applies economy mode substitution to a model ID. + * Returns the cheaper economy alternative, or the original if no mapping exists. + */ +export function applyEconomyMode(model: string): string { + return ECONOMY_MODEL_MAP[model] ?? model; +} + /** * Shape of model preference fields within `.squad/config.json`. */ export interface ModelPreferenceConfig { defaultModel?: string; agentModelOverrides?: Record; + economyMode?: boolean; +} + +/** + * Reads the economy mode setting from `.squad/config.json`. + * + * @param squadDir - Path to the `.squad/` directory + * @returns True if economyMode is enabled, false otherwise + */ +export function readEconomyMode(squadDir: string): boolean { + const configPath = join(squadDir, 'config.json'); + if (!existsSync(configPath)) { + return false; + } + try { + const raw = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw); + return parsed !== null && + typeof parsed === 'object' && + parsed.economyMode === true; + } catch { + return false; + } +} + +/** + * Writes the economy mode setting to `.squad/config.json`. + * Merges with existing config β€” does not overwrite other fields. + * + * @param squadDir - Path to the `.squad/` directory + * @param enabled - Whether economy mode should be enabled + */ +export function writeEconomyMode(squadDir: string, enabled: boolean): void { + const configPath = join(squadDir, 'config.json'); + let config: Record = {}; + if (existsSync(configPath)) { + try { + config = JSON.parse(readFileSync(configPath, 'utf-8')); + } catch { + config = { version: 1 }; + } + } else { + config = { version: 1 }; + } + + if (enabled) { + config.economyMode = true; + } else { + delete config.economyMode; + } + + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); } /** @@ -652,6 +733,10 @@ export function writeAgentModelOverrides( * * Per-agent overrides from config.json take priority over the global defaultModel. * + * Economy mode modifier: when active (via economyMode option or config.json), + * shifts model selection at Layer 3 and Layer 4 to cheaper alternatives per + * ECONOMY_MODEL_MAP. Layers 0–2 (explicit user preferences) are never overridden. + * * @param options - Resolution inputs * @returns Resolved model ID */ @@ -661,10 +746,12 @@ export function resolveModel(options: { sessionDirective?: string | null; charterPreference?: string | null; taskModel?: string | null; + /** When true, apply economy mode substitution at Layer 3/4. Overrides config. */ + economyMode?: boolean; }): string { const { agentName, squadDir, sessionDirective, charterPreference, taskModel } = options; - // Layer 0a: Per-agent persistent override + // Layer 0a: Per-agent persistent override (explicit β€” economy does not apply) if (squadDir && agentName) { const agentOverrides = readAgentModelOverrides(squadDir); if (agentOverrides[agentName]) { @@ -672,7 +759,7 @@ export function resolveModel(options: { } } - // Layer 0b: Global persistent config + // Layer 0b: Global persistent config (explicit β€” economy does not apply) if (squadDir) { const persistedModel = readModelPreference(squadDir); if (persistedModel) { @@ -680,21 +767,28 @@ export function resolveModel(options: { } } - // Layer 1: Session-wide user directive + // Layer 1: Session-wide user directive (explicit β€” economy does not apply) if (sessionDirective) { return sessionDirective; } - // Layer 2: Charter preference + // Layer 2: Charter preference (explicit β€” economy does not apply) if (charterPreference) { return charterPreference; } - // Layer 3: Task-aware auto-selection + // Determine if economy mode is active (option takes precedence over config) + const isEconomy = + options.economyMode !== undefined + ? options.economyMode + : (squadDir ? readEconomyMode(squadDir) : false); + + // Layer 3: Task-aware auto-selection (economy mode applies) if (taskModel) { - return taskModel; + return isEconomy ? applyEconomyMode(taskModel) : taskModel; } - // Layer 4: Default - return 'claude-haiku-4.5'; + // Layer 4: Default (economy mode applies) + const defaultModel = 'claude-haiku-4.5'; + return isEconomy ? applyEconomyMode(defaultModel) : defaultModel; } diff --git a/test/economy-mode.test.ts b/test/economy-mode.test.ts new file mode 100644 index 000000000..9835b4816 --- /dev/null +++ b/test/economy-mode.test.ts @@ -0,0 +1,291 @@ +/** + * Tests for economy mode model selection (issue #500). + * + * Validates that when economy mode is active: + * - Layer 3 (task-aware) and Layer 4 (default) models are downgraded + * - Layer 0–2 (explicit preferences) are never overridden + * - config.json read/write round-trips correctly + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { + resolveModel, + readEconomyMode, + writeEconomyMode, + applyEconomyMode, + ECONOMY_MODEL_MAP, +} from '@bradygaster/squad-sdk/config'; +import { + resolveModel as sdkResolveModel, +} from '@bradygaster/squad-sdk/agents'; + +let squadDir: string; + +beforeEach(() => { + squadDir = mkdtempSync(join(tmpdir(), 'squad-economy-')); +}); + +afterEach(() => { + rmSync(squadDir, { recursive: true, force: true }); +}); + +// ============================================================================ +// ECONOMY_MODEL_MAP + applyEconomyMode +// ============================================================================ + +describe('ECONOMY_MODEL_MAP', () => { + it('maps premium models to standard', () => { + expect(ECONOMY_MODEL_MAP['claude-opus-4.6']).toBe('claude-sonnet-4.5'); + expect(ECONOMY_MODEL_MAP['claude-opus-4.6-fast']).toBe('claude-sonnet-4.5'); + expect(ECONOMY_MODEL_MAP['claude-opus-4.5']).toBe('claude-sonnet-4.5'); + }); + + it('maps standard sonnet models to fast', () => { + expect(ECONOMY_MODEL_MAP['claude-sonnet-4.6']).toBe('gpt-4.1'); + expect(ECONOMY_MODEL_MAP['claude-sonnet-4.5']).toBe('gpt-4.1'); + }); + + it('maps haiku to cheapest fast', () => { + expect(ECONOMY_MODEL_MAP['claude-haiku-4.5']).toBe('gpt-4.1'); + }); +}); + +describe('applyEconomyMode', () => { + it('returns economy model for known models', () => { + expect(applyEconomyMode('claude-opus-4.6')).toBe('claude-sonnet-4.5'); + expect(applyEconomyMode('claude-sonnet-4.6')).toBe('gpt-4.1'); + expect(applyEconomyMode('claude-haiku-4.5')).toBe('gpt-4.1'); + }); + + it('returns original model when no economy mapping exists', () => { + expect(applyEconomyMode('gpt-4.1')).toBe('gpt-4.1'); + expect(applyEconomyMode('gpt-5-mini')).toBe('gpt-5-mini'); + expect(applyEconomyMode('unknown-model-xyz')).toBe('unknown-model-xyz'); + }); +}); + +// ============================================================================ +// readEconomyMode / writeEconomyMode +// ============================================================================ + +describe('readEconomyMode', () => { + it('returns false when config.json does not exist', () => { + expect(readEconomyMode(squadDir)).toBe(false); + }); + + it('returns false when economyMode field is absent', () => { + writeFileSync(join(squadDir, 'config.json'), JSON.stringify({ version: 1 })); + expect(readEconomyMode(squadDir)).toBe(false); + }); + + it('returns false when economyMode is false', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: false }) + ); + expect(readEconomyMode(squadDir)).toBe(false); + }); + + it('returns true when economyMode is true', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: true }) + ); + expect(readEconomyMode(squadDir)).toBe(true); + }); + + it('returns false on malformed JSON', () => { + writeFileSync(join(squadDir, 'config.json'), '{ bad json'); + expect(readEconomyMode(squadDir)).toBe(false); + }); +}); + +describe('writeEconomyMode', () => { + it('creates config.json with economyMode: true', () => { + writeEconomyMode(squadDir, true); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.economyMode).toBe(true); + expect(raw.version).toBe(1); + }); + + it('removes economyMode field when set to false', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: true }) + ); + writeEconomyMode(squadDir, false); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw).not.toHaveProperty('economyMode'); + }); + + it('merges with existing config without clobbering other fields', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + writeEconomyMode(squadDir, true); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + expect(raw.economyMode).toBe(true); + }); + + it('round-trips: write on β†’ read true', () => { + writeEconomyMode(squadDir, true); + expect(readEconomyMode(squadDir)).toBe(true); + }); + + it('round-trips: write off β†’ read false', () => { + writeEconomyMode(squadDir, true); + writeEconomyMode(squadDir, false); + expect(readEconomyMode(squadDir)).toBe(false); + }); +}); + +// ============================================================================ +// resolveModel β€” economy mode option +// ============================================================================ + +describe('resolveModel economy mode (option)', () => { + it('Layer 4 default: uses gpt-4.1 instead of haiku when economyMode: true', () => { + expect(resolveModel({ economyMode: true })).toBe('gpt-4.1'); + }); + + it('Layer 4 default: uses haiku when economyMode: false', () => { + expect(resolveModel({ economyMode: false })).toBe('claude-haiku-4.5'); + }); + + it('Layer 3 code task: uses gpt-4.1 instead of sonnet when economyMode: true', () => { + expect(resolveModel({ taskModel: 'claude-sonnet-4.6', economyMode: true })).toBe('gpt-4.1'); + }); + + it('Layer 3 architecture task: uses sonnet instead of opus when economyMode: true', () => { + expect(resolveModel({ taskModel: 'claude-opus-4.6', economyMode: true })).toBe('claude-sonnet-4.5'); + }); + + it('Layer 3 docs task: uses gpt-4.1 instead of haiku when economyMode: true', () => { + expect(resolveModel({ taskModel: 'claude-haiku-4.5', economyMode: true })).toBe('gpt-4.1'); + }); + + it('Layer 2 charter preference: NOT overridden by economy mode', () => { + expect( + resolveModel({ charterPreference: 'claude-opus-4.6', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 1 session directive: NOT overridden by economy mode', () => { + expect( + resolveModel({ sessionDirective: 'claude-opus-4.6', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 0b global config: NOT overridden by economy mode', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + expect( + resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 0a per-agent override: NOT overridden by economy mode', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ + version: 1, + agentModelOverrides: { eecom: 'claude-opus-4.6' }, + }) + ); + expect( + resolveModel({ agentName: 'eecom', squadDir, taskModel: 'claude-haiku-4.5', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); +}); + +// ============================================================================ +// resolveModel β€” economy mode from config.json +// ============================================================================ + +describe('resolveModel economy mode (from config)', () => { + it('uses economy model when economyMode: true in config', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: true }) + ); + expect(resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6' })).toBe('gpt-4.1'); + }); + + it('uses normal model when economyMode absent from config', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1 }) + ); + expect(resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6' })).toBe('claude-sonnet-4.6'); + }); + + it('explicit economyMode option overrides config setting', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: false }) + ); + // Option says true, config says false β†’ option wins + expect( + resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6', economyMode: true }) + ).toBe('gpt-4.1'); + }); +}); + +// ============================================================================ +// SDK model-selector resolveModel economy mode +// ============================================================================ + +describe('SDK resolveModel (agents) economy mode', () => { + it('code task β†’ gpt-4.1 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'code', economyMode: true }); + expect(result.model).toBe('gpt-4.1'); + expect(result.source).toBe('task-auto'); + }); + + it('docs task β†’ gpt-4.1 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'docs', economyMode: true }); + expect(result.model).toBe('gpt-4.1'); + }); + + it('mechanical task β†’ gpt-4.1 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'mechanical', economyMode: true }); + expect(result.model).toBe('gpt-4.1'); + }); + + it('visual task β†’ claude-sonnet-4.5 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'visual', economyMode: true }); + expect(result.model).toBe('claude-sonnet-4.5'); + }); + + it('code task β†’ claude-sonnet-4.6 when economyMode: false', () => { + const result = sdkResolveModel({ taskType: 'code', economyMode: false }); + expect(result.model).toBe('claude-sonnet-4.6'); + }); + + it('user override NOT affected by economy mode', () => { + const result = sdkResolveModel({ + taskType: 'code', + userOverride: 'claude-opus-4.6', + economyMode: true, + }); + expect(result.model).toBe('claude-opus-4.6'); + expect(result.source).toBe('user-override'); + }); + + it('charter preference NOT affected by economy mode', () => { + const result = sdkResolveModel({ + taskType: 'code', + charterPreference: 'claude-opus-4.6', + economyMode: true, + }); + expect(result.model).toBe('claude-opus-4.6'); + expect(result.source).toBe('charter'); + }); +});