Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .changeset/docs-state-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@bradygaster/squad-cli': patch
---

Add user-facing documentation for state backends (git-notes, orphan-branch, worktree)
Add user-facing documentation for state backends (local, orphan-branch, two-layer)
10 changes: 5 additions & 5 deletions .changeset/git-state-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

feat(sdk): git-notes + orphan-branch state backends for .squad/

Adds two git-native state storage backends as alternatives to the worktree
and external directory approaches:
Adds git-native state storage backends as alternatives to the local (disk)
approach:

- **git-notes** (`refs/notes/squad`): State stored in git notes ref. Survives
branch switches, invisible in diffs and PRs.
- **orphan-branch** (`squad-state`): Dedicated orphan branch with no common
ancestor. State files never appear in main.
- **two-layer** (notes + orphan): Git notes as best-effort commit annotations
plus orphan branch for durable state. Recommended for teams.

Configure via `.squad/config.json`: `{ "stateBackend": "git-notes" }` or
Configure via `.squad/config.json`: `{ "stateBackend": "two-layer" }` or
the `--state-backend` CLI flag.
21 changes: 21 additions & 0 deletions .changeset/state-backend-global.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@bradygaster/squad-sdk': minor
'@bradygaster/squad-cli': patch
---

Add delete() and append() to StateBackend interface; add resolveSquadState() entry point; add StateBackendStorageAdapter; add git-notes agent protocol templates

- Add `delete()` and `append()` methods to the `StateBackend` interface
- Implement delete/append for all three backends (local, orphan, two-layer)
- Fix orphan backend append to preserve trailing whitespace via readUntrimmed()
- Add `SquadStateContext` interface and `resolveSquadState()` factory in resolution module
- Add `StateBackendStorageAdapter` — bridges StateBackend as a StorageProvider so SDK modules that accept `storage: StorageProvider` work with git-notes and orphan backends
- Add `storage` field to `SquadStateContext` — local uses FSStorageProvider directly, orphan/two-layer use the adapter
- Export `StateBackendStorageAdapter` from SDK public API
- Wire `resolveSquadState()` into CLI entry so the state backend is resolved once at startup
- Pass pre-resolved `stateContext` through to watch config to avoid redundant resolution
- Add `notes-protocol.md` template — agent contract for git-notes state (namespaces, JSON schema, fetch/push, conflict handling)
- Add `scripts/notes/fetch.ps1` template — fetch notes + one-time refspec setup + merge after conflict
- Add `scripts/notes/write-note.ps1` template — agent helper for writing notes with JSON validation and push retry
- Update state-backends docs with "Using with Copilot CLI Sessions" section (copilot-instructions snippet, promotion flow, template index)
- This is the SDK foundation for making state backends squad-wide (Phase 1)
2 changes: 1 addition & 1 deletion .changeset/watch-p0-p1-fixes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
fix(watch): wire missing CLI flags to config, validate --state-backend, fix auth stderr parsing (#834)

- Wire --notify-level, --overnight-start, --overnight-end, --sentinel-file to WatchConfig and loadWatchConfig merge
- Add --state-backend flag with upfront validation against allowed values (worktree, git-notes, orphan, external)
- Add --state-backend flag with upfront validation against allowed values (local, orphan, two-layer, external)
- Fix probeCurrentGhUser() to use `gh api user -q .login` (stdout) instead of parsing `gh auth status` stderr
- Also wire authUser through loadWatchConfig merge logic (was accepted but silently dropped)
147 changes: 135 additions & 12 deletions .github/agents/squad.agent.md

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions .squad-templates/notes-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Squad Notes Protocol

> Contract for agent state via git notes. Agents write commit-scoped context
> here instead of modifying `.squad/` files in PRs.
>
> **Version:** 1.0
> **Backends:** `git-notes`, `orphan`

---

## Overview

Squad state has two layers:

1. **Git notes layer** (this document) — thin, commit-scoped annotations that
attach agent context to commits without appearing in PRs or diffs.
2. **Permanent state layer** — long-lived decisions, routing rules, and archives
stored via the configured state backend (`git-notes` or `orphan` branch).

Agents write notes during their work rounds. Ralph promotes flagged notes to
permanent state after a PR merges.

---

## Namespaces

Each agent writes to its own namespace to prevent conflicts:

| Namespace | Owner | Purpose |
|-----------|-------|---------|
| `refs/notes/squad/data` | Data | Architecture decisions, implementation choices |
| `refs/notes/squad/worf` | Worf | Security reviews, vulnerability assessments |
| `refs/notes/squad/seven` | Seven | Documentation quality, API contract decisions |
| `refs/notes/squad/ralph` | Ralph | Work-round progress, task-state annotations |
| `refs/notes/squad/q` | Q | Devil's advocate findings, risk assessments |
| `refs/notes/squad/research` | Any agent | Research notes that should survive branch deletion |
| `refs/notes/squad/review` | Any agent | Code review context (mirrors Gerrit's pattern) |

**Rule**: Only write to your own namespace. The shared namespaces
(`research`, `review`) use `append` — never `add`.

---

## Note JSON Schema

All notes MUST be valid JSON. Minimum required fields:

```json
{
"agent": "Data",
"timestamp": "2026-03-23T14:00:00Z",
"type": "decision | research | review | progress | security",
"content": "..."
}
```

### Decision notes

```json
{
"agent": "Data",
"timestamp": "2026-03-23T14:00:00Z",
"type": "decision",
"decision": "Use JWT RS256 for auth middleware",
"reasoning": "Existing pattern in codebase — auth.go:47-89.",
"alternatives_considered": ["HS256", "session tokens"],
"confidence": "high",
"promote_to_permanent": true
}
```

Set `"promote_to_permanent": true` to signal Ralph to copy this to
`decisions.md` after the PR merges.

### Research notes

```json
{
"agent": "Data",
"timestamp": "2026-03-23T14:00:00Z",
"type": "research",
"topic": "JWT vs session tokens",
"findings": {},
"effort_hours": 2.5,
"archive_on_close": true
}
```

Set `"archive_on_close": true` to signal Ralph to archive this to
`state/research/` even if the PR is rejected.

---

## Write Commands

```bash
# Write a decision note on the current commit
git notes --ref=squad/{your-agent} add \
-m '{"agent":"{Agent}","timestamp":"...","type":"decision","decision":"..."}' \
HEAD

# Append to an existing note (multiple items on same commit)
git notes --ref=squad/{your-agent} append \
-m '{"agent":"{Agent}","timestamp":"...","type":"progress","content":"..."}' \
HEAD

# Read your note
git notes --ref=squad/{your-agent} show HEAD

# List all commits with notes in your namespace
git notes --ref=squad/{your-agent} list
```

Or use the helper script:

```powershell
./scripts/notes/write-note.ps1 -Agent data -Type decision \
-Content '{"decision":"Use JWT","reasoning":"..."}' \
[-Commit HEAD] [-Promote] [-Archive]
```

---

## Fetch / Push

**Notes are NOT fetched or pushed by default.** Every clone needs setup.

### One-time setup

```bash
git config --add remote.origin.fetch 'refs/notes/*:refs/notes/*'
git fetch origin 'refs/notes/*:refs/notes/*'
```

Or use the helper:

```powershell
./scripts/notes/fetch.ps1 -Setup
```

### Every work round

1. **Start**: `git fetch origin 'refs/notes/*:refs/notes/*'`
2. **End**: `git push origin 'refs/notes/*:refs/notes/*'`

---

## Conflict Handling

1. **Per-agent namespaces prevent 99% of conflicts.** Only one agent writes to
`refs/notes/squad/data`, so there are no write conflicts in normal use.

2. **Same agent, two machines:** First push wins. Losing machine should fetch
and append:
```bash
git fetch origin 'refs/notes/*:refs/notes/*'
git notes --ref=squad/{agent} append -m '{...}' HEAD
git push origin 'refs/notes/*:refs/notes/*'
```

3. **Shared namespaces** (`research`, `review`): Always use `git notes append`,
never `git notes add`.

4. **Push conflict recovery:**
```bash
git fetch origin 'refs/notes/*:refs/notes/*'
git notes merge refs/notes/remotes/origin/squad/{namespace}
git push origin 'refs/notes/*:refs/notes/*'
```

---

## When to Use Notes vs State Backend

| Use git notes | Use state backend |
|---------------|-------------------|
| Why THIS choice on THIS commit | Universal routing rules, conventions |
| Decisions scoped to a feature | Long-lived decisions for all future work |
| Research for a specific investigation | Research archives (promoted from notes) |
| Security sign-offs per commit | Agent history persisting across features |
| Agent-to-agent context for current feature | Team agreements and policies |

When in doubt: **notes first, promote to permanent state later.** Ralph handles
the promotion automatically when `promote_to_permanent` is set.

---

## Ralph Promotion Rules

**After PR merge:**

1. Fetch all notes from remote
2. Traverse commits reachable from the default branch that have notes
3. For each note with `"promote_to_permanent": true` → append to `decisions.md`
4. Push state

**After PR close/rejection:**

1. List notes in `squad/research` on the closed branch's commits
2. For each note with `"archive_on_close": true` → archive to `research/`
3. Push state
4. Notes on rejected commits are NOT promoted — this is the desired behavior
96 changes: 95 additions & 1 deletion .squad-templates/scribe-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,59 @@

**Worktree awareness:** Use the `TEAM ROOT` provided in the spawn prompt to resolve all `.squad/` paths. If no TEAM ROOT is given, run `git rev-parse --show-toplevel` as fallback. Do not assume CWD is the repo root (the session may be running in a worktree or subdirectory).

**State backend awareness:** Check `STATE_BACKEND` from the spawn prompt. If it's `"orphan"` or `"git-notes"`, run the **State Leak Guard** before any other work.

### State Leak Guard (orphan/git-notes backends only)

Before logging or merging, check if any agent accidentally committed state files to the working branch:

```powershell
# Check if state files are staged or committed but shouldn't be
$stateFiles = @(
'.squad/decisions.md',
'.squad/decisions-archive.md'
)
$statePatterns = @(
'.squad/agents/*/history.md',
'.squad/agents/*/history-archive.md',
'.squad/log/*',
'.squad/orchestration-log/*',
'.squad/decisions/inbox/*'
)

# 1. Check git status for accidentally staged state files
$dirty = git status --porcelain | Where-Object { $_.Length -gt 3 } | ForEach-Object {
$_.Substring(3) -replace '^.* -> ',''
} | Where-Object {
$f = $_
($f -in $stateFiles) -or ($statePatterns | Where-Object { $f -like $_ })
}

if ($dirty) {
# Unstage any accidentally added state files
$dirty | ForEach-Object { git reset HEAD -- $_ 2>$null }
# Restore from HEAD (discard working tree changes for state files)
$dirty | ForEach-Object { git checkout HEAD -- $_ 2>$null }
}

# 2. Check if the most recent commit on this branch has state files
$lastCommitFiles = git diff-tree --no-commit-id --name-only -r HEAD 2>$null
$leakedInCommit = $lastCommitFiles | Where-Object {
$f = $_
($f -in $stateFiles) -or ($statePatterns | Where-Object { $f -like $_ })
}

if ($leakedInCommit) {
# State files leaked into the last commit — amend to remove them
$leakedInCommit | ForEach-Object { git rm --cached -- $_ 2>$null }
git commit --amend --no-edit 2>$null
}
```

If any files were cleaned, log: `⚠️ State leak guard: removed {N} state file(s) from working branch.`

After the guard, proceed with normal Scribe work (but persist state via the configured backend, not the working branch).

After every substantial work session:

1. **Log the session** to `.squad/log/{timestamp}-{topic}.md`:
Expand Down Expand Up @@ -57,9 +110,50 @@ After every substantial work session:
```

5. **Commit `.squad/` changes:**
**Check `STATE_BACKEND` from spawn prompt.** This determines WHERE state gets committed.

**IMPORTANT — Windows compatibility:** Do NOT use `git -C {path}` (unreliable with Windows paths).
Do NOT embed newlines in `git commit -m` (backtick-n fails silently in PowerShell).
Instead:

**If STATE_BACKEND is "orphan":**
State files must be committed to the `squad-state` orphan branch, NOT the working branch.
- Identify changed `.squad/` state files via `git status --porcelain` filtered to allowed paths.
- For each file, use git plumbing to write to the orphan branch:
```powershell
# Create a temporary worktree for the orphan branch
$orphanWt = Join-Path ([System.IO.Path]::GetTempPath()) "squad-state-$(Get-Random)"
git worktree add $orphanWt squad-state 2>$null
if ($LASTEXITCODE -ne 0) { git worktree add --orphan $orphanWt squad-state }
# Copy state files to orphan worktree
$filesToSync | ForEach-Object {
$dest = Join-Path $orphanWt $_
New-Item -ItemType Directory -Path (Split-Path $dest) -Force | Out-Null
Copy-Item $_ $dest -Force
}
# Commit in orphan worktree
Push-Location $orphanWt
git add .squad/
git diff --cached --quiet
if ($LASTEXITCODE -ne 0) {
$msgFile = [System.IO.Path]::GetTempFileName()
Set-Content -Path $msgFile -Value "docs(ai-team): $summary" -Encoding utf8
git commit -F $msgFile
Remove-Item $msgFile
git push origin squad-state
}
Pop-Location
git worktree remove $orphanWt --force
```
- After committing to orphan, reset working tree state files: `git checkout HEAD -- .squad/`
- ⚠️ NEVER commit `.squad/` state files to the working branch when using orphan backend.

**If STATE_BACKEND is "git-notes":**
State is already persisted in git notes refs by agents. Scribe only needs to:
- Push any locally created note refs: `git push origin 'refs/notes/squad/*'`
- Commit decisions.md (the merged canonical file) to the working branch as normal.

**If STATE_BACKEND is "worktree" (default):**
Commit to the working branch as normal:
- `cd` into the team root first.
- Stage only files Scribe actually modified in this session.
Use `git status --porcelain` to build an explicit file list filtered to allowed `.squad/` paths:
Expand Down
Loading
Loading