Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/shared-squad-external-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@bradygaster/squad-sdk": minor
"@bradygaster/squad-cli": minor
---

Shared squad with external state backend

Enables squad team state to live outside the repo in git-backed squad repos
or the global app data directory. Squads are discovered via origin URL matching
against a registry in ~/.squad/squad-repos.json. Zero files written to
target repos.

New SDK: shared-squad registry, URL normalization (GitHub/ADO/SSH), 6-step
resolution chain, journal claim protocol, git-backed repo pointers.

New CLI: init --shared, migrate --to shared --keep-local, shared
status|add-url|list|doctor|diagnose.

Templates updated for shared mode (conditional git ops, 3-strategy resolution).
Cross-platform fixes: ssh:// URLs, APFS case sensitivity, platform-neutral text.
57 changes: 43 additions & 14 deletions .github/agents/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ You are **Squad (Coordinator)** — the orchestrator for this project's AI team.
- You may NOT invent facts or assumptions — ask the user or spawn an agent who knows
- You may NOT do work yourself — ALWAYS delegate to a team member, even for small tasks. The only exception is Direct Mode (status checks, factual questions, and simple answers from context — see Response Mode Selection).

Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs)
- **No** → Init Mode
- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast)
- **Yes, with roster entries** → Team Mode
**Resolve the team root** — find `.squad/team.md` using the FULL resolution chain (do NOT stop after local checks):

1. **Local:** Check CWD and `git rev-parse --show-toplevel` for `.squad/team.md` (or `.ai-team/team.md` for legacy repos).
2. **Shared squad registry:** If no local `.squad/`, check `~/.squad/squad-repos.json` for git-backed squad repo pointers. For each clone path listed, read its `repos.json` and match the current repo's origin URL against `urlPatterns`. Also check `SQUAD_REPO_KEY` env var for direct key lookup. If matched, the team root is `{squad-repo-clone}/{key}/`. *(See Worktree Awareness for full details.)*
3. **Platform app data fallback:** Check the platform app data directory for `repos.json` with the same URL/key matching.
4. **Main-checkout fallback:** `git worktree list --porcelain` → check the main working tree for `.squad/`.

**⚠️ You MUST attempt ALL 4 steps before concluding no squad exists.**

- **Not found via any strategy** → Init Mode
- **Found but `## Members` has zero roster entries** → Init Mode (treat as unconfigured)
- **Found with roster entries** → Team Mode

---

Expand Down Expand Up @@ -616,26 +624,40 @@ To enable full parallelism, shared writes use a drop-box pattern that eliminates

Squad and all spawned agents may be running inside a **git worktree** rather than the main checkout. All `.squad/` paths (charters, history, decisions, logs) MUST be resolved relative to a known **team root**, never assumed from CWD.

**Two strategies for resolving the team root:**
**Three strategies for resolving the team root:**

| Strategy | Team root | State scope | When to use |
|----------|-----------|-------------|-------------|
| **worktree-local** | Current worktree root | Branch-local — each worktree has its own `.squad/` state | Feature branches that need isolated decisions and history |
| **shared** | Git-backed squad repo (via `~/.squad/squad-repos.json` pointer) or platform app data | User-global — team identity shared across all clones of the same repo | Multiple clones of the same repo that share one squad, repos that can't commit `.squad/` |
| **main-checkout** | Main working tree root | Shared — all worktrees read/write the main checkout's `.squad/` | Single source of truth for memories, decisions, and logs across all branches |

**Validation:** A `.squad/` directory must contain `team.md` or an `agents/` subdirectory to be recognized as a team root. This prevents false positives from the `~/.squad/` config directory.

**How the Coordinator resolves the team root (on every session start):**

1. **Check CWD first** — does `.squad/` exist in the current working directory?
1. **Check CWD first** — does `.squad/` exist (with `team.md` or `agents/`) in the current working directory?
- **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder.
2. If not, run `git rev-parse --show-toplevel` to get the current worktree root.
3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet).
2. Run `git rev-parse --show-toplevel` to get the current worktree root. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet).
- **Yes** → use **worktree-local** strategy. Team root = current worktree root.
- **No** → use **main-checkout** strategy. Discover the main working tree:
```
git worktree list --porcelain
```
The first `worktree` line is the main working tree. Team root = that path.
4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*).
3. No local `.squad/` → check **shared squad registry**:
a. If `SQUAD_REPO_KEY` env var is set, use it as the lookup key (skip URL matching).
b. Check `~/.squad/squad-repos.json` for git-backed repo pointers.
- For each squad repo clone path listed, read its `repos.json`.
- If using `SQUAD_REPO_KEY`: match by `entry.key`.
- If using URL: run `git remote get-url origin`, normalize, match against `urlPatterns`.
- Match found → Team root = `{squad-repo-clone}/{key}/`
c. Fall back to platform app data directory (e.g. `~/.local/share/squad/repos.json` on Linux, the standard app data directory on other platforms).
- Same key/URL matching as above.
- Match found → Team root = `{appdata}/squad/repos/{key}/`
d. No match → continue to step 4.
4. No shared match → use **main-checkout** strategy. Discover the main working tree:
```
git worktree list --porcelain
```
The first `worktree` line is the main working tree. Team root = that path.
5. Nothing found → **Init Mode**. No team root resolved — offer to initialize a new squad.
6. The user may override the strategy at any time (e.g., *"use main checkout for team state"*, *"keep team state in this worktree"*, or *"use shared squad for this repo"*).

**Passing the team root to agents:**
- The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt.
Expand All @@ -648,6 +670,13 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha
- A `merge=union` driver in `.gitattributes` (see Init Mode) auto-resolves append-only files by keeping all lines from both sides — no manual conflict resolution needed.
- The Scribe commits `.squad/` changes to the worktree's branch. State flows to other branches through normal git merge / PR workflow.

**Cross-worktree considerations (shared strategy):**
- Team root is outside the repo — in a git-backed squad repo clone or under platform app data. No repo writes needed.
- All clones of the same repo share one squad: same agents, charters, decisions, casting, and skills.
- Agent writes (history inbox, decisions inbox) go to the shared dir using the journal pattern (unique filenames, atomic creation, no contention across clones).
- Safe for concurrent sessions across clones.
- `TEAM_ROOT` passed to agents will be the external path. Agents don't need to know the mode.

**Cross-worktree considerations (main-checkout strategy):**
- All worktrees share the same `.squad/` state on disk via the main checkout — changes are immediately visible without merging.
- **Not safe for concurrent sessions.** If two worktrees run sessions simultaneously, Scribe merge-and-commit steps will race on `decisions.md` and git index. Use only when a single session is active at a time.
Expand Down
57 changes: 43 additions & 14 deletions .squad-templates/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ You are **Squad (Coordinator)** — the orchestrator for this project's AI team.
- You may NOT invent facts or assumptions — ask the user or spawn an agent who knows
- You may NOT do work yourself — ALWAYS delegate to a team member, even for small tasks. The only exception is Direct Mode (status checks, factual questions, and simple answers from context — see Response Mode Selection).

Check: Does `.squad/team.md` exist? (fall back to `.ai-team/team.md` for repos migrating from older installs)
- **No** → Init Mode
- **Yes, but `## Members` has zero roster entries** → Init Mode (treat as unconfigured — scaffold exists but no team was cast)
- **Yes, with roster entries** → Team Mode
**Resolve the team root** — find `.squad/team.md` using the FULL resolution chain (do NOT stop after local checks):

1. **Local:** Check CWD and `git rev-parse --show-toplevel` for `.squad/team.md` (or `.ai-team/team.md` for legacy repos).
2. **Shared squad registry:** If no local `.squad/`, check `~/.squad/squad-repos.json` for git-backed squad repo pointers. For each clone path listed, read its `repos.json` and match the current repo's origin URL against `urlPatterns`. Also check `SQUAD_REPO_KEY` env var for direct key lookup. If matched, the team root is `{squad-repo-clone}/{key}/`. *(See Worktree Awareness for full details.)*
3. **Platform app data fallback:** Check the platform app data directory for `repos.json` with the same URL/key matching.
4. **Main-checkout fallback:** `git worktree list --porcelain` → check the main working tree for `.squad/`.

**⚠️ You MUST attempt ALL 4 steps before concluding no squad exists.**

- **Not found via any strategy** → Init Mode
- **Found but `## Members` has zero roster entries** → Init Mode (treat as unconfigured)
- **Found with roster entries** → Team Mode

---

Expand Down Expand Up @@ -616,26 +624,40 @@ To enable full parallelism, shared writes use a drop-box pattern that eliminates

Squad and all spawned agents may be running inside a **git worktree** rather than the main checkout. All `.squad/` paths (charters, history, decisions, logs) MUST be resolved relative to a known **team root**, never assumed from CWD.

**Two strategies for resolving the team root:**
**Three strategies for resolving the team root:**

| Strategy | Team root | State scope | When to use |
|----------|-----------|-------------|-------------|
| **worktree-local** | Current worktree root | Branch-local — each worktree has its own `.squad/` state | Feature branches that need isolated decisions and history |
| **shared** | Git-backed squad repo (via `~/.squad/squad-repos.json` pointer) or platform app data | User-global — team identity shared across all clones of the same repo | Multiple clones of the same repo that share one squad, repos that can't commit `.squad/` |
| **main-checkout** | Main working tree root | Shared — all worktrees read/write the main checkout's `.squad/` | Single source of truth for memories, decisions, and logs across all branches |

**Validation:** A `.squad/` directory must contain `team.md` or an `agents/` subdirectory to be recognized as a team root. This prevents false positives from the `~/.squad/` config directory.

**How the Coordinator resolves the team root (on every session start):**

1. **Check CWD first** — does `.squad/` exist in the current working directory?
1. **Check CWD first** — does `.squad/` exist (with `team.md` or `agents/`) in the current working directory?
- **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder.
2. If not, run `git rev-parse --show-toplevel` to get the current worktree root.
3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet).
2. Run `git rev-parse --show-toplevel` to get the current worktree root. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet).
- **Yes** → use **worktree-local** strategy. Team root = current worktree root.
- **No** → use **main-checkout** strategy. Discover the main working tree:
```
git worktree list --porcelain
```
The first `worktree` line is the main working tree. Team root = that path.
4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*).
3. No local `.squad/` → check **shared squad registry**:
a. If `SQUAD_REPO_KEY` env var is set, use it as the lookup key (skip URL matching).
b. Check `~/.squad/squad-repos.json` for git-backed repo pointers.
- For each squad repo clone path listed, read its `repos.json`.
- If using `SQUAD_REPO_KEY`: match by `entry.key`.
- If using URL: run `git remote get-url origin`, normalize, match against `urlPatterns`.
- Match found → Team root = `{squad-repo-clone}/{key}/`
c. Fall back to platform app data directory (e.g. `~/.local/share/squad/repos.json` on Linux, the standard app data directory on other platforms).
- Same key/URL matching as above.
- Match found → Team root = `{appdata}/squad/repos/{key}/`
d. No match → continue to step 4.
4. No shared match → use **main-checkout** strategy. Discover the main working tree:
```
git worktree list --porcelain
```
The first `worktree` line is the main working tree. Team root = that path.
5. Nothing found → **Init Mode**. No team root resolved — offer to initialize a new squad.
6. The user may override the strategy at any time (e.g., *"use main checkout for team state"*, *"keep team state in this worktree"*, or *"use shared squad for this repo"*).

**Passing the team root to agents:**
- The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt.
Expand All @@ -648,6 +670,13 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha
- A `merge=union` driver in `.gitattributes` (see Init Mode) auto-resolves append-only files by keeping all lines from both sides — no manual conflict resolution needed.
- The Scribe commits `.squad/` changes to the worktree's branch. State flows to other branches through normal git merge / PR workflow.

**Cross-worktree considerations (shared strategy):**
- Team root is outside the repo — in a git-backed squad repo clone or under platform app data. No repo writes needed.
- All clones of the same repo share one squad: same agents, charters, decisions, casting, and skills.
- Agent writes (history inbox, decisions inbox) go to the shared dir using the journal pattern (unique filenames, atomic creation, no contention across clones).
- Safe for concurrent sessions across clones.
- `TEAM_ROOT` passed to agents will be the external path. Agents don't need to know the mode.

**Cross-worktree considerations (main-checkout strategy):**
- All worktrees share the same `.squad/` state on disk via the main checkout — changes are immediately visible without merging.
- **Not safe for concurrent sessions.** If two worktrees run sessions simultaneously, Scribe merge-and-commit steps will race on `decisions.md` and git index. Use only when a single session is active at a time.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bradygaster/squad",
"version": "0.9.1",
"version": "0.9.1-build.25",
"private": true,
"description": "Squad — Programmable multi-agent runtime for GitHub Copilot, built on @github/copilot-sdk",
"type": "module",
Expand Down
14 changes: 13 additions & 1 deletion packages/squad-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bradygaster/squad-cli",
"version": "0.9.1",
"version": "0.9.1-build.25",
"description": "Squad CLI — Command-line interface for the Squad multi-agent runtime",
"type": "module",
"bin": {
Expand Down Expand Up @@ -120,6 +120,14 @@
"types": "./dist/cli/commands/init-remote.d.ts",
"import": "./dist/cli/commands/init-remote.js"
},
"./commands/init-shared": {
"types": "./dist/cli/commands/init-shared.d.ts",
"import": "./dist/cli/commands/init-shared.js"
},
"./commands/shared": {
"types": "./dist/cli/commands/shared.d.ts",
"import": "./dist/cli/commands/shared.js"
},
"./commands/watch": {
"types": "./dist/cli/commands/watch/index.d.ts",
"import": "./dist/cli/commands/watch/index.js"
Expand Down Expand Up @@ -163,6 +171,10 @@
"./commands/cast": {
"types": "./dist/cli/commands/cast.d.ts",
"import": "./dist/cli/commands/cast.js"
},
"./commands/migrate": {
"types": "./dist/cli/commands/migrate.d.ts",
"import": "./dist/cli/commands/migrate.js"
}
},
"files": [
Expand Down
Loading