Repository navigation
Exploration: should OpenSpec stop merging specs deterministically in openspec archive? #2033
Description
Activity
Thanks for opening this up. Here's one data point, from a small production site that moved to
OpenSpec 1.14 a few days ago. The whole app is regenerated from the specs, so the main specs
have to stay exactly right.How we archive (Q1). One PR carries a change's deltas plus the code. After that PR
merges, a separate PR does nothing except runopenspec archive <name> --yes(several
changes in name order). Archiving inside the change's own PR doesn't scale: parallel changes
conflict inspecs/. Archiving later by hand gets forgotten.To make that archive PR safe to merge without review, CI re-runs
openspec archiveon the
base and checks the PR is exactly that output, plus a few more conditions. This is where
the deterministic merge earns its keep for us. It makes the archive step verifiable,
which matters more to us than making it automatic. If an agent did the merge, we'd lose that
oracle and have to review every archive by hand.What the CLI could still guarantee (Q2), and parallel changes (Q3). The silent overwrite
you describe is the case we worried about most. Change B, written against the old text of a
requirement, archives after change A and replaces A's edit. We guard against it with
base fingerprints, essentially Phase 0 of your
parallel delta plan:- each change stores a SHA-256 of every requirement block it modifies, removes or renames,
as the block read when the delta was written (nullfor ADDED). Blocks are canonicalised
first: line endings, trailing whitespace, runs of blank lines; - a PR check fails if a change's fingerprints are already stale, while the author can
still fix the delta; - the archive check compares fingerprints before archiving each change, so both cases are
caught: two changes active at once, and B started before A was archived.
It needs no git history (squash merges and rebases don't matter), and it behaves the same
on GitHub and Azure DevOps. Whatever happens to the merge, the CLI could still own this
guarantee: "this delta was written against the requirement as it is now".Other sharp edges we hit on 1.14.0, in case they're useful:
- archiving a change that creates a capability writes a placeholder Purpose, which then
failsvalidate --strict, unless the delta has a## Purpose; archive --yesarchives with open tasks;- archiving several changes in different orders gives a different requirement order;
validate --specsdoesn't check SHALL/MUST in main specs (only in deltas);- a change folder named
-yis parsed as an option.
On Q5 and the three directions, for what it's worth:
- we'd keep a deterministic merge, anchored on stable IDs (direction 3), and add
fingerprints; - we already prefix every requirement name with a permanent ID (
NAV-5 …), which in
practice avoids most header drift; - an agent-performed merge (direction 1) would be fine as a helper for resolving a
fingerprint conflict. We'd still want the CLI to be the one that can re-run and check the
result.
We plan to run this on a second, larger project. If it holds up, we'd be glad to contribute
the fingerprint piece, coordinating with #1387 (concord), which detects the same drift from
git history.- each change stores a SHA-256 of every requirement block it modifies, removes or renames,
Tim's comment is the CI case I was missing. A small production site on OpenSpec 1.14: one PR carries a change's deltas plus the code, and after it merges a follow-up PR only runs
openspec archive --yes(several changes, in name order). CI re-runs archive on the base and checks the PR is exactly that output, so the deterministic merge is the oracle. Prefixing names with a permanent ID (NAV-5 …) already dodges most of the header drift. I think that's Direction 3 still living inside the heading. The hole I still see is that archive matches the heading string, so an agent rename can break the join or grow a twin SHALL whilevalidatestays green.I've been modelling the address as something that isn't the heading: one named idea in reqlan,
rq:pins andproven byedges under the OpenSpec markdown folder, checked by graph static analysis. Honest ceiling: named bindings, not SMT. Not Spec Kit, not SpecGraph or GraphSpec, and this.rqis not SPARQL. The delta and main prose stay OpenSpec's. Fingerprints (the delta was written against the requirement as it is now) answer a different question than a stable ID. I think they stack; they don't replace the address. That address is what archive, or abeforeArchivehook (#1910), would key off. Not proposing a PR. You asked for discussion first, and I'm not suggesting OpenSpec adopt another process kit. Our side of the under-folder pattern is still unfinished. If Direction 3 stays on the table, happy to share a concrete pattern once you've narrowed the archive model.Disclosure: I build reqlan, so weigh this with that bias.
The question
openspec archivemerges delta specs into main specs by matching requirement headers. We're starting to think a deterministic merge isn't realistic to support well, and that the merge belongs with an agent instead. We don't yet know what should replace it, or what that would break.What we're seeing
validateand only fails at archive, sometimes weeks later (validate: MODIFIED/REMOVED/RENAMED-from headers that don't exist in base spec aren't caught until archive (proposal: opt-in cross-change MODIFIED) #1112).MODIFIEDreads a rename as a dropped scenario and blocks archive #1697).archive.ts,specs-apply.ts,requirement-blocks.ts) has had 46 fix commits. For example, parser grammar (REMOVED and RENAMED entries written with*or+bullets are silently ignored #1799, Repeated delta section headers silently discard requirements #1801, A requirement written outside a delta section is dropped with no diagnostic #1803, Malformed RENAMED pairs silently skip a rename, or rename the wrong requirement #1805,show --json --deltas-onlyreports an invented MODIFIED for a bullet-form REMOVED, while archive deletes the requirement #1855, A REMOVED heading with a CommonMark closing#run is skipped, with a false "already removed" warning #1859, ADDED and RENAMED accept a name that differs only in case or spacing, leaving two copies of one requirement #1863, A delta atspecs/<capability>.mdis green-lit bystatus/apply, rejected byvalidate, then archived without being merged #1869) and lost formatting (archive: rebuilt specs end with an extra blank line at EOF #1527, openspec archive does not preserve blank lines around ## Requirements in the target spec #1625,archiverewrites the inside of fenced code blocks #1797, archive: define blank-line normalization policy for fenced code #1711, archive: applying a delta rewrites a CRLF spec to LF, turning a one-requirement change into a whole-file diff #1935, Feature: preserve specodelic frontmatter and four-layer tables when openspec archive deploys a delta #2017)./opsx:archiveand/opsx:bulk-archivehave the agent do the merge, then move the folder. So there are two merge paths, and users notice the difference (question: why achive skill or command relies on llm to manually merge specs why it does not use openspec archive cli command? #656, AI commands and skills for/opsx:archivedo not invokeopenspec archiveCLI, unlike official docs and tutorials #863).Directions we're thinking about
None of these are decided. Each one has trade-offs we haven't worked through.
openspec archiveas it is.Open questions
openspec archivefrom CI or scripts?How you can help
Related
lifecycle: status— record change state as data, not directory position (experimental) #1683, feat(sync): fold delta specs without archiving the change #1813MODIFIEDreads a rename as a dropped scenario and blocks archive #1697, feat(validator): detect stray delta headers that silently truncate main specs #954, Feature: preserve specodelic frontmatter and four-layer tables when openspec archive deploys a delta #2017, and the parser and formatting issues above--yesskips the incomplete-task stop, and the refusal tells callers to rerun with--yes#2006, Harden archive workflow against incomplete or invalid spec synchronization #1890