Skip to content

feat: add atomic repository-scoped state creation (#1947) - #1948

Merged
bradygaster merged 11 commits into
github:devfrom
cyrusjamula:u/cyrusjamula/squad/1947-atomic-state-create
Oct 8, 2026
Merged

bradygaster merged 11 commits into
github:devfrom
cyrusjamula:u/cyrusjamula/squad/1947-atomic-state-create

Conversation

@cyrusjamula

Copy link
Copy Markdown

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

  • Adds createIfAbsent to StorageProvider, its implementations, state backends, and the storage adapter.
  • Uses exclusive filesystem creation locally, conditional insert for SQLite, and snapshot-aware Git CAS loops for notes and orphan backends.
  • Exposes exported typed conflict and uncertainty errors; conflict is never returned as success.
  • Makes two-layer disagreement or secondary-layer failure uncertain rather than silently succeeding.
  • Registers squad_state_create_if_absent in ToolRegistry and state MCP so it appears in tools/list.
  • Preserves existing unconditional write behavior.
  • Updates public storage/state documentation, synchronized templates, the Azure Blob sample, and package changesets.

Validation

  • vitest run test/state-backend-create-if-absent.test.ts - 39/39 passed
  • vitest run test/cli/state-mcp.test.ts - 6/6 passed
  • npm run build - passed

Integration

After the release containing this changeset, consumers should update their exact @bradygaster/squad-cli pin, add squad_state_create_if_absent to the MCP allowlist, restart the MCP server, verify the tool appears in tools/list, and use it instead of squad_state_write for exclusive canonical artifact creation. The released version is intentionally determined by the release process rather than hardcoded in this PR.

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 github#1947

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 205b7c5

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 3 item(s) to address before review

Status Check Details
❌ Single commit 11 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 .squad/ or docs/proposals/ files
✅ No merge conflicts No merge conflicts
✅ Copilot threads resolved 1 active Copilot thread(s) resolved (3 outdated skipped)
❌ CI passing 6 check(s) still running

Files Changed (32 files, +1480 −43)

File +/−
.changeset/1947-create-if-absent.md +13 −0
.squad-templates/scribe-charter.md +8 −3
.squad-templates/skills/coordinator-source-of-truth/SKILL.md +1 −1
.squad-templates/spawn-reference.md +5 −0
docs/src/content/docs/features/state-backends.md +124 −1
docs/src/content/docs/features/storage-provider.md +16 −1
packages/squad-cli/src/cli/commands/state-mcp.ts +3 −1
packages/squad-cli/templates/scribe-charter.md +8 −3
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 +282 −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 +23 −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 +69 −1
packages/squad-sdk/templates/scribe-charter.md +8 −3
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 −3
templates/skills/coordinator-source-of-truth/SKILL.md +1 −1
templates/spawn-reference.md +5 −0
test/cli/state-mcp.test.ts +75 −9
test/compat-v041.test.ts +3 −2
test/helpers/typescript-source-loader.mjs +33 −0
test/state-backend-create-if-absent.test.ts +614 −0
test/tools.test.ts +5 −3
workflows/shared/builtins/scribe-charter.md +8 −3
workflows/squad-workflows.manifest.json +1 −1

Total: +1480 −43


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

🟠 Impact Analysis — PR #1948

Risk tier: 🟠 HIGH

📊 Summary

Metric Count
Files changed 32
Files added 3
Files modified 29
Files deleted 0
Modules touched 6
Critical files 2

🎯 Risk Factors

  • 32 files changed (21-50 → HIGH)
  • 6 modules touched (5-8 → HIGH)
  • Critical files touched: packages/squad-sdk/src/storage/index.ts, packages/squad-sdk/src/tools/index.ts

📦 Modules Affected

docs (2 files)
  • docs/src/content/docs/features/state-backends.md
  • docs/src/content/docs/features/storage-provider.md
root (7 files)
  • .changeset/1947-create-if-absent.md
  • samples/storage-provider-azure/azure-blob-storage-provider.ts
  • templates/scribe-charter.md
  • templates/skills/coordinator-source-of-truth/SKILL.md
  • templates/spawn-reference.md
  • workflows/shared/builtins/scribe-charter.md
  • workflows/squad-workflows.manifest.json
squad-cli (4 files)
  • packages/squad-cli/src/cli/commands/state-mcp.ts
  • packages/squad-cli/templates/scribe-charter.md
  • packages/squad-cli/templates/skills/coordinator-source-of-truth/SKILL.md
  • packages/squad-cli/templates/spawn-reference.md
squad-sdk (11 files)
  • packages/squad-sdk/src/state-backend.ts
  • packages/squad-sdk/src/storage/fs-storage-provider.ts
  • packages/squad-sdk/src/storage/in-memory-storage-provider.ts
  • packages/squad-sdk/src/storage/index.ts
  • packages/squad-sdk/src/storage/sqlite-storage-provider.ts
  • packages/squad-sdk/src/storage/storage-error.ts
  • packages/squad-sdk/src/storage/storage-provider.ts
  • packages/squad-sdk/src/tools/index.ts
  • packages/squad-sdk/templates/scribe-charter.md
  • packages/squad-sdk/templates/skills/coordinator-source-of-truth/SKILL.md
  • packages/squad-sdk/templates/spawn-reference.md
templates (3 files)
  • .squad-templates/scribe-charter.md
  • .squad-templates/skills/coordinator-source-of-truth/SKILL.md
  • .squad-templates/spawn-reference.md
tests (5 files)
  • test/cli/state-mcp.test.ts
  • test/compat-v041.test.ts
  • test/helpers/typescript-source-loader.mjs
  • test/state-backend-create-if-absent.test.ts
  • test/tools.test.ts

⚠️ Critical Files

  • packages/squad-sdk/src/storage/index.ts
  • packages/squad-sdk/src/tools/index.ts

This report is generated automatically for every PR. See #733 for details.

@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

🏗️ Architectural Review

Authoritative evidence: base-controlled pull_request_target reporter.

⚠️ Architectural review: 1 warning(s).

Severity Category Finding Files
🟡 warning sweeping-refactor This PR touches 32 files (32 modified/added, 0 deleted). Large PRs are harder to review — consider splitting if possible. —

Automated architectural review — informational only.

@cyrusjamula
cyrusjamula marked this pull request as ready for review August 30, 2026 02:39
Copilot AI lite review requested due to automatic review settings August 30, 2026 02:39

Copilot AI left a comment

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.

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 High severity · 2 Medium severity · 1 Low severity

New issues introduced by this change (4)
Severity Finding
High severity packages/​squad-sdk/​src/​state-backend.ts — WorktreeBackend.createIfAbsent uses fsWriteSync(fd, buf) once; writeSync can legally perform a…
Medium severity test/​state-backend-create-if-absent.test.ts — These "concurrent create" tests for git-backed/two-layer backends are sequential (a simple…
Low severity 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…
Medium severity 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 createIfAbsent on StorageProvider and implements it for FS, in-memory, and SQLite providers; wires it through StateBackendStorageAdapter.
  • 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.

Comment thread packages/squad-sdk/src/state-backend.ts Resolved

@bradygaster bradygaster left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Security & State-Safety Review — PR #1948

Verdict: REQUEST CHANGES (not approved)

I independently verified the diff, built both packages from the PR's fork commit (bdad2c5), and ran the test suites myself rather than trusting the PR body's claims. npm run build passed; vitest run test/state-backend-create-if-absent.test.ts (39/39) and test/cli/state-mcp.test.ts (6/6) passed as claimed. A full-repo vitest run shows 13 pre-existing failures, all in unrelated "externalized state" tests (#1396–#1399) that this PR's diff cannot touch (confirmed: no changes to resolution/export/rc/copilot/cast code) — not a regression from this PR.

The overall design is strong: real OS-level exclusivity (O_EXCL/wx) for FS/local, INSERT OR IGNORE + rows-modified check for SQLite, git-native CAS loops that re-read at the exact pinned ref/tree before retrying (correctly distinguishing "someone else created this key" from "someone else changed something unrelated"), a assertRepositoryIdentity check that fails closed against the git rev-parse --show-toplevel-walks-up-to-enclosing-repo confusion, and an Azure sample using If-None-Match: *. Two-layer disagreement is fail-closed (StateBackendUncertaintyError), not silently reported as success. Template sync across the 3 physical copies of coordinator-source-of-truth/SKILL.md is complete.

However, 4 Copilot review threads on this PR are still unresolved, and I confirmed by reading the source that at least one is a real, unaddressed correctness bug:

  1. [high] Silent truncation / false-success in WorktreeBackend.createIfAbsent — packages/squad-sdk/src/state-backend.ts (~L422-424): after the exclusive openSync(..., 'wx'), the write is fsWriteSync(fd, buf) called once, with no check of the returned byte count and no retry loop. fs.writeSync can return fewer bytes than the buffer length; if that happens here, the call does not throw, createIfAbsent returns success, and the "canonical" artifact is silently truncated/corrupted on disk with no error surfaced to the caller. This directly contradicts the PR's own stated invariant ("never silently succeeds"). Note the async FSStorageProvider.createIfAbsent sibling implementation avoids this by using fh.writeFile(...), which loops to completion — the sync path needs the same treatment (loop until all bytes are written, or use writeFileSync(fd, buf) which handles this internally).
  2. [medium] SQLiteStorageProvider.createIfAbsent doesn't map persistence failures to the typed uncertainty error — packages/squad-sdk/src/storage/sqlite-storage-provider.ts (~L153): this.persist() runs un-guarded after the in-memory INSERT OR IGNORE succeeds. If the on-disk write/rename fails (ENOSPC/EACCES/etc.), the row exists in memory but not on disk, and the raw error propagates as a generic failure instead of StateBackendUncertaintyError — violating the documented typed-error contract callers are told to rely on.
  3. [medium] "Concurrent create" tests for git-notes/orphan/two-layer backends are sequential, not concurrent — test/state-backend-create-if-absent.test.ts (~L144 and the equivalent blocks for GitNotes/Orphan/TwoLayer): these run a plain for loop calling createIfAbsent one at a time on two instances, which passes even for a non-atomic implementation. The CAS-loop logic looks correct on inspection, but the "exactly one of two concurrent creators wins" claim is unverified for the git-native backends — only the FS/InMemory suites use real Promise.all/parallel contention. Needs true parallel execution (worker threads / child processes) or the "concurrent" framing should be removed from the test names.
  4. [low] Docs hardcode a shipping version that contradicts the PR description — docs/src/content/docs/features/state-backends.md (~L297-299) states the feature ships in 0.14.0, while the PR body says the version is intentionally left to the release process. Pick one.

Per review protocol, a verified high-severity correctness gap (item 1) that undermines the PR's core "never silently succeeds" guarantee is blocking. Please fix items 1–2 (and ideally 3), then request re-review.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Friendly nudge — this PR has had no activity for 7 days.

What needs attention:

  • 💬 2 unresolved review thread(s) (2 from Copilot). Address and resolve them.
  • 🔄 Changes requested by @bradygaster. Address their feedback and request re-review.
  • ⬇️ 59 commits behind dev. Rebase to pick up latest changes.

If this PR is abandoned, please close it. If it's blocked on something external, leave a comment so the team knows.
This is an automated check that runs on weekdays. It won't nudge the same PR more than once per week.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Friendly nudge — this PR has had no activity for 8 days.

What needs attention:

  • 💬 2 unresolved review thread(s) (2 from Copilot). Address and resolve them.
  • 🔄 Changes requested by @bradygaster. Address their feedback and request re-review.
  • ⬇️ 86 commits behind dev. Rebase to pick up latest changes.

If this PR is abandoned, please close it. If it's blocked on something external, leave a comment so the team knows.
This is an automated check that runs on weekdays. It won't nudge the same PR more than once per week.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

👋 Friendly nudge — this PR has had no activity for 7 days.

What needs attention:

  • 💬 2 unresolved review thread(s) (2 from Copilot). Address and resolve them.
  • 🔄 Changes requested by @bradygaster. Address their feedback and request re-review.
  • ⬇️ 167 commits behind dev. Rebase to pick up latest changes.

If this PR is abandoned, please close it. If it's blocked on something external, leave a comment so the team knows.
This is an automated check that runs on weekdays. It won't nudge the same PR more than once per week.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d70ed23-b351-4d22-929e-3ff74135ba83
Closes github#1947

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d70ed23-b351-4d22-929e-3ff74135ba83

@bradygaster bradygaster left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified remediation in 8f29358: complete local writes, typed SQLite persistence uncertainty, real child-process CAS coverage, release-derived versioning, and policy-safe Git fixtures. Focused tests, type-checks, package builds, root build, lint, and docs checks pass.

Closes github#1947

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d70ed23-b351-4d22-929e-3ff74135ba83
Closes github#1947

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d70ed23-b351-4d22-929e-3ff74135ba83
Closes github#1947

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d70ed23-b351-4d22-929e-3ff74135ba83
Closes github#1947

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 5d70ed23-b351-4d22-929e-3ff74135ba83
@bradygaster
bradygaster merged commit edd2d9c into github:dev Oct 8, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add atomic repository-scoped create-if-absent state operation

3 participants