Skip to content

SDD Storage

The workspace provider owns SDD placement. Resolve project-owned storage through repository roles:

sase repo path plans
sase repo path research
sase repo path designs
sase repo path beads
sase repo path designs --ensure

sase repo path is read-only unless -e/--ensure is passed. --ensure clones or synchronizes the selected sidecar. Launched agents receive SASE_SDD_DIR, SASE_SDD_PLANS_DIR, SASE_SDD_BEADS_DIR, and one SASE_SDD_<ROLE>_DIR variable for every configured document sidecar (role punctuation becomes _). Thus SASE_SDD_RESEARCH_DIR exists only when the research role exists. In split layouts, SASE_SDD_BEADS_DIR is the dedicated beads sidecar root once a project records one, and ${SASE_SDD_PLANS_DIR}/beads for a project that has not been migrated yet.

Resolved Layouts

Resolved layout Plans root Meaning
in_tree {workspace}/sdd The code repository. Built-in bare-git projects use this provider policy.
separate_repo {workspace}/.sase/sdd One provider sidecar. This is GitHub's declared policy and the recorded legacy layout.
sidecar_repos {workspace}/sase/repos/plans A recorded split layout created by managed GitHub initialization.
local {primary}/.sase/sdd A primary-workspace fallback for providerless projects or providers with no SDD policy.

Provider policy and resolved layout are related but different. A GitHub provider declares separate_repo; explicit initialization resolves that requirement into configured sidecars and records sidecar_repos. The latter is a materialized store-record value, not another provider policy. Before initialization, or for an unmigrated legacy record, GitHub still resolves as separate_repo.

A positive materialized-store record at {primary}/.sase/sdd-store.json is authoritative, including while offline. Old negative records are not policy and are retried at the next materialization attempt.

Sidecar roles

Initialized managed GitHub projects use a store record with storage: sidecar_repos. For compatibility, the record contains a role-keyed sidecars map and resolved remotes. That record—not clone or remote existence—is the layout authority. Legacy records continue to use the single-root layout unchanged.

Five role names are reserved: plans is the canonical plan corpus, beads is the bead event store, agents is hidden machine-level agent data plus the canonical prompt and prompt-artifact archive, attachments is the hidden bare store for public bead attachment bytes, and attachments-private is the hidden bare store for private bead attachment bytes. beads, agents, attachments, and attachments-private are never document roles. Every other enabled repos.sidecar role is a document sidecar: a month-sharded Markdown corpus labeled by its role name. A document role such as designs receives its own clone and store root, sase repo path resolution, doctor checks, commit routing, agent environment variable, plan-search kind, and sase's TUI Plans kind. research is only the document role seeded by default; it has no storage-level privilege. The shipped research README and directory map remain an optional presentation preset, while other document roles receive the generic README.

The record also carries an optional beads sidecar, and its presence selects the schema version:

Schema version sidecars.beads Bead state resolves to
3 recorded <workspace>/sase/repos/beads
2 absent <workspace>/sase/repos/plans/beads

The schema version is derived from content, not blanket-bumped: a project that has not adopted a beads sidecar keeps writing a schema-2 record and resolving bead state inside the plans clone exactly as before. A beads entry is rejected below schema version 3, and a schema-3 record is rejected by an older sase install with the usual "upgrade sase" error—so the new build must be installed on every machine that touches a project before that project is migrated.

The plans sidecar keeps monthly plan directories at its root (<YYYYMM>/*.md). Canonical committed prompts are stored in the hidden agents sidecar at prompts/<YYYYMM>/*.md; copied prompt-linked bytes live in that sidecar's content-addressed files/objects/sha256/<hex-prefix>/<sha256> object store. Every other document sidecar likewise keeps <YYYYMM>/ directories at its root. The beads sidecar keeps bead state at its repository root—config.json, metadata.json, and events/—plus generated bead pages under pages/, so its clone root is itself the bead directory. In an event store, issues.jsonl is a local, gitignored export created on demand with sase bead export; legacy stores without events still use it as their tracked state. The SQLite read model is also local to each clone and rebuildable from events. See bead storage. Kind resolution is therefore:

Kind Resolved path
plans <workspace>/sase/repos/plans
agents ~/.sase/projects/<project-key>/repos/agents
attachments ~/.sase/projects/<project-key>/repos/attachments (bare clone)
attachments-private ~/.sase/projects/<project-key>/repos/attachments-private (bare clone)
<document-role> <workspace>/sase/repos/<document-role>
beads <workspace>/sase/repos/beads (schema 2: .../repos/plans/beads)

Once a project is migrated, the plans clone no longer owns bead state; its bead history stays behind only as an archive. Because the cooperative write lock and repository-health preflight are keyed on the repository root, bead writes and plan writes no longer contend, and a wedged bead rebase no longer blocks plan commits or epic approval.

sase bead materializes the beads sidecar on demand: when the store record names one and sase/repos/beads is missing or has a mismatched origin, the command clones it before reading or writing, and reports an error naming the repository and remote if that clone cannot be made usable. A project with a schema-2 record clones nothing extra. Because a beads sidecar is injected into the default linked-repo set for every managed project, but the remote only exists after adoption, the beads role defaults to auto_clone: false and stays inventory-visible even before adoption, materializing only through the on-demand path above. A project that opts a beads entry into auto_clone: true still respects the pre-adoption gate: SASE reports auto_clone: true only once the store record actually names a beads sidecar, so an unmigrated project's beads role is never auto-cloned even under an explicit override.

Initialization clones, initializes, and pushes every configured sidecar in the workspace where it runs. After that, normal numbered-workspace preparation evicts the complete sase/repos/ tree and clones plans directly from its recorded remote. When the primary workspace already has a matching clone of a sidecar, the new clone borrows its objects to shorten the transfer (see Network Git Operations). A newly prepared workspace leaves the beads role and ordinary document sidecars lazy unless auto_clone: true; a consumer can materialize one with sase repo path <role> --ensure, or let sase bead and the agent-launch bead claim materialize beads on first use. GitHub HTTPS values in legacy records resolve in memory to canonical SSH (git@host:owner/repo.git, or ssh://git@host:port/owner/repo.git) before inventory, launch, retained-clone synchronization, or on-demand materialization consumes them. Each clone's origin is that resolved SSH or local remote; a retained matching HTTPS clone is rewritten in place without losing local state. Other HTTP(S) values fail before Git executes. This read-time normalization does not rewrite .sase/sdd-store.json; rerun sase repo init to persist the migration, but launches are safe without doing so. Pull-with-rebase applies when synchronizing a retained existing clone; a sidecar freshly cloned for launch is used without a redundant pull or rebase.

The retired sdd.storage and sdd.version_controlled configuration keys no longer select a mode. SASE ignores and strips them before schema validation, and sase doctor reports where to remove them. This keeps old configuration files loadable without allowing project or user config to override provider policy.

GitHub Sidecar Repositories

Managed GitHub projects seed public <owner>/<repo>--plans, <owner>/<repo>--research, and <owner>/<repo>--beads sidecars by default, writing their project-local repos.sidecar declarations when absent. Additional sidecars are declared under repos.sidecar, and any entry can pin repo: to override the derived <owner>/<repo>--<name> convention. Configured sidecars are prepared by initialization in the current workspace. In later workspaces, only the plans clone is automatic; the beads role and ordinary document roles materialize according to auto_clone or on demand. The provider still supports <owner>/<repo>--sdd discovery and sdd.repo.name overrides for unmigrated legacy stores.

Set is_sase_managed: true in the repository's own sase/sase.yml, then run sase repo init to create or connect the provider store and refresh generated SDD guides. Without that local marker, explicit init and --check skip before provider work. The #gh setup step also materializes the sidecar before claiming and launching work. Authentication, authorization, network, discovery, creation, label, clone, import, or initial-push failures stop setup; GitHub projects do not fall back to local storage.

Explicit sase repo init (including sase init repo and bare-onboarding dispatch) probes GitHub before materialization. If the sidecar is absent, creation requires a fresh interactive y/yes response to a prompt naming its host, repository, and public visibility; the default is no. Non-interactive input and sase init --yes cannot grant this resource-specific authorization. Existing sidecars and non-explicit materialization consumers retain their normal provider-owned behavior.

First Launch On A New Machine

A sase-managed project whose sidecars already exist remotely does not need an explicit sase repo init on every machine. When the first agent launches into a workspace that has no materialized store record, SASE connects this machine to the project's existing store and prints Connected existing SDD sidecars for first use on this machine. The step is remote-create-free: it clones or adopts repositories that already exist and records the local materialization, but never creates a missing remote repository. It is a no-op when the project is not sase-managed, is not a project directory, already has a materialized record, or uses a provider policy that is not remote-backed.

A required sidecar that does not exist remotely stops the launch with the remedy named directly — run sase repo init in the project root to create it — instead of an opaque clone failure. The agents sidecar is the one exception: a missing agents repository prints a warning naming it and continues with the remaining roles, because creating it needs the interactive authorization described above.

Split initialization is a single record-last transaction:

  1. SASE serializes setup and preflights every enabled configured sidecar repository.
  2. The provider creates or adopts each repository and SASE clones it at the linked-repository location.
  3. SASE writes deterministic per-repository README and infographic assets, then commits and pushes generated drift. The beads clone is seeded with a root-level beads.db* ignore rule; an un-migrated plans clone keeps its beads/-prefixed one.
  4. SASE adopts any bead state still living in the plans clone (see below).
  5. Only after the configured compatibility roles succeed does SASE write the split store record—schema version 3 when a beads sidecar was recorded, schema version 2 otherwise.

Bead State Adoption

sase repo init moves bead state out of the plans sidecar. The move is rerunnable and idempotent, and sase repo init --check reports it as a distinct planned action (adopt bead state from the plans sidecar) so a dry run tells you a data move is pending.

  1. Adoption is a no-op when the plans clone has no beads/ directory, or when the beads clone already holds bead state.
  2. Otherwise every entry of <plans>/beads/ is copied to the beads clone root, excluding the local beads.db, beads.db-shm, and beads.db-wal flock files. A minimal store of only config.json and issues.jsonl is valid and copies cleanly.
  3. The copy is committed to the beads clone as Import bead state from <plans-repo>@<sha> and pushed. A failed push aborts adoption, so the record is never written against bead state that exists only locally.
  4. The schema-3 record is written. This is the switch: bead commands now resolve to the beads sidecar.
  5. Only afterwards is beads/ removed from the plans clone, along with its beads/beads.db* ignore lines, and committed as Move bead state to the beads sidecar. A failure here is a warning, not a command failure—the authoritative switch already happened, and the next sase repo init cleans up the duplicate.

Adoption is therefore reversible until step 4: everything before the record write leaves a complete but unreferenced beads repository and a fully working project. Bead history is not filtered out of the plans repository; it stays there as an archive, and the event store remains the real history source for sase bead history.

Reads, Writes, and Offline Use

Directory-only consumers resolve paths without network or filesystem writes. Operations that write SDD data—such as prompt export, bead initialization and mutation, and link repair with --write—materialize a provider-required store first and fail if it cannot be made usable.

Once a positive record exists, reads from an existing clone work offline. A legacy single-root (separate_repo) workspace clone is copied from the primary workspace's .sase/sdd/ clone and can be created offline; a split sidecar clone always takes its refs from the recorded remote, so creating one needs that remote. Refresh pulls are best effort. Separate-repository commits remain local if an ordinary follow-up push fails so they can be inspected and pushed manually; only the initial adoption push is transactional.

sdd.push_after_commit controls pushes after later SDD commits: async starts a detached background push, true pushes synchronously, and false skips the push.

Network Git Operations

SDD git clone, git fetch, and git push run with Git progress enabled so SASE can tell a slow transfer from a hung one; the progress output is captured, not printed. A transfer is aborted only after it makes no progress for the network timeout or runs past the absolute transfer ceiling, so a large but healthy clone is not cut off at a fixed wall-clock limit. Other SDD Git commands keep wall-clock timeouts.

Store integration, repository-health, and recovery fetches, bead-sync pushes, and agents-sidecar publication retry up to three attempts, waiting 0.25 and then 1 second (or a longer delay the failure classifier reports), when an attempt timed out or failed with a transport error that the Rust core classifies as transient. A retry never starts after the caller's deadline has passed.

Sidecar clones have their own bounded retry loop:

  • In a numbered workspace, SASE first clones with the primary workspace's clone of the same sidecar as an object reference (git clone --reference-if-able ... --dissociate) when that clone exists and has the same origin. Refs still come from the recorded remote, and the new clone copies what it borrowed, so it never depends on the primary clone afterward. If that attempt fails or times out, SASE immediately retries without the reference.
  • A clone makes at most four attempts in total, waiting 0.25, 1, and 2 seconds between them. Each retry allows 50% more idle time than the first attempt (120, 180, 240, and 300 seconds by default), within any deadline the caller sets. Partial output is removed before every retry.
  • Timeouts and failures classified as transient network or transport errors are retried. Permanent failures, such as a missing repository or rejected credentials, stop immediately.
  • Clones from hosted remotes such as GitHub take a machine-wide clone permit (a lock under the managed temp root's sdd-remote-clone-pool/), so concurrent agent launches clone sidecars one at a time by default. The permit is held only while Git runs.

If an agent launch still cannot clone a sidecar for a transient reason, the launch fails but releases its workspace claim instead of holding the workspace for dismissal.

SDD fetches and pushes, failed commands, and commands slower than the slow-command threshold are logged to ~/.sase/logs/tui_git_ops.jsonl with their duration and limit; clone records also note the attempt number and whether an object reference was used. The vcs.git_transport_margin check in sase doctor reads the 20 most recent network records and warns when at least two of them, and at least 40% overall, used 85% or more of their limit. With -v, the warning lists the affected sidecar stores, their durations, and how many samples ran with or without a reference. Repeated near-limit samples usually mean cold clones or a degraded network path.

Variable Default Meaning
SASE_SDD_GIT_NETWORK_TIMEOUT 120 Seconds without progress before a transfer is aborted
SASE_SDD_GIT_NETWORK_TRANSFER_CEILING 900 Absolute seconds a clone, fetch, or push may run
SASE_SDD_GIT_LOCAL_TIMEOUT 30 Wall-clock seconds for other SDD Git commands
SASE_SDD_GIT_SLOW_MS 1000 Duration at which a successful command is logged
SASE_SDD_REMOTE_CLONE_CONCURRENCY 1 Machine-wide concurrent hosted-remote sidecar clones
SASE_TUI_GIT_OPS_PATH (unset) Alternate Git operation log, also read by the doctor check

Zero, negative, or unparseable values fall back to the defaults.

Concurrency and Recovery

SASE serializes cooperating SDD writers with a lock in the store repository's Git directory and retries transient Git index-lock errors. Short Git-directory metadata writes wait up to 10 seconds by default. Operations that can mutate the shared worktree—bead mutation, sync, commit, mutation health preflight, integration, and recovery—wait up to 180 seconds and abort without changing the worktree if the lock remains unavailable. Transactional integration reports that contention as a busy-but-healthy outcome, so it cannot authorize destructive recovery. SASE_SDD_STORE_WRITE_LOCK_TIMEOUT sets one non-negative override for both wait bounds, and SASE_SDD_GIT_LOCK_RETRY_DELAYS supplies comma-separated per-command retry delays.

Epic approvals for one project additionally serialize on a primary-workspace-keyed lock under ~/.sase/locks/epic-plan-launches/. The lock covers sidecar materialization as well as plan archiving and bead writes. If a host approval preflight cannot acquire a contended lock within its shorter wait bound, it defers the store health check to the detached launch, which repeats that check while holding the lock, instead of failing the approval.

Managed Git commands disable rerere and rerere.autoupdate, so a user's ambient Git configuration cannot replay a cached textual conflict resolution over SASE's semantic bead merge. During a rebase SASE also resolves conflicts it can prove are generated: bead event state, artifact-link indexes and managed ## Links / ## Referenced By blocks, and the AGENTS / COMMITS rows of a month-sharded plan's generated header (see Artifact Links). Any other conflict, including authored plan text, is not merged. Ordinary transactional integration restores the pre-rebase state after a failed rebase and refuses unsafe or unprovable recovery. A Git command that hits its timeout mid-integration gets the same treatment: the in-progress rebase is aborted and the rollback to the locked starting state is verified, so a slow remote cannot leave the checkout wedged mid-rebase.

The background bead sync worker adds one more rollback. Before publishing, it checks that its unpublished commits do not drop or rewrite bead events already on the upstream. When that check rejects the integrated HEAD, or integration times out, the worker pins the rejected HEAD under refs/sase/recovery/, restores the pre-integration HEAD (aborting any in-progress rebase), and reports the sync as failed with a summary of the rollback; the full detail is in the sync log. A stream that holds every published event but in a different order — the shape an older timestamp-sorting merge wrote — is not treated as dropping events, so clones wedged by that merge integrate and publish their pending changes.

Machine-managed disposable sidecar clones have one additional recovery path for a wedged checkout. Before resetting to the configured upstream, SASE snapshots local branch and dirty-worktree state under refs/sase/recovery/ and a SASE-labelled stash for manual inspection. After a later successful integration, cleanup removes at most 50 snapshots per pass that are older than 30 days and whose protected history is already reachable from a remote-tracking ref. Fresh snapshots and snapshots protecting unpushed commits are retained.

Agent launches into numbered workspaces rescue before they evict. When a sidecar clone holds commits that could not be published, the launch pins the in-clone recovery ref above and additionally copies the local-only commits and worktree into the durable rescue store outside the workspace (~/.sase/projects/<project>/rescue/<YYYYMM>/, falling back to ~/.sase/rescue/), then proceeds with eviction instead of failing. Each rescue entry holds a local-commits.bundle, a worktree.patch, and a manifest.json whose restore commands recover by hand: git fetch <bundle> 'refs/*:refs/sase/rescued/<stamp>/*' followed by git apply --index <patch>.

When an upstream-present sidecar integration reaches a repeatable failure such as unsupported conflicts or failed recovery, SASE records a per-clone failure marker. Further pulls are suppressed for the machine-recovery cooldown instead of retrying the same rebase on every command; the cooldown is at least five minutes and grows to match a larger configured bead-refresh TTL. A successful integration clears the marker. Remote outages and an unavailable cooperative lock do not create this failure cooldown, and a clone that still holds unpublished bead commits is never parked by it. Both that unpublished-commit check and the pull's integration find the bead store inside a clone from its actual layout (the clone root for a split --beads sidecar, beads/ inside a combined --plans clone).