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:
- SASE serializes setup and preflights every enabled configured sidecar repository.
- The provider creates or adopts each repository and SASE clones it at the linked-repository location.
- 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 itsbeads/-prefixed one. - SASE adopts any bead state still living in the plans clone (see below).
- 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.
- Adoption is a no-op when the plans clone has no
beads/directory, or when the beads clone already holds bead state. - Otherwise every entry of
<plans>/beads/is copied to the beads clone root, excluding the localbeads.db,beads.db-shm, andbeads.db-walflock files. A minimal store of onlyconfig.jsonandissues.jsonlis valid and copies cleanly. - 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. - The schema-3 record is written. This is the switch: bead commands now resolve to the beads sidecar.
- Only afterwards is
beads/removed from the plans clone, along with itsbeads/beads.db*ignore lines, and committed asMove bead state to the beads sidecar. A failure here is a warning, not a command failure—the authoritative switch already happened, and the nextsase repo initcleans 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).