diff --git a/docs/features/consult-mode.md b/docs/features/consult-mode.md new file mode 100644 index 000000000..caaf2edf5 --- /dev/null +++ b/docs/features/consult-mode.md @@ -0,0 +1,263 @@ +# Consult Mode + +> ⚠️ **Experimental** — Squad is alpha software. APIs, commands, and behavior may change between releases. + + +Consult mode lets you bring your personal squad to projects you don't own — OSS contributions, client work, temporary collaborations — without leaving any trace. Your team consults, does the work, learns things, and returns home with only the generic learnings you approve. + +--- + +## The Problem + +You have a personal squad at your global path (e.g., `~/.config/squad/.squad` on Linux) with agents, skills, and decisions refined over time. When you contribute to someone else's project, you face a dilemma: + +- **Pollute the project?** Running `squad init` creates a `.squad/` folder they didn't ask for +- **Pollute your squad?** Project-specific knowledge bleeds into your global squad +- **Work without your team?** Lose the productivity benefits you've built up + +--- + +## The Solution + +Your team **consults** on a project. They bring their expertise, do the work, and learn things. When done, they extract what's reusable and return home. The project never knows Squad was there. + +| Aspect | Normal Mode | Consult Mode | +|--------|-------------|--------------| +| Squad location | `.squad/` in project | **Copy** of personal squad into project `.squad/` | +| Git visibility | Committed or `.gitignore` | Invisible via `.git/info/exclude` | +| Writes go to | Project `.squad/` | Project `.squad/` (isolated copy) | +| After session | Stays in project | Extract generic learnings → personal squad, discard rest | + +--- + +## Quick Start + +### OSS Contribution + +```bash +cd ~/projects/kubernetes-dashboard +squad consult # Enter consult mode +# ... do your work with your squad ... +squad extract # Review and extract generic learnings +squad extract --clean --yes # Clean up after extraction +``` + +### Client Work + +```bash +cd ~/client-projects/acme-corp +squad consult # Enter consult mode +# ... work on the project ... +squad extract --dry-run # Preview what would be extracted +squad extract --clean # Extract and clean up (prompts for confirmation) +``` + +### Check Status + +```bash +squad consult --status # See if consult mode is active +squad consult --check # Dry-run: show what would happen +``` + +--- + +## Command Reference + +### `squad consult` + +Enter consult mode with your personal squad. + +```bash +squad consult # Enter consult mode +squad consult --status # Check current consult mode status +squad consult --check # Dry-run: show what would happen without creating files +``` + +**What happens:** + +1. Copies your personal squad into the project's `.squad/` directory +2. Adds `.squad/` and `.github/agents/squad.agent.md` to `.git/info/exclude` +3. Patches the Scribe charter with extraction instructions +4. Creates a staging area at `.squad/extract/` for generic learnings + +**Created structure:** + +``` +.squad/ # Full copy of personal squad +├── config.json # { "consult": true, "sourceSquad": "...", ... } +├── agents/ # Copied from personal squad +├── skills/ # Copied from personal squad +├── decisions.md # Copied from personal squad +├── scribe-charter.md # Patched with consult mode extraction instructions +├── sessions/ # Local session history +└── extract/ # Staging area for generic learnings + +.github/agents/ +└── squad.agent.md # Points to local .squad/ (also excluded from git) +``` + +**Requirements:** + +- You must have a personal squad configured +- The project must not already have a committed `.squad/` folder + +--- + +### `squad extract` + +Extract generic learnings from a consult session back to your personal squad. + +```bash +squad extract # Review and extract generic learnings +squad extract --dry-run # Preview what would be extracted (no changes) +squad extract --clean # Also delete project .squad/ after (prompts for confirmation) +squad extract --clean --yes # Delete without confirmation +squad extract --accept-risks # Allow extraction despite license risks +``` + +**What happens:** + +1. Reads the project's LICENSE file +2. Loads staged learnings from `.squad/extract/` +3. Presents an interactive selection UI +4. Merges selected items to your personal squad +5. Logs the consultation to `/consultations/{project}.md` +6. Optionally cleans up the project `.squad/` directory + +**Example output:** + +``` +📤 Learnings staged for extraction: + +⚠️ License: MIT (safe to extract) + +Found 3 learning(s) in .squad/extract/: + [1] use-async-await.md + [2] validate-inputs.md + [3] prefer-composition.md + +Select learnings to extract (space to toggle, enter to confirm): +❯ ◉ use-async-await.md + ◉ validate-inputs.md + ◉ prefer-composition.md + +Extract 3 learning(s)? [Y/n] +``` + +--- + +## Learning Classification + +During your consult session, the **Scribe** automatically classifies decisions as they're made: + +### Generic (applies to any project) + +Copied to `.squad/extract/` for later extraction: + +- "Always use async/await instead of callbacks" +- "Validate inputs at API boundaries" +- "Prefer composition over inheritance" +- Best practices, coding standards, patterns that work anywhere + +### Project-specific (only applies here) + +Kept in local `decisions.md` only — not extracted: + +- References to specific file paths in the project +- Project-specific config, APIs, or schemas +- Decisions that mention "this project" or "this codebase" + +**You always have final say.** The Scribe proposes by writing to `extract/`, you approve or reject via `squad extract`. No extraction happens without your explicit confirmation. + +--- + +## License Handling + +### Permissive Licenses (Safe) + +MIT, Apache, BSD, ISC — proceed normally: + +``` +⚠️ License: MIT (safe to extract) +``` + +### Copyleft Licenses (Blocked) + +GPL, AGPL, LGPL — extraction is blocked by default: + +``` +🚫 License: GPL-3.0 (copyleft) + Extraction blocked. Patterns from copyleft projects may carry + license obligations that affect your future work. + + See: https://squad.dev/docs/license-risk + + To proceed anyway: squad extract --accept-risks +``` + +To override: + +```bash +squad extract --accept-risks +``` + +--- + +## Technical Notes + +### Git Invisibility + +Consult mode uses `.git/info/exclude` to hide Squad files: + +- Same syntax as `.gitignore` +- Lives inside `.git/`, so it's never committed +- Project owners never see it +- `git status` shows nothing Squad-related + +### Why Copy Instead of Reference? + +Your personal squad is **copied** into the project rather than referenced: + +- Changes during the session don't pollute your personal squad +- Session-specific decisions stay isolated until explicitly extracted +- Works offline (no dependency on external path) +- Clean separation between "consulting" and "bringing home" + +### Consultation Log + +All consultations are tracked in your personal squad at `consultations/{project}.md`: + +```markdown +# kubernetes-dashboard + +**First consulted:** 2026-02-27 +**Last session:** 2026-03-15 +**License:** Apache-2.0 + +## Sessions + +### 2026-02-27 +- use-async-await.md: "### Always use async/await..." +- validate-inputs.md: "### Validate inputs at API..." + +### 2026-03-15 +- prefer-composition.md: "### Prefer composition over..." +``` + +--- + +## Tips + +- Run `squad consult --check` before entering consult mode to preview what will happen +- Use `squad extract --dry-run` to review staged learnings without committing +- The `--clean` flag is convenient for OSS drive-by contributions where you won't return +- Consult mode errors out if the project already has a committed `.squad/` — use normal mode instead +- Your personal squad is never modified during the session — only via explicit `squad extract` + +--- + +## Next Steps + +- **Set up a personal squad:** See [Your Personal Squad](../guide/personal-squad.md) for initial setup with `squad init --global` +- **Learn about sharing:** See [Export & Import](./export-import.md) for portable team snapshots +- **Upstream inheritance:** See [Upstream Inheritance](./upstream-inheritance.md) for knowledge sharing across teams diff --git a/docs/guide/personal-squad.md b/docs/guide/personal-squad.md index 8d8ee1b0a..92d7adec3 100644 --- a/docs/guide/personal-squad.md +++ b/docs/guide/personal-squad.md @@ -205,12 +205,14 @@ The more projects you connect, the richer these reviews become. Your Lead builds You join an open-source project. New repo, unfamiliar code. But your agents already know *you*. +For projects you don't own — OSS contributions, client work, temporary collaborations — use **consult mode**. Your team consults invisibly, and the project never knows Squad was there: + ```bash cd ~/projects/new-oss-contribution -squad init +squad consult ``` -Your personal squad connects. The agents don't know the codebase yet — they'll learn it. But they already know your preferences: +Your personal squad is copied into the project's `.squad/` directory, hidden via `.git/info/exclude`. The agents don't know the codebase yet — they'll learn it. But they already know your preferences: - How you like code structured - What testing patterns you follow @@ -223,8 +225,16 @@ Your personal squad connects. The agents don't know the codebase yet — they'll Your agents explore the repo with your familiar voice. They ask the questions you'd ask. The codebase is new — but your relationship with your team isn't. +When you're done, extract the generic learnings back to your personal squad and clean up: + +```bash +squad extract --clean +``` + It's not a cold start. It's your team meeting a new project. +> 📖 **Full guide:** [Consult Mode](../features/consult-mode.md) — invisible consulting, learning extraction, license handling. + --- ## 7. Use Case: Automating Your Personal Workflow @@ -279,11 +289,11 @@ What works well today: - Shared team identity across projects - Skills that accumulate and carry over - Consistent agent behavior everywhere you work +- **Consult mode** — bring your team to projects you don't own, invisibly ([docs](../features/consult-mode.md)) What's still rough: - No sync mechanism between machines yet — `~/.squad/` is local to your machine - Project keys aren't used for anything yet (that `null` in config.json) -- The resolution system is simple — no conflict handling if team identity diverges - No UI for browsing your global skills or agent histories (it's files for now) We're building in the open. If something feels off, [open an issue](https://github.com/bradygaster/squad/issues). If something feels right, we want to hear about that too. diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index e0989212e..fadc3ef39 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -43,6 +43,7 @@ async function main(): Promise { console.log(` Overwrites: squad.agent.md, templates dir (.squad/templates/)`); console.log(` Never touches: .squad/ or .ai-team/ (your team state)`); console.log(` Flags: --global (upgrade personal squad), --migrate-directory (rename .ai-team/ → .squad/)`); + console.log(` ${BOLD}upstream${RESET} Manage upstream squad inheritance`); console.log(` ${BOLD}status${RESET} Show which squad is active and why`); console.log(` ${BOLD}triage${RESET} Scan for work and categorize issues`); console.log(` Usage: triage [--interval ]`); @@ -71,6 +72,10 @@ async function main(): Promise { console.log(` Usage: nap [--deep] [--dry-run]`); console.log(` Flags: --deep (thorough cleanup), --dry-run (preview only)`); console.log(` ${BOLD}doctor${RESET} Validate squad setup (check files, config, health)`); + console.log(` ${BOLD}consult${RESET} Enter consult mode with your personal squad`); + console.log(` Flags: --status, --check`); + console.log(` ${BOLD}extract${RESET} Extract learnings from consult mode session`); + console.log(` Flags: --dry-run, --clean, --yes, --accept-risks`); console.log(` ${BOLD}help${RESET} Show this help message`); console.log(`\nFlags:`); console.log(` ${BOLD}--version, -v${RESET} Print version`); @@ -265,6 +270,24 @@ async function main(): Promise { return; } + if (cmd === 'consult') { + const { runConsult } = await import('./cli/commands/consult.js'); + await runConsult(process.cwd(), args.slice(1)); + return; + } + + if (cmd === 'extract') { + const { runExtract } = await import('./cli/commands/extract.js'); + await runExtract(process.cwd(), args.slice(1)); + return; + } + + if (cmd === 'upstream') { + const { upstreamCommand } = await import('./cli/commands/upstream.js'); + await upstreamCommand(args.slice(1)); + return; + } + // Unknown command fatal(`Unknown command: ${cmd}\n Run 'squad help' for usage information.`); } @@ -277,3 +300,5 @@ main().catch(err => { } process.exit(1); }); + + diff --git a/packages/squad-cli/src/cli/shell/components/App.tsx b/packages/squad-cli/src/cli/shell/components/App.tsx index 2ad5f9b17..07785becc 100644 --- a/packages/squad-cli/src/cli/shell/components/App.tsx +++ b/packages/squad-cli/src/cli/shell/components/App.tsx @@ -407,8 +407,9 @@ export const App: React.FC = ({ registry, renderer, teamRoot, version, }} - {/* Live region: bounded height so InputPrompt stays in viewport */} - + {/* Live region: bounded height only while processing so InputPrompt stays in viewport; + auto-sized when idle to avoid blank space below the agent panel. */} + diff --git a/packages/squad-cli/src/cli/shell/index.ts b/packages/squad-cli/src/cli/shell/index.ts index 101b37d4b..a6b8c2bf6 100644 --- a/packages/squad-cli/src/cli/shell/index.ts +++ b/packages/squad-cli/src/cli/shell/index.ts @@ -21,7 +21,7 @@ import { SquadClient } from '@bradygaster/squad-sdk/client'; import type { SquadSession } from '@bradygaster/squad-sdk/client'; import type { SquadPermissionHandler } from '@bradygaster/squad-sdk/client'; import type { ShellMessage } from './types.js'; -import { initSquadTelemetry, TIMEOUTS, StreamingPipeline, recordAgentSpawn, recordAgentDuration, recordAgentError, recordAgentDestroy, RuntimeEventBus } from '@bradygaster/squad-sdk'; +import { initSquadTelemetry, TIMEOUTS, StreamingPipeline, recordAgentSpawn, recordAgentDuration, recordAgentError, recordAgentDestroy, RuntimeEventBus, resolveSquad, resolveGlobalSquadPath } from '@bradygaster/squad-sdk'; import type { UsageEvent } from '@bradygaster/squad-sdk'; import { enableShellMetrics, recordShellSessionDuration, recordAgentResponseLatency, recordShellError } from './shell-metrics.js'; import { buildCoordinatorPrompt, buildInitModePrompt, parseCoordinatorResponse, hasRosterEntries } from './coordinator.js'; @@ -164,7 +164,24 @@ export async function runShell(): Promise { const registry = new SessionRegistry(); const renderer = new ShellRenderer(); - const teamRoot = process.cwd(); + + // Resolve teamRoot: local .squad/ → global squad → cwd (init mode) + const teamRoot = (() => { + const cwd = process.cwd(); + // 1. Walk up from cwd looking for a local .squad/ + const localSquad = resolveSquad(cwd); + if (localSquad) { + return pathResolve(localSquad, '..'); + } + // 2. Fall back to global (personal) squad path + const globalPath = resolveGlobalSquadPath(); + const globalSquadDir = join(globalPath, '.squad'); + if (existsSync(globalSquadDir)) { + return globalPath; + } + // 3. No squad found — use cwd (triggers init mode) + return cwd; + })(); // Session persistence — create or resume a previous session // Skip resume on first run (no team.md or .first-run marker present) diff --git a/packages/squad-sdk/package.json b/packages/squad-sdk/package.json index d5c316061..01175c042 100644 --- a/packages/squad-sdk/package.json +++ b/packages/squad-sdk/package.json @@ -161,16 +161,9 @@ "node": ">=20" }, "dependencies": { - "@github/copilot-sdk": "^0.1.29" - }, - "peerDependencies": { + "@github/copilot-sdk": "^0.1.29", "@opentelemetry/api": "^1.9.0" }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - } - }, "optionalDependencies": { "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.2", "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.2", @@ -183,7 +176,6 @@ "ws": "^8.18.0" }, "devDependencies": { - "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0", "@types/ws": "^8.5.13", "typescript": "^5.7.0" diff --git a/test/acceptance/acceptance.test.ts b/test/acceptance/acceptance.test.ts index 7457acd6e..b17b9e07f 100644 --- a/test/acceptance/acceptance.test.ts +++ b/test/acceptance/acceptance.test.ts @@ -39,3 +39,7 @@ runFeature(join(featuresDir, 'doctor-extended.feature'), registry); runFeature(join(featuresDir, 'help-comprehensive.feature'), registry); runFeature(join(featuresDir, 'error-paths.feature'), registry); runFeature(join(featuresDir, 'exit-codes.feature'), registry); + +// Consult and extract command tests +runFeature(join(featuresDir, 'consult-command.feature'), registry); +runFeature(join(featuresDir, 'extract-command.feature'), registry); diff --git a/test/acceptance/features/consult-command.feature b/test/acceptance/features/consult-command.feature new file mode 100644 index 000000000..650100cbc --- /dev/null +++ b/test/acceptance/features/consult-command.feature @@ -0,0 +1,31 @@ +Feature: Consult command + + Scenario: Help text shows consult command + When I run "squad help" + Then the output contains "consult" + And the output contains "Enter consult mode with your personal squad" + And the exit code is 0 + + Scenario: Consult in non-git directory fails + Given a directory without a ".squad" directory + When I run "squad consult" in the temp directory + Then the output contains "Not a git repository" + And the exit code is 1 + + Scenario: Consult --status in non-consult directory + Given a directory without a ".squad" directory + When I run "squad consult --status" in the temp directory + Then the output contains "Not in consult mode" + And the exit code is 0 + + Scenario: Consult blocked in squadified project + Given the current directory has a ".squad" directory + When I run "squad consult --check" + Then the output contains "already has a .squad/" + And the exit code is 1 + + Scenario: Extract requires consult mode + Given a directory without a ".squad" directory + When I run "squad extract" in the temp directory + Then the output contains "No .squad/config.json found" + And the exit code is 1 diff --git a/test/acceptance/features/extract-command.feature b/test/acceptance/features/extract-command.feature new file mode 100644 index 000000000..e599514ba --- /dev/null +++ b/test/acceptance/features/extract-command.feature @@ -0,0 +1,19 @@ +Feature: Extract command + + Scenario: Help text shows extract command + When I run "squad help" + Then the output contains "extract" + And the output contains "Extract learnings from consult mode session" + And the exit code is 0 + + Scenario: Extract outside consult mode fails + Given a directory without a ".squad" directory + When I run "squad extract" in the temp directory + Then the output contains "No .squad/config.json found" + And the exit code is 1 + + Scenario: Extract --dry-run option exists + When I run "squad help" + Then the output contains "extract" + And the output contains "--dry-run" + And the exit code is 0 diff --git a/test/acceptance/features/help-comprehensive.feature b/test/acceptance/features/help-comprehensive.feature index 4212ee94e..8afe6cb46 100644 --- a/test/acceptance/features/help-comprehensive.feature +++ b/test/acceptance/features/help-comprehensive.feature @@ -3,10 +3,7 @@ Feature: Help comprehensive Scenario: Help lists all core commands When I run "squad help" Then the output contains "Usage:" - And the output contains "Getting Started" - And the output contains "Development" - And the output contains "Team Management" - And the output contains "Utilities" + And the output contains "Commands:" And the output contains "init" And the output contains "upgrade" And the output contains "status" diff --git a/test/acceptance/features/help.feature b/test/acceptance/features/help.feature index 2fd90a960..a470348ac 100644 --- a/test/acceptance/features/help.feature +++ b/test/acceptance/features/help.feature @@ -3,9 +3,8 @@ Feature: Help screen Scenario: Show help with --help flag When I run "squad --help" Then the output contains "Usage:" - And the output contains "Getting Started" + And the output contains "Commands:" And the output contains "init" - And the output contains "squad --help" And the exit code is 0 Scenario: Show help with help command diff --git a/test/acceptance/features/init-command.feature b/test/acceptance/features/init-command.feature index b5ecd9db3..0a77a7060 100644 --- a/test/acceptance/features/init-command.feature +++ b/test/acceptance/features/init-command.feature @@ -3,8 +3,7 @@ Feature: Init command Scenario: Init in existing project shows ready message Given the current directory has a ".squad" directory When I run "squad init" - Then the output contains "Scaffold ready" - And the output contains "already exists" + Then the output contains "already exists" And the exit code is 0 Scenario: Init exit code is zero on success diff --git a/test/acceptance/features/status-extended.feature b/test/acceptance/features/status-extended.feature index a9c3b353a..3abab3537 100644 --- a/test/acceptance/features/status-extended.feature +++ b/test/acceptance/features/status-extended.feature @@ -4,12 +4,12 @@ Feature: Status command extended Given the current directory has a ".squad" directory When I run "squad status" Then the output contains "Squad Status" - And the output contains "Here:" + And the output contains "Active squad:" And the output contains "Path:" And the exit code is 0 Scenario: Status in directory without squad shows no active squad Given a directory without a ".squad" directory When I run "squad status" in the temp directory - Then the output does not contain "Here: repo" + Then the output does not contain "Active squad: repo" And the exit code is 0 diff --git a/test/acceptance/features/status.feature b/test/acceptance/features/status.feature index 80305f22c..ba94d4c6b 100644 --- a/test/acceptance/features/status.feature +++ b/test/acceptance/features/status.feature @@ -4,5 +4,5 @@ Feature: Status command Given the current directory has a ".squad" directory When I run "squad status" Then the output contains "Squad Status" - And the output contains "Here:" + And the output contains "Active squad:" And the exit code is 0 diff --git a/test/cli/consult.test.ts b/test/cli/consult.test.ts index e51df5fe4..33154f4f9 100644 --- a/test/cli/consult.test.ts +++ b/test/cli/consult.test.ts @@ -178,4 +178,118 @@ describe('CLI: squad consult', () => { expect(result.stderr).toMatch(/no personal squad/i); }); }); + + describe('happy path: init global → consult → status → extract', () => { + const globalConfig = join(TEST_ROOT, 'xdg-config'); + const projectDir = join(TEST_ROOT, 'their-project'); + const envWithGlobal = { XDG_CONFIG_HOME: globalConfig }; + + beforeEach(() => { + // 1. Create a personal (global) squad via `squad init --global` + mkdirSync(globalConfig, { recursive: true }); + const initResult = runSquad('init --global', TEST_ROOT, envWithGlobal); + expect(initResult.exitCode).toBe(0); + + // Verify the personal squad was created + const personalSquadDir = join(globalConfig, 'squad', '.squad'); + expect(existsSync(personalSquadDir)).toBe(true); + + // 2. Create a fresh project with its own git repo (no .squad/) + mkdirSync(projectDir, { recursive: true }); + initGitRepo(projectDir); + }); + + it('squad consult sets up consult mode in the project', () => { + const result = runSquad('consult', projectDir, envWithGlobal); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Consult mode activated'); + + // .squad/ should exist in the project + expect(existsSync(join(projectDir, '.squad'))).toBe(true); + expect(existsSync(join(projectDir, '.squad', 'config.json'))).toBe(true); + + // config.json should have consult: true + const config = JSON.parse( + readFileSync(join(projectDir, '.squad', 'config.json'), 'utf-8'), + ); + expect(config.consult).toBe(true); + expect(config.sourceSquad).toBeTruthy(); + + // extract/ staging directory should exist + expect(existsSync(join(projectDir, '.squad', 'extract'))).toBe(true); + + // .git/info/exclude should contain .squad/ + const excludePath = join(projectDir, '.git', 'info', 'exclude'); + expect(existsSync(excludePath)).toBe(true); + const excludeContent = readFileSync(excludePath, 'utf-8'); + expect(excludeContent).toContain('.squad/'); + + // git status should show nothing (invisible to project) + const gitStatus = execSync('git status --porcelain', { + cwd: projectDir, + encoding: 'utf-8', + }); + expect(gitStatus).not.toContain('.squad'); + }); + + it('squad consult --status reports active consult mode', () => { + // First enter consult mode + runSquad('consult', projectDir, envWithGlobal); + + // Then check status + const result = runSquad('consult --status', projectDir, envWithGlobal); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Consult mode active'); + }); + + it('squad consult --check shows dry-run without creating files', () => { + const result = runSquad('consult --check', projectDir, envWithGlobal); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Dry-run'); + expect(result.stdout).toContain('consult: true'); + + // .squad/ should NOT exist (dry run) + expect(existsSync(join(projectDir, '.squad'))).toBe(false); + }); + + it('squad extract --dry-run shows staged learnings without modifying', () => { + // Enter consult mode + runSquad('consult', projectDir, envWithGlobal); + + // Stage a learning manually (simulating what Scribe does during a session) + const extractDir = join(projectDir, '.squad', 'extract'); + writeFileSync( + join(extractDir, 'use-async-await.md'), + '### Always use async/await\n\nPrefer async/await over raw promises.', + ); + + // Dry-run extract + const result = runSquad('extract --dry-run', projectDir, envWithGlobal); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Dry-run'); + expect(result.stdout).toContain('use-async-await.md'); + + // The learning should still be in extract/ (not removed) + expect(existsSync(join(extractDir, 'use-async-await.md'))).toBe(true); + }); + + it('squad extract with no staged learnings reports empty', () => { + // Enter consult mode (no learnings staged) + runSquad('consult', projectDir, envWithGlobal); + + const result = runSquad('extract', projectDir, envWithGlobal); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('No learnings staged'); + }); + + it('squad consult fails if project already has .squad/', () => { + // Enter consult mode first time + runSquad('consult', projectDir, envWithGlobal); + + // Try again — should fail because .squad/ already exists + const result = runSquad('consult', projectDir, envWithGlobal); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/already has/i); + }); + }); });