diff --git a/.copilot/skills/bleed-check/SKILL.md b/.copilot/skills/bleed-check/SKILL.md new file mode 100644 index 000000000..f8f64e4fe --- /dev/null +++ b/.copilot/skills/bleed-check/SKILL.md @@ -0,0 +1,145 @@ +# Bleed-Check: Cross-Branch Audit + +## Confidence +High + +## Domain +Periodic cross-branch bleed audits, stowaway file detection + +## Problem Statement +Features developed on forks can accidentally include files intended for \.squad/\, docs, or unrelated purposes. These "stowaway" files pollute the upstream repository if not caught before PR merge. A periodic audit across all open PRs by a contributor identifies and flags these stragglers before they reach main. + +## High-Risk Shared Files + +**These files are the #1 bleed vectors** — they appear in almost every docs PR and are where cross-branch contamination happens most: + +- **`navigation.ts`** — contains site structure and nav entries for all features +- **`test/docs-build.test.ts`** — build verification tests that reference multiple PRs' output paths +- **`docs/` directories** — shared documentation structure + +**When checking these files, verify entries are ONLY for this PR's content — not entries from other concurrent PRs or stale previous runs. Flag full-file rewrites of these shared files — surgical additions only.** + +## Trigger + +- **Scheduled**: Twice per session day (morning and afternoon sweeps) +- **Proactive**: If >4 hours since last check, coordinator offers proactive bleed audit reminder + +## Scope + +All open PRs by **diberry** targeting **bradygaster/squad** + +Query: +\\\ash +gh pr list --author diberry --repo bradygaster/squad --base dev --state open +\\\ + +## Process + +### 1. List PRs +Fetch all open PRs from diberry targeting bradygaster/squad. + +### 2. For Each PR: Check File List +Retrieve the file list: +\\\ash +gh pr view {pr-number} --repo bradygaster/squad --json files +\\\ + +### 3. Flag Stowaways +Check each file against the PR's stated purpose (from title/description). Red flags: + +| Red Flag | Example | Why It's Bad | +|---|---|---| +| \.squad/\ files | \.squad/decisions/...\, \.squad/agents/...\ | Should not ship in app PRs | +| Navigation entries | \.squad/routing.md\ changes | Wrong PR can cause nav breakage | +| Test expectations | \.squad/agents/*/expected-output\ | Unmaintainable across PRs | +| Full-file rewrites | Accidental large refactors | Out of scope, causes merge debt | +| Build artifacts | \dist/\, \uild/\, \.next/\ | Should be in \.gitignore\ | +| Root-level strays | Unexpected \.env.local\, \secrets.json\ | Likely accidental commits | + +### 3.5: Convention Gate Checks + +While auditing files, also check for house-style violations. **These are blockers, not nits — per team directive.** + +| Convention | Rule | Blocker? | +|-----------|------|----------| +| Internal link format | Use bare paths like `/features/memory`, not `/docs/features/memory` | ✅ Yes | +| Blank lines | Single blank line between sections (not double) | ✅ Yes | +| Entry duplication | Each nav entry appears exactly once | ✅ Yes | +| Stale TDD comments | Clean up "RED PHASE", "TODO: implement", "WIP" markers before merge | ✅ Yes | + +### 3.6: CI Path Debugging Pattern + +When CI reports a step as successful but tests fail on a missing file, path mismatches often indicate cross-branch contamination or stale config: + +Example: CI says "generator succeeded — output at docs/public/" but the test looks for docs/dist/ and fails. + +**Check actual path**: +\\\ash +ls -la docs/public/ +ls -la docs/dist/ +grep "outDir" build.config.ts +grep "docs/dist" test/docs-build.test.ts +\\\ + +**Pattern**: Add `ls -la {expected-path}` verification steps when debugging CI file issues. This reveals if the build wrote to the wrong directory (often from stale config or entries from another PR). + +### 4. Output Format: Emoji-Based Table + +\\\ +| PR # | Title | Status | Bleed? | Details | +|---|---|---|---|---| +| #42 | Add auth feature | 🟢 CLEAN | No | All files in scope | +| #43 | Refactor parser | 🔴 BLEED | Yes | \.squad/routing.md\ found (stowaway) | +| #44 | Update docs | 🟢 CLEAN | No | Docs changes as intended | +\\\ + +Status indicators: +- 🟢 CLEAN: All files align with PR purpose +- 🔴 BLEED: Stowaway files detected, PR needs cleanup + +## Coordinator Behavior + +The Copilot acting as coordinator: + +1. **Track Last Check Time**: Record timestamp of last bleed audit +2. **Proactive Reminders**: After 4+ hours, suggest: "Hey, time for a bleed check? Want me to audit your open PRs?" +3. **Detailed Reports**: Show emoji table with file-by-file breakdown +4. **Actionable Guidance**: If bleed detected, suggest: "Pull request #43 has \.squad/routing.md\ - should this be removed before merging?" + +## Session Integration + +- Check at start of session for any overnight bleeds +- Offer mid-session reminder if threshold exceeded +- Report findings before upstream PR opens +- Track frequency for team metrics + +## Example Session Flow + +\\\ +✓ Session starts + "Last bleed check was 6 hours ago. Want me to run an audit?" + [User: Yes] + +→ Auditing 4 open PRs by diberry... + | #42 | auth feature | 🟢 CLEAN | + | #43 | parser refactor | 🔴 BLEED | .squad/routing.md detected | + | #44 | docs update | 🟢 CLEAN | + | #45 | ui polish | 🟢 CLEAN | + +⚠️ Found 1 bleed in PR #43. Recommend removing .squad/routing.md before merging. +\\\ + +## Files to Inspect + +- PR title and description (stated purpose) +- \gh pr view --json files\ output (file manifest) +- Commit diff per file (spot-check suspect files) +- PR body for merge strategy notes + +## Non-Bleed Scenarios + +NOT considered bleed: +- Legitimate test files for the PR feature +- README or CONTRIBUTING updates (if PR documents the change) +- New src/ files (app code is in scope) +- Configuration files directly related to the feature (tsconfig.json tweaks, etc.) diff --git a/.squad/skills/fork-first-pipeline/SKILL.md b/.squad/skills/fork-first-pipeline/SKILL.md new file mode 100644 index 000000000..b66273b06 --- /dev/null +++ b/.squad/skills/fork-first-pipeline/SKILL.md @@ -0,0 +1,227 @@ +# Fork-First PR Pipeline + +## Confidence +High + +## Domain +PR workflow, cross-fork collaboration + +## Problem Statement +PRs opened directly on the upstream repository get messy iteration in public. Code review feedback creates visible churn. Force-push history is exposed. This workflow keeps development clean by staging changes on the fork first, then opening a single clean upstream PR after review is complete. + +## 8-Step Pipeline + +\\\ +BRANCH → FORK PR → REVIEW → FIX → BLEED CHECK → CLEAN → UPSTREAM → DONE +\\\ + +### Step 1: BRANCH +Create a feature branch locally: +\\\ash +git checkout -b squad/{issue-number}-{slug} +\\\ + +### Step 2: FORK PR +Push to fork and open PR **against your fork's dev branch**: +\\\ash +git push origin {branch-name} +gh pr create --base dev --draft # Opens on fork/dev, not upstream +\\\ + +### Step 3: REVIEW +Iterate on the fork PR with teammates. Collect feedback via review comments. This happens in your fork, not upstream. + +### Step 4: FIX +Address review comments. Commit changes directly to the feature branch (don't squash yet). + +### Step 5: BLEED CHECK +Run a bleed audit to verify no stowaway files are committed. Check for: +- \.squad/\ files (should not be in app PRs) +- Navigation entries for wrong PR +- Test expectations for wrong PR +- Full-file rewrites +- Build artifacts +- Root-level strays + +If bleed detected, fix on the feature branch. + +### Step 5.5: REBASE +Before squashing for upstream, rebase the feature branch against `origin/dev` to avoid full-file rewrites: +\\\ash +git fetch origin dev +git rebase origin/dev +\\\ + +#### Shared File Strategy +For files shared across PRs (navigation.ts, test files, CI workflows): +- **Never** make full-file changes on feature branches +- **Always** reset to dev first, then make surgical additions: + \\\ash + git checkout origin/dev -- docs/src/navigation.ts + # Then manually add ONLY the entries for this PR's content + \\\ +- This prevents diffs that rewrite the entire file, which cause merge conflicts with every other PR + +#### Serial Branch Operations — Critical Rule + +⚠️ **When multiple agents fix different PRs in parallel on the same physical repository (even if different branches), they all run `git checkout` in the same working tree.** This causes: +- Working tree conflicts (both agents try to update `.git/index` simultaneously) +- Commits landing on wrong branches (Agent A's commit lands on Agent B's branch) +- Race conditions (`.git/HEAD` and `.git/index` become inconsistent) + +**RULE: Bleed fixes across multiple PRs must be done SERIALLY (one at a time) unless using git worktrees.** Never launch parallel agents that checkout different branches in the same repo without worktrees. + +**Exception**: Use `git worktree add ../squad-195 -b squad/195-fix-issue origin/dev` to give each agent an isolated working directory. + +#### When Rebase Fails +If rebase has conflicts on shared files: +1. `git rebase --abort` +2. Reset the shared files to dev: `git checkout origin/dev -- {file}` +3. Re-add only this PR's surgical changes +4. `git commit --amend --no-edit` +5. Continue with step 6 CLEAN + +### Step 6: CLEAN +Prepare for upstream PR: +- Squash commits into logical units +- Clean up commit messages +- Remove any \.squad/\ files if present +- Check for stale TDD comments ("RED PHASE", "TODO: implement", "WIP") that no longer apply +- Verify no \/docs/\ prefix in titles +- Remove double blank lines from description + +**Note on `.squad/skills/` files**: The `.squad/skills/` directory may be gitignored in some configurations. If you need to commit skill files to the fork, use `git add -f` to force them through, even if they match `.gitignore` patterns. + +### Step 7: UPSTREAM +Open PR on upstream repository against radygaster/squad:dev: +\\\ash +gh pr create --repo bradygaster/squad --base dev --fill +\\\ + +After the PR is created, undraft it to mark it ready for review. The upstream PR is the **final presentation** — it should not remain in draft: +\\\ash +gh pr ready {pr-number} --repo bradygaster/squad +\\\ + +The upstream PR signals completion of the fork pipeline. Leaving it in draft confuses reviewers — they may assume work is still in progress when iteration is already complete. +### Step 8: DONE +Upstream PR is merged. Close or keep fork PR for reference. + +## Anti-Patterns + +| Anti-Pattern | Why It Fails | Better Way | +|---|---|---| +| Open upstream PR before fork review complete | Public iteration, messy history | Complete review cycle on fork first | +| Force-push to upstream branch | Breaks links, confuses reviewers | Squash locally, push once | +| Skip bleed check | Stowaway files merge upstream | Always audit before upstream PR | +| Commit \.squad/\ files in app PRs | Repo pollution, merge conflicts | Exclude from staging, bleed check catches this | +| Open multiple PRs per feature | Fragmented review, merge chaos | One upstream PR per feature | +| Skip rebase before upstream | Diverged branch creates full-file diffs | Always rebase against origin/dev before step 6 | +| Parallel branch checkouts in same repo | Multiple agents switch branches simultaneously, causing working tree conflicts and commits landing on wrong branches | Use git worktrees or fix PRs serially | +| Forgetting to stash skill file updates | `.squad/skills/` files are gitignored, so they don't commit to the fork | Use `git add -f` when committing skill files | +| Leaving stale TDD comments in code | "RED PHASE", "TODO: implement" markers confuse reviewers after merge | Clean up all TDD markers before approval | +| Leave upstream PR in draft | Signals incomplete work when pipeline is done | Undraft upstream PR after opening — it's presentation-ready | + +## Pre-Upstream Gate Checklist + +Before opening the upstream PR, verify: + +- [ ] **Flight approval**: Fork PR merged into fork/dev +- [ ] **FIDO approval**: Code quality, tests pass, no security issues +- [ ] **Bleed check pass**: Zero stowaway files, no \.squad/\ commits +- [ ] **Squash commits**: 1-3 logical commits, not N from iteration +- [ ] **Clean description**: No double blank lines, clear problem/solution +- [ ] **No \.squad/\ files**: Excluded from commit entirely +- [ ] **No \/docs/\ prefix**: If docs changes, they go elsewhere +- [ ] **No double blank lines**: Markdown/description formatting clean + +## Team Review Protocol + +The fork PR review phase enforces team-wide quality standards. These patterns are mandatory. + +### Dual Reviewer Gate +Every fork PR must be reviewed by **BOTH Flight (architecture)** and **FIDO (quality)** before approval. The rule: +- **Both APPROVE**: Ready to proceed +- **One APPROVE + One REJECT**: Not ready — re-review cycle required +- **No single-reviewer approvals**: A partial approval does not unblock + +Both reviewers must sign off in the same review round or in sequential re-review rounds (see Review→Fix→Re-review Loop below). + +### Convention Issues Are Blockers +The following are **NOT nits** — they are blockers per team directive. Flight and FIDO must flag these as `NEEDS FIXES`, never as "non-blocking notes": + +- **`/docs/` prefix in internal links**: Links like `/docs/features/memory` should be bare: `/features/memory`. The /docs/ prefix is stripped at build time; links must not include it. +- **Double blank lines**: Single blank line is house style. Two or more consecutive blank lines must be reduced to one. +- **Table ordering discontinuities**: If tables reference order (e.g., step 1, 2, 3), they must be sequential without gaps. +- **Whitespace violations**: Trailing spaces, tabs instead of spaces, or inconsistent indentation. + +When a reviewer finds any of these, the review verdict is **NEEDS FIXES** with findings documented. The author must correct all flagged conventions. + +### Review→Fix→Re-review Loop +After reviewers identify blockers: + +1. **Fix**: Author addresses all blocker comments on the feature branch (use `git commit --amend --no-edit` for fast-forward fixes, or new commits if refactoring is complex). +2. **Re-run reviews**: **Both Flight AND FIDO must re-review**, even if only one reviewer found issues. This ensures convention fixes don't introduce new problems. +3. **Loop until both APPROVE**: Repeat until both reviewers vote APPROVE. +4. **Bleed check**: After both APPROVE, proceed to Step 5 (bleed check). + +Do not skip to bleed check after a single reviewer fix. + +### Reviewer Lockout +If Flight rejects a fork PR, the original author is locked out from self-revising. A **different agent** must make the fix and commit it. This is enforced mechanically: + +- **Original author**: Locked from further commits on the feature branch after rejection. +- **Different agent** (e.g., Booster stepping in for PAO): Makes the fixes, commits, and force-pushes with `--force-with-lease`. +- **Re-review**: Both Flight and FIDO re-review the updated PR. + +This rule prevents single-agent fixation loops and ensures peer review diversity. + +### Known PAO Quirks (Watch for These) +If PAO is the author, Flight and FIDO should watch for: + +- **`.squad/` file creep**: PAO often includes `.squad/agents/pao/history.md` or other `.squad/` files in commits. These must be explicitly excluded via bleed check or pre-commit rules. +- **`/docs/` prefix overuse**: PAO tends to add `/docs/` prefix to all internal links. Every reviewer must instruct "use bare paths — no /docs/ prefix." +- **Force-push caution**: Before PAO pushes, verify `git diff --cached --stat` matches intent and `git diff --cached --diff-filter=D` shows zero unintended deletions. + +This is not a critique — it is a known pattern. Treat it as a checklist, not a personal issue. + +### Review Comments Posted on PR +Formal reviews are posted to the fork PR as: +``` +gh pr comment {pr-number} --body "REVIEW: {verdict} + +## Findings +{findings} + +## Summary +{summary}" +``` + +Verdict options: `APPROVE`, `NEEDS FIXES`, `REQUEST CHANGES`. + +Findings should reference specific files, line numbers, and convention violations. Summary should restate the path forward (re-review required, bleed check next, etc.). + +### Tamir Reviewer Rule +Only assign Tamir as a reviewer on **upstream PRs** where his original code PR is the source material for the content being documented. Do not add him to every upstream PR by default. On fork PRs, Flight and FIDO are the standard reviewers. + +### Commit Hygiene +To ensure clean histories: + +- **One commit per fork PR**: Squash all review iterations into a single logical commit before opening upstream PR. +- **Amend, don't create new commits**: Use `git commit --amend --no-edit` to fix blockers without adding commit count. +- **Force-push safely**: Use `--force-with-lease` to prevent accidental overwrites if teammates push simultaneously (rare in single-agent fork setup, but good practice). +- **Always verify before push**: + ``` + git diff --cached --stat # File count must match intent + git diff --cached --diff-filter=D # Should show zero unintended deletions + ``` + +## Workflow Summary + +This pipeline separates concerns: +- **Fork PR**: Messy iteration, team feedback, bleed capture +- **Upstream PR**: Clean, single-commit, ready-to-merge + +Result: Upstream PRs are lean, reviewed, and production-ready. + + diff --git a/docs/src/content/docs/concepts/nap-vs-compact.md b/docs/src/content/docs/concepts/nap-vs-compact.md new file mode 100644 index 000000000..cd4b54f73 --- /dev/null +++ b/docs/src/content/docs/concepts/nap-vs-compact.md @@ -0,0 +1,52 @@ +--- +title: Nap vs /compact +description: How Squad nap and Copilot CLI /compact solve different memory problems at different layers. +--- + +# Nap vs /compact + +Users hit memory limits in two ways: the chat gets too big, or agent memory piles up over time. + +The pain shows up for users as slow agents, lost context, or token pressure — even though the fixes happen behind the scenes. + +## How each one works + +**Squad Nap** fixes a long-term team problem by archiving shared memory to disk. Nothing is lost, and future agents stay lean. + +**/compact** fixes a right-now chat problem by summarizing and freeing tokens. Original messages are gone. + +## Comparison + +| | Squad Nap | /compact | +|---|---|---| +| Purpose | Archive squad memory | Shrink current chat | +| Scope | Persistent, shared | Ephemeral, session-only | +| Method | Move full data to disk | Replace with summary | +| Logic | Size + age rules | One-pass LLM | +| Trigger | Thresholds hit | User or token pressure | +| Benefit | Future agents | Current conversation | +| Recovery | Full (archives exist) | None | +| Model | Filing cabinet | Whiteboard erase | + +## When to use each + +**Use `squad nap` when:** +- Your `.squad/` files are growing and agents feel slower +- You've completed a phase and want to archive old work +- You're preparing to spawn new agents (smaller state = cheaper spawns) +- You want to keep a permanent archive of past decisions (they go to `-archive.md`) + +**Use `/compact` when:** +- Your current conversation is getting long and token-heavy +- You want to continue the session but free up context space for new questions +- You're running out of conversation tokens within a single session +- You want a summary of your chat history before moving on + +## Using both together + +They're **not** mutually exclusive — use both in sequence for optimal team hygiene: + +1. **In your Squad session:** Tell the team to `take a nap`. This cleans up persistent state files, making future agent spawns lighter and faster. +2. **After the session:** If your conversation was long, use `/compact` in the CLI to summarize the transcript. This frees tokens for the next session. + +**Bottom line: /compact helps this conversation. Squad Nap helps every conversation after this.** diff --git a/docs/src/content/docs/features/context-hygiene.md b/docs/src/content/docs/features/context-hygiene.md index bef518244..2a3317834 100644 --- a/docs/src/content/docs/features/context-hygiene.md +++ b/docs/src/content/docs/features/context-hygiene.md @@ -24,7 +24,7 @@ Over multiple sessions, Squad's `.squad/` files grow — agent histories, decisi ## Nap -**What it does:** Summarizes accumulated work into smaller, more efficient memory files. This is the same as running `/compact` in the CLI or `squad nap` from the command line. +**What it does:** Summarizes accumulated work in `.squad/` team state files into smaller, more efficient memory files. This compresses your persistent team knowledge — agent histories, decisions, and logs across multiple sessions. When you tell the team to "take a nap," each agent: @@ -52,7 +52,7 @@ squad nap --deep # Thorough cleanup with recursive descent squad nap --dry-run # Preview what would be cleaned up ``` -In the interactive shell, use `/compact` for the same effect. +> **Note:** For session-level conversation compression, use `/compact` in the Copilot CLI interactive shell. See [Nap vs /compact](/concepts/nap-vs-compact) for the difference between team state cleanup and session transcript compression. --- diff --git a/packages/squad-cli/src/cli-entry.ts b/packages/squad-cli/src/cli-entry.ts index 77df6b9d7..3d218232a 100644 --- a/packages/squad-cli/src/cli-entry.ts +++ b/packages/squad-cli/src/cli-entry.ts @@ -272,7 +272,10 @@ async function main(): Promise { } const sdkMod = hasGlobal ? await lazySquadSdk() : null; - const dest = hasGlobal ? sdkMod!.resolveGlobalSquadPath() : process.cwd(); + // When isGlobal, pass the global root — init.ts handles personal-squad/ subdirectory + const dest = hasGlobal + ? sdkMod!.resolveGlobalSquadPath() + : process.cwd(); const noWorkflows = args.includes('--no-workflows'); const sdk = args.includes('--sdk'); const roles = args.includes('--roles'); diff --git a/packages/squad-cli/src/cli/core/init.ts b/packages/squad-cli/src/cli/core/init.ts index 2a0def976..2c5569948 100644 --- a/packages/squad-cli/src/cli/core/init.ts +++ b/packages/squad-cli/src/cli/core/init.ts @@ -118,6 +118,22 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi await typewrite(`${DIM}Let's build your team.${RESET}`, 8); console.log(); + // ── Global init: bootstrap personal-squad/ only (skip repo scaffold) ── + if (options.isGlobal) { + const personalDir = ensurePersonalSquadDir(); + // Create additional directories for a complete personal squad structure + for (const sub of ['casting', 'skills']) { + const dir = path.join(personalDir, sub); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + console.log(`${GREEN}${BOLD}✓${RESET} Personal squad initialized at ${DIM}${personalDir}${RESET}`); + console.log(`${DIM} Add agents with: squad personal add --role ${RESET}`); + console.log(); + return; + } + // Detect project type const projectType = detectProjectType(dest); @@ -299,19 +315,11 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi console.log(); // ── Personal squad bridge ─────────────────────────────────────────── - if (options.isGlobal) { - // Global init: ensure personal-squad/ directory exists alongside .squad/ - const personalDir = ensurePersonalSquadDir(); - console.log(`${GREEN}${BOLD}✓${RESET} Personal squad initialized at ${DIM}${personalDir}${RESET}`); - console.log(`${DIM} Add agents with: squad personal add --role ${RESET}`); + // Note: isGlobal returns early above, so this is always the repo-init path + const personalDir = resolvePersonalSquadDir(); + if (personalDir) { + console.log(`${GREEN}${BOLD}✓${RESET} Personal squad detected — your personal agents will be available here.`); console.log(); - } else { - // Repo init: inform user if personal squad is available - const personalDir = resolvePersonalSquadDir(); - if (personalDir) { - console.log(`${GREEN}${BOLD}✓${RESET} Personal squad detected — your personal agents will be available here.`); - console.log(); - } } if (squadInfo.isLegacy) { diff --git a/test/cli-global.test.ts b/test/cli-global.test.ts index e9d640d6b..e82918317 100644 --- a/test/cli-global.test.ts +++ b/test/cli-global.test.ts @@ -95,13 +95,13 @@ describe('squad status routing logic', () => { // ============================================================================ describe('--global flag routing', () => { - it('init --global resolves to global path, not cwd', () => { - // Replicate the routing logic from src/index.ts: - // const dest = hasGlobal ? resolveGlobalSquadPath() : process.cwd(); + it('init --global resolves to personal-squad path, not global root or cwd', () => { + // Replicate the routing logic from src/cli-entry.ts: + // const dest = hasGlobal ? path.join(resolveGlobalSquadPath(), 'personal-squad') : process.cwd(); const hasGlobal = true; - const dest = hasGlobal ? resolveGlobalSquadPath() : process.cwd(); + const dest = hasGlobal ? join(resolveGlobalSquadPath(), 'personal-squad') : process.cwd(); - expect(dest).toBe(resolveGlobalSquadPath()); + expect(dest).toBe(join(resolveGlobalSquadPath(), 'personal-squad')); expect(dest).not.toBe(process.cwd()); }); diff --git a/test/cli/global-init-fix.test.ts b/test/cli/global-init-fix.test.ts new file mode 100644 index 000000000..14e9732aa --- /dev/null +++ b/test/cli/global-init-fix.test.ts @@ -0,0 +1,154 @@ +/** + * Tests for Issue #53 — squad init --global file placement fix. + * + * Verifies that `squad init --global` creates the personal-squad directory + * structure at the correct location (personal-squad/) and does NOT create + * a `.squad/` directory at the global config root. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { randomBytes } from 'node:crypto'; +import { + resolveGlobalSquadPath, + resolvePersonalSquadDir, + ensurePersonalSquadDir, +} from '@bradygaster/squad-sdk/resolution'; + +// Isolated temp dir inside CWD — randomized to avoid parallel collisions +const TMP = join(process.cwd(), `.test-global-init-${randomBytes(4).toString('hex')}`); + +function cleanup() { + if (existsSync(TMP)) rmSync(TMP, { recursive: true, force: true }); +} + +/** Override env so resolveGlobalSquadPath() returns a sandboxed path inside TMP. */ +function sandboxEnv(): Record { + const saved: Record = { + APPDATA: process.env['APPDATA'], + XDG_CONFIG_HOME: process.env['XDG_CONFIG_HOME'], + HOME: process.env['HOME'], + }; + if (process.platform === 'win32') { + process.env['APPDATA'] = TMP; + } else if (process.platform === 'darwin') { + process.env['HOME'] = TMP; + } else { + process.env['XDG_CONFIG_HOME'] = TMP; + } + return saved; +} + +function restoreEnv(saved: Record) { + for (const [key, val] of Object.entries(saved)) { + if (val !== undefined) process.env[key] = val; + else delete process.env[key]; + } +} + +// ============================================================================ +// Global init creates files inside personal-squad/ +// ============================================================================ + +describe('issue #53 — global init creates files in personal-squad/', () => { + let globalDir: string; + let personalDir: string; + let savedEnv: Record; + + beforeEach(() => { + cleanup(); + mkdirSync(TMP, { recursive: true }); + savedEnv = sandboxEnv(); + globalDir = resolveGlobalSquadPath(); + personalDir = join(globalDir, 'personal-squad'); + }); + + afterEach(() => { + restoreEnv(savedEnv); + cleanup(); + }); + + it('global init creates agents/ inside personal-squad/ directory', () => { + const result = ensurePersonalSquadDir(); + expect(result).toBe(personalDir); + expect(existsSync(join(personalDir, 'agents'))).toBe(true); + }); + + it('global init does NOT create .squad/ at global root level', () => { + ensurePersonalSquadDir(); + const dotSquad = join(globalDir, '.squad'); + expect(existsSync(dotSquad)).toBe(false); + }); + + it('personal-squad/ is discoverable by resolvePersonalSquadDir()', () => { + ensurePersonalSquadDir(); + const discovered = resolvePersonalSquadDir(); + expect(discovered).toBe(personalDir); + }); + + it('global init creates config.json in personal-squad/', () => { + ensurePersonalSquadDir(); + const configPath = join(personalDir, 'config.json'); + expect(existsSync(configPath)).toBe(true); + + const config = JSON.parse(readFileSync(configPath, 'utf-8')); + expect(config).toHaveProperty('defaultModel'); + expect(config).toHaveProperty('ghostProtocol', true); + }); + + it('ensurePersonalSquadDir is idempotent', () => { + const first = ensurePersonalSquadDir(); + const second = ensurePersonalSquadDir(); + expect(first).toBe(second); + expect(existsSync(join(personalDir, 'agents'))).toBe(true); + expect(existsSync(join(personalDir, 'config.json'))).toBe(true); + }); +}); + +// ============================================================================ +// runInit with isGlobal skips full scaffold +// ============================================================================ + +describe('issue #53 — runInit with isGlobal skips .squad/ scaffold', () => { + let globalDir: string; + let personalDir: string; + let savedEnv: Record; + + beforeEach(() => { + cleanup(); + mkdirSync(TMP, { recursive: true }); + savedEnv = sandboxEnv(); + globalDir = resolveGlobalSquadPath(); + personalDir = join(globalDir, 'personal-squad'); + }); + + afterEach(() => { + restoreEnv(savedEnv); + cleanup(); + }); + + it('runInit with isGlobal=true creates personal-squad/ with casting/ and skills/', { timeout: 15_000 }, async () => { + const { runInit } = await import('../../packages/squad-cli/src/cli/core/init.js'); + + await runInit(globalDir, { isGlobal: true }); + + expect(existsSync(personalDir)).toBe(true); + expect(existsSync(join(personalDir, 'agents'))).toBe(true); + expect(existsSync(join(personalDir, 'casting'))).toBe(true); + expect(existsSync(join(personalDir, 'skills'))).toBe(true); + expect(existsSync(join(personalDir, 'config.json'))).toBe(true); + + // .squad/ should NOT exist at the global root + const dotSquad = join(globalDir, '.squad'); + expect(existsSync(dotSquad)).toBe(false); + }); + + it('runInit with isGlobal=true does not create .squad/ at global root', { timeout: 15_000 }, async () => { + const { runInit } = await import('../../packages/squad-cli/src/cli/core/init.js'); + await runInit(globalDir, { isGlobal: true }); + + const dotSquad = join(globalDir, '.squad'); + expect(existsSync(dotSquad)).toBe(false); + }); +});