Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/prevent-silent-spec-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@fission-ai/openspec": minor
---

Prevent silent spec drop: `openspec archive` and `openspec validate` now share one schema-aware delta gate, so a spec-driven change can no longer archive while leaving `openspec/specs/` stale.

- **Archive blocks silent drops.** When a change's schema produces delta specs, `openspec archive` now refuses (and `openspec validate` reports invalid) for: no delta specs at all (`CHANGE_NO_DELTAS`), a declared capability with no delta spec (partial drop), or a `specs/<cap>/spec.md` written as a full spec instead of a delta (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`). `--yes` no longer bypasses validation. Use `--skip-specs` (or `--no-validate`) to intentionally archive without specs.
- **Schema-aware (#997).** The delta requirement applies only when the change's schema actually produces delta specs, so proposal-only / lighter schemas now pass `validate` and `archive` without delta specs.
- **Declared-capability coverage.** Every capability listed in a proposal's `## Capabilities` must have a matching `specs/<id>/spec.md`, enforced by `validate` and `archive`.
- **Archive skills call the CLI.** `/opsx:archive` and the bulk-archive workflow now run `openspec archive` (which validates, syncs, and moves) instead of judging sync state and moving files agent-side — so the guarantees above hold for agent workflows too.
- **Apply is enforceable.** `openspec instructions apply` exits non-zero when blocked, and the `spec-driven` apply gate requires `specs`.
- **Recovery.** `openspec validate --archived` audits already-archived changes for capabilities that never reached `openspec/specs/`.
2 changes: 2 additions & 0 deletions openspec/changes/prevent-silent-spec-drop/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29
168 changes: 168 additions & 0 deletions openspec/changes/prevent-silent-spec-drop/design.md

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions openspec/changes/prevent-silent-spec-drop/proposal.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
## MODIFIED Requirements

### Requirement: Archive Validation

The archive command SHALL validate changes before applying them to ensure data integrity. When validation is enabled, the command SHALL run delta-spec validation and declared-capability coverage consistently with `openspec validate` — that is, whenever the user has not opted out of spec updates via `--skip-specs` — rather than only when delta specs already exist. A change that has no delta specs SHALL be blocked with the same `CHANGE_NO_DELTAS` error that `openspec validate` reports; a change whose spec file is present but not in delta format SHALL be blocked with the same "No delta sections found" error; and a change that declares capabilities it has not delivered as delta specs SHALL be blocked with the same coverage errors — instead of being silently archived with missing, malformed, or partial spec updates. This delta requirement SHALL apply only when the change's schema graph includes a `specs` artifact, so schemas that legitimately have no delta specs are not forced to provide them.

#### Scenario: Pre-archive validation

- **WHEN** executing `openspec archive change-name`
- **THEN** validate the change structure first
- **AND** only proceed if validation passes
- **AND** show validation errors if it fails

#### Scenario: No delta specs blocks archive

- **WHEN** executing `openspec archive change-name` without `--skip-specs` or `--no-validate`
- **AND** the change has no delta specs under `changes/change-name/specs/` (the directory is absent or contains no delta sections)
- **THEN** archive validation fails with the `CHANGE_NO_DELTAS` error
- **AND** the archive is aborted with a non-zero exit code
- **AND** the change is not moved to the archive directory

#### Scenario: --yes does not bypass the validation guard

- **WHEN** executing `openspec archive change-name --yes` without `--skip-specs` or `--no-validate`
- **AND** the change has no delta specs (under a schema that produces delta specs)
- **THEN** the archive is still blocked with `CHANGE_NO_DELTAS` and a non-zero exit code
- **AND** the change is not moved to the archive directory
- **AND** `--yes` only suppresses confirmation prompts, never the validation gate

#### Scenario: Non-delta-format spec blocks archive

- **WHEN** executing `openspec archive change-name` without `--skip-specs` or `--no-validate`
- **AND** `changes/change-name/specs/<cap>/spec.md` exists but contains no delta sections (it is a full spec)
- **THEN** archive validation fails with the "No delta sections found" error for that file
- **AND** the archive is aborted with a non-zero exit code
- **AND** the change is not moved to the archive directory

#### Scenario: Schema without a specs artifact is exempt

- **WHEN** executing `openspec archive change-name` under a schema whose graph has no `specs` artifact
- **AND** the change has no delta specs
- **THEN** the delta requirement does not apply
- **AND** the archive proceeds (no `CHANGE_NO_DELTAS` block)

#### Scenario: Declared but undelivered capability blocks archive

- **WHEN** executing `openspec archive change-name` without `--skip-specs` or `--no-validate`
- **AND** the proposal declares a capability that has no matching delta spec under `changes/change-name/specs/`
- **THEN** archive validation fails with the declared-capability coverage error for that capability
- **AND** the archive is aborted with a non-zero exit code
- **AND** the change is not moved to the archive directory

#### Scenario: Archive validation parity with validate command

- **WHEN** a change would be reported invalid by `openspec validate change-name`
- **THEN** `openspec archive change-name` (with validation enabled) blocks on the same errors
- **AND** does not archive the change

#### Scenario: Force archive without validation

- **WHEN** executing `openspec archive change-name --no-validate`
- **THEN** skip validation (unsafe mode)
- **AND** show warning about skipping validation

### Requirement: Skip Specs Option

The archive command SHALL support a `--skip-specs` flag that skips all spec update operations and proceeds directly to archiving. Because `--skip-specs` declares that the change intentionally has no specs to sync, it SHALL also skip delta-spec validation, so a change with no delta specs can still be archived under this explicit opt-out.

#### Scenario: Skipping spec updates with flag

- **WHEN** executing `openspec archive <change> --skip-specs`
- **THEN** skip spec discovery and update confirmation
- **AND** skip the delta-spec validation that would otherwise block a change with no delta specs
- **AND** proceed directly to moving the change to archive
- **AND** display a message indicating specs were skipped

#### Scenario: Skip-specs allows a spec-less change to archive

- **WHEN** executing `openspec archive <change> --skip-specs` on a change with no delta specs
- **THEN** the `CHANGE_NO_DELTAS` block does not apply
- **AND** the change is archived
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## MODIFIED Requirements

### Requirement: Apply Instructions Command

The system SHALL generate schema-aware apply instructions via `openspec instructions apply`. When the apply phase is blocked by missing required artifacts, the command SHALL exit with a non-zero status so that scripts, loops, and agents stop instead of proceeding past a printed warning.

#### Scenario: Generate apply instructions

- **WHEN** user runs `openspec instructions apply --change <id>`
- **AND** all required artifacts (per schema's `apply.requires`) exist
- **THEN** the system outputs:
- `contextFiles` mapping artifact IDs to arrays of concrete paths for all existing artifacts
- Schema-specific instruction text
- Progress tracking file path (if `apply.tracks` is set)
- **AND** exits with status code 0

#### Scenario: Apply blocked by missing artifacts

- **WHEN** user runs `openspec instructions apply --change <id>`
- **AND** required artifacts are missing
- **THEN** the system indicates apply is blocked
- **AND** lists which artifacts must be created first
- **AND** exits with a non-zero status code

#### Scenario: Apply blocked in JSON mode

- **WHEN** user runs `openspec instructions apply --change <id> --json`
- **AND** required artifacts are missing
- **THEN** the system prints the full JSON payload with `state` set to `blocked`
- **AND** exits with a non-zero status code

#### Scenario: Apply instructions JSON output

- **WHEN** user runs `openspec instructions apply --change <id> --json`
- **THEN** the system outputs JSON with:
- `contextFiles`: object mapping artifact IDs to arrays of concrete paths for existing artifacts
- `instruction`: the apply instruction text
- `tracks`: path to progress file or null
- `applyRequires`: list of required artifact IDs

### Requirement: Schema Apply Block

The system SHALL support an `apply` block in schema definitions that controls when and how implementation begins. For the built-in `spec-driven` schema, the apply gate SHALL include the `specs` artifact so that a change with no delta specs is reported as blocked rather than ready.

#### Scenario: Schema with apply block

- **WHEN** a schema defines an `apply` block
- **THEN** the system uses `apply.requires` to determine which artifacts must exist before apply
- **AND** uses `apply.tracks` to identify the file for progress tracking (or null if none)
- **AND** uses `apply.instruction` for guidance shown to the agent

#### Scenario: Schema without apply block

- **WHEN** a schema has no `apply` block
- **THEN** the system requires all artifacts to exist before apply is available
- **AND** uses default instruction: "All artifacts complete. Proceed with implementation."

#### Scenario: Spec-driven requires specs before apply

- **WHEN** a `spec-driven` change has `tasks.md` but no delta specs under `specs/`
- **THEN** `openspec instructions apply` reports the apply phase as blocked
- **AND** lists `specs` among the missing required artifacts
- **AND** exits with a non-zero status code
143 changes: 143 additions & 0 deletions openspec/changes/prevent-silent-spec-drop/specs/cli-validate/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
## ADDED Requirements

### Requirement: Schema-Aware Delta Validation

`openspec validate` SHALL gate delta-spec validation (including the at-least-one-delta `CHANGE_NO_DELTAS` rule) on whether the change's schema produces delta specs, so that `validate` and `archive` apply the same rule and proposal-only schemas are not forced to have delta specs. A schema produces delta specs when any of its artifacts' `generates` glob writes under `specs/`. This SHALL apply to both single-change and bulk (`--changes` / `--all`) validation.

#### Scenario: Spec-driven change with no delta specs fails

- **WHEN** validating a change whose schema produces delta specs (e.g. `spec-driven`)
- **AND** the change has no delta specs under `specs/`
- **THEN** validation fails with the `CHANGE_NO_DELTAS` error and a non-zero exit code

#### Scenario: Proposal-only schema with no delta specs passes

- **WHEN** validating a change whose schema does NOT produce delta specs (no artifact generates under `specs/`)
- **AND** the change has no delta specs
- **THEN** the `CHANGE_NO_DELTAS` rule does not apply
- **AND** the change is not marked invalid on account of missing delta specs

#### Scenario: Validate and archive agree on the delta requirement

- **WHEN** the same change is checked by `openspec validate` and (with validation enabled) by `openspec archive`
- **THEN** both apply the identical schema-aware delta gate and reach the same verdict on whether delta specs are required

#### Scenario: Indeterminate schema defaults to requiring deltas

- **WHEN** resolving the change's schema throws (e.g. a malformed project schema), as distinct from absent metadata which resolves to the default schema
- **THEN** delta-spec validation is applied (fail-safe to the current strict behavior), not skipped

#### Scenario: Schema resolution is memoized within a run

- **WHEN** bulk validation (`--changes` / `--all`) checks many changes
- **THEN** `schemaProducesDeltaSpecs` is resolved at most once per distinct schema name, not once per change

### Requirement: Declared Capability Coverage

Validating a change SHALL deterministically check that every capability declared in the proposal's `## Capabilities` section has a corresponding delta spec present, so a change cannot pass validation while silently omitting specs it promised. The check SHALL be a pure function of the proposal text and the change's `specs/` directory listing: the same inputs always produce the same result. The check SHALL apply only when the change's schema graph includes a `specs` artifact, so schemas that legitimately have no delta specs are never required to declare them.

A declared capability is resolved as follows:

- The `### New Capabilities` and `### Modified Capabilities` subsections under `## Capabilities` are considered. Both heading form (`### New Capabilities`) and bold-label form (`**New Capabilities**:`) SHALL be accepted, so a proposal written to the documented template is never under-extracted.
- A capability id is the first inline-code span (`` `id` ``) of a top-level `- ` bullet within those subsections.
- A capability id that is not kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`) SHALL be reported as a malformed-capability warning rather than silently ignored, so a mis-cased declaration cannot make coverage pass vacuously. Inline code that is not the first span of a capability bullet (paths, prose) is not treated as a capability.
- `None`, `_None_`, HTML-comment, and empty subsections declare nothing.

A declared capability `id` is covered when `specs/<id>/spec.md` is present. For a Modified capability, the declared id MUST be the existing spec folder name (as the schema instruction directs), which is the same key `findSpecUpdates` uses — so coverage and sync stay aligned. Coverage checks presence only; whether a present spec file is a well-formed delta (uses `## ADDED/MODIFIED/REMOVED/RENAMED Requirements` headers) is enforced by the existing delta-spec validation, so a present-but-non-delta file produces one precise "No delta sections found" error rather than a duplicate. Coverage is one-directional: delta specs for capabilities not declared in the proposal SHALL NOT be flagged.

#### Scenario: Declared capability has no spec file

- **WHEN** validating a change (under a schema with a `specs` artifact) whose proposal declares capability `foo`
- **AND** no `specs/foo/spec.md` exists
- **THEN** validation reports an ERROR identifying `foo` as a declared capability missing its delta spec at `specs/foo/spec.md`
- **AND** the error guidance explains that the `## Capabilities` id must match the delta spec folder name (for a Modified capability, the existing `openspec/specs/<id>/` folder)
- **AND** the change is invalid (non-zero exit)

#### Scenario: Bold-label capabilities form is recognized

- **WHEN** a proposal declares capabilities using bold labels (`**New Capabilities**:` / `**Modified Capabilities**:`) rather than `###` headings
- **THEN** the declared capabilities are extracted the same way as the heading form

#### Scenario: Mis-cased capability id is reported, not dropped

- **WHEN** a declared capability bullet's id is not kebab-case (e.g. `User-Auth`)
- **THEN** validation reports a malformed-capability warning naming the bullet
- **AND** does not silently omit it from coverage

#### Scenario: Capability deletion or rename uses explicit opt-out

- **WHEN** a change's purpose is to delete or rename a whole capability/spec folder, for which there is no natural delta spec
- **THEN** the user explicitly passes `--skip-specs` (the skill never does so on its own) and coverage does not block
- **AND** removing a capability's requirements (rather than the whole capability) is expressed as a `## REMOVED Requirements` delta under the existing folder, which satisfies coverage normally

#### Scenario: Declared capability present but not in delta format

- **WHEN** a proposal declares capability `foo`
- **AND** `specs/foo/spec.md` exists but contains no delta sections (it is a full spec)
- **THEN** the delta-spec validation reports a "No delta sections found" ERROR for `specs/foo/spec.md`
- **AND** the coverage check does not additionally report `foo` as missing

#### Scenario: One ERROR per missing capability

- **WHEN** a proposal declares capabilities `foo`, `bar`, and `baz`
- **AND** only `specs/foo/spec.md` exists
- **THEN** validation reports a distinct ERROR for `bar` and for `baz`

#### Scenario: All declared capabilities covered

- **WHEN** every capability declared in the proposal has a matching `specs/<id>/spec.md`
- **THEN** the coverage check contributes no errors

#### Scenario: Schema without a specs artifact is exempt

- **WHEN** validating a change whose schema graph has no `specs` artifact
- **THEN** the coverage check is not applied and contributes no errors

#### Scenario: Proposal without a Capabilities section

- **WHEN** validating a change whose proposal has no `## Capabilities` section
- **THEN** the coverage check declares nothing and contributes no errors
- **AND** other validation (such as the at-least-one-delta rule) is unaffected

#### Scenario: Empty or None subsection declares nothing

- **WHEN** a `### Modified Capabilities` subsection contains only `None`, `_None_`, an HTML comment, or no bullets
- **THEN** no capability is declared from that subsection

#### Scenario: Capability id is the first inline-code token

- **WHEN** a declared capability bullet is `` - `tool-command-surface`: classifies tools as `adapter` or `none` ``
- **THEN** the declared id is `tool-command-surface`
- **AND** the inline code in the description (`adapter`, `none`) is ignored

#### Scenario: Undeclared delivered spec is not flagged

- **WHEN** a change ships `specs/extra/spec.md` for a capability not listed in the proposal
- **THEN** the coverage check does not report an error for `extra`

#### Scenario: Coverage applies in single and bulk validation

- **WHEN** running `openspec validate <change>` or `openspec validate --changes` (including the interactive single-item path, which shares the same validation routine)
- **THEN** the declared-capability coverage check runs for each validated change

### Requirement: Archived Spec-Drift Audit

`openspec validate` SHALL provide a deterministic audit that detects already-archived changes whose declared capabilities never reached `openspec/specs/`, so teams who accumulated silent drift before this change can find it. The audit is detection only; it does not regenerate lost spec content.

#### Scenario: Archived change with un-synced capability is flagged

- **WHEN** the audit runs over `openspec/changes/archive/`
- **AND** an archived change's proposal declares a capability with no corresponding `openspec/specs/<id>/spec.md`
- **THEN** the audit reports that archived change and the missing capability as drift

#### Scenario: Audit is forward-only and says so

- **WHEN** an archived change's proposal predates the `## Capabilities` contract (its capabilities cannot be extracted)
- **THEN** the audit does not report false drift for it
- **AND** the audit output notes that pre-contract archives are not auditable, rather than implying completeness

#### Scenario: Audit does not regenerate content

- **WHEN** drift is detected
- **THEN** the audit points the user at what to rebuild
- **AND** does not write or fabricate spec content
Loading
Loading