Skip to content
Open
2 changes: 2 additions & 0 deletions openspec/changes/allow-specless-changes/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-15
152 changes: 152 additions & 0 deletions openspec/changes/allow-specless-changes/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
## Context

OpenSpec's `spec-driven` schema requires every change to include delta specs. Two separate systems enforce this:

1. **Validation** (`Validator.validateChangeDeltaSpecs()`): hard-fails with `CHANGE_NO_DELTAS` when `totalDeltas === 0`
2. **Artifact graph** (`detectCompleted()` in `state.ts`): `specs` artifact completion is determined by file existence (`specs/**/*.md`). Since `tasks` depends on `specs`, a change with no spec files can never reach `tasks: ready`.

No config is passed into either system today. The Validator receives only `strictMode`; `detectCompleted()` receives only the graph and change directory.

### Key code locations

- `src/core/project-config.ts` — `ProjectConfigSchema` (Zod), `readProjectConfig()`
- `src/core/validation/validator.ts` — `validateChangeDeltaSpecs()`, line 269: `totalDeltas === 0` → ERROR
- `src/core/validation/constants.ts` — `VALIDATION_MESSAGES.CHANGE_NO_DELTAS`
- `src/core/artifact-graph/state.ts` — `detectCompleted()`: iterates artifacts, checks file existence
- `src/commands/validate.ts` — `ValidateCommand.validateByType()`: creates Validator, calls validation

### Existing patterns

- Config is flat: `schema` (string), `context` (string), `rules` (record of string arrays). No nested config objects.
- No `z.enum` or tri-state config patterns exist today.
- Validation levels are `'ERROR' | 'WARNING' | 'INFO'`. Report validity: `errors === 0` (non-strict) or `errors === 0 && warnings === 0` (strict).
- The proposal instruction and template are static (from `schema.yaml`). Config-aware guidance uses the existing `rules` injection mechanism.

## Goals / Non-Goals

**Goals:**
- Add top-level `requireSpecDeltas` tri-state to project config: `"error"` (default) | `"warn"` | `false`
- Validation respects the setting: ERROR, WARNING, or silent
- Artifact graph respects the setting: synthetically complete `specs` when not required
- Propose workflow works end-to-end for specless changes (with `rules.proposal` guidance in config)

**Non-Goals:**
- Per-change override via `.openspec.yaml` — deferred (adds I/O to `detectCompleted()`)
- Dynamic instruction adaptation based on config — projects use `rules` config instead
- New `--no-specs` flag for `openspec new change` — potential follow-up

## Decisions

### 1. Config shape: top-level `requireSpecDeltas` tri-state

```typescript
const RequireSpecDeltasSchema = z.union([
z.enum(["error", "warn"]),
z.literal(false),
]);

// Added to ProjectConfigSchema alongside schema, context, rules
requireSpecDeltas: RequireSpecDeltasSchema.optional()
```

**Why top-level:** The existing config is flat — `schema`, `context`, `rules`. A top-level key follows the same pattern. This setting affects both validation and the artifact graph, so it doesn't belong under a `validation` namespace.

**Why tri-state over boolean:** The primary use case needs full suppression (no output). A boolean forces a choice between "error" and "silent", losing the middle ground of "visible but non-blocking."

**Why `false` instead of `"off"`:** `false` is YAML's native representation for "disabled." Bare `off` is a YAML 1.1 boolean alias for `false`, which would cause confusion.

### 2. Thread config into the Validator

Pass a config object instead of bare `strictMode`:

```typescript
type SpecDeltaRequirement = 'error' | 'warn' | false;

interface ValidatorConfig {
strictMode?: boolean;
requireSpecDeltas?: SpecDeltaRequirement; // default 'error'
}
```

`ValidateCommand` reads project config via `readProjectConfig(process.cwd())` and passes `requireSpecDeltas` through. The Validator remains a pure logic class with no I/O.

### 3. Tri-state behavior in `validateChangeDeltaSpecs()`

```typescript
if (totalDeltas === 0) {
if (this.requireSpecDeltas === 'error') {
issues.push({ level: 'ERROR', ... });
} else if (this.requireSpecDeltas === 'warn') {
issues.push({ level: 'WARNING', ... });
}
// false → no issue emitted
}
```

Existing `createReport()` handles the rest: `valid = strictMode ? (errors === 0 && warnings === 0) : (errors === 0)`.

### 4. Synthetic completion in `detectCompleted()`

In `state.ts`, after the file-existence scan, if the `specs` artifact is not completed and the config allows specless changes:

```typescript
export function detectCompleted(
graph: ArtifactGraph,
changeDir: string,
options?: { requireSpecDeltas?: 'error' | 'warn' | false }
): CompletedSet {
const completed = new Set<string>();
// ... existing file-existence loop ...

// Synthetically mark specs as complete when not required
const specsArtifact = graph.getAllArtifacts().find(a => a.id === 'specs');
if (specsArtifact && !completed.has('specs') &&
options?.requireSpecDeltas !== undefined &&
options?.requireSpecDeltas !== 'error') {
completed.add('specs');
}

return completed;
}
```

**Why hardcode `'specs'`:** The `specs` artifact is the only one with a glob pattern that might legitimately have zero files. A more general "optional artifacts" system would be over-engineering for now.

### 5. "Skipped" status in display

`formatChangeStatus()` already has access to the graph, completed set, and change directory. To distinguish "done" from "skipped":

```typescript
// In formatChangeStatus(), when mapping artifact statuses:
if (context.completed.has(artifact.id)) {
const hasFiles = artifactOutputExists(context.changeDir, artifact.generates);
return {
id: artifact.id,
outputPath: artifact.generates,
status: hasFiles ? 'done' : 'skipped',
};
}
```

If the artifact is in the completed set but has no files on disk, it was synthetically completed → `'skipped'`.

Add to `ArtifactStatus.status`: `'done' | 'ready' | 'blocked' | 'skipped'`
Add to `getStatusIndicator()`: `'skipped'` → `[~]` with `chalk.dim`
Add to `getStatusColor()`: `'skipped'` → `chalk.dim`

Skipped artifacts count toward the completed total in `printStatusText()` (they unblock downstream work).

### 6. No schema changes needed

`apply.requires: [tasks]` doesn't include `specs` directly. The blocker was `tasks.requires: [specs, design]` in the artifact dependency graph, which the synthetic completion resolves.

## Risks / Trade-offs

- **[Risk] Teams accidentally skip specs for feature work** → `"warn"` keeps it visible; `false` requires explicit opt-in in config. Teams can use `--strict` in CI.
- **[Risk] Tri-state is the first `z.union` in config** → Well-established pattern (ESLint). Resilient parsing handles per-field failures.
- **[Risk] Hardcoding `'specs'` in detectCompleted** → Pragmatic for now. If more artifacts need optional behavior, generalize to an `optional` field in schema.yaml.
- **[Risk] `loadChangeContext` needs config access** → It already imports `readProjectConfig` (line 8 of instruction-loader.ts). Minimal wiring.

## Open Questions

_(none)_
63 changes: 63 additions & 0 deletions openspec/changes/allow-specless-changes/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Why

Teams using OpenSpec for product specs and high-level technical standards often need to track changes that don't modify spec-level requirements — bug fixes, documentation refactoring, dependency upgrades, infrastructure work, or implementation-only refactors. The `openspec/changes/` directory is valuable as a repository for technical change descriptions and team review artifacts even when no spec requirements change.

Today, `openspec validate` hard-fails with `CHANGE_NO_DELTAS` when a change has zero delta specs, and the artifact dependency graph blocks `tasks` on `specs` completion (determined by file existence of `specs/**/*.md`). This forces teams to either (a) skip the openspec workflow for non-spec changes, fragmenting their process, or (b) create artificial spec changes just to pass validation. Neither is acceptable.

## What Changes

Add `requireSpecDeltas` as a top-level tri-state setting in `openspec/config.yaml`:
- `"error"` (default when omitted) — current behavior, hard-fail on missing deltas
- `"warn"` — emit a WARNING but pass validation
- `false` — completely suppress the check, no output at all

When set to `"warn"` or `false`, two things change:
1. **Validation**: the `CHANGE_NO_DELTAS` check emits the configured level (or nothing) instead of ERROR
2. **Artifact graph**: `detectCompleted()` synthetically marks `specs` as complete so that `tasks` is unblocked and the propose workflow can proceed without writing spec files

The proposal instruction/template in the schema is static and doesn't adapt to this config. Projects using this feature should add a `rules.proposal` entry in config.yaml to tell the AI that Capabilities sections are optional. This uses the existing rules injection mechanism — no code changes needed.

Example config for specless workflow:
```yaml
schema: spec-driven
requireSpecDeltas: false

rules:
proposal:
- "The Capabilities section is optional. If the change has no spec-level requirement changes, leave New Capabilities and Modified Capabilities as 'None'."
```

### Considered and deferred: per-change metadata

A `skipSpecDeltas: true` field in `.openspec.yaml` would allow per-change overrides of the project default. This was considered but deferred because:
- The artifact graph fix (synthetic completion in `detectCompleted()`) would need to read change metadata, adding I/O to a currently simple function
- The project-level config covers the primary use case (teams that routinely make non-spec changes)
- Can be added later without breaking changes

## Capabilities

### New Capabilities

_(none — this extends existing capabilities)_

### Modified Capabilities

- `config-loading`: Add top-level `requireSpecDeltas` (tri-state: `"error"` | `"warn"` | `false`) to the ProjectConfig schema and resilient parsing
- `cli-validate`: Respect `requireSpecDeltas` when evaluating the `CHANGE_NO_DELTAS` check
- `artifact-graph`: Synthetically mark `specs` as complete in `detectCompleted()` when `requireSpecDeltas` is not `"error"`
- `cli-artifact-workflow`: Add `"skipped"` status to status display for synthetically completed artifacts (indicator `[~]`, dim color, `"skipped"` in JSON)

## Non-goals

- Making the `specs` artifact disappear from `openspec status` — it remains visible as `[~] skipped`
- Changing the `ChangeSchema` Zod validation for `deltas.min(1)` — that validates the proposal's Capabilities section, not spec files
- Dynamic instruction adaptation — the schema's proposal instruction is static; projects use `rules` config to adjust AI guidance
- Per-change override via `.openspec.yaml` — deferred for a future change

## Impact

- `src/core/project-config.ts` — extend `ProjectConfigSchema` with optional `requireSpecDeltas`
- `src/core/validation/validator.ts` — `validateChangeDeltaSpecs()` respects the tri-state setting
- `src/commands/validate.ts` — read project config and pass setting to the validator
- `src/core/artifact-graph/state.ts` — `detectCompleted()` synthetically marks `specs` done when config allows
- `src/core/validation/constants.ts` — add message for the warning case
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## MODIFIED Requirements

### Requirement: State Detection
The system SHALL detect artifact completion state by scanning the filesystem, and SHALL synthetically mark artifacts as complete when project config indicates they are not required.

#### Scenario: Simple file exists
- **WHEN** an artifact generates "proposal.md" and the file exists
- **THEN** the artifact is marked as completed

#### Scenario: Glob pattern matches files
- **WHEN** an artifact generates "specs/**/*.md" and matching files exist
- **THEN** the artifact is marked as completed

#### Scenario: No matching files
- **WHEN** an artifact generates "specs/**/*.md" and no matching files exist
- **AND** the project config does not set `requireSpecDeltas` (defaults to `"error"`)
- **THEN** the artifact is marked as not completed

#### Scenario: Missing change directory
- **WHEN** the change directory does not exist
- **THEN** all artifacts are marked as not completed (empty state)

#### Scenario: Specs artifact synthetically completed when requireSpecDeltas is not "error"
- **WHEN** the `specs` artifact generates "specs/**/*.md" and no matching files exist
- **AND** the project config has `requireSpecDeltas` set to `"warn"` or `false`
- **THEN** the `specs` artifact SHALL be synthetically marked as completed
- **AND** downstream artifacts that depend on `specs` (e.g. `tasks`) SHALL become ready

#### Scenario: Specs artifact not synthetically completed when requireSpecDeltas is "error"
- **WHEN** the `specs` artifact generates "specs/**/*.md" and no matching files exist
- **AND** the project config has `requireSpecDeltas` set to `"error"` or is omitted
- **THEN** the `specs` artifact SHALL NOT be synthetically marked as completed
- **AND** downstream artifacts that depend on `specs` SHALL remain blocked

#### Scenario: Specs artifact with files present ignores config
- **WHEN** the `specs` artifact generates "specs/**/*.md" and matching files exist
- **THEN** the artifact is marked as completed regardless of `requireSpecDeltas` setting
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## MODIFIED Requirements

### Requirement: Status Command

The system SHALL display artifact completion status for a change, including scaffolded (empty) changes and skipped artifacts.

#### Scenario: Show status with all states

- **WHEN** user runs `openspec status --change <id>`
- **THEN** the system displays each artifact with status indicator:
- `[x]` for completed artifacts
- `[ ]` for ready artifacts
- `[-]` for blocked artifacts (with missing dependencies listed)
- `[~]` for skipped artifacts (synthetically completed due to config)

#### Scenario: Status shows completion summary

- **WHEN** user runs `openspec status --change <id>`
- **THEN** output includes completion percentage and count (e.g., "2/4 artifacts complete")
- **AND** skipped artifacts count toward the completed total

#### Scenario: Status JSON output with skipped status

- **WHEN** user runs `openspec status --change <id> --json`
- **AND** an artifact is synthetically completed (e.g. `specs` when `requireSpecDeltas` is not `"error"` and no spec files exist)
- **THEN** the artifact's status SHALL be `"skipped"` (not `"done"`)

#### Scenario: Status on scaffolded change

- **WHEN** user runs `openspec status --change <id>` on a change with no generated artifact files yet
- **THEN** system displays all artifacts with their status
- **AND** root artifacts (no dependencies) show as ready `[ ]`
- **AND** dependent artifacts show as blocked `[-]`

### Requirement: Output Formatting

The system SHALL provide consistent output formatting.

#### Scenario: Color output

- **WHEN** terminal supports colors
- **THEN** status indicators use colors: green (done), yellow (ready), red (blocked), dim (skipped)

#### Scenario: No color output

- **WHEN** `--no-color` flag is used or NO_COLOR environment variable is set
- **THEN** output uses text-only indicators without ANSI colors
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## MODIFIED Requirements

### Requirement: Validation SHALL provide actionable remediation steps
Validation output SHALL include specific guidance to fix each error, including expected structure, example headers, and suggested commands to verify fixes.

#### Scenario: No deltas found in change (default behavior)
- **WHEN** validating a change with zero parsed deltas
- **AND** the project config does not set `requireSpecDeltas` (defaults to `"error"`)
- **THEN** show error "No deltas found" with guidance:
- Explain that change specs must include `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, or `## RENAMED Requirements`
- Remind authors that files must live under `openspec/changes/{id}/specs/<capability>/spec.md`
- Include an explicit note: "Spec delta files cannot start with titles before the operation headers"
- Suggest running `openspec show {id} --json --deltas-only` for debugging
- Note: "If this change intentionally has no spec deltas, set `requireSpecDeltas: false` in openspec/config.yaml"

#### Scenario: No deltas found, requireSpecDeltas is "warn"
- **WHEN** validating a change with zero parsed deltas
- **AND** the project config has `requireSpecDeltas` set to `"warn"`
- **THEN** emit a WARNING stating "Change has no spec deltas (allowed by config)"
- **AND** validation SHALL pass (report.valid is `true` in non-strict mode)

#### Scenario: CLI output when validation passes with warnings
- **WHEN** validating a change that passes (report.valid is `true`)
- **AND** the report contains one or more WARNING-level issues
- **THEN** the CLI SHALL print "<type> '<id>' is valid" to stdout
- **AND** the CLI SHALL print each WARNING issue to stderr (e.g. `⚠ [WARNING] file: ...`)
- **AND** the CLI SHALL exit with code 0
- **AND** the Next steps footer SHALL NOT be printed (it is reserved for errors)

#### Scenario: No deltas found, requireSpecDeltas is "warn", strict mode
- **WHEN** validating a change with zero parsed deltas
- **AND** the project config has `requireSpecDeltas` set to `"warn"`
- **AND** `--strict` mode is enabled
- **THEN** emit a WARNING stating "Change has no spec deltas (allowed by config)"
- **AND** validation SHALL fail (report.valid is `false` because strict mode treats warnings as errors)

#### Scenario: No deltas found, requireSpecDeltas is false
- **WHEN** validating a change with zero parsed deltas
- **AND** the project config has `requireSpecDeltas` set to `false`
- **THEN** emit no issue (no error, no warning, no info) for the missing deltas
- **AND** validation SHALL pass

#### Scenario: Missing required sections
- **WHEN** a required section is missing
- **THEN** include expected header names and a minimal skeleton:
- For Spec: `## Purpose`, `## Requirements`
- For Change: `## Why`, `## What Changes`
- Provide an example snippet of the missing section with placeholder prose ready to copy
- Mention the quick-reference section in `openspec/AGENTS.md` as the authoritative template

#### Scenario: Missing requirement descriptive text
- **WHEN** a requirement header lacks descriptive text before scenarios
- **THEN** emit an error explaining that `### Requirement:` lines must be followed by narrative text before any `#### Scenario:` headers
- Show compliant example: "### Requirement: Foo" followed by "The system SHALL ..."
- Suggest adding 1-2 sentences describing the normative behavior prior to listing scenarios
- Reference the pre-validation checklist in `openspec/AGENTS.md`
Loading