Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions openspec/changes/add-specs-apply-command/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-06
77 changes: 77 additions & 0 deletions openspec/changes/add-specs-apply-command/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
## Context

Currently, delta specs are only applied to main specs when running `openspec archive`. This bundles two concerns:
1. Applying spec changes (delta → main)
2. Archiving the change (move to archive folder)

Users want flexibility to apply specs earlier, especially when iterating. The archive command already contains the reconciliation logic in `buildUpdatedSpec()`.

## Goals / Non-Goals

**Goals:**
- Decouple spec application from archiving
- Provide `/opsx:specs` skill for agents to apply specs on demand
- Add `openspec specs apply` CLI command for direct invocation
- Keep operation idempotent (safe to run multiple times)

**Non-Goals:**
- Tracking whether specs have been applied (no state)
- Changing archive behavior (it will continue to apply specs)
- Supporting partial application (all deltas apply together)

## Decisions

### 1. Reuse existing reconciliation logic

**Decision**: Extract `buildUpdatedSpec()` logic from `ArchiveCommand` into a shared module.

**Rationale**: The archive command already implements delta parsing and application. Rather than duplicate, we extract and reuse.

**Alternatives considered**:
- Duplicate logic in new command (rejected: maintenance burden)
- Have specs apply call archive with flags (rejected: coupling)

### 2. No state tracking

**Decision**: Don't track whether specs have been applied. Each invocation reads delta and main specs, reconciles.

**Rationale**:
- Idempotent operations don't need state
- Avoids sync issues between flag and reality
- Simpler implementation and mental model

**Alternatives considered**:
- Track `specsApplied: true` in `.openspec.yaml` (rejected: unnecessary complexity)
- Store snapshot of applied deltas (rejected: over-engineering)

### 3. Agent-driven for skill, CLI for direct use

**Decision**:
- Skill (`/opsx:specs`) instructs agent to use CLI command
- CLI command (`openspec specs apply`) does the actual work

**Rationale**: Consistent with other opsx skills. CLI provides the capability, skill provides the agent workflow.

### 4. Archive behavior unchanged

**Decision**: Archive continues to apply specs as part of its flow. If specs are already reconciled, the operation is a no-op.

**Rationale**: Backward compatibility. Users who don't use `/opsx:specs` get the same experience.

## Risks / Trade-offs

**[Risk] Multiple changes modify same spec**
→ Last to apply wins. Same as today with archive. Users should coordinate or use sequential archives.

**[Risk] User applies specs then continues editing deltas**
→ Running `/opsx:specs` again reconciles. Idempotent design handles this.

**[Trade-off] No undo mechanism**
→ Users can `git checkout` main specs if needed. Explicit undo command is out of scope.

## Implementation Approach

1. Extract spec application logic from `ArchiveCommand.buildUpdatedSpec()` into `src/core/specs-apply.ts`
2. Create `openspec specs apply` command that uses extracted logic
3. Add skill template for `/opsx:specs` in `skill-templates.ts`
4. Register skill in managed skills
32 changes: 32 additions & 0 deletions openspec/changes/add-specs-apply-command/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Why

Spec application is currently bundled with archive - users must run `openspec archive` to apply delta specs to main specs. This couples two distinct concerns (applying specs vs. archiving the change) and forces users to wait until they're "done" to see main specs updated. Users want the flexibility to apply specs earlier in the workflow while iterating.

## What Changes

- Add `/opsx:specs` skill that applies delta specs to main specs as a standalone action
- The operation is idempotent - safe to run multiple times, agent reconciles main specs to match deltas
- Archive continues to work as today (applies specs if not already reconciled, then moves to archive)
- No new state tracking - the agent reads delta and main specs, reconciles on each run

**Workflow becomes:**
```
/opsx:new → /opsx:continue → /opsx:apply → archive
└── /opsx:specs (optional, anytime)
```

## Capabilities

### New Capabilities
- `specs-apply-skill`: Skill template for `/opsx:specs` command that reconciles main specs with delta specs

### Modified Capabilities
- `cli-artifact-workflow`: Add `openspec specs apply --change <name>` CLI command to support the skill

## Impact

- **Skills**: New `openspec-specs-apply` skill in `skill-templates.ts`
- **CLI**: New `specs apply` subcommand in artifact workflow commands
- **Archive**: No changes needed - already does reconciliation, will continue to work
- **Agent workflow**: Users gain flexibility to apply specs before archive
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## ADDED Requirements

### Requirement: Specs Apply Command
The system SHALL provide a `specs apply` subcommand for applying delta specs to main specs.

#### Scenario: Apply specs for a change
- **WHEN** user runs `openspec specs apply --change <name>`
- **THEN** the system applies delta specs from the change to main specs
- **AND** displays a summary of changes made

#### Scenario: JSON output
- **WHEN** user runs `openspec specs apply --change <name> --json`
- **THEN** the system outputs JSON with:
- `changeName`: the change being applied
- `capabilities`: array of affected capabilities with counts
- `totals`: aggregate counts of added/modified/removed/renamed

#### Scenario: Missing change parameter
- **WHEN** user runs `openspec specs apply` without `--change`
- **THEN** the system displays an error with list of available changes

#### Scenario: Change has no delta specs
- **WHEN** user runs `openspec specs apply --change <name>`
- **AND** the change has no `specs/` directory or no delta spec files
- **THEN** the system displays "No delta specs found for change <name>"

#### Scenario: Dry run mode
- **WHEN** user runs `openspec specs apply --change <name> --dry-run`
- **THEN** the system shows what would be changed without writing files
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## ADDED Requirements

### Requirement: Specs Apply Skill
The system SHALL provide an `/opsx:specs` skill that applies delta specs from a change to the main specs.

#### Scenario: Apply delta specs to main specs
- **WHEN** agent executes `/opsx:specs` with a change name
- **THEN** the agent reads delta specs from `openspec/changes/<name>/specs/`
- **AND** reads corresponding main specs from `openspec/specs/`
- **AND** reconciles main specs to match what the deltas describe

#### Scenario: Idempotent operation
- **WHEN** agent executes `/opsx:specs` multiple times on the same change
- **THEN** the result is the same as running it once
- **AND** no duplicate requirements are created

#### Scenario: Change selection prompt
- **WHEN** agent executes `/opsx:specs` without specifying a change
- **THEN** the agent prompts user to select from available changes
- **AND** shows changes that have delta specs

### Requirement: Delta Reconciliation Logic
The agent SHALL reconcile main specs with delta specs using the delta operation headers.

#### Scenario: ADDED requirements
- **WHEN** delta contains `## ADDED Requirements` with a requirement
- **AND** the requirement does not exist in main spec
- **THEN** add the requirement to main spec

#### Scenario: ADDED requirement already exists
- **WHEN** delta contains `## ADDED Requirements` with a requirement
- **AND** a requirement with the same name already exists in main spec
- **THEN** update the existing requirement to match the delta version

#### Scenario: MODIFIED requirements
- **WHEN** delta contains `## MODIFIED Requirements` with a requirement
- **AND** the requirement exists in main spec
- **THEN** replace the requirement in main spec with the delta version

#### Scenario: REMOVED requirements
- **WHEN** delta contains `## REMOVED Requirements` with a requirement name
- **AND** the requirement exists in main spec
- **THEN** remove the requirement from main spec

#### Scenario: RENAMED requirements
- **WHEN** delta contains `## RENAMED Requirements` with FROM:/TO: format
- **AND** the FROM requirement exists in main spec
- **THEN** rename the requirement to the TO name

#### Scenario: New capability spec
- **WHEN** delta spec exists for a capability not in main specs
- **THEN** create new main spec file at `openspec/specs/<capability>/spec.md`

### Requirement: Skill Output
The skill SHALL provide clear feedback on what was applied.

#### Scenario: Show applied changes
- **WHEN** reconciliation completes successfully
- **THEN** display summary of changes per capability:
- Number of requirements added
- Number of requirements modified
- Number of requirements removed
- Number of requirements renamed

#### Scenario: No changes needed
- **WHEN** main specs already match delta specs
- **THEN** display "Specs already in sync - no changes needed"
45 changes: 45 additions & 0 deletions openspec/changes/add-specs-apply-command/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Tasks

### Core Implementation

- [x] Extract spec application logic from `ArchiveCommand` into `src/core/specs-apply.ts`
- Move `buildUpdatedSpec()`, `findSpecUpdates()`, `writeUpdatedSpec()` to shared module
- Keep `ArchiveCommand` importing from the new module
- Ensure all validation logic is preserved

- [x] ~~Create `openspec specs apply` CLI command~~ → Changed to agent-driven approach
- Removed CLI command - skill is now agent-driven
- Agent reads delta specs and directly edits main specs
- Allows intelligent merging (add scenarios without copying entire requirements)

### Skill Template

- [x] Add `getSpecsApplySkillTemplate()` function in `src/core/templates/skill-templates.ts`
- Skill name: `openspec-specs-apply`
- Description: Apply delta specs to main specs
- **Agent-driven**: Instructions for agent to read deltas and edit main specs directly

- [x] Add `/opsx:specs` slash command template in `skill-templates.ts`
- Mirror the skill template for slash command format
- **Agent-driven**: No CLI command, agent does the merge

### Registration

- [x] Register skill in managed skills (via `artifact-experimental-setup`)
- Add to skill list with appropriate metadata
- Ensure it appears in setup output

### Design Decision

**Why agent-driven instead of CLI-driven?**

The programmatic merge operates at requirement-level granularity:
- MODIFIED requires copying ALL scenarios, not just the changed ones
- If agent forgets a scenario, it gets deleted
- Delta specs become bloated with copied content

Agent-driven approach:
- Agent can apply partial updates (add a scenario without copying others)
- Delta represents *intent*, not wholesale replacement
- More flexible and natural editing workflow
- Archive still uses programmatic merge (for finalized changes)
72 changes: 72 additions & 0 deletions openspec/specs/specs-sync-skill/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# specs-sync-skill Specification

## Purpose
Defines the agent skill for syncing delta specs from changes to main specs.

## Requirements

### Requirement: Specs Sync Skill
The system SHALL provide an `/opsx:sync` skill that syncs delta specs from a change to the main specs.

#### Scenario: Sync delta specs to main specs
- **WHEN** agent executes `/opsx:sync` with a change name
- **THEN** the agent reads delta specs from `openspec/changes/<name>/specs/`
- **AND** reads corresponding main specs from `openspec/specs/`
- **AND** reconciles main specs to match what the deltas describe

#### Scenario: Idempotent operation
- **WHEN** agent executes `/opsx:sync` multiple times on the same change
- **THEN** the result is the same as running it once
- **AND** no duplicate requirements are created

#### Scenario: Change selection prompt
- **WHEN** agent executes `/opsx:sync` without specifying a change
- **THEN** the agent prompts user to select from available changes
- **AND** shows changes that have delta specs

### Requirement: Delta Reconciliation Logic
The agent SHALL reconcile main specs with delta specs using the delta operation headers.

#### Scenario: ADDED requirements
- **WHEN** delta contains `## ADDED Requirements` with a requirement
- **AND** the requirement does not exist in main spec
- **THEN** add the requirement to main spec

#### Scenario: ADDED requirement already exists
- **WHEN** delta contains `## ADDED Requirements` with a requirement
- **AND** a requirement with the same name already exists in main spec
- **THEN** update the existing requirement to match the delta version

#### Scenario: MODIFIED requirements
- **WHEN** delta contains `## MODIFIED Requirements` with a requirement
- **AND** the requirement exists in main spec
- **THEN** replace the requirement in main spec with the delta version

#### Scenario: REMOVED requirements
- **WHEN** delta contains `## REMOVED Requirements` with a requirement name
- **AND** the requirement exists in main spec
- **THEN** remove the requirement from main spec

#### Scenario: RENAMED requirements
- **WHEN** delta contains `## RENAMED Requirements` with FROM:/TO: format
- **AND** the FROM requirement exists in main spec
- **THEN** rename the requirement to the TO name

#### Scenario: New capability spec
- **WHEN** delta spec exists for a capability not in main specs
- **THEN** create new main spec file at `openspec/specs/<capability>/spec.md`

### Requirement: Skill Output
The skill SHALL provide clear feedback on what was applied.

#### Scenario: Show applied changes
- **WHEN** reconciliation completes successfully
- **THEN** display summary of changes per capability:
- Number of requirements added
- Number of requirements modified
- Number of requirements removed
- Number of requirements renamed

#### Scenario: No changes needed
- **WHEN** main specs already match delta specs
- **THEN** display "Specs already in sync - no changes needed"
7 changes: 6 additions & 1 deletion src/commands/artifact-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
type SchemaInfo,
} from '../core/artifact-graph/index.js';
import { createChange, validateChangeName } from '../utils/change-utils.js';
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate } from '../core/templates/skill-templates.js';
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate } from '../core/templates/skill-templates.js';
import { FileSystemUtils } from '../utils/file-system.js';

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -797,19 +797,22 @@ async function artifactExperimentalSetupCommand(): Promise<void> {
const continueChangeSkill = getContinueChangeSkillTemplate();
const applyChangeSkill = getApplyChangeSkillTemplate();
const ffChangeSkill = getFfChangeSkillTemplate();
const syncSpecsSkill = getSyncSpecsSkillTemplate();

// Get command templates
const newCommand = getOpsxNewCommandTemplate();
const continueCommand = getOpsxContinueCommandTemplate();
const applyCommand = getOpsxApplyCommandTemplate();
const ffCommand = getOpsxFfCommandTemplate();
const syncCommand = getOpsxSyncCommandTemplate();

// Create skill directories and SKILL.md files
const skills = [
{ template: newChangeSkill, dirName: 'openspec-new-change' },
{ template: continueChangeSkill, dirName: 'openspec-continue-change' },
{ template: applyChangeSkill, dirName: 'openspec-apply-change' },
{ template: ffChangeSkill, dirName: 'openspec-ff-change' },
{ template: syncSpecsSkill, dirName: 'openspec-sync-specs' },
];

const createdSkillFiles: string[] = [];
Expand Down Expand Up @@ -838,6 +841,7 @@ ${template.instructions}
{ template: continueCommand, fileName: 'continue.md' },
{ template: applyCommand, fileName: 'apply.md' },
{ template: ffCommand, fileName: 'ff.md' },
{ template: syncCommand, fileName: 'sync.md' },
];

const createdCommandFiles: string[] = [];
Expand Down Expand Up @@ -894,6 +898,7 @@ ${template.content}
console.log(' • /opsx:continue - Create the next artifact');
console.log(' • /opsx:apply - Implement tasks');
console.log(' • /opsx:ff - Fast-forward: create all artifacts at once');
console.log(' • /opsx:sync - Sync delta specs to main specs');
console.log();
console.log(chalk.yellow('💡 This is an experimental feature.'));
console.log(' Feedback welcome at: https://github.com/Fission-AI/OpenSpec/issues');
Expand Down
Loading
Loading