Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/skip-specs-explicit-zero-delta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": minor
---

Add `skip_specs: true` change metadata for work with no spec-level behavior change (pure refactors, tooling, docs). `openspec validate` accepts a zero-delta change that declares the marker (honored only when the metadata parses under the shared change-metadata schema) and errors when the marker and delta specs are both present, the artifact graph no longer blocks `tasks` on spec files for such changes, `openspec status` renders the specs stage as explicitly skipped, and the propose/specs guidance points to the marker instead of contradicting the validator.
4 changes: 2 additions & 2 deletions docs/agent-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id
`{ "items": [ { "id", "type": "change"|"spec", "valid", "issues": [ { "level", "path", "message", "line"?, "column"? } ], "durationMs" } ], "summary": { "totals": {items,passed,failed}, "byType": {...} }, "version": "1.0", "root" }`. Exit 1 when any item fails.

### 4.4 `status --json`
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"ready"|"blocked", missingDeps?} ], "root" }`. No active changes: `{ "changes": [], "message", "root" }`, exit 0.
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", missingDeps?} ], "root" }`. `"skipped"` marks an artifact whose `generates` path is under `specs/` in a change whose `.openspec.yaml` declares `skip_specs: true`; it satisfies dependencies but must not be created. No active changes: `{ "changes": [], "message", "root" }`, exit 0.

### 4.5 `instructions <artifact> --json`
`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "template", "dependencies": [{id,done,path,description}], "unlocks", "root" }`.
`{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "skipped"?, "warning"?, "template", "dependencies": [{id,done,path,description,skipped?}], "unlocks", "root" }`. `"skipped": true` (with `"warning"`) appears when the change declares `skip_specs: true` and this artifact is skipped — do not create its files. A dependency entry with `skipped: true` is satisfied without files — do not try to read its paths.

`ReferenceIndexEntry`: `{ "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] }` — resolved entries carry root/specs/fetch; unresolved carry store_id + warning status. Index capped at 50KB (`reference_index_truncated`).

Expand Down
8 changes: 7 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ Validate changes and specs for structural issues.
openspec validate [item-name] [options]
```

A change with zero spec deltas fails validation unless its `.openspec.yaml` declares `skip_specs: true` (for pure refactors, tooling, or docs work — see [Recipe 5](examples.md#recipe-5-a-refactor-with-no-behavior-change)).

**Arguments:**

| Argument | Required | Description |
Expand Down Expand Up @@ -587,7 +589,7 @@ openspec archive [change-name] [options]
| Option | Description |
|--------|-------------|
| `-y, --yes` | Skip confirmation prompts |
| `--skip-specs` | Skip spec updates (for infrastructure/tooling/doc-only changes) |
| `--skip-specs` | Skip spec updates for one archive run. A change that permanently has no spec deltas should declare `skip_specs: true` in its `.openspec.yaml` instead — it archives with no flag |
| `--no-validate` | Skip validation (requires confirmation) |

**Examples:**
Expand Down Expand Up @@ -693,6 +695,8 @@ Progress: 2/4 artifacts complete
[-] tasks (blocked by: design)
```

A change that declares `skip_specs: true` shows its specs stage as `[~] specs (skipped: change declares skip_specs)` and excludes it from the progress count.

**Output (JSON):**

```json
Expand Down Expand Up @@ -759,6 +763,8 @@ openspec instructions design --change add-dark-mode --json
- Content from dependency artifacts
- Per-artifact rules from config

For an artifact skipped via `skip_specs: true`, the output is a warning only (JSON adds `skipped`/`warning` fields) — the artifact must not be created.

---

### `openspec templates`
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ openspec/changes/add-dark-mode/
├── proposal.md # Why and what
├── design.md # How (technical approach)
├── tasks.md # Implementation checklist
├── .openspec.yaml # Change metadata (optional)
├── .openspec.yaml # Change metadata (optional): schema, created, skip_specs
└── specs/ # Delta specs
└── ui/
└── spec.md # What's changing in ui/spec.md
Expand Down
11 changes: 10 additions & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,16 @@ AI: Created the change. The proposal states the goal (split the
Ready for implementation.
```

When you archive a change that doesn't touch specs, you can tell the terminal command to skip the spec step:
Declare the empty delta explicitly by setting `skip_specs: true` in the change's `.openspec.yaml`:

```yaml
schema: spec-driven
skip_specs: true
```

Without the marker, `openspec validate` rejects a change with zero deltas (so a forgotten specs phase still gets caught); with it, validation passes and `openspec status` shows the specs stage as explicitly skipped rather than pending. If the refactor turns out to change behavior after all, remove `skip_specs` from `.openspec.yaml` and write the delta specs — validate treats the marker plus spec files as a conflict, so the stale marker can't linger silently.

Archiving a marked change needs no extra flags (there are no deltas to merge). Independently, the `--skip-specs` flag tells the terminal command to skip the spec step explicitly:

```bash
$ openspec archive refactor-payment-module --skip-specs
Expand Down
13 changes: 13 additions & 0 deletions schemas/spec-driven/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ artifacts:
proposal and specs phases. Research existing specs before filling this in.
Each capability listed here will need a corresponding spec file.

Every change must either declare at least one capability (new or
modified) or explicitly opt out of specs: `openspec validate` rejects a
change with zero deltas unless the change's `.openspec.yaml` sets
`skip_specs: true`. Use `skip_specs: true` only when no spec-level
behavior changes (pure refactor, tooling, docs) - specs describe
behavior, so if behavior does not change, no spec should change either.
Do not invent a requirement just to satisfy validation.

Keep it concise (1-2 pages). Focus on the "why" not the "how" -
implementation details belong in design.md.

Expand Down Expand Up @@ -55,6 +63,11 @@ artifacts:
- New capabilities: use the exact kebab-case name from the proposal (specs/<capability>/spec.md).
- Modified capabilities: use the existing spec folder name from openspec/specs/<capability>/ when creating the delta spec at specs/<capability>/spec.md.

There must be at least one spec file unless the change's `.openspec.yaml`
sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
rejects a zero-delta change without that marker. If the proposal lists no
capabilities and `skip_specs` is not set, revisit the proposal first.

Delta operations (use ## headers):
- **ADDED Requirements**: New capabilities
- **MODIFIED Requirements**: Changed behavior - MUST include full updated content
Expand Down
6 changes: 5 additions & 1 deletion schemas/spec-driven/templates/proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
### Modified Capabilities
<!-- Existing capabilities whose REQUIREMENTS are changing (not just implementation).
Only list here if spec-level behavior changes. Each needs a delta spec file.
Use existing spec names from openspec/specs/. Leave empty if no requirement changes. -->
Use existing spec names from openspec/specs/. Leave empty if no requirement
changes. A change with no capabilities at all (pure refactor, tooling, docs)
must set `skip_specs: true` in its .openspec.yaml - openspec validate rejects
a zero-delta change without that marker. Do not invent a requirement just to
satisfy validation. -->
- `<existing-name>`: <what requirement is changing>

## Impact
Expand Down
4 changes: 2 additions & 2 deletions skills/openspec-archive-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ Archive a completed change in the experimental workflow.
Parse the JSON to understand:
- `schemaName`: The workflow being used
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context
- `artifacts`: List of artifacts with their status (`done` or other)
- `artifacts`: List of artifacts with their status (`done`, `skipped`, or other)

**If any artifacts are not `done`:**
**If any artifacts are neither `done` nor `skipped`** (skipped artifacts satisfy the requirement - the change declares skip_specs):
- Display warning listing incomplete artifacts
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
Expand Down
5 changes: 3 additions & 2 deletions skills/openspec-continue-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Continue working on a change by creating the next artifact.
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
- `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.

Expand Down Expand Up @@ -65,7 +65,8 @@ Continue working on a change by creating the next artifact.
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
- `dependencies`: Completed artifacts to read for context
- `dependencies`: Completed artifacts to read for context (entries with `skipped: true` have no files - do not look for them)
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - pick another artifact
- **Create the artifact file**:
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
- Use `template` as the structure - fill in its sections
Expand Down
5 changes: 3 additions & 2 deletions skills/openspec-ff-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
Expand All @@ -66,8 +67,8 @@ Fast-forward through artifact creation - generate everything needed to start imp

b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- Check if every artifact ID in `applyRequires` has `status: "done"` or `status: "skipped"` in the artifacts array (skipped artifacts satisfy the requirement - do not try to create them)
- Stop when all `applyRequires` artifacts are done or skipped

c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
Expand Down
5 changes: 3 additions & 2 deletions skills/openspec-propose/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ When ready to implement, run /opsx:apply
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `skipped`/`warning`: present when the change declares skip_specs and this artifact must NOT be created - stop and pick another artifact
- `resolvedOutputPath`: Resolved path or pattern to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context - always re-read them from disk, even if you saw them earlier in the conversation (the user may have edited them)
Expand All @@ -75,8 +76,8 @@ When ready to implement, run /opsx:apply

b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- Check if every artifact ID in `applyRequires` has `status: "done"` or `status: "skipped"` in the artifacts array (skipped artifacts satisfy the requirement - do not try to create them)
- Stop when all `applyRequires` artifacts are done or skipped

c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
Expand Down
2 changes: 1 addition & 1 deletion skills/openspec-update-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Revise a change's existing planning artifacts and keep them coherent. Never edit
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
- `artifacts`: Array of artifacts with their status ("done", "skipped", "ready", "blocked")
- `isComplete`: Boolean indicating if all artifacts are complete
- `planningHome`, `changeRoot`, `artifactPaths`, and `actionContext`: path and scope context. Use these instead of assuming repo-local paths.

Expand Down
27 changes: 22 additions & 5 deletions src/commands/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import { JsonConverter } from '../core/converters/json-converter.js';
import { Validator } from '../core/validation/validator.js';
import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
import { ChangeParser } from '../core/parsers/change-parser.js';
import { Change } from '../core/schemas/index.js';
import type { RootOutput } from '../core/root-selection.js';
Expand Down Expand Up @@ -218,7 +219,7 @@ export class ChangeCommand {
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
});
// Next steps footer to guide fixing issues
this.printNextSteps();
this.printNextSteps(report.issues);
if (!options?.json) {
process.exitCode = 1;
}
Expand Down Expand Up @@ -251,11 +252,27 @@ export class ChangeCommand {
return match ? match[1].trim() : changeName;
}

private printNextSteps(): void {
private printNextSteps(issues: Array<{ message: string }> = []): void {
const bullets: string[] = [];
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
// Branch on the exact marker messages: the generic no-deltas guidance
// also mentions skip_specs and must not trigger the marker bullets.
const conflictIssue = issues.some(i =>
i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT)
);
const invalidMarkerIssue = issues.some(i =>
i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA)
);
if (conflictIssue) {
bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> is required)');
} else if (invalidMarkerIssue) {
bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> is required)');
bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
} else {
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
}
console.error('Next steps:');
bullets.forEach(b => console.error(` ${b}`));
}
Expand Down
23 changes: 20 additions & 3 deletions src/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ora from 'ora';
import path from 'path';
import { Validator } from '../core/validation/validator.js';
import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
import {
resolveRootForCommand,
toRootOutput,
Expand Down Expand Up @@ -226,13 +227,29 @@ export class ValidateCommand {
const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
}
this.printNextSteps(type, id, root);
this.printNextSteps(type, id, root, report.issues);
}
}

private printNextSteps(type: ItemType, id: string, root: ResolvedOpenSpecRoot): void {
private printNextSteps(type: ItemType, id: string, root: ResolvedOpenSpecRoot, issues: Array<{ message: string }> = []): void {
const bullets: string[] = [];
if (type === 'change') {
// The delta-authoring bullets contradict a marker-related error ("add
// deltas" vs "remove skip_specs or the files"), so branch on the exact
// marker messages - the generic no-deltas guidance also mentions
// skip_specs, which must not trigger this.
const conflictIssue = issues.some(i =>
i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT)
);
const invalidMarkerIssue = issues.some(i =>
i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA)
);
if (type === 'change' && conflictIssue) {
bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> is required)');
} else if (type === 'change' && invalidMarkerIssue) {
bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> is required)');
bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
} else if (type === 'change') {
bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
bullets.push('- Each requirement MUST include at least one #### Scenario: block');
bullets.push(`- Debug parsed deltas: ${withStoreFlag(root, `openspec show ${id} --json --deltas-only`)}`);
Expand Down
Loading
Loading