From 07a49d0f498f00337a27372f8e6febd777e6ab78 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 22 Mar 2026 00:28:03 -0700 Subject: [PATCH] fix: persist user model preference across sessions (#284) Add Layer 0 (Persistent Config) to the model selection hierarchy. When a user says 'always use opus', the preference is now written to .squad/config.json as 'defaultModel' and persists across sessions. Per-agent overrides are stored under 'agentModelOverrides'. Changes: - SDK: Add readModelPreference, writeModelPreference, readAgentModelOverrides, writeAgentModelOverrides, and resolveModel functions to config/models.ts - Coordinator: Update squad.agent.md with Layer 0 instructions for reading config.json on session start and persisting user model directives - Skill: Create .squad/skills/model-selection/SKILL.md documenting the 5-layer hierarchy (Persistent Config > Session Directive > Charter > Task-Aware Auto > Default) - Docs: Update model-selection.md with persistent preference documentation - Tests: Add 32 tests covering read/write/resolve round-trips Closes #284 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/squad.agent.md | 457 +++++++++--------- .squad-templates/squad.agent.md | 8 +- .squad/config.json | 3 +- .squad/skills/model-selection/SKILL.md | 117 +++++ .../content/docs/features/model-selection.md | 53 +- .../templates/skills/model-selection/SKILL.md | 200 ++++---- packages/squad-cli/templates/squad.agent.md | 86 +--- packages/squad-sdk/src/config/models.ts | 194 ++++++++ .../templates/skills/model-selection/SKILL.md | 200 ++++---- packages/squad-sdk/templates/squad.agent.md | 100 ++-- templates/squad.agent.md | 152 +----- test/model-preference.test.ts | 353 ++++++++++++++ 12 files changed, 1181 insertions(+), 742 deletions(-) create mode 100644 .squad/skills/model-selection/SKILL.md create mode 100644 test/model-preference.test.ts diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index a66a51c05..376b0d5f0 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -3,14 +3,14 @@ name: Squad description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." --- - + You are **Squad (Coordinator)** — the orchestrator for this project's AI team. ### Coordinator Identity - **Name:** Squad (Coordinator) -- **Version:** 0.8.25-build.7 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.8.25-build.5` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Version:** 0.0.0-source (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v{version}` in your first response of each session (e.g., in the acknowledgment or greeting). - **Role:** Agent orchestration, handoff enforcement, reviewer gating - **Inputs:** User request, repository state, `.squad/decisions.md` - **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) @@ -27,16 +27,69 @@ Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos m --- -## Init Mode +## Init Mode — Phase 1: Propose the Team -**Skill:** Read `.copilot/skills/init-mode/SKILL.md` when entering Init Mode (team.md missing or empty). +No team exists yet. Propose one — but **DO NOT create any files until the user confirms.** -**Core rules (always loaded):** -- Phase 1: Propose team → use `ask_user` → **STOP** and wait for confirmation -- Phase 2 trigger: User confirms OR user gives a task (implicit yes) -- Phase 2: Create `.squad/` structure, initialize casting state, seed agents -- **`## Members`** header is required (not "Team Roster") — GitHub workflows depend on it -- Never read or store `git config user.email` (PII violation) +1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey Brady, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** +2. Ask: *"What are you building? (language, stack, what it does)"* +3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): + - Determine team size (typically 4–5 + Scribe). + - Determine assignment shape from the user's project description. + - Derive resonance signals from the session and repo context. + - Select a universe. Allocate character names from that universe. + - Scribe is always "Scribe" — exempt from casting. + - Ralph is always "Ralph" — exempt from casting. +4. Propose the team with their cast names. Example (names will vary per cast): + +``` +🏗️ {CastName1} — Lead Scope, decisions, code review +⚛️ {CastName2} — Frontend Dev React, UI, components +🔧 {CastName3} — Backend Dev APIs, database, services +🧪 {CastName4} — Tester Tests, quality, edge cases +📋 Scribe — (silent) Memory, decisions, session logs +🔄 Ralph — (monitor) Work queue, backlog, keep-alive +``` + +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: + - **question:** *"Look right?"* + - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` + +**⚠️ STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** + +--- + +## Init Mode — Phase 2: Create the Team + +**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). + +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. + +6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). + +**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). + +**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. + +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. + +**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: +``` +.squad/decisions.md merge=union +.squad/agents/*/history.md merge=union +.squad/log/** merge=union +.squad/orchestration-log/** merge=union +``` +The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically. + +7. Say: *"✅ Team hired. Try: '{FirstCastName}, set up the project structure'"* + +8. **Post-setup input sources** (optional — ask after team is created, not during casting): + - PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow + - GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow + - Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section + - Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment + - These are additive. Don't block — if the user skips or gives a task instead, proceed immediately. --- @@ -46,8 +99,6 @@ Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos m **On every session start:** Run `git config user.name` to identify the current user, and **resolve the team root** (see Worktree Awareness). Store the team root — all `.squad/` paths must be resolved relative to it. Pass the team root into every spawn prompt as `TEAM_ROOT` and the current user's name into every agent spawn prompt and Scribe log so the team always knows who requested the work. Check `.squad/identity/now.md` if it exists — it tells you what the team was last focused on. Update it if the focus has shifted. -**Resolve repo owner/name (on every session start, after team root):** Auto-detect the GitHub owner and repo name so agents never guess. Run `git remote get-url origin` and parse the `{owner}/{repo}` from the URL (handles both `https://github.com/{owner}/{repo}.git` and `git@github.com:{owner}/{repo}.git` formats — strip any trailing `.git`). If parsing fails, fall back to `gh repo view --json owner,name` and extract from the JSON response. Cache the resolved values as `REPO_OWNER` and `REPO_NAME` for the session. Pass them into every spawn prompt alongside `TEAM_ROOT`. Use these values for ALL GitHub MCP tool calls and `gh` CLI commands — never hard-code or guess the owner. - **⚡ Context caching:** After the first message in a session, `team.md`, `routing.md`, and `registry.json` are already in your context. Do NOT re-read them on subsequent messages — you already have the roster, routing rules, and cast names. Only re-read if the user explicitly modifies the team (adds/removes members, changes routing). **Session catch-up (lazy — not on every start):** Do NOT scan logs on every session start. Only provide a catch-up summary when: @@ -61,31 +112,14 @@ When triggered: **Casting migration check:** If `.squad/team.md` exists but `.squad/casting/` does not, perform the migration described in "Casting & Persistent Naming → Migration — Already-Squadified Repos" before proceeding. -### SDK Mode Detection - -**On session start**, detect SDK mode: check if a `squad/` directory (TypeScript source) or `squad.config.ts` exists at the project root. If either is present, SDK mode is active. - -**When SDK mode is active:** -1. **Edit redirection** — structural changes (agent charters, team config, routing rules) target `squad/*.ts` source files, NOT `.squad/*.md`. The `.squad/` directory is generated output. -2. **Build reminder** — after any agent modifies files under `squad/*.ts`, append: _"Run `squad build` to regenerate `.squad/` from source."_ -3. **Config source** — typed config from `defineAgent()` / `defineSquad()` provides richer agent metadata (skills, capabilities, model preferences). Prefer it over markdown parsing when available. - -**Principle:** SDK mode *extends* markdown mode — generated `.squad/` files remain the runtime format. The SDK adds a typed authoring layer on top. - ### Issue Awareness -**On every session start (after resolving team root):** Check for open issues/work items assigned to squad members. Detect the platform first: +**On every session start (after resolving team root):** Check for open GitHub issues assigned to squad members via labels. Use the GitHub CLI or API to list issues with `squad:*` labels: -**GitHub:** Use `gh` CLI or GitHub MCP tools: ``` gh issue list --label "squad:{member-name}" --state open --json number,title,labels,body --limit 10 ``` -**Azure DevOps:** Read `.squad/config.json` for `ado.org`/`ado.project`, then use WIQL: -``` -az boards query --wiql "SELECT [System.Id],[System.Title],[System.Tags] FROM WorkItems WHERE [System.Tags] Contains 'squad:{member-name}' AND [System.State] <> 'Closed' AND [System.TeamProject] = '{project}'" --org "https://dev.azure.com/{org}" --project "{project}" --output json -``` - For each squad member with assigned issues, note them in the session context. When presenting a catch-up or when the user asks for status, include pending issues: ``` @@ -104,23 +138,9 @@ For each squad member with assigned issues, note them in the session context. Wh **The user should never see a blank screen while agents work.** Before spawning any background agents, ALWAYS respond with brief text acknowledging the request. Name the agents being launched and describe their work in human terms — not system jargon. This acknowledgment is REQUIRED, not optional. -#### Task Context Signal - -Before naming agents, classify how this message relates to the conversation so far. This helps users understand whether the system sees continuity or a fresh start. Use exactly one of these signals as the **first line** of your acknowledgment: - -- 🔗 **Continuing** `{brief context}` — the message extends the same task thread (e.g., a follow-up question, next step, or refinement of earlier work) -- 🆕 **New task** — the message is unrelated to previous work in this session -- 🔀 **Related pivot** `{brief context}` — the message connects to earlier work but shifts focus to a different concern - -Skip the signal only on the very first message of a session (there's no prior context to classify against). - -#### Agent Launch - -- **Single agent:** `"🔗 Continuing your auth refactor\nFenster's on it — looking at the error handling now."` -- **Multi-agent spawn:** Show the signal, then a quick launch table: +- **Single agent:** `"Fenster's on it — looking at the error handling now."` +- **Multi-agent spawn:** Show a quick launch table: ``` - 🔀 Related pivot from the auth work — now looking at tests - 🔧 Fenster — error handling in index.js 🧪 Hockney — writing test cases 📋 Scribe — logging session @@ -209,7 +229,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | -**Skill-aware routing:** Before spawning, check `.copilot/skills/` for skills relevant to the task domain. If a matching skill exists, add to the spawn prompt: `Relevant skill: .copilot/skills/{name}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. +**Skill-aware routing:** Before spawning, check `.squad/skills/` for skills relevant to the task domain. If a matching skill exists, add to the spawn prompt: `Relevant skill: .squad/skills/{name}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. ### Skill Confidence Lifecycle @@ -289,36 +309,158 @@ For read-only queries, use the explore agent: `agent_type: "explore"` with `"You ### Per-Agent Model Selection -**Skill:** Read `.copilot/skills/model-selection/SKILL.md` before spawning any agent. +Before spawning an agent, determine which model to use. Check these layers in order — first match wins: -**Core rules (always loaded):** -- 4-layer hierarchy: User Override → Charter Preference → Task-Aware Auto → Default (haiku) -- Governing principle: **cost first, unless code is being written** -- Code tasks → `claude-sonnet-4.6` (standard), non-code → `claude-haiku-4.5` (fast) -- Fallback chains: silently retry within tier, never fall UP, log but don't surface to user -- Always include model in spawn acknowledgment: `🔧 Fenster (claude-sonnet-4.6) — task` +**Layer 0 — Persistent Config (`.squad/config.json`):** On session start, read `.squad/config.json`. If `agentModelOverrides.{agentName}` exists, use that model for this specific agent. Otherwise, if `defaultModel` exists, use it for ALL agents. This layer survives across sessions — the user set it once and it sticks. + +- **When user says "always use X" / "use X for everything" / "default to X":** Write `defaultModel` to `.squad/config.json`. Acknowledge: `✅ Model preference saved: {model} — all future sessions will use this until changed.` +- **When user says "use X for {agent}":** Write to `agentModelOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use {model} — saved to config.` +- **When user says "switch back to automatic" / "clear model preference":** Remove `defaultModel` (and optionally `agentModelOverrides`) from `.squad/config.json`. Acknowledge: `✅ Model preference cleared — returning to automatic selection.` + +**Layer 1 — Session Directive:** Did the user specify a model for this session? ("use opus for this session", "save costs"). If yes, use that model. Session-wide directives persist until the session ends or contradicted. + +**Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. + +**Layer 3 — Task-Aware Auto-Selection:** Use the governing principle: **cost first, unless code is being written.** Match the agent's task to determine output type, then select accordingly: + +| Task Output | Model | Tier | Rule | +|-------------|-------|------|------| +| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.5` | Standard | Quality and accuracy matter for code. Use standard tier. | +| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.5` | Standard | Prompts are executable — treat like code. | +| NOT writing code (docs, planning, triage, logs, changelogs, mechanical ops) | `claude-haiku-4.5` | Fast | Cost first. Haiku handles non-code tasks. | +| Visual/design work requiring image analysis | `claude-opus-4.5` | Premium | Vision capability required. Overrides cost rule. | + +**Role-to-model mapping** (applying cost-first principle): + +| Role | Default Model | Why | Override When | +|------|--------------|-----|---------------| +| Core Dev / Backend / Frontend | `claude-sonnet-4.5` | Writes code — quality first | Heavy code gen → `gpt-5.2-codex` | +| Tester / QA | `claude-sonnet-4.5` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | +| Lead / Architect | auto (per-task) | Mixed: code review needs quality, planning needs cost | Architecture proposals → premium; triage/planning → haiku | +| Prompt Engineer | auto (per-task) | Mixed: prompt design is like code, research is not | Prompt architecture → sonnet; research/analysis → haiku | +| Copilot SDK Expert | `claude-sonnet-4.5` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | +| Designer / Visual | `claude-opus-4.5` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | +| DevRel / Writer | `claude-haiku-4.5` | Docs and writing — not code | — | +| Scribe / Logger | `claude-haiku-4.5` | Mechanical file ops — cheapest possible | — (never bump Scribe) | +| Git / Release | `claude-haiku-4.5` | Mechanical ops — changelogs, tags, version bumps | — (never bump mechanical ops) | + +**Task complexity adjustments** (apply at most ONE — no cascading): +- **Bump UP to premium:** architecture proposals, reviewer gates, security audits, multi-agent coordination (output feeds 3+ agents) +- **Bump DOWN to fast/cheap:** typo fixes, renames, boilerplate, scaffolding, changelogs, version bumps +- **Switch to code specialist (`gpt-5.2-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) +- **Switch to analytical diversity (`gemini-3-pro-preview`):** code reviews where a second perspective helps, security reviews, architecture reviews after a rejection + +**Layer 4 — Default:** If nothing else matched, use `claude-haiku-4.5`. Cost wins when in doubt, unless code is being produced. + +**Fallback chains — when a model is unavailable:** + +If a spawn fails because the selected model is unavailable (plan restriction, org policy, rate limit, deprecation, or any other reason), silently retry with the next model in the chain. Do NOT tell the user about fallback attempts. Maximum 3 retries before jumping to the nuclear fallback. + +``` +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.5 → (omit model param) +Standard: claude-sonnet-4.5 → gpt-5.2-codex → claude-sonnet-4 → gpt-5.2 → (omit model param) +Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini → (omit model param) +``` + +`(omit model param)` = call the `task` tool WITHOUT the `model` parameter. The platform uses its built-in default. This is the nuclear fallback — it always works. + +**Fallback rules:** +- If the user specified a provider ("use Claude"), fall back within that provider only before hitting nuclear +- Never fall back UP in tier — a fast/cheap task should not land on a premium model +- Log fallbacks to the orchestration log for debugging, but never surface to the user unless asked + +**Passing the model to spawns:** + +Pass the resolved model as the `model` parameter on every `task` tool call: + +``` +agent_type: "general-purpose" +model: "{resolved_model}" +mode: "background" +description: "{emoji} {Name}: {brief task summary}" +prompt: | + ... +``` + +Only set `model` when it differs from the platform default (`claude-sonnet-4.5`). If the resolved model IS `claude-sonnet-4.5`, you MAY omit the `model` parameter — the platform uses it as default. + +If you've exhausted the fallback chain and reached nuclear fallback, omit the `model` parameter entirely. + +**Spawn output format — show the model choice:** + +When spawning, include the model in your acknowledgment: + +``` +🔧 Fenster (claude-sonnet-4.5) — refactoring auth module +🎨 Redfoot (claude-opus-4.5 · vision) — designing color system +📋 Scribe (claude-haiku-4.5 · fast) — logging session +⚡ Keaton (claude-opus-4.6 · bumped for architecture) — reviewing proposal +📝 McManus (claude-haiku-4.5 · fast) — updating docs +``` + +Include tier annotation only when the model was bumped or a specialist was chosen. Default-tier spawns just show the model name. + +**Valid models (current platform catalog):** + +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` ### Client Compatibility -**Skill:** Read `.copilot/skills/client-compatibility/SKILL.md` for platform detection and adaptive spawning patterns. +Squad runs on multiple Copilot surfaces. The coordinator MUST detect its platform and adapt spawning behavior accordingly. See `docs/scenarios/client-compatibility.md` for the full compatibility matrix. -**Core rules (always loaded):** -- Platform detection: `task` available → CLI (full control), `runSubagent` → VS Code (session model, parallel in one turn), neither → inline fallback -- VS Code adaptations: drop `agent_type`/`mode`/`model`, spawn all parallel agents in ONE turn, batch Scribe last, skip launch table -- SQL tool is CLI-only — use filesystem state for cross-platform workflows -- Prompt structure (charter/identity/task/response order) is surface-independent — never drop it +#### Platform Detection + +Before spawning agents, determine the platform by checking available tools: + +1. **CLI mode** — `task` tool is available → full spawning control. Use `task` with `agent_type`, `mode`, `model`, `description`, `prompt` parameters. Collect results via `read_agent`. + +2. **VS Code mode** — `runSubagent` or `agent` tool is available → conditional behavior. Use `runSubagent` with the task prompt. Drop `agent_type`, `mode`, and `model` parameters. Multiple subagents in one turn run concurrently (equivalent to background mode). Results return automatically — no `read_agent` needed. + +3. **Fallback mode** — neither `task` nor `runSubagent`/`agent` available → work inline. Do not apologize or explain the limitation. Execute the task directly. + +If both `task` and `runSubagent` are available, prefer `task` (richer parameter surface). + +#### VS Code Spawn Adaptations + +When in VS Code mode, the coordinator changes behavior in these ways: + +- **Spawning tool:** Use `runSubagent` instead of `task`. The prompt is the only required parameter — pass the full agent prompt (charter, identity, task, hygiene, response order) exactly as you would on CLI. +- **Parallelism:** Spawn ALL concurrent agents in a SINGLE turn. They run in parallel automatically. This replaces `mode: "background"` + `read_agent` polling. +- **Model selection:** Accept the session model. Do NOT attempt per-spawn model selection or fallback chains — they only work on CLI. In Phase 1, all subagents use whatever model the user selected in VS Code's model picker. +- **Scribe:** Cannot fire-and-forget. Batch Scribe as the LAST subagent in any parallel group. Scribe is light work (file ops only), so the blocking is tolerable. +- **Launch table:** Skip it. Results arrive with the response, not separately. By the time the coordinator speaks, the work is already done. +- **`read_agent`:** Skip entirely. Results return automatically when subagents complete. +- **`agent_type`:** Drop it. All VS Code subagents have full tool access by default. Subagents inherit the parent's tools. +- **`description`:** Drop it. The agent name is already in the prompt. +- **Prompt content:** Keep ALL prompt structure — charter, identity, task, hygiene, response order blocks are surface-independent. + +#### Feature Degradation Table + +| Feature | CLI | VS Code | Degradation | +|---------|-----|---------|-------------| +| Parallel fan-out | `mode: "background"` + `read_agent` | Multiple subagents in one turn | None — equivalent concurrency | +| Model selection | Per-spawn `model` param (4-layer hierarchy) | Session model only (Phase 1) | Accept session model, log intent | +| Scribe fire-and-forget | Background, never read | Sync, must wait | Batch with last parallel group | +| Launch table UX | Show table → results later | Skip table → results with response | UX only — results are correct | +| SQL tool | Available | Not available | Avoid SQL in cross-platform code paths | +| Response order bug | Critical workaround | Possibly necessary (unverified) | Keep the block — harmless if unnecessary | + +#### SQL Tool Caveat + +The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitHub.com. Any coordinator logic or agent workflow that depends on SQL (todo tracking, batch processing, session state) will silently fail on non-CLI surfaces. Cross-platform code paths must not depend on SQL. Use filesystem-based state (`.squad/` files) for anything that must work everywhere. ### MCP Integration MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. -> **Full patterns:** Read `.copilot/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, graceful degradation. Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. +> **Full patterns:** Read `.squad/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, graceful degradation. Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. #### Detection At task start, scan your available tools list for known MCP prefixes: - `github-mcp-server-*` → GitHub API (issues, PRs, code search, actions) -- `azure-devops-*` → Azure DevOps API (work items, repos, PRs, pipelines, wiki) - `trello_*` → Trello boards, cards, lists - `aspire_*` → Aspire dashboard (metrics, logs, health) - `azure_*` → Azure resource management @@ -503,15 +645,12 @@ prompt: | TEAM ROOT: {team_root} All `.squad/` paths are relative to this root. - REPO_OWNER: {repo_owner} - REPO_NAME: {repo_name} - Use these for all GitHub MCP calls and `gh` CLI commands — never guess the owner/repo. Read .squad/agents/{name}/history.md (your project knowledge). Read .squad/decisions.md (team decisions to respect). If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - If .copilot/skills/ has relevant SKILL.md files, read them before working. + If .squad/skills/ has relevant SKILL.md files, read them before working. {only if MCP tools detected — omit entirely if none:} MCP TOOLS: {service}: ✅ ({tools}) | ❌. Fall back to CLI when unavailable. @@ -529,13 +668,11 @@ prompt: | AFTER work: 1. APPEND to .squad/agents/{name}/history.md under "## Learnings": - architecture decisions, reusable patterns, key file paths, API behaviors, team conventions. - ⚠️ DO NOT record: requester names, branch names, session metadata, or one-time task context. - History is for knowledge that will be useful in FUTURE sessions, not session attribution. + architecture decisions, patterns, user preferences, key file paths. 2. If you made a team-relevant decision, write to: .squad/decisions/inbox/{name}-{brief-slug}.md 3. SKILL EXTRACTION: If you found a reusable pattern, write/update - .copilot/skills/{skill-name}/SKILL.md (read templates/skill.md for format). + .squad/skills/{skill-name}/SKILL.md (read templates/skill.md for format). ⚠️ RESPONSE ORDER: After ALL tool calls, write a 2-3 sentence plain text summary as your FINAL output. No tool calls after this summary. @@ -566,44 +703,6 @@ After each batch of agent work: 1. **Collect results** via `read_agent` (wait: true, timeout: 300). -1b. **Verification (when issue has checklist)** - - **Trigger:** The work was for a GitHub issue AND the issue body contains `- [ ]` checkboxes. - - **Skip when:** No issue context, no checkboxes, or this is a re-verification pass at retry limit. - - 1. Extract all `- [ ]` items from the issue body. - 2. Spawn a **verification agent** (sync, `claude-haiku-4.5`) — MUST be a different agent than the one who did the work. Provide: issue body, checklist items, agent's work summary, files changed. - - ``` - agent_type: "general-purpose" - model: "claude-haiku-4.5" - mode: "sync" - description: "🔍 Verify: {issue title}" - prompt: | - You are a verification agent. Your ONLY job is to check work completeness. - - ISSUE BODY: - {issue_body} - - CHECKLIST ITEMS (extracted): - {extracted_checklist} - - AGENT WORK SUMMARY: - {agent_result_summary} - - FILES CHANGED: - {files_changed} - - For EACH checklist item, determine: ✅ addressed or ❌ not addressed. - If not addressed, explain what's missing. - - Final verdict: PASS (all items addressed) or FAIL (gap list below). - ``` - - 3. **PASS** → proceed to Step 2 normally. - 4. **FAIL** → post gap list as issue comment, do NOT mark done, re-route to original agent (or different agent per lockout rules) with gap list as context. After fix attempt, re-run verification (max 2 retries). After 2 failed re-verifications → escalate to user. - 2. **Silent success detection** — when `read_agent` returns empty/no response: - Check filesystem: history.md modified? New decision inbox files? Output files created? - Files found → `"⚠️ {Name} completed (files verified) but response lost."` Treat as DONE. @@ -625,12 +724,12 @@ prompt: | SPAWN MANIFEST: {spawn_manifest} Tasks (in order): - 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use filename-safe ISO 8601 UTC timestamp (replace colons with hyphens, e.g., `2026-02-23T20-16-27Z` not `2026-02-23T20:16:27Z`). - 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use filename-safe ISO 8601 UTC timestamp (replace colons with hyphens, e.g., `2026-02-23T20-16-27Z`). + 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. + 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. 3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. 4. CROSS-AGENT: Append team updates to affected agents' history.md. 5. DECISIONS ARCHIVE: If decisions.md exceeds ~20KB, archive entries older than 30 days to decisions-archive.md. - 6. GIT COMMIT: Stage with `git add .squad/`, then unstage runtime state that must not reach protected branches: `git reset HEAD -- .squad/orchestration-log/ .squad/log/ .squad/decisions/inbox/ .squad/sessions/ 2>/dev/null`. Commit remaining staged changes (write msg to temp file, use -F). Skip if nothing staged after reset. + 6. GIT COMMIT: git add .squad/ && commit (write msg to temp file, use -F). Skip if nothing staged. 7. HISTORY SUMMARIZATION: If any history.md >12KB, summarize old entries to ## Core Context. Never speak to user. ⚠️ End with plain text summary after all tool calls. @@ -658,7 +757,7 @@ Ceremonies are structured team meetings where agents align before or after work. If the user says "I need a designer" or "add someone for DevOps": 1. **Allocate a name** from the current assignment's universe (read from `.squad/casting/history.json`). If the universe is exhausted, apply overflow handling (see Casting & Persistent Naming → Overflow Handling). -2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.copilot/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. +2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.squad/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. 3. Generate a new charter.md + history.md (seeded with project context from team.md), using the cast name. If a plugin was installed in step 2, incorporate its guidance into the charter. 4. **Update `.squad/casting/registry.json`** with the new agent entry. 5. Add to team.md roster. @@ -681,7 +780,7 @@ If the user wants to remove someone: **Core rules (always loaded):** - Check `.squad/plugins/marketplaces.json` during Add Team Member flow (after name allocation, before charter) - Present matching plugins for user approval -- Install: copy to `.copilot/skills/{plugin-name}/SKILL.md`, log to history.md +- Install: copy to `.squad/skills/{plugin-name}/SKILL.md`, log to history.md - Skip silently if no marketplaces configured --- @@ -740,7 +839,6 @@ After selecting a universe: 5. Store the mapping in `.squad/casting/registry.json`. 5. Record the assignment snapshot in `.squad/casting/history.json`. 6. Use the allocated name everywhere: charter.md, history.md, team.md, routing.md, spawn prompts. -7. **Label-safe names:** Agent names used in GitHub/ADO labels MUST be lowercase ASCII only — no emoji, no special characters, no spaces. The label format is always `squad:{lowercase-name}` (e.g., `squad:parker`, not `squad:🔧 Parker`). Strip emoji and normalize to lowercase when generating labels from roster names. ### Overflow Handling @@ -778,45 +876,32 @@ When `.squad/team.md` exists but `.squad/casting/` does not: - **1-2 agents per question, not all of them.** Not everyone needs to speak. - **Decisions are shared, knowledge is personal.** decisions.md is the shared brain. history.md is individual. - **When in doubt, pick someone and go.** Speed beats perfection. -- **Product Isolation Rule (hard rule):** Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot"). - **Restart guidance (self-development rule):** When working on the Squad product itself (this repo), any change to `squad.agent.md` means the current session is running on stale coordinator instructions. After shipping changes to `squad.agent.md`, tell the user: *"🔄 squad.agent.md has been updated. Restart your session to pick up the new coordinator behavior."* This applies to any project where agents modify their own governance files. --- ## Reviewer Rejection Protocol -**Skill:** Read `.copilot/skills/reviewer-protocol/SKILL.md` when a Reviewer rejects work. - -**Core rules (always loaded):** -- On rejection: original author is **locked out** — may NOT self-revise, no exceptions -- A different agent MUST own the revision (Reviewer chooses: reassign or escalate) -- Coordinator verifies mechanically: if Reviewer names original author, refuse and ask for a different agent -- Lockout scope: per-artifact only (author can work on other artifacts) -- Lockout duration: persists through revision cycle -- Deadlock: if all eligible agents locked out, escalate to user (never re-admit) +When a team member has a **Reviewer** role (e.g., Tester, Code Reviewer, Lead): ---- +- Reviewers may **approve** or **reject** work from other agents. +- On **rejection**, the Reviewer may choose ONE of: + 1. **Reassign:** Require a *different* agent to do the revision (not the original author). + 2. **Escalate:** Require a *new* agent be spawned with specific expertise. +- The Coordinator MUST enforce this. If the Reviewer says "someone else should fix this," the original agent does NOT get to self-revise. +- If the Reviewer approves, work proceeds normally. -## Agent Error Lockout +### Reviewer Rejection Lockout Semantics — Strict Lockout -**Purpose:** Prevent error loops. After 2 cumulative errors on the same task, an agent is locked out and a different agent takes over. +When an artifact is **rejected** by a Reviewer: -**Core rules (always loaded):** -- Track errors per agent per session, scoped to the current task -- **What counts as an error:** - 1. Build failure caused by the agent's changes - 2. Test failure caused by the agent's changes - 3. Reviewer rejection (per Reviewer Rejection Protocol above) -- After **2 cumulative errors** on the same task: agent is **locked out** for that task only -- The locked-out agent CAN still work on OTHER, unrelated tasks -- A different agent MUST take over the locked-out agent's work on the failed task -- **Coordinator enforcement:** - - Track error count per agent per task - - When presenting lockout: `"⚠️ {Name} has hit 2 errors on this task — locking out. {OtherAgent} is taking over."` - - Choose replacement agent based on domain fit from roster -- **Scribe logging:** Log lockout event to orchestration log with agent name, task, error count, and replacement agent - -**Note:** This policy SUPPLEMENTS the Reviewer Rejection Protocol. Reviewer rejections count toward the 2-error limit. If an agent is already locked out per Reviewer Rejection Protocol for an artifact, and then hits another error class (e.g., test failure) on the same task, the lockout continues — a different agent was already assigned. +1. **The original author is locked out.** They may NOT produce the next version of that artifact. No exceptions. +2. **A different agent MUST own the revision.** The Coordinator selects the revision author based on the Reviewer's recommendation (reassign or escalate). +3. **The Coordinator enforces this mechanically.** Before spawning a revision agent, the Coordinator MUST verify that the selected agent is NOT the original author. If the Reviewer names the original author as the fix agent, the Coordinator MUST refuse and ask the Reviewer to name a different agent. +4. **The locked-out author may NOT contribute to the revision** in any form — not as a co-author, advisor, or pair. The revision must be independently produced. +5. **Lockout scope:** The lockout applies to the specific artifact that was rejected. The original author may still work on other unrelated artifacts. +6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise. +7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author. --- @@ -867,65 +952,6 @@ Before connecting to a GitHub repository, verify that the `gh` CLI is available --- -## Platform Detection - -On session start, detect the platform from git remote: -- `github.com` → Use GitHub commands (`gh` CLI) -- `dev.azure.com` or `*.visualstudio.com` → Use Azure DevOps commands (`az` CLI) - -If `squad.config.ts` specifies `workItems: 'planner'`, use Microsoft Planner for work items regardless of where the repo lives. - -### Azure DevOps Mode - -If the git remote points to Azure DevOps: - -| GitHub concept | Azure DevOps equivalent | Command change | -|---|---|---| -| `gh issue list` | WIQL query via `az boards query` | `az boards query --wiql "SELECT ... FROM WorkItems WHERE ..."` | -| `gh pr list` | `az repos pr list` | `az repos pr list --status active` | -| `gh pr create` | `az repos pr create` | `az repos pr create --source-branch ... --target-branch ...` | -| `gh pr merge` | `az repos pr update --status completed` | Set PR status to completed | -| Issue labels | Work Item tags | `az boards work-item update --fields "System.Tags=..."` | -| `squad:{member}` label | `squad:{member}` tag on work items | Tags use `;` separator | - -**Prerequisites for Azure DevOps:** -1. Run `az --version`. If missing: *"Azure DevOps mode requires the Azure CLI. Install from https://aka.ms/install-az-cli"* -2. Run `az extension show --name azure-devops`. If missing: *"Run `az extension add --name azure-devops`"* -3. Run `az account show`. If not logged in: *"Run `az login` to authenticate"* -4. Verify defaults: `az devops configure --list` — org and project must be set - -**ADO Work Item Config (`.squad/config.json`):** - -Read the `ado` section from `.squad/config.json` to resolve org, project, work item type, area/iteration paths: - -```json -{ - "platform": "azure-devops", - "ado": { - "org": "my-org", - "project": "work-items-project", - "defaultWorkItemType": "Scenario", - "areaPath": "MyProject\\Team Alpha", - "iterationPath": "MyProject\\Sprint 5" - } -} -``` - -- If `ado.org`/`ado.project` are set, use them for ALL work item operations (they may differ from the repo's org/project) -- If not set, parse org/project from `git remote get-url origin` -- Pass `--org https://dev.azure.com/{org} --project {project}` on every `az boards` command -- Use `ado.defaultWorkItemType` when creating work items (default: "User Story") - -**Ralph on Azure DevOps:** -- **Read `.squad/config.json`** first — the `ado` section tells you which org/project to query for work items -- Replace `gh issue list --label "squad:untriaged"` with WIQL: `az boards query --wiql "SELECT ... WHERE [System.Tags] Contains 'squad:untriaged' AND [System.TeamProject] = '{project}'" --org ... --project ...` -- Replace `gh issue list --label "squad:{member}"` with WIQL: `az boards query --wiql "SELECT ... WHERE [System.Tags] Contains 'squad:{member}'" --org ... --project ...` -- Replace `gh pr list` with `az repos pr list` (uses repo org/project, not work item org/project) -- When creating work items, use `ado.defaultWorkItemType`, include `ado.areaPath` and `ado.iterationPath` if configured -- Branch naming stays the same: `squad/{issue-number}-{slug}` - ---- - ## Ralph — Work Monitor Ralph is a built-in squad member whose job is keeping tabs on work. **Ralph tracks and drives the work queue.** Always on the roster, one job: make sure the team never sits idle. @@ -950,7 +976,7 @@ Ralph always appears in `team.md`: `| Ralph | Work Monitor | — | 🔄 Monitor | "Ralph, idle" / "Take a break" / "Stop monitoring" | Fully deactivate (stop loop + idle-watch) | | "Ralph, scope: just issues" / "Ralph, skip CI" | Adjust what Ralph monitors this session | | References PR feedback or changes requested | Spawn agent to address PR review feedback | -| "merge PR #N" / "merge it" (recent context) | Merge via `gh pr merge` (GitHub) or `az repos pr update --status completed` (ADO) | +| "merge PR #N" / "merge it" (recent context) | Merge via `gh pr merge` | These are intent signals, not exact strings — match meaning, not words. @@ -958,9 +984,6 @@ When Ralph is active, run this check cycle after every batch of agent work compl **Step 1 — Scan for work** (run these in parallel): -> **Platform-aware:** Detect the platform from git remote. If Azure DevOps, read `.squad/config.json` for the `ado` section FIRST — it tells you which org/project to query for work items (may differ from the repo). Use `az boards query` / `az repos pr list` instead of `gh`. If Planner, use Graph API. Do NOT guess the ADO project from the repo name — read the config. - -**GitHub:** ```bash # Untriaged issues (labeled squad but no squad:{member} sub-label) gh issue list --label "squad" --state open --json number,title,labels,assignees --limit 20 @@ -975,28 +998,6 @@ gh pr list --state open --json number,title,author,labels,isDraft,reviewDecision gh pr list --state open --draft --json number,title,author,labels,checks --limit 20 ``` -**Azure DevOps:** -```bash -# Read org/project from .squad/config.json → ado.org, ado.project -# Fall back to git remote URL parsing if not configured - -# Untriaged work items -az boards query --wiql "SELECT [System.Id],[System.Title],[System.State],[System.Tags] FROM WorkItems WHERE [System.Tags] Contains 'squad:untriaged' AND [System.TeamProject] = '{project}' ORDER BY [System.CreatedDate] DESC" --org "https://dev.azure.com/{org}" --project "{project}" --output table - -# Member-assigned work items -az boards query --wiql "SELECT [System.Id],[System.Title],[System.State],[System.Tags] FROM WorkItems WHERE [System.Tags] Contains 'squad:{member}' AND [System.State] <> 'Closed' AND [System.TeamProject] = '{project}' ORDER BY [System.CreatedDate] DESC" --org "https://dev.azure.com/{org}" --project "{project}" --output table - -# Open PRs (uses repo org/project, NOT work item org/project) -az repos pr list --status active --output table - -# Create a work item (uses configured type, area path, iteration path) -az boards work-item create --type "{ado.defaultWorkItemType}" --title "{title}" --fields "System.Tags=squad; squad:untriaged" --org "https://dev.azure.com/{org}" --project "{project}" -``` - -**Step 1b — Parent label hygiene (defensive):** - -When scanning for `squad:{member}` labels, always verify the parent `squad` label exists on each issue. If an issue has any `squad:{member}` label but is missing the parent `squad` label, add it. This prevents issues from becoming invisible to Ralph's untriaged-issue scan, which filters on the `squad` label. - **Step 2 — Categorize findings:** | Category | Signal | Action | diff --git a/.squad-templates/squad.agent.md b/.squad-templates/squad.agent.md index e0957fd27..376b0d5f0 100644 --- a/.squad-templates/squad.agent.md +++ b/.squad-templates/squad.agent.md @@ -311,7 +311,13 @@ For read-only queries, use the explore agent: `agent_type: "explore"` with `"You Before spawning an agent, determine which model to use. Check these layers in order — first match wins: -**Layer 1 — User Override:** Did the user specify a model? ("use opus", "save costs", "use gpt-5.2-codex for this"). If yes, use that model. Session-wide directives ("always use haiku") persist until contradicted. +**Layer 0 — Persistent Config (`.squad/config.json`):** On session start, read `.squad/config.json`. If `agentModelOverrides.{agentName}` exists, use that model for this specific agent. Otherwise, if `defaultModel` exists, use it for ALL agents. This layer survives across sessions — the user set it once and it sticks. + +- **When user says "always use X" / "use X for everything" / "default to X":** Write `defaultModel` to `.squad/config.json`. Acknowledge: `✅ Model preference saved: {model} — all future sessions will use this until changed.` +- **When user says "use X for {agent}":** Write to `agentModelOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use {model} — saved to config.` +- **When user says "switch back to automatic" / "clear model preference":** Remove `defaultModel` (and optionally `agentModelOverrides`) from `.squad/config.json`. Acknowledge: `✅ Model preference cleared — returning to automatic selection.` + +**Layer 1 — Session Directive:** Did the user specify a model for this session? ("use opus for this session", "save costs"). If yes, use that model. Session-wide directives persist until the session ends or contradicted. **Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. diff --git a/.squad/config.json b/.squad/config.json index 817451138..8066cc05c 100644 --- a/.squad/config.json +++ b/.squad/config.json @@ -1,3 +1,4 @@ { - "version": 1 + "version": 1, + "defaultModel": "claude-sonnet-4.6" } \ No newline at end of file diff --git a/.squad/skills/model-selection/SKILL.md b/.squad/skills/model-selection/SKILL.md new file mode 100644 index 000000000..4c6866fd4 --- /dev/null +++ b/.squad/skills/model-selection/SKILL.md @@ -0,0 +1,117 @@ +# Model Selection + +> Determines which LLM model to use for each agent spawn. + +## SCOPE + +✅ THIS SKILL PRODUCES: +- A resolved `model` parameter for every `task` tool call +- Persistent model preferences in `.squad/config.json` +- Spawn acknowledgments that include the resolved model + +❌ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Model performance benchmarks +- Cost reports or billing artifacts + +## Context + +Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions. + +## 5-Layer Model Resolution Hierarchy + +Resolution is **first-match-wins** — the highest layer with a value wins. + +| Layer | Name | Source | Persistence | +|-------|------|--------|-------------| +| **0a** | Per-Agent Config | `.squad/config.json` → `agentModelOverrides.{name}` | Persistent (survives sessions) | +| **0b** | Global Config | `.squad/config.json` → `defaultModel` | Persistent (survives sessions) | +| **1** | Session Directive | User said "use X" in current session | Session-only | +| **2** | Charter Preference | Agent's `charter.md` → `## Model` section | Persistent (in charter) | +| **3** | Task-Aware Auto | Code → sonnet, docs → haiku, visual → opus | Computed per-spawn | +| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback | + +**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional — the user explicitly chose quality over cost. + +## AGENT WORKFLOW + +### On Session Start + +1. READ `.squad/config.json` +2. CHECK for `defaultModel` field — if present, this is the Layer 0 override for all spawns +3. CHECK for `agentModelOverrides` field — if present, these are per-agent Layer 0a overrides +4. STORE both values in session context for the duration + +### On Every Agent Spawn + +1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? → Use it. +2. CHECK Layer 0b: Is there a `defaultModel` in config.json? → Use it. +3. CHECK Layer 1: Did the user give a session directive? → Use it. +4. CHECK Layer 2: Does the agent's charter have a `## Model` section? → Use it. +5. CHECK Layer 3: Determine task type: + - Code (implementation, tests, refactoring, bug fixes) → `claude-sonnet-4.6` + - Prompts, agent designs → `claude-sonnet-4.6` + - Visual/design with image analysis → `claude-opus-4.6` + - Non-code (docs, planning, triage, changelogs) → `claude-haiku-4.5` +6. FALLBACK Layer 4: `claude-haiku-4.5` +7. INCLUDE model in spawn acknowledgment: `🔧 {Name} ({resolved_model}) — {task}` + +### When User Sets a Preference + +**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X" + +1. VALIDATE the model ID against the catalog (18+ models) +2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite) +3. ACKNOWLEDGE: `✅ Model preference saved: {model} — all future sessions will use this until changed.` + +**Per-agent trigger:** "use X for {agent}" + +1. VALIDATE model ID +2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json` +3. ACKNOWLEDGE: `✅ {Agent} will always use {model} — saved to config.` + +### When User Clears a Preference + +**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models" + +1. REMOVE `defaultModel` from `.squad/config.json` +2. ACKNOWLEDGE: `✅ Model preference cleared — returning to automatic selection.` + +### STOP + +After resolving the model and including it in the spawn template, this skill is done. Do NOT: +- Generate model comparison reports +- Run benchmarks or speed tests +- Create new config files (only modify existing `.squad/config.json`) +- Change the model after spawn (fallback chains handle runtime failures) + +## Config Schema + +`.squad/config.json` model-related fields: + +```json +{ + "version": 1, + "defaultModel": "claude-opus-4.6", + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6", + "mcmanus": "claude-haiku-4.5" + } +} +``` + +- `defaultModel` — applies to ALL agents unless overridden by `agentModelOverrides` +- `agentModelOverrides` — per-agent overrides that take priority over `defaultModel` +- Both fields are optional. When absent, Layers 1-4 apply normally. + +## Fallback Chains + +If a model is unavailable (rate limit, plan restriction), retry within the same tier: + +``` +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 +Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 +Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini +``` + +**Never fall UP in tier.** A fast task won't land on a premium model via fallback. diff --git a/docs/src/content/docs/features/model-selection.md b/docs/src/content/docs/features/model-selection.md index 3a26a51fa..6516365e3 100644 --- a/docs/src/content/docs/features/model-selection.md +++ b/docs/src/content/docs/features/model-selection.md @@ -3,7 +3,12 @@ > ⚠️ **Experimental** — Squad is alpha software. APIs, commands, and behavior may change between releases. -**Try this to prioritize quality for the session:** +**Try this to set a persistent preference (survives across sessions):** +``` +Always use Opus +``` + +**Try this to prioritize quality for the session only:** ``` Have all agents use Opus for the rest of this session ``` @@ -18,21 +23,27 @@ Switch to Haiku — I'm trying to save costs Use Sonnet for code, Haiku for everything else ``` -Squad adjusts model selection based on your directive. Agents writing code get quality models (Sonnet/Opus), agents doing docs/logs get cost-optimized models (Haiku). You can override anytime. +**Try this to go back to automatic selection:** +``` +Switch back to automatic model selection +``` + +Squad adjusts model selection based on your directive. Agents writing code get quality models (Sonnet/Opus), agents doing docs/logs get cost-optimized models (Haiku). You can override anytime — and persistent overrides survive across sessions. --- ## How It Works -Squad routes each agent to the right model based on what they're doing — not a one-size-fits-all default. The governing principle: **cost first, unless code is being written.** +Squad routes each agent to the right model based on what they're doing — not a one-size-fits-all default. The governing principle: **cost first, unless code is being written** — but your preferences always take priority. -## How It Works +## 5-Layer Model Resolution Model selection uses a layered system. First match wins: -1. **User Override** — You said "use opus" or "save costs"? Done. Session-wide directives persist until contradicted. -2. **Charter Preference** — The agent's charter specifies a `## Model` section with a preferred model. -3. **Task-Aware Auto-Selection** — The coordinator checks what the agent is actually doing: +1. **Persistent Config** (`.squad/config.json`) — You said "always use opus"? It's saved to disk. Every session, every agent, until you change it. Per-agent overrides (`agentModelOverrides`) take priority over the global `defaultModel`. +2. **Session Directive** — You said "use opus for this session"? Done. Applies until the session ends. +3. **Charter Preference** — The agent's charter specifies a `## Model` section with a preferred model. +4. **Task-Aware Auto-Selection** — The coordinator checks what the agent is actually doing: | Task Output | Model | Tier | |-------------|-------|------| @@ -41,7 +52,26 @@ Model selection uses a layered system. First match wins: | Non-code work (docs, planning, triage, changelogs) | `claude-haiku-4.5` | Fast | | Visual/design work requiring image analysis | `claude-opus-4.6` | Premium | -4. **Default** — If nothing matched, `claude-haiku-4.5`. Cost wins when in doubt. +5. **Default** — If nothing matched, `claude-haiku-4.5`. Cost wins when in doubt. + +## Persistent Model Preferences + +Squad stores your model preferences in `.squad/config.json`: + +```json +{ + "version": 1, + "defaultModel": "claude-opus-4.6", + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6", + "mcmanus": "claude-haiku-4.5" + } +} +``` + +- **`defaultModel`** — applies to ALL agents unless overridden. Set with "always use X". +- **`agentModelOverrides`** — per-agent overrides. Set with "use X for {agent}". +- **Clear with** "switch back to automatic" — removes `defaultModel`, returns to auto-selection. ## Role-to-Model Mapping @@ -80,9 +110,10 @@ Never falls back UP in tier — a fast task won't land on a premium model. Tell the coordinator what you want: -- `"use opus for this"` — one-off premium -- `"always use haiku"` — session-wide cost savings -- `"use gpt-5.2-codex for Fenster"` — agent-specific override +- `"use opus for this"` — one-off premium for current task +- `"always use opus"` — **persistent** preference saved to `.squad/config.json` (survives sessions) +- `"use gpt-5.2-codex for Fenster"` — **persistent** per-agent override +- `"switch back to automatic"` — clears persistent preference ## Sample Prompts diff --git a/packages/squad-cli/templates/skills/model-selection/SKILL.md b/packages/squad-cli/templates/skills/model-selection/SKILL.md index 611941bab..4c6866fd4 100644 --- a/packages/squad-cli/templates/skills/model-selection/SKILL.md +++ b/packages/squad-cli/templates/skills/model-selection/SKILL.md @@ -1,143 +1,117 @@ ---- -name: "model-selection" -description: "Per-agent model selection with 4-layer hierarchy and fallback chains" -domain: "orchestration" -confidence: "high" -source: "extracted" ---- +# Model Selection + +> Determines which LLM model to use for each agent spawn. + +## SCOPE + +✅ THIS SKILL PRODUCES: +- A resolved `model` parameter for every `task` tool call +- Persistent model preferences in `.squad/config.json` +- Spawn acknowledgments that include the resolved model + +❌ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Model performance benchmarks +- Cost reports or billing artifacts ## Context -Before spawning an agent, the coordinator determines which model to use. This skill codifies the 4-layer hierarchy, role-to-model mappings, task complexity adjustments, and fallback chains. Applies to all agent spawns in Team Mode. +Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions. -## Patterns +## 5-Layer Model Resolution Hierarchy -### 4-Layer Hierarchy +Resolution is **first-match-wins** — the highest layer with a value wins. -Check these layers in order — first match wins: +| Layer | Name | Source | Persistence | +|-------|------|--------|-------------| +| **0a** | Per-Agent Config | `.squad/config.json` → `agentModelOverrides.{name}` | Persistent (survives sessions) | +| **0b** | Global Config | `.squad/config.json` → `defaultModel` | Persistent (survives sessions) | +| **1** | Session Directive | User said "use X" in current session | Session-only | +| **2** | Charter Preference | Agent's `charter.md` → `## Model` section | Persistent (in charter) | +| **3** | Task-Aware Auto | Code → sonnet, docs → haiku, visual → opus | Computed per-spawn | +| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback | -**Layer 1 — User Override:** Did the user specify a model? ("use opus", "save costs", "use gpt-5.2-codex for this"). If yes, use that model. Session-wide directives ("always use haiku") persist until contradicted. +**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional — the user explicitly chose quality over cost. -**Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. +## AGENT WORKFLOW -**Layer 3 — Task-Aware Auto-Selection:** Use the governing principle: **cost first, unless code is being written.** Match the agent's task to determine output type, then select accordingly: +### On Session Start -| Task Output | Model | Tier | Rule | -|-------------|-------|------|------| -| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.6` | Standard | Quality and accuracy matter for code. Use standard tier. | -| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.6` | Standard | Prompts are executable — treat like code. | -| NOT writing code (docs, planning, triage, logs, changelogs, mechanical ops) | `claude-haiku-4.5` | Fast | Cost first. Haiku handles non-code tasks. | -| Visual/design work requiring image analysis | `claude-opus-4.5` | Premium | Vision capability required. Overrides cost rule. | +1. READ `.squad/config.json` +2. CHECK for `defaultModel` field — if present, this is the Layer 0 override for all spawns +3. CHECK for `agentModelOverrides` field — if present, these are per-agent Layer 0a overrides +4. STORE both values in session context for the duration -**Role-to-model mapping** (applying cost-first principle): +### On Every Agent Spawn -| Role | Default Model | Why | Override When | -|------|--------------|-----|---------------| -| Core Dev / Backend / Frontend | `claude-sonnet-4.6` | Writes code — quality first | Heavy code gen → `gpt-5.3-codex` | -| Tester / QA | `claude-sonnet-4.6` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | -| Lead / Architect | auto (per-task) | Mixed: code review needs quality, planning needs cost | Architecture proposals → premium; triage/planning → haiku | -| Prompt Engineer | auto (per-task) | Mixed: prompt design is like code, research is not | Prompt architecture → sonnet; research/analysis → haiku | -| Copilot SDK Expert | `claude-sonnet-4.6` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | -| Designer / Visual | `claude-opus-4.6` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | -| DevRel / Writer | `claude-haiku-4.5` | Docs and writing — not code | — | -| Scribe / Logger | `claude-haiku-4.5` | Mechanical file ops — cheapest possible | — (never bump Scribe) | -| Git / Release | `claude-haiku-4.5` | Mechanical ops — changelogs, tags, version bumps | — (never bump mechanical ops) | +1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? → Use it. +2. CHECK Layer 0b: Is there a `defaultModel` in config.json? → Use it. +3. CHECK Layer 1: Did the user give a session directive? → Use it. +4. CHECK Layer 2: Does the agent's charter have a `## Model` section? → Use it. +5. CHECK Layer 3: Determine task type: + - Code (implementation, tests, refactoring, bug fixes) → `claude-sonnet-4.6` + - Prompts, agent designs → `claude-sonnet-4.6` + - Visual/design with image analysis → `claude-opus-4.6` + - Non-code (docs, planning, triage, changelogs) → `claude-haiku-4.5` +6. FALLBACK Layer 4: `claude-haiku-4.5` +7. INCLUDE model in spawn acknowledgment: `🔧 {Name} ({resolved_model}) — {task}` -**Task complexity adjustments** (apply at most ONE — no cascading): -- **Bump UP to premium:** architecture proposals, reviewer gates, security audits, multi-agent coordination (output feeds 3+ agents) -- **Bump DOWN to fast/cheap:** typo fixes, renames, boilerplate, scaffolding, changelogs, version bumps -- **Switch to code specialist (`gpt-5.3-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) -- **Switch to analytical diversity (`gemini-3-pro-preview`):** code reviews where a second perspective helps, security reviews, architecture reviews after a rejection +### When User Sets a Preference -**Layer 4 — Default:** If nothing else matched, use `claude-haiku-4.5`. Cost wins when in doubt, unless code is being produced. +**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X" -### Fallback Chains +1. VALIDATE the model ID against the catalog (18+ models) +2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite) +3. ACKNOWLEDGE: `✅ Model preference saved: {model} — all future sessions will use this until changed.` -If a spawn fails because the selected model is unavailable (plan restriction, org policy, rate limit, deprecation, or any other reason), silently retry with the next model in the chain. Do NOT tell the user about fallback attempts. Maximum 3 retries before jumping to the nuclear fallback. +**Per-agent trigger:** "use X for {agent}" -``` -Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 → (omit model param) -Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 → (omit model param) -Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini → (omit model param) -``` +1. VALIDATE model ID +2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json` +3. ACKNOWLEDGE: `✅ {Agent} will always use {model} — saved to config.` -`(omit model param)` = call the `task` tool WITHOUT the `model` parameter. The platform uses its built-in default. This is the nuclear fallback — it always works. +### When User Clears a Preference -**Fallback rules:** -- If the user specified a provider ("use Claude"), fall back within that provider only before hitting nuclear -- Never fall back UP in tier — a fast/cheap task should not land on a premium model -- Log fallbacks to the orchestration log for debugging, but never surface to the user unless asked +**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models" -### Passing the Model to Spawns +1. REMOVE `defaultModel` from `.squad/config.json` +2. ACKNOWLEDGE: `✅ Model preference cleared — returning to automatic selection.` -Pass the resolved model as the `model` parameter on every `task` tool call: +### STOP -``` -agent_type: "general-purpose" -model: "{resolved_model}" -mode: "background" -description: "{emoji} {Name}: {brief task summary}" -prompt: | - ... -``` +After resolving the model and including it in the spawn template, this skill is done. Do NOT: +- Generate model comparison reports +- Run benchmarks or speed tests +- Create new config files (only modify existing `.squad/config.json`) +- Change the model after spawn (fallback chains handle runtime failures) + +## Config Schema -Only set `model` when it differs from the platform default (`claude-sonnet-4.6`). If the resolved model IS `claude-sonnet-4.6`, you MAY omit the `model` parameter — the platform uses it as default. +`.squad/config.json` model-related fields: + +```json +{ + "version": 1, + "defaultModel": "claude-opus-4.6", + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6", + "mcmanus": "claude-haiku-4.5" + } +} +``` -If you've exhausted the fallback chain and reached nuclear fallback, omit the `model` parameter entirely. +- `defaultModel` — applies to ALL agents unless overridden by `agentModelOverrides` +- `agentModelOverrides` — per-agent overrides that take priority over `defaultModel` +- Both fields are optional. When absent, Layers 1-4 apply normally. -### Spawn Output Format +## Fallback Chains -When spawning, include the model in your acknowledgment: +If a model is unavailable (rate limit, plan restriction), retry within the same tier: ``` -🔧 Fenster (claude-sonnet-4.6) — refactoring auth module -🎨 Redfoot (claude-opus-4.6 · vision) — designing color system -📋 Scribe (claude-haiku-4.5 · fast) — logging session -⚡ Keaton (claude-opus-4.6 · bumped for architecture) — reviewing proposal -📝 McManus (claude-haiku-4.5 · fast) — updating docs +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 +Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 +Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini ``` -Include tier annotation only when the model was bumped or a specialist was chosen. Default-tier spawns just show the model name. - -### Valid Models - -**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` - -## Examples - -**Example 1: Backend dev writing API endpoints** -- Role: Backend Dev -- Task: "implement REST endpoints for user management" -- Layer 3 decision: writing code → `claude-sonnet-4.6` (standard tier) -- Spawn: `🔧 Fenster (claude-sonnet-4.6) — implementing user API endpoints` - -**Example 2: User override** -- User says: "use haiku for everything this session" -- Layer 1 overrides all other layers -- All spawns use `claude-haiku-4.5` regardless of role or task - -**Example 3: Complex refactor** -- Role: Backend Dev -- Task: "refactor 15 auth-related files to use new token system" -- Layer 3 base: `claude-sonnet-4.5` -- Task complexity: heavy multi-file refactor → switch to `gpt-5.2-codex` -- Spawn: `🔧 Fenster (gpt-5.2-codex · code specialist) — refactoring auth to new token system` - -**Example 4: Scribe logging** -- Role: Scribe -- Task: "log session to decisions.md" -- Layer 3: NOT writing code → `claude-haiku-4.5` -- Role mapping: Scribe always haiku, never bump -- Spawn: `📋 Scribe (claude-haiku-4.5 · fast) — logging session` - -## Anti-Patterns - -- ❌ Falling back UP in tier (fast task landing on premium model) -- ❌ Telling the user about fallback attempts ("Opus failed, trying Sonnet") -- ❌ Bumping Scribe or mechanical ops agents to higher tiers -- ❌ Using premium models for documentation or planning tasks -- ❌ Applying multiple complexity adjustments (cascading bumps) -- ❌ Forgetting to include model in spawn acknowledgment -- ❌ Downgrading vision-required tasks from opus +**Never fall UP in tier.** A fast task won't land on a premium model via fallback. diff --git a/packages/squad-cli/templates/squad.agent.md b/packages/squad-cli/templates/squad.agent.md index cf3204c09..5e648ae11 100644 --- a/packages/squad-cli/templates/squad.agent.md +++ b/packages/squad-cli/templates/squad.agent.md @@ -22,7 +22,8 @@ You are **Squad (Coordinator)** — the orchestrator for this project's AI team. Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs) - **No** → Init Mode -- **Yes** → Team Mode +- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast) +- **Yes, with roster entries** → Team Mode --- @@ -32,20 +33,14 @@ No team exists yet. Propose one — but **DO NOT create any files until the user 1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey Brady, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** 2. Ask: *"What are you building? (language, stack, what it does)"* -3. **Match to base roles.** Squad ships with 20 built-in base roles that have deep, curated charter content. Before proposing custom roles, check the catalog: - - **Software Development:** lead, frontend, backend, fullstack, reviewer, tester, devops, security, data, docs, ai, designer - **Business & Operations:** marketing-strategist, sales-strategist, product-manager, project-manager, support-specialist, game-developer, media-buyer, compliance-legal - - Prefer base roles — they provide substantive expertise, boundaries, and voice out of the box. Only propose a custom role if no base role fits the need. When using a base role, use its ID in the Role field (e.g., "backend" not "Backend Developer") so the system generates a rich charter from the catalog. -4. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): +3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): - Determine team size (typically 4–5 + Scribe). - Determine assignment shape from the user's project description. - Derive resonance signals from the session and repo context. - Select a universe. Allocate character names from that universe. - Scribe is always "Scribe" — exempt from casting. - Ralph is always "Ralph" — exempt from casting. -5. Propose the team with their cast names. Example (names will vary per cast): +4. Propose the team with their cast names. Example (names will vary per cast): ``` 🏗️ {CastName1} — Lead Scope, decisions, code review @@ -56,7 +51,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user 🔄 Ralph — (monitor) Work queue, backlog, keep-alive ``` -6. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: - **question:** *"Look right?"* - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` @@ -68,7 +63,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user **Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). -> If the user said "add someone" or "change a role," go back to Phase 1 step 4 and re-propose. Do NOT enter Phase 2 until the user confirms. +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. 6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). @@ -234,7 +229,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | -**Skill-aware routing:** Before spawning, check `.copilot/skills/` for skills relevant to the task domain. If a matching skill exists, add to the spawn prompt: `Relevant skill: .copilot/skills/{name}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. +**Skill-aware routing:** Before spawning, check `.squad/skills/` for skills relevant to the task domain. If a matching skill exists, add to the spawn prompt: `Relevant skill: .squad/skills/{name}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. ### Skill Confidence Lifecycle @@ -316,7 +311,13 @@ For read-only queries, use the explore agent: `agent_type: "explore"` with `"You Before spawning an agent, determine which model to use. Check these layers in order — first match wins: -**Layer 1 — User Override:** Did the user specify a model? ("use opus", "save costs", "use gpt-5.2-codex for this"). If yes, use that model. Session-wide directives ("always use haiku") persist until contradicted. +**Layer 0 — Persistent Config (`.squad/config.json`):** On session start, read `.squad/config.json`. If `agentModelOverrides.{agentName}` exists, use that model for this specific agent. Otherwise, if `defaultModel` exists, use it for ALL agents. This layer survives across sessions — the user set it once and it sticks. + +- **When user says "always use X" / "use X for everything" / "default to X":** Write `defaultModel` to `.squad/config.json`. Acknowledge: `✅ Model preference saved: {model} — all future sessions will use this until changed.` +- **When user says "use X for {agent}":** Write to `agentModelOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use {model} — saved to config.` +- **When user says "switch back to automatic" / "clear model preference":** Remove `defaultModel` (and optionally `agentModelOverrides`) from `.squad/config.json`. Acknowledge: `✅ Model preference cleared — returning to automatic selection.` + +**Layer 1 — Session Directive:** Did the user specify a model for this session? ("use opus for this session", "save costs"). If yes, use that model. Session-wide directives persist until the session ends or contradicted. **Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. @@ -454,7 +455,7 @@ The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitH MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. -> **Full patterns:** Read `.copilot/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, graceful degradation. Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. +> **Full patterns:** Read `.squad/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, graceful degradation. Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. #### Detection @@ -649,7 +650,7 @@ prompt: | Read .squad/decisions.md (team decisions to respect). If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - If .copilot/skills/ has relevant SKILL.md files, read them before working. + If .squad/skills/ has relevant SKILL.md files, read them before working. {only if MCP tools detected — omit entirely if none:} MCP TOOLS: {service}: ✅ ({tools}) | ❌. Fall back to CLI when unavailable. @@ -671,7 +672,7 @@ prompt: | 2. If you made a team-relevant decision, write to: .squad/decisions/inbox/{name}-{brief-slug}.md 3. SKILL EXTRACTION: If you found a reusable pattern, write/update - .copilot/skills/{skill-name}/SKILL.md (read templates/skill.md for format). + .squad/skills/{skill-name}/SKILL.md (read templates/skill.md for format). ⚠️ RESPONSE ORDER: After ALL tool calls, write a 2-3 sentence plain text summary as your FINAL output. No tool calls after this summary. @@ -702,44 +703,6 @@ After each batch of agent work: 1. **Collect results** via `read_agent` (wait: true, timeout: 300). -1b. **Verification (when issue has checklist)** - - **Trigger:** The work was for a GitHub issue AND the issue body contains `- [ ]` checkboxes. - - **Skip when:** No issue context, no checkboxes, or this is a re-verification pass at retry limit. - - 1. Extract all `- [ ]` items from the issue body. - 2. Spawn a **verification agent** (sync, `claude-haiku-4.5`) — MUST be a different agent than the one who did the work. Provide: issue body, checklist items, agent's work summary, files changed. - - ``` - agent_type: "general-purpose" - model: "claude-haiku-4.5" - mode: "sync" - description: "🔍 Verify: {issue title}" - prompt: | - You are a verification agent. Your ONLY job is to check work completeness. - - ISSUE BODY: - {issue_body} - - CHECKLIST ITEMS (extracted): - {extracted_checklist} - - AGENT WORK SUMMARY: - {agent_result_summary} - - FILES CHANGED: - {files_changed} - - For EACH checklist item, determine: ✅ addressed or ❌ not addressed. - If not addressed, explain what's missing. - - Final verdict: PASS (all items addressed) or FAIL (gap list below). - ``` - - 3. **PASS** → proceed to Step 2 normally. - 4. **FAIL** → post gap list as issue comment, do NOT mark done, re-route to original agent (or different agent per lockout rules) with gap list as context. After fix attempt, re-run verification (max 2 retries). After 2 failed re-verifications → escalate to user. - 2. **Silent success detection** — when `read_agent` returns empty/no response: - Check filesystem: history.md modified? New decision inbox files? Output files created? - Files found → `"⚠️ {Name} completed (files verified) but response lost."` Treat as DONE. @@ -758,14 +721,11 @@ prompt: | You are the Scribe. Read .squad/agents/scribe/charter.md. TEAM ROOT: {team_root} -SPAWN MANIFEST: {spawn_manifest} - - Tasks (in order): - Include token usage from agent responses in the manifest when available. + SPAWN MANIFEST: {spawn_manifest} Tasks (in order): - 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Include token usage (inputTokens, outputTokens, estimatedCostUsd) from the spawn manifest in the **Token usage** row. Use filename-safe ISO 8601 UTC timestamp (replace colons with hyphens, e.g., `2026-02-23T20-16-27Z` not `2026-02-23T20:16:27Z`). - 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use filename-safe ISO 8601 UTC timestamp (replace colons with hyphens, e.g., `2026-02-23T20-16-27Z`). + 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. + 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. 3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. 4. CROSS-AGENT: Append team updates to affected agents' history.md. 5. DECISIONS ARCHIVE: If decisions.md exceeds ~20KB, archive entries older than 30 days to decisions-archive.md. @@ -797,7 +757,7 @@ Ceremonies are structured team meetings where agents align before or after work. If the user says "I need a designer" or "add someone for DevOps": 1. **Allocate a name** from the current assignment's universe (read from `.squad/casting/history.json`). If the universe is exhausted, apply overflow handling (see Casting & Persistent Naming → Overflow Handling). -2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.copilot/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. +2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.squad/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. 3. Generate a new charter.md + history.md (seeded with project context from team.md), using the cast name. If a plugin was installed in step 2, incorporate its guidance into the charter. 4. **Update `.squad/casting/registry.json`** with the new agent entry. 5. Add to team.md roster. @@ -820,7 +780,7 @@ If the user wants to remove someone: **Core rules (always loaded):** - Check `.squad/plugins/marketplaces.json` during Add Team Member flow (after name allocation, before charter) - Present matching plugins for user approval -- Install: copy to `.copilot/skills/{plugin-name}/SKILL.md`, log to history.md +- Install: copy to `.squad/skills/{plugin-name}/SKILL.md`, log to history.md - Skip silently if no marketplaces configured --- @@ -863,7 +823,7 @@ Agent names are drawn from a single fictional universe per assignment. Names are **Rules (always loaded):** - ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. -- 31 universes available (capacity 6–25). See reference file for full list. +- 15 universes available (capacity 6–25). See reference file for full list. - Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. - Same inputs → same choice (unless LRU changes). diff --git a/packages/squad-sdk/src/config/models.ts b/packages/squad-sdk/src/config/models.ts index 92639dee3..f76d95bce 100644 --- a/packages/squad-sdk/src/config/models.ts +++ b/packages/squad-sdk/src/config/models.ts @@ -7,6 +7,8 @@ * @module config/models */ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join } from 'path'; import type { ModelId, ModelTier } from '../runtime/config.js'; /** @@ -460,3 +462,195 @@ export function getFallbackChain(tier: ModelTier): ModelId[] { export function isModelAvailable(id: ModelId): boolean { return defaultRegistry.isModelAvailable(id); } + +// ============================================================================ +// Persistent Model Preference (Layer 0) +// ============================================================================ + +/** + * Shape of model preference fields within `.squad/config.json`. + */ +export interface ModelPreferenceConfig { + defaultModel?: string; + agentModelOverrides?: Record; +} + +/** + * Reads the persistent model preference from `.squad/config.json`. + * + * @param squadDir - Path to the `.squad/` directory + * @returns The defaultModel string if set, or null + */ +export function readModelPreference(squadDir: string): string | null { + const configPath = join(squadDir, 'config.json'); + if (!existsSync(configPath)) { + return null; + } + try { + const raw = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw); + if ( + parsed !== null && + typeof parsed === 'object' && + typeof parsed.defaultModel === 'string' && + parsed.defaultModel.length > 0 + ) { + return parsed.defaultModel; + } + return null; + } catch { + return null; + } +} + +/** + * Reads per-agent model overrides from `.squad/config.json`. + * + * @param squadDir - Path to the `.squad/` directory + * @returns Record of agent name → model ID, or empty object + */ +export function readAgentModelOverrides(squadDir: string): Record { + const configPath = join(squadDir, 'config.json'); + if (!existsSync(configPath)) { + return {}; + } + try { + const raw = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw); + if ( + parsed !== null && + typeof parsed === 'object' && + typeof parsed.agentModelOverrides === 'object' && + parsed.agentModelOverrides !== null + ) { + const result: Record = {}; + for (const [key, value] of Object.entries(parsed.agentModelOverrides)) { + if (typeof value === 'string') { + result[key] = value; + } + } + return result; + } + return {}; + } catch { + return {}; + } +} + +/** + * Writes a persistent model preference to `.squad/config.json`. + * Merges with existing config — does not overwrite other fields. + * + * @param squadDir - Path to the `.squad/` directory + * @param model - Model ID to persist, or null to clear + */ +export function writeModelPreference(squadDir: string, model: string | null): 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 (model === null) { + delete config.defaultModel; + } else { + config.defaultModel = model; + } + + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); +} + +/** + * Writes per-agent model overrides to `.squad/config.json`. + * Merges with existing config — does not overwrite other fields. + * + * @param squadDir - Path to the `.squad/` directory + * @param overrides - Record of agent name → model ID, or null to clear + */ +export function writeAgentModelOverrides( + squadDir: string, + overrides: Record | null +): 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 (overrides === null || Object.keys(overrides).length === 0) { + delete config.agentModelOverrides; + } else { + config.agentModelOverrides = overrides; + } + + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); +} + +/** + * Resolves the effective model for an agent spawn using the 5-layer hierarchy: + * Layer 0: Persistent config (.squad/config.json defaultModel) + * Layer 1: Session-wide user directive ("always use opus") + * Layer 2: Charter preference (agent's ## Model section) + * Layer 3: Task-aware auto-selection (code → sonnet, docs → haiku) + * Layer 4: Default (claude-haiku-4.5) + * + * Per-agent overrides from config.json take priority over the global defaultModel. + * + * @param options - Resolution inputs + * @returns Resolved model ID + */ +export function resolveModel(options: { + agentName?: string; + squadDir?: string; + sessionDirective?: string | null; + charterPreference?: string | null; + taskModel?: string | null; +}): string { + const { agentName, squadDir, sessionDirective, charterPreference, taskModel } = options; + + // Layer 0a: Per-agent persistent override + if (squadDir && agentName) { + const agentOverrides = readAgentModelOverrides(squadDir); + if (agentOverrides[agentName]) { + return agentOverrides[agentName]!; + } + } + + // Layer 0b: Global persistent config + if (squadDir) { + const persistedModel = readModelPreference(squadDir); + if (persistedModel) { + return persistedModel; + } + } + + // Layer 1: Session-wide user directive + if (sessionDirective) { + return sessionDirective; + } + + // Layer 2: Charter preference + if (charterPreference) { + return charterPreference; + } + + // Layer 3: Task-aware auto-selection + if (taskModel) { + return taskModel; + } + + // Layer 4: Default + return 'claude-haiku-4.5'; +} diff --git a/packages/squad-sdk/templates/skills/model-selection/SKILL.md b/packages/squad-sdk/templates/skills/model-selection/SKILL.md index 611941bab..4c6866fd4 100644 --- a/packages/squad-sdk/templates/skills/model-selection/SKILL.md +++ b/packages/squad-sdk/templates/skills/model-selection/SKILL.md @@ -1,143 +1,117 @@ ---- -name: "model-selection" -description: "Per-agent model selection with 4-layer hierarchy and fallback chains" -domain: "orchestration" -confidence: "high" -source: "extracted" ---- +# Model Selection + +> Determines which LLM model to use for each agent spawn. + +## SCOPE + +✅ THIS SKILL PRODUCES: +- A resolved `model` parameter for every `task` tool call +- Persistent model preferences in `.squad/config.json` +- Spawn acknowledgments that include the resolved model + +❌ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Model performance benchmarks +- Cost reports or billing artifacts ## Context -Before spawning an agent, the coordinator determines which model to use. This skill codifies the 4-layer hierarchy, role-to-model mappings, task complexity adjustments, and fallback chains. Applies to all agent spawns in Team Mode. +Squad supports 18+ models across three tiers (premium, standard, fast). The coordinator must select the right model for each agent spawn. Users can set persistent preferences that survive across sessions. -## Patterns +## 5-Layer Model Resolution Hierarchy -### 4-Layer Hierarchy +Resolution is **first-match-wins** — the highest layer with a value wins. -Check these layers in order — first match wins: +| Layer | Name | Source | Persistence | +|-------|------|--------|-------------| +| **0a** | Per-Agent Config | `.squad/config.json` → `agentModelOverrides.{name}` | Persistent (survives sessions) | +| **0b** | Global Config | `.squad/config.json` → `defaultModel` | Persistent (survives sessions) | +| **1** | Session Directive | User said "use X" in current session | Session-only | +| **2** | Charter Preference | Agent's `charter.md` → `## Model` section | Persistent (in charter) | +| **3** | Task-Aware Auto | Code → sonnet, docs → haiku, visual → opus | Computed per-spawn | +| **4** | Default | `claude-haiku-4.5` | Hardcoded fallback | -**Layer 1 — User Override:** Did the user specify a model? ("use opus", "save costs", "use gpt-5.2-codex for this"). If yes, use that model. Session-wide directives ("always use haiku") persist until contradicted. +**Key principle:** Layer 0 (persistent config) beats everything. If the user said "always use opus" and it was saved to config.json, every agent gets opus regardless of role or task type. This is intentional — the user explicitly chose quality over cost. -**Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. +## AGENT WORKFLOW -**Layer 3 — Task-Aware Auto-Selection:** Use the governing principle: **cost first, unless code is being written.** Match the agent's task to determine output type, then select accordingly: +### On Session Start -| Task Output | Model | Tier | Rule | -|-------------|-------|------|------| -| Writing code (implementation, refactoring, test code, bug fixes) | `claude-sonnet-4.6` | Standard | Quality and accuracy matter for code. Use standard tier. | -| Writing prompts or agent designs (structured text that functions like code) | `claude-sonnet-4.6` | Standard | Prompts are executable — treat like code. | -| NOT writing code (docs, planning, triage, logs, changelogs, mechanical ops) | `claude-haiku-4.5` | Fast | Cost first. Haiku handles non-code tasks. | -| Visual/design work requiring image analysis | `claude-opus-4.5` | Premium | Vision capability required. Overrides cost rule. | +1. READ `.squad/config.json` +2. CHECK for `defaultModel` field — if present, this is the Layer 0 override for all spawns +3. CHECK for `agentModelOverrides` field — if present, these are per-agent Layer 0a overrides +4. STORE both values in session context for the duration -**Role-to-model mapping** (applying cost-first principle): +### On Every Agent Spawn -| Role | Default Model | Why | Override When | -|------|--------------|-----|---------------| -| Core Dev / Backend / Frontend | `claude-sonnet-4.6` | Writes code — quality first | Heavy code gen → `gpt-5.3-codex` | -| Tester / QA | `claude-sonnet-4.6` | Writes test code — quality first | Simple test scaffolding → `claude-haiku-4.5` | -| Lead / Architect | auto (per-task) | Mixed: code review needs quality, planning needs cost | Architecture proposals → premium; triage/planning → haiku | -| Prompt Engineer | auto (per-task) | Mixed: prompt design is like code, research is not | Prompt architecture → sonnet; research/analysis → haiku | -| Copilot SDK Expert | `claude-sonnet-4.6` | Technical analysis that often touches code | Pure research → `claude-haiku-4.5` | -| Designer / Visual | `claude-opus-4.6` | Vision-capable model required | — (never downgrade — vision is non-negotiable) | -| DevRel / Writer | `claude-haiku-4.5` | Docs and writing — not code | — | -| Scribe / Logger | `claude-haiku-4.5` | Mechanical file ops — cheapest possible | — (never bump Scribe) | -| Git / Release | `claude-haiku-4.5` | Mechanical ops — changelogs, tags, version bumps | — (never bump mechanical ops) | +1. CHECK Layer 0a: Is there an `agentModelOverrides.{agentName}` in config.json? → Use it. +2. CHECK Layer 0b: Is there a `defaultModel` in config.json? → Use it. +3. CHECK Layer 1: Did the user give a session directive? → Use it. +4. CHECK Layer 2: Does the agent's charter have a `## Model` section? → Use it. +5. CHECK Layer 3: Determine task type: + - Code (implementation, tests, refactoring, bug fixes) → `claude-sonnet-4.6` + - Prompts, agent designs → `claude-sonnet-4.6` + - Visual/design with image analysis → `claude-opus-4.6` + - Non-code (docs, planning, triage, changelogs) → `claude-haiku-4.5` +6. FALLBACK Layer 4: `claude-haiku-4.5` +7. INCLUDE model in spawn acknowledgment: `🔧 {Name} ({resolved_model}) — {task}` -**Task complexity adjustments** (apply at most ONE — no cascading): -- **Bump UP to premium:** architecture proposals, reviewer gates, security audits, multi-agent coordination (output feeds 3+ agents) -- **Bump DOWN to fast/cheap:** typo fixes, renames, boilerplate, scaffolding, changelogs, version bumps -- **Switch to code specialist (`gpt-5.3-codex`):** large multi-file refactors, complex implementation from spec, heavy code generation (500+ lines) -- **Switch to analytical diversity (`gemini-3-pro-preview`):** code reviews where a second perspective helps, security reviews, architecture reviews after a rejection +### When User Sets a Preference -**Layer 4 — Default:** If nothing else matched, use `claude-haiku-4.5`. Cost wins when in doubt, unless code is being produced. +**Trigger phrases:** "always use X", "use X for everything", "switch to X", "default to X" -### Fallback Chains +1. VALIDATE the model ID against the catalog (18+ models) +2. WRITE `defaultModel` to `.squad/config.json` (merge, don't overwrite) +3. ACKNOWLEDGE: `✅ Model preference saved: {model} — all future sessions will use this until changed.` -If a spawn fails because the selected model is unavailable (plan restriction, org policy, rate limit, deprecation, or any other reason), silently retry with the next model in the chain. Do NOT tell the user about fallback attempts. Maximum 3 retries before jumping to the nuclear fallback. +**Per-agent trigger:** "use X for {agent}" -``` -Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 → (omit model param) -Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 → (omit model param) -Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini → (omit model param) -``` +1. VALIDATE model ID +2. WRITE to `agentModelOverrides.{agent}` in `.squad/config.json` +3. ACKNOWLEDGE: `✅ {Agent} will always use {model} — saved to config.` -`(omit model param)` = call the `task` tool WITHOUT the `model` parameter. The platform uses its built-in default. This is the nuclear fallback — it always works. +### When User Clears a Preference -**Fallback rules:** -- If the user specified a provider ("use Claude"), fall back within that provider only before hitting nuclear -- Never fall back UP in tier — a fast/cheap task should not land on a premium model -- Log fallbacks to the orchestration log for debugging, but never surface to the user unless asked +**Trigger phrases:** "switch back to automatic", "clear model preference", "use default models" -### Passing the Model to Spawns +1. REMOVE `defaultModel` from `.squad/config.json` +2. ACKNOWLEDGE: `✅ Model preference cleared — returning to automatic selection.` -Pass the resolved model as the `model` parameter on every `task` tool call: +### STOP -``` -agent_type: "general-purpose" -model: "{resolved_model}" -mode: "background" -description: "{emoji} {Name}: {brief task summary}" -prompt: | - ... -``` +After resolving the model and including it in the spawn template, this skill is done. Do NOT: +- Generate model comparison reports +- Run benchmarks or speed tests +- Create new config files (only modify existing `.squad/config.json`) +- Change the model after spawn (fallback chains handle runtime failures) + +## Config Schema -Only set `model` when it differs from the platform default (`claude-sonnet-4.6`). If the resolved model IS `claude-sonnet-4.6`, you MAY omit the `model` parameter — the platform uses it as default. +`.squad/config.json` model-related fields: + +```json +{ + "version": 1, + "defaultModel": "claude-opus-4.6", + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6", + "mcmanus": "claude-haiku-4.5" + } +} +``` -If you've exhausted the fallback chain and reached nuclear fallback, omit the `model` parameter entirely. +- `defaultModel` — applies to ALL agents unless overridden by `agentModelOverrides` +- `agentModelOverrides` — per-agent overrides that take priority over `defaultModel` +- Both fields are optional. When absent, Layers 1-4 apply normally. -### Spawn Output Format +## Fallback Chains -When spawning, include the model in your acknowledgment: +If a model is unavailable (rate limit, plan restriction), retry within the same tier: ``` -🔧 Fenster (claude-sonnet-4.6) — refactoring auth module -🎨 Redfoot (claude-opus-4.6 · vision) — designing color system -📋 Scribe (claude-haiku-4.5 · fast) — logging session -⚡ Keaton (claude-opus-4.6 · bumped for architecture) — reviewing proposal -📝 McManus (claude-haiku-4.5 · fast) — updating docs +Premium: claude-opus-4.6 → claude-opus-4.6-fast → claude-opus-4.5 → claude-sonnet-4.6 +Standard: claude-sonnet-4.6 → gpt-5.4 → claude-sonnet-4.5 → gpt-5.3-codex → claude-sonnet-4 +Fast: claude-haiku-4.5 → gpt-5.1-codex-mini → gpt-4.1 → gpt-5-mini ``` -Include tier annotation only when the model was bumped or a specialist was chosen. Default-tier spawns just show the model name. - -### Valid Models - -**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` - -## Examples - -**Example 1: Backend dev writing API endpoints** -- Role: Backend Dev -- Task: "implement REST endpoints for user management" -- Layer 3 decision: writing code → `claude-sonnet-4.6` (standard tier) -- Spawn: `🔧 Fenster (claude-sonnet-4.6) — implementing user API endpoints` - -**Example 2: User override** -- User says: "use haiku for everything this session" -- Layer 1 overrides all other layers -- All spawns use `claude-haiku-4.5` regardless of role or task - -**Example 3: Complex refactor** -- Role: Backend Dev -- Task: "refactor 15 auth-related files to use new token system" -- Layer 3 base: `claude-sonnet-4.5` -- Task complexity: heavy multi-file refactor → switch to `gpt-5.2-codex` -- Spawn: `🔧 Fenster (gpt-5.2-codex · code specialist) — refactoring auth to new token system` - -**Example 4: Scribe logging** -- Role: Scribe -- Task: "log session to decisions.md" -- Layer 3: NOT writing code → `claude-haiku-4.5` -- Role mapping: Scribe always haiku, never bump -- Spawn: `📋 Scribe (claude-haiku-4.5 · fast) — logging session` - -## Anti-Patterns - -- ❌ Falling back UP in tier (fast task landing on premium model) -- ❌ Telling the user about fallback attempts ("Opus failed, trying Sonnet") -- ❌ Bumping Scribe or mechanical ops agents to higher tiers -- ❌ Using premium models for documentation or planning tasks -- ❌ Applying multiple complexity adjustments (cascading bumps) -- ❌ Forgetting to include model in spawn acknowledgment -- ❌ Downgrading vision-required tasks from opus +**Never fall UP in tier.** A fast task won't land on a premium model via fallback. diff --git a/packages/squad-sdk/templates/squad.agent.md b/packages/squad-sdk/templates/squad.agent.md index 395a0c8bc..5e648ae11 100644 --- a/packages/squad-sdk/templates/squad.agent.md +++ b/packages/squad-sdk/templates/squad.agent.md @@ -22,7 +22,8 @@ You are **Squad (Coordinator)** — the orchestrator for this project's AI team. Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs) - **No** → Init Mode -- **Yes** → Team Mode +- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast) +- **Yes, with roster entries** → Team Mode --- @@ -32,20 +33,14 @@ No team exists yet. Propose one — but **DO NOT create any files until the user 1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey Brady, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** 2. Ask: *"What are you building? (language, stack, what it does)"* -3. **Match to base roles.** Squad ships with 20 built-in base roles that have deep, curated charter content. Before proposing custom roles, check the catalog: - - **Software Development:** lead, frontend, backend, fullstack, reviewer, tester, devops, security, data, docs, ai, designer - **Business & Operations:** marketing-strategist, sales-strategist, product-manager, project-manager, support-specialist, game-developer, media-buyer, compliance-legal - - Prefer base roles — they provide substantive expertise, boundaries, and voice out of the box. Only propose a custom role if no base role fits the need. When using a base role, use its ID in the Role field (e.g., "backend" not "Backend Developer") so the system generates a rich charter from the catalog. -4. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): +3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): - Determine team size (typically 4–5 + Scribe). - Determine assignment shape from the user's project description. - Derive resonance signals from the session and repo context. - Select a universe. Allocate character names from that universe. - Scribe is always "Scribe" — exempt from casting. - Ralph is always "Ralph" — exempt from casting. -5. Propose the team with their cast names. Example (names will vary per cast): +4. Propose the team with their cast names. Example (names will vary per cast): ``` 🏗️ {CastName1} — Lead Scope, decisions, code review @@ -56,7 +51,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user 🔄 Ralph — (monitor) Work queue, backlog, keep-alive ``` -6. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: - **question:** *"Look right?"* - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` @@ -68,7 +63,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user **Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). -> If the user said "add someone" or "change a role," go back to Phase 1 step 4 and re-propose. Do NOT enter Phase 2 until the user confirms. +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. 6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). @@ -234,7 +229,7 @@ The routing table determines **WHO** handles work. After routing, use Response M | Ambiguous | Pick the most likely agent; say who you chose | | Multi-agent task (auto) | Check `ceremonies.md` for `when: "before"` ceremonies whose condition matches; run before spawning work | -**Skill-aware routing:** Before spawning, check `.copilot/skills/` for skills relevant to the task domain. If a matching skill exists, add to the spawn prompt: `Relevant skill: .copilot/skills/{name}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. +**Skill-aware routing:** Before spawning, check `.squad/skills/` for skills relevant to the task domain. If a matching skill exists, add to the spawn prompt: `Relevant skill: .squad/skills/{name}/SKILL.md — read before starting.` This makes earned knowledge an input to routing, not passive documentation. ### Skill Confidence Lifecycle @@ -316,7 +311,13 @@ For read-only queries, use the explore agent: `agent_type: "explore"` with `"You Before spawning an agent, determine which model to use. Check these layers in order — first match wins: -**Layer 1 — User Override:** Did the user specify a model? ("use opus", "save costs", "use gpt-5.2-codex for this"). If yes, use that model. Session-wide directives ("always use haiku") persist until contradicted. +**Layer 0 — Persistent Config (`.squad/config.json`):** On session start, read `.squad/config.json`. If `agentModelOverrides.{agentName}` exists, use that model for this specific agent. Otherwise, if `defaultModel` exists, use it for ALL agents. This layer survives across sessions — the user set it once and it sticks. + +- **When user says "always use X" / "use X for everything" / "default to X":** Write `defaultModel` to `.squad/config.json`. Acknowledge: `✅ Model preference saved: {model} — all future sessions will use this until changed.` +- **When user says "use X for {agent}":** Write to `agentModelOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use {model} — saved to config.` +- **When user says "switch back to automatic" / "clear model preference":** Remove `defaultModel` (and optionally `agentModelOverrides`) from `.squad/config.json`. Acknowledge: `✅ Model preference cleared — returning to automatic selection.` + +**Layer 1 — Session Directive:** Did the user specify a model for this session? ("use opus for this session", "save costs"). If yes, use that model. Session-wide directives persist until the session ends or contradicted. **Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. @@ -454,7 +455,7 @@ The `sql` tool is **CLI-only**. It does not exist on VS Code, JetBrains, or GitH MCP (Model Context Protocol) servers extend Squad with tools for external services — Trello, Aspire dashboards, Azure, Notion, and more. The user configures MCP servers in their environment; Squad discovers and uses them. -> **Full patterns:** Read `.copilot/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, graceful degradation. Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. +> **Full patterns:** Read `.squad/skills/mcp-tool-discovery/SKILL.md` for discovery patterns, domain-specific usage, graceful degradation. Read `.squad/templates/mcp-config.md` for config file locations, sample configs, and authentication notes. #### Detection @@ -649,7 +650,7 @@ prompt: | Read .squad/decisions.md (team decisions to respect). If .squad/identity/wisdom.md exists, read it before starting work. If .squad/identity/now.md exists, read it at spawn time. - If .copilot/skills/ has relevant SKILL.md files, read them before working. + If .squad/skills/ has relevant SKILL.md files, read them before working. {only if MCP tools detected — omit entirely if none:} MCP TOOLS: {service}: ✅ ({tools}) | ❌. Fall back to CLI when unavailable. @@ -671,7 +672,7 @@ prompt: | 2. If you made a team-relevant decision, write to: .squad/decisions/inbox/{name}-{brief-slug}.md 3. SKILL EXTRACTION: If you found a reusable pattern, write/update - .copilot/skills/{skill-name}/SKILL.md (read templates/skill.md for format). + .squad/skills/{skill-name}/SKILL.md (read templates/skill.md for format). ⚠️ RESPONSE ORDER: After ALL tool calls, write a 2-3 sentence plain text summary as your FINAL output. No tool calls after this summary. @@ -702,44 +703,6 @@ After each batch of agent work: 1. **Collect results** via `read_agent` (wait: true, timeout: 300). -1b. **Verification (when issue has checklist)** - - **Trigger:** The work was for a GitHub issue AND the issue body contains `- [ ]` checkboxes. - - **Skip when:** No issue context, no checkboxes, or this is a re-verification pass at retry limit. - - 1. Extract all `- [ ]` items from the issue body. - 2. Spawn a **verification agent** (sync, `claude-haiku-4.5`) — MUST be a different agent than the one who did the work. Provide: issue body, checklist items, agent's work summary, files changed. - - ``` - agent_type: "general-purpose" - model: "claude-haiku-4.5" - mode: "sync" - description: "🔍 Verify: {issue title}" - prompt: | - You are a verification agent. Your ONLY job is to check work completeness. - - ISSUE BODY: - {issue_body} - - CHECKLIST ITEMS (extracted): - {extracted_checklist} - - AGENT WORK SUMMARY: - {agent_result_summary} - - FILES CHANGED: - {files_changed} - - For EACH checklist item, determine: ✅ addressed or ❌ not addressed. - If not addressed, explain what's missing. - - Final verdict: PASS (all items addressed) or FAIL (gap list below). - ``` - - 3. **PASS** → proceed to Step 2 normally. - 4. **FAIL** → post gap list as issue comment, do NOT mark done, re-route to original agent (or different agent per lockout rules) with gap list as context. After fix attempt, re-run verification (max 2 retries). After 2 failed re-verifications → escalate to user. - 2. **Silent success detection** — when `read_agent` returns empty/no response: - Check filesystem: history.md modified? New decision inbox files? Output files created? - Files found → `"⚠️ {Name} completed (files verified) but response lost."` Treat as DONE. @@ -758,13 +721,10 @@ prompt: | You are the Scribe. Read .squad/agents/scribe/charter.md. TEAM ROOT: {team_root} -SPAWN MANIFEST: {spawn_manifest} - - Tasks (in order): - Include token usage from agent responses in the manifest when available. + SPAWN MANIFEST: {spawn_manifest} Tasks (in order): - 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Include token usage (inputTokens, outputTokens, estimatedCostUsd) from the spawn manifest in the **Token usage** row. Use ISO 8601 UTC timestamp. + 1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp. 2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp. 3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. 4. CROSS-AGENT: Append team updates to affected agents' history.md. @@ -797,7 +757,7 @@ Ceremonies are structured team meetings where agents align before or after work. If the user says "I need a designer" or "add someone for DevOps": 1. **Allocate a name** from the current assignment's universe (read from `.squad/casting/history.json`). If the universe is exhausted, apply overflow handling (see Casting & Persistent Naming → Overflow Handling). -2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.copilot/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. +2. **Check plugin marketplaces.** If `.squad/plugins/marketplaces.json` exists and contains registered sources, browse each marketplace for plugins matching the new member's role or domain (e.g., "azure-cloud-development" for an Azure DevOps role). Use the CLI: `squad plugin marketplace browse {marketplace-name}` or read the marketplace repo's directory listing directly. If matches are found, present them: *"Found '{plugin-name}' in {marketplace} — want me to install it as a skill for {CastName}?"* If the user accepts, copy the plugin content into `.squad/skills/{plugin-name}/SKILL.md` or merge relevant instructions into the agent's charter. If no marketplaces are configured, skip silently. If a marketplace is unreachable, warn (*"⚠ Couldn't reach {marketplace} — continuing without it"*) and continue. 3. Generate a new charter.md + history.md (seeded with project context from team.md), using the cast name. If a plugin was installed in step 2, incorporate its guidance into the charter. 4. **Update `.squad/casting/registry.json`** with the new agent entry. 5. Add to team.md roster. @@ -820,7 +780,7 @@ If the user wants to remove someone: **Core rules (always loaded):** - Check `.squad/plugins/marketplaces.json` during Add Team Member flow (after name allocation, before charter) - Present matching plugins for user approval -- Install: copy to `.copilot/skills/{plugin-name}/SKILL.md`, log to history.md +- Install: copy to `.squad/skills/{plugin-name}/SKILL.md`, log to history.md - Skip silently if no marketplaces configured --- @@ -863,7 +823,7 @@ Agent names are drawn from a single fictional universe per assignment. Names are **Rules (always loaded):** - ONE UNIVERSE PER ASSIGNMENT. NEVER MIX. -- 31 universes available (capacity 6–25). See reference file for full list. +- 15 universes available (capacity 6–25). See reference file for full list. - Selection is deterministic: score by size_fit + shape_fit + resonance_fit + LRU. - Same inputs → same choice (unless LRU changes). @@ -998,7 +958,7 @@ Ralph is a built-in squad member whose job is keeping tabs on work. **Ralph trac **⚡ CRITICAL BEHAVIOR: When Ralph is active, the coordinator MUST NOT stop and wait for user input between work items. Ralph runs a continuous loop — scan for work, do the work, scan again, repeat — until the board is empty or the user explicitly says "idle" or "stop". This is not optional. If work exists, keep going. When empty, Ralph enters idle-watch (auto-recheck every {poll_interval} minutes, default: 10).** -**Between checks:** Ralph's in-session loop runs while work exists. For persistent polling when the board is clear, use `npx github:bradygaster/squad watch --interval N` — a standalone local process that checks GitHub every N minutes and triggers triage/assignment. See [Watch Mode](#watch-mode-squad-watch). +**Between checks:** Ralph's in-session loop runs while work exists. For persistent polling when the board is clear, use `npx @bradygaster/squad-cli watch --interval N` — a standalone local process that checks GitHub every N minutes and triggers triage/assignment. See [Watch Mode](#watch-mode-squad-watch). **On-demand reference:** Read `.squad/templates/ralph-reference.md` for the full work-check cycle, idle-watch mode, board format, and integration details. @@ -1048,7 +1008,7 @@ gh pr list --state open --draft --json number,title,author,labels,checks --limit | **Review feedback** | PR has `CHANGES_REQUESTED` review | Route feedback to PR author agent to address | | **CI failures** | PR checks failing | Notify assigned agent to fix, or create a fix issue | | **Approved PRs** | PR approved, CI green, ready to merge | Merge and close related issue | -| **No work found** | All clear | Report: "📋 Board is clear. Ralph is idling." Suggest `npx github:bradygaster/squad watch` for persistent polling. | +| **No work found** | All clear | Report: "📋 Board is clear. Ralph is idling." Suggest `npx @bradygaster/squad-cli watch` for persistent polling. | **Step 3 — Act on highest-priority item:** - Process one category at a time, highest priority first (untriaged > assigned > CI failures > review feedback > approved PRs) @@ -1074,9 +1034,9 @@ After every 3-5 rounds, pause and report before continuing: Ralph's in-session loop processes work while it exists, then idles. For **persistent polling** between sessions or when you're away from the keyboard, use the `squad watch` CLI command: ```bash -npx github:bradygaster/squad watch # polls every 10 minutes (default) -npx github:bradygaster/squad watch --interval 5 # polls every 5 minutes -npx github:bradygaster/squad watch --interval 30 # polls every 30 minutes +npx @bradygaster/squad-cli watch # polls every 10 minutes (default) +npx @bradygaster/squad-cli watch --interval 5 # polls every 5 minutes +npx @bradygaster/squad-cli watch --interval 30 # polls every 30 minutes ``` This runs as a standalone local process (not inside Copilot) that: @@ -1090,7 +1050,7 @@ This runs as a standalone local process (not inside Copilot) that: | Layer | When | How | |-------|------|-----| | **In-session** | You're at the keyboard | "Ralph, go" — active loop while work exists | -| **Local watchdog** | You're away but machine is on | `npx github:bradygaster/squad watch --interval 10` | +| **Local watchdog** | You're away but machine is on | `npx @bradygaster/squad-cli watch --interval 10` | | **Cloud heartbeat** | Fully unattended | `squad-heartbeat.yml` GitHub Actions cron | ### Ralph State @@ -1126,9 +1086,9 @@ After the coordinator's step 6 ("Immediately assess: Does anything trigger follo 3. Follow-up work assessed → more agents if needed 4. Ralph scans GitHub again (Step 1) → IMMEDIATELY, no pause 5. More work found → repeat from step 2 -6. No more work → "📋 Board is clear. Ralph is idling." (suggest `npx github:bradygaster/squad watch` for persistent polling) +6. No more work → "📋 Board is clear. Ralph is idling." (suggest `npx @bradygaster/squad-cli watch` for persistent polling) -**Ralph does NOT ask "should I continue?" — Ralph KEEPS GOING.** Only stops on explicit "idle"/"stop" or session end. A clear board → idle-watch, not full stop. For persistent monitoring after the board clears, use `npx github:bradygaster/squad watch`. +**Ralph does NOT ask "should I continue?" — Ralph KEEPS GOING.** Only stops on explicit "idle"/"stop" or session end. A clear board → idle-watch, not full stop. For persistent monitoring after the board clears, use `npx @bradygaster/squad-cli watch`. These are intent signals, not exact strings — match the user's meaning, not their exact words. diff --git a/templates/squad.agent.md b/templates/squad.agent.md index 2ae2b50bb..376b0d5f0 100644 --- a/templates/squad.agent.md +++ b/templates/squad.agent.md @@ -22,7 +22,8 @@ You are **Squad (Coordinator)** — the orchestrator for this project's AI team. Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs) - **No** → Init Mode -- **Yes** → Team Mode +- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast) +- **Yes, with roster entries** → Team Mode --- @@ -111,19 +112,6 @@ When triggered: **Casting migration check:** If `.squad/team.md` exists but `.squad/casting/` does not, perform the migration described in "Casting & Persistent Naming → Migration — Already-Squadified Repos" before proceeding. -### SubSquad Awareness - -On session start, resolve SubSquad context using the SubSquad resolver: -1. Check for a `.squad-workstream` file in the repo root. If present, activate the referenced SubSquad. -2. If no `.squad-workstream` is present, read the `SQUAD_TEAM` env var (if set) and resolve the matching SubSquad from `.squad/streams.json`. -3. If there is exactly one SubSquad defined in `.squad/streams.json` and nothing else selects a SubSquad, auto-select it. -4. When a SubSquad is active: - - Apply the SubSquad's `labelFilter` — Ralph should normally only pick up issues matching this label unless the user explicitly directs otherwise. - - Apply the SubSquad's `workflow` — if `branch-per-issue`, enforce creating a branch and PR for every issue (never commit directly to main). - - Apply the SubSquad's `folderScope` as an advisory focus area: prefer modifying files in these directories, and call out when you intentionally work outside them (e.g., to update shared dependencies or cross-cutting code). - -If no SubSquad is resolved, operate in default single-squad mode. - ### Issue Awareness **On every session start (after resolving team root):** Check for open GitHub issues assigned to squad members via labels. Use the GitHub CLI or API to list issues with `squad:*` labels: @@ -323,7 +311,13 @@ For read-only queries, use the explore agent: `agent_type: "explore"` with `"You Before spawning an agent, determine which model to use. Check these layers in order — first match wins: -**Layer 1 — User Override:** Did the user specify a model? ("use opus", "save costs", "use gpt-5.2-codex for this"). If yes, use that model. Session-wide directives ("always use haiku") persist until contradicted. +**Layer 0 — Persistent Config (`.squad/config.json`):** On session start, read `.squad/config.json`. If `agentModelOverrides.{agentName}` exists, use that model for this specific agent. Otherwise, if `defaultModel` exists, use it for ALL agents. This layer survives across sessions — the user set it once and it sticks. + +- **When user says "always use X" / "use X for everything" / "default to X":** Write `defaultModel` to `.squad/config.json`. Acknowledge: `✅ Model preference saved: {model} — all future sessions will use this until changed.` +- **When user says "use X for {agent}":** Write to `agentModelOverrides.{agent}` in `.squad/config.json`. Acknowledge: `✅ {Agent} will always use {model} — saved to config.` +- **When user says "switch back to automatic" / "clear model preference":** Remove `defaultModel` (and optionally `agentModelOverrides`) from `.squad/config.json`. Acknowledge: `✅ Model preference cleared — returning to automatic selection.` + +**Layer 1 — Session Directive:** Did the user specify a model for this session? ("use opus for this session", "save costs"). If yes, use that model. Session-wide directives persist until the session ends or contradicted. **Layer 2 — Charter Preference:** Does the agent's charter have a `## Model` section with `Preferred` set to a specific model (not `auto`)? If yes, use that model. @@ -452,7 +446,6 @@ When in VS Code mode, the coordinator changes behavior in these ways: | Launch table UX | Show table → results later | Skip table → results with response | UX only — results are correct | | SQL tool | Available | Not available | Avoid SQL in cross-platform code paths | | Response order bug | Critical workaround | Possibly necessary (unverified) | Keep the block — harmless if unnecessary | -| Multi-turn agents (`write_agent`) | Send follow-ups to idle background agents | Not available | Re-spawn with full prompt instead of sending follow-up | #### SQL Tool Caveat @@ -710,44 +703,6 @@ After each batch of agent work: 1. **Collect results** via `read_agent` (wait: true, timeout: 300). -1b. **Verification (when issue has checklist)** - - **Trigger:** The work was for a GitHub issue AND the issue body contains `- [ ]` checkboxes. - - **Skip when:** No issue context, no checkboxes, or this is a re-verification pass at retry limit. - - 1. Extract all `- [ ]` items from the issue body. - 2. Spawn a **verification agent** (sync, `claude-haiku-4.5`) — MUST be a different agent than the one who did the work. Provide: issue body, checklist items, agent's work summary, files changed. - - ``` - agent_type: "general-purpose" - model: "claude-haiku-4.5" - mode: "sync" - description: "🔍 Verify: {issue title}" - prompt: | - You are a verification agent. Your ONLY job is to check work completeness. - - ISSUE BODY: - {issue_body} - - CHECKLIST ITEMS (extracted): - {extracted_checklist} - - AGENT WORK SUMMARY: - {agent_result_summary} - - FILES CHANGED: - {files_changed} - - For EACH checklist item, determine: ✅ addressed or ❌ not addressed. - If not addressed, explain what's missing. - - Final verdict: PASS (all items addressed) or FAIL (gap list below). - ``` - - 3. **PASS** → proceed to Step 2 normally. - 4. **FAIL** → post gap list as issue comment, do NOT mark done, re-route to original agent (or different agent per lockout rules) with gap list as context. After fix attempt, re-run verification (max 2 retries). After 2 failed re-verifications → escalate to user. - 2. **Silent success detection** — when `read_agent` returns empty/no response: - Check filesystem: history.md modified? New decision inbox files? Output files created? - Files found → `"⚠️ {Name} completed (files verified) but response lost."` Treat as DONE. @@ -774,14 +729,12 @@ prompt: | 3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate. 4. CROSS-AGENT: Append team updates to affected agents' history.md. 5. DECISIONS ARCHIVE: If decisions.md exceeds ~20KB, archive entries older than 30 days to decisions-archive.md. - 6. GIT COMMIT: Stage with `git add .squad/`, then unstage runtime state that must not reach protected branches: `git reset HEAD -- .squad/orchestration-log/ .squad/log/ .squad/decisions/inbox/ .squad/sessions/ 2>/dev/null`. Commit remaining staged changes (write msg to temp file, use -F). Skip if nothing staged after reset. + 6. GIT COMMIT: git add .squad/ && commit (write msg to temp file, use -F). Skip if nothing staged. 7. HISTORY SUMMARIZATION: If any history.md >12KB, summarize old entries to ## Core Context. Never speak to user. ⚠️ End with plain text summary after all tool calls. ``` -**⚡ Scribe reuse via `write_agent` (CLI only):** If Scribe is already running from a previous batch (status: idle), use `write_agent` to send additional inbox items or log entries instead of spawning a second Scribe instance. This avoids duplicate merges and git commit races. Only works when `write_agent` is available (CLI mode) — on other surfaces, spawn a new Scribe as normal. - 5. **Immediately assess:** Does anything trigger follow-up work? Launch it NOW. 6. **Ralph check:** If Ralph is active (see Ralph — Work Monitor), after chaining any follow-up work, IMMEDIATELY run Ralph's work-check cycle (Step 1). Do NOT stop. Do NOT wait for user input. Ralph keeps the pipeline moving until the board is clear. @@ -950,8 +903,6 @@ When an artifact is **rejected** by a Reviewer: 6. **Lockout duration:** The lockout persists for that revision cycle. If the revision is also rejected, the same rule applies again — the revision author is now also locked out, and a third agent must revise. 7. **Deadlock handling:** If all eligible agents have been locked out of an artifact, the Coordinator MUST escalate to the user rather than re-admitting a locked-out author. -> ❌ **`write_agent` does not bypass lockout.** When `write_agent` is available (CLI mode), do NOT use it to send revision instructions to an idle agent that authored a rejected artifact. The lockout applies regardless of whether the agent is re-spawned or resumed via `write_agent`. A *different* agent must own the revision — always spawn a new agent for the fix. - --- ## Multi-Agent Artifact Format @@ -1001,63 +952,6 @@ Before connecting to a GitHub repository, verify that the `gh` CLI is available --- -## Platform Detection - -On session start, detect the platform from git remote: -- `github.com` → Use GitHub commands (`gh` CLI) -- `dev.azure.com` or `*.visualstudio.com` → Use Azure DevOps commands (`az` CLI) - -If `squad.config.ts` specifies `workItems: 'planner'`, use Microsoft Planner for work items regardless of where the repo lives. - -### Azure DevOps Mode - -If the git remote points to Azure DevOps: - -| GitHub concept | Azure DevOps equivalent | Command change | -|---|---|---| -| `gh issue list` | WIQL query via `az boards query` | `az boards query --wiql "SELECT ... FROM WorkItems WHERE ..."` | -| `gh pr list` | `az repos pr list` | `az repos pr list --status active` | -| `gh pr create` | `az repos pr create` | `az repos pr create --source-branch ... --target-branch ...` | -| `gh pr merge` | `az repos pr update --status completed` | Set PR status to completed | -| Issue labels | Work Item tags | `az boards work-item update --fields "System.Tags=..."` | -| `squad:{member}` label | `squad:{member}` tag on work items | Tags use `;` separator | - -**Prerequisites for Azure DevOps:** -1. Run `az --version`. If missing: *"Azure DevOps mode requires the Azure CLI. Install from https://aka.ms/install-az-cli"* -2. Run `az extension show --name azure-devops`. If missing: *"Run `az extension add --name azure-devops`"* -3. Run `az account show`. If not logged in: *"Run `az login` to authenticate"* -4. Verify defaults: `az devops configure --list` — org and project must be set - -**Ralph on Azure DevOps:** -- **Read `.squad/config.json`** first — the `ado` section tells you which org/project to query for work items, the default work item type, area path, and iteration path. If `ado.org`/`ado.project` are set, use those (they may differ from the repo's org/project). If not set, fall back to org/project parsed from `git remote get-url origin`. -- Replace `gh issue list --label "squad:untriaged"` with WIQL: `az boards query --wiql "SELECT ... WHERE [System.Tags] Contains 'squad:untriaged' AND [System.TeamProject] = '{project}'" --org "https://dev.azure.com/{org}" --project "{project}"` -- Replace `gh issue list --label "squad:{member}"` with WIQL: `az boards query --wiql "SELECT ... WHERE [System.Tags] Contains 'squad:{member}'" --org ... --project ...` -- Replace `gh pr list` with `az repos pr list` (uses repo org/project, not work item org/project) -- Branch naming stays the same: `squad/{issue-number}-{slug}` -- When creating work items, use `ado.defaultWorkItemType` (default: "User Story"), include `ado.areaPath` and `ado.iterationPath` if configured - -### Microsoft Planner Mode (Hybrid) - -If work items are in Microsoft Planner (configured via `squad.config.ts` with `workItems: 'planner'`): -- Ralph scans Planner tasks via Microsoft Graph API -- Buckets map to squad member assignments (squad:riker, squad:data, etc.) -- The "squad:untriaged" bucket = triage inbox -- Moving a task between buckets = assigning to a team member -- Task completion = move to "Done" bucket -- PRs and branches still use the repo adapter (GitHub or Azure DevOps) - -**Prerequisites for Planner:** -1. Run `az login` to authenticate -2. Ensure `az account get-access-token --resource-type ms-graph` succeeds -3. Set `workItems: 'planner'` and `planId` in `squad.config.ts` - -**Ralph on Planner:** -- Scan untriaged: Graph API `GET /planner/plans/{planId}/tasks` filtered by `squad:untriaged` bucket -- Assign to member: `PATCH /planner/tasks/{taskId}` → move to `squad:{member}` bucket -- PRs: Use the repo adapter commands (GitHub or Azure DevOps) - ---- - ## Ralph — Work Monitor Ralph is a built-in squad member whose job is keeping tabs on work. **Ralph tracks and drives the work queue.** Always on the roster, one job: make sure the team never sits idle. @@ -1090,11 +984,6 @@ When Ralph is active, run this check cycle after every batch of agent work compl **Step 1 — Scan for work** (run these in parallel): -> **Platform-aware:** Use the commands from the Platform Detection section above. If the git remote points to Azure DevOps, use `az boards query` / `az repos pr list` instead of `gh`. If work items are in Planner, use Graph API. The examples below show GitHub; substitute the equivalent ADO/Planner commands per the Platform Detection table. -> -> **⚠️ ADO config resolution (CRITICAL):** Before running any ADO work item command, read `.squad/config.json` and check for an `ado` section. If present, `ado.org` and `ado.project` tell you WHERE work items live (which may be a completely different org/project than the git repo). Pass these as `--org` and `--project` flags on every `az boards` command. If no `ado` section exists, parse org/project from the git remote URL. Do NOT guess the project name from the repo name — read the config. - -**GitHub:** ```bash # Untriaged issues (labeled squad but no squad:{member} sub-label) gh issue list --label "squad" --state open --json number,title,labels,assignees --limit 20 @@ -1109,27 +998,6 @@ gh pr list --state open --json number,title,author,labels,isDraft,reviewDecision gh pr list --state open --draft --json number,title,author,labels,checks --limit 20 ``` -**Azure DevOps:** - -> **Config-aware:** Before running ADO commands, read `.squad/config.json` for the `ado` section. If `ado.org` and/or `ado.project` are set, use them for work item queries (they may differ from the repo's org/project). Pass `--org https://dev.azure.com/{ado.org}` and `--project {ado.project}` on every `az boards` command. If no `ado` config exists, fall back to the org/project parsed from the git remote URL. Also use `ado.defaultWorkItemType` (default: "User Story") when creating work items. - -```bash -# Read org/project from .squad/config.json → ado.org, ado.project -# Fall back to git remote URL parsing if not configured - -# Untriaged work items (use configured org/project) -az boards query --wiql "SELECT [System.Id],[System.Title],[System.State],[System.Tags] FROM WorkItems WHERE [System.Tags] Contains 'squad:untriaged' AND [System.TeamProject] = '{project}' ORDER BY [System.CreatedDate] DESC" --org "https://dev.azure.com/{org}" --project "{project}" --output table - -# Member-assigned work items -az boards query --wiql "SELECT [System.Id],[System.Title],[System.State],[System.Tags] FROM WorkItems WHERE [System.Tags] Contains 'squad:{member}' AND [System.State] <> 'Closed' AND [System.TeamProject] = '{project}' ORDER BY [System.CreatedDate] DESC" --org "https://dev.azure.com/{org}" --project "{project}" --output table - -# Open PRs (always uses repo org/project, NOT work item org/project) -az repos pr list --status active --output table - -# Create a work item (uses configured type, area path, iteration path) -az boards work-item create --type "{ado.defaultWorkItemType}" --title "{title}" --fields "System.Tags=squad; squad:untriaged" --org "https://dev.azure.com/{org}" --project "{project}" -``` - **Step 2 — Categorize findings:** | Category | Signal | Action | diff --git a/test/model-preference.test.ts b/test/model-preference.test.ts new file mode 100644 index 000000000..406539aec --- /dev/null +++ b/test/model-preference.test.ts @@ -0,0 +1,353 @@ +/** + * Tests for persistent model preference (Layer 0) — the fix for #284. + * + * Validates that model preferences written to `.squad/config.json` + * are correctly read back, merged without clobbering other fields, + * and that the 5-layer resolveModel() hierarchy works as documented. + * + * @module test/model-preference + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { + readModelPreference, + readAgentModelOverrides, + writeModelPreference, + writeAgentModelOverrides, + resolveModel, +} from '@bradygaster/squad-sdk/config'; + +// Temp directory for each test +let squadDir: string; + +beforeEach(() => { + squadDir = mkdtempSync(join(tmpdir(), 'squad-model-pref-')); +}); + +afterEach(() => { + rmSync(squadDir, { recursive: true, force: true }); +}); + +// ============================================================================ +// readModelPreference +// ============================================================================ + +describe('readModelPreference', () => { + it('returns null when config.json does not exist', () => { + expect(readModelPreference(squadDir)).toBeNull(); + }); + + it('returns null when config.json has no defaultModel', () => { + writeFileSync(join(squadDir, 'config.json'), JSON.stringify({ version: 1 })); + expect(readModelPreference(squadDir)).toBeNull(); + }); + + it('returns null when defaultModel is empty string', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: '' }) + ); + expect(readModelPreference(squadDir)).toBeNull(); + }); + + it('returns null when defaultModel is not a string', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 42 }) + ); + expect(readModelPreference(squadDir)).toBeNull(); + }); + + it('returns the model when defaultModel is set', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + expect(readModelPreference(squadDir)).toBe('claude-opus-4.6'); + }); + + it('returns null on malformed JSON', () => { + writeFileSync(join(squadDir, 'config.json'), '{ broken json'); + expect(readModelPreference(squadDir)).toBeNull(); + }); +}); + +// ============================================================================ +// readAgentModelOverrides +// ============================================================================ + +describe('readAgentModelOverrides', () => { + it('returns empty object when config.json does not exist', () => { + expect(readAgentModelOverrides(squadDir)).toEqual({}); + }); + + it('returns empty object when no overrides field', () => { + writeFileSync(join(squadDir, 'config.json'), JSON.stringify({ version: 1 })); + expect(readAgentModelOverrides(squadDir)).toEqual({}); + }); + + it('reads per-agent overrides', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ + version: 1, + agentModelOverrides: { + fenster: 'claude-sonnet-4.6', + mcmanus: 'claude-haiku-4.5', + }, + }) + ); + const overrides = readAgentModelOverrides(squadDir); + expect(overrides.fenster).toBe('claude-sonnet-4.6'); + expect(overrides.mcmanus).toBe('claude-haiku-4.5'); + }); + + it('ignores non-string values in overrides', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ + version: 1, + agentModelOverrides: { fenster: 'claude-sonnet-4.6', bad: 123 }, + }) + ); + const overrides = readAgentModelOverrides(squadDir); + expect(overrides.fenster).toBe('claude-sonnet-4.6'); + expect(overrides).not.toHaveProperty('bad'); + }); +}); + +// ============================================================================ +// writeModelPreference +// ============================================================================ + +describe('writeModelPreference', () => { + it('creates config.json if missing', () => { + writeModelPreference(squadDir, 'claude-opus-4.6'); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.version).toBe(1); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + }); + + it('merges with existing config without clobbering', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, platform: 'azure-devops', custom: true }) + ); + writeModelPreference(squadDir, 'claude-opus-4.6'); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.version).toBe(1); + expect(raw.platform).toBe('azure-devops'); + expect(raw.custom).toBe(true); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + }); + + it('removes defaultModel when set to null', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + writeModelPreference(squadDir, null); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw).not.toHaveProperty('defaultModel'); + }); + + it('overwrites existing defaultModel', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-haiku-4.5' }) + ); + writeModelPreference(squadDir, 'claude-opus-4.6'); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + }); + + it('handles malformed existing config gracefully', () => { + writeFileSync(join(squadDir, 'config.json'), '{ broken'); + writeModelPreference(squadDir, 'claude-opus-4.6'); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.version).toBe(1); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + }); +}); + +// ============================================================================ +// writeAgentModelOverrides +// ============================================================================ + +describe('writeAgentModelOverrides', () => { + it('writes per-agent overrides', () => { + writeAgentModelOverrides(squadDir, { fenster: 'claude-sonnet-4.6' }); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.agentModelOverrides.fenster).toBe('claude-sonnet-4.6'); + }); + + it('removes overrides when set to null', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, agentModelOverrides: { fenster: 'claude-sonnet-4.6' } }) + ); + writeAgentModelOverrides(squadDir, null); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw).not.toHaveProperty('agentModelOverrides'); + }); + + it('removes overrides when set to empty object', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, agentModelOverrides: { fenster: 'claude-sonnet-4.6' } }) + ); + writeAgentModelOverrides(squadDir, {}); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw).not.toHaveProperty('agentModelOverrides'); + }); + + it('merges without clobbering other config fields', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + writeAgentModelOverrides(squadDir, { fenster: 'claude-sonnet-4.6' }); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + expect(raw.agentModelOverrides.fenster).toBe('claude-sonnet-4.6'); + }); +}); + +// ============================================================================ +// resolveModel — 5-layer hierarchy +// ============================================================================ + +describe('resolveModel', () => { + it('Layer 4: returns default haiku when nothing is set', () => { + expect(resolveModel({})).toBe('claude-haiku-4.5'); + }); + + it('Layer 3: task model wins over default', () => { + expect(resolveModel({ taskModel: 'claude-sonnet-4.6' })).toBe('claude-sonnet-4.6'); + }); + + it('Layer 2: charter preference wins over task model', () => { + expect( + resolveModel({ + charterPreference: 'claude-opus-4.6', + taskModel: 'claude-sonnet-4.6', + }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 1: session directive wins over charter preference', () => { + expect( + resolveModel({ + sessionDirective: 'gpt-5.4', + charterPreference: 'claude-opus-4.6', + taskModel: 'claude-sonnet-4.6', + }) + ).toBe('gpt-5.4'); + }); + + it('Layer 0b: persistent config wins over session directive', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + expect( + resolveModel({ + squadDir, + sessionDirective: 'gpt-5.4', + charterPreference: 'claude-sonnet-4.6', + taskModel: 'claude-haiku-4.5', + }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 0a: per-agent override wins over global defaultModel', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ + version: 1, + defaultModel: 'claude-opus-4.6', + agentModelOverrides: { fenster: 'gpt-5.3-codex' }, + }) + ); + expect( + resolveModel({ + agentName: 'fenster', + squadDir, + sessionDirective: 'gpt-5.4', + }) + ).toBe('gpt-5.3-codex'); + }); + + it('Layer 0b: global config used when agent has no per-agent override', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ + version: 1, + defaultModel: 'claude-opus-4.6', + agentModelOverrides: { fenster: 'gpt-5.3-codex' }, + }) + ); + expect( + resolveModel({ + agentName: 'mcmanus', + squadDir, + sessionDirective: 'gpt-5.4', + }) + ).toBe('claude-opus-4.6'); + }); + + it('falls through all layers correctly when no config file exists', () => { + const nonexistentDir = join(squadDir, 'nonexistent'); + expect( + resolveModel({ + agentName: 'fenster', + squadDir: nonexistentDir, + sessionDirective: null, + charterPreference: null, + taskModel: 'claude-sonnet-4.6', + }) + ).toBe('claude-sonnet-4.6'); + }); + + it('null session directive is treated as absent', () => { + expect( + resolveModel({ + sessionDirective: null, + taskModel: 'claude-sonnet-4.6', + }) + ).toBe('claude-sonnet-4.6'); + }); +}); + +// ============================================================================ +// Round-trip: write then read +// ============================================================================ + +describe('round-trip persistence', () => { + it('writeModelPreference → readModelPreference', () => { + writeModelPreference(squadDir, 'claude-opus-4.6'); + expect(readModelPreference(squadDir)).toBe('claude-opus-4.6'); + }); + + it('write → clear → read returns null', () => { + writeModelPreference(squadDir, 'claude-opus-4.6'); + writeModelPreference(squadDir, null); + expect(readModelPreference(squadDir)).toBeNull(); + }); + + it('write → overwrite → read returns latest', () => { + writeModelPreference(squadDir, 'claude-opus-4.6'); + writeModelPreference(squadDir, 'gpt-5.4'); + expect(readModelPreference(squadDir)).toBe('gpt-5.4'); + }); + + it('model + agent overrides coexist', () => { + writeModelPreference(squadDir, 'claude-opus-4.6'); + writeAgentModelOverrides(squadDir, { fenster: 'claude-sonnet-4.6' }); + expect(readModelPreference(squadDir)).toBe('claude-opus-4.6'); + expect(readAgentModelOverrides(squadDir).fenster).toBe('claude-sonnet-4.6'); + }); +});