Core Concepts
Design principles
SDF is built around three principles:
Shell out, don't reimplement. SDF delegates to git, gh, and claude CLIs. It is pure orchestration — no custom git library, no custom GitHub API client. This means you get the behavior of the tools you already know and trust.
PR descriptions are the source of truth. Since .sdf/ is local-only, the authoritative record of stack structure and intent lives in the PR descriptions on GitHub. SDF can reconstruct stack topology from the PR graph via sdf fetch, making the workflow resilient to lost local state.
Fail loudly, recover clearly. When something goes wrong, SDF leaves the repository in a known state and tells you exactly what to run to recover. It never silently swallows errors.
Understanding SDF's model makes every command feel obvious. There are two things to know: stacks and nodes.
Stacks
A stack is a named, linear chain of branches that all descend from a common base (usually main). Each branch in the chain becomes a pull request that depends on the PR above it.
main
└─ users-feature/db-schema ← PR #142
└─ users-feature/repository ← PR #143
└─ users-feature/controller ← PR #144
A repository can have multiple independent stacks — one for each feature being developed in parallel. Each stack has its own JSON file in .sdf/stacks/<name>.json, stored locally on your machine.
Stack JSON
{
"stack_id": "users-feature",
"base": "main",
"nodes": [
{ "branch": "users-feature/db-schema", "pr": 142, "status": "open", "base_tip": "abc123", "nav_hash": "e3b0c4..." },
{ "branch": "users-feature/repository", "pr": 143, "status": "open", "base_tip": "def456" },
{ "branch": "users-feature/controller", "pr": 144, "status": "draft", "base_tip": "ghi789" }
]
}
The base_tip records the commit hash at the tip of the parent branch at the time of the last sync. This allows SDF to detect when a parent has moved and a rebase is needed.
The optional nav_hash is a SHA-256 hash of the PR stack navigation section. SDF uses it to avoid redundant API calls when updating PR descriptions — if the hash hasn't changed, the description update is skipped.
Nodes
A node is one branch (and its corresponding PR) within a stack. Nodes are ordered — the first node's parent is the stack base, and each subsequent node's parent is the node above it.
Node status
Each node tracks its PR status. The possible values are:
| Status | Meaning |
|---|---|
open | The PR is open and under review. |
draft | The PR is a draft, not yet ready for review. |
merged | The PR has been merged into its base. |
closed | The PR was closed without merging. |
Merged nodes stay in the stack
When a node's PR merges, sdf sync does not remove it from the stack. Instead, the node remains in the stack JSON with status "merged". The ParentBranch() function skips over merged nodes when computing the effective parent for downstream branches — so downstream nodes rebase onto the stack base (or the nearest non-merged ancestor) automatically.
This means merged PRs are shown as completed in the stack topology rather than silently disappearing.
Configuration
SDF uses a two-tier configuration system:
- Global config —
~/.config/sdf/config.json, user-specific defaults - Repo config —
.sdf/config.json, per-repo overrides
Repo config takes precedence over global config on a field-by-field basis. Run sdf config show to see the effective merged config.
{
"branch_prefix": {
"enabled": true,
"scope": "users-feature",
"separator": "/"
},
"pr_title": {
"conventional_commits": true,
"ticket_pattern": ""
},
"sync": {
"with_content": false
}
}
| Field | Type | Description |
|---|---|---|
branch_prefix.enabled | bool | Whether new branches are auto-prefixed with the stack ID (default true). |
branch_prefix.scope | string | Scope used as branch prefix and conventional commit scope. If empty, falls back to the stack ID. |
branch_prefix.separator | string | Character between prefix and branch name (default "/"). |
pr_title.conventional_commits | bool | Generate PR titles with conventional commit format using scope (default true). |
pr_title.ticket_pattern | string | Regex to extract a ticket ID from the branch name for PR titles. |
sync.with_content | bool | Whether sdf sync updates PR titles and descriptions (default false). |
Repository layout
.sdf/ ← local-only (gitignored)
config.json ← repo config
stacks/
users-feature.json ← stack topology
auth-feature.json ← another stack
local.json ← ephemeral state (sync progress, etc.)
The entire .sdf/ directory is local-only — it is listed in .gitignore and is never pushed to the remote. Each developer's machine maintains its own copy of the stack metadata. PR descriptions on GitHub serve as the shared source of truth for stack structure and context.
Stack resolution
When you run a command that operates on a stack (sdf sync, sdf status, sdf branch, etc.), SDF determines which stack to use with this logic:
- Explicit
--stackflag — always takes priority - Positional argument — e.g.
sdf sync my-feature - Current branch — if your checked-out branch belongs to a stack, that stack is used
- Single stack — if there's only one stack in the repo, it's used automatically
If none of these resolve to a stack (e.g. you're on main with multiple stacks and didn't specify one), SDF reports an error listing the available stacks. With shell completions installed, you can tab-complete stack names directly — useful when working with multiple stacks.
Local state
The file .sdf/local.json stores ephemeral state that should not be shared between developers:
- Sync progress — when a sync is paused for manual conflict resolution, the paused branch, resume index, and original branch are saved here.
sdf sync --continuereads this to resume. - Split sessions — maps stack names to Claude session IDs so split refinement can resume across invocations.
This file is automatically created when needed and can be safely deleted — SDF recreates it as necessary.