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
- Core reads `.specd/active-worktrees.yaml` (gitignored) and passes each saved selection to the corresponding adapter when constructing it.
- 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.
- 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.
- 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
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)
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:
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
fsadapter knows how to list git worktrees and how to substitute paths; ans3orgit-remoteadapter 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:
extendsmechanism forspecd.local.yamlfirst. This approach is self-contained.git worktree list; the user never declares them in specd config.fsadapter, not in a config switching mechanism.specd worktree usemultiple times, without the added complexity.Design
Port interface
The
fsadapter implements these by resolving the workspace path to its git repo root and runninggit worktree list --porcelain. Other adapters leave them unimplemented.Flow
fs, this means substituting the repo root inspecs.fs.pathandcodeRootwith the selected worktree root. The relative path within the repo stays the same.listWorktrees()on each adapter that supports it, prompts when there is ambiguity and no saved selection, then callsSetActiveWorktree(workspace, worktreePath)— a core use case that writes the selection to the state file.State file
Path substitution (fs adapter internals)
Core never sees this substitution — it hands the worktree selection to the adapter and the adapter resolves the final path.
Responsibility split
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
Lists all workspaces that support worktrees, their available worktrees (from git), and which one is currently active.
Sets the active worktree for a workspace. If
[worktree]is omitted and multiple are available, prompts interactively. Writes the selection to the state file.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 rawspecd.yamlpaths.Related