| name | sdd:drift |
| description | SDD — Spec-Driven Development: detect code that drifted from .specs/<domain>/spec.md (changed without spec update). |
Usage: /sdd:drift (no arguments — scans every domain via the union discovery in layered-context).
Detects files that have changed since the corresponding domain's living spec (centralized .specs/<domain>/spec.md or a colocated specPath) was last updated. Useful for catching code that evolved without going through /sdd:specify → /sdd:plan → /sdd:implement, so the living spec is now lying.
Steps
1. Load configuration
Read .sdd.json (if present):
specExempt: glob list of paths to ignore. Default: ["*.config.*", "*.test.*", "**/migrations/**", "scripts/**"].
driftCheck: "off" | "warn" | "gate". Default: "warn". off short-circuits the skill (✓ Drift check disabled (.sdd.json#driftCheck = off)); warn and gate both run the report — gate is informational here (the gating decision is up to the surrounding workflow / CI).
domains: same map used by layered-context. Used to resolve which files belong to which domain and to discover colocated domains.
Stop only if there are both no configured domains in .sdd.json#domains and no .specs/*/spec.md on disk, with: No domains configured and no .specs/ folder — nothing to check. Run /sdd:init or seed .specs/<domain>/spec.md first.
2. Discover domains + last-spec-update commit
Enumerate domains and orphans by running the resolver script:
python3 lib/scripts/resolve-spec-paths.py --all
The JSON domains[] is the union of .sdd.json#domains and the .specs/*/spec.md glob (de-duplicated by resolved path), each with its resolved specPath. The JSON orphans[] is the orphan list (see below).
For each discovered domain:
- Take its resolved
specPath from the script output (colocated or centralized — already computed).
- Get the last commit that modified that resolved path:
git log -n 1 --format=%H -- <resolved-spec-path>
Skip the domain if the file is untracked (no commits yet) — log ℹ <domain>: spec.md not yet committed; skipping drift check.
Also report each entry in the script's orphans[] (a *.spec.md in the tree that no configured specPath claims): ℹ Orphan living spec <path> — not referenced by any .sdd.json domain.
3. Find drifted files per domain
For each domain with a tracked spec:
- Resolve the domain's file pattern:
- If
.sdd.json#domains.<domain>.pattern exists, use that regex.
- Otherwise, fall back to a path prefix match against
<domain>/ and src/<domain>/ (anything inside a directory whose basename equals the domain name).
- List files changed since the spec's last commit:
git log --since-commit=<spec-commit> --name-only --pretty=format: -- <pattern-paths>
Or equivalently git diff --name-only <spec-commit>..HEAD -- <pattern-paths>. Pick the form that respects the pattern.
- Filter out:
- Files matching any glob in
specExempt.
- The spec.md itself.
- Files outside the matching pattern.
- The result is the drift list for this domain.
4. Severity heuristic
Per drifted file, classify:
| Signal | Severity |
|---|
File listed in any specs/*/.spec-context.json#files_modified since the spec's last commit | tracked (changed via SDD pipeline — Layer 1 wasn't synced; treat as a missed sync) |
File outside files_modified of any spec | unspeced (changed entirely outside SDD) |
File matches specExempt | (filtered out earlier; never appears) |
unspeced is more concerning than tracked — tracked may just mean the spec author didn't add a delta block, while unspeced means SDD never saw the change at all.
5. Report
Display per-domain:
🔍 Spec drift report
📁 <resolved-spec-path> (last updated <YYYY-MM-DD>, commit <abbrev>)
<N> files changed since spec was last updated:
tracked src/auth/login.ts — touched in spec 014-add-rate-limit (no delta block)
tracked src/auth/session.ts — touched in spec 014-add-rate-limit (no delta block)
unspeced src/auth/oauth.ts — changed outside SDD pipeline
👉 Run /sdd:specify to add a delta spec, or add the path to specExempt in .sdd.json.
If a domain has no drift, output ✓ <domain> — in sync on a single line.
If every domain is in sync, the final line is ✓ All domains in sync. and nothing else.
Always exit with success (this skill never halts the pipeline). Surrounding workflows can choose to treat unspeced rows as gates.