7.9 KiB
Worktree Specs
Requirement
worktree must be cross-platform as far as practical. Core semantics should work across macOS, Linux, and Windows. Copy-on-write is a platform/filesystem acceleration and must not define the product model.
API
create
create(input: {
from: AbsolutePath
name?: string
into?: AbsolutePath
}): AbsolutePath
Default behavior:
- Source is
from. namedefaults to a generated directory name.intodefaults to the managed worktree directory.- Copy the whole workspace, including dirty, staged, untracked, and ignored files.
- Detach
HEADin the new workspace. - Return the path of the new workspace.
If from is already a managed worktree, create copies that exact worktree. Do not resolve back to an earlier workspace. Metadata should record the immediate source worktree as its parent.
Default storage is a hidden sibling directory of the original registered workspace:
/projects/app/ original workspace
/projects/.worktrees/app/task-a/ created worktree
/projects/.worktrees/app/task-b/ created worktree
- Created worktrees must not be stored inside the workspace being copied, because an exact copy would recursively contain existing worktrees.
- If
fromis an original unregistered workspace, its sibling.worktrees/<workspace-name>/directory becomes the default destination directory. - If
fromis already managed, descendants use the default destination directory associated with the original workspace rather than nesting storage beside each descendant. - If
intois provided, use it instead of the default destination directory. - If the original workspace is itself a filesystem mount root, its sibling default destination may not support copy-on-write with it; provide
intoon the same filesystem in that case.
remove
remove(input: {
at: AbsolutePath
}): void
remove deletes a managed worktree and its full descendant subtree.
atmust identify a worktree created by this tool; the registered source root cannot be removed.- Resolve all descendants through
parent_idand remove their directories deepest-first. - Verify each existing directory's
.worktreemarker before deleting it. - Refuse removal if any descendant path is missing, because it may be a moved workspace that has not been linked yet.
- After successful filesystem removal, delete the subtree records from the database.
link
link(input: {
at: AbsolutePath
to?: AbsolutePath
}): void
link reconnects a moved managed worktree to its registry record and can change its parent.
- Read the ULID from
.worktreeatat. - Look up the existing worktree record by ULID.
- If its recorded path is
at, leave its location unchanged. - If its recorded path is different and missing, update it to
at. - If its recorded path is different and still exists, fail because this is a duplicate identity, not a move.
- If the ULID is unknown to the database, fail;
.worktreealone does not include the ancestry needed to rebuild the record. - If
.worktreeis missing, look upatby its absolute path. If it matches an existing record, recreate the marker with that record's ULID. - If
.worktreeis missing andatdoes not match an existing record, fail. A moved workspace without its marker cannot be identified safely. - If
tois provided, set the worktree's parent to the managed worktree atto. - Refuse
tofor an original registered workspace; only worktrees created by this tool can be reparented. - Refuse
toif it isator a descendant ofat, because reparenting must not create a cycle.
children
children(input: {
of: AbsolutePath
}): AbsolutePath[]
children returns the direct managed children created from of.
ancestors
ancestors(input: {
of: AbsolutePath
}): AbsolutePath[]
ancestors returns the managed ancestry of of, ordered from its immediate parent to the root workspace.
Metadata
Metadata is stored in a central SQLite database in the platform-appropriate user data directory.
SQLite is not overkill: multiple processes and agents may create, inspect, or remove worktrees concurrently. It provides cross-platform transactions and locking without building a safe JSON registry protocol.
Start with one table:
CREATE TABLE worktree (
id TEXT PRIMARY KEY,
parent_id TEXT REFERENCES worktree(id) ON DELETE CASCADE,
path TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL
);
CREATE INDEX worktree_parent_id_idx ON worktree(parent_id);
- Every managed worktree has a stable generated
id. idis a ULID generated when the workspace is first registered or created.idis stored in the central database and in a.worktreemarker file at the root of the workspace..worktreecontains the worktree ULID and allows a moved workspace to be rediscovered and verified against the database.- When a managed workspace is copied, the copied
.worktreemarker is replaced with the new workspace's ULID. - The original registered workspace has
parent_id = NULL. - A created worktree has
parent_idset to the source worktreeid. pathis its current location, not its identity.- Provenance is a rooted tree. Descendants of any worktree can be listed through recursive queries over
parent_id. removedeletes a whole subtree, so no surviving record depends on deleted ancestry.
Moved Worktrees
If a worktree is moved outside the tool, its recorded path becomes missing. The tool cannot discover an arbitrary new location without being given a path or scanning a configured directory.
When link is run against a directory containing .worktree, the tool reads its ULID and reconciles the database path if the recorded path no longer exists.
If both the recorded path and the provided path exist with the same ULID, the tool must refuse automatic reconciliation because the directory was copied without assigning a new identity.
Git Integration
Git support is an integration for directories that contain repositories; it does not define the core worktree model.
When registering or creating from a Git repository:
- Add
/.worktreeto.git/info/excludeso the identity marker does not appear in local Git status. - Copy the directory with its staged, unstaged, untracked, ignored, and cached state intact.
- If
HEADresolves to a commit, detachHEADin the created destination at that same commit. - Preserve the copied index and working tree state while detaching.
- If the repository has no commits yet, leave its unborn branch state unchanged because there is no commit to detach to.
Refuse creation from a Git repository when:
- It is a linked Git worktree whose
.gitis not an independent directory. - A merge, rebase, cherry-pick, revert, or bisect is in progress.
- Git lock or inconsistent index state makes an exact safe copy unclear.
The tool does not create branches, commit changes, or otherwise replace normal Git commands.
Copy Strategies
Copying is implemented behind a strategy boundary so platform-specific copy-on-write backends can be added independently.
- The production strategy on Linux uses reflink cloning.
- The production strategy on macOS uses APFS
clonefiledirectory cloning. - If no implemented copy-on-write strategy succeeds,
createfails. - Full byte copying is not implemented as a fallback.
- Future strategies may add Windows copy-on-write support without changing the API.
Packaging
The project ships four interfaces backed by the same implementation and metadata model:
- Native library containing the core API and implementation.
- CLI package providing the
worktreeexecutable. - Bun FFI package for use from Bun applications.
- Node FFI package for use from Node.js applications.
The CLI and language bindings should remain thin and expose the same API semantics as the native library.
For CLI ergonomics, worktree create defaults from to the current working directory when no source path is provided.