OpenSpec Rejected Proposals: A Decision Memory Convention

No reject state. Here's the workaround.

Page content

An agent that proposed “move persistence into a shared library” six months ago will happily propose it again unless something durable says it was already rejected – and OpenSpec has no built-in state for that today.

/opsx:archive is built for one outcome: a change that shipped. It syncs the delta specs into openspec/specs/ and moves the folder to openspec/changes/archive/YYYY-MM-DD-<name>/ as a record of what changed and why. There is no /opsx:reject or /opsx:abandon counterpart, and nothing in the archive format tells a future proposal “this exact idea was investigated and turned down.” That gap matters most in exactly the codebases where OpenSpec is otherwise a good fit: brownfield systems with a small number of contributors and agents that periodically re-explore the same architectural questions – merge these two services, share this persistence layer, replace this HTTP boundary with a direct import.

A layered decision archive feeding context back into a new proposal

This is not a hypothetical gap. It was raised directly with OpenSpec’s own maintainers as a feature request, and the way that conversation played out is worth knowing before you improvise your own fix: what the project actually concluded shapes which convention is worth adopting. This guide walks through what happens if you rely on /opsx:archive alone, the real discussion that already happened in OpenSpec’s issue tracker, and a lightweight decision.md pattern you can adopt today without waiting for – or needing – core support.

Why Archiving Alone Doesn’t Record a Rejected Decision

Archiving a change you decided not to build technically works – the folder moves out of your active list either way. The problem is what that archived folder fails to communicate once it is sitting next to dozens of shipped changes:

  • No status field. An archived change looks identical whether it shipped or was abandoned three messages into /opsx:propose. A teammate or an agent scanning openspec/changes/archive/ cannot tell the difference without opening every proposal folder and reading the artifacts inside it.
  • No signal to check first. Nothing in the default workflow instructs an agent to search the archive before drafting a new proposal. /opsx:propose drafts from your current request and the state of the codebase, full stop – it does not cross-reference prior rejected changes unless you tell it to.
  • Delta specs you don’t want synced. If a rejected change already has draft delta specs and you archive it the ordinary way, /opsx:archive will offer to sync those deltas into openspec/specs/ first. Accepting that offer teaches your canonical specs to describe behavior you decided not to build, which quietly corrupts the “what does the system currently do” record that every other proposal reads before it plans anything.

None of this is a bug. /opsx:archive is doing exactly what its documentation says it does: complete a change that shipped. The rejection case sits outside that documented scope on purpose, and OpenSpec’s own team-workflow guide is explicit that most of what it recommends – branch conventions, PR review order, when to archive – is convention layered on top of the tool, not something OpenSpec enforces for you. Handling a rejection is one more convention you get to define yourself, and the CLI already gives you the flag you need to do it cleanly: pass --skip-specs when you archive a change you are not shipping, so openspec archive investigate-shared-persistence --skip-specs files the folder away without touching openspec/specs/ at all.

What OpenSpec’s Maintainers Actually Decided About ADR Support

Before inventing a house convention, it is worth reading how this exact question played out in public, because the resolution is more specific – and more interesting – than “no.” GitHub issue #557 opened in January 2026 with a request for first-class Architecture Decision Record support: durable records that persist independently of any single change’s lifecycle, so a rejected or superseded decision stays visible to every future proposal. A contributor even opened a pull request implementing it.

What followed was seven months of genuinely substantive back-and-forth involving lead maintainer Tabish Bidiwale (@TabishB) and several deeply engaged community members, covering immutable versus mutable records, whether an ADR belongs to the research phase or the design phase, cross-change ownership when one decision cascades into a dozen later changes, and how ADRs relate to specs as the “authoritative” description of the system. Tabish Bidiwale’s early framing set the direction the thread ultimately settled on: OpenSpec should stay lightweight by default and make specialized workflows like ADRs configurable through its schema system rather than baking them into core. A community member later summarized where the discussion landed:

ADR workflows are valuable, but OpenSpec does not currently have first-class/native ADR support… the direction discussed here is to keep the default workflow lightweight and make specialized workflows configurable.

Maintainer Clay Good (@clay-good) closed the issue on that basis in August 2026 and moved it into GitHub Discussion #1553 so the conversation could keep evolving without staying open as an unresolved bug. That is a reasonable call for a tool whose entire pitch is avoiding Spec Kit-style ceremony by default. It also means the fix lives one layer up, in one of two places:

  1. A community schema. The spec-driven-with-adr schema, built by OpenSpec technical advisor Hari Krishnan (@harikrishnan83) and documented on intent-driven.dev, adds a fifth artifact to OpenSpec’s default four-artifact pipeline. It exists because the default schema loses design.md’s reasoning the moment a change is archived – only the spec deltas get synced forward, so the “why” behind a decision disappears with the change unless something else preserves it.
  2. A repository-level convention. A small, hand-rolled decision.md file plus a naming rule, which costs nothing to adopt and does not require installing a custom schema.

The rest of this guide covers option two in depth, since it is the lower-friction starting point for most teams – and, as the section on the community schema below shows, it is compatible with switching to that heavier tooling later if your rejection log grows large enough to earn it.

The decision.md Convention for Recording a Rejected Change

Structure a rejected investigation the same way you would a shipped one, but stop before syncing any deltas, and add one file that states the outcome plainly:

openspec/
  changes/
    archive/
      2026-09-16-rejected-shared-persistence-layer/
        proposal.md
        decision.md

decision.md answers the same four questions a proper Architecture Decision Record does – what was decided, why, what alternatives existed, and what would change the answer:

# Decision

Status: Rejected

## Decision

Do not replace the service-to-service HTTP boundary with a direct
package import between the two Go services.

## Reasons

- Increases compile-time coupling between independently deployed services.
- Makes the persistence layer an implicit, undocumented contract.
- The measured benefit (latency, code duplication) was smaller than
  the coupling cost in this codebase.

## Alternatives considered

- Shared internal Go module -- rejected for the same coupling reason.
- gRPC instead of HTTP -- deferred, not rejected; revisit if HTTP
  overhead becomes a measured bottleneck.

## Reconsider only if

- The two services are intentionally merged into one deployable, or
- Latency measurements show the HTTP hop is a proven bottleneck.

## Related

- Architecture rule: services communicate over HTTP, not shared packages.

The one hard rule that makes this whole convention work: do not run the sync step for a rejected change. If /opsx:propose already drafted delta specs before you decided against the change, use the flag the CLI already gives you for exactly this situation:

openspec archive investigate-shared-persistence --skip-specs

--skip-specs tells openspec archive to file the change away without touching openspec/specs/ at all, which is the safest default for anything you are archiving without shipping. Accepting the ordinary sync prompt instead would merge the rejected idea’s delta specs into your canonical specs, and canonical openspec/specs/ should describe what the system currently does, not every idea that was drafted and turned down. If a change permanently produces no spec changes for a structural reason – a pure investigation folder, say – OpenSpec also supports declaring skip_specs: true in that change’s .openspec.yaml so it archives cleanly without the flag every time.

Naming Rejected Changes So Humans and Agents Can Scan the Archive

A decision.md file only helps if someone opens the folder. Prefix the folder name with the outcome so both a human skimming ls openspec/changes/archive/ and an agent listing changes can tell status without opening a single file:

2026-09-16-rejected-shared-persistence-layer/
2026-09-20-abandoned-react-router-migration/
2026-10-01-superseded-old-auth-design/
2026-10-10-add-project-filtering/          # shipped, no prefix needed

This mirrors the status vocabulary already recommended for standalone decision records – proposed, accepted, superseded, deprecated – applied to OpenSpec’s own archive instead of a separate docs/decisions/ folder. Keep the vocabulary small. Three or four consistent prefixes beat a free-text status line that every proposal spells slightly differently.

How to Make Your Agent Check the Archive Before Proposing Again

Naming and a decision.md file solve discoverability for a human skimming the folder. They do nothing on their own to make an agent search the archive before drafting a new proposal – that has to be an explicit instruction, because /opsx:propose does not do it by default, and no amount of tidy file naming changes that on its own.

Two places to put that instruction, matching how OpenSpec already expects project-specific guidance to be injected:

In openspec/config.yaml, under the context: field that gets injected into every planning request (mind the 50KB cap covered in the OpenSpec quickstart):

context: |
  Before proposing a change, search openspec/changes/archive for folders
  prefixed "rejected-" or "abandoned-" that describe a materially similar
  idea. If one exists, summarize its decision.md and state what has
  changed before proposing the idea again. Do not re-litigate a rejected
  decision without new evidence.

In AGENTS.md or your project’s own agent instructions, as a standing rule rather than a per-request context blob:

## Rejected OpenSpec changes

When a proposal is investigated and rejected:

1. Do not sync or apply its delta specs.
2. Add `decision.md` with Status, Decision, Reasons, Alternatives
   considered, and Reconsider only if.
3. Prefix the archived folder name: `rejected-<name>` or `abandoned-<name>`.
4. Before proposing a materially similar change, search
   `openspec/changes/archive/` and reference the prior decision.
5. Do not reopen a rejected decision unless its documented
   reconsideration conditions have actually changed.
flowchart TD A[New idea worth a change] --> B{Search openspec/changes/archive} B -->|Similar rejected decision found| C[Summarize prior decision.md] C --> D{Reconsideration conditions changed?} D -->|No| E[Do not re-propose. Reference the decision.] D -->|Yes| F["/opsx:propose with the changed context stated"] B -->|Nothing similar found| F

Neither instruction guarantees compliance – an agent can still skip the search step, the same way it can skip reading any other context you inject. But it is the difference between “the information exists somewhere in the repo” and “the agent is told, every time, to go look for it,” and only the second one actually reduces repeated investigations in practice.

A Worked Example: Rejecting a Proposal, Then Correctly Reconsidering It

Put the pieces together on a concrete case. Say a teammate asks an agent to look at replacing a service-to-service HTTP call with a direct Go package import, to shave off network latency.

  1. Explore, then propose. /opsx:explore reads both services, and /opsx:propose replace-http-with-direct-import drafts a proposal, a design doc weighing the latency win against the coupling cost, and a draft delta spec.
  2. Investigate and reject. After reviewing the design doc, the team decides the coupling cost – two independently deployed services now sharing a compile-time dependency – outweighs a latency win nobody has actually measured as a problem. Nothing gets built.
  3. Archive without syncing. Rather than deleting the folder, run openspec archive replace-http-with-direct-import --skip-specs, then add decision.md to the archived folder with Status: Rejected, the reasons above, and a Reconsider only if clause naming the condition that would change the answer – for example, “latency measurements show the HTTP hop is a proven bottleneck.” Rename the folder with a rejected- prefix so it reads as openspec/changes/archive/2026-09-16-rejected-replace-http-with-direct-import/.
  4. Months later, someone re-raises it. A different contributor, or the same agent in a fresh session, gets asked to “speed up the checkout-to-inventory call” and starts drafting a proposal that looks a lot like the same idea. Because openspec/config.yaml instructs the agent to search the archive first, it finds the rejected folder, reads decision.md, and reports back: “A materially similar change was proposed and rejected on 2026-09-16 for coupling reasons. The reconsideration condition was ’latency measurements show the HTTP hop is a proven bottleneck.’ Do you have new measurements, or is this a different problem?”
  5. The team supplies new evidence. If profiling now shows the HTTP hop genuinely dominates checkout latency, that is exactly the changed circumstance the original decision.md asked for. The agent proceeds with /opsx:propose, and the new proposal’s decision.md – once this one is also archived, accepted or rejected – references the prior one under Related, so the archive reads as a continuous decision history rather than two unconnected folders that happen to describe the same idea.

That fifth step is the entire point of the convention. Without it, step 4 either does not happen at all – the agent just re-investigates from zero – or it happens by luck, because a human remembered the prior conversation. The decision.md file and the archive-search instruction turn “someone might remember” into something the workflow actually checks.

OpenSpec’s Archive vs. a Dedicated ADR Log: Who Owns What

Once you are maintaining decision.md files inside the archive, it is worth being explicit about which artifact answers which question, so the convention does not quietly turn into duplicate documentation:

Artifact Answers
openspec/specs/ What does the system currently do?
openspec/changes/<name>/ (active) What are we proposing to change, right now?
openspec/changes/archive/<name>/ What changed (or was rejected) in the past, and why?
docs/adr/ (standalone, tool-neutral) What durable architectural rule did we learn, independent of any single change?

For a decision narrow enough to belong to one investigation – “we looked at sharing this persistence layer and said no” – the decision.md-inside-the-archive convention above is enough. For a decision that should outlive and constrain many future changes – “services communicate over HTTP, never shared packages” – promote it to a standalone Architecture Decision Record in docs/adr/, and have the rejected change’s decision.md reference it under Related. That split keeps OpenSpec’s archive focused on individual investigations while the ADR log holds the small number of rules that should survive any single tool’s lifecycle – including a future migration off OpenSpec entirely.

When to Adopt the spec-driven-with-adr Schema Instead

The hand-rolled convention above costs nothing and fits inside fifteen minutes of setup, which makes it the right default. But it is worth understanding what the more structured alternative actually does before you decide you have outgrown a naming prefix.

spec-driven-with-adr inserts a fifth artifact, adr, between design and tasks in OpenSpec’s pipeline. Rather than writing ADR content directly into the change folder, the adr step produces a short change-local adr.md review manifest and, when the change introduces a genuinely durable architectural commitment, a numbered record at the repository root – /adr/0042-use-postgres-for-catalog.md, sibling to openspec/, not nested inside it. Every ADR the schema creates is immutable once accepted: the schema’s own instructions call this out as an “iron rule” – you never edit an accepted record’s status, body, or date. To change a previous decision, you write a new ADR whose Supersedes: field names the old one, and future designs walk that supersession chain to know which decisions are still in force. That is a more rigorous version of exactly the “reconsider only if” idea in the decision.md convention above, enforced by the schema rather than left to a human remembering to write it down.

It is worth being precise about what this schema does and does not solve. It is built for decisions that get accepted and need to survive archiving – Postgres over DynamoDB, JWT over session cookies – not for proposals that were investigated and rejected without shipping anything. A rejected investigation still has nowhere obvious to live under this schema either; you would layer the same decision.md-and-naming convention from this guide on top of it, just referencing /adr/ records instead of a standalone docs/adr/ folder.

Reach for it once you notice any of these:

  • Your rejected-decision count is large enough that grepping openspec/changes/archive/ for prefixes stops being fast.
  • You want durable architectural decisions validated and cross-referenced against every new design automatically, rather than by convention and grep.
  • Multiple contributors keep inventing slightly different decision.md shapes, and you want a schema to enforce one immutable, numbered format instead.

Installing a custom schema is a bigger commitment than a naming convention – it changes what /opsx:propose generates for every future change, not just rejected ones – so treat it as a step up once the lightweight version is visibly straining, not a default first move.

Conclusion

OpenSpec’s archive was designed around one outcome – a change that shipped – and its own maintainers have been explicit, after a seven-month public discussion, that first-class rejection or ADR support is not coming to the core workflow soon. That leaves the fix where OpenSpec already puts most of its team conventions: in your repository, not in the tool. A decision.md file, the --skip-specs flag on archive, a rejected-/abandoned- naming prefix, and an explicit instruction telling the agent to search the archive before proposing are enough to stop most repeated investigations. Reach for the spec-driven-with-adr schema only once that lightweight convention is genuinely straining under the number of decisions you are tracking – and even then, keep the distinction clear: it manages decisions you accepted and want to survive archiving, not the ones you turned down.

Subscribe

Get new posts on AI systems, Infrastructure, and AI engineering.