Skip to content

Design Decisions

Gregor Biswanger edited this page Aug 26, 2026 · 4 revisions

Design Decisions

Why FeatherSpec is shaped the way it is. Every rule here has a cost attached to breaking it.

One rule, one place

No rule and no workflow is written down twice — apart from the reinforcements listed below, which are deliberate and documented.

  • AGENTS.md is the only place mutable state livesDocLanguage, the architecture: snapshot, and Style & Output Preferences. Every command that changes those writes to AGENTS.md. CLAUDE.md is one line and holds no copy.
  • Rules and workflow bodies are single files both tools read. No .github/instructions/, no .github/copilot-instructions.md — they would only duplicate what .claude/rules/ and .claude/commands/ already provide. The one Copilot-side folder that does exist, .github/prompts/, holds loaders: a link and a sentence, never a rule.
  • AGENTS.md states policy; rule files state craft. AGENTS.md defines the spec and plan lifecycle and the Memory Bank file set. .claude/rules/specs.md, plans.md and memory-bank.md cover only how to write those files, and link to AGENTS.md for the policy. .specs/README.md is orientation and links rather than a restatement.

This is not tidiness for its own sake. Two copies of a rule drift, and when they drift the model follows whichever it read last. One copy cannot drift.

The documented exceptions

They all exist for the same reason: a path-scoped rule loads only when a matching file is read, which is too late for a workflow that is creating or moving that very file.

Reinforcement Where Why
Spec essentials (DocLanguage, the **Status:** line, the document structure) commands/sdd-specify.md a brand-new spec has not been read, so .claude/rules/specs.md may not have loaded yet
Plan essentials (DocLanguage, naming, the **Status:** line, the file structure) commands/sdd-plan.md same reason — a brand-new plan file has not been read, so .claude/rules/plans.md may not have loaded yet
Lifecycle safety rules (one folder at a time, delete the original, duplicate check, plan travels with spec) commands/sdd-lifecycle.md this workflow performs the moves and deletions; the safety rules must be in front of the model as it acts
Report schema and map budgets (owned by .claude/rules/architecture-map.md) commands/sdd-architecture-scan.md a scout runs in a fresh context, and a brand-new .architecture/ map has never been read — the rule would not be loaded at the moment it matters

AGENTS.md remains authoritative in every case: if a copy ever diverges, fix the copy. Do not "clean up" these by deleting them — each one is load-order insurance, and removing it reintroduces a real failure mode. The regression would be silent, too: nothing fails, the new plan file just comes out in the wrong language with no status line.

Each exception is annotated from both ends. The command says it is restating something and names AGENTS.md as authoritative; the corresponding rule file carries a reverse note saying why the command repeats it. specs.md, plans.md and memory-bank.md all carry that note. A deliberate duplication that is not labelled as one is indistinguishable from an accident, and the next contributor deletes it.

The rule that follows from this: a command may restate a rule when it must be in front of the model at the moment it acts. Anything else is a copy, and copies drift. The Copilot persona file used to hold five of them — an operating protocol with a fourth step the constitution does not have, a memory-sync rule missing its "same change set" clause, a preference rule missing its rewrite clause. All five had drifted from the original before anyone noticed. It is a pointer now.

One scout, two dialects

The deep scan's sdd-scout agent must exist for both tools, and it is the one artifact where a single shared file is impossible: Claude Code wants tools: Read, Grep, Glob, Write — a comma-separated string — while VS Code wants tools: ['read', 'search', 'edit'] as a YAML array, plus user-invocable: false to keep a subagent-only worker out of the persona dropdown. So the scout ships twice, .claude/agents/sdd-scout.md and .github/agents/sdd-scout.agent.md, with byte-identical bodies and an annotation in each naming the other. It is the same both-ends rule as every other documented exception: an unlabelled deliberate duplication is indistinguishable from an accident, and the next contributor deletes it. Because VS Code also reads Claude-format files from .claude/agents/, the shipped settings turn that location off — otherwise it would surface a second scout whose Claude-dialect tool string VS Code cannot restrict.

One action, one autonomy level

A gate that exists in one file and not in another is not a gate — the effective behaviour is whatever happened to be in context. The architecture snapshot used to be written immediately for "obvious" drift by the constitution and only after confirmation by /sdd-architecture-update, with new top-level folder listed on both sides. There is now exactly one gate for that write, and the drift rule proposes rather than writes.

The same principle sets the shape of the non-negotiables: always, ask first, never — with ask first naming the operations whose blast radius reaches your codebase rather than a Markdown file (dependencies, migrations, deleting or moving files, git writes, network calls). The earlier version had no ask first tier at all, and every gate in the repository protected a document while nothing protected the code.

/sdd-architecture-scan keeps the invariant intact: the scan analyzes, but it never writes the snapshot. It ends by running /sdd-architecture-update with its distilled findings as the observed state, so the snapshot still has exactly one write path and one confirmation gate — a deep scan changes how much the delta report knows, not who holds the pen.

Volatile working folders

.sdd-scan/ and .sdd-update/ are the only folders in the template whose contents are disposable by design — the scan's inventory, worklist and scout reports, the updater's fetched release trees: working state, not product. They are gitignored because working scratch is machine-local, can get large, and must never look like curated documentation. Everything worth keeping is distilled onward into the snapshot, the .architecture/ maps and the Memory Bank before the scan offers to delete the folder. The worklist is also what makes the scan resumable across sessions; committing it for a shared scan is the documented team-resume option in Configuration.

One version, one line

v1.2.0 made the template versioned and derived projects updatable — the mechanism lives in Updating & Versioning, the release ritual in Releasing. Four decisions shape it:

  • Releases are annotated git tags surfaced as GitHub Releases, and the number is a migration signal. Template-semver: MAJOR means a derived project faces a real migration step, MINOR is additive and merges in around customizations, PATCH is wording. That is why the brownfield deep scan shipped as 1.1.0, not 2.0.0 — it is purely additive, and a MAJOR spent on an additive release teaches users that major bumps are safe to take blindly, which destroys the signal before the first real breaking restructure needs it.
  • The version stamp is one line in AGENTS.md's managed block. No manifest, no registry, no new state store — AGENTS.md is already the only place mutable state lives, so FeatherSpecVersion: sits beside DocLanguage. One line in the file every adoption keeps survives every adoption mode, including a project whose .git was deleted at adoption.
  • The updater compares fetched release trees, not maintained metadata. Classification comes from canonically hashing three real states of every file — the version you started from, the target, your copy — so an authoring mistake can never misclassify a user's file: a maintained list of "what we ship" could drift, a fetched tree cannot. The model merges only true conflicts, and every merge is previewed and confirmed before it is written.
  • The updater is an agent-executed command, not a CLI. A CLI would be a second implementation of the workflow to keep in sync; a command is one body both tools run. The constitution's ask first tier already gates its network fetches and git writes, the version stamp moves last so an interrupted run never claims to be finished, and it resumes from its files in .sdd-update/ — the same pattern .sdd-scan/'s worklist established.

No skills on purpose

A skill advertises itself: both tools read every skill's name and description up front so the model can decide to load it. Claude Code drops that advertising when a skill sets disable-model-invocation: true, but VS Code does not document the same guarantee — so eleven workflow descriptions could sit in the system prompt of every single request.

Workflows here are commands, which no model is offered:

  • .claude/commands/<name>.md holds the body and Claude Code runs it as /<name>
  • .github/prompts/<name>.prompt.md is a one-line loader that points Copilot at that same body

Reach for a skill only when a workflow genuinely needs the model to trigger it on its own. These do not: they create, move and delete files, and they should run when you say so.

Deliberate constraints in the shared bodies

Because one body file is executed by both tools, it is written to the lowest common denominator. Each constraint has a reason:

  • No !`shell` injection. Copilot does not support it; it would land in the prompt as literal text. Bodies instruct the model to run a command and use the result instead.
  • No $ARGUMENTS / $0 / $1. Same reason — Copilot's documented mechanism is ${input:…}. Bodies say "the user may name a spec path after the command; if none is given, list the candidates and ask." Both tools append the user's trailing text to the invocation, so the model still sees it.
  • No tool names in the bodies. The tool that fetches a web page is WebFetch in one and #fetch in the other, so bodies name the capability ("your web search or fetch capability") and state what to do when it is missing. Same for terminal work: bodies say which command to run and use the result, rather than assuming a tool name.
  • The file name is the command name in both tools, so .claude/commands/sdd-plan.md and .github/prompts/sdd-plan.prompt.md must share the stem.

A comment at the top of each body restates this, so nobody reintroduces the syntax later.

Why planning writes a file

A plan that lives only in the chat is gone when the session ends. Making the plan a file changes three things:

  1. Resumability — a new session reads Current step and Session handoff and continues, instead of asking you to re-explain.
  2. Reviewability — the plan shows up in a pull request next to the code it produced.
  3. Impact analysis — the traceability table can be read in reverse, so a changed requirement yields the list of steps and code paths it reaches before anything is edited.

The rule that makes all three work: the plan is updated in the same change set as the code. The assistant's own todo list is scratch state that dies with the session; the plan file is the durable one, and when the two differ, the file wins and gets corrected.

Why the data folders are top-level

.specs/ and .memory-bank/ are your project's content, not tool configuration. The predecessor template kept them under .github/, which made them look like GitHub plumbing and tied them to one vendor's folder. At the top level they are visibly data, they are tool-neutral, and they survive any decision you later make about which assistant to keep.

Why sdd- prefixes everything

Two reasons: the commands do not shadow Claude Code's built-in commands, and typing /sdd shows you the whole workflow set in one autocomplete group.

Clone this wiki locally