Repository navigation
Stable anchors for design decisions so other artifacts can link to them #2056
Description
The problem, in one or two sentences
When proposal.md or tasks.md mentions a design decision, it does so in free text ("as decided above", "per decision 3"). A reviewer can't jump to that decision, and an agent working in a fresh context can't reliably tell which decision a task implements. Nothing flags a mention whose decision has since been renumbered, renamed or removed.
Who this affects
Everyone who uses the spec-driven schema and writes a design.md, with any agent. Reviewers are hit hardest when they read a change on GitHub/GitLab or in an editor's Markdown preview. So are agents running /opsx:apply in a new session, because they rely on the written artifacts and not on the conversation that produced them.
What you tried
I tested this in a small playground repo: pierreboissinot/openspec-playground. It has a ~40-line bookmark CLI, OpenSpec 1.14.1 and the Claude Code integration. A fresh headless Claude Code session ran /opsx:propose once per variant with the same feature prompt: tags plus a Netscape HTML export, with backward-compatible storage.
1. Default spec-driven schema (baseline): artifacts
design.mdhad six decisions, each under its own heading, with no IDs.- Nothing linked to a decision: 0 links across
proposal.md,tasks.mdanddesign.md. tasks.mdcited spec scenarios by quoted name, but restated the design choices inline. For example, task 2.1 repeats the whole--tagparsing decision.- One risk ("hand-rolled parsing can drift") is the trade-off of a decision, with no link between them.
2. Same prompt, plus per-artifact rules in openspec/config.yaml: artifacts (decision headings ### D<n>: <title>; risks and tasks link with relative Markdown links)
- The agent followed the rules: decisions
D1–D6, 4 risks and the migration plan link to decisions, and 6 tasks link to decisions without restating them. - A small script applying GitHub's heading-slug rules found that all 11 links resolve.
- Two problems showed up:
- YAML pitfall. Writing the convention as a rule (
### D<n>: <short title>) makes YAML read the item as a key/value pair. The rule is dropped and the CLI prints only a warning. It works once the item is quoted. - Silent breakage. Retitling one decision (
D5: Export rendering→D5: Render the export by building strings with html.escape) broke 2 links.openspec validate --strictstill reports the change as valid.
- YAML pitfall. Writing the convention as a rule (
So a convention carried only by rules works, but each team has to rediscover it (including the quoting trap), and nothing catches links that break later. The default design.md template gives no structure per decision: ## Decisions holds only a placeholder comment.
Caveats: one run per variant, one agent (Claude Code), one model.
What you have in mind
This is a sketch to discuss, not a fixed design.
Step 1: template and instructions only (no CLI change)
- In
schemas/spec-driven/templates/design.mdand thedesigninstruction inschema.yaml, give each decision its own heading with a stable ID that is never reused, e.g.### D1: Scope model in global config. - In the
tasksinstruction (and whereverdesign.mdcontent is referenced), ask for links to decisions as standard relative Markdown links:[D1](./design.md#d1-scope-model-in-global-config). These are plain links, so they work in every agent, renderer and editor, with nothing to install.
Step 2: optional, a separate change proposal if step 1 is worth it
openspec validatewould report dangling decision references, i.e. links whose target decision doesn't exist. Advisory only.- It would reuse the segment normalization being defined for scenario references in docs(openspec): propose test-side scenario references and a coverage report #2029 / docs(openspec): propose linking tests to spec scenarios #1979, so OpenSpec has a single anchor grammar and not two.
Open questions
- What the anchor points at.
- (a) The generated heading slug: zero syntax, but it changes when the title changes.
- (b) An explicit ID such as
<a id="d1"></a>: stable, but it puts HTML into the artifacts. - (c) The ID alone (
D1), resolved by OpenSpec and not by the renderer: stable, but it isn't a clickable link everywhere.
- Scope. Links between files in the same change survive archiving, because the files move together. A link from a live spec to a decision in an archived
design.mddoesn't survive. I'd keep specs out of scope here. Decisions that outlive their change belong to the broader question in Exploration: giving project knowledge beyond behavior a home in OpenSpec #1980 (and Discussion 1553 on ADRs).
Related: #1980, #783, #1979, #2029, Discussion 1553, Discussion 1564
Activity