mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-03 22:13:19 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
675941ac83 | ||
|
|
586ef61244 | ||
|
|
eb15cdb983 | ||
|
|
cd172a4427 | ||
|
|
b7f5a429de | ||
|
|
a5c10ed5e7 | ||
|
|
1bc849554c |
@@ -0,0 +1,107 @@
|
||||
# Experimental Workflow (OPSX)
|
||||
|
||||
> **Status:** Experimental. Things might break. Feedback welcome on [Discord](https://discord.gg/BYjPaKbqMt).
|
||||
>
|
||||
> **Compatibility:** Claude Code only (for now)
|
||||
|
||||
## What Is It?
|
||||
|
||||
OPSX is a new way to work with OpenSpec changes. Instead of one big proposal, you build **artifacts** step-by-step:
|
||||
|
||||
```
|
||||
proposal → specs → design → tasks → implementation → archive
|
||||
```
|
||||
|
||||
Each artifact has dependencies. Can't write tasks until you have specs. Can't implement until you have tasks. The system tracks what's ready and what's blocked.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# 1. Make sure you have openspec installed and initialized
|
||||
openspec init
|
||||
|
||||
# 2. Generate the experimental skills
|
||||
openspec artifact-experimental-setup
|
||||
```
|
||||
|
||||
This creates skills in `.claude/skills/` that Claude Code auto-detects.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:new` | Start a new change |
|
||||
| `/opsx:continue` | Create the next artifact |
|
||||
| `/opsx:ff` | Fast-forward (create all artifacts at once) |
|
||||
| `/opsx:apply` | Implement the tasks |
|
||||
| `/opsx:sync` | Sync delta specs to main specs |
|
||||
| `/opsx:archive` | Archive when done |
|
||||
|
||||
## Usage
|
||||
|
||||
### Start a new change
|
||||
```
|
||||
/opsx:new
|
||||
```
|
||||
You'll be asked what you want to build and which workflow schema to use.
|
||||
|
||||
### Build artifacts step-by-step
|
||||
```
|
||||
/opsx:continue
|
||||
```
|
||||
Creates one artifact at a time. Good for reviewing each step.
|
||||
|
||||
### Or fast-forward
|
||||
```
|
||||
/opsx:ff add-dark-mode
|
||||
```
|
||||
Creates all artifacts in one go. Good when you know what you want.
|
||||
|
||||
### Implement
|
||||
```
|
||||
/opsx:apply
|
||||
```
|
||||
Works through tasks, checking them off as you go.
|
||||
|
||||
### Sync specs and archive
|
||||
```
|
||||
/opsx:sync # Update main specs with your delta specs
|
||||
/opsx:archive # Move to archive when done
|
||||
```
|
||||
|
||||
## What's Different?
|
||||
|
||||
**Standard workflow** (`/openspec:proposal`):
|
||||
- One big proposal document
|
||||
- Linear phases: plan → implement → archive
|
||||
- All-or-nothing artifact creation
|
||||
|
||||
**Experimental workflow** (`/opsx:*`):
|
||||
- Discrete artifacts with dependencies
|
||||
- Fluid actions (not phases) - update artifacts anytime
|
||||
- Step-by-step or fast-forward
|
||||
- Schema-driven (can customize the workflow)
|
||||
|
||||
The key insight: work isn't linear. You implement, realize the design is wrong, update it, continue. OPSX supports this.
|
||||
|
||||
## Schemas
|
||||
|
||||
Schemas define what artifacts exist and their dependencies. Currently available:
|
||||
|
||||
- **spec-driven** (default): proposal → specs → design → tasks
|
||||
- **tdd**: tests → implementation → docs
|
||||
|
||||
Run `openspec schemas` to see available schemas.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `/opsx:ff` when you have a clear idea, `/opsx:continue` when exploring
|
||||
- Tasks track progress via checkboxes in `tasks.md`
|
||||
- Delta specs (in `specs/`) get synced to main specs with `/opsx:sync`
|
||||
- If you get stuck, the status command shows what's blocked: `openspec status --change "name"`
|
||||
|
||||
## Feedback
|
||||
|
||||
This is rough. That's intentional - we're learning what works.
|
||||
|
||||
Found a bug? Have ideas? Join us on [Discord](https://discord.gg/BYjPaKbqMt) or open an issue on [GitHub](https://github.com/Fission-AI/openspec/issues).
|
||||
@@ -1,11 +0,0 @@
|
||||
## Why
|
||||
Manual setup for new changes leads to formatting mistakes in spec deltas and slows agents who must recreate the same file skeletons for every proposal. A built-in scaffold command will generate compliant templates so assistants can focus on the change content instead of structure.
|
||||
|
||||
## What Changes
|
||||
- Add an `openspec scaffold <change-id>` CLI command that creates a change directory (if it does not already exist) with validated `proposal.md`, `tasks.md`, and spec delta templates.
|
||||
- Update CLI documentation and quick-reference guidance so agents discover the scaffold workflow before drafting files manually, including reminders on when to create spec deltas.
|
||||
- Add automated coverage (unit/integ tests) to ensure the command respects naming rules, copies templates correctly, fails for existing directories, and produces output that passes `openspec validate --strict` untouched.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/cli-scaffold`
|
||||
- Affected code: `src/cli/index.ts`, `src/commands`, `docs/`
|
||||
@@ -1,36 +0,0 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Scaffolding Command Registration
|
||||
The CLI SHALL expose an `openspec scaffold <change-id>` command that validates the change identifier before generating files.
|
||||
|
||||
#### Scenario: Registering scaffold command
|
||||
- **WHEN** a user runs `openspec scaffold add-user-notifications`
|
||||
- **THEN** the CLI SHALL reject invalid identifiers (non kebab-case) before proceeding
|
||||
- **AND** display usage documentation via `openspec scaffold --help`
|
||||
- **AND** exit with code 0 after successful scaffolding
|
||||
|
||||
### Requirement: Change Directory Structure
|
||||
The scaffold command SHALL create the standard change workspace (if it does not already exist) with proposal, tasks, optional design, and `specs/` directories laid out according to OpenSpec conventions.
|
||||
|
||||
#### Scenario: Generating change workspace
|
||||
- **WHEN** scaffolding a new change with id `add-user-notifications`
|
||||
- **THEN** create `openspec/changes/add-user-notifications/` if it does not exist
|
||||
- **AND** copy the default template bundle (proposal, tasks, design placeholders) into that directory in a single operation
|
||||
- **AND** create an empty `openspec/changes/add-user-notifications/specs/` directory ready for capability-specific deltas that will be authored later
|
||||
|
||||
### Requirement: Template Content Guidance
|
||||
The scaffold command SHALL populate generated Markdown files with OpenSpec-compliant templates so authors can copy, edit, and pass validation without reformatting.
|
||||
|
||||
#### Scenario: Populating proposal and tasks templates
|
||||
- **WHEN** the scaffold command writes `proposal.md`
|
||||
- **THEN** include the `## Why`, `## What Changes`, and `## Impact` headings with placeholder guidance text
|
||||
- **AND** ensure `tasks.md` starts with `## 1. Implementation` and numbered checklist items using `- [ ]` syntax
|
||||
- **AND** annotate optional sections (like `design.md`) with inline TODO comments so users understand when to keep or delete them
|
||||
- **AND** include a short reminder inside `specs/README.md` (or similar) instructing authors to add deltas once they know the affected capability
|
||||
|
||||
### Requirement: Idempotent Execution
|
||||
The scaffold command SHALL be safe to rerun, preserving user edits while filling in any missing managed sections.
|
||||
|
||||
#### Scenario: Rerunning scaffold on existing change
|
||||
- **WHEN** the command is executed again for an existing change directory containing user-edited files
|
||||
- **THEN** leave existing content untouched except for managed placeholder regions or missing files that need creation
|
||||
- **AND** update the filesystem summary to highlight which files were skipped, created, or refreshed
|
||||
@@ -1,12 +0,0 @@
|
||||
## 1. CLI scaffolding command
|
||||
- [ ] 1.1 Register an `openspec scaffold` command in the CLI entrypoint with `change-id` argument validation.
|
||||
- [ ] 1.2 Implement generator logic that copies the default change template bundle (`proposal.md`, `tasks.md`, optional `design.md`, `specs/README.md`) into `openspec/changes/<id>/`, creating the directory tree in a single pass.
|
||||
- [ ] 1.3 Detect when `openspec/changes/<id>/` already exists and exit with a clear error instead of overwriting user files.
|
||||
|
||||
## 2. Templates and documentation
|
||||
- [ ] 2.1 Update `openspec/AGENTS.md` quick reference so agents see `openspec scaffold` before drafting files manually.
|
||||
- [ ] 2.2 Refresh CLI docs/README/help text to mention the scaffold workflow, template bundle contents, and when to add spec deltas manually.
|
||||
|
||||
## 3. Test coverage
|
||||
- [ ] 3.1 Add unit tests covering name validation, template copying, and existing-directory failures.
|
||||
- [ ] 3.2 Add integration coverage ensuring a freshly scaffolded change (without deltas) passes `openspec validate --strict` until the author customizes it.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-06
|
||||
@@ -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
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
## 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
|
||||
+67
@@ -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"
|
||||
@@ -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)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Schema Apply Block
|
||||
|
||||
The system SHALL support an `apply` block in schema definitions that controls when and how implementation begins.
|
||||
|
||||
#### Scenario: Schema with apply block
|
||||
|
||||
- **WHEN** a schema defines an `apply` block
|
||||
- **THEN** the system uses `apply.requires` to determine which artifacts must exist before apply
|
||||
- **AND** uses `apply.tracks` to identify the file for progress tracking (or null if none)
|
||||
- **AND** uses `apply.instruction` for guidance shown to the agent
|
||||
|
||||
#### Scenario: Schema without apply block
|
||||
|
||||
- **WHEN** a schema has no `apply` block
|
||||
- **THEN** the system requires all artifacts to exist before apply is available
|
||||
- **AND** uses default instruction: "All artifacts complete. Proceed with implementation."
|
||||
|
||||
### Requirement: Apply Instructions Command
|
||||
|
||||
The system SHALL generate schema-aware apply instructions via `openspec instructions apply`.
|
||||
|
||||
#### Scenario: Generate apply instructions
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id>`
|
||||
- **AND** all required artifacts (per schema's `apply.requires`) exist
|
||||
- **THEN** the system outputs:
|
||||
- Context files from all existing artifacts
|
||||
- Schema-specific instruction text
|
||||
- Progress tracking file path (if `apply.tracks` is set)
|
||||
|
||||
#### Scenario: Apply blocked by missing artifacts
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id>`
|
||||
- **AND** required artifacts are missing
|
||||
- **THEN** the system indicates apply is blocked
|
||||
- **AND** lists which artifacts must be created first
|
||||
|
||||
#### Scenario: Apply instructions JSON output
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id> --json`
|
||||
- **THEN** the system outputs JSON with:
|
||||
- `contextFiles`: array of paths to existing artifacts
|
||||
- `instruction`: the apply instruction text
|
||||
- `tracks`: path to progress file or null
|
||||
- `applyRequires`: list of required artifact IDs
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Status Command
|
||||
|
||||
The system SHALL display artifact completion status for a change, including apply readiness.
|
||||
|
||||
#### Scenario: Status JSON includes apply requirements
|
||||
|
||||
- **WHEN** user runs `openspec status --change <id> --json`
|
||||
- **THEN** the system outputs JSON with:
|
||||
- `changeName`, `schemaName`, `isComplete`, `artifacts` array
|
||||
- `applyRequires`: array of artifact IDs needed for apply phase
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-07
|
||||
@@ -0,0 +1,84 @@
|
||||
## Context
|
||||
|
||||
The experimental workflow (OPSX) provides a complete lifecycle for creating changes:
|
||||
- `/opsx:new` - Scaffold a new change with schema
|
||||
- `/opsx:continue` - Create next artifact
|
||||
- `/opsx:ff` - Fast-forward all artifacts
|
||||
- `/opsx:apply` - Implement tasks
|
||||
- `/opsx:sync` - Sync delta specs to main
|
||||
|
||||
The missing piece is archiving. The existing `openspec archive` command works but:
|
||||
1. Applies specs programmatically (not agent-driven)
|
||||
2. Doesn't use the artifact graph for completion checking
|
||||
3. Doesn't integrate with the OPSX workflow philosophy
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add `/opsx:archive` skill to complete the OPSX workflow lifecycle
|
||||
- Use artifact graph for schema-aware completion checking
|
||||
- Integrate with `/opsx:sync` for agent-driven spec syncing
|
||||
- Preserve `.openspec.yaml` schema metadata in archive
|
||||
|
||||
**Non-Goals:**
|
||||
- Replacing the existing `openspec archive` CLI command
|
||||
- Changing how specs are applied in the CLI command
|
||||
- Modifying the artifact graph or schema system
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Skill-only implementation (no new CLI command)
|
||||
|
||||
The `/opsx:archive` will be a slash command/skill only, not a new CLI command.
|
||||
|
||||
**Rationale**: The existing `openspec archive` CLI command already handles the core archive functionality (moving to archive folder, date prefixing). The OPSX version just needs different pre-archive checks and optional sync prompting, which are agent behaviors better suited to a skill.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Adding flags to `openspec archive` (e.g., `--experimental`) - Rejected: adds complexity to CLI, harder to maintain two code paths
|
||||
- New CLI command `openspec archive-experimental` - Rejected: unnecessary duplication, agent skills are the OPSX pattern
|
||||
|
||||
### Decision 2: Prompt for sync before archive
|
||||
|
||||
The skill will check for unsynced delta specs and prompt the user before archiving.
|
||||
|
||||
**Rationale**: The OPSX philosophy is agent-driven intelligent merging via `/opsx:sync`. Rather than programmatically applying specs like the regular archive command, we prompt the user to sync first if needed. This maintains workflow flexibility (user can decline and just archive).
|
||||
|
||||
**Flow**:
|
||||
1. Check if `specs/` directory exists in the change
|
||||
2. If yes, ask: "This change has delta specs. Would you like to sync them to main specs before archiving?"
|
||||
3. If user says yes, execute `/opsx:sync` logic
|
||||
4. Proceed with archive regardless of answer
|
||||
|
||||
### Decision 3: Use artifact graph for completion checking
|
||||
|
||||
The skill will use `openspec status --change "<name>" --json` to check artifact completion instead of just validating proposal.md and specs.
|
||||
|
||||
**Rationale**: The experimental workflow is schema-aware. Different schemas have different required artifacts. The artifact graph knows which artifacts are complete/incomplete for the current schema.
|
||||
|
||||
**Behavior**:
|
||||
- Show warning if any artifacts are not `done`
|
||||
- Don't block archive (user may have valid reasons to archive early)
|
||||
- List incomplete artifacts so user can make informed decision
|
||||
|
||||
### Decision 4: Reuse tasks.md completion check from regular archive
|
||||
|
||||
The skill will parse tasks.md and warn about incomplete tasks, same as regular archive.
|
||||
|
||||
**Rationale**: Task completion checking is valuable regardless of workflow. The logic is simple (count `- [ ]` vs `- [x]`) and doesn't need special OPSX handling.
|
||||
|
||||
### Decision 5: Move change to archive/ with date prefix
|
||||
|
||||
Same archive behavior as regular command: move to `openspec/changes/archive/YYYY-MM-DD-<name>/`.
|
||||
|
||||
**Rationale**: Consistency with existing archive convention. The `.openspec.yaml` file moves with the change, preserving schema metadata.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**Risk**: Users confused about when to use `/opsx:archive` vs `openspec archive`
|
||||
→ **Mitigation**: Documentation should clarify: use `/opsx:archive` if you've been using the OPSX workflow, use `openspec archive` otherwise. Both produce the same archived result.
|
||||
|
||||
**Risk**: Incomplete sync if user declines and has delta specs
|
||||
→ **Mitigation**: The prompt is informational; user has full control. They may want to archive without syncing (e.g., abandoned change). Log a note in output.
|
||||
|
||||
**Trade-off**: No programmatic spec application in OPSX archive
|
||||
→ **Accepted**: This is intentional. OPSX philosophy is agent-driven merging. If user wants programmatic application, use `openspec archive` instead.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
The experimental workflow (OPSX) provides a schema-driven, artifact-by-artifact approach to creating changes with `/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:apply`, and `/opsx:sync`. However, there's no corresponding archive command to finalize and archive completed changes. Users must currently fall back to the regular `openspec archive` command, which doesn't integrate with the OPSX philosophy of agent-driven spec syncing and schema-aware artifact tracking.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `/opsx:archive` slash command for archiving changes in the experimental workflow
|
||||
- Use artifact graph to check completion status (schema-aware) instead of just validating proposal + specs
|
||||
- Prompt for `/opsx:sync` before archiving instead of programmatically applying specs
|
||||
- Preserve `.openspec.yaml` schema metadata when moving to archive
|
||||
- Integrate with existing OPSX commands for a cohesive workflow
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `opsx-archive-skill`: Slash command and skill for archiving completed changes in the experimental workflow. Checks artifact completion via artifact graph, verifies task completion, optionally syncs specs via `/opsx:sync`, and moves the change to `archive/YYYY-MM-DD-<name>/`.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(none - this is a new skill that doesn't modify existing specs)
|
||||
|
||||
## Impact
|
||||
|
||||
- New file: `.claude/commands/opsx/archive.md`
|
||||
- New skill definition (generated via `openspec artifact-experimental-setup`)
|
||||
- No changes to existing archive command or other OPSX commands
|
||||
- Completes the OPSX command suite for full lifecycle management
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: OPSX Archive Skill
|
||||
|
||||
The system SHALL provide an `/opsx:archive` skill that archives completed changes in the experimental workflow.
|
||||
|
||||
#### Scenario: Archive a change with all artifacts complete
|
||||
|
||||
- **WHEN** agent executes `/opsx:archive` with a change name
|
||||
- **AND** all artifacts in the schema are complete
|
||||
- **AND** all tasks are complete
|
||||
- **THEN** the agent moves the change to `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||
- **AND** displays success message with archived location
|
||||
|
||||
#### Scenario: Change selection prompt
|
||||
|
||||
- **WHEN** agent executes `/opsx:archive` without specifying a change
|
||||
- **THEN** the agent prompts user to select from available changes
|
||||
- **AND** shows only active changes (excludes archive/)
|
||||
|
||||
### Requirement: Artifact Completion Check
|
||||
|
||||
The skill SHALL check artifact completion status using the artifact graph before archiving.
|
||||
|
||||
#### Scenario: Incomplete artifacts warning
|
||||
|
||||
- **WHEN** agent checks artifact status
|
||||
- **AND** one or more artifacts have status other than `done`
|
||||
- **THEN** display warning listing incomplete artifacts
|
||||
- **AND** prompt user for confirmation to continue
|
||||
- **AND** proceed if user confirms
|
||||
|
||||
#### Scenario: All artifacts complete
|
||||
|
||||
- **WHEN** agent checks artifact status
|
||||
- **AND** all artifacts have status `done`
|
||||
- **THEN** proceed without warning
|
||||
|
||||
### Requirement: Task Completion Check
|
||||
|
||||
The skill SHALL check task completion status from tasks.md before archiving.
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
|
||||
- **WHEN** agent reads tasks.md
|
||||
- **AND** incomplete tasks are found (marked with `- [ ]`)
|
||||
- **THEN** display warning showing count of incomplete tasks
|
||||
- **AND** prompt user for confirmation to continue
|
||||
- **AND** proceed if user confirms
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
|
||||
- **WHEN** agent reads tasks.md
|
||||
- **AND** all tasks are complete (marked with `- [x]`)
|
||||
- **THEN** proceed without task-related warning
|
||||
|
||||
#### Scenario: No tasks file
|
||||
|
||||
- **WHEN** tasks.md does not exist
|
||||
- **THEN** proceed without task-related warning
|
||||
|
||||
### Requirement: Spec Sync Prompt
|
||||
|
||||
The skill SHALL prompt to sync delta specs before archiving if specs exist.
|
||||
|
||||
#### Scenario: Delta specs exist
|
||||
|
||||
- **WHEN** agent checks for delta specs
|
||||
- **AND** `specs/` directory exists in the change with spec files
|
||||
- **THEN** prompt user: "This change has delta specs. Would you like to sync them to main specs before archiving?"
|
||||
- **AND** if user confirms, execute `/opsx:sync` logic
|
||||
- **AND** proceed with archive regardless of sync choice
|
||||
|
||||
#### Scenario: No delta specs
|
||||
|
||||
- **WHEN** agent checks for delta specs
|
||||
- **AND** no `specs/` directory or no spec files exist
|
||||
- **THEN** proceed without sync prompt
|
||||
|
||||
### Requirement: Archive Process
|
||||
|
||||
The skill SHALL move the change to the archive folder with date prefix.
|
||||
|
||||
#### Scenario: Successful archive
|
||||
|
||||
- **WHEN** archiving a change
|
||||
- **THEN** create `archive/` directory if it doesn't exist
|
||||
- **AND** generate target name as `YYYY-MM-DD-<change-name>` using current date
|
||||
- **AND** move entire change directory to archive location
|
||||
- **AND** preserve `.openspec.yaml` file in archived change
|
||||
|
||||
#### Scenario: Archive already exists
|
||||
|
||||
- **WHEN** target archive directory already exists
|
||||
- **THEN** fail with error message
|
||||
- **AND** suggest renaming existing archive or using different date
|
||||
|
||||
### Requirement: Skill Output
|
||||
|
||||
The skill SHALL provide clear feedback about the archive operation.
|
||||
|
||||
#### Scenario: Archive complete with sync
|
||||
|
||||
- **WHEN** archive completes after syncing specs
|
||||
- **THEN** display summary:
|
||||
- Specs synced (from `/opsx:sync` output)
|
||||
- Change archived to location
|
||||
- Schema that was used
|
||||
|
||||
#### Scenario: Archive complete without sync
|
||||
|
||||
- **WHEN** archive completes without syncing specs
|
||||
- **THEN** display summary:
|
||||
- Note that specs were not synced (if applicable)
|
||||
- Change archived to location
|
||||
- Schema that was used
|
||||
|
||||
#### Scenario: Archive complete with warnings
|
||||
|
||||
- **WHEN** archive completes with incomplete artifacts or tasks
|
||||
- **THEN** include note about what was incomplete
|
||||
- **AND** suggest reviewing if archive was intentional
|
||||
@@ -0,0 +1,23 @@
|
||||
## 1. Create Slash Command
|
||||
|
||||
- [x] 1.1 Create `.claude/commands/opsx/archive.md` with skill definition
|
||||
- [x] 1.2 Add YAML frontmatter (name, description, category, tags)
|
||||
- [x] 1.3 Implement change selection logic (prompt if not provided)
|
||||
- [x] 1.4 Implement artifact completion check using `openspec status --json`
|
||||
- [x] 1.5 Implement task completion check (parse tasks.md for `- [ ]`)
|
||||
- [x] 1.6 Implement spec sync prompt (check for specs/ directory, offer `/opsx:sync`)
|
||||
- [x] 1.7 Implement archive process (move to archive/YYYY-MM-DD-<name>/)
|
||||
- [x] 1.8 Add output formatting for success/warning cases
|
||||
|
||||
## 2. Regenerate Skills
|
||||
|
||||
- [x] 2.1 Run `openspec artifact-experimental-setup` to regenerate skills
|
||||
- [x] 2.2 Verify skill appears in `.claude/skills/` directory
|
||||
|
||||
## 3. Testing
|
||||
|
||||
- [x] 3.1 Test `/opsx:archive` with a complete change (all artifacts, all tasks done)
|
||||
- [x] 3.2 Test `/opsx:archive` with incomplete artifacts (verify warning shown)
|
||||
- [x] 3.3 Test `/opsx:archive` with incomplete tasks (verify warning shown)
|
||||
- [x] 3.4 Test `/opsx:archive` with delta specs (verify sync prompt shown)
|
||||
- [x] 3.5 Test `/opsx:archive` without change name (verify selection prompt)
|
||||
@@ -1,12 +0,0 @@
|
||||
## Why
|
||||
Validation currently errors on changes without spec deltas, even when the change is intentionally proposal-only or tooling-only. This creates false negatives and noisy CI.
|
||||
|
||||
## What Changes
|
||||
- Make change validation scope-aware: validate only artifacts that exist.
|
||||
- Only error on "No deltas found" if spec delta files exist but parse to zero deltas.
|
||||
- Keep archive stricter: if specs exist but parse to zero deltas, fail; allow `--skip-specs` for tooling-only changes.
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-validate
|
||||
- Affected code: `src/commands/validate.ts`, `src/core/validation/validator.ts`
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Scope-Aware Change Validation
|
||||
The validator SHALL validate only artifacts that exist for a change, avoiding errors for proposal-only or tooling-only changes.
|
||||
|
||||
#### Scenario: Proposal-only change
|
||||
- **WHEN** a change contains `proposal.md` but has no `specs/` directory or contains no `*/spec.md` files
|
||||
- **THEN** validate the proposal (Why/What sections)
|
||||
- **AND** do not require or validate spec deltas
|
||||
|
||||
#### Scenario: Delta validation when specs exist
|
||||
- **WHEN** a change contains one or more `specs/<capability>/spec.md` files
|
||||
- **THEN** validate delta-formatted specs with existing rules (SHALL/MUST, scenarios, duplicates, conflicts)
|
||||
|
||||
## 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
|
||||
- **WHEN** validating a change that contains `specs/` with one or more `*/spec.md` files but the parser finds zero deltas
|
||||
- **THEN** show error "No deltas found" with guidance:
|
||||
- Ensure `openspec/changes/{id}/specs/` has `.md` files that include delta headers
|
||||
- Use delta headers: `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`
|
||||
- Each requirement must include at least one `#### Scenario:` block
|
||||
- Try: `openspec change show {id} --json --deltas-only` to inspect parsed deltas
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
## 1. Validator changes
|
||||
- [ ] 1.1 Change `validateChangeDeltaSpecs` to only emit "Change must have at least one delta" when `specs/` exists and contains at least one `*/spec.md` but parsed total deltas is 0
|
||||
- [ ] 1.2 Return valid (no error) when `specs/` directory is missing or has no `spec.md` files
|
||||
|
||||
## 2. CLI changes
|
||||
- [ ] 2.1 In bulk validation, keep current behavior (call delta validator). Behavior remains correct after 1.1
|
||||
- [ ] 2.2 Add a short INFO log in human-readable mode when a change has no `specs/` (optional)
|
||||
|
||||
## 3. Documentation
|
||||
- [ ] 3.1 Update README and template: "Validation checks only existing artifacts. Proposal-only changes are valid without spec deltas."
|
||||
|
||||
## 4. Tests
|
||||
- [ ] 4.1 Add test: proposal-only change passes validation without deltas
|
||||
- [ ] 4.2 Add test: specs present but zero parsed deltas → ERROR
|
||||
- [ ] 4.3 Add test: specs present with proper deltas → valid
|
||||
|
||||
@@ -27,6 +27,13 @@ The system SHALL display artifact completion status for a change, including scaf
|
||||
- **WHEN** user runs `openspec status --change <id> --json`
|
||||
- **THEN** the system outputs JSON with changeName, schemaName, isComplete, and artifacts array
|
||||
|
||||
#### Scenario: Status JSON includes apply requirements
|
||||
|
||||
- **WHEN** user runs `openspec status --change <id> --json`
|
||||
- **THEN** the system outputs JSON with:
|
||||
- `changeName`, `schemaName`, `isComplete`, `artifacts` array
|
||||
- `applyRequires`: array of artifact IDs needed for apply phase
|
||||
|
||||
#### Scenario: Status on scaffolded change
|
||||
|
||||
- **WHEN** user runs `openspec status --change <id>` on a change with no artifacts
|
||||
@@ -159,6 +166,52 @@ The system SHALL implement artifact workflow commands in isolation for easy remo
|
||||
- **WHEN** user runs `--help` on any artifact workflow command
|
||||
- **THEN** help text indicates the command is experimental
|
||||
|
||||
### Requirement: Schema Apply Block
|
||||
|
||||
The system SHALL support an `apply` block in schema definitions that controls when and how implementation begins.
|
||||
|
||||
#### Scenario: Schema with apply block
|
||||
|
||||
- **WHEN** a schema defines an `apply` block
|
||||
- **THEN** the system uses `apply.requires` to determine which artifacts must exist before apply
|
||||
- **AND** uses `apply.tracks` to identify the file for progress tracking (or null if none)
|
||||
- **AND** uses `apply.instruction` for guidance shown to the agent
|
||||
|
||||
#### Scenario: Schema without apply block
|
||||
|
||||
- **WHEN** a schema has no `apply` block
|
||||
- **THEN** the system requires all artifacts to exist before apply is available
|
||||
- **AND** uses default instruction: "All artifacts complete. Proceed with implementation."
|
||||
|
||||
### Requirement: Apply Instructions Command
|
||||
|
||||
The system SHALL generate schema-aware apply instructions via `openspec instructions apply`.
|
||||
|
||||
#### Scenario: Generate apply instructions
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id>`
|
||||
- **AND** all required artifacts (per schema's `apply.requires`) exist
|
||||
- **THEN** the system outputs:
|
||||
- Context files from all existing artifacts
|
||||
- Schema-specific instruction text
|
||||
- Progress tracking file path (if `apply.tracks` is set)
|
||||
|
||||
#### Scenario: Apply blocked by missing artifacts
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id>`
|
||||
- **AND** required artifacts are missing
|
||||
- **THEN** the system indicates apply is blocked
|
||||
- **AND** lists which artifacts must be created first
|
||||
|
||||
#### Scenario: Apply instructions JSON output
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id> --json`
|
||||
- **THEN** the system outputs JSON with:
|
||||
- `contextFiles`: array of paths to existing artifacts
|
||||
- `instruction`: the apply instruction text
|
||||
- `tracks`: path to progress file or null
|
||||
- `applyRequires`: list of required artifact IDs
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Next Command
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# OPSX Archive Skill Spec
|
||||
|
||||
### Requirement: OPSX Archive Skill
|
||||
|
||||
The system SHALL provide an `/opsx:archive` skill that archives completed changes in the experimental workflow.
|
||||
|
||||
#### Scenario: Archive a change with all artifacts complete
|
||||
|
||||
- **WHEN** agent executes `/opsx:archive` with a change name
|
||||
- **AND** all artifacts in the schema are complete
|
||||
- **AND** all tasks are complete
|
||||
- **THEN** the agent moves the change to `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||
- **AND** displays success message with archived location
|
||||
|
||||
#### Scenario: Change selection prompt
|
||||
|
||||
- **WHEN** agent executes `/opsx:archive` without specifying a change
|
||||
- **THEN** the agent prompts user to select from available changes
|
||||
- **AND** shows only active changes (excludes archive/)
|
||||
|
||||
### Requirement: Artifact Completion Check
|
||||
|
||||
The skill SHALL check artifact completion status using the artifact graph before archiving.
|
||||
|
||||
#### Scenario: Incomplete artifacts warning
|
||||
|
||||
- **WHEN** agent checks artifact status
|
||||
- **AND** one or more artifacts have status other than `done`
|
||||
- **THEN** display warning listing incomplete artifacts
|
||||
- **AND** prompt user for confirmation to continue
|
||||
- **AND** proceed if user confirms
|
||||
|
||||
#### Scenario: All artifacts complete
|
||||
|
||||
- **WHEN** agent checks artifact status
|
||||
- **AND** all artifacts have status `done`
|
||||
- **THEN** proceed without warning
|
||||
|
||||
### Requirement: Task Completion Check
|
||||
|
||||
The skill SHALL check task completion status from tasks.md before archiving.
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
|
||||
- **WHEN** agent reads tasks.md
|
||||
- **AND** incomplete tasks are found (marked with `- [ ]`)
|
||||
- **THEN** display warning showing count of incomplete tasks
|
||||
- **AND** prompt user for confirmation to continue
|
||||
- **AND** proceed if user confirms
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
|
||||
- **WHEN** agent reads tasks.md
|
||||
- **AND** all tasks are complete (marked with `- [x]`)
|
||||
- **THEN** proceed without task-related warning
|
||||
|
||||
#### Scenario: No tasks file
|
||||
|
||||
- **WHEN** tasks.md does not exist
|
||||
- **THEN** proceed without task-related warning
|
||||
|
||||
### Requirement: Spec Sync Prompt
|
||||
|
||||
The skill SHALL prompt to sync delta specs before archiving if specs exist.
|
||||
|
||||
#### Scenario: Delta specs exist
|
||||
|
||||
- **WHEN** agent checks for delta specs
|
||||
- **AND** `specs/` directory exists in the change with spec files
|
||||
- **THEN** prompt user: "This change has delta specs. Would you like to sync them to main specs before archiving?"
|
||||
- **AND** if user confirms, execute `/opsx:sync` logic
|
||||
- **AND** proceed with archive regardless of sync choice
|
||||
|
||||
#### Scenario: No delta specs
|
||||
|
||||
- **WHEN** agent checks for delta specs
|
||||
- **AND** no `specs/` directory or no spec files exist
|
||||
- **THEN** proceed without sync prompt
|
||||
|
||||
### Requirement: Archive Process
|
||||
|
||||
The skill SHALL move the change to the archive folder with date prefix.
|
||||
|
||||
#### Scenario: Successful archive
|
||||
|
||||
- **WHEN** archiving a change
|
||||
- **THEN** create `archive/` directory if it doesn't exist
|
||||
- **AND** generate target name as `YYYY-MM-DD-<change-name>` using current date
|
||||
- **AND** move entire change directory to archive location
|
||||
- **AND** preserve `.openspec.yaml` file in archived change
|
||||
|
||||
#### Scenario: Archive already exists
|
||||
|
||||
- **WHEN** target archive directory already exists
|
||||
- **THEN** fail with error message
|
||||
- **AND** suggest renaming existing archive or using different date
|
||||
|
||||
### Requirement: Skill Output
|
||||
|
||||
The skill SHALL provide clear feedback about the archive operation.
|
||||
|
||||
#### Scenario: Archive complete with sync
|
||||
|
||||
- **WHEN** archive completes after syncing specs
|
||||
- **THEN** display summary:
|
||||
- Specs synced (from `/opsx:sync` output)
|
||||
- Change archived to location
|
||||
- Schema that was used
|
||||
|
||||
#### Scenario: Archive complete without sync
|
||||
|
||||
- **WHEN** archive completes without syncing specs
|
||||
- **THEN** display summary:
|
||||
- Note that specs were not synced (if applicable)
|
||||
- Change archived to location
|
||||
- Schema that was used
|
||||
|
||||
#### Scenario: Archive complete with warnings
|
||||
|
||||
- **WHEN** archive completes with incomplete artifacts or tasks
|
||||
- **THEN** include note about what was incomplete
|
||||
- **AND** suggest reviewing if archive was intentional
|
||||
@@ -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"
|
||||
@@ -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, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate } from '../core/templates/skill-templates.js';
|
||||
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate } from '../core/templates/skill-templates.js';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -796,17 +796,26 @@ async function artifactExperimentalSetupCommand(): Promise<void> {
|
||||
const newChangeSkill = getNewChangeSkillTemplate();
|
||||
const continueChangeSkill = getContinueChangeSkillTemplate();
|
||||
const applyChangeSkill = getApplyChangeSkillTemplate();
|
||||
const ffChangeSkill = getFfChangeSkillTemplate();
|
||||
const syncSpecsSkill = getSyncSpecsSkillTemplate();
|
||||
const archiveChangeSkill = getArchiveChangeSkillTemplate();
|
||||
|
||||
// Get command templates
|
||||
const newCommand = getOpsxNewCommandTemplate();
|
||||
const continueCommand = getOpsxContinueCommandTemplate();
|
||||
const applyCommand = getOpsxApplyCommandTemplate();
|
||||
const ffCommand = getOpsxFfCommandTemplate();
|
||||
const syncCommand = getOpsxSyncCommandTemplate();
|
||||
const archiveCommand = getOpsxArchiveCommandTemplate();
|
||||
|
||||
// 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' },
|
||||
{ template: archiveChangeSkill, dirName: 'openspec-archive-change' },
|
||||
];
|
||||
|
||||
const createdSkillFiles: string[] = [];
|
||||
@@ -834,6 +843,9 @@ ${template.instructions}
|
||||
{ template: newCommand, fileName: 'new.md' },
|
||||
{ template: continueCommand, fileName: 'continue.md' },
|
||||
{ template: applyCommand, fileName: 'apply.md' },
|
||||
{ template: ffCommand, fileName: 'ff.md' },
|
||||
{ template: syncCommand, fileName: 'sync.md' },
|
||||
{ template: archiveCommand, fileName: 'archive.md' },
|
||||
];
|
||||
|
||||
const createdCommandFiles: string[] = [];
|
||||
@@ -889,6 +901,9 @@ ${template.content}
|
||||
console.log(' • /opsx:new - Start a new change');
|
||||
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(' • /opsx:archive - Archive a completed change');
|
||||
console.log();
|
||||
console.log(chalk.yellow('💡 This is an experimental feature.'));
|
||||
console.log(' Feedback welcome at: https://github.com/Fission-AI/OpenSpec/issues');
|
||||
|
||||
+8
-331
@@ -4,17 +4,11 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre
|
||||
import { Validator } from './validation/validator.js';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
extractRequirementsSection,
|
||||
parseDeltaSpec,
|
||||
normalizeRequirementName,
|
||||
type RequirementBlock,
|
||||
} from './parsers/requirement-blocks.js';
|
||||
|
||||
interface SpecUpdate {
|
||||
source: string;
|
||||
target: string;
|
||||
exists: boolean;
|
||||
}
|
||||
findSpecUpdates,
|
||||
buildUpdatedSpec,
|
||||
writeUpdatedSpec,
|
||||
type SpecUpdate,
|
||||
} from './specs-apply.js';
|
||||
|
||||
export class ArchiveCommand {
|
||||
async execute(
|
||||
@@ -167,7 +161,7 @@ export class ArchiveCommand {
|
||||
console.log('Skipping spec updates (--skip-specs flag provided).');
|
||||
} else {
|
||||
// Find specs to update
|
||||
const specUpdates = await this.findSpecUpdates(changeDir, mainSpecsDir);
|
||||
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
||||
|
||||
if (specUpdates.length > 0) {
|
||||
console.log('\nSpecs to update:');
|
||||
@@ -194,7 +188,7 @@ export class ArchiveCommand {
|
||||
const prepared: Array<{ update: SpecUpdate; rebuilt: string; counts: { added: number; modified: number; removed: number; renamed: number } }> = [];
|
||||
try {
|
||||
for (const update of specUpdates) {
|
||||
const built = await this.buildUpdatedSpec(update, changeName!);
|
||||
const built = await buildUpdatedSpec(update, changeName!);
|
||||
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -219,7 +213,7 @@ export class ArchiveCommand {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
||||
await writeUpdatedSpec(p.update, p.rebuilt, p.counts);
|
||||
totals.added += p.counts.added;
|
||||
totals.modified += p.counts.modified;
|
||||
totals.removed += p.counts.removed;
|
||||
@@ -301,323 +295,6 @@ export class ArchiveCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: replaced by shared task-progress utilities
|
||||
private async checkIncompleteTasks(_tasksPath: string): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async findSpecUpdates(changeDir: string, mainSpecsDir: string): Promise<SpecUpdate[]> {
|
||||
const updates: SpecUpdate[] = [];
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
const targetFile = path.join(mainSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
|
||||
// Check if target exists
|
||||
let exists = false;
|
||||
try {
|
||||
await fs.access(targetFile);
|
||||
exists = true;
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
updates.push({
|
||||
source: specFile,
|
||||
target: targetFile,
|
||||
exists
|
||||
});
|
||||
} catch {
|
||||
// Source spec doesn't exist, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No specs directory in change
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
private async buildUpdatedSpec(update: SpecUpdate, changeName: string): Promise<{ rebuilt: string; counts: { added: number; modified: number; removed: number; renamed: number } }> {
|
||||
// Read change spec content (delta-format expected)
|
||||
const changeContent = await fs.readFile(update.source, 'utf-8');
|
||||
|
||||
// Parse deltas from the change spec file
|
||||
const plan = parseDeltaSpec(changeContent);
|
||||
const specName = path.basename(path.dirname(update.target));
|
||||
|
||||
// Pre-validate duplicates within sections
|
||||
const addedNames = new Set<string>();
|
||||
for (const add of plan.added) {
|
||||
const name = normalizeRequirementName(add.name);
|
||||
if (addedNames.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in ADDED for header "### Requirement: ${add.name}"`
|
||||
);
|
||||
}
|
||||
addedNames.add(name);
|
||||
}
|
||||
const modifiedNames = new Set<string>();
|
||||
for (const mod of plan.modified) {
|
||||
const name = normalizeRequirementName(mod.name);
|
||||
if (modifiedNames.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`
|
||||
);
|
||||
}
|
||||
modifiedNames.add(name);
|
||||
}
|
||||
const removedNamesSet = new Set<string>();
|
||||
for (const rem of plan.removed) {
|
||||
const name = normalizeRequirementName(rem);
|
||||
if (removedNamesSet.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`
|
||||
);
|
||||
}
|
||||
removedNamesSet.add(name);
|
||||
}
|
||||
const renamedFromSet = new Set<string>();
|
||||
const renamedToSet = new Set<string>();
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromNorm = normalizeRequirementName(from);
|
||||
const toNorm = normalizeRequirementName(to);
|
||||
if (renamedFromSet.has(fromNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate FROM in RENAMED for header "### Requirement: ${from}"`
|
||||
);
|
||||
}
|
||||
if (renamedToSet.has(toNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate TO in RENAMED for header "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
renamedFromSet.add(fromNorm);
|
||||
renamedToSet.add(toNorm);
|
||||
}
|
||||
|
||||
// Pre-validate cross-section conflicts
|
||||
const conflicts: Array<{ name: string; a: string; b: string }> = [];
|
||||
for (const n of modifiedNames) {
|
||||
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'REMOVED' });
|
||||
if (addedNames.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'ADDED' });
|
||||
}
|
||||
for (const n of addedNames) {
|
||||
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
|
||||
}
|
||||
// Renamed interplay: MODIFIED must reference the NEW header, not FROM
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromNorm = normalizeRequirementName(from);
|
||||
const toNorm = normalizeRequirementName(to);
|
||||
if (modifiedNames.has(fromNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
// Detect ADDED colliding with a RENAMED TO
|
||||
if (addedNames.has(toNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (conflicts.length > 0) {
|
||||
const c = conflicts[0];
|
||||
throw new Error(
|
||||
`${specName} validation failed - requirement present in multiple sections (${c.a} and ${c.b}) for header "### Requirement: ${c.name}"`
|
||||
);
|
||||
}
|
||||
const hasAnyDelta = (plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length) > 0;
|
||||
if (!hasAnyDelta) {
|
||||
throw new Error(
|
||||
`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
|
||||
`Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`
|
||||
);
|
||||
}
|
||||
|
||||
// Load or create base target content
|
||||
let targetContent: string;
|
||||
let isNewSpec = false;
|
||||
try {
|
||||
targetContent = await fs.readFile(update.target, 'utf-8');
|
||||
} catch {
|
||||
// Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
|
||||
// REMOVED will be ignored with a warning since there's nothing to remove
|
||||
if (plan.modified.length > 0 || plan.renamed.length > 0) {
|
||||
throw new Error(
|
||||
`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs. MODIFIED and RENAMED operations require an existing spec.`
|
||||
);
|
||||
}
|
||||
// Warn about REMOVED requirements being ignored for new specs
|
||||
if (plan.removed.length > 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Warning: ${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`
|
||||
)
|
||||
);
|
||||
}
|
||||
isNewSpec = true;
|
||||
targetContent = this.buildSpecSkeleton(specName, changeName);
|
||||
}
|
||||
|
||||
// Extract requirements section and build name->block map
|
||||
const parts = extractRequirementsSection(targetContent);
|
||||
const nameToBlock = new Map<string, RequirementBlock>();
|
||||
for (const block of parts.bodyBlocks) {
|
||||
nameToBlock.set(normalizeRequirementName(block.name), block);
|
||||
}
|
||||
|
||||
// Apply operations in order: RENAMED → REMOVED → MODIFIED → ADDED
|
||||
// RENAMED
|
||||
for (const r of plan.renamed) {
|
||||
const from = normalizeRequirementName(r.from);
|
||||
const to = normalizeRequirementName(r.to);
|
||||
if (!nameToBlock.has(from)) {
|
||||
throw new Error(
|
||||
`${specName} RENAMED failed for header "### Requirement: ${r.from}" - source not found`
|
||||
);
|
||||
}
|
||||
if (nameToBlock.has(to)) {
|
||||
throw new Error(
|
||||
`${specName} RENAMED failed for header "### Requirement: ${r.to}" - target already exists`
|
||||
);
|
||||
}
|
||||
const block = nameToBlock.get(from)!;
|
||||
const newHeader = `### Requirement: ${to}`;
|
||||
const rawLines = block.raw.split('\n');
|
||||
rawLines[0] = newHeader;
|
||||
const renamedBlock: RequirementBlock = {
|
||||
headerLine: newHeader,
|
||||
name: to,
|
||||
raw: rawLines.join('\n'),
|
||||
};
|
||||
nameToBlock.delete(from);
|
||||
nameToBlock.set(to, renamedBlock);
|
||||
}
|
||||
|
||||
// REMOVED
|
||||
for (const name of plan.removed) {
|
||||
const key = normalizeRequirementName(name);
|
||||
if (!nameToBlock.has(key)) {
|
||||
// For new specs, REMOVED requirements are already warned about and ignored
|
||||
// For existing specs, missing requirements are an error
|
||||
if (!isNewSpec) {
|
||||
throw new Error(
|
||||
`${specName} REMOVED failed for header "### Requirement: ${name}" - not found`
|
||||
);
|
||||
}
|
||||
// Skip removal for new specs (already warned above)
|
||||
continue;
|
||||
}
|
||||
nameToBlock.delete(key);
|
||||
}
|
||||
|
||||
// MODIFIED
|
||||
for (const mod of plan.modified) {
|
||||
const key = normalizeRequirementName(mod.name);
|
||||
if (!nameToBlock.has(key)) {
|
||||
throw new Error(
|
||||
`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`
|
||||
);
|
||||
}
|
||||
// Replace block with provided raw (ensure header line matches key)
|
||||
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
|
||||
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
||||
throw new Error(
|
||||
`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`
|
||||
);
|
||||
}
|
||||
nameToBlock.set(key, mod);
|
||||
}
|
||||
|
||||
// ADDED
|
||||
for (const add of plan.added) {
|
||||
const key = normalizeRequirementName(add.name);
|
||||
if (nameToBlock.has(key)) {
|
||||
throw new Error(
|
||||
`${specName} ADDED failed for header "### Requirement: ${add.name}" - already exists`
|
||||
);
|
||||
}
|
||||
nameToBlock.set(key, add);
|
||||
}
|
||||
|
||||
// Duplicates within resulting map are implicitly prevented by key uniqueness.
|
||||
|
||||
// Recompose requirements section preserving original ordering where possible
|
||||
const keptOrder: RequirementBlock[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const block of parts.bodyBlocks) {
|
||||
const key = normalizeRequirementName(block.name);
|
||||
const replacement = nameToBlock.get(key);
|
||||
if (replacement) {
|
||||
keptOrder.push(replacement);
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
// Append any newly added that were not in original order
|
||||
for (const [key, block] of nameToBlock.entries()) {
|
||||
if (!seen.has(key)) {
|
||||
keptOrder.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
const reqBody = [
|
||||
parts.preamble && parts.preamble.trim() ? parts.preamble.trimEnd() : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.concat(keptOrder.map(b => b.raw))
|
||||
.join('\n\n')
|
||||
.trimEnd();
|
||||
|
||||
const rebuilt = [
|
||||
parts.before.trimEnd(),
|
||||
parts.headerLine,
|
||||
reqBody,
|
||||
parts.after
|
||||
]
|
||||
.filter((s, idx) => !(idx === 0 && s === ''))
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
return {
|
||||
rebuilt,
|
||||
counts: {
|
||||
added: plan.added.length,
|
||||
modified: plan.modified.length,
|
||||
removed: plan.removed.length,
|
||||
renamed: plan.renamed.length,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async writeUpdatedSpec(update: SpecUpdate, rebuilt: string, counts: { added: number; modified: number; removed: number; renamed: number }): Promise<void> {
|
||||
// Create target directory if needed
|
||||
const targetDir = path.dirname(update.target);
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
await fs.writeFile(update.target, rebuilt);
|
||||
|
||||
const specName = path.basename(path.dirname(update.target));
|
||||
console.log(`Applying changes to openspec/specs/${specName}/spec.md:`);
|
||||
if (counts.added) console.log(` + ${counts.added} added`);
|
||||
if (counts.modified) console.log(` ~ ${counts.modified} modified`);
|
||||
if (counts.removed) console.log(` - ${counts.removed} removed`);
|
||||
if (counts.renamed) console.log(` → ${counts.renamed} renamed`);
|
||||
}
|
||||
|
||||
private buildSpecSkeleton(specFolderName: string, changeName: string): string {
|
||||
const titleBase = specFolderName;
|
||||
return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`;
|
||||
}
|
||||
|
||||
private getArchiveDate(): string {
|
||||
// Returns date in YYYY-MM-DD format
|
||||
return new Date().toISOString().split('T')[0];
|
||||
|
||||
@@ -99,6 +99,8 @@ export interface ChangeStatus {
|
||||
schemaName: string;
|
||||
/** Whether all artifacts are complete */
|
||||
isComplete: boolean;
|
||||
/** Artifact IDs required before apply phase (from schema's apply.requires) */
|
||||
applyRequires: string[];
|
||||
/** Status of each artifact */
|
||||
artifacts: ArtifactStatus[];
|
||||
}
|
||||
@@ -252,6 +254,10 @@ function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[
|
||||
* @returns Formatted change status
|
||||
*/
|
||||
export function formatChangeStatus(context: ChangeContext): ChangeStatus {
|
||||
// Load schema to get apply phase configuration
|
||||
const schema = resolveSchema(context.schemaName);
|
||||
const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
|
||||
|
||||
const artifacts = context.graph.getAllArtifacts();
|
||||
const ready = new Set(context.graph.getNextArtifacts(context.completed));
|
||||
const blocked = context.graph.getBlocked(context.completed);
|
||||
@@ -290,6 +296,7 @@ export function formatChangeStatus(context: ChangeContext): ChangeStatus {
|
||||
changeName: context.changeName,
|
||||
schemaName: context.schemaName,
|
||||
isComplete: context.graph.isComplete(context.completed),
|
||||
applyRequires,
|
||||
artifacts: artifactStatuses,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* Spec Application Logic
|
||||
*
|
||||
* Extracted from ArchiveCommand to enable standalone spec application.
|
||||
* Applies delta specs from a change to main specs without archiving.
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
extractRequirementsSection,
|
||||
parseDeltaSpec,
|
||||
normalizeRequirementName,
|
||||
type RequirementBlock,
|
||||
} from './parsers/requirement-blocks.js';
|
||||
import { Validator } from './validation/validator.js';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export interface SpecUpdate {
|
||||
source: string;
|
||||
target: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface ApplyResult {
|
||||
capability: string;
|
||||
added: number;
|
||||
modified: number;
|
||||
removed: number;
|
||||
renamed: number;
|
||||
}
|
||||
|
||||
export interface SpecsApplyOutput {
|
||||
changeName: string;
|
||||
capabilities: ApplyResult[];
|
||||
totals: {
|
||||
added: number;
|
||||
modified: number;
|
||||
removed: number;
|
||||
renamed: number;
|
||||
};
|
||||
noChanges: boolean;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find all delta spec files that need to be applied from a change.
|
||||
*/
|
||||
export async function findSpecUpdates(changeDir: string, mainSpecsDir: string): Promise<SpecUpdate[]> {
|
||||
const updates: SpecUpdate[] = [];
|
||||
const changeSpecsDir = path.join(changeDir, 'specs');
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const specFile = path.join(changeSpecsDir, entry.name, 'spec.md');
|
||||
const targetFile = path.join(mainSpecsDir, entry.name, 'spec.md');
|
||||
|
||||
try {
|
||||
await fs.access(specFile);
|
||||
|
||||
// Check if target exists
|
||||
let exists = false;
|
||||
try {
|
||||
await fs.access(targetFile);
|
||||
exists = true;
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
updates.push({
|
||||
source: specFile,
|
||||
target: targetFile,
|
||||
exists,
|
||||
});
|
||||
} catch {
|
||||
// Source spec doesn't exist, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No specs directory in change
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an updated spec by applying delta operations.
|
||||
* Returns the rebuilt content and counts of operations.
|
||||
*/
|
||||
export async function buildUpdatedSpec(
|
||||
update: SpecUpdate,
|
||||
changeName: string
|
||||
): Promise<{ rebuilt: string; counts: { added: number; modified: number; removed: number; renamed: number } }> {
|
||||
// Read change spec content (delta-format expected)
|
||||
const changeContent = await fs.readFile(update.source, 'utf-8');
|
||||
|
||||
// Parse deltas from the change spec file
|
||||
const plan = parseDeltaSpec(changeContent);
|
||||
const specName = path.basename(path.dirname(update.target));
|
||||
|
||||
// Pre-validate duplicates within sections
|
||||
const addedNames = new Set<string>();
|
||||
for (const add of plan.added) {
|
||||
const name = normalizeRequirementName(add.name);
|
||||
if (addedNames.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in ADDED for header "### Requirement: ${add.name}"`
|
||||
);
|
||||
}
|
||||
addedNames.add(name);
|
||||
}
|
||||
const modifiedNames = new Set<string>();
|
||||
for (const mod of plan.modified) {
|
||||
const name = normalizeRequirementName(mod.name);
|
||||
if (modifiedNames.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`
|
||||
);
|
||||
}
|
||||
modifiedNames.add(name);
|
||||
}
|
||||
const removedNamesSet = new Set<string>();
|
||||
for (const rem of plan.removed) {
|
||||
const name = normalizeRequirementName(rem);
|
||||
if (removedNamesSet.has(name)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`
|
||||
);
|
||||
}
|
||||
removedNamesSet.add(name);
|
||||
}
|
||||
const renamedFromSet = new Set<string>();
|
||||
const renamedToSet = new Set<string>();
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromNorm = normalizeRequirementName(from);
|
||||
const toNorm = normalizeRequirementName(to);
|
||||
if (renamedFromSet.has(fromNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate FROM in RENAMED for header "### Requirement: ${from}"`
|
||||
);
|
||||
}
|
||||
if (renamedToSet.has(toNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - duplicate TO in RENAMED for header "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
renamedFromSet.add(fromNorm);
|
||||
renamedToSet.add(toNorm);
|
||||
}
|
||||
|
||||
// Pre-validate cross-section conflicts
|
||||
const conflicts: Array<{ name: string; a: string; b: string }> = [];
|
||||
for (const n of modifiedNames) {
|
||||
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'REMOVED' });
|
||||
if (addedNames.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'ADDED' });
|
||||
}
|
||||
for (const n of addedNames) {
|
||||
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
|
||||
}
|
||||
// Renamed interplay: MODIFIED must reference the NEW header, not FROM
|
||||
for (const { from, to } of plan.renamed) {
|
||||
const fromNorm = normalizeRequirementName(from);
|
||||
const toNorm = normalizeRequirementName(to);
|
||||
if (modifiedNames.has(fromNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
// Detect ADDED colliding with a RENAMED TO
|
||||
if (addedNames.has(toNorm)) {
|
||||
throw new Error(
|
||||
`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (conflicts.length > 0) {
|
||||
const c = conflicts[0];
|
||||
throw new Error(
|
||||
`${specName} validation failed - requirement present in multiple sections (${c.a} and ${c.b}) for header "### Requirement: ${c.name}"`
|
||||
);
|
||||
}
|
||||
const hasAnyDelta = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
|
||||
if (!hasAnyDelta) {
|
||||
throw new Error(
|
||||
`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
|
||||
`Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`
|
||||
);
|
||||
}
|
||||
|
||||
// Load or create base target content
|
||||
let targetContent: string;
|
||||
let isNewSpec = false;
|
||||
try {
|
||||
targetContent = await fs.readFile(update.target, 'utf-8');
|
||||
} catch {
|
||||
// Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
|
||||
// REMOVED will be ignored with a warning since there's nothing to remove
|
||||
if (plan.modified.length > 0 || plan.renamed.length > 0) {
|
||||
throw new Error(
|
||||
`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs. MODIFIED and RENAMED operations require an existing spec.`
|
||||
);
|
||||
}
|
||||
// Warn about REMOVED requirements being ignored for new specs
|
||||
if (plan.removed.length > 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Warning: ${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`
|
||||
)
|
||||
);
|
||||
}
|
||||
isNewSpec = true;
|
||||
targetContent = buildSpecSkeleton(specName, changeName);
|
||||
}
|
||||
|
||||
// Extract requirements section and build name->block map
|
||||
const parts = extractRequirementsSection(targetContent);
|
||||
const nameToBlock = new Map<string, RequirementBlock>();
|
||||
for (const block of parts.bodyBlocks) {
|
||||
nameToBlock.set(normalizeRequirementName(block.name), block);
|
||||
}
|
||||
|
||||
// Apply operations in order: RENAMED → REMOVED → MODIFIED → ADDED
|
||||
// RENAMED
|
||||
for (const r of plan.renamed) {
|
||||
const from = normalizeRequirementName(r.from);
|
||||
const to = normalizeRequirementName(r.to);
|
||||
if (!nameToBlock.has(from)) {
|
||||
throw new Error(`${specName} RENAMED failed for header "### Requirement: ${r.from}" - source not found`);
|
||||
}
|
||||
if (nameToBlock.has(to)) {
|
||||
throw new Error(`${specName} RENAMED failed for header "### Requirement: ${r.to}" - target already exists`);
|
||||
}
|
||||
const block = nameToBlock.get(from)!;
|
||||
const newHeader = `### Requirement: ${to}`;
|
||||
const rawLines = block.raw.split('\n');
|
||||
rawLines[0] = newHeader;
|
||||
const renamedBlock: RequirementBlock = {
|
||||
headerLine: newHeader,
|
||||
name: to,
|
||||
raw: rawLines.join('\n'),
|
||||
};
|
||||
nameToBlock.delete(from);
|
||||
nameToBlock.set(to, renamedBlock);
|
||||
}
|
||||
|
||||
// REMOVED
|
||||
for (const name of plan.removed) {
|
||||
const key = normalizeRequirementName(name);
|
||||
if (!nameToBlock.has(key)) {
|
||||
// For new specs, REMOVED requirements are already warned about and ignored
|
||||
// For existing specs, missing requirements are an error
|
||||
if (!isNewSpec) {
|
||||
throw new Error(`${specName} REMOVED failed for header "### Requirement: ${name}" - not found`);
|
||||
}
|
||||
// Skip removal for new specs (already warned above)
|
||||
continue;
|
||||
}
|
||||
nameToBlock.delete(key);
|
||||
}
|
||||
|
||||
// MODIFIED
|
||||
for (const mod of plan.modified) {
|
||||
const key = normalizeRequirementName(mod.name);
|
||||
if (!nameToBlock.has(key)) {
|
||||
throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
|
||||
}
|
||||
// Replace block with provided raw (ensure header line matches key)
|
||||
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
|
||||
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
|
||||
throw new Error(
|
||||
`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`
|
||||
);
|
||||
}
|
||||
nameToBlock.set(key, mod);
|
||||
}
|
||||
|
||||
// ADDED
|
||||
for (const add of plan.added) {
|
||||
const key = normalizeRequirementName(add.name);
|
||||
if (nameToBlock.has(key)) {
|
||||
throw new Error(`${specName} ADDED failed for header "### Requirement: ${add.name}" - already exists`);
|
||||
}
|
||||
nameToBlock.set(key, add);
|
||||
}
|
||||
|
||||
// Duplicates within resulting map are implicitly prevented by key uniqueness.
|
||||
|
||||
// Recompose requirements section preserving original ordering where possible
|
||||
const keptOrder: RequirementBlock[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const block of parts.bodyBlocks) {
|
||||
const key = normalizeRequirementName(block.name);
|
||||
const replacement = nameToBlock.get(key);
|
||||
if (replacement) {
|
||||
keptOrder.push(replacement);
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
// Append any newly added that were not in original order
|
||||
for (const [key, block] of nameToBlock.entries()) {
|
||||
if (!seen.has(key)) {
|
||||
keptOrder.push(block);
|
||||
}
|
||||
}
|
||||
|
||||
const reqBody = [parts.preamble && parts.preamble.trim() ? parts.preamble.trimEnd() : '']
|
||||
.filter(Boolean)
|
||||
.concat(keptOrder.map((b) => b.raw))
|
||||
.join('\n\n')
|
||||
.trimEnd();
|
||||
|
||||
const rebuilt = [parts.before.trimEnd(), parts.headerLine, reqBody, parts.after]
|
||||
.filter((s, idx) => !(idx === 0 && s === ''))
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
return {
|
||||
rebuilt,
|
||||
counts: {
|
||||
added: plan.added.length,
|
||||
modified: plan.modified.length,
|
||||
removed: plan.removed.length,
|
||||
renamed: plan.renamed.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an updated spec to disk.
|
||||
*/
|
||||
export async function writeUpdatedSpec(
|
||||
update: SpecUpdate,
|
||||
rebuilt: string,
|
||||
counts: { added: number; modified: number; removed: number; renamed: number }
|
||||
): Promise<void> {
|
||||
// Create target directory if needed
|
||||
const targetDir = path.dirname(update.target);
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
await fs.writeFile(update.target, rebuilt);
|
||||
|
||||
const specName = path.basename(path.dirname(update.target));
|
||||
console.log(`Applying changes to openspec/specs/${specName}/spec.md:`);
|
||||
if (counts.added) console.log(` + ${counts.added} added`);
|
||||
if (counts.modified) console.log(` ~ ${counts.modified} modified`);
|
||||
if (counts.removed) console.log(` - ${counts.removed} removed`);
|
||||
if (counts.renamed) console.log(` → ${counts.renamed} renamed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a skeleton spec for new capabilities.
|
||||
*/
|
||||
export function buildSpecSkeleton(specFolderName: string, changeName: string): string {
|
||||
const titleBase = specFolderName;
|
||||
return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all delta specs from a change to main specs.
|
||||
*
|
||||
* @param projectRoot - The project root directory
|
||||
* @param changeName - The name of the change to apply
|
||||
* @param options - Options for the operation
|
||||
* @returns Result of the operation with counts
|
||||
*/
|
||||
export async function applySpecs(
|
||||
projectRoot: string,
|
||||
changeName: string,
|
||||
options: {
|
||||
dryRun?: boolean;
|
||||
skipValidation?: boolean;
|
||||
silent?: boolean;
|
||||
} = {}
|
||||
): Promise<SpecsApplyOutput> {
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
const mainSpecsDir = path.join(projectRoot, 'openspec', 'specs');
|
||||
|
||||
// Verify change exists
|
||||
try {
|
||||
const stat = await fs.stat(changeDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Change '${changeName}' not found.`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Change '${changeName}' not found.`);
|
||||
}
|
||||
|
||||
// Find specs to update
|
||||
const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
|
||||
|
||||
if (specUpdates.length === 0) {
|
||||
return {
|
||||
changeName,
|
||||
capabilities: [],
|
||||
totals: { added: 0, modified: 0, removed: 0, renamed: 0 },
|
||||
noChanges: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare all updates first (validation pass, no writes)
|
||||
const prepared: Array<{
|
||||
update: SpecUpdate;
|
||||
rebuilt: string;
|
||||
counts: { added: number; modified: number; removed: number; renamed: number };
|
||||
}> = [];
|
||||
|
||||
for (const update of specUpdates) {
|
||||
const built = await buildUpdatedSpec(update, changeName);
|
||||
prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
|
||||
}
|
||||
|
||||
// Validate rebuilt specs unless validation is skipped
|
||||
if (!options.skipValidation) {
|
||||
const validator = new Validator();
|
||||
for (const p of prepared) {
|
||||
const specName = path.basename(path.dirname(p.update.target));
|
||||
const report = await validator.validateSpecContent(specName, p.rebuilt);
|
||||
if (!report.valid) {
|
||||
const errors = report.issues
|
||||
.filter((i) => i.level === 'ERROR')
|
||||
.map((i) => ` ✗ ${i.message}`)
|
||||
.join('\n');
|
||||
throw new Error(`Validation errors in rebuilt spec for ${specName}:\n${errors}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build results
|
||||
const capabilities: ApplyResult[] = [];
|
||||
const totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
|
||||
|
||||
for (const p of prepared) {
|
||||
const capability = path.basename(path.dirname(p.update.target));
|
||||
|
||||
if (!options.dryRun) {
|
||||
// Write the updated spec
|
||||
const targetDir = path.dirname(p.update.target);
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
await fs.writeFile(p.update.target, p.rebuilt);
|
||||
|
||||
if (!options.silent) {
|
||||
console.log(`Applying changes to openspec/specs/${capability}/spec.md:`);
|
||||
if (p.counts.added) console.log(` + ${p.counts.added} added`);
|
||||
if (p.counts.modified) console.log(` ~ ${p.counts.modified} modified`);
|
||||
if (p.counts.removed) console.log(` - ${p.counts.removed} removed`);
|
||||
if (p.counts.renamed) console.log(` → ${p.counts.renamed} renamed`);
|
||||
}
|
||||
} else if (!options.silent) {
|
||||
console.log(`Would apply changes to openspec/specs/${capability}/spec.md:`);
|
||||
if (p.counts.added) console.log(` + ${p.counts.added} added`);
|
||||
if (p.counts.modified) console.log(` ~ ${p.counts.modified} modified`);
|
||||
if (p.counts.removed) console.log(` - ${p.counts.removed} removed`);
|
||||
if (p.counts.renamed) console.log(` → ${p.counts.renamed} renamed`);
|
||||
}
|
||||
|
||||
capabilities.push({
|
||||
capability,
|
||||
...p.counts,
|
||||
});
|
||||
|
||||
totals.added += p.counts.added;
|
||||
totals.modified += p.counts.modified;
|
||||
totals.removed += p.counts.removed;
|
||||
totals.renamed += p.counts.renamed;
|
||||
}
|
||||
|
||||
return {
|
||||
changeName,
|
||||
capabilities,
|
||||
totals,
|
||||
noChanges: false,
|
||||
};
|
||||
}
|
||||
@@ -360,6 +360,239 @@ This skill supports the "actions on a change" model:
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-ff-change skill
|
||||
* Fast-forward through artifact creation
|
||||
*/
|
||||
export function getFfChangeSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-ff-change',
|
||||
description: 'Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.',
|
||||
instructions: `Fast-forward through artifact creation - generate everything needed to start implementation in one go.
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → \`add-user-auth\`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
\`\`\`bash
|
||||
openspec new change "<name>"
|
||||
\`\`\`
|
||||
This creates a scaffolded change at \`openspec/changes/<name>/\`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>" --json
|
||||
\`\`\`
|
||||
Parse the JSON to get:
|
||||
- \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`)
|
||||
- \`artifacts\`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is \`ready\` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
\`\`\`bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
\`\`\`
|
||||
- The instructions JSON includes:
|
||||
- \`template\`: The template content to use
|
||||
- \`instruction\`: Schema-specific guidance for this artifact type
|
||||
- \`outputPath\`: Where to write the artifact
|
||||
- \`dependencies\`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file following the schema's \`instruction\`
|
||||
- Show brief progress: "✓ Created <artifact-id>"
|
||||
|
||||
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
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>"
|
||||
\`\`\`
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run \`/opsx:apply\` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the \`instruction\` field from \`openspec instructions\` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use the \`template\` as a starting point, filling in based on context
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's \`apply.requires\`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, suggest continuing that change instead
|
||||
- Verify each artifact file exists after writing before proceeding to next`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-sync-specs skill
|
||||
* For syncing delta specs from a change to main specs (agent-driven)
|
||||
*/
|
||||
export function getSyncSpecsSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-sync-specs',
|
||||
description: 'Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.',
|
||||
instructions: `Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have delta specs (under \`specs/\` directory).
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Find delta specs**
|
||||
|
||||
Look for delta spec files in \`openspec/changes/<name>/specs/*/spec.md\`.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- \`## ADDED Requirements\` - New requirements to add
|
||||
- \`## MODIFIED Requirements\` - Changes to existing requirements
|
||||
- \`## REMOVED Requirements\` - Requirements to remove
|
||||
- \`## RENAMED Requirements\` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
3. **For each delta spec, apply changes to main specs**
|
||||
|
||||
For each capability with a delta spec at \`openspec/changes/<name>/specs/<capability>/spec.md\`:
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at \`openspec/specs/<capability>/spec.md\` (may not exist yet)
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios (don't need to copy existing ones)
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Create \`openspec/specs/<capability>/spec.md\`
|
||||
- Add Purpose section (can be brief, mark as TBD)
|
||||
- Add Requirements section with the ADDED requirements
|
||||
|
||||
4. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
\`\`\`markdown
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: \`### Requirement: Old Name\`
|
||||
- TO: \`### Requirement: New Name\`
|
||||
\`\`\`
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you can apply **partial updates**:
|
||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||
- The delta represents *intent*, not a wholesale replacement
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
\`\`\`
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result`
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Slash Command Templates
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -719,3 +952,551 @@ This skill supports the "actions on a change" model:
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Template for /opsx:ff slash command
|
||||
*/
|
||||
export function getOpsxFfCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: Fast Forward',
|
||||
description: 'Create a change and generate all artifacts needed for implementation in one go',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'artifacts', 'experimental'],
|
||||
content: `Fast-forward through artifact creation - generate everything needed to start implementation.
|
||||
|
||||
**Input**: The argument after \`/opsx:ff\` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → \`add-user-auth\`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
\`\`\`bash
|
||||
openspec new change "<name>"
|
||||
\`\`\`
|
||||
This creates a scaffolded change at \`openspec/changes/<name>/\`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>" --json
|
||||
\`\`\`
|
||||
Parse the JSON to get:
|
||||
- \`applyRequires\`: array of artifact IDs needed before implementation (e.g., \`["tasks"]\`)
|
||||
- \`artifacts\`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is \`ready\` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
\`\`\`bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
\`\`\`
|
||||
- The instructions JSON includes:
|
||||
- \`template\`: The template content to use
|
||||
- \`instruction\`: Schema-specific guidance for this artifact type
|
||||
- \`outputPath\`: Where to write the artifact
|
||||
- \`dependencies\`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file following the schema's \`instruction\`
|
||||
- Show brief progress: "✓ Created <artifact-id>"
|
||||
|
||||
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
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>"
|
||||
\`\`\`
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run \`/opsx:apply\` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the \`instruction\` field from \`openspec instructions\` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use the \`template\` as a starting point, filling in based on context
|
||||
|
||||
**Guardrails**
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's \`apply.requires\`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-archive-change skill
|
||||
* For archiving completed changes in the experimental workflow
|
||||
*/
|
||||
export function getArchiveChangeSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-archive-change',
|
||||
description: 'Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.',
|
||||
instructions: `Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run \`openspec status --change "<name>" --json\` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- \`schemaName\`: The workflow being used
|
||||
- \`artifacts\`: List of artifacts with their status (\`done\` or other)
|
||||
|
||||
**If any artifacts are not \`done\`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically \`tasks.md\`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with \`- [ ]\` (incomplete) vs \`- [x]\` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Check if delta specs need syncing**
|
||||
|
||||
Check if \`specs/\` directory exists in the change with spec files.
|
||||
|
||||
**If delta specs exist, perform a quick sync check:**
|
||||
|
||||
a. **For each delta spec** at \`openspec/changes/<name>/specs/<capability>/spec.md\`:
|
||||
- Extract requirement names (lines matching \`### Requirement: <name>\`)
|
||||
- Note which sections exist (ADDED, MODIFIED, REMOVED)
|
||||
|
||||
b. **Check corresponding main spec** at \`openspec/specs/<capability>/spec.md\`:
|
||||
- If main spec doesn't exist → needs sync
|
||||
- If main spec exists, check if ADDED requirement names appear in it
|
||||
- If any ADDED requirements are missing from main spec → needs sync
|
||||
|
||||
c. **Report findings:**
|
||||
|
||||
**If sync needed:**
|
||||
\`\`\`
|
||||
⚠️ Delta specs may not be synced:
|
||||
- specs/auth/spec.md → Main spec missing requirement "Token Refresh"
|
||||
- specs/api/spec.md → Main spec doesn't exist yet
|
||||
|
||||
Would you like to sync now before archiving?
|
||||
\`\`\`
|
||||
- Use **AskUserQuestion tool** with options: "Sync now", "Archive without syncing"
|
||||
- If user chooses sync, execute /opsx:sync logic (use the openspec-sync-specs skill)
|
||||
|
||||
**If already synced (all requirements found):**
|
||||
- Proceed without prompting (specs appear to be in sync)
|
||||
|
||||
**If no delta specs exist:** Proceed without sync-related checks.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
\`\`\`bash
|
||||
mkdir -p openspec/changes/archive
|
||||
\`\`\`
|
||||
|
||||
Generate target name using current date: \`YYYY-MM-DD-<change-name>\`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
\`\`\`bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
\`\`\`
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
\`\`\`
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "⚠️ Not synced")
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||
- Quick sync check: look for requirement names in delta specs, verify they exist in main specs`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:sync slash command
|
||||
*/
|
||||
export function getOpsxSyncCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: Sync',
|
||||
description: 'Sync delta specs from a change to main specs',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'specs', 'experimental'],
|
||||
content: `Sync delta specs from a change to main specs.
|
||||
|
||||
This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
|
||||
|
||||
**Input**: Optionally specify \`--change <name>\` after \`/opsx:sync\`. If omitted, MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show changes that have delta specs (under \`specs/\` directory).
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Find delta specs**
|
||||
|
||||
Look for delta spec files in \`openspec/changes/<name>/specs/*/spec.md\`.
|
||||
|
||||
Each delta spec file contains sections like:
|
||||
- \`## ADDED Requirements\` - New requirements to add
|
||||
- \`## MODIFIED Requirements\` - Changes to existing requirements
|
||||
- \`## REMOVED Requirements\` - Requirements to remove
|
||||
- \`## RENAMED Requirements\` - Requirements to rename (FROM:/TO: format)
|
||||
|
||||
If no delta specs found, inform user and stop.
|
||||
|
||||
3. **For each delta spec, apply changes to main specs**
|
||||
|
||||
For each capability with a delta spec at \`openspec/changes/<name>/specs/<capability>/spec.md\`:
|
||||
|
||||
a. **Read the delta spec** to understand the intended changes
|
||||
|
||||
b. **Read the main spec** at \`openspec/specs/<capability>/spec.md\` (may not exist yet)
|
||||
|
||||
c. **Apply changes intelligently**:
|
||||
|
||||
**ADDED Requirements:**
|
||||
- If requirement doesn't exist in main spec → add it
|
||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||
|
||||
**MODIFIED Requirements:**
|
||||
- Find the requirement in main spec
|
||||
- Apply the changes - this can be:
|
||||
- Adding new scenarios (don't need to copy existing ones)
|
||||
- Modifying existing scenarios
|
||||
- Changing the requirement description
|
||||
- Preserve scenarios/content not mentioned in the delta
|
||||
|
||||
**REMOVED Requirements:**
|
||||
- Remove the entire requirement block from main spec
|
||||
|
||||
**RENAMED Requirements:**
|
||||
- Find the FROM requirement, rename to TO
|
||||
|
||||
d. **Create new main spec** if capability doesn't exist yet:
|
||||
- Create \`openspec/specs/<capability>/spec.md\`
|
||||
- Add Purpose section (can be brief, mark as TBD)
|
||||
- Add Requirements section with the ADDED requirements
|
||||
|
||||
4. **Show summary**
|
||||
|
||||
After applying all changes, summarize:
|
||||
- Which capabilities were updated
|
||||
- What changes were made (requirements added/modified/removed/renamed)
|
||||
|
||||
**Delta Spec Format Reference**
|
||||
|
||||
\`\`\`markdown
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: New Feature
|
||||
The system SHALL do something new.
|
||||
|
||||
#### Scenario: Basic case
|
||||
- **WHEN** user does X
|
||||
- **THEN** system does Y
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Existing Feature
|
||||
#### Scenario: New scenario to add
|
||||
- **WHEN** user does A
|
||||
- **THEN** system does B
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Deprecated Feature
|
||||
|
||||
## RENAMED Requirements
|
||||
|
||||
- FROM: \`### Requirement: Old Name\`
|
||||
- TO: \`### Requirement: New Name\`
|
||||
\`\`\`
|
||||
|
||||
**Key Principle: Intelligent Merging**
|
||||
|
||||
Unlike programmatic merging, you can apply **partial updates**:
|
||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||
- The delta represents *intent*, not a wholesale replacement
|
||||
- Use your judgment to merge changes sensibly
|
||||
|
||||
**Output On Success**
|
||||
|
||||
\`\`\`
|
||||
## Specs Synced: <change-name>
|
||||
|
||||
Updated main specs:
|
||||
|
||||
**<capability-1>**:
|
||||
- Added requirement: "New Feature"
|
||||
- Modified requirement: "Existing Feature" (added 1 scenario)
|
||||
|
||||
**<capability-2>**:
|
||||
- Created new spec file
|
||||
- Added requirement: "Another Feature"
|
||||
|
||||
Main specs are now updated. The change remains active - archive when implementation is complete.
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Read both delta and main specs before making changes
|
||||
- Preserve existing content not mentioned in delta
|
||||
- If something is unclear, ask for clarification
|
||||
- Show what you're changing as you go
|
||||
- The operation should be idempotent - running twice should give same result`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:archive slash command
|
||||
*/
|
||||
export function getOpsxArchiveCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: Archive',
|
||||
description: 'Archive a completed change in the experimental workflow',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'archive', 'experimental'],
|
||||
content: `Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify \`--change <name>\` after \`/opsx:archive\`. If omitted, MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run \`openspec list --json\` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run \`openspec status --change "<name>" --json\` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- \`schemaName\`: The workflow being used
|
||||
- \`artifacts\`: List of artifacts with their status (\`done\` or other)
|
||||
|
||||
**If any artifacts are not \`done\`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically \`tasks.md\`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with \`- [ ]\` (incomplete) vs \`- [x]\` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Check if delta specs need syncing**
|
||||
|
||||
Check if \`specs/\` directory exists in the change with spec files.
|
||||
|
||||
**If delta specs exist, perform a quick sync check:**
|
||||
|
||||
a. **For each delta spec** at \`openspec/changes/<name>/specs/<capability>/spec.md\`:
|
||||
- Extract requirement names (lines matching \`### Requirement: <name>\`)
|
||||
- Note which sections exist (ADDED, MODIFIED, REMOVED)
|
||||
|
||||
b. **Check corresponding main spec** at \`openspec/specs/<capability>/spec.md\`:
|
||||
- If main spec doesn't exist → needs sync
|
||||
- If main spec exists, check if ADDED requirement names appear in it
|
||||
- If any ADDED requirements are missing from main spec → needs sync
|
||||
|
||||
c. **Report findings:**
|
||||
|
||||
**If sync needed:**
|
||||
\`\`\`
|
||||
⚠️ Delta specs may not be synced:
|
||||
- specs/auth/spec.md → Main spec missing requirement "Token Refresh"
|
||||
- specs/api/spec.md → Main spec doesn't exist yet
|
||||
|
||||
Would you like to sync now before archiving?
|
||||
\`\`\`
|
||||
- Use **AskUserQuestion tool** with options: "Sync now", "Archive without syncing"
|
||||
- If user chooses sync, execute \`/opsx:sync\` logic
|
||||
|
||||
**If already synced (all requirements found):**
|
||||
- Proceed without prompting (specs appear to be in sync)
|
||||
|
||||
**If no delta specs exist:** Proceed without sync-related checks.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
\`\`\`bash
|
||||
mkdir -p openspec/changes/archive
|
||||
\`\`\`
|
||||
|
||||
Generate target name using current date: \`YYYY-MM-DD-<change-name>\`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
\`\`\`bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
\`\`\`
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / not synced / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
\`\`\`
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
\`\`\`
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
\`\`\`
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
\`\`\`
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
\`\`\`
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ⚠️ Not synced
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta specs were not synced (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
\`\`\`
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
\`\`\`
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Quick sync check: look for requirement names in delta specs, verify they exist in main specs
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use /opsx:sync approach (agent-driven)`
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user