From 1ae753c930f5235f2925865b9929c446c4867627 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 22 Mar 2026 02:43:29 -0700 Subject: [PATCH 1/4] feat: economy mode skill, governance proposals for #500 and #344 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .squad/skills/economy-mode/SKILL.md: Layer 3 cost modifier - Economy table shifts code tasks to gpt-4.1/gpt-5-mini, arch to sonnet - Activation: session phrase, config.json economyMode:true, or --economy flag - Layer 0/1/2 (user intent) always overrides economy mode - πŸ’° indicator in spawn acknowledgments - Confidence: low (first implementation) - Add .squad/decisions/inbox/procedures-economy-mode-governance.md - Draft additions to squad.agent.md for Flight review - Economy mode paragraph after Layer 0, updated table, πŸ’° convention - Valid models catalog audit findings (claude-sonnet-4.6 missing, etc.) - Add .squad/decisions/inbox/procedures-personal-squad-governance.md - Five gap analysis: Init Mode, consult mode detection, TEAM_ROOT semantics, charter templates, missing consult-mode skill - Proposed CONSULT_MODE spawn signal and 🧳 acknowledgment format - Proposed new consult-mode skill (post-governance-approval) - Update procedures history.md with learnings Closes #500 References #344 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .squad/agents/procedures/history.md | 22 +++ .../procedures-economy-mode-governance.md | 126 ++++++++++++++ .../procedures-personal-squad-governance.md | 158 ++++++++++++++++++ .squad/skills/economy-mode/SKILL.md | 114 +++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 .squad/decisions/inbox/procedures-economy-mode-governance.md create mode 100644 .squad/decisions/inbox/procedures-personal-squad-governance.md create mode 100644 .squad/skills/economy-mode/SKILL.md diff --git a/.squad/agents/procedures/history.md b/.squad/agents/procedures/history.md index 2390dfc70..ea52095bf 100644 --- a/.squad/agents/procedures/history.md +++ b/.squad/agents/procedures/history.md @@ -80,3 +80,25 @@ **Pattern:** NEVER/ALWAYS sections in charters compress well β€” fold bullet lists into single-paragraph summaries. Essential workflow details (Scribe's commit steps) should stay verbose. +### 2026-03-22: Economy mode skill and personal squad governance (#500, #344) + +**Task:** Two governance tasks β€” economy mode skill design and personal squad coordinator awareness. + +**Economy mode (SKILL.md):** +- Created `.squad/skills/economy-mode/SKILL.md` as a Layer 3 modifier, not a new resolution layer +- Key design decision: economy mode ONLY affects Layer 3 auto-selection β€” Layer 0/1/2 (user intent) always wins +- `πŸ’°` indicator in spawn acknowledgments keeps it transparent +- Activation via session phrase, persistent config (`economyMode: true` in config.json), or CLI flag +- Architecture trips shift from opus β†’ sonnet; code tasks shift from sonnet β†’ gpt-4.1/gpt-5-mini +- Confidence: `low` β€” first implementation, not yet validated + +**Personal squad governance (proposals):** +- Gap analysis: coordinator has no consult mode awareness despite full SDK implementation +- Five gaps identified: Init Mode missing personal squad resolution, no consult mode detection, TEAM_ROOT has no personal-squad semantics, charter templates lack consult-mode patterns, no consult-mode skill +- Proposed `CONSULT_MODE: true` as spawn prompt signal, `🧳 consult` in acknowledgments +- Proposed new consult-mode skill (after governance approval β€” skill after governance, not before) + +**Governance workflow pattern:** When proposals touch squad.agent.md (governance territory), write to `decisions/inbox/` for Flight review. Don't directly edit squad.agent.md β€” Flight reviews governance changes. + +**Catalog audit finding:** `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex` appear in model-selection SKILL.md fallback chains but are absent from squad.agent.md's "Valid models" catalog. Documented in economy-mode governance proposal for Flight to address. + diff --git a/.squad/decisions/inbox/procedures-economy-mode-governance.md b/.squad/decisions/inbox/procedures-economy-mode-governance.md new file mode 100644 index 000000000..4561be940 --- /dev/null +++ b/.squad/decisions/inbox/procedures-economy-mode-governance.md @@ -0,0 +1,126 @@ +# Proposal: Economy Mode Integration in squad.agent.md + +**By:** Procedures (Prompt Engineer) +**Date:** 2026-03-22 +**Issues:** #500 +**Status:** DRAFT β€” for Flight review before merging to squad.agent.md + +--- + +## Summary + +Economy mode is a new session/persistent modifier that shifts Layer 3 (Task-Aware Auto-Selection) to cost-optimized alternatives. This proposal documents the governance additions needed in `squad.agent.md`. + +**Note to Flight:** Procedures owns the skill design. Squad.agent.md is governance β€” Flight reviews before commit. + +--- + +## 1. New Paragraph After Layer 0 (Per-Agent Model Selection section) + +Insert after the existing Layer 0 bullet points and before "**Layer 1 β€” Session Directive**": + +--- + +**Economy Mode β€” Cost Modifier (Layer 3 override):** Economy mode shifts all Layer 3 auto-selection to cost-optimized alternatives. It does NOT override Layer 0 (persistent config), Layer 1 (explicit session directive), or Layer 2 (charter preference) β€” user intent always wins. + +- **Activation (session):** User says "use economy mode", "save costs", "go cheap" β†’ activate for this session only. +- **Activation (persistent):** User says "always use economy mode" OR `"economyMode": true` in `.squad/config.json` β†’ persists across sessions. +- **Deactivation:** "turn off economy mode" or remove `economyMode` from `config.json`. +- **On session start:** Read `.squad/config.json`. If `economyMode: true`, activate economy mode before any spawns. + +--- + +## 2. Economy Model Selection Table + +Add after Layer 3 normal table: + +--- + +**Economy Mode Layer 3 Table** (active when economy mode is on): + +| Task Output | Normal Mode | Economy Mode | +|-------------|-------------|--------------| +| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | +| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | + +Prefer `gpt-4.1` over `gpt-5-mini` for structured output or tool use. Prefer `gpt-5-mini` for pure text generation. + +--- + +## 3. Spawn Acknowledgment Convention + +Add to the spawn acknowledgment format guidance: + +--- + +When economy mode is active, include `πŸ’° economy` after the model name in spawn acknowledgments: + +``` +πŸ”§ Fenster (gpt-4.1 Β· πŸ’° economy) β€” fixing auth bug +πŸ“‹ Scribe (gpt-4.1 Β· πŸ’° economy) β€” logging decision +``` + +This gives the user instant visibility that cost-optimized models are in use. + +--- + +## 4. Valid Models Catalog Audit + +Current "Valid models" section lists: + +``` +Premium: claude-opus-4.6, claude-opus-4.6-fast, claude-opus-4.5 +Standard: claude-sonnet-4.5, claude-sonnet-4, gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1, gpt-5, gemini-3-pro-preview +Fast/Cheap: claude-haiku-4.5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 +``` + +**Audit findings:** +- `claude-opus-4.6` and `claude-opus-4.6-fast` are listed but not used in the Layer 3 table (table uses `claude-opus-4.5`). The Layer 3 table should reference `claude-opus-4.6` as the premium default for consistency with the catalog. +- `claude-sonnet-4.6` appears in the model-selection SKILL.md but is absent from the valid models list in squad.agent.md. Add it under Standard. +- Economy mode introduces `gpt-4.1` and `gpt-5-mini` as primary alternatives β€” both are already in the Fast/Cheap catalog. No additions needed. + +**Proposed updated catalog:** + +``` +Premium: claude-opus-4.6, claude-opus-4.6-fast, claude-opus-4.5 +Standard: claude-sonnet-4.6, claude-sonnet-4.5, claude-sonnet-4, gpt-5.4, gpt-5.3-codex, gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1, gpt-5, gemini-3-pro-preview +Fast/Cheap: claude-haiku-4.5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 +``` + +(Added `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex` which appear in the model-selection SKILL.md fallback chains but are missing from squad.agent.md's catalog.) + +--- + +## 5. Config Schema Addition + +Add `economyMode` to the config schema reference in squad.agent.md (wherever `defaultModel` is documented): + +```json +{ + "version": 1, + "defaultModel": "claude-sonnet-4.6", + "economyMode": true, + "agentModelOverrides": { + "fenster": "claude-sonnet-4.6" + } +} +``` + +--- + +## Rationale + +Economy mode solves a real user need: "I want all agents to run cheaper, but I don't want to set each one individually." It's a session-level modifier that works orthogonally to the existing hierarchy β€” no layer gets changed, only Layer 3's lookup table swaps. The `πŸ’°` indicator keeps it transparent. + +The skill (`economy-mode/SKILL.md`) covers the coordinator behavior in detail. This proposal is the governance side β€” ensuring squad.agent.md is the authoritative source for the feature. + +--- + +## References + +- Skill: `.squad/skills/economy-mode/SKILL.md` +- Issue: #500 +- Model selection skill: `.squad/skills/model-selection/SKILL.md` diff --git a/.squad/decisions/inbox/procedures-personal-squad-governance.md b/.squad/decisions/inbox/procedures-personal-squad-governance.md new file mode 100644 index 000000000..b46bb17de --- /dev/null +++ b/.squad/decisions/inbox/procedures-personal-squad-governance.md @@ -0,0 +1,158 @@ +# Proposal: Personal Squad Governance Awareness in squad.agent.md + +**By:** Procedures (Prompt Engineer) +**Date:** 2026-03-22 +**Issues:** #344 +**Status:** DRAFT β€” for Flight review before merging to squad.agent.md + +--- + +## Summary + +Squad has a consult mode (implemented, per `prd-consult-mode.md`) and personal squad semantics (via `resolveGlobalSquadPath()`), but `squad.agent.md` doesn't tell the coordinator how to reason about either. This proposal documents the gaps and the governance additions needed. + +--- + +## Gap Analysis + +### Gap 1: Init Mode references `--global` without explaining personal squad resolution + +Current Init Mode says "run `squad init --global` for a personal squad" (implied by CLI docs) but squad.agent.md doesn't explain what a personal squad IS or how the coordinator should detect it. + +**What agents need to know:** +- Personal squad = a squad at the global path (resolved via `resolveGlobalSquadPath()`) + - Linux/macOS: `~/.config/squad/.squad` + - macOS (alt): `~/Library/Application Support/squad/.squad` + - Windows: `%APPDATA%\squad\.squad` +- If `.squad/config.json` contains `"consult": true`, the coordinator is working inside a consult session +- `sourceSquad` in `config.json` points to the original personal squad (for Scribe extraction context) + +### Gap 2: No coordinator guidance for consult mode + +`squad.agent.md` mentions nothing about consult mode. The coordinator doesn't know: +- How to recognize it's in a consult session +- That writes go to the project `.squad/` (isolated copy) β€” NOT the personal squad +- That Scribe's charter is patched with extraction instructions +- That `.squad/extract/` is a staging area for generic learnings + +### Gap 3: TEAM_ROOT works, but personal squad semantics are absent + +The coordinator resolves `TEAM_ROOT` correctly (Worktree Awareness section), but: +- No distinction between "project squad" vs "personal squad copy in consult mode" +- No guidance on what to tell agents about their squad context when in consult mode + +### Gap 4: Charter templates have no personal-squad-aware patterns + +Agent charters have no concept of: +- Consult mode restrictions (agents shouldn't commit to project, shouldn't pollute personal squad) +- Extraction tagging (Scribe needs to flag decisions as generic vs project-specific) + +### Gap 5: No skill for consult-mode behavior + +There is no skill for consult-mode coordinator behavior, even though consult mode has distinct patterns (invisibility, extraction, isolation). + +--- + +## Proposed squad.agent.md Additions + +### Addition 1: Consult Mode Detection (in Team Mode β†’ On Session Start) + +After "resolve the team root" and before Issue Awareness, add: + +--- + +**Consult Mode Detection:** After resolving team root, check `.squad/config.json` for `"consult": true`. + +- If `consult: true` β†’ **Consult mode is active.** This is a personal squad consulting on a project. + - The `.squad/` directory is an isolated copy of the user's personal squad. + - `sourceSquad` in `config.json` contains the path to the original personal squad. + - Do NOT read or write to `sourceSquad` β€” it's out of scope. Only operate within TEAM_ROOT. + - Scribe's charter is already patched with extraction instructions β€” no coordinator action needed. + - Include `🧳 consult` in your session acknowledgment: `Squad v{version} (🧳 consult β€” {projectName})` + - Remind agents: decisions they make here are project-isolated until explicitly extracted. +- If `consult: false` or absent β†’ Normal mode. Team root is authoritative. + +--- + +### Addition 2: Personal Squad Path Reference + +Add a new subsection under "Worktree Awareness": + +--- + +**Personal Squad Paths:** The global squad path is resolved by `resolveGlobalSquadPath()`: + +| Platform | Path | +|----------|------| +| Linux | `~/.config/squad/.squad` | +| macOS | `~/Library/Application Support/squad/.squad` | +| Windows | `%APPDATA%\squad\.squad` | + +The coordinator should NEVER hard-code these paths. Use `squad --global` or `resolveGlobalSquadPath()` to resolve. Only relevant in consult mode (to understand the `sourceSquad` field) β€” the coordinator does NOT read the personal squad directly during a session. + +--- + +### Addition 3: Consult Mode Spawn Guidance + +Add to the spawn template section: + +--- + +**In consult mode:** Pass `CONSULT_MODE: true` and `PROJECT_NAME: {projectName}` in spawn prompts alongside `TEAM_ROOT`. This lets agents know: +1. Their decisions will be reviewed for extraction β€” keep project-specific and generic reasoning separate +2. They should NOT reference personal squad paths or personal squad agent names +3. Scribe will classify their decisions β€” agents should write clear, extractable decision rationale + +--- + +### Addition 4: Consult Mode Acknowledgment Format + +Add to spawn acknowledgment conventions: + +``` +🧳 consult mode active β€” Fenster (claude-sonnet-4.5) β€” refactoring auth module + ↳ decisions staged in .squad/extract/ for review before extraction +``` + +--- + +## Proposed New Skill + +**Skill needed:** `.squad/skills/consult-mode/SKILL.md` + +Should cover: +- Detecting consult mode from config.json +- Coordinator behavior changes (CONSULT_MODE in spawn prompts) +- Scribe extraction workflow (already documented in prd-consult-mode.md β€” condense into skill) +- Acknowledgment format conventions +- STOP: extraction is always user-driven via `squad extract` β€” coordinator never auto-extracts + +This skill should be authored after this governance proposal is approved, to avoid the skill getting ahead of the governance. + +--- + +## Charter Template Additions + +All agent charter templates should include a note in "How I Work": + +```markdown +**Consult Mode Awareness:** If `CONSULT_MODE: true` is in my spawn prompt, I'm working on a project outside my home squad. My decisions here are project-isolated. Write extractable rationale so Scribe can classify them for `squad extract` review. +``` + +This should be added to `.squad/templates/charter.md` (if it exists) and `.squad/agents/scribe/charter.md` (Scribe already has extraction logic, but clarifying the classification responsibility is valuable). + +--- + +## Rationale + +Consult mode is fully implemented at the SDK level (`prd-consult-mode.md`, `squad consult` command) but the coordinator has no awareness of it. The result: agents running in a consult session have no context that they're in a temporary, isolated copy of a personal squad. They might make decisions as if they're permanent, or reference the project in ways that pollute the personal squad on extraction. + +These governance additions close the loop between the implementation (CLI + SDK) and the runtime behavior (coordinator + agents). + +--- + +## References + +- Consult mode PRD: `.squad/identity/prd-consult-mode.md` +- Issue: #344 +- Flight ambient personal squad note: `.squad/decisions/inbox/flight-ambient-personal-squad.md` diff --git a/.squad/skills/economy-mode/SKILL.md b/.squad/skills/economy-mode/SKILL.md new file mode 100644 index 000000000..696e778c4 --- /dev/null +++ b/.squad/skills/economy-mode/SKILL.md @@ -0,0 +1,114 @@ +--- +name: "economy-mode" +description: "Shifts Layer 3 model selection to cost-optimized alternatives when economy mode is active." +domain: "model-selection" +confidence: "low" +source: "manual" +--- + +## SCOPE + +βœ… THIS SKILL PRODUCES: +- A modified Layer 3 model selection table applied when economy mode is active +- `economyMode: true` written to `.squad/config.json` when activated persistently +- Spawn acknowledgments with `πŸ’°` indicator when economy mode is active + +❌ THIS SKILL DOES NOT PRODUCE: +- Code, tests, or documentation +- Cost reports or billing artifacts +- Changes to Layer 0, Layer 1, or Layer 2 resolution (user intent always wins) + +## Context + +Economy mode shifts Layer 3 (Task-Aware Auto-Selection) to lower-cost alternatives. It does NOT override persistent config (`defaultModel`, `agentModelOverrides`) or per-agent charter preferences β€” those represent explicit user intent and always take priority. + +Use this skill when the user wants to reduce costs across an entire session or permanently, without manually specifying models for each agent. + +## Activation Methods + +| Method | How | +|--------|-----| +| Session phrase | "use economy mode", "save costs", "go cheap", "reduce costs" | +| Persistent config | `"economyMode": true` in `.squad/config.json` | +| CLI flag | `squad --economy` | + +**Deactivation:** "turn off economy mode", "disable economy mode", or remove `economyMode` from `config.json`. + +## Economy Model Selection Table + +When economy mode is **active**, Layer 3 auto-selection uses this table instead of the normal defaults: + +| Task Output | Normal Mode | Economy Mode | +|-------------|-------------|--------------| +| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | +| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | +| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | + +**Prefer `gpt-4.1` over `gpt-5-mini`** when the task involves structured output or agentic tool use. Prefer `gpt-5-mini` for pure text generation tasks where latency matters. + +## AGENT WORKFLOW + +### On Session Start + +1. READ `.squad/config.json` +2. CHECK for `economyMode: true` β€” if present, activate economy mode for the session +3. STORE economy mode state in session context + +### On User Phrase Trigger + +**Session-only (no config change):** "use economy mode", "save costs", "go cheap" + +1. SET economy mode active for this session +2. ACKNOWLEDGE: `βœ… Economy mode active β€” using cost-optimized models this session. (Layer 0 and Layer 2 preferences still apply)` + +**Persistent:** "always use economy mode", "save economy mode" + +1. WRITE `economyMode: true` to `.squad/config.json` (merge, don't overwrite other fields) +2. ACKNOWLEDGE: `βœ… Economy mode saved β€” cost-optimized models will be used until disabled.` + +### On Every Agent Spawn (Economy Mode Active) + +1. CHECK Layer 0a/0b first (agentModelOverrides, defaultModel) β€” if set, use that. Economy mode does NOT override Layer 0. +2. CHECK Layer 1 (session directive for a specific model) β€” if set, use that. Economy mode does NOT override explicit session directives. +3. CHECK Layer 2 (charter preference) β€” if set, use that. Economy mode does NOT override charter preferences. +4. APPLY economy table at Layer 3 instead of normal table. +5. INCLUDE `πŸ’°` in spawn acknowledgment: `πŸ”§ {Name} ({model} Β· πŸ’° economy) β€” {task}` + +### On Deactivation + +**Trigger phrases:** "turn off economy mode", "disable economy mode", "use normal models" + +1. REMOVE `economyMode` from `.squad/config.json` (if it was persisted) +2. CLEAR session economy mode state +3. ACKNOWLEDGE: `βœ… Economy mode disabled β€” returning to standard model selection.` + +### STOP + +After updating economy mode state and including the `πŸ’°` indicator in spawn acknowledgments, this skill is done. Do NOT: +- Change Layer 0, Layer 1, or Layer 2 model choices +- Override charter-specified models +- Generate cost reports or comparisons +- Fall back to premium models via economy mode (economy mode never bumps UP) + +## Config Schema + +`.squad/config.json` economy-related fields: + +```json +{ + "version": 1, + "economyMode": true +} +``` + +- `economyMode` β€” when `true`, Layer 3 uses the economy table. Optional; absent = economy mode off. +- Combines with `defaultModel` and `agentModelOverrides` β€” Layer 0 always wins. + +## Anti-Patterns + +- **Don't override Layer 0 in economy mode.** If the user set `defaultModel: "claude-opus-4.6"`, they want quality. Economy mode only affects Layer 3 auto-selection. +- **Don't silently apply economy mode.** Always acknowledge when activated or deactivated. +- **Don't treat economy mode as permanent by default.** Session phrases activate session-only; only "always" or `config.json` persist it. +- **Don't bump premium tasks down too far.** Architecture and security reviews shift from opus to sonnet in economy mode β€” they do NOT go to fast/cheap models. From e35d5a39d9feddd3d74b3ac8f2d78db432b5c9b1 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 22 Mar 2026 02:50:37 -0700 Subject: [PATCH 2/4] feat: economy mode for cost-conscious model selection (#500) Add economy mode that shifts model selection to cheaper alternatives at Layer 3 (task-aware auto) and Layer 4 (default). Explicit user preferences at Layers 0-2 are never overridden. SDK changes: - Add ECONOMY_MODEL_MAP and applyEconomyMode() to config/models.ts - Add readEconomyMode() / writeEconomyMode() for config.json persistence - Update resolveModel() in config/models.ts with economyMode option - Update resolveModel() in agents/model-selector.ts with economyMode option CLI changes: - Add 'squad economy [on|off]' command for persistent toggle - Add --economy global flag for session-level activation (SQUAD_ECONOMY_MODE=1) Tests: - 34 new tests in test/economy-mode.test.ts (all passing) Economy model table: claude-opus-4.6 -> claude-sonnet-4.5 (architecture/review) claude-sonnet-4.6 -> gpt-4.1 (code writing) claude-haiku-4.5 -> gpt-4.1 (docs/mechanical) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .squad/agents/eecom/history.md | 19 ++ .squad/decisions/inbox/eecom-economy-mode.md | 37 +++ packages/squad-cli/src/cli-entry.ts | 14 + .../squad-cli/src/cli/commands/economy.ts | 68 ++++ .../squad-sdk/src/agents/model-selector.ts | 85 ++--- packages/squad-sdk/src/config/models.ts | 110 ++++++- test/economy-mode.test.ts | 291 ++++++++++++++++++ 7 files changed, 577 insertions(+), 47 deletions(-) create mode 100644 .squad/decisions/inbox/eecom-economy-mode.md create mode 100644 packages/squad-cli/src/cli/commands/economy.ts create mode 100644 test/economy-mode.test.ts diff --git a/.squad/agents/eecom/history.md b/.squad/agents/eecom/history.md index b4007c402..7fce94c9c 100644 --- a/.squad/agents/eecom/history.md +++ b/.squad/agents/eecom/history.md @@ -103,3 +103,22 @@ CLI completeness audit (2026-03-08) confirmed: 26 primary commands routed in cli Implemented Flight's privacy-first adoption monitoring strategy on PR #326 branch. Moved `.squad/adoption/` β†’ `.github/adoption/` for better GitHub integration. Stripped tracking.md to aggregate-only metrics (removed all individual repo names/URLs). Updated GitHub Action workflow (adoption-report.yml) and monitoring script (scripts/adoption-monitor.mjs) to write reports to `.github/adoption/reports/`. Removed "Built with Squad" showcase link from README.md (deferred to Tier 2 opt-in feature). This honors the principle: collect aggregate metrics via public APIs, but never publish individual repo lists without explicit consent. Test discipline: verified npm run build passes; docs-build.test.ts passed structure tests (Astro build failure unrelated to changes). Committed with clear message explaining privacy rationale. πŸ“Œ **Team update (2026-03-10T12-55-49Z):** Adoption tracking Tier 1 complete and merged to decisions.md. Privacy-first architecture confirmed: aggregate metrics only, opt-in for individual repos, public showcase only when 5+ projects opt in. Append-only file governance enforced (no deletions in history.md or decisions.md). Microsoft ampersand style guide adopted for documentation. + +### Economy Mode Implementation (#500) (2026-03-20) + +**Context:** Issue #500 requested economy mode β€” a session-level and persistent modifier that shifts model selection to cheaper alternatives. + +**Architecture decision:** Economy mode is a Layer 3/4 modifier only. Layers 0–2 (explicit user preferences: config.json, session directive, charter) are never downgraded. This preserves user intent while enabling cost savings on auto-selected tasks. + +**Implementation:** +1. `ECONOMY_MODEL_MAP` + `applyEconomyMode()` in `config/models.ts` β€” pure mapping function for premiumβ†’standard and standardβ†’fast downgrades +2. `readEconomyMode()` + `writeEconomyMode()` β€” config.json read/write functions (same merge-without-clobber pattern as `writeModelPreference()`) +3. `resolveModel()` in `config/models.ts` updated with `economyMode?: boolean` option; falls back to reading from `squadDir` if not provided +4. `resolveModel()` in `agents/model-selector.ts` updated with `economyMode?: boolean` β€” both SDK resolvers are economy-aware +5. `squad economy [on|off]` command in CLI for persistent toggle +6. `--economy` global flag in `cli-entry.ts` sets `SQUAD_ECONOMY_MODE=1` env var for session scope +7. 34 new tests in `test/economy-mode.test.ts` β€” all pass + +**Key pattern:** Both resolveModel implementations follow identical principle: explicit overrides (user choice) are sacred; economy only affects computed auto-selection. + +**PR:** #500 branch `squad/500-economy-mode` diff --git a/.squad/decisions/inbox/eecom-economy-mode.md b/.squad/decisions/inbox/eecom-economy-mode.md new file mode 100644 index 000000000..d534aa5d6 --- /dev/null +++ b/.squad/decisions/inbox/eecom-economy-mode.md @@ -0,0 +1,37 @@ +# Economy Mode Design β€” #500 + +**Date:** 2026-03-20 +**Author:** EECOM +**Issue:** #500 + +## Decision + +Economy mode is implemented as a modifier that shifts model selection at Layer 3 (task-aware auto) and Layer 4 (default fallback) only. Layers 0–2 (explicit user preferences) are never downgraded. + +## Model Map + +| Normal | Economy | Use case | +|--------|---------|----------| +| `claude-opus-4.6` | `claude-sonnet-4.5` | Architecture, review | +| `claude-sonnet-4.6` | `gpt-4.1` | Code writing | +| `claude-sonnet-4.5` | `gpt-4.1` | Code writing | +| `claude-haiku-4.5` | `gpt-4.1` | Docs, planning, mechanical | + +## Activation + +1. **Persistent:** `"economyMode": true` in `.squad/config.json` (survives sessions) +2. **Session:** `--economy` CLI flag (sets `SQUAD_ECONOMY_MODE=1` env var, current session only) +3. **Toggle command:** `squad economy on|off` writes to config.json + +## Hierarchy Integration + +Economy mode is a Layer 3/4 modifier β€” it does NOT override explicit preferences (Layers 0–2). This is intentional: if a user said "always use opus", economy mode respects that choice. + +## Implementation Points + +- `ECONOMY_MODEL_MAP` + `applyEconomyMode()` in `packages/squad-sdk/src/config/models.ts` +- `readEconomyMode()` + `writeEconomyMode()` in `packages/squad-sdk/src/config/models.ts` +- `resolveModel()` in `config/models.ts` accepts `economyMode?: boolean` option; reads from config if not provided +- `resolveModel()` in `agents/model-selector.ts` also supports `economyMode?: boolean` +- `squad economy [on|off]` command in `packages/squad-cli/src/cli/commands/economy.ts` +- `--economy` global flag in `cli-entry.ts` diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index da8ce8b56..6a121f853 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -132,6 +132,11 @@ async function checkNodeSqlite(): Promise { async function main(): Promise { const args = process.argv.slice(2); const hasGlobal = args.includes('--global'); + // --economy activates economy mode for this session (sets env var for spawner) + const hasEconomy = args.includes('--economy'); + if (hasEconomy) { + process.env['SQUAD_ECONOMY_MODE'] = '1'; + } const rawCmd = args[0]; const cmd = rawCmd?.trim() || ''; @@ -225,12 +230,15 @@ async function main(): Promise { console.log(` upstream remove `); console.log(` upstream list`); console.log(` upstream sync [name]`); + console.log(` ${BOLD}economy${RESET} Toggle economy mode (cost-conscious model selection)`); + console.log(` Usage: economy [on|off]`); console.log(` ${BOLD}help${RESET} Show this help message`); console.log(`\nFlags:`); console.log(` ${BOLD}--version, -v${RESET} Print version`); console.log(` ${BOLD}--help, -h${RESET} Show help`); console.log(` ${BOLD}--global${RESET} Use personal (global) squad path (for init, upgrade)`); + console.log(` ${BOLD}--economy${RESET} Activate economy mode for this session (cheaper models)`); console.log(`\nInstallation:`); console.log(` npm install --save-dev @bradygaster/squad-cli`); console.log(`\nInsider channel:`); @@ -605,6 +613,12 @@ async function main(): Promise { return; } + if (cmd === 'economy') { + const { runEconomy } = await import('./cli/commands/economy.js'); + await runEconomy(process.cwd(), args.slice(1)); + return; + } + // Unknown command fatal(`Unknown command: ${cmd}\n Run 'squad doctor' to check your setup, or 'squad help' for usage information.`); } diff --git a/packages/squad-cli/src/cli/commands/economy.ts b/packages/squad-cli/src/cli/commands/economy.ts new file mode 100644 index 000000000..3043d03a2 --- /dev/null +++ b/packages/squad-cli/src/cli/commands/economy.ts @@ -0,0 +1,68 @@ +/** + * Economy mode command β€” toggle cost-conscious model selection. + * + * Usage: + * squad economy on β€” enable economy mode (persisted to config.json) + * squad economy off β€” disable economy mode + * squad economy β€” show current status + */ + +import { join } from 'node:path'; +import { existsSync, readFileSync } from 'node:fs'; +import { writeEconomyMode, readEconomyMode } from '@bradygaster/squad-sdk/config'; +import { fatal } from '../core/errors.js'; +import { BOLD, RESET, GREEN, DIM } from '../core/output.js'; + +function resolveSquadDir(cwd: string): string | null { + // Walk up to find .squad/ + let dir = cwd; + for (let i = 0; i < 10; i++) { + const candidate = join(dir, '.squad'); + if (existsSync(candidate)) { + return candidate; + } + const parent = join(dir, '..'); + if (parent === dir) break; + dir = parent; + } + return null; +} + +export async function runEconomy(cwd: string, subArgs: string[]): Promise { + const squadDir = resolveSquadDir(cwd); + if (!squadDir) { + fatal('No squad found. Run "squad init" first.'); + return; + } + + const sub = subArgs[0]?.toLowerCase(); + + if (sub === 'on') { + writeEconomyMode(squadDir, true); + console.log(`${GREEN}βœ“${RESET} Economy mode ${BOLD}enabled${RESET} β€” cheaper models will be used for auto-selection.`); + console.log(` Saved to: ${DIM}${join(squadDir, 'config.json')}${RESET}`); + return; + } + + if (sub === 'off') { + writeEconomyMode(squadDir, false); + console.log(`${GREEN}βœ“${RESET} Economy mode ${BOLD}disabled${RESET} β€” returning to standard model selection.`); + console.log(` Saved to: ${DIM}${join(squadDir, 'config.json')}${RESET}`); + return; + } + + // Status display + const enabled = readEconomyMode(squadDir); + console.log(`\n${BOLD}Economy Mode${RESET}\n`); + console.log(` Status: ${enabled ? `${GREEN}enabled${RESET}` : `${DIM}disabled${RESET}`}`); + console.log(` Config: ${DIM}${join(squadDir, 'config.json')}${RESET}\n`); + if (enabled) { + console.log(` When active, auto-selected models are downgraded:`); + console.log(` ${DIM}claude-opus-4.6 β†’ claude-sonnet-4.5 (architecture/review)${RESET}`); + console.log(` ${DIM}claude-sonnet-4.6 β†’ gpt-4.1 (code writing)${RESET}`); + console.log(` ${DIM}claude-haiku-4.5 β†’ gpt-4.1 (docs/mechanical)${RESET}`); + console.log(` Explicit overrides (config.json, charter) are never changed.\n`); + } else { + console.log(` Usage: squad economy on | off\n`); + } +} diff --git a/packages/squad-sdk/src/agents/model-selector.ts b/packages/squad-sdk/src/agents/model-selector.ts index 2e2eee305..0fa0bfacb 100644 --- a/packages/squad-sdk/src/agents/model-selector.ts +++ b/packages/squad-sdk/src/agents/model-selector.ts @@ -3,6 +3,7 @@ */ import { MODELS } from '../runtime/constants.js'; +import { applyEconomyMode } from '../config/models.js'; import type { EventBus } from '../runtime/event-bus.js'; /** @@ -32,6 +33,8 @@ export interface ModelResolutionOptions { taskType: TaskType; /** Agent role (for context) */ agentRole?: string; + /** When true, apply economy mode substitution at Layer 3/4 */ + economyMode?: boolean; } /** @@ -55,9 +58,9 @@ export interface ResolvedModel { * @returns Resolved model with tier and fallback chain */ export function resolveModel(options: ModelResolutionOptions): ResolvedModel { - const { userOverride, charterPreference, taskType } = options; + const { userOverride, charterPreference, taskType, economyMode } = options; - // Layer 1: User Override + // Layer 1: User Override (explicit β€” economy does not apply) if (userOverride && userOverride.trim().length > 0) { const tier = inferTierFromModel(userOverride); return { @@ -68,7 +71,7 @@ export function resolveModel(options: ModelResolutionOptions): ResolvedModel { }; } - // Layer 2: Charter Preference + // Layer 2: Charter Preference (explicit β€” economy does not apply) if (charterPreference && charterPreference.trim().length > 0 && charterPreference !== 'auto') { const tier = inferTierFromModel(charterPreference); return { @@ -79,66 +82,70 @@ export function resolveModel(options: ModelResolutionOptions): ResolvedModel { }; } - // Layer 3: Task-Aware Auto-Selection - const autoSelected = selectModelForTask(taskType); + // Layer 3: Task-Aware Auto-Selection (economy mode applies) + const autoSelected = selectModelForTask(taskType, economyMode); if (autoSelected) { return autoSelected; } - // Layer 4: Default + // Layer 4: Default (economy mode applies) + const defaultModel = economyMode + ? applyEconomyMode(MODELS.SELECTOR_DEFAULT) + : MODELS.SELECTOR_DEFAULT; + const defaultTier = inferTierFromModel(defaultModel); return { - model: MODELS.SELECTOR_DEFAULT, - tier: MODELS.SELECTOR_DEFAULT_TIER, + model: defaultModel, + tier: defaultTier, source: 'default', - fallbackChain: [...MODELS.FALLBACK_CHAINS[MODELS.SELECTOR_DEFAULT_TIER]], + fallbackChain: [...MODELS.FALLBACK_CHAINS[defaultTier]], }; } /** - * Select model based on task type. + * Select model based on task type, with optional economy mode substitution. * * @param taskType - Type of task being performed + * @param economyMode - When true, downgrade model to cheaper alternative * @returns Resolved model or undefined if no match */ -function selectModelForTask(taskType: TaskType): ResolvedModel | undefined { +function selectModelForTask(taskType: TaskType, economyMode?: boolean): ResolvedModel | undefined { + let model: string | undefined; + let tier: ModelTier | undefined; + switch (taskType) { case 'code': - return { - model: 'claude-sonnet-4.6', - tier: 'standard', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.standard], - }; - + model = 'claude-sonnet-4.6'; + tier = 'standard'; + break; case 'prompt': - return { - model: 'claude-sonnet-4.6', - tier: 'standard', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.standard], - }; - + model = 'claude-sonnet-4.6'; + tier = 'standard'; + break; case 'visual': - return { - model: 'claude-opus-4.6', - tier: 'premium', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.premium], - }; - + model = 'claude-opus-4.6'; + tier = 'premium'; + break; case 'docs': case 'planning': case 'mechanical': - return { - model: 'claude-haiku-4.5', - tier: 'fast', - source: 'task-auto', - fallbackChain: [...MODELS.FALLBACK_CHAINS.fast], - }; - + model = 'claude-haiku-4.5'; + tier = 'fast'; + break; default: return undefined; } + + if (economyMode) { + model = applyEconomyMode(model); + tier = inferTierFromModel(model); + } + + return { + model, + tier, + source: 'task-auto', + fallbackChain: [...MODELS.FALLBACK_CHAINS[tier]], + }; } export function inferTierFromModel(model: string): ModelTier { diff --git a/packages/squad-sdk/src/config/models.ts b/packages/squad-sdk/src/config/models.ts index 370040bc6..cac32e4a8 100644 --- a/packages/squad-sdk/src/config/models.ts +++ b/packages/squad-sdk/src/config/models.ts @@ -511,12 +511,93 @@ export function estimateCost(model: string, inputTokens: number, outputTokens: n // Persistent Model Preference (Layer 0) // ============================================================================ +/** + * Economy mode model map: normal model β†’ cheaper alternative. + * + * Applied at Layer 3 (task-aware auto) and Layer 4 (default) when + * economy mode is active. Layers 0–2 (explicit preferences) are + * never substituted β€” the user's explicit choice always wins. + * + * Table source: issue #500 + */ +export const ECONOMY_MODEL_MAP: Record = { + // Premium β†’ standard downgrade (architecture/review tasks) + 'claude-opus-4.6': 'claude-sonnet-4.5', + 'claude-opus-4.6-fast': 'claude-sonnet-4.5', + 'claude-opus-4.5': 'claude-sonnet-4.5', + // Standard β†’ fast downgrade (code writing, docs, planning, triage) + 'claude-sonnet-4.6': 'gpt-4.1', + 'claude-sonnet-4.5': 'gpt-4.1', + // Fast β†’ cheapest fast (scribe/mechanical, docs) + 'claude-haiku-4.5': 'gpt-4.1', +}; + +/** + * Applies economy mode substitution to a model ID. + * Returns the cheaper economy alternative, or the original if no mapping exists. + */ +export function applyEconomyMode(model: string): string { + return ECONOMY_MODEL_MAP[model] ?? model; +} + /** * Shape of model preference fields within `.squad/config.json`. */ export interface ModelPreferenceConfig { defaultModel?: string; agentModelOverrides?: Record; + economyMode?: boolean; +} + +/** + * Reads the economy mode setting from `.squad/config.json`. + * + * @param squadDir - Path to the `.squad/` directory + * @returns True if economyMode is enabled, false otherwise + */ +export function readEconomyMode(squadDir: string): boolean { + const configPath = join(squadDir, 'config.json'); + if (!existsSync(configPath)) { + return false; + } + try { + const raw = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw); + return parsed !== null && + typeof parsed === 'object' && + parsed.economyMode === true; + } catch { + return false; + } +} + +/** + * Writes the economy mode setting to `.squad/config.json`. + * Merges with existing config β€” does not overwrite other fields. + * + * @param squadDir - Path to the `.squad/` directory + * @param enabled - Whether economy mode should be enabled + */ +export function writeEconomyMode(squadDir: string, enabled: boolean): void { + const configPath = join(squadDir, 'config.json'); + let config: Record = {}; + if (existsSync(configPath)) { + try { + config = JSON.parse(readFileSync(configPath, 'utf-8')); + } catch { + config = { version: 1 }; + } + } else { + config = { version: 1 }; + } + + if (enabled) { + config.economyMode = true; + } else { + delete config.economyMode; + } + + writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8'); } /** @@ -652,6 +733,10 @@ export function writeAgentModelOverrides( * * Per-agent overrides from config.json take priority over the global defaultModel. * + * Economy mode modifier: when active (via economyMode option or config.json), + * shifts model selection at Layer 3 and Layer 4 to cheaper alternatives per + * ECONOMY_MODEL_MAP. Layers 0–2 (explicit user preferences) are never overridden. + * * @param options - Resolution inputs * @returns Resolved model ID */ @@ -661,10 +746,12 @@ export function resolveModel(options: { sessionDirective?: string | null; charterPreference?: string | null; taskModel?: string | null; + /** When true, apply economy mode substitution at Layer 3/4. Overrides config. */ + economyMode?: boolean; }): string { const { agentName, squadDir, sessionDirective, charterPreference, taskModel } = options; - // Layer 0a: Per-agent persistent override + // Layer 0a: Per-agent persistent override (explicit β€” economy does not apply) if (squadDir && agentName) { const agentOverrides = readAgentModelOverrides(squadDir); if (agentOverrides[agentName]) { @@ -672,7 +759,7 @@ export function resolveModel(options: { } } - // Layer 0b: Global persistent config + // Layer 0b: Global persistent config (explicit β€” economy does not apply) if (squadDir) { const persistedModel = readModelPreference(squadDir); if (persistedModel) { @@ -680,21 +767,28 @@ export function resolveModel(options: { } } - // Layer 1: Session-wide user directive + // Layer 1: Session-wide user directive (explicit β€” economy does not apply) if (sessionDirective) { return sessionDirective; } - // Layer 2: Charter preference + // Layer 2: Charter preference (explicit β€” economy does not apply) if (charterPreference) { return charterPreference; } - // Layer 3: Task-aware auto-selection + // Determine if economy mode is active (option takes precedence over config) + const isEconomy = + options.economyMode !== undefined + ? options.economyMode + : (squadDir ? readEconomyMode(squadDir) : false); + + // Layer 3: Task-aware auto-selection (economy mode applies) if (taskModel) { - return taskModel; + return isEconomy ? applyEconomyMode(taskModel) : taskModel; } - // Layer 4: Default - return 'claude-haiku-4.5'; + // Layer 4: Default (economy mode applies) + const defaultModel = 'claude-haiku-4.5'; + return isEconomy ? applyEconomyMode(defaultModel) : defaultModel; } diff --git a/test/economy-mode.test.ts b/test/economy-mode.test.ts new file mode 100644 index 000000000..9835b4816 --- /dev/null +++ b/test/economy-mode.test.ts @@ -0,0 +1,291 @@ +/** + * Tests for economy mode model selection (issue #500). + * + * Validates that when economy mode is active: + * - Layer 3 (task-aware) and Layer 4 (default) models are downgraded + * - Layer 0–2 (explicit preferences) are never overridden + * - config.json read/write round-trips correctly + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { + resolveModel, + readEconomyMode, + writeEconomyMode, + applyEconomyMode, + ECONOMY_MODEL_MAP, +} from '@bradygaster/squad-sdk/config'; +import { + resolveModel as sdkResolveModel, +} from '@bradygaster/squad-sdk/agents'; + +let squadDir: string; + +beforeEach(() => { + squadDir = mkdtempSync(join(tmpdir(), 'squad-economy-')); +}); + +afterEach(() => { + rmSync(squadDir, { recursive: true, force: true }); +}); + +// ============================================================================ +// ECONOMY_MODEL_MAP + applyEconomyMode +// ============================================================================ + +describe('ECONOMY_MODEL_MAP', () => { + it('maps premium models to standard', () => { + expect(ECONOMY_MODEL_MAP['claude-opus-4.6']).toBe('claude-sonnet-4.5'); + expect(ECONOMY_MODEL_MAP['claude-opus-4.6-fast']).toBe('claude-sonnet-4.5'); + expect(ECONOMY_MODEL_MAP['claude-opus-4.5']).toBe('claude-sonnet-4.5'); + }); + + it('maps standard sonnet models to fast', () => { + expect(ECONOMY_MODEL_MAP['claude-sonnet-4.6']).toBe('gpt-4.1'); + expect(ECONOMY_MODEL_MAP['claude-sonnet-4.5']).toBe('gpt-4.1'); + }); + + it('maps haiku to cheapest fast', () => { + expect(ECONOMY_MODEL_MAP['claude-haiku-4.5']).toBe('gpt-4.1'); + }); +}); + +describe('applyEconomyMode', () => { + it('returns economy model for known models', () => { + expect(applyEconomyMode('claude-opus-4.6')).toBe('claude-sonnet-4.5'); + expect(applyEconomyMode('claude-sonnet-4.6')).toBe('gpt-4.1'); + expect(applyEconomyMode('claude-haiku-4.5')).toBe('gpt-4.1'); + }); + + it('returns original model when no economy mapping exists', () => { + expect(applyEconomyMode('gpt-4.1')).toBe('gpt-4.1'); + expect(applyEconomyMode('gpt-5-mini')).toBe('gpt-5-mini'); + expect(applyEconomyMode('unknown-model-xyz')).toBe('unknown-model-xyz'); + }); +}); + +// ============================================================================ +// readEconomyMode / writeEconomyMode +// ============================================================================ + +describe('readEconomyMode', () => { + it('returns false when config.json does not exist', () => { + expect(readEconomyMode(squadDir)).toBe(false); + }); + + it('returns false when economyMode field is absent', () => { + writeFileSync(join(squadDir, 'config.json'), JSON.stringify({ version: 1 })); + expect(readEconomyMode(squadDir)).toBe(false); + }); + + it('returns false when economyMode is false', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: false }) + ); + expect(readEconomyMode(squadDir)).toBe(false); + }); + + it('returns true when economyMode is true', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: true }) + ); + expect(readEconomyMode(squadDir)).toBe(true); + }); + + it('returns false on malformed JSON', () => { + writeFileSync(join(squadDir, 'config.json'), '{ bad json'); + expect(readEconomyMode(squadDir)).toBe(false); + }); +}); + +describe('writeEconomyMode', () => { + it('creates config.json with economyMode: true', () => { + writeEconomyMode(squadDir, true); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.economyMode).toBe(true); + expect(raw.version).toBe(1); + }); + + it('removes economyMode field when set to false', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: true }) + ); + writeEconomyMode(squadDir, false); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw).not.toHaveProperty('economyMode'); + }); + + it('merges with existing config without clobbering other fields', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + writeEconomyMode(squadDir, true); + const raw = JSON.parse(readFileSync(join(squadDir, 'config.json'), 'utf-8')); + expect(raw.defaultModel).toBe('claude-opus-4.6'); + expect(raw.economyMode).toBe(true); + }); + + it('round-trips: write on β†’ read true', () => { + writeEconomyMode(squadDir, true); + expect(readEconomyMode(squadDir)).toBe(true); + }); + + it('round-trips: write off β†’ read false', () => { + writeEconomyMode(squadDir, true); + writeEconomyMode(squadDir, false); + expect(readEconomyMode(squadDir)).toBe(false); + }); +}); + +// ============================================================================ +// resolveModel β€” economy mode option +// ============================================================================ + +describe('resolveModel economy mode (option)', () => { + it('Layer 4 default: uses gpt-4.1 instead of haiku when economyMode: true', () => { + expect(resolveModel({ economyMode: true })).toBe('gpt-4.1'); + }); + + it('Layer 4 default: uses haiku when economyMode: false', () => { + expect(resolveModel({ economyMode: false })).toBe('claude-haiku-4.5'); + }); + + it('Layer 3 code task: uses gpt-4.1 instead of sonnet when economyMode: true', () => { + expect(resolveModel({ taskModel: 'claude-sonnet-4.6', economyMode: true })).toBe('gpt-4.1'); + }); + + it('Layer 3 architecture task: uses sonnet instead of opus when economyMode: true', () => { + expect(resolveModel({ taskModel: 'claude-opus-4.6', economyMode: true })).toBe('claude-sonnet-4.5'); + }); + + it('Layer 3 docs task: uses gpt-4.1 instead of haiku when economyMode: true', () => { + expect(resolveModel({ taskModel: 'claude-haiku-4.5', economyMode: true })).toBe('gpt-4.1'); + }); + + it('Layer 2 charter preference: NOT overridden by economy mode', () => { + expect( + resolveModel({ charterPreference: 'claude-opus-4.6', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 1 session directive: NOT overridden by economy mode', () => { + expect( + resolveModel({ sessionDirective: 'claude-opus-4.6', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 0b global config: NOT overridden by economy mode', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, defaultModel: 'claude-opus-4.6' }) + ); + expect( + resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); + + it('Layer 0a per-agent override: NOT overridden by economy mode', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ + version: 1, + agentModelOverrides: { eecom: 'claude-opus-4.6' }, + }) + ); + expect( + resolveModel({ agentName: 'eecom', squadDir, taskModel: 'claude-haiku-4.5', economyMode: true }) + ).toBe('claude-opus-4.6'); + }); +}); + +// ============================================================================ +// resolveModel β€” economy mode from config.json +// ============================================================================ + +describe('resolveModel economy mode (from config)', () => { + it('uses economy model when economyMode: true in config', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: true }) + ); + expect(resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6' })).toBe('gpt-4.1'); + }); + + it('uses normal model when economyMode absent from config', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1 }) + ); + expect(resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6' })).toBe('claude-sonnet-4.6'); + }); + + it('explicit economyMode option overrides config setting', () => { + writeFileSync( + join(squadDir, 'config.json'), + JSON.stringify({ version: 1, economyMode: false }) + ); + // Option says true, config says false β†’ option wins + expect( + resolveModel({ squadDir, taskModel: 'claude-sonnet-4.6', economyMode: true }) + ).toBe('gpt-4.1'); + }); +}); + +// ============================================================================ +// SDK model-selector resolveModel economy mode +// ============================================================================ + +describe('SDK resolveModel (agents) economy mode', () => { + it('code task β†’ gpt-4.1 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'code', economyMode: true }); + expect(result.model).toBe('gpt-4.1'); + expect(result.source).toBe('task-auto'); + }); + + it('docs task β†’ gpt-4.1 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'docs', economyMode: true }); + expect(result.model).toBe('gpt-4.1'); + }); + + it('mechanical task β†’ gpt-4.1 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'mechanical', economyMode: true }); + expect(result.model).toBe('gpt-4.1'); + }); + + it('visual task β†’ claude-sonnet-4.5 when economyMode: true', () => { + const result = sdkResolveModel({ taskType: 'visual', economyMode: true }); + expect(result.model).toBe('claude-sonnet-4.5'); + }); + + it('code task β†’ claude-sonnet-4.6 when economyMode: false', () => { + const result = sdkResolveModel({ taskType: 'code', economyMode: false }); + expect(result.model).toBe('claude-sonnet-4.6'); + }); + + it('user override NOT affected by economy mode', () => { + const result = sdkResolveModel({ + taskType: 'code', + userOverride: 'claude-opus-4.6', + economyMode: true, + }); + expect(result.model).toBe('claude-opus-4.6'); + expect(result.source).toBe('user-override'); + }); + + it('charter preference NOT affected by economy mode', () => { + const result = sdkResolveModel({ + taskType: 'code', + charterPreference: 'claude-opus-4.6', + economyMode: true, + }); + expect(result.model).toBe('claude-opus-4.6'); + expect(result.source).toBe('charter'); + }); +}); From d4e9642e71baccd6023ca667b0a023c392fae84d Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 22 Mar 2026 02:56:22 -0700 Subject: [PATCH 3/4] Scribe: Wave 1 decisions merged (#329/#344/#500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merged 9 inbox decisions β†’ decisions.md (personal squad impl plan, economy mode design/governance, persistent model preference, user directives, template sync, ESM fix) - Deleted inbox files (merge complete) - Updated agent histories: Flight (personal squad plan), EECOM (economy mode), Procedures (governance proposals) - Appended team updates to agent contexts Fleet Status: - Flight: Personal squad design validated, 19-task plan across 4 PRs (MVP = PR #1+3) - EECOM: Economy mode complete (34 tests), PR #504 open - Procedures: Economy + consult-mode governance drafted, PR #503 open - Directives: #464 rate limit UX (soft dep #500), #502 bug P1 next-up, triage includes discussions Ready for execution: EECOM Phase 1, Procedures Phase 3 (concurrent). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .squad/agents/eecom/history.md | 2 + .squad/agents/flight/history.md | 21 +++ .squad/agents/procedures/history.md | 2 + .squad/decisions.md | 139 +++++++++++++++ .squad/decisions/inbox/eecom-economy-mode.md | 37 ---- .../decisions/inbox/fenster-template-audit.md | 21 --- .squad/decisions/inbox/gnc-node22-esm.md | 25 --- .../procedures-economy-mode-governance.md | 126 -------------- .../procedures-personal-squad-governance.md | 158 ------------------ 9 files changed, 164 insertions(+), 367 deletions(-) delete mode 100644 .squad/decisions/inbox/eecom-economy-mode.md delete mode 100644 .squad/decisions/inbox/fenster-template-audit.md delete mode 100644 .squad/decisions/inbox/gnc-node22-esm.md delete mode 100644 .squad/decisions/inbox/procedures-economy-mode-governance.md delete mode 100644 .squad/decisions/inbox/procedures-personal-squad-governance.md diff --git a/.squad/agents/eecom/history.md b/.squad/agents/eecom/history.md index 7fce94c9c..0cd497032 100644 --- a/.squad/agents/eecom/history.md +++ b/.squad/agents/eecom/history.md @@ -4,6 +4,8 @@ ## Learnings +πŸ“Œ **Team update (2026-03-22T09-35Z β€” Wave 1):** Economy mode fully implemented: ECONOMY_MODEL_MAP + resolveModel() integration in SDK, `squad economy on|off` CLI command, `--economy` flag, 34 tests passing. PR #504 open for review. Soft dependency: #464 rate limit UX should offer economy mode as recovery. Next: Phase 1 of ambient personal squad (T1–T5, T19) β€” ready to start immediately after merging current work. Procedures wrote governance proposals for squad.agent.md β€” awaiting Flight review. + ### CLI Entry Point Architecture cli-entry.ts is the central router for ~30+ CLI commands using dynamic imports (lazy-loading). Commands are routed via if-else blocks. Has a recurring "unwired command" bug class β€” implementations exist in cli/commands/ but aren't routed in cli-entry.ts. The cli-command-wiring.test.ts regression test catches this by verifying every .ts file in cli/commands/ is imported. diff --git a/.squad/agents/flight/history.md b/.squad/agents/flight/history.md index 2ded0bf56..c9462ead8 100644 --- a/.squad/agents/flight/history.md +++ b/.squad/agents/flight/history.md @@ -4,6 +4,8 @@ --- +πŸ“Œ **Team update (2026-03-22T09-35Z β€” Wave 1):** Ambient personal squad design validated and 19-task implementation plan authored across 4 PRs (Phase 1 SDK, Phase 2 CLI, Phase 3 governance, Phase 4 tests). MVP = PR #1 + PR #3. EECOM executing Phase 1–2 (SDK + CLI), Procedures executing Phase 3 (governance) concurrently. All design gaps resolved; dependency graph established. Procedures wrote governance proposals for personal squad + economy mode β€” awaiting your review. Sims to execute Phase 4 after Phase 1+2 merge. Directive captured: bug #502 (node:sqlite, P1) to be picked up after Wave 1. No blocking issues β€” ready for execution. + ## Core Context Three-branch model (main/dev/insiders). Apollo 13 team, 3931 tests. Boundary review heuristic: "Squad Ships It" β€” if Squad doesn't ship the code, it's IRL content. Proposal-first: meaningful changes need docs/proposals/ before code. Two-error lockout policy: agent locked out after 2 errors in a session. Test name-agnosticism: framework tests must never depend on dev team's agent names. @@ -40,3 +42,22 @@ Approved PR #331 ("docs: scenario and feature guides from blog analysis") for me Created `.squad/skills/content-triage/SKILL.md` to codify the boundary heuristic from PR #331. Defines repeatable workflow for triaging external content (blog posts, sample repos, videos, talks) to determine what belongs in Squad's public docs vs IRL tracking. Key components: (1) "Squad Ships It" litmus test β€” if Squad doesn't ship the code/config, it's IRL content; (2) triage workflow triggered by `content-triage` label or external content reference in issue body; (3) output format with boundary analysis, sub-issues for PAO (doc extraction), and IRL reference entry for Scribe; (4) label convention (`content:blog`, `content:sample`, `content:video`, `content:talk`); (5) Ralph integration for routing to Flight, creating sub-issues, and notifying Scribe. Examples include Tamir blog analysis (PR #331), sample repo with ops patterns, and conference talk. Pattern prevents infrastructure docs from polluting Squad's public docs while ensuring community content accelerates adoption through proper extraction and referencing. πŸ“Œ **Team update (2026-03-11T01:27:57Z):** Content triage skill finalized; "Squad Ships It" boundary heuristic codified into shared team decision (decisions.md). Remote Squad access phased rollout approved (Discussions bot β†’ Copilot Extension β†’ Chat bot). PR #331 boundary review pattern established as standard for all doc PRs. Triage workflow enables Flight to scale as community content accelerates. + +### Ambient Personal Squad Architecture Review (#329 + #344) + +**Design validated:** The `flight-ambient-personal-squad.md` proposal is structurally sound. Key finding: `multi-squad.ts` already stores personal squad paths as direct dirs (`squads/{name}/`) with no nested `.squad/` subfolder β€” the "each team IS the squad root" convention is already the implementation, not a change needed. + +**Five gaps found in the design doc:** +1. No `resolvePersonalAgents()` function signature β€” added in implementation plan (T2). +2. Scenario 9 contradiction: personal agents wrote to project orchestration log, violating ghost protocol. Resolution: coordinator writes audit trail (project state), not the personal agent. +3. `--team-root` scope was undefined. Decision: additive CLI flag on `squad init`, backward compat with existing `config.json` teamRoot. +4. `squad personal init` was missing β€” bootstrapping path for first-time users. Added as T6 subcommand. +5. `SQUAD_NO_PERSONAL` env var was in Open Questions but absent from phases. Added to T1. + +**Architecture decision:** Need `ensureSquadPathTriple` in `resolution.ts` (T4) β€” personal agents write to a third root (personal squad dir). Without it, ghost protocol is advisory-only and not enforced by path guards in SDK. + +**Phasing:** Four PRs. MVP = PR #1 (SDK Foundation) + PR #3 (Governance). Users see ambient cast immediately; `squad personal` commands are quality-of-life on top. + +**Implementation plan written to:** `.squad/decisions/inbox/flight-329-344-implementation-plan.md` + +πŸ“Œ **Team update (2026-03-24):** Ambient personal squad design reviewed and approved with 5 gaps identified and resolved. Implementation plan broken into 4 PRs across EECOM (SDK + CLI), Procedures (governance), and Sims (tests). MVP path = SDK foundation + governance updates. Phased to avoid one giant PR. diff --git a/.squad/agents/procedures/history.md b/.squad/agents/procedures/history.md index ea52095bf..ba4b42c07 100644 --- a/.squad/agents/procedures/history.md +++ b/.squad/agents/procedures/history.md @@ -4,6 +4,8 @@ ## Learnings +πŸ“Œ **Team update (2026-03-22T09-35Z β€” Wave 1):** Economy mode governance proposal and personal squad consult-mode governance proposal authored for squad.agent.md β€” both DRAFT, awaiting Flight review before merging. Economy mode adds Layer 3 table + spawn convention (`πŸ’° economy`) + model catalog audit. Personal squad adds consult mode detection, path reference table, spawn guidance. Persistent model preference (Layer 0) documented. Proposed new skill: `.squad/skills/consult-mode/SKILL.md` (post-approval). Deterministic skill pattern proven effective. PR #503 open with skills module. Next: Flight review β†’ merge governance to squad.agent.md. No blocking issues. + ### 2026-03-10: Deterministic skill pattern **Problem:** Skills were too loose. The distributed-mesh skill was tested in a real project (mesh-demo), and agents generated 76 lines of validator code, 5 test files with 43 tests, regenerated sync scripts that should have been copied from templates, and left decision files empty. The skill document let agents interpret intent instead of following explicit steps. diff --git a/.squad/decisions.md b/.squad/decisions.md index 67af34b19..9992fb61b 100644 --- a/.squad/decisions.md +++ b/.squad/decisions.md @@ -5999,3 +5999,142 @@ Brady must create Automation token at https://www.npmjs.com/settings/{username}/ - `.github/workflows/squad-release.yml` β€” GitHub Release creation - `.squad/agents/kobayashi/history.md` β€” Implementation details +--- + +## Wave 1 Decisions (#329/#344/#500) β€” 2026-03-22T09-35Z + +### Implementation Plan: Ambient Personal Squad (#329 + #344) + +**Author:** Flight (Lead) +**Date:** 2026-03-22 +**Issues:** #329, #344 + +Personal squad implementation across 4 PRs with clear phase dependencies: + +- **PR #1 β€” SDK Foundation** (EECOM): ResolvedSquadPaths.personalDir, resolvePersonalSquadDir(), PersonalAgentMeta type, resolvePersonalAgents(), mergeSessionCast(), ensureSquadPathTriple() +- **PR #2 β€” CLI Surface** (EECOM): squad personal {init,list,add,remove}, squad cast, --team-root flag, wiring + exports +- **PR #3 β€” Governance** (Procedures, concurrent): squad.agent.md updates for personal squad, Ghost Protocol in templates, personal-charter.md template +- **PR #4 β€” Tests** (Sims): E2E ambient discovery, Ghost Protocol, routing scenarios, unit tests for SDK functions + +**MVP = PR #1 + PR #3.** Phase 1 unblocks Phase 2. + +**Key Decisions:** +- Personal agents tagged with `origin: 'personal'` and Ghost Protocol applied +- Audit trail (personal agent participation) is coordinator-written in project orchestration log +- SQUAD_NO_PERSONAL env var gates ambient discovery at Phase 1, earliest point +- Dual-root path guard (ensureSquadPathDual) extended to triple-root (ensureSquadPathTriple) for personal squad dirs +- Personal squad paths resolved via platform detection (never hard-coded) + +**Blocking:** None β€” design validated against codebase. Ready for EECOM Phase 1 start. + +### Economy Mode Design β€” #500 + +**Author:** EECOM +**Date:** 2026-03-20 + +Economy mode implemented as Layer 3/4 modifier (never overrides explicit preferences Layers 0–2): + +| Normal | Economy | Use Case | +|--------|---------|----------| +| claude-opus-4.6 | claude-sonnet-4.5 | Architecture, review | +| claude-sonnet-4.6 | gpt-4.1 | Code writing | +| claude-sonnet-4.5 | gpt-4.1 | Code writing | +| claude-haiku-4.5 | gpt-4.1 | Docs, planning, mechanical | + +**Activation:** +- Persistent: `"economyMode": true` in `.squad/config.json` +- Session: `--economy` CLI flag (SQUAD_ECONOMY_MODE=1 env var) +- Toggle: `squad economy on|off` command + +**Implementation:** +- ECONOMY_MODEL_MAP + applyEconomyMode() in packages/squad-sdk/src/config/models.ts +- readEconomyMode() + writeEconomyMode() in config/models.ts +- resolveModel() accepts economyMode option +- squad economy {on|off} command +- --economy global flag in cli-entry.ts +- 34 tests β€” PR #504 open + +**Key Decision:** Economy mode respects user intent. If a user says "always use opus", economy mode defers to that choice. + +### Economy Mode Governance β€” #500 + +**Author:** Procedures (Prompt Engineer) +**Date:** 2026-03-22 + +Governance additions needed in squad.agent.md: + +1. **Economy Mode Section** β€” positioned as Layer 3 override, respects Layers 0–2 +2. **Economy Model Selection Table** β€” per-task mapping (code, docs, architecture review, etc.) +3. **Spawn Acknowledgment Convention** β€” include `πŸ’° economy` indicator when active +4. **Valid Models Catalog Audit** β€” added claude-sonnet-4.6, gpt-5.4, gpt-5.3-codex; confirmed gpt-4.1, gpt-5-mini already present + +**Status:** DRAFT β€” awaiting Flight review before merging to squad.agent.md. + +### Personal Squad Governance β€” Consult Mode Awareness (#344) + +**Author:** Procedures (Prompt Engineer) +**Date:** 2026-03-22 + +Governance additions for squad.agent.md to close consult-mode coordinator awareness gap: + +1. **Consult Mode Detection** β€” check config.json for `"consult": true` after resolving team root +2. **Personal Squad Path Reference** β€” platform-specific paths (Linux, macOS, Windows) resolved via resolveGlobalSquadPath(), never hard-coded +3. **Consult Mode Spawn Guidance** β€” pass CONSULT_MODE: true + PROJECT_NAME in spawn prompts so agents know decisions are project-isolated +4. **Consult Mode Acknowledgment Format** β€” `🧳 consult mode active β€” {Agent}` with extraction staging notes +5. **Charter Template Additions** β€” all agent charters gain "Consult Mode Awareness" note in "How I Work" + +**Proposed Skill** (post-approval): `.squad/skills/consult-mode/SKILL.md` β€” coordinator behavior for consult mode (detection, spawn guidance, extraction workflow). + +**Status:** DRAFT β€” awaiting Flight review before merging. + +### Persistent Model Preference via config.json β€” #284 + +**Author:** Procedures (Prompt Engineer) +**Date:** 2026-03-17 + +Model selection uses 5-layer hierarchy with Layer 0 (Persistent Config) stored in .squad/config.json: + +- `defaultModel` β€” global preference +- `agentModelOverrides` β€” per-agent overrides (keyed by agent name) + +Coordinator reads these on session start. Persists new preferences when user says "always use X." + +**Impact:** All agents respect Layer 0 when spawning. Model preferences travel with the repo (checked into git). + +### User Directives (Captured 2026-03-22) + +**Rate Limit Recovery UX (#464 soft dependency):** +When Squad detects a rate limit, offer actionable recovery: (1) switch to equivalent/alternative models, (2) offer economy mode (#500) as fallback. Rate limits should be a pivot point, not a dead end. *Status: Directive captured; soft dependency on #500.* + +**Bug #502 β€” Next Priority:** +node:sqlite installer dependency bug is workshop blocker (P1). Pick up immediately after Wave 1 (#329/#344/#500) finishes. *Status: Queued for Wave 2.* + +**GitHub Discussions in Triage:** +Include GitHub Discussions in triage workflow alongside issues and PRs. Scan and respond to open discussions as part of the workflow. *Status: Directive captured.* + +### Template Directory Sync Enforcement β€” #461 + +**Author:** Fenster (Core Dev) +**Date:** 2026-07-16 + +Template files have 5 duplicate locations. Canonical source is `.squad-templates/`. All copies in `templates/`, `packages/squad-cli/templates/`, `packages/squad-sdk/templates/`, `.github/agents/` must match canonical. + +**Enforcement:** `test/template-sync.test.ts` enforces byte-for-byte parity for casting-policy.json, universe count parity for squad.agent.md, cross-file count validation. + +**Impact:** All team members editing template files must update all locations + pass sync tests. + +### Dual-Layer ESM Fix for vscode-jsonrpc β€” #449 + +**Author:** GNC +**Date:** 2026-07-25 + +ESM module resolution uses dual-layer postinstall strategy: + +1. **Layer 1 (canonical):** Inject `exports` field into vscode-jsonrpc@8.2.1/package.json +2. **Layer 2 (defense-in-depth):** Patch copilot-sdk/dist/session.js to add .js extension +3. **Layer 3 (runtime):** cli-entry.ts Module._resolveFilename intercept (handles npx cache hits) + +`squad doctor` now detects both Layer 1 and Layer 2 issues. Matches vscode-jsonrpc v9.x forward-compatibility. + +**Impact:** If users report ESM errors on Node 22/24, direct them to `squad doctor`. + diff --git a/.squad/decisions/inbox/eecom-economy-mode.md b/.squad/decisions/inbox/eecom-economy-mode.md deleted file mode 100644 index d534aa5d6..000000000 --- a/.squad/decisions/inbox/eecom-economy-mode.md +++ /dev/null @@ -1,37 +0,0 @@ -# Economy Mode Design β€” #500 - -**Date:** 2026-03-20 -**Author:** EECOM -**Issue:** #500 - -## Decision - -Economy mode is implemented as a modifier that shifts model selection at Layer 3 (task-aware auto) and Layer 4 (default fallback) only. Layers 0–2 (explicit user preferences) are never downgraded. - -## Model Map - -| Normal | Economy | Use case | -|--------|---------|----------| -| `claude-opus-4.6` | `claude-sonnet-4.5` | Architecture, review | -| `claude-sonnet-4.6` | `gpt-4.1` | Code writing | -| `claude-sonnet-4.5` | `gpt-4.1` | Code writing | -| `claude-haiku-4.5` | `gpt-4.1` | Docs, planning, mechanical | - -## Activation - -1. **Persistent:** `"economyMode": true` in `.squad/config.json` (survives sessions) -2. **Session:** `--economy` CLI flag (sets `SQUAD_ECONOMY_MODE=1` env var, current session only) -3. **Toggle command:** `squad economy on|off` writes to config.json - -## Hierarchy Integration - -Economy mode is a Layer 3/4 modifier β€” it does NOT override explicit preferences (Layers 0–2). This is intentional: if a user said "always use opus", economy mode respects that choice. - -## Implementation Points - -- `ECONOMY_MODEL_MAP` + `applyEconomyMode()` in `packages/squad-sdk/src/config/models.ts` -- `readEconomyMode()` + `writeEconomyMode()` in `packages/squad-sdk/src/config/models.ts` -- `resolveModel()` in `config/models.ts` accepts `economyMode?: boolean` option; reads from config if not provided -- `resolveModel()` in `agents/model-selector.ts` also supports `economyMode?: boolean` -- `squad economy [on|off]` command in `packages/squad-cli/src/cli/commands/economy.ts` -- `--economy` global flag in `cli-entry.ts` diff --git a/.squad/decisions/inbox/fenster-template-audit.md b/.squad/decisions/inbox/fenster-template-audit.md deleted file mode 100644 index baf4e12ae..000000000 --- a/.squad/decisions/inbox/fenster-template-audit.md +++ /dev/null @@ -1,21 +0,0 @@ -# Decision: Template directory sync enforcement - -**Author:** Fenster (Core Dev) -**Date:** 2026-07-16 -**Issue:** #461 -**PR:** #462 - -## Context - -PR #460 fixed universe count in some but not all template copies, revealing that we had 5 locations with duplicated files and no automated enforcement. - -## Decision - -1. `.squad-templates/` is the **canonical source** for all template files. -2. All copies in `templates/`, `packages/squad-cli/templates/`, `packages/squad-sdk/templates/`, and `.github/agents/` must match canonical. -3. `test/template-sync.test.ts` enforces byte-for-byte parity for casting-policy.json, universe count parity for squad.agent.md, and cross-file count validation. -4. Any future template changes must update ALL locations and pass the sync tests. - -## Impact - -All team members editing template files. diff --git a/.squad/decisions/inbox/gnc-node22-esm.md b/.squad/decisions/inbox/gnc-node22-esm.md deleted file mode 100644 index 6fa657a85..000000000 --- a/.squad/decisions/inbox/gnc-node22-esm.md +++ /dev/null @@ -1,25 +0,0 @@ -# Decision: Dual-Layer ESM Fix for vscode-jsonrpc - -**By:** GNC -**Issue:** #449 -**Date:** 2025-07-25 - -## What - -ESM module resolution for `vscode-jsonrpc` subpath imports now uses a dual-layer postinstall strategy: - -1. **Layer 1 (canonical):** Inject `exports` field into `vscode-jsonrpc@8.2.1/package.json` β€” fixes ALL subpath imports at once. -2. **Layer 2 (defense-in-depth):** Patch `copilot-sdk/dist/session.js` to add `.js` extension β€” kept as fallback. -3. **Layer 3 (runtime):** `cli-entry.ts` `Module._resolveFilename` intercept β€” handles npx cache hits where postinstall never runs. - -`squad doctor` now detects both Layer 1 and Layer 2 issues. - -## Why - -- vscode-jsonrpc@8.2.1 has no `exports` field; Node 22+ strict ESM rejects extensionless subpath imports. -- Patching individual files (session.js) doesn't scale β€” any future copilot-sdk file using `vscode-jsonrpc/node` would break. -- The exports field injection matches what vscode-jsonrpc v9.x ships, making it forward-compatible. - -## Impact - -All agents should know: if a user reports ESM import errors on Node 22/24, direct them to `squad doctor` which now detects and reports the exact issue. diff --git a/.squad/decisions/inbox/procedures-economy-mode-governance.md b/.squad/decisions/inbox/procedures-economy-mode-governance.md deleted file mode 100644 index 4561be940..000000000 --- a/.squad/decisions/inbox/procedures-economy-mode-governance.md +++ /dev/null @@ -1,126 +0,0 @@ -# Proposal: Economy Mode Integration in squad.agent.md - -**By:** Procedures (Prompt Engineer) -**Date:** 2026-03-22 -**Issues:** #500 -**Status:** DRAFT β€” for Flight review before merging to squad.agent.md - ---- - -## Summary - -Economy mode is a new session/persistent modifier that shifts Layer 3 (Task-Aware Auto-Selection) to cost-optimized alternatives. This proposal documents the governance additions needed in `squad.agent.md`. - -**Note to Flight:** Procedures owns the skill design. Squad.agent.md is governance β€” Flight reviews before commit. - ---- - -## 1. New Paragraph After Layer 0 (Per-Agent Model Selection section) - -Insert after the existing Layer 0 bullet points and before "**Layer 1 β€” Session Directive**": - ---- - -**Economy Mode β€” Cost Modifier (Layer 3 override):** Economy mode shifts all Layer 3 auto-selection to cost-optimized alternatives. It does NOT override Layer 0 (persistent config), Layer 1 (explicit session directive), or Layer 2 (charter preference) β€” user intent always wins. - -- **Activation (session):** User says "use economy mode", "save costs", "go cheap" β†’ activate for this session only. -- **Activation (persistent):** User says "always use economy mode" OR `"economyMode": true` in `.squad/config.json` β†’ persists across sessions. -- **Deactivation:** "turn off economy mode" or remove `economyMode` from `config.json`. -- **On session start:** Read `.squad/config.json`. If `economyMode: true`, activate economy mode before any spawns. - ---- - -## 2. Economy Model Selection Table - -Add after Layer 3 normal table: - ---- - -**Economy Mode Layer 3 Table** (active when economy mode is on): - -| Task Output | Normal Mode | Economy Mode | -|-------------|-------------|--------------| -| Writing code (implementation, refactoring, bug fixes) | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | -| Writing prompts or agent designs | `claude-sonnet-4.5` | `gpt-4.1` or `gpt-5-mini` | -| Docs, planning, triage, changelogs, mechanical ops | `claude-haiku-4.5` | `gpt-4.1` or `gpt-5-mini` | -| Architecture, code review, security audits | `claude-opus-4.5` | `claude-sonnet-4.5` | -| Scribe / logger / mechanical file ops | `claude-haiku-4.5` | `gpt-4.1` | - -Prefer `gpt-4.1` over `gpt-5-mini` for structured output or tool use. Prefer `gpt-5-mini` for pure text generation. - ---- - -## 3. Spawn Acknowledgment Convention - -Add to the spawn acknowledgment format guidance: - ---- - -When economy mode is active, include `πŸ’° economy` after the model name in spawn acknowledgments: - -``` -πŸ”§ Fenster (gpt-4.1 Β· πŸ’° economy) β€” fixing auth bug -πŸ“‹ Scribe (gpt-4.1 Β· πŸ’° economy) β€” logging decision -``` - -This gives the user instant visibility that cost-optimized models are in use. - ---- - -## 4. Valid Models Catalog Audit - -Current "Valid models" section lists: - -``` -Premium: claude-opus-4.6, claude-opus-4.6-fast, claude-opus-4.5 -Standard: claude-sonnet-4.5, claude-sonnet-4, gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1, gpt-5, gemini-3-pro-preview -Fast/Cheap: claude-haiku-4.5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 -``` - -**Audit findings:** -- `claude-opus-4.6` and `claude-opus-4.6-fast` are listed but not used in the Layer 3 table (table uses `claude-opus-4.5`). The Layer 3 table should reference `claude-opus-4.6` as the premium default for consistency with the catalog. -- `claude-sonnet-4.6` appears in the model-selection SKILL.md but is absent from the valid models list in squad.agent.md. Add it under Standard. -- Economy mode introduces `gpt-4.1` and `gpt-5-mini` as primary alternatives β€” both are already in the Fast/Cheap catalog. No additions needed. - -**Proposed updated catalog:** - -``` -Premium: claude-opus-4.6, claude-opus-4.6-fast, claude-opus-4.5 -Standard: claude-sonnet-4.6, claude-sonnet-4.5, claude-sonnet-4, gpt-5.4, gpt-5.3-codex, gpt-5.2-codex, gpt-5.2, gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1, gpt-5, gemini-3-pro-preview -Fast/Cheap: claude-haiku-4.5, gpt-5.1-codex-mini, gpt-5-mini, gpt-4.1 -``` - -(Added `claude-sonnet-4.6`, `gpt-5.4`, `gpt-5.3-codex` which appear in the model-selection SKILL.md fallback chains but are missing from squad.agent.md's catalog.) - ---- - -## 5. Config Schema Addition - -Add `economyMode` to the config schema reference in squad.agent.md (wherever `defaultModel` is documented): - -```json -{ - "version": 1, - "defaultModel": "claude-sonnet-4.6", - "economyMode": true, - "agentModelOverrides": { - "fenster": "claude-sonnet-4.6" - } -} -``` - ---- - -## Rationale - -Economy mode solves a real user need: "I want all agents to run cheaper, but I don't want to set each one individually." It's a session-level modifier that works orthogonally to the existing hierarchy β€” no layer gets changed, only Layer 3's lookup table swaps. The `πŸ’°` indicator keeps it transparent. - -The skill (`economy-mode/SKILL.md`) covers the coordinator behavior in detail. This proposal is the governance side β€” ensuring squad.agent.md is the authoritative source for the feature. - ---- - -## References - -- Skill: `.squad/skills/economy-mode/SKILL.md` -- Issue: #500 -- Model selection skill: `.squad/skills/model-selection/SKILL.md` diff --git a/.squad/decisions/inbox/procedures-personal-squad-governance.md b/.squad/decisions/inbox/procedures-personal-squad-governance.md deleted file mode 100644 index b46bb17de..000000000 --- a/.squad/decisions/inbox/procedures-personal-squad-governance.md +++ /dev/null @@ -1,158 +0,0 @@ -# Proposal: Personal Squad Governance Awareness in squad.agent.md - -**By:** Procedures (Prompt Engineer) -**Date:** 2026-03-22 -**Issues:** #344 -**Status:** DRAFT β€” for Flight review before merging to squad.agent.md - ---- - -## Summary - -Squad has a consult mode (implemented, per `prd-consult-mode.md`) and personal squad semantics (via `resolveGlobalSquadPath()`), but `squad.agent.md` doesn't tell the coordinator how to reason about either. This proposal documents the gaps and the governance additions needed. - ---- - -## Gap Analysis - -### Gap 1: Init Mode references `--global` without explaining personal squad resolution - -Current Init Mode says "run `squad init --global` for a personal squad" (implied by CLI docs) but squad.agent.md doesn't explain what a personal squad IS or how the coordinator should detect it. - -**What agents need to know:** -- Personal squad = a squad at the global path (resolved via `resolveGlobalSquadPath()`) - - Linux/macOS: `~/.config/squad/.squad` - - macOS (alt): `~/Library/Application Support/squad/.squad` - - Windows: `%APPDATA%\squad\.squad` -- If `.squad/config.json` contains `"consult": true`, the coordinator is working inside a consult session -- `sourceSquad` in `config.json` points to the original personal squad (for Scribe extraction context) - -### Gap 2: No coordinator guidance for consult mode - -`squad.agent.md` mentions nothing about consult mode. The coordinator doesn't know: -- How to recognize it's in a consult session -- That writes go to the project `.squad/` (isolated copy) β€” NOT the personal squad -- That Scribe's charter is patched with extraction instructions -- That `.squad/extract/` is a staging area for generic learnings - -### Gap 3: TEAM_ROOT works, but personal squad semantics are absent - -The coordinator resolves `TEAM_ROOT` correctly (Worktree Awareness section), but: -- No distinction between "project squad" vs "personal squad copy in consult mode" -- No guidance on what to tell agents about their squad context when in consult mode - -### Gap 4: Charter templates have no personal-squad-aware patterns - -Agent charters have no concept of: -- Consult mode restrictions (agents shouldn't commit to project, shouldn't pollute personal squad) -- Extraction tagging (Scribe needs to flag decisions as generic vs project-specific) - -### Gap 5: No skill for consult-mode behavior - -There is no skill for consult-mode coordinator behavior, even though consult mode has distinct patterns (invisibility, extraction, isolation). - ---- - -## Proposed squad.agent.md Additions - -### Addition 1: Consult Mode Detection (in Team Mode β†’ On Session Start) - -After "resolve the team root" and before Issue Awareness, add: - ---- - -**Consult Mode Detection:** After resolving team root, check `.squad/config.json` for `"consult": true`. - -- If `consult: true` β†’ **Consult mode is active.** This is a personal squad consulting on a project. - - The `.squad/` directory is an isolated copy of the user's personal squad. - - `sourceSquad` in `config.json` contains the path to the original personal squad. - - Do NOT read or write to `sourceSquad` β€” it's out of scope. Only operate within TEAM_ROOT. - - Scribe's charter is already patched with extraction instructions β€” no coordinator action needed. - - Include `🧳 consult` in your session acknowledgment: `Squad v{version} (🧳 consult β€” {projectName})` - - Remind agents: decisions they make here are project-isolated until explicitly extracted. -- If `consult: false` or absent β†’ Normal mode. Team root is authoritative. - ---- - -### Addition 2: Personal Squad Path Reference - -Add a new subsection under "Worktree Awareness": - ---- - -**Personal Squad Paths:** The global squad path is resolved by `resolveGlobalSquadPath()`: - -| Platform | Path | -|----------|------| -| Linux | `~/.config/squad/.squad` | -| macOS | `~/Library/Application Support/squad/.squad` | -| Windows | `%APPDATA%\squad\.squad` | - -The coordinator should NEVER hard-code these paths. Use `squad --global` or `resolveGlobalSquadPath()` to resolve. Only relevant in consult mode (to understand the `sourceSquad` field) β€” the coordinator does NOT read the personal squad directly during a session. - ---- - -### Addition 3: Consult Mode Spawn Guidance - -Add to the spawn template section: - ---- - -**In consult mode:** Pass `CONSULT_MODE: true` and `PROJECT_NAME: {projectName}` in spawn prompts alongside `TEAM_ROOT`. This lets agents know: -1. Their decisions will be reviewed for extraction β€” keep project-specific and generic reasoning separate -2. They should NOT reference personal squad paths or personal squad agent names -3. Scribe will classify their decisions β€” agents should write clear, extractable decision rationale - ---- - -### Addition 4: Consult Mode Acknowledgment Format - -Add to spawn acknowledgment conventions: - -``` -🧳 consult mode active β€” Fenster (claude-sonnet-4.5) β€” refactoring auth module - ↳ decisions staged in .squad/extract/ for review before extraction -``` - ---- - -## Proposed New Skill - -**Skill needed:** `.squad/skills/consult-mode/SKILL.md` - -Should cover: -- Detecting consult mode from config.json -- Coordinator behavior changes (CONSULT_MODE in spawn prompts) -- Scribe extraction workflow (already documented in prd-consult-mode.md β€” condense into skill) -- Acknowledgment format conventions -- STOP: extraction is always user-driven via `squad extract` β€” coordinator never auto-extracts - -This skill should be authored after this governance proposal is approved, to avoid the skill getting ahead of the governance. - ---- - -## Charter Template Additions - -All agent charter templates should include a note in "How I Work": - -```markdown -**Consult Mode Awareness:** If `CONSULT_MODE: true` is in my spawn prompt, I'm working on a project outside my home squad. My decisions here are project-isolated. Write extractable rationale so Scribe can classify them for `squad extract` review. -``` - -This should be added to `.squad/templates/charter.md` (if it exists) and `.squad/agents/scribe/charter.md` (Scribe already has extraction logic, but clarifying the classification responsibility is valuable). - ---- - -## Rationale - -Consult mode is fully implemented at the SDK level (`prd-consult-mode.md`, `squad consult` command) but the coordinator has no awareness of it. The result: agents running in a consult session have no context that they're in a temporary, isolated copy of a personal squad. They might make decisions as if they're permanent, or reference the project in ways that pollute the personal squad on extraction. - -These governance additions close the loop between the implementation (CLI + SDK) and the runtime behavior (coordinator + agents). - ---- - -## References - -- Consult mode PRD: `.squad/identity/prd-consult-mode.md` -- Issue: #344 -- Flight ambient personal squad note: `.squad/decisions/inbox/flight-ambient-personal-squad.md` From d8b0ea143c7e965187e253688feac3bc337eee6a Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 22 Mar 2026 03:08:05 -0700 Subject: [PATCH 4/4] fix: hard-fail on Node <22.5.0 with clear message (#502) node:sqlite (required by the Copilot SDK for session storage) was added in Node 22.5.0. Previously Squad only warned and continued, leading to cryptic ERR_UNKNOWN_BUILTIN_MODULE crashes later. Changes: - cli-entry.ts: replace soft warn+continue with hard process.exit(1) and a clear upgrade message when Node <22.5.0 is detected at startup. Remove now-redundant checkNodeSqlite() function and its call site. - doctor.ts: add checkNodeVersion() check to squad doctor diagnostics so users can detect the version gap proactively. - package.json (root, squad-cli, squad-sdk): correct engines field from >=20 to >=22.5.0, enabling npm/npx to warn at install time. - test/cli/doctor.test.ts: add 5 tests for checkNodeVersion pass/fail paths; update check-count assertion. Closes #502 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package.json | 4 +- packages/squad-cli/package.json | 4 +- packages/squad-cli/src/cli-entry.ts | 56 ++++++------------- packages/squad-cli/src/cli/commands/doctor.ts | 26 ++++++++- packages/squad-sdk/package.json | 2 +- test/cli/doctor.test.ts | 38 ++++++++++++- 6 files changed, 81 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 4460c2306..7b2b82be9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad", - "version": "0.8.25-build.7", + "version": "0.8.25-build.10", "private": true, "description": "Squad β€” Programmable multi-agent runtime for GitHub Copilot, built on @github/copilot-sdk", "type": "module", @@ -23,7 +23,7 @@ "docs:preview": "cd docs && npm run preview" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.5.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index cd5073d58..a67125734 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-cli", - "version": "0.8.25-build.6", + "version": "0.8.25-build.10", "description": "Squad CLI β€” Command-line interface for the Squad multi-agent runtime", "type": "module", "bin": { @@ -158,7 +158,7 @@ "postbuild": "node -e \"require('fs').cpSync('src/remote-ui', 'dist/remote-ui', {recursive: true})\"" }, "engines": { - "node": ">=20" + "node": ">=22.5.0" }, "dependencies": { "@bradygaster/squad-sdk": "*", diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index 6a121f853..a123b745a 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -53,25 +53,22 @@ Module._resolveFilename = function (request: string, parent: unknown, isMain: bo return _origResolveFilename.call(this, request, parent, isMain, options); }; -// Pre-flight: detect missing node:sqlite before the Copilot SDK tries to use it. -// The @github/copilot SDK lazily imports node:sqlite for session storage. -// Node.js <22.5.0 and some 22.x builds don't include the builtin. (#214) -try { - await import('node:sqlite'); -} catch { - // Module not available β€” install a shim so the SDK's lazy require succeeds. - // We register a minimal stub that throws a descriptive error only if - // DatabaseSync is actually instantiated (it may never be on short sessions). - const { register } = await import('node:module'); - // No shim to register β€” just surface a clear message at startup so users - // know what's happening instead of seeing an opaque stack trace. - const nodeVersion = process.versions.node; - console.warn( - `⚠ node:sqlite is not available in Node.js v${nodeVersion}.\n` + - ' The Copilot SDK uses node:sqlite for session storage.\n' + - ' Upgrade to Node.js β‰₯22.5.0 or launch with --experimental-sqlite.\n' + - ' Squad will attempt to continue, but session persistence may fail.\n', - ); +// Pre-flight: require Node.js β‰₯22.5.0 for node:sqlite (#214, #502). +// node:sqlite is used by the Copilot SDK for session storage. +// Fail fast with a clear message rather than letting users hit a cryptic +// ERR_UNKNOWN_BUILTIN_MODULE crash when the SDK loads. +{ + const parts = process.versions.node.split('.').map(Number); + const major = parts[0] ?? 0; + const minor = parts[1] ?? 0; + if (major < 22 || (major === 22 && minor < 5)) { + console.error( + `βœ— Squad requires Node.js β‰₯22.5.0 (you have v${process.versions.node}).\n` + + ` node:sqlite (required by the Copilot SDK for session storage) was added in Node 22.5.0.\n` + + ` Upgrade at: https://nodejs.org/en/download\n`, + ); + process.exit(1); + } } // --------------------------------------------------------------------------- @@ -109,26 +106,6 @@ const lazyRunShell = () => import('./cli/shell/index.js'); // Use local version resolver instead of importing VERSION from squad-sdk const VERSION = getPackageVersion(); -/** - * Pre-flight: warn if node:sqlite is unavailable (#214). - * The @github/copilot SDK lazily imports node:sqlite for session storage. - * Node.js <22.5.0 and some 22.x builds lack this builtin, causing an - * opaque ERR_UNKNOWN_BUILTIN_MODULE crash. Surface a clear message instead. - */ -async function checkNodeSqlite(): Promise { - try { - await import('node:sqlite'); - } catch { - const nodeVersion = process.versions.node; - console.warn( - `⚠ node:sqlite is not available in Node.js v${nodeVersion}.\n` + - ' The Copilot SDK uses node:sqlite for session storage.\n' + - ' Upgrade to Node.js β‰₯22.5.0 or launch with --experimental-sqlite.\n' + - ' Squad will attempt to continue, but session persistence may fail.\n', - ); - } -} - async function main(): Promise { const args = process.argv.slice(2); const hasGlobal = args.includes('--global'); @@ -248,7 +225,6 @@ async function main(): Promise { // No args β†’ launch interactive shell; whitespace-only arg β†’ show help if (rawCmd === undefined) { - await checkNodeSqlite(); // Fire-and-forget update check β€” non-blocking, never delays shell startup import('./cli/self-update.js').then(m => m.notifyIfUpdateAvailable(VERSION)).catch(() => {}); const { runShell } = await lazyRunShell(); diff --git a/packages/squad-cli/src/cli/commands/doctor.ts b/packages/squad-cli/src/cli/commands/doctor.ts index 298bac099..1e19023aa 100644 --- a/packages/squad-cli/src/cli/commands/doctor.ts +++ b/packages/squad-cli/src/cli/commands/doctor.ts @@ -215,6 +215,27 @@ function checkDecisionsMd(squadDir: string): DoctorCheck { // ── ESM compatibility checks ──────────────────────────────────────── +// ── environment checks ───────────────────────────────────────────── + +/** + * Check that Node.js is β‰₯22.5.0 for node:sqlite availability. + * Accepts an optional version string for testing. + */ +export function checkNodeVersion(nodeVersion?: string): DoctorCheck { + const version = nodeVersion ?? process.versions.node; + const parts = version.split('.').map(Number); + const major = parts[0] ?? 0; + const minor = parts[1] ?? 0; + const ok = major > 22 || (major === 22 && minor >= 5); + return { + name: 'Node.js β‰₯22.5.0 (node:sqlite)', + status: ok ? 'pass' : 'fail', + message: ok + ? `v${version} β€” node:sqlite available` + : `v${version} β€” node:sqlite requires β‰₯22.5.0. Upgrade at https://nodejs.org/en/download`, + }; +} + /** * Check that vscode-jsonrpc has the `exports` field needed for Node 22/24+ * strict ESM subpath resolution. Without it, `import('vscode-jsonrpc/node')` @@ -344,7 +365,10 @@ export async function runDoctor(cwd?: string): Promise { checks.push(checkDecisionsMd(squadDir)); } - // 10–11 ESM compatibility (Node 22/24+) + // 10. Node.js version (node:sqlite availability) + checks.push(checkNodeVersion()); + + // 11-12. ESM compatibility (Node 22/24+) checks.push(checkVscodeJsonrpcExports(resolvedCwd)); checks.push(checkCopilotSdkSessionPatch(resolvedCwd)); diff --git a/packages/squad-sdk/package.json b/packages/squad-sdk/package.json index 6a3804c62..5fc8902e3 100644 --- a/packages/squad-sdk/package.json +++ b/packages/squad-sdk/package.json @@ -181,7 +181,7 @@ "build": "tsc -p tsconfig.json" }, "engines": { - "node": ">=20" + "node": ">=22.5.0" }, "dependencies": { "@github/copilot-sdk": "^0.1.32", diff --git a/test/cli/doctor.test.ts b/test/cli/doctor.test.ts index f7869dd0f..0935f3671 100644 --- a/test/cli/doctor.test.ts +++ b/test/cli/doctor.test.ts @@ -11,7 +11,7 @@ import { mkdir, rm, writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { randomBytes } from 'crypto'; -import { runDoctor, getDoctorMode } from '@bradygaster/squad-cli/commands/doctor'; +import { runDoctor, getDoctorMode, checkNodeVersion } from '@bradygaster/squad-cli/commands/doctor'; import type { DoctorCheck } from '@bradygaster/squad-cli/commands/doctor'; const TEST_ROOT = join(process.cwd(), `.test-doctor-${randomBytes(4).toString('hex')}`); @@ -65,8 +65,8 @@ describe('squad doctor', () => { const squadDirCheck = checks.find((c: DoctorCheck) => c.name === '.squad/ directory exists'); expect(squadDirCheck?.status).toBe('fail'); - // When .squad/ is missing the file checks are skipped β€” only .squad/ + 2 ESM checks - expect(checks.length).toBe(3); + // When .squad/ is missing the file checks are skipped β€” only .squad/ + Node version + 2 ESM checks + expect(checks.length).toBe(4); }); it('detects remote mode from config.json with teamRoot', async () => { @@ -98,6 +98,38 @@ describe('squad doctor', () => { expect(mode).toBe('local'); }); + it('reports node:sqlite check as pass on current Node version', async () => { + const checks = await runDoctor(TEST_ROOT); + const nodeCheck = checks.find((c: DoctorCheck) => c.name.includes('node:sqlite')); + expect(nodeCheck).toBeDefined(); + // Tests run on Node >= 22.5.0 β€” should always pass in CI + expect(nodeCheck?.status).toBe('pass'); + }); + + it('checkNodeVersion returns fail for Node <22.5.0', () => { + const result = checkNodeVersion('20.18.0'); + expect(result.status).toBe('fail'); + expect(result.message).toContain('22.5.0'); + expect(result.message).toContain('nodejs.org'); + }); + + it('checkNodeVersion returns fail for Node 22.4.x', () => { + const result = checkNodeVersion('22.4.0'); + expect(result.status).toBe('fail'); + expect(result.message).toContain('22.5.0'); + }); + + it('checkNodeVersion returns pass for Node 22.5.0', () => { + const result = checkNodeVersion('22.5.0'); + expect(result.status).toBe('pass'); + expect(result.message).toContain('22.5.0'); + }); + + it('checkNodeVersion returns pass for Node 24.x', () => { + const result = checkNodeVersion('24.0.0'); + expect(result.status).toBe('pass'); + }); + it('warns on absolute teamRoot', async () => { await scaffold(TEST_ROOT); const abs = process.platform === 'win32' ? 'C:\\some\\absolute\\path' : '/some/absolute/path';