Skip to content

feat(validate): accept zero-delta changes that declare skip_specs#1399

Open
clay-good wants to merge 11 commits into
mainfrom
fix/propose-guidance-zero-deltas
Open

feat(validate): accept zero-delta changes that declare skip_specs#1399
clay-good wants to merge 11 commits into
mainfrom
fix/propose-guidance-zero-deltas

Conversation

@clay-good

@clay-good clay-good commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1384.

Status: ready for re-review (round 2 review feedback addressed).

What was missing: the propose guidance said "leave Modified Capabilities empty if no requirement changes," but openspec validate unconditionally rejects a change with zero deltas (CHANGE_NO_DELTAS). A pure refactor — which Recipe 5 explicitly documents as having no spec delta — followed the guidance and then failed at validate time, after proposal/design/tasks were already written. There was no way to say "this change intentionally touches no specs."

What it does: a change can now declare skip_specs: true in its .openspec.yaml:

  • openspec validate accepts zero deltas for such a change (INFO note instead of ERROR). The marker is honored only when the file parses under the shared ChangeMetadataSchema — metadata that status/instructions would reject (e.g. missing schema) yields an explicit error instead of a silently-honored marker.
  • Marker + any spec file under specs/ is an explicit conflict error, never a silent drop.
  • The artifact graph counts specs-producing artifacts as complete for such changes (unblocking tasks), tracked separately so openspec status renders them as [~] skipped, not [x] done.
  • openspec instructions specs on such a change emits only a warning naming the marker (no creation directive or template), and the --json payload carries the same skipped/warning fields; instructions apply counts skipped artifacts as present, so custom schemas whose apply phase requires the specs artifact don't dead-end.
  • The conflict fires on any file under specs/ (root spec.md, headerless files, stray notes), and openspec archive runs the same check, so validate and archive always agree. A metadata file that mentions skip_specs but isn't valid YAML gets the explicit invalid-metadata error rather than a misleading zero-delta hint.
  • Agent-facing surfaces updated coherently: continue/update skill templates and docs/agent-contract.md document the new skipped status and instructions fields (skills regenerated, golden hashes updated); docs/cli.md documents the marker and the [~] rendering.
  • The CHANGE_NO_DELTAS guidance, propose/specs schema instructions, and proposal template now point at the marker ("do not invent a requirement just to satisfy validation"), and Recipe 5 shows it.
  • A forgotten specs phase is still caught: without the marker, zero deltas remain a hard error, unchanged.

Proof (real CLI output):

$ openspec validate refactor-payment-module --type change   # no marker
✗ [ERROR] file: Change must have at least one delta. ... set "skip_specs: true" ...

$ cat .openspec.yaml   # adversarial: marker without required schema field
skip_specs: true
$ openspec validate refactor-payment-module --type change
✗ [ERROR] .openspec.yaml: skip_specs is set but .openspec.yaml is not valid change
metadata, so the marker is not honored. Fix the metadata (schema: Invalid input: ...)
✗ [ERROR] file: Change must have at least one delta. ...

$ openspec validate refactor-payment-module --type change   # valid metadata + marker
Change 'refactor-payment-module' is valid
$ openspec status --change refactor-payment-module
Progress: 1/3 artifacts complete (1 skipped)
[x] proposal
[ ] design
[~] specs (skipped: change declares skip_specs)
[-] tasks (blocked by: design)

Tests cover accept/reject/conflict/malformed-metadata/adversarial-metadata paths (test/core/validation.skip-specs.test.ts), the graph unblock and skipped status (instruction-loader.test.ts), and the metadata schema. Full suite green except the 17 known environment-only zsh-installer failures (#1321).

Notes: per-change and explicit by design — unlike the project-wide requireSpecDeltas config proposed in #977, the marker is a reviewable statement in the change itself. Archive enforces the marker (conflict and unhonorable-metadata states block), and its behavior-critical paths are mutation-tested: reverting any of the 12 key behaviors kills at least one test. Deliberately out of scope: an unmarked zero-delta change still archives with only non-blocking warnings — that gap predates this PR (pinned by the existing archive tests) and closing it would change behavior for changes that never opted into anything; the archive code comment states the boundary explicitly.

The proposal instruction and template told authors to leave Modified
Capabilities 'empty if no requirement changes', but openspec validate
unconditionally rejects a change with zero deltas (CHANGE_NO_DELTAS).
An author following the guidance literally passed the propose phase and
only discovered the contradiction at validate time, after every artifact
was already written.

State the >=1-delta constraint where authors read it: the proposal
instruction, the specs instruction, and the proposal template comment.
Guidance-only; no validation behavior changes.

Fixes #1384

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clay-good
clay-good requested a review from TabishB as a code owner July 20, 2026 20:50
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The spec-driven workflow now supports explicit skip_specs: true metadata for zero-delta changes. Validation accepts those changes, rejects conflicts with spec files, and marks spec-producing artifacts as skipped and complete.

Changes

Explicit zero-delta workflow

Layer / File(s) Summary
Zero-delta guidance and metadata contract
schemas/spec-driven/..., src/core/change-metadata/schema.ts, docs/examples.md, .changeset/*
Guidance and examples document skip_specs: true, while change metadata validates it as an optional boolean.
skip_specs validation behavior
src/core/validation/*, test/core/validation.skip-specs.test.ts, test/utils/change-metadata.test.ts
Validation accepts explicit zero-delta changes, rejects spec-file conflicts and malformed metadata, and adds coverage for these cases.
Workflow completion and status handling
src/core/artifact-graph/instruction-loader.ts, src/core/change-status-policy.ts, src/commands/workflow/*, test/core/artifact-graph/instruction-loader.test.ts
Spec-generating artifacts are marked skipped and complete when specs are explicitly skipped, with updated status indicators, progress output, instructions, types, and tests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: tabishb, tabishb

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested skip_specs escape hatch and resolves the validate-vs-guidance contradiction in #1384.
Out of Scope Changes check ✅ Passed The changes stay focused on skip_specs validation, workflow handling, docs, and tests, with no unrelated features added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: validation now accepts zero-delta changes when they declare skip_specs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/propose-guidance-zero-deltas

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@schemas/spec-driven/templates/proposal.md`:
- Around line 18-20: Update the guidance comment in the proposal template to
include the pure-refactor requirement for a testable non-functional property,
matching the corresponding instructions in schema.yaml. Preserve the existing
capability/delta guidance and clarify that pure refactors must still define at
least one verifiable non-functional outcome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4c2c50c-57ad-47d7-9a5a-362952053446

📥 Commits

Reviewing files that changed from the base of the PR and between 60f720c and 149b95e.

📒 Files selected for processing (2)
  • schemas/spec-driven/schema.yaml
  • schemas/spec-driven/templates/proposal.md

Comment thread schemas/spec-driven/templates/proposal.md Outdated
…emplate

Address review: the schemas files use plain hyphens, not em-dashes; the
proposal template comment now carries the same pure-refactor advice as the
schema instruction (CodeRabbit suggestion); the specs-instruction note moves
below the per-capability bullets so it no longer interrupts the list intro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trades the late validation failure for a worse contract: pure refactors are now told to invent a changed requirement, while docs/examples.md still says they have no delta and docs/concepts.md says implementation-only changes do not belong in specs. Please model true no-behavior work as an explicit skipped/not-applicable specs state, or make the hard-minimum policy a coordinated product and documentation change rather than updating only the schema prompt.

A pure refactor has no spec deltas by design (docs/examples.md Recipe 5),
but openspec validate hard-rejected every zero-delta change, so the propose
guidance either contradicted the validator or pushed authors to invent
requirements. Model the no-behavior-change case explicitly instead:
skip_specs: true in the change's .openspec.yaml makes validate accept zero
deltas (and error when the marker coexists with delta spec files), marks
specs-producing artifacts complete so the workflow isn't blocked on files
that must not exist, and the propose/specs guidance now points at the
marker rather than telling refactors to fabricate a delta.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying openspec-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 97196c8
Status: ✅  Deploy successful!
Preview URL: https://5df6b1c4.openspec-docs.pages.dev
Branch Preview URL: https://fix-propose-guidance-zero-de.openspec-docs.pages.dev

View logs

@clay-good clay-good changed the title fix(schemas): align propose guidance with validate's one-delta minimum feat(validate): accept zero-delta changes that declare skip_specs Jul 21, 2026
@clay-good

Copy link
Copy Markdown
Collaborator Author

Reworked per the review: the previous revision kept the hard one-delta minimum and told pure refactors to name a changed guarantee, which contradicted docs/examples.md Recipe 5 and concepts.md. This revision takes the suggested first option — an explicit not-applicable specs state. skip_specs: true in the change's .openspec.yaml makes validate accept zero deltas (and error if the marker coexists with delta files), unblocks the artifact graph, and the guidance/docs now describe that contract instead of asking authors to invent requirements. Without the marker, zero deltas still hard-fail, so the late-discovery footgun from #1384 stays fixed for changes that simply forgot their specs.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new direction fixes the fake-requirement contradiction, but changeDeclaresSkipSpecs() trusts raw skip_specs: true without validating the metadata shape: I reproduced a .openspec.yaml containing only that field, with the required schema missing, passing as valid while status/instructions reject the same metadata. Please validate the marker through the shared metadata schema (and add the adversarial case), and surface specs as explicitly skipped/not-applicable in status rather than collapsing them to [x] complete.

Review follow-ups: the marker was read from raw YAML, so metadata that
status/instructions reject (e.g. skip_specs without the required schema
field) silently passed validate; it now only counts when the file parses
under ChangeMetadataSchema, and a set-but-invalid marker surfaces as an
explicit error. Status also stops presenting the synthesized specs
completion as done: skipped artifacts render as '[~] specs (skipped:
change declares skip_specs)' and drop out of the progress denominator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator Author

Both round-2 points addressed in 215494a:

  1. Marker validated through the shared schema. readSkipSpecsMarker now runs the file through ChangeMetadataSchema; the marker only counts when the metadata parses. Your repro — .openspec.yaml containing only skip_specs: true — now fails validate with an explicit .openspec.yaml error naming the missing field, plus the unchanged zero-delta error, matching what status/instructions reject. Adversarial tests added for both validateChangeDeltaSpecs and validateChange.

  2. Skipped is now a first-class status. loadChangeContext tracks synthesized completions in skippedArtifacts (kept in completed so dependents unblock), ArtifactStatus gained 'skipped', and openspec status renders [~] specs (skipped: change declares skip_specs) with the skipped artifact excluded from the progress denominator instead of [x] complete.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/core/validation.skip-specs.test.ts`:
- Around line 109-119: Update Validator.validateChange so the CHANGE_NO_DELTAS
issue is filtered only when readSkipSpecsMarker(changeDir) reports valid
metadata, not merely when the marker is declared; preserve the existing
filtering behavior for valid skip_specs metadata and retain the no-deltas error
for invalid metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1c00c9d8-d600-4b32-bd61-254cb5087582

📥 Commits

Reviewing files that changed from the base of the PR and between 25bb2fe and 215494a.

📒 Files selected for processing (10)
  • .changeset/skip-specs-explicit-zero-delta.md
  • docs/examples.md
  • src/commands/workflow/shared.ts
  • src/commands/workflow/status.ts
  • src/core/artifact-graph/instruction-loader.ts
  • src/core/change-status-policy.ts
  • src/core/validation/constants.ts
  • src/core/validation/validator.ts
  • test/core/artifact-graph/instruction-loader.test.ts
  • test/core/validation.skip-specs.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • .changeset/skip-specs-explicit-zero-delta.md
  • test/core/artifact-graph/instruction-loader.test.ts
  • src/core/validation/constants.ts
  • src/core/validation/validator.ts

Comment thread test/core/validation.skip-specs.test.ts
…_specs change

Requesting instructions for an artifact the change skips would otherwise
hand an agent 'create spec files' guidance whose output validate then
rejects as a conflict. The instructions payload now carries skipped: true
(JSON included) and the text output opens with a warning naming the marker
and the way out (remove skip_specs if requirements now change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/workflow/instructions.ts`:
- Around line 192-199: Update printInstructionsText so when instructions.skipped
is true, it stops before emitting the creation directive and output path, or
otherwise makes the remaining instructions explicitly non-actionable. Preserve
the existing warning text and normal creation instructions for non-skipped
artifacts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f66235ae-0361-4f11-82bf-21c3fcc9161e

📥 Commits

Reviewing files that changed from the base of the PR and between 215494a and 8814ccc.

📒 Files selected for processing (3)
  • src/commands/workflow/instructions.ts
  • src/core/artifact-graph/instruction-loader.ts
  • test/core/artifact-graph/instruction-loader.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/core/artifact-graph/instruction-loader.test.ts
  • src/core/artifact-graph/instruction-loader.ts

Comment thread src/commands/workflow/instructions.ts Outdated
clay-good and others added 6 commits July 20, 2026 20:18
… too

validateChangeDeltaSpecs already explains when a set marker is not honored
because the metadata fails the shared schema; validateChange (archive's
non-blocking proposal pass) kept only the bare no-deltas error. Both paths
now surface the same invalid-metadata message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tifact

The warning was followed by the normal creation directive, output path,
and template - a contradictory prompt that could still push an agent to
write spec files validate rejects as conflicting. The text output for a
skipped artifact is now the warning alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y, and agent surfaces

Deep-check follow-ups before re-review:

- The marker read moves to utils (readSkipSpecsMarker) and is shared by
  validate and archive; a metadata file that mentions skip_specs but is
  not valid YAML now gets the explicit invalid-metadata error instead of
  silently falling back to the zero-delta error whose guidance tells the
  user to set the very marker they set.
- The marker conflict now fires on ANY file under specs/ (root spec.md,
  headerless files, stray notes) - previously only parsed deltas counted,
  so validate and the artifact graph's specs/** glob could disagree, and
  archive skipped the check entirely for headerless files. Archive now
  runs delta validation for a marked change with any spec content, so the
  two commands agree.
- instructions apply treats skipped artifacts as present when computing
  missingArtifacts: a custom schema whose apply phase requires the
  specs-producing artifact (or has no apply block) no longer reports a
  marked change permanently blocked while status calls it complete.
- The instructions JSON payload carries the do-not-create warning
  (skipped + warning fields), not just the text output, since workflow
  skills drive the CLI in --json mode.
- continue/update-change skill templates and docs/agent-contract.md now
  document the 'skipped' artifact status and the new instructions fields;
  skills/ regenerated and golden hashes updated; cli.md documents the
  marker under validate and the [~] status rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion, agent-journey gaps

Correctness round: a non-boolean skip_specs (skip_specs: "yes") was
silently ignored while the zero-delta guidance told the user to set the
marker they had set - the unhonorable-marker error now fires on key
presence, not value. The any-file conflict probe ran unconditionally
outside the historical error handling, crashing validate with raw
ENOTDIR/EACCES where the old path degraded to the no-deltas error - it is
now gated on a declared marker and fails closed into the conflict, in both
validate and archive. Dot-files (.DS_Store, .gitkeep) no longer trigger
the conflict - hasAnyFileUnder skips dot entries like discoverSpecFiles.
The invalid-YAML heuristic is anchored so comments mentioning skip_specs
don't claim the marker was set.

Coherence round: instructions for dependents no longer present a skipped
specs artifact as a done dependency with files to read - it renders as
status="skipped" with no path, and DependencyInfo carries skipped for
JSON consumers. The propose/ff skill completion loops accept skipped
alongside done, so schemas without an apply block can't deadlock an agent
waiting for specs to become done. Archive also forces validation when the
marker is mentioned but unhonorable, the validate next-steps footer stops
telling marker changes to add deltas, and docs cover the un-skip path,
the instructions JSON fields, the flag/marker distinction, and the
.openspec.yaml fields. Skills regenerated; golden hashes updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… unguarded

A mutation pass showed two surviving mutations: disabling archive's
marker enforcement (the only place the marker gates a destructive
operation) and disabling the marker-aware next-steps footer both left
the whole suite green. New tests kill both: archive of a marked change
blocks on stray spec files and on unhonorable metadata (and still
archives cleanly with none), and the validate footer shows the marker
bullets on a conflict while a plain zero-delta change keeps the
delta-authoring bullets (the generic guidance mentions skip_specs and
must not flip the branch). Kills verified by re-running both mutations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…; distinct footer for unhonorable markers

Fourth review round (alfred-style simulator, mutation adversary, cold
reader):

- The archive skill - the terminal step of the skip_specs journey - still
  told agents to warn that any non-done artifact is incomplete, so the one
  change every other surface calls complete triggered a false confirmation
  prompt. Both prompt variants now accept done or skipped; skills
  regenerated, hashes updated.
- The validate/change next-steps footer now distinguishes the two marker
  errors: a conflict leads with deleting the spec files, an unhonorable
  marker (invalid metadata, no files) leads with fixing .openspec.yaml
  instead of telling the user to delete files that don't exist.
- The archive comment claiming 'archive never accepts a change validate
  rejects' overclaimed: an UNMARKED zero-delta change still archives with
  only non-blocking warnings, a pre-existing gap this PR does not change.
  The comment now says exactly that.
- validateChange's marker scope documented (proposal-level pass; the
  conflict gate lives in validateChangeDeltaSpecs); the skip_specs schema
  comment and agent-contract now state the generates-under-specs/ prefix
  as the contract custom schemas inherit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One remaining cross-surface trust gap: readSkipSpecsMarker validates only the Zod shape, not whether schema actually resolves. I reproduced .openspec.yaml with schema: does-not-exist and skip_specs: true: openspec validate refactor reports valid with exit 0, while openspec status rejects the same metadata; archive can likewise bypass delta validation when specs/ is empty. Please route the marker through the same resolvable-metadata contract as status and instructions, then add this adversarial case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feedback: propose phase's 'leave empty if no requirement changes' guidance conflicts with validate's hard CHANGE_NO_DELTAS >=1 constraint

2 participants