Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/docs-state-backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bradygaster/squad-cli': patch
---

Add user-facing documentation for state backends (git-notes, orphan-branch, worktree)
285 changes: 285 additions & 0 deletions docs/src/content/docs/features/state-backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# State Backends

> ⚠️ **Experimental** — Squad is alpha software. APIs, commands, and behavior may change between releases.


**Try this to use git-notes for state storage:**
```bash
squad watch --state-backend git-notes
```

**Try this to use an orphan branch:**
```bash
squad watch --state-backend orphan
```

**Try this to set a persistent default:**
```bash
echo '{ "stateBackend": "git-notes" }' > .squad/config.json
```

Squad supports multiple **state backends** for storing `.squad/` state. Each backend determines _where_ and _how_ decisions, skills, agent memories, and session logs are persisted — without changing how agents interact with the data.

---

## The Problem

The default **worktree** backend stores `.squad/` state as regular files in the working tree. This works well for most workflows, but has trade-offs:

- **Branch pollution:** `.squad/` files appear in diffs and PRs
- **Branch-switch loss:** State can be lost when switching branches (if not committed)
- **Merge conflicts:** Multiple branches modifying `.squad/` files can conflict

State backends solve this by moving `.squad/` data into Git-native structures that live outside the working tree.

---

## Available Backends

### Worktree (default)

State lives as regular files in `.squad/` inside the working tree. This is the standard behavior — what you get out of the box.

```bash
squad watch --state-backend worktree
```

**Pros:**
- Simple and familiar — files on disk
- Easy to inspect, edit, and commit
- Works with all Git tools and IDEs

**Cons:**
- Files appear in `git status` and diffs
- Branch switches can lose uncommitted state

**Best for:** Most projects, especially when you want squad state committed alongside code.

---

### Git Notes

State is stored in [Git notes](https://git-scm.com/docs/git-notes) under `refs/notes/squad`. Notes are attached to `HEAD`, keeping data associated with commits but invisible in the working tree.

```bash
squad watch --state-backend git-notes
```

**How it works:**
- All state is serialized as a single JSON blob attached as a note on `HEAD`
- Reading loads the JSON, writing updates and reattaches it
- Notes travel with `git push` / `git fetch` when configured (see [Sharing](#sharing-git-notes-state))

**Pros:**
- Working tree stays completely clean — no `.squad/` files
- State is associated with specific commits
- No merge conflicts from `.squad/` files in PRs

**Cons:**
- State is per-commit — switching to a different commit loses the note context
- Requires `git notes` familiarity for debugging
- Not human-readable without `git notes show`

**Best for:** Repos where you want zero `.squad/` files in the working tree or PRs.

#### Sharing Git Notes State

By default, Git doesn't push notes. To share git-notes state across clones:

```bash
# Push notes
git push origin refs/notes/squad

# Fetch notes
git fetch origin refs/notes/squad:refs/notes/squad
```

Or configure automatic fetch in `.git/config`:

```ini
[remote "origin"]
fetch = +refs/notes/squad:refs/notes/squad
```

---

### Orphan Branch

State lives on a dedicated orphan branch (`squad-state` by default). The branch has no common history with your main branches — it's a completely separate tree used only for squad data.

```bash
squad watch --state-backend orphan
```

**How it works:**
- An orphan branch `squad-state` is created automatically on first write
- Each state file is stored as a blob in the branch's tree
- Reads use `git show squad-state:<path>`, writes create new commits on the branch
- The branch is never checked out — all operations use Git plumbing commands

**Pros:**
- Working tree stays clean
- State is versioned with full Git history
- Easy to inspect: `git log squad-state`, `git show squad-state:decisions.md`
- Pushes/fetches with normal branch operations

**Cons:**
- An extra branch in your repository
- Slightly more complex than worktree for debugging
- Concurrent writes to the branch can conflict (single-writer recommended)

**Best for:** Teams who want Git-versioned state without polluting the main branch history.

---

## Configuration

### CLI Flag (per-invocation)

Pass `--state-backend` to `squad watch` or `squad triage`:

```bash
squad watch --state-backend git-notes
squad triage --state-backend git-notes
squad watch --state-backend orphan
squad watch --state-backend worktree
```
Comment on lines +137 to +146

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--state-backend is documented here as working with “any squad command”, but the CLI currently only parses/accepts this flag for the watch/triage command path. Please narrow the wording (or list the supported commands) so readers don’t try it with other subcommands and get unexpected behavior.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in ab256ef.


### Config File (persistent)

Set a default in `.squad/config.json`:

```json
{
"stateBackend": "git-notes"
}
```

This persists across invocations. The CLI flag overrides the config file when both are present.

### Priority Order

| Priority | Source | Example |
|----------|--------|---------|
| 1 (highest) | CLI flag | `--state-backend orphan` |
| 2 | `.squad/config.json` | `"stateBackend": "orphan"` |
| 3 (default) | Built-in default | `worktree` |

### Fallback Behavior

If a non-default backend fails to initialize (e.g., Git is not available, permissions issue), Squad automatically falls back to the **worktree** backend with a warning:

```
Warning: State backend 'git-notes' failed: <reason>. Falling back to 'worktree'.
```

---

## Comparison

| Feature | Worktree | Git Notes | Orphan Branch |
|---------|----------|-----------|---------------|
| Working tree clean | ❌ | ✅ | ✅ |
| Appears in PRs | Yes (if committed) | No | No |
| Human-readable on disk | ✅ Files | ❌ JSON blob | ⚠️ Via `git show` |
| Git history | Via normal commits | Per-note | Per-branch commits |
| Branch-switch safe | ❌ (if uncommitted) | ⚠️ | ✅ |
| Easy to inspect | ✅ `cat .squad/...` | ⚠️ `git notes show` | ⚠️ `git show squad-state:...` |
| Sharing across clones | Normal push/pull | Requires notes fetch config | Normal branch push/pull |
| Concurrent-write safe | ✅ (filesystem) | ⚠️ (last writer wins) | ⚠️ (single writer) |

---

## Inspecting State

### Worktree

```bash
cat .squad/decisions.md
ls .squad/skills/
```

### Git Notes

```bash
# Show all state as JSON
git notes --ref=squad show HEAD

# Pretty-print
git notes --ref=squad show HEAD | python -m json.tool
```

### Orphan Branch

```bash
# List all state files
git ls-tree --name-only -r squad-state

# Read a specific file
git show squad-state:decisions.md

# View commit history
git log --oneline squad-state
```

---

## SDK Usage

The state backend is available programmatically via the Squad SDK:

```typescript
import {
resolveStateBackend,
type StateBackend,
} from '@bradygaster/squad-sdk';

// Resolve backend from config + CLI override
const backend: StateBackend = resolveStateBackend(
'.squad', // squadDir
process.cwd(), // repoRoot
'git-notes' // optional CLI override
);

// Use the backend
backend.write('decisions.md', '# Decisions\n...');
const content = backend.read('decisions.md');
const exists = backend.exists('skills/my-skill/SKILL.md');
const entries = backend.list('skills');
```

All backends implement the same `StateBackend` interface:

```typescript
interface StateBackend {
read(relativePath: string): string | undefined;
write(relativePath: string, content: string): void;
exists(relativePath: string): boolean;
list(relativeDir: string): string[];
readonly name: string;
}
```

---

## Security

State backends include hardening against common injection attacks:

- **Path traversal:** `..` segments are rejected
- **Null byte injection:** `\0` characters are rejected
- **Newline injection:** `\n` and `\r` characters are rejected (prevents Git plumbing manipulation)
- **Tab injection:** `\t` characters are rejected (prevents mktree format corruption)
- **Empty segments:** Double slashes (`//`) are rejected

All validation is centralized in `validateStateKey()` and applied uniformly across all backends.

---

## Notes

- State backends are **opt-in** — the default is `worktree` (no behavior change)
- All backends implement the same interface — agents don't know or care which backend is active
- The `external` backend type exists as a stub for future external storage (see [External State](./external-state.md))
- State backends are available in the **insider** release channel (`@bradygaster/squad-cli@insider`)
- 30+ tests cover all backends including security hardening scenarios
Loading