Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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 sync specs earlier, especially when iterating. The archive command already contains the reconciliation logic in `buildUpdatedSpec()`.

## Goals / Non-Goals

**Goals:**
- Decouple spec syncing from archiving
- Provide `/opsx:sync` skill for agents to sync specs on demand
- Keep operation idempotent (safe to run multiple times)

**Non-Goals:**
- Tracking whether specs have been synced (no state)
- Changing archive behavior (it will continue to apply specs)
- Supporting partial application (all deltas sync 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 sync call archive with flags (rejected: coupling)

### 2. No state tracking

**Decision**: Don't track whether specs have been synced. 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 `specsSynced: true` in `.openspec.yaml` (rejected: unnecessary complexity)
- Store snapshot of synced deltas (rejected: over-engineering)

### 3. Agent-driven approach (no CLI command)

**Decision**: The `/opsx:sync` skill is fully agent-driven - the agent reads delta specs and directly edits main specs.

**Rationale**:
- Allows intelligent merging (add scenarios without copying entire requirements)
- Delta represents *intent*, not wholesale replacement
- More flexible and natural editing workflow
- Archive still uses programmatic merge (for finalized changes)

### 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:sync` get the same experience.

## Risks / Trade-offs

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

**[Risk] User syncs specs then continues editing deltas**
→ Running `/opsx:sync` 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. Add skill template for `/opsx:sync` in `skill-templates.ts`
3. 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 sync specs earlier in the workflow while iterating.

## What Changes

- Add `/opsx:sync` skill that syncs 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
- Agent-driven approach allows intelligent merging (partial updates, adding scenarios)

**Workflow becomes:**
```
/opsx:new → /opsx:continue → /opsx:apply → archive
└── /opsx:sync (optional, anytime)
```
Comment on lines +14 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Specify the fenced code block language.

The code block lacks a language identifier, which is required for proper syntax highlighting and validation.

🔎 Proposed fix
-```
+```
 /opsx:new → /opsx:continue → /opsx:apply → archive
                                   │
                                   └── /opsx:sync (optional, anytime)
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
/opsx:new → /opsx:continue → /opsx:apply → archive
└── /opsx:sync (optional, anytime)
```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

14-14: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In @openspec/changes/add-specs-apply-command/proposal.md around lines 14 - 18,
The fenced code block containing the flow diagram starting with '/opsx:new →
/opsx:continue → /opsx:apply → archive' is missing a language identifier; update
the opening fence to include a language token (for example "text" or another
appropriate language) so the block becomes ```text (or chosen language) to
enable proper syntax highlighting and validation for that block.


## Capabilities

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

### Modified Capabilities
- None (agent-driven, no CLI command needed)

## Impact

- **Skills**: New `openspec-sync-specs` skill in `skill-templates.ts`
- **Archive**: No changes needed - already does reconciliation, will continue to work
- **Agent workflow**: Users gain flexibility to sync specs before archive
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## ADDED 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 synced.

#### Scenario: Show synced 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"
40 changes: 40 additions & 0 deletions openspec/changes/add-specs-apply-command/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## 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

### Skill Template

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

- [x] Add `/opsx:sync` 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