-
Notifications
You must be signed in to change notification settings - Fork 444
docs: add user-facing documentation for state backends #996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ``` | ||
|
|
||
| ### 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--state-backendis documented here as working with “any squad command”, but the CLI currently only parses/accepts this flag for thewatch/triagecommand path. Please narrow the wording (or list the supported commands) so readers don’t try it with other subcommands and get unexpected behavior.There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied in
ab256ef.