feat: add atomic repository-scoped state creation (#1947) - #1948
feat: add atomic repository-scoped state creation (#1947)#1948cyrusjamula wants to merge 2 commits into
Conversation
Add true atomic create-if-absent semantics across all state backends (local/WorktreeBackend, git-notes/GitNotesBackend, orphan-branch/ OrphanBranchBackend, two-layer/TwoLayerBackend), all StorageProvider implementations (FSStorageProvider, InMemoryStorageProvider, SQLiteStorageProvider), StateBackendStorageAdapter, ToolRegistry (squad_state_create_if_absent), state-mcp MCP bridge, and Azure Blob sample provider. Key guarantees: - Exactly one concurrent creator succeeds; all others receive StateKeyConflictError (typed, exported from squad-sdk) - Existing content is never overwritten - Repository scope is verified at operation time; invalid/ambiguous identity fails with StateBackendUncertaintyError (never silently widens) - Two-layer disagreement/failure is fail-closed: orphan succeeds but notes disagrees => StateBackendUncertaintyError, never success-shaped conflict - Local backend uses O_CREAT|O_EXCL (no process-local locking needed) - Git-native backends use compare-and-swap loops with re-check after CAS loss; retry exhaustion is uncertainty, not conflict - Repeated legitimate conflicts never trip the circuit breaker Registered in: StorageProvider interface, every implementation/adapter, ToolRegistry, state-mcp MCP_TOOL_ALIASES + tools/list, public docs, changeset. Tests: 39 createIfAbsent tests (WorktreeBackend, GitNotesBackend, OrphanBranchBackend, TwoLayerBackend, FSStorageProvider, InMemoryStorageProvider, SQLiteStorageProvider, ToolRegistry) + 6 state-mcp bridge tests including squad_state_create_if_absent MCP surface. Closes bradygaster#1947 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 2 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | |
| ✅ | No merge conflicts | No merge conflicts |
| ❌ | Copilot threads resolved | 2 unresolved Copilot thread(s) — fix and resolve before merging |
| ❌ | CI passing | 6 check(s) still running |
Files Changed (26 files, +1323 −13)
| File | +/− |
|---|---|
.changeset/1947-create-if-absent.md |
+13 −0 |
.squad-templates/scribe-charter.md |
+8 −1 |
.squad-templates/spawn-reference.md |
+5 −0 |
.squad/skills/coordinator-source-of-truth/SKILL.md |
+1 −1 |
docs/src/content/docs/features/state-backends.md |
+123 −0 |
docs/src/content/docs/features/storage-provider.md |
+16 −1 |
packages/squad-cli/src/cli/commands/state-mcp.ts |
+1 −0 |
packages/squad-cli/templates/scribe-charter.md |
+8 −1 |
packages/squad-cli/templates/skills/coordinator-source-of-truth/SKILL.md |
+1 −1 |
packages/squad-cli/templates/spawn-reference.md |
+5 −0 |
packages/squad-sdk/src/state-backend.ts |
+275 −1 |
packages/squad-sdk/src/storage/fs-storage-provider.ts |
+45 −2 |
packages/squad-sdk/src/storage/in-memory-storage-provider.ts |
+12 −0 |
packages/squad-sdk/src/storage/index.ts |
+1 −1 |
packages/squad-sdk/src/storage/sqlite-storage-provider.ts |
+15 −0 |
packages/squad-sdk/src/storage/storage-error.ts |
+43 −0 |
packages/squad-sdk/src/storage/storage-provider.ts |
+22 −0 |
packages/squad-sdk/src/tools/index.ts |
+68 −0 |
packages/squad-sdk/templates/scribe-charter.md |
+8 −1 |
packages/squad-sdk/templates/skills/coordinator-source-of-truth/SKILL.md |
+1 −1 |
packages/squad-sdk/templates/spawn-reference.md |
+5 −0 |
samples/storage-provider-azure/azure-blob-storage-provider.ts |
+32 −1 |
templates/scribe-charter.md |
+8 −1 |
templates/spawn-reference.md |
+5 −0 |
test/cli/state-mcp.test.ts |
+47 −0 |
test/state-backend-create-if-absent.test.ts |
+555 −0 |
Total: +1323 −13
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
🟠 Impact Analysis — PR #1948Risk tier: 🟠 HIGH 📊 Summary
🎯 Risk Factors
📦 Modules Affecteddocs (2 files)
root (4 files)
squad-cli (4 files)
squad-sdk (11 files)
squad-state (1 file)
templates (2 files)
tests (2 files)
|
|
🏗️ Architectural Review
Automated architectural review — informational only. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
There are correctness/contract gaps to address (notably around atomic local writes and typed uncertainty behavior), plus at least one concurrency test that doesn’t actually exercise concurrent contention.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (4)
| Severity | Finding |
|---|---|
packages/squad-sdk/src/state-backend.ts — WorktreeBackend.createIfAbsent uses fsWriteSync(fd, buf) once; writeSync can legally perform a… |
|
test/state-backend-create-if-absent.test.ts — These "concurrent create" tests for git-backed/two-layer backends are sequential (a simple… |
|
docs/src/content/docs/features/state-backends.md — This docs section hardcodes the first shipping version as 0.14.0, but the PR description states the… |
|
packages/squad-sdk/src/storage/sqlite-storage-provider.ts — SQLiteStorageProvider.createIfAbsent calls this.persist() without translating persistence failures… |
What changed in this PR
Adds an atomic, repository-scoped “create-if-absent” state primitive across Squad’s storage/state stack, exposing it via MCP as squad_state_create_if_absent with typed conflict vs uncertainty outcomes to prevent concurrent creators from overwriting canonical artifacts.
Changes:
- Introduces
createIfAbsentonStorageProviderand implements it for FS, in-memory, and SQLite providers; wires it throughStateBackendStorageAdapter. - Implements backend-specific atomic creation semantics for local, git-notes, orphan-branch, and two-layer state backends (including fail-closed uncertainty on two-layer disagreement).
- Registers the new MCP tool, adds tests, and updates docs/templates/samples to describe usage and failure shapes.
| File | Description |
|---|---|
| test/state-backend-create-if-absent.test.ts | Adds cross-backend tests for create-if-absent semantics, conflicts, isolation, and uncertainty. |
| test/cli/state-mcp.test.ts | Verifies state-mcp exposes squad_state_create_if_absent and exercises canonical-key creation behavior. |
| templates/spawn-reference.md | Documents when to use squad_state_create_if_absent and how to handle conflict/uncertainty. |
| templates/scribe-charter.md | Updates Scribe guidance to prefer create-if-absent for exclusive canonical artifacts. |
| samples/storage-provider-azure/azure-blob-storage-provider.ts | Adds an Azure Blob sample implementation using conditional upload for atomic create. |
| packages/squad-sdk/templates/spawn-reference.md | Syncs spawn-reference template with create-if-absent guidance. |
| packages/squad-sdk/templates/skills/coordinator-source-of-truth/SKILL.md | Updates state-tool list to include create-if-absent for mutable state. |
| packages/squad-sdk/templates/scribe-charter.md | Syncs Scribe charter template with exclusive-canonical-artifact guidance. |
| packages/squad-sdk/src/tools/index.ts | Registers squad_state_create_if_absent tool with typed failure shapes. |
| packages/squad-sdk/src/storage/storage-provider.ts | Extends StorageProvider interface with createIfAbsent and its error contract. |
| packages/squad-sdk/src/storage/storage-error.ts | Adds exported typed errors: StateKeyConflictError and StateBackendUncertaintyError. |
| packages/squad-sdk/src/storage/sqlite-storage-provider.ts | Implements createIfAbsent via INSERT OR IGNORE and conflict detection. |
| packages/squad-sdk/src/storage/index.ts | Re-exports new typed storage errors from the SDK storage entrypoint. |
| packages/squad-sdk/src/storage/in-memory-storage-provider.ts | Implements createIfAbsent for test-friendly in-memory provider. |
| packages/squad-sdk/src/storage/fs-storage-provider.ts | Implements createIfAbsent using exclusive open (wx) with typed conflict/uncertainty. |
| packages/squad-sdk/src/state-backend.ts | Adds createIfAbsent to backends + repo-identity fail-closed checks for git-native backends. |
| packages/squad-cli/templates/spawn-reference.md | Syncs CLI template spawn-reference with create-if-absent guidance. |
| packages/squad-cli/templates/skills/coordinator-source-of-truth/SKILL.md | Syncs CLI skill template to include create-if-absent in state tools. |
| packages/squad-cli/templates/scribe-charter.md | Syncs CLI template Scribe charter with exclusive-canonical-artifact guidance. |
| packages/squad-cli/src/cli/commands/state-mcp.ts | Adds alias mapping so state-mcp bridge exposes the new tool name. |
| docs/src/content/docs/features/storage-provider.md | Documents createIfAbsent as a core StorageProvider method and its semantics. |
| docs/src/content/docs/features/state-backends.md | Documents atomic create-if-absent semantics, typed errors, and MCP tool surface. |
| .squad/skills/coordinator-source-of-truth/SKILL.md | Updates canonical skill content to include create-if-absent among state tools. |
| .squad-templates/spawn-reference.md | Syncs canonical spawn template with create-if-absent guidance. |
| .squad-templates/scribe-charter.md | Syncs canonical Scribe template with exclusive-canonical-artifact guidance. |
| .changeset/1947-create-if-absent.md | Declares a minor bump for sdk/cli and summarizes the new capability. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| try { | ||
| const buf = Buffer.from(content, 'utf-8'); | ||
| fsWriteSync(fd, buf); | ||
| } catch (writeErr: unknown) { |
| it('exactly one of two sequential instances succeeds (concurrent create)', () => { | ||
| const b1 = new GitNotesBackend(TMP); | ||
| const b2 = new GitNotesBackend(TMP); | ||
| let successes = 0; | ||
| let conflicts = 0; |
| `createIfAbsent` and the `squad_state_create_if_absent` MCP tool first ship in | ||
| `@bradygaster/squad-sdk` and `@bradygaster/squad-cli` **0.14.0** (the next minor | ||
| release after 0.13.1). Consumers pinned to `0.12.0` or `0.13.x` must upgrade the | ||
| pin to `0.14.0`, add `squad_state_create_if_absent` to their MCP tool allowlist, | ||
| and restart the state MCP server before the tool appears in `tools/list`. |
| if (db.getRowsModified() === 0) { | ||
| throw new StateKeyConflictError(filePath); | ||
| } | ||
| this.persist(); | ||
| } |



Closes #1947
Why
State MCP currently exposes unconditional writes, so concurrent workflows cannot safely claim a canonical repository-scoped artifact without risking overwrite. This adds a true create-if-absent primitive that fails closed when identity or outcome cannot be proven.
Approach
createIfAbsenttoStorageProvider, its implementations, state backends, and the storage adapter.squad_state_create_if_absentin ToolRegistry and state MCP so it appears intools/list.writebehavior.Validation
vitest run test/state-backend-create-if-absent.test.ts- 39/39 passedvitest run test/cli/state-mcp.test.ts- 6/6 passednpm run build- passedIntegration
After the release containing this changeset, consumers should update their exact
@bradygaster/squad-clipin, addsquad_state_create_if_absentto the MCP allowlist, restart the MCP server, verify the tool appears intools/list, and use it instead ofsquad_state_writefor exclusive canonical artifact creation. The released version is intentionally determined by the release process rather than hardcoded in this PR.