Skip to content

How to keep specs consistent and up-to-date with spec-kit? #620

Closed

Description

@elebescond

With spec-kit, each feature defines its own spec. This works well at first, but over time and with the multiplication of features, some descriptions become outdated or are enriched by new features.

Example

  • Feature 001 – User Login defines that a user can log in with email + password.
  • A few months later, feature 009 – Two-Factor Authentication enhances the login by adding an extra step (OTP code).

In this case, the original spec of feature 001 becomes partially outdated or needs to be complemented by feature 009.

Discussion

  • Is there a recommended approach to consolidate specs over time?
  • Should the original feature be modified, or is it better to keep the historical record and add links to subsequent evolutions?
  • Are there patterns used by the community to manage the evolution and consistency of specs as new features are added?
  • Idea: Would it be useful to introduce a /close command (or equivalent) to mark a feature as deprecated and automatically update a centralized project documentation?

Activity

  1. elebescond commented on Sep 27, 2025

    @elebescond
    Author

    Following up on my own question:

    I think the up-to-date centralized documentation could be considered an integral part of the spec constitution, not just a supporting tool.
    This would ensure that as features evolve, the “official” documentation reflects the current state and remains consistent with the history of specs.

  2. jperfetto commented on Sep 29, 2025

    @jperfetto

    I have a similar concern. I think it's essential that specs are maintained/curated/refactored to maintain a single, logically organized source of truth that both humans and AI can refer to. An implementation path could be to do a git commit on the spec update(s), and then use that commit to kick-off planning of a migration plan to implement the spec change.

    Doing that, your example might be extended as:

    • Spec 001 - Initial, Minimal Product is implemented, which amongst other things defines email + password login
    • A few months later, Spec 009 - Authentication is implemented, which more thoroughly considers authentication requirements and states that both email + password and OAuth login should be supported. The mention of email + password is removed from 001 in the same commit that adds 009. A migration plan + implementation is kicked off.
    • A few months later, Spec 009 - Authentication is amended, to incorporate the new two-factor auth feature. A migration plan + implementation is kicked off.
  3. michelegirini commented on Oct 18, 2025

    @michelegirini

    What if SpecKit could compact multiple implemented features into a single “milestone” spec — kind of like how LLMs summarize long conversations?

    After several /specify → /plan → /tasks → implement cycles, we could run /speckit.compact to:

    Synthesize all completed specs into a concise milestone summary

    Capture shared patterns, architecture, and lessons

    Detect drift between intent and implementation

    Optionally trigger a light refactor phase to align the codebase with the consolidated spec

    Basically: milestones not as planning containers, but as post-implementation compaction points — compressing implementation history into reusable context and keeping both specs and code coherent as the system evolves.

  4. gideoncatz commented on Oct 20, 2025

    @gideoncatz

    This is indeed a key point!
    It is important to have an up-to-date, consolidated specification describing the overall project.
    So I'm joining the question :-)

  5. timgaunt commented on Nov 5, 2025

    @timgaunt

    I agree, it would be great if there was a centralised/consolidated spec that keeps track of the entire current definition

  6. amerzad commented on Nov 11, 2025

    @amerzad

    That is exactly the killer feature of Openspec.

  7. tjohnson4 commented on Nov 15, 2025

    @tjohnson4

    I also ran into this very early in my testing of this tool. The generated output after the spec is implmented quickly becomes out of date due the the fine tuning changes applied to get the feature ready for actual use. In a perfect world there would be some background job that can detect when feature deviate from the original spec and suggests changes to bring it up to date. I envision this could also be used to create specs for existing features that are missing specs within your project.

  8. gwallan commented on Dec 30, 2025

    @gwallan

    I once considered not merging the specs directory into the main branch each time a branch is merged, so as to ensure that the development branch retains its own archival information from the development process.

  9. github-actions commented on May 30, 2026

    @github-actions
    Contributor

    This issue has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.

  10. dan-s-github commented on Jun 1, 2026

    @dan-s-github

    just came across this FR while investigating the same problems, wonder if there is an extension available that would solve this

  11. dmarcelino commented on Jun 30, 2026

    @dmarcelino

    I have the same concern. There's also a discussion about this here: #152

  12. zaytsevand commented on Aug 12, 2026

    @zaytsevand

    The 001-login / 009-2FA example is exactly the case I hit, and I ended up splitting it into two problems that turned out to need different tools.

    "What did we already decide?" — context from earlier specs never reaching the current one. I published spec-kit-memory for that (community catalog id memory): it hooks before_specify / before_plan and recalls prior specs and decisions from a configurable memory backend before you write the new spec.

    "Which earlier promise did this new spec just invalidate?" — the sharper half of your question, and the one that actually bit me. A merged spec in my project made a closed per-intent allowlist its central safety control. A later spec made that intent set page-derived and unbounded. The first spec was not wrong — an assumption underneath it moved, and nobody wrote the amendment. Both merged separately, each passing its own gate. /speckit-analyze could not see it, because it reads one feature directory: spec.md, plan.md, tasks.md, constitution. The hole survived three review rounds before a concrete behavioural question walked straight into it.

    So I wrote spec-kit-coherence. It hooks after_analyze and diffs the feature against its merged siblings — where "merged" is read from git ls-tree <base-ref>, so a spec that only exists in your working tree is a peer under construction, not a promise already made.

    Two halves on purpose:

    • A deterministic script enumerates siblings from declared links only — spec numbers cited in the prose, # Implements: headers in files the branch changed, contracts declaring themselves a delta over another spec. Never "specs that feel related", which is how this degenerates into a second architecture review. It also flags the two defects that need no judgment: a cited spec with no directory, and a delta contract with no base contract to be a delta of.
    • A judgment pass then classifies each sibling invariant HOLDS / SILENTLY BROKEN / AMENDED — where AMENDED requires the amendment to actually live in the sibling, not merely in the new spec's prose. An amendment written only in the changing document has not been made.

    On your example it would surface 001's "log in with email + password" as a closed-world promise sitting against 009's added step, and say plainly that the artefact to amend is 001 — and that this is a governance change to 001's promises, not a code edit.

    Answering @dan-s-github's June question directly: this is that extension, such as it is. Fair warning that it is new — source is public, but no release is cut yet and it is not in the community catalog, so it is a specify extension add --dev <path> install for now.

  13. mnriem commented on Aug 12, 2026

    @mnriem
    Collaborator

    As indicated above you can deliver an extension and/or preset to deliver this above and beyond the core SDD process

  14. github-actions commented on Oct 13, 2026

    @github-actions
    Contributor

    This issue has been automatically marked as stale because it has had no activity for 60 days. It will be closed in 30 days unless there is a substantive update. If the problem still applies, please provide updated context such as whether it is still reproducible, the affected version or environment, its current impact, and any new reproduction details. A comment that only states that the issue is "still relevant" does not give maintainers enough information to reconsider it.

  15. added
    triage-nice-to-haveVerdict: evidence-backed fix or greenlit feature — land after review
    on Oct 15, 2026
  16. Khunjua commented on Oct 20, 2026

    @Khunjua
    Contributor

    We measured how big this problem is on a real repo.

    We took frank-fs/frank: 17 features in specs/, one Spec Kit template. Then we folded all 17 into one backlog, oldest first. At each feature we asked: is this story new, does it revise an earlier one, or is it a duplicate?

    70 user stories as written became 45. 10 were rewritten by later features, 14 were the same story written again, 1 was dropped. Five features added no new stories at all, they only changed old ones.

    One story was touched by four features: 001 → 010 → 014 → 015, all about API compatibility. To know what it says today, you read four spec.md files in order.

    And 20 times a later requirement looked like it replaces an earlier one, but no spec says so. One requirement, "all HTML through Hox h()", was put in doubt three times.

    So per-feature specs work like a change log, not a spec of the system. The 001-login / 009-2FA case is the normal case, not an edge case.

  17. chenhz01 commented on Oct 29, 2026

    @chenhz01

    I've hit the same wall, and I think the answer is not "consolidate the specs into one" — it's "detect the drift mechanically, and keep the history".

    Reason: merging 001 into 009 destroys the only record of why 001 existed. What's actually missing is a way to know, without reading everything, which specs no longer describe current behaviour.

    So I built a zero-dependency checker and ran it against a reproduction of the exact example in this issue (001 login + 009 2FA). Real output, trimmed in three documented ways to keep it readable: the per-finding evidence JSON and re-check command are omitted, and findings 3–6 have their repeated claim wording abbreviated with …. Everything else is verbatim. (The CLI strings are Chinese because that's the language I work in — I've glossed each line.)

    规格漂移判定 · demo                              # spec drift report · demo
    spec 2 份 | 测试文件 1 个 | 判定 8 条              # 2 specs | 1 test file | 8 findings
    
    沿用的 spec 清单:                                # specs read
      [001] 001-user-login      status=Implemented    FR=3  SC=2  story=1
      [009] 009-two-factor      status=Draft          FR=4  SC=2  story=1
    
    🔴 1. [D1]  009-two-factor
       1 处需求仍标记 NEEDS CLARIFICATION,但 spec 未被阻挡
       # 1 requirement still marked NEEDS CLARIFICATION, and the spec is not blocked
    
    🔴 2. [D3] FR-001 001-user-login
       FR-001 在任何测试文件里都找不到足够的特征词锚点(2 个非通用词中仅 0 个出现,需 ≥2)
       # no test file has enough distinguishing terms: 0 of 2 appeared, needs >= 2
    
    🔴 3. [D3] FR-003 001-user-login
       FR-003 …(6 个非通用词中仅 0 个出现,需 ≥2)        # 0 of 6 appeared
    🔴 4. [D3] FR-001 009-two-factor
       FR-001 …(4 个非通用词中仅 0 个出现,需 ≥2)        # 0 of 4 appeared
    🔴 5. [D3] FR-003 009-two-factor
       FR-003 …(7 个非通用词中仅 0 个出现,需 ≥2)        # 0 of 7 appeared
    🔴 6. [D3] FR-004 009-two-factor
       FR-004 …(3 个非通用词中仅 1 个出现,需 ≥2)        # 1 of 3 appeared — the 2-term rule firing
    
    🟡 7. [D2]  009-two-factor
       Status 仍是「Draft」,但 4/4 条 FR 的关键词已在实现代码里出现
       # status is still Draft, but the terms of all 4 FRs already appear in the implementation
    
    🟡 8. [D4] FR-002 vs FR-002 001-user-login ← 009-two-factor
       001-user-login 的 FR-002 与更晚的 009-two-factor 的 FR-002 关键词重叠 50%,
       且 001-user-login 未引用 009 → 疑似已被取代但未标注
       # FR-002 of 001 shares 50% of its terms with FR-002 of the later 009, and 001 never
       # references 009 → suspected superseded, but not marked
    
    汇总:high=6 mid=2 low=0                          # summary: high=6 mid=2 low=0
    说明:本工具只报「可复核的信号」,不做语义判断。D4 是候选而非结论,需人工确认。
    # note: this tool only reports re-checkable signals; it makes no semantic judgement.
    # D4 is a candidate, never a verdict — a human confirms it.
    

    --json emits the same findings as structured data — rule id, spec, FR id, evidence, and the exact re-check command. The prose fields inside it stay in Chinese; only the structure is language-neutral. A --lang en mode is the obvious next step and I haven't built it.

    That last one (D4) is the actual question you asked: "is 001's spec now outdated because of 009?" The tool doesn't answer it — it points at the pair and quotes both texts, and a human decides.

    What I learned building it: all three obvious approaches fail

    1. Don't grep the test files. Comments and docstrings poison the result. My own test file had a trailing comment — # nothing below covers: sign in with email and password — and that comment alone made FR-001 look "covered". Strip comments and docstrings first; only count what's in the assertion region.

    2. Drop the vocabulary every spec shares. email and password appear in every FR of both specs. If a test mentions "email" once (for a format validation test), every FR suddenly looks anchored. Cut terms whose document frequency exceeds ~40% before matching. This one change moved the result from 3 findings to 7.

    3. One matching word is not a match. A test named test_email_validation_rejects_missing_at contains "rejects" — which "matched" an entirely unrelated FR-004 about rejecting a reused OTP. Require at least two distinct terms to co-occur.

    The generalisable point: "the vocabulary overlaps" and "the requirement is covered" are different claims, and grep can't tell them apart without these three filters.

    What it deliberately does not do

    • No semantic judgement. D4 is a candidate list, not a verdict.
    • No LLM call. It runs in CI in under a second.
    • Doesn't modify your files. Every finding carries the exact command to re-check it yourself.

    The part that makes it useful day-to-day

    python spec_drift.py --root . --json            # machine-readable
    python spec_drift.py --root . --fail-on D3      # exit 1 if any FR lost its test anchor
    

    --fail-on D3 turns "specs should stay consistent" from a wish into a CI gate — which is the thing that actually stops drift, rather than detecting it after the fact.

    (One detail I got wrong the first time: my CI ran that gate with continue-on-error: true, which means it could never fail. A green badge that proves nothing. It now asserts the opposite — the gate must exit 1 on the broken fixture and exit 0 on the clean one. If a gate can't go red, it isn't a gate.)

    I ran it on real projects before posting this — and the first version was unusable

    Rather than tune thresholds on my own two-spec toy, I cloned three public repos that actually use spec-kit and ran the checker on their real specs:

    repo specs test files findings (first run) after fix
    thooams/uolt 35 103 406 31
    XEBIA-ACE/New_Prompt_UMS 23 27 19 19
    sardylan/live-qso-map 18 76 12 11

    406 findings on one repo is not a strict checker, it's a broken one. Cause: D4 compares every pair of specs, so 35 specs produce ~600 pairs — and specs from the same project are consecutive iterations that share vocabulary by construction. Three fixes brought it to 31: threshold 34% → 45%, keep only the strongest overlap per spec pair, and cap the list at 20 with an explicit "N more folded" line.

    The other two repos are where the signal held up:

    • New_Prompt_UMS: 17 D1 findings. 17 requirements still marked NEEDS CLARIFICATION, in specs whose status is no longer Draft. That's not a heuristic artifact — those are unresolved decisions shipped as if they were settled.
    • live-qso-map: 10 D2 findings. Status says Draft, but the FRs' distinctive terms are already in the implementation. That's "spec lags reality", the exact inverse of the problem in this issue.

    Then I scanned the ecosystem, and found something bigger than drift

    I pulled every specs/*/spec.md from 57 public repos that use spec-kit. Not a sample of
    convenient repos — everything code search could find:

    projects 57
    specs 825
    functional requirements 9,864
    specs still marked Draft 364 (44%)
    unresolved [NEEDS CLARIFICATION: ...] 239 across 25 projects (44%)
    cross-spec overlaps ≥45% 2,300 pairs across 24 projects

    The number that reframes this issue isn't the overlap count. It's that 44% of specs are
    still Draft
    .

    If almost half the specs in the wild never reach any terminal state, then "is this spec
    still accurate?"
    is not a question the data can answer — because no spec was ever marked
    as accurate in the first place. There is no "done" to drift away from.

    That's why I think the useful fix isn't consolidation and isn't a /compact command.
    It's giving a spec a lifecycle it can end: Draft → Active → Superseded-by #NNN.
    Once a superseded spec points at its successor, D4-style detection becomes unnecessary —
    you just read the pointer. Detection is only needed because nobody wrote the pointer down.

    Concretely, three small changes would make this problem tractable:

    1. A Superseded-by field in the spec header. One line. Costs nothing, and turns a
      latent inconsistency into a queryable edge.
    2. Status that can be Active. Right now the only states are Draft and whatever
      people invent; 44% never leave Draft, which makes status carry no information.
    3. A gate that refuses [NEEDS CLARIFICATION] in a spec whose status is not Draft.
      239 instances of exactly that exist today across 25 projects.

    The checker I built detects the symptom. Those three changes remove the cause. I'd rather
    argue about the three changes than about my thresholds.

    What I still don't know

    Whether the 31 remaining findings in uolt are true positives. I only verified that the count became readable, not that each one is correct. Checking that needs someone who knows the project.

    So if this is worth pursuing at all: pick one spec pair from your own output and I'll walk through why it fired. That's the fastest way to find out whether the heuristic deserves to exist — much faster than me tuning it in the dark.

    Known limits

    • D4 is lexical, not semantic. Two requirements can share terms and still be independent. Treat output as a reading list.
    • Non-English specs are approximated. No tokenizer; CJK runs are sliced into 2–4 character chunks. Good enough to be useful, not linguistically correct.
    • SC (success criteria) are not checked. They're usually not test-anchored by construction, so they need a different signal.
    • Thresholds will misfire on unusual vocabulary. 40% / 2-term / 45% are calibrated on 5 projects total (2 synthetic, 3 real). That is not a lot.

    Repo + run instructions: https://github.com/chenhz01/spec-drift

    git clone https://github.com/chenhz01/spec-drift && cd spec-drift
    python selfcheck.py               # 14 assertions: must fire on demo/, must stay silent on clean/
    python spec_drift.py --root demo  # reproduces the output above

    demo/ is the 001-login + 009-2FA case, clean/ is a zero-defect fixture. The self-check
    asserts both directions — a checker that only ever says "looks fine" is as untrustworthy
    as one that only ever complains.

    I'm not proposing this as the solution — just as a concrete starting point that others can re-run and argue with, which seemed more useful than a sixteenth "+1, we need this" on a thread that's been open for 13 months.

  18. locked and limited conversation to collaborators on Nov 4, 2026
  19. converted this issue into a discussion #5241 on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage-nice-to-haveVerdict: evidence-backed fix or greenlit feature — land after review

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions