Skip to content

feat: worktree (git) support on storage adapters (fs) #15

@lsmonki

Description

@lsmonki

Problem

When a project has external workspaces (specs pointing outside the repo root), and those external repos have git worktrees active for different features, switching between contexts requires manually editing `specd.local.yaml` each time to redirect workspace paths.

This affects any project with external workspaces — not just coordinator repos. Concrete configurations where this arises:

  • Pure coordinator repo — no code, only manages specs from multiple external repos. Each external repo may have worktrees for different features in flight simultaneously.
  • Normal project with external workspaces — a regular repo declares an external workspace pointing to another repo (e.g. `../shared-platform/specd/specs`), and that external repo has a feature worktree active.

In all cases, `specd.yaml` has stable paths pointing to the external repos' main branches. Working against a feature worktree requires either creating a per-feature `specd.local.yaml` (full replacement, expensive to maintain) or manually editing it each time you switch context.

Decision: active worktree state file + adapter-owned detection and substitution

Worktree support is an adapter capability, not a core concern. The fs adapter knows how to list git worktrees and how to substitute paths; an s3 or git-remote adapter would simply not implement the capability. Core never touches paths — it only stores and forwards the active selection to the adapter.

This approach was chosen over named worktree profiles because:

  • No prerequisites — named profiles would require a partial-override / extends mechanism for specd.local.yaml first. This approach is self-contained.
  • Git is the source of truth — available worktrees are discovered automatically via git worktree list; the user never declares them in specd config.
  • Correct layer — worktree logic (path resolution, git metadata) belongs in the fs adapter, not in a config switching mechanism.
  • Self-cleaning — stale selections (worktree deleted) are detected and cleared automatically. No profile lifecycle to manage.
  • Named bundles are not needed — the only thing profiles would add is grouping multiple workspace selections under a name. That case is covered by calling specd worktree use multiple times, without the added complexity.

Design

Port interface

abstract class SpecsStoragePort {
  // existing methods...

  supportsWorktrees?(): boolean
  listWorktrees?(): Promise<WorktreeInfo[]>
}

The fs adapter implements these by resolving the workspace path to its git repo root and running git worktree list --porcelain. Other adapters leave them unimplemented.

Flow

  1. Core reads `.specd/active-worktrees.yaml` (gitignored) and passes each saved selection to the corresponding adapter when constructing it.
  2. The adapter applies the selection internally — for fs, this means substituting the repo root in specs.fs.path and codeRoot with the selected worktree root. The relative path within the repo stays the same.
  3. CLI calls listWorktrees() on each adapter that supports it, prompts when there is ambiguity and no saved selection, then calls SetActiveWorktree(workspace, worktreePath) — a core use case that writes the selection to the state file.
  4. Subsequent commands read the file and pass the selection to the adapter; no prompting.

State file

# .specd/active-worktrees.yaml (gitignored)
workspaces:
  auth: /projects/auth-feature-xyz
  payments: /projects/payments-main

Path substitution (fs adapter internals)

specd.yaml declares:  ../auth/specd/specs
auth repo root:       /projects/auth
selected worktree:    /projects/auth-feature-xyz
resolved path:        /projects/auth-feature-xyz/specd/specs

Core never sees this substitution — it hands the worktree selection to the adapter and the adapter resolves the final path.

Responsibility split

  • Core — stores active selections in `.specd/active-worktrees.yaml`. Exposes `SetActiveWorktree(workspace, path)` use case. Passes selections to adapters at construction time.
  • fs adapter — implements `listWorktrees()` and applies path substitution internally.
  • CLI — calls `listWorktrees()` on each adapter, prompts when ambiguous, calls `SetActiveWorktree`.
  • MCP / CI — non-interactive: uses the saved selection if present; falls back to the original `specd.yaml` path (or fails with a clear error) if there is unresolved ambiguity.

Invalidation

If a saved worktree path no longer exists on disk, the adapter detects it at initialization, core warns and clears that entry — falling back to the original path or re-prompting. `specd worktree reset [workspace]` forces re-selection without deleting the worktree.

Precedence

`specd.local.yaml` always wins — if present, `active-worktrees.yaml` is not applied.

CLI surface

New commands

specd worktree list

Lists all workspaces that support worktrees, their available worktrees (from git), and which one is currently active.

specd worktree use <workspace> [worktree]

Sets the active worktree for a workspace. If [worktree] is omitted and multiple are available, prompts interactively. Writes the selection to the state file.

specd worktree reset [workspace]

Clears the active selection for one or all workspaces, triggering re-detection on next command.

Existing commands that should reflect active worktrees

  • specd config show — display the resolved path alongside the configured path for any workspace with an active worktree override.
  • specd project overview — show a worktree status section: which worktree is active per workspace, whether any selection is stale.
  • specd project context — compiled context must reflect the resolved (substituted) paths, not the raw specd.yaml paths.

Related

  • `specd.local.yaml` full-replacement constraint (currently no partial overrides)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions