mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-12 05:00:36 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acdce5b2ba | ||
|
|
4f47bd2f37 | ||
|
|
9eb22410a9 | ||
|
|
f8747203a7 | ||
|
|
17016d5eaa | ||
|
|
a91adbdbd7 | ||
|
|
ac8f6d281c | ||
|
|
44ca42fbe1 | ||
|
|
39645d9284 |
@@ -1,926 +0,0 @@
|
||||
# OpenSpec Experimental Release Plan
|
||||
|
||||
This document outlines the plan to release the experimental artifact workflow system for user testing.
|
||||
|
||||
## Overview
|
||||
|
||||
The goal is to allow users to test the new artifact-driven workflow system alongside the existing OpenSpec commands. This experimental system (`opsx`) provides a more granular, step-by-step approach to creating change artifacts.
|
||||
|
||||
## Three Workflow Modes
|
||||
|
||||
### 1. Old Workflow (Current Production)
|
||||
- **Commands**: `/openspec:proposal`, `/openspec:apply`, `/openspec:archive`
|
||||
- **Behavior**: Hardcoded slash commands that generate all artifacts in one command
|
||||
- **Status**: Production, unchanged
|
||||
|
||||
### 2. New Artifact System - Batch Mode (Future)
|
||||
- **Commands**: Refactored `/openspec:proposal` using schemas
|
||||
- **Behavior**: Schema-driven but generates all artifacts at once (like legacy)
|
||||
- **Status**: Not in scope for this experimental release
|
||||
- **Note**: This is a future refactor to unify the old system with schemas
|
||||
|
||||
### 3. New Artifact System - Granular Mode (Experimental)
|
||||
- **Commands**: `/opsx:new`, `/opsx:continue`
|
||||
- **Behavior**: One artifact at a time, dependency-driven, iterative
|
||||
- **Status**: Target for this experimental release
|
||||
|
||||
---
|
||||
|
||||
## Work Items
|
||||
|
||||
### 1. Rename AWF to OPSX
|
||||
|
||||
**Current State:**
|
||||
- Commands: `/awf:start`, `/awf:continue`
|
||||
- Files: `.claude/commands/awf/start.md`, `.claude/commands/awf/continue.md`
|
||||
|
||||
**Target State:**
|
||||
- Commands: `/opsx:new`, `/opsx:continue`
|
||||
- Files: `.claude/commands/opsx/new.md`, `.claude/commands/opsx/continue.md`
|
||||
|
||||
**Tasks:**
|
||||
- [x] Create `.claude/commands/opsx/` directory
|
||||
- [x] Rename `start.md` → `new.md` and update content
|
||||
- [x] Copy `continue.md` with updated references
|
||||
- [x] Update all references from "awf" to "opsx" in command content
|
||||
- [x] Update frontmatter (name, description) to use "opsx" naming
|
||||
- [x] Remove `.claude/commands/awf/` directory
|
||||
|
||||
**CLI Commands:**
|
||||
The underlying CLI commands (`openspec status`, `openspec instructions`, etc.) remain unchanged. Only the slash command names change.
|
||||
|
||||
---
|
||||
|
||||
### 2. Remove WF Skill Files
|
||||
|
||||
**Current State:**
|
||||
- `.claude/commands/wf/start.md` - References non-existent `openspec wf` commands
|
||||
- `.claude/commands/wf/continue.md` - References non-existent `openspec wf` commands
|
||||
|
||||
**Target State:**
|
||||
- Directory and files removed
|
||||
|
||||
**Tasks:**
|
||||
- [x] Delete `.claude/commands/wf/start.md`
|
||||
- [x] Delete `.claude/commands/wf/continue.md`
|
||||
- [x] Delete `.claude/commands/wf/` directory
|
||||
|
||||
---
|
||||
|
||||
### 3. Add Agent Skills for Experimental Workflow
|
||||
|
||||
**Purpose:**
|
||||
Generate experimental workflow skills using the [Agent Skills](https://agentskills.io/specification) open standard.
|
||||
|
||||
**Why Skills Instead of Slash Commands:**
|
||||
- **Cross-editor compatibility**: Skills work in Claude Code, Cursor, Windsurf, and other compatible editors automatically
|
||||
- **Simpler implementation**: Single directory (`.claude/skills/`) instead of 18+ editor-specific configurators
|
||||
- **Standard format**: Open standard with simple YAML frontmatter + markdown
|
||||
- **User invocation**: Users explicitly invoke skills when they want to use them
|
||||
|
||||
**Behavior:**
|
||||
1. Create `.claude/skills/` directory if it doesn't exist
|
||||
2. Generate two skills using the Agent Skills specification:
|
||||
- `openspec-new-change/SKILL.md` - Start a new change with artifact workflow
|
||||
- `openspec-continue-change/SKILL.md` - Continue working on a change (create next artifact)
|
||||
3. Skills are added **alongside** existing `/openspec:*` commands (not replacing)
|
||||
|
||||
**Supported Editors:**
|
||||
- Claude Code (native support)
|
||||
- Cursor (native support via Settings → Rules → Import Settings)
|
||||
- Windsurf (imports `.claude` configs)
|
||||
- Cline, Codex, and other Agent Skills-compatible editors
|
||||
|
||||
**Tasks:**
|
||||
- [x] Create skill template content for `openspec-new-change` (based on current opsx:new)
|
||||
- [x] Create skill template content for `openspec-continue-change` (based on current opsx:continue)
|
||||
- [x] Add temporary `artifact-experimental-setup` command to CLI
|
||||
- [x] Implement skill file generation (YAML frontmatter + markdown body)
|
||||
- [x] Add success message with usage instructions
|
||||
|
||||
**Note:** The `artifact-experimental-setup` command is temporary and will be merged into `openspec init` once the experimental workflow is promoted to stable.
|
||||
|
||||
**Skill Format:**
|
||||
Each skill is a directory with a `SKILL.md` file:
|
||||
```
|
||||
.claude/skills/
|
||||
├── openspec-new-change/
|
||||
│ └── SKILL.md # name, description, instructions
|
||||
├── openspec-continue-change/
|
||||
│ └── SKILL.md # name, description, instructions
|
||||
└── openspec-apply-change/
|
||||
└── SKILL.md # name, description, instructions
|
||||
```
|
||||
|
||||
**CLI Interface:**
|
||||
```bash
|
||||
openspec artifact-experimental-setup
|
||||
|
||||
# Output:
|
||||
# 🧪 Experimental Artifact Workflow Skills Created
|
||||
#
|
||||
# ✓ .claude/skills/openspec-new-change/SKILL.md
|
||||
# ✓ .claude/skills/openspec-continue-change/SKILL.md
|
||||
# ✓ .claude/skills/openspec-apply-change/SKILL.md
|
||||
#
|
||||
# 📖 Usage:
|
||||
#
|
||||
# Skills work automatically in compatible editors:
|
||||
# • Claude Code - Auto-detected, ready to use
|
||||
# • Cursor - Enable in Settings → Rules → Import Settings
|
||||
# • Windsurf - Auto-imports from .claude directory
|
||||
#
|
||||
# Ask Claude naturally:
|
||||
# • "I want to start a new OpenSpec change to add <feature>"
|
||||
# • "Continue working on this change"
|
||||
#
|
||||
# Claude will automatically use the appropriate skill.
|
||||
#
|
||||
# 💡 This is an experimental feature.
|
||||
# Feedback welcome at: https://github.com/Fission-AI/OpenSpec/issues
|
||||
```
|
||||
|
||||
**Implementation Notes:**
|
||||
- Simple file writing: Create directories and write templated `SKILL.md` files (no complex logic)
|
||||
- Use existing `FileSystemUtils.writeFile()` pattern like slash command configurators
|
||||
- Template structure: YAML frontmatter + markdown body
|
||||
- Keep existing `/opsx:*` slash commands for now (manual cleanup later)
|
||||
- Skills use invocation model (user explicitly asks Claude to use them)
|
||||
- Skill `description` field guides when Claude suggests using the skill
|
||||
- Each `SKILL.md` has required fields: `name` (matches directory) and `description`
|
||||
|
||||
---
|
||||
|
||||
### 4. Update `/opsx:new` Command Content
|
||||
|
||||
**Current Behavior (awf:start):**
|
||||
1. Ask user what they want to build (if no input)
|
||||
2. Create change directory
|
||||
3. Show artifact status
|
||||
4. Show what's ready
|
||||
5. Get instructions for proposal
|
||||
6. STOP and wait
|
||||
|
||||
**New Behavior (opsx:new):**
|
||||
Same flow but with updated naming:
|
||||
- References to "awf" → "opsx"
|
||||
- References to `/awf:continue` → `/opsx:continue`
|
||||
- Update frontmatter name/description
|
||||
|
||||
**Tasks:**
|
||||
- [x] Update all "awf" references to "opsx"
|
||||
- [x] Update command references in prompt text
|
||||
- [x] Verify CLI commands still work (they use `openspec`, not `awf`)
|
||||
|
||||
---
|
||||
|
||||
### 5. Update `/opsx:continue` Command Content
|
||||
|
||||
**Current Behavior (awf:continue):**
|
||||
1. Prompt for change selection (if not provided)
|
||||
2. Check current status
|
||||
3. Create ONE artifact based on what's ready
|
||||
4. Show progress and what's unlocked
|
||||
5. STOP
|
||||
|
||||
**New Behavior (opsx:continue):**
|
||||
Same flow with updated naming.
|
||||
|
||||
**Tasks:**
|
||||
- [x] Update all "awf" references to "opsx"
|
||||
- [x] Update command references in prompt text
|
||||
|
||||
---
|
||||
|
||||
### 6. End-to-End Testing
|
||||
|
||||
**Objective:**
|
||||
Run through a complete workflow with Claude using the new skills to create a real feature, validating the entire flow works.
|
||||
|
||||
**Test Scenario:**
|
||||
Use a real OpenSpec feature as the test case (dog-fooding).
|
||||
|
||||
**Test Flow:**
|
||||
1. Run `openspec artifact-experimental-setup` to create skills
|
||||
2. Verify `.claude/skills/openspec-new-change/SKILL.md` created
|
||||
3. Verify `.claude/skills/openspec-continue-change/SKILL.md` created
|
||||
4. Verify `.claude/skills/openspec-apply-change/SKILL.md` created
|
||||
5. Ask Claude: "I want to start a new OpenSpec change to add feature X"
|
||||
6. Verify Claude invokes the `openspec-new-change` skill
|
||||
7. Verify change directory created at `openspec/changes/add-feature-x/`
|
||||
8. Verify proposal template shown
|
||||
9. Ask Claude: "Continue working on this change"
|
||||
10. Verify Claude invokes the `openspec-continue-change` skill
|
||||
11. Verify `proposal.md` created with content
|
||||
12. Ask Claude: "Continue" (create specs)
|
||||
13. Verify `specs/*.md` created
|
||||
14. Ask Claude: "Continue" (create design)
|
||||
15. Verify `design.md` created
|
||||
16. Ask Claude: "Continue" (create tasks)
|
||||
17. Verify `tasks.md` created
|
||||
18. Verify status shows 4/4 complete
|
||||
19. Implement the feature based on tasks
|
||||
20. Run `/openspec:archive` to archive the change
|
||||
|
||||
**Validation Checklist:**
|
||||
- [ ] `openspec artifact-experimental-setup` creates correct directory structure
|
||||
- [ ] Skills are auto-detected in Claude Code
|
||||
- [ ] Skill descriptions trigger appropriate invocations
|
||||
- [ ] Skills create change directory and show proposal template
|
||||
- [ ] Skills correctly identify ready artifacts
|
||||
- [ ] Skills create artifacts with meaningful content
|
||||
- [ ] Dependency detection works (specs requires proposal, etc.)
|
||||
- [ ] Progress tracking is accurate
|
||||
- [ ] Template content is useful and well-structured
|
||||
- [ ] Error handling works (invalid names, missing changes, etc.)
|
||||
- [ ] Works with different schemas (spec-driven, tdd)
|
||||
- [ ] Test in Cursor (Settings → Rules → Import Settings)
|
||||
|
||||
**Document Results:**
|
||||
- Create test log documenting what worked and what didn't
|
||||
- Note any friction points or confusing UX
|
||||
- Identify bugs or improvements needed before user release
|
||||
|
||||
---
|
||||
|
||||
### 7. Documentation for Users
|
||||
|
||||
**Create user-facing documentation explaining:**
|
||||
|
||||
1. **What is the experimental workflow?**
|
||||
- A new way to create OpenSpec changes step-by-step using Agent Skills
|
||||
- One artifact at a time with dependency tracking
|
||||
- More interactive and iterative than the batch approach
|
||||
- Works across Claude Code, Cursor, Windsurf, and other compatible editors
|
||||
|
||||
2. **How to set up experimental workflow**
|
||||
```bash
|
||||
openspec artifact-experimental-setup
|
||||
```
|
||||
|
||||
Note: This is a temporary command that will be integrated into `openspec init` once promoted to stable.
|
||||
|
||||
3. **Available skills**
|
||||
- `openspec-new-change` - Start a new change with artifact workflow
|
||||
- `openspec-continue-change` - Continue working (create next artifact)
|
||||
|
||||
4. **How to use**
|
||||
- **Claude Code**: Skills are auto-detected, just ask Claude naturally
|
||||
- "I want to start a new OpenSpec change to add X"
|
||||
- "Continue working on this change"
|
||||
- **Cursor**: Enable in Settings → Rules → Import Settings
|
||||
- **Windsurf**: Auto-imports `.claude` directory
|
||||
|
||||
5. **Example workflow**
|
||||
- Step-by-step walkthrough with natural language interactions
|
||||
- Show how Claude invokes skills based on user requests
|
||||
|
||||
6. **Feedback mechanism**
|
||||
- GitHub issue template for feedback
|
||||
- What to report (bugs, UX issues, suggestions)
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Create `docs/experimental-workflow.md` user guide
|
||||
- [ ] Add GitHub issue template for experimental feedback
|
||||
- [ ] Update README with mention of experimental features
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
1. Remove WF skill files
|
||||
└── (no dependencies)
|
||||
|
||||
2. Rename AWF to OPSX
|
||||
└── (no dependencies)
|
||||
|
||||
3. Add Agent Skills
|
||||
└── Depends on: Rename AWF to OPSX (uses opsx content as templates)
|
||||
|
||||
4. Update opsx:new content
|
||||
└── Depends on: Rename AWF to OPSX
|
||||
|
||||
5. Update opsx:continue content
|
||||
└── Depends on: Rename AWF to OPSX
|
||||
|
||||
6. E2E Testing
|
||||
└── Depends on: Add Agent Skills (tests the skills workflow)
|
||||
|
||||
7. User Documentation
|
||||
└── Depends on: E2E Testing (need to know final behavior)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly NOT part of this experimental release:
|
||||
|
||||
1. **Batch mode refactor** - Making legacy `/openspec:proposal` use schemas
|
||||
2. **New schemas** - Only shipping with existing `spec-driven` and `tdd`
|
||||
3. **Schema customization UI** - No `openspec schema list` or similar
|
||||
4. **Multiple editor support in CLI** - Skills work cross-editor automatically via `.claude/skills/`
|
||||
5. **Replacing existing commands** - Skills are additive, not replacing `/openspec:*` or `/opsx:*`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
The experimental release is ready when:
|
||||
|
||||
1. `openspec-new-change`, `openspec-continue-change`, and `openspec-apply-change` skills work end-to-end
|
||||
2. `openspec artifact-experimental-setup` creates skills in `.claude/skills/`
|
||||
3. Skills work in Claude Code and are compatible with Cursor/Windsurf
|
||||
4. At least one complete workflow has been tested manually
|
||||
5. User documentation exists explaining how to generate and use skills
|
||||
6. Feedback mechanism is in place
|
||||
7. WF skill files are removed
|
||||
8. No references to "awf" remain in user-facing content
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Schema selection** - Should `opsx:new` allow selecting a schema, or always use `spec-driven`?
|
||||
- Current: Always uses `spec-driven` as default
|
||||
- Consider: Add `--schema tdd` option or prompt
|
||||
|
||||
2. **Namespace in CLI** - Should experimental CLI commands be namespaced?
|
||||
- Current: `openspec status`, `openspec instructions` (no namespace)
|
||||
- Alternative: `openspec opsx status` (explicit experimental namespace)
|
||||
- Recommendation: Keep current, less typing for users
|
||||
|
||||
3. **Deprecation path** - If opsx becomes the default, how do we migrate?
|
||||
- Not needed for experimental release
|
||||
- Document that command names may change
|
||||
|
||||
---
|
||||
|
||||
## Estimated Work Breakdown
|
||||
|
||||
| Item | Complexity | Notes |
|
||||
|------|------------|-------|
|
||||
| Remove WF files | Trivial | Just delete 2 files + directory |
|
||||
| Rename AWF → OPSX | Low | File renames + content updates |
|
||||
| Add Agent Skills | **Low** | **Simple: 3-4 files, single output directory, standard format** |
|
||||
| Update opsx:new content | Low | Text replacements |
|
||||
| Update opsx:continue content | Low | Text replacements |
|
||||
| E2E Testing | Medium | Manual testing, documenting results |
|
||||
| User Documentation | Medium | New docs, issue template |
|
||||
|
||||
**Key Improvement:** Switching to Agent Skills reduces complexity significantly:
|
||||
- **Before:** 20+ files (type definitions, 18+ editor configurators, editor selection UI)
|
||||
- **After:** 3-4 files (skill templates, simple CLI command)
|
||||
- **Cross-editor:** Works automatically in Claude Code, Cursor, Windsurf without extra code
|
||||
|
||||
---
|
||||
|
||||
## User Feedback from E2E Testing
|
||||
|
||||
### What Worked Well
|
||||
|
||||
1. **Clear dependency graph** ⭐ HIGH PRIORITY - KEEP
|
||||
- The status command showing blocked/unblocked artifacts was intuitive:
|
||||
```
|
||||
[x] proposal
|
||||
[ ] design
|
||||
[-] tasks (blocked by: design, specs)
|
||||
```
|
||||
- Users always knew what they could work on next
|
||||
- **Relevance**: Core UX strength to preserve
|
||||
|
||||
2. **Structured instructions output** ⭐ HIGH PRIORITY - KEEP
|
||||
- `openspec instructions <artifact>` gave templates, output paths, and context in one call
|
||||
- Very helpful for understanding what to create
|
||||
- **Relevance**: Essential for agent-driven workflow
|
||||
|
||||
3. **Simple scaffolding** ✅ WORKS WELL
|
||||
- `openspec new change "name"` just worked - created directory structure without fuss
|
||||
- **Relevance**: Good baseline, room for improvement (see pain points)
|
||||
|
||||
---
|
||||
|
||||
### Pain Points & Confusion
|
||||
|
||||
1. **Redundant CLI calls** ⚠️ MEDIUM PRIORITY
|
||||
- Users called both `status` AND `next` every time, but they overlap significantly
|
||||
- `status` already shows what's blocked
|
||||
- **Recommendation**: Consider merging or making `next` give actionable guidance beyond just listing names
|
||||
- **Relevance**: Reduces friction in iterative workflow
|
||||
|
||||
2. **Specs directory structure was ambiguous** 🔥 HIGH PRIORITY - FIX
|
||||
- Instructions said: `Write to: .../specs/**/*.md`
|
||||
- Users had to guess: `specs/spec.md`? `specs/game/spec.md`? `specs/tic-tac-toe/spec.md`?
|
||||
- Users ended up doing manual `mkdir -p .../specs/tic-tac-toe` then writing `spec.md` inside
|
||||
- **Recommendation**: CLI should scaffold this directory structure automatically
|
||||
- **Relevance**: Critical agent UX - ambiguous paths cause workflow friction
|
||||
|
||||
3. **Repetitive --change flag** ⚠️ MEDIUM PRIORITY
|
||||
- Every command needed `--change "tic-tac-toe-game"`
|
||||
- After 10+ calls, this felt verbose
|
||||
- **Recommendation**: `openspec use "tic-tac-toe-game"` to set context, then subsequent commands assume that change
|
||||
- **Relevance**: Quality of life improvement for iterative sessions
|
||||
|
||||
4. **No validation feedback** 🔥 HIGH PRIORITY - ADD
|
||||
- After writing each artifact, users just ran `status` hoping it would show `[x]`
|
||||
- Questions raised:
|
||||
- How did it know the artifact was "done"? File existence?
|
||||
- What if spec format was wrong (e.g., wrong heading levels)?
|
||||
- **Recommendation**: Add `openspec validate --change "name"` to check content quality
|
||||
- **Relevance**: Critical for user confidence and catching errors early
|
||||
|
||||
5. **Query-heavy, action-light CLI** 🔥 HIGH PRIORITY - ENHANCE
|
||||
- Most commands retrieve info. The only "action" is `new change`
|
||||
- Artifact creation is manual Write to guessed paths
|
||||
- **Recommendation**: `openspec create proposal --change "name"` could scaffold the file with template pre-filled, then user just edits
|
||||
- **Relevance**: Directly impacts agent productivity - reduce manual file writing
|
||||
|
||||
6. **Instructions output was verbose** ⚠️ LOW PRIORITY
|
||||
- XML-style output (`<artifact>`, `<template>`, `<instruction>`) was parseable but long
|
||||
- Key info (output path, template) was buried in ~50 lines
|
||||
- **Recommendation**: Add compact mode or structured JSON output for agents
|
||||
- **Relevance**: Nice-to-have for agent parsing efficiency
|
||||
|
||||
---
|
||||
|
||||
### Workflow Friction
|
||||
|
||||
1. **Mandatory "STOP and wait" after showing proposal template** ⚠️ MEDIUM PRIORITY
|
||||
- The skill said "STOP and wait" after showing the proposal template
|
||||
- This felt overly cautious when user had already provided enough context (e.g., "tic tac toe, single player vs AI, minimal aesthetics")
|
||||
- **Recommendation**: Make the pause optional or conditional based on context clarity
|
||||
- **Relevance**: Reduces unnecessary round-trips in agent conversations
|
||||
|
||||
2. **No connection to implementation** 🔥 HIGH PRIORITY - ROADMAP ITEM
|
||||
- After 4/4 artifacts complete, then what? The workflow ends at planning
|
||||
- No `openspec apply` or guidance on how to execute the tasks
|
||||
- User asked "would you like me to implement?" but that's outside OpenSpec's scope currently
|
||||
- **Recommendation**: Add implementation bridge - either:
|
||||
- `openspec apply` command to start execution phase
|
||||
- Clear handoff to existing `/openspec:apply` workflow
|
||||
- Documentation on next steps after planning completes
|
||||
- **Relevance**: Critical missing piece - users expect end-to-end workflow
|
||||
|
||||
---
|
||||
|
||||
### Priority Summary
|
||||
|
||||
**MUST FIX (High Priority):**
|
||||
1. Specs directory structure ambiguity (#2)
|
||||
2. Add validation feedback (#4)
|
||||
3. Make CLI more action-oriented (#5)
|
||||
4. Bridge to implementation phase (#2 in Workflow Friction)
|
||||
5. Keep clear dependency graph (#1 in What Worked)
|
||||
6. Keep structured instructions (#2 in What Worked)
|
||||
|
||||
**SHOULD FIX (Medium Priority):**
|
||||
1. Reduce redundant CLI calls (#1)
|
||||
2. Repetitive `--change` flag (#3)
|
||||
3. Mandatory STOP behavior (#1 in Workflow Friction)
|
||||
|
||||
**NICE TO HAVE (Low Priority):**
|
||||
1. Compact instructions output mode (#6)
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions (from E2E Testing Feedback)
|
||||
|
||||
Based on dev testing and analysis of agent workflow friction, we identified three blockers for experimental release and made the following decisions.
|
||||
|
||||
### Blockers Identified
|
||||
|
||||
From the pain points in E2E testing, three issues are blocking the experimental release:
|
||||
|
||||
1. **Specs directory ambiguity** - Agents don't know where to write spec files or how to name capabilities
|
||||
2. **CLI is query-heavy** - Most commands retrieve info, artifact creation is manual
|
||||
3. **Apply integration missing** - After 4/4 artifacts complete, no guidance on implementation phase
|
||||
|
||||
### Decision 1: Capability Discovery in Proposal (RESOLVED)
|
||||
|
||||
**Problem:** The specs artifact instruction says "Create one spec file per capability in `specs/<name>/spec.md`" but:
|
||||
- Agent doesn't know what `<name>` should be
|
||||
- Capability identification requires research (existing specs, codebase)
|
||||
- Proposal template asks for "Affected specs" but doesn't structure it
|
||||
- Research happens implicitly, output isn't captured
|
||||
|
||||
**Decision:** Enrich the proposal template to explicitly capture capability discovery.
|
||||
|
||||
**Current proposal template:**
|
||||
```markdown
|
||||
## Why
|
||||
## What Changes
|
||||
## Impact
|
||||
- Affected specs: List capabilities... ← vague, easy to skip
|
||||
- Affected code: ...
|
||||
```
|
||||
|
||||
**New proposal template:**
|
||||
```markdown
|
||||
## Why
|
||||
## What Changes
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- Capabilities being introduced (will create new specs/<name>/spec.md) -->
|
||||
- `<name>`: <brief description of what this capability covers>
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- Existing capabilities being changed (will update existing specs) -->
|
||||
- `<existing-name>`: <what's changing>
|
||||
|
||||
## Impact
|
||||
<!-- Affected code, APIs, dependencies, systems -->
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Proposal already asks for capabilities (just poorly) - this makes it explicit
|
||||
- Captured output is reviewable (vs implicit research that can't be verified)
|
||||
- Creates clear contract between proposal and specs phases
|
||||
- Distinguishes NEW vs MODIFIED upfront (critical for specs phase)
|
||||
- Agent can't skip research - it's part of the deliverable
|
||||
|
||||
**Implementation:**
|
||||
- Update `schemas/spec-driven/templates/proposal.md`
|
||||
- Update proposal instruction in `schemas/spec-driven/schema.yaml`
|
||||
- Update skill instructions to guide capability discovery
|
||||
|
||||
### Decision 2: CLI Action Commands (IN PROGRESS)
|
||||
|
||||
**Problem:** CLI is mostly query-oriented. Agents run `openspec status`, `openspec next`, `openspec instructions` but then must manually write files.
|
||||
|
||||
#### Decision 2a: Remove `openspec next` command (RESOLVED)
|
||||
|
||||
**Problem:** The `next` command is redundant. It only shows which artifacts are ready, but `status` already shows this information (artifacts with status "ready" vs "blocked" vs "done").
|
||||
|
||||
**Current behavior:**
|
||||
```bash
|
||||
openspec status --change "X" # Shows: proposal (done), specs (ready), design (blocked), tasks (blocked)
|
||||
openspec next --change "X" # Shows: ["specs"] ← redundant
|
||||
```
|
||||
|
||||
**Decision:** Remove the `next` command. Agents should use `status` which provides the same info plus more context.
|
||||
|
||||
**Implementation:**
|
||||
- Remove `next` command from CLI
|
||||
- Update skill instructions to use `status` instead of `next`
|
||||
- Update AGENTS.md references
|
||||
|
||||
#### Decision 2b: CLI Scaffolding (RESOLVED - NO)
|
||||
|
||||
**Problem:** After getting instructions, agents manually write files. Should CLI scaffold artifacts instead?
|
||||
|
||||
**Options considered:**
|
||||
- Add `openspec create <artifact>` commands that scaffold files with templates
|
||||
- Keep current approach where agent writes files directly from instructions
|
||||
- Hybrid: CLI can scaffold, agent can also write directly
|
||||
|
||||
**Decision:** Keep current flow. No scaffolding commands.
|
||||
|
||||
**Rationale (from agent ergonomics perspective):**
|
||||
- One Write is better than multiple Edits - agent composes full content atomically
|
||||
- `instructions` already provides template in context - scaffolding just moves it to a file
|
||||
- Fewer tool calls: `instructions` + Write (2) vs `create` + `instructions` + Read + Edit×N (4+)
|
||||
- Scaffolding doesn't solve the real problem (not knowing WHAT to write)
|
||||
- Real problem solved by proposal template change (capability discovery)
|
||||
|
||||
**For multi-file artifacts (specs):** Scaffolding can't help because CLI doesn't know capability names until proposal is complete. The capability discovery in proposal solves this.
|
||||
|
||||
### Decision 3: Apply Integration (RESOLVED)
|
||||
|
||||
**Original problem:** After planning completes (4/4 artifacts), the experimental workflow ends. No guidance on implementation.
|
||||
|
||||
**Key insight: No phases, just actions.**
|
||||
|
||||
Through discussion, we realized phases (planning → implementation → archive) are an artificial constraint. Work is fluid:
|
||||
- You might start implementing, realize the design is wrong → update design.md
|
||||
- You're halfway through tasks, discover a new requirement → update specs
|
||||
- You bounce between "planning" and "implementing" constantly
|
||||
|
||||
**The better model: Actions on a Change**
|
||||
|
||||
A change is a thing (with artifacts). Actions are verbs you perform on a change. Actions aren't phases - they're fluid operations you can perform anytime.
|
||||
|
||||
| Action | What it does | Skill | CLI Command |
|
||||
|--------|--------------|-------|-------------|
|
||||
| `new` | Create a change (scaffold directory) | `opsx:new` | `openspec new change` |
|
||||
| `continue` | Create next artifact (dependency-aware) | `opsx:continue` | `openspec instructions` |
|
||||
| `apply` | Implement tasks (execute, check off) | `opsx:apply` (NEW) | TBD |
|
||||
| `update` | Refresh/update artifacts based on learnings | `opsx:update` (NEW) | TBD |
|
||||
| `explore` | Research, ask questions, understand | `opsx:explore` (NEW) | TBD |
|
||||
| `validate` | Check artifacts are correct/complete | TBD | `openspec validate` |
|
||||
| `archive` | Finalize and move to archive | existing | `openspec archive` |
|
||||
|
||||
**Key principles:**
|
||||
- Actions are modeled as skills (primary interface for agents)
|
||||
- Some skills have matching CLI commands for convenience
|
||||
- Skills and CLI commands are decoupled - not everything needs both
|
||||
- Actions can be performed in any order (with soft prerequisites)
|
||||
- No linear phase gates
|
||||
|
||||
**What the schema defines:**
|
||||
- Artifacts (what they are, where they go)
|
||||
- Dependencies (what must exist first)
|
||||
- Required vs optional
|
||||
- Templates + instructions
|
||||
|
||||
**What the schema does NOT define:**
|
||||
- Phases
|
||||
- When you can modify things
|
||||
- Linear workflow
|
||||
|
||||
**Progress tracking:**
|
||||
- tasks.md checkboxes = implementation progress
|
||||
- Artifact existence = planning progress
|
||||
- Archive readiness = user decides (or all tasks done)
|
||||
|
||||
**For experimental release:**
|
||||
- Create `opsx:apply` skill (guidance for implementing tasks)
|
||||
- Document the "actions on a change" model
|
||||
- Other actions (update, explore) can come later
|
||||
|
||||
---
|
||||
|
||||
### Design: `openspec-apply-change` Skill
|
||||
|
||||
#### Overview
|
||||
|
||||
The apply skill guides agents through implementing tasks from a completed (or in-progress) change. Unlike the old `/openspec:apply` command, this skill:
|
||||
- Is **fluid** - can be invoked anytime, not just after all artifacts are done
|
||||
- Allows **artifact updates** - if implementation reveals issues, update design/specs
|
||||
- Works **until done** - keeps going through tasks until complete or blocked
|
||||
- Tracks **progress via checkboxes** - tasks.md is the source of truth
|
||||
|
||||
#### Skill Metadata
|
||||
|
||||
```yaml
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||
```
|
||||
|
||||
#### When to Invoke
|
||||
|
||||
The skill should be invoked when:
|
||||
- User says "implement this change" or "start implementing"
|
||||
- User says "work on the tasks" or "do the next task"
|
||||
- User says "apply this change"
|
||||
- All artifacts are complete and user wants to proceed
|
||||
- User wants to continue implementation after a break
|
||||
|
||||
#### Input
|
||||
|
||||
- Optionally: change name
|
||||
- Optionally: specific task number to work on
|
||||
- If omitted: prompt for change selection (same pattern as continue-change)
|
||||
|
||||
#### Steps
|
||||
|
||||
```markdown
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use **AskUserQuestion** to let user select.
|
||||
|
||||
Show changes that have tasks.md (implementation-ready).
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
2. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- Context file paths (proposal, specs, design, tasks)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If blocked (missing artifacts): show message, suggest `openspec-continue-change`
|
||||
- If all done: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
3. **Read context files**
|
||||
|
||||
Read the files listed in the instructions:
|
||||
- `proposal.md` - why and what
|
||||
- `specs/*.md` - requirements and scenarios
|
||||
- `design.md` - technical approach (if exists)
|
||||
- `tasks.md` - the implementation checklist
|
||||
|
||||
4. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
5. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in tasks.md: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
6. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
```
|
||||
|
||||
#### Output Format
|
||||
|
||||
**During implementation:**
|
||||
```
|
||||
## Implementing: add-user-auth
|
||||
|
||||
Working on task 3/7: Create UserAuth service class
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: Add login endpoint to AuthController
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 5/7: Add JWT token generation
|
||||
[...implementation happening...]
|
||||
```
|
||||
|
||||
**On completion:**
|
||||
```
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** add-user-auth
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Create UserAuth service class
|
||||
- [x] Add login endpoint to AuthController
|
||||
- [x] Add JWT token generation
|
||||
- [x] Add logout endpoint
|
||||
- [x] Add auth middleware
|
||||
- [x] Write unit tests
|
||||
- [x] Update API documentation
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
```
|
||||
|
||||
**On pause (issue encountered):**
|
||||
```
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** add-user-auth
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
Task 5 "Add JWT token generation" - the design specifies using RS256 but
|
||||
the existing auth library only supports HS256.
|
||||
|
||||
**Options:**
|
||||
1. Update design.md to use HS256 instead
|
||||
2. Add a new JWT library that supports RS256
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
#### Guardrails
|
||||
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context before starting (specs, design)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
|
||||
#### Fluid Workflow Integration
|
||||
|
||||
The apply skill supports the "actions on a change" model:
|
||||
|
||||
**Can be invoked anytime:**
|
||||
- Before all artifacts are done (if tasks.md exists)
|
||||
- After partial implementation
|
||||
- Interleaved with other actions (update, continue)
|
||||
|
||||
**Allows artifact updates:**
|
||||
- If implementation reveals design issues → suggest `opsx:update` or manual edit
|
||||
- If requirements need clarification → suggest updating specs
|
||||
- Not phase-locked - work fluidly
|
||||
|
||||
**Example fluid workflow:**
|
||||
```
|
||||
User: "Implement add-user-auth"
|
||||
→ openspec-apply-change: implements tasks 1, 2, 3, 4...
|
||||
→ Pauses at task 5: "Design says RS256 but library only supports HS256"
|
||||
|
||||
User: "Let's use HS256 instead, update the design"
|
||||
→ User edits design.md (or uses opsx:update in future)
|
||||
|
||||
User: "Continue implementing"
|
||||
→ openspec-apply-change: implements tasks 5, 6, 7
|
||||
→ "All tasks complete! Ready to archive."
|
||||
```
|
||||
|
||||
#### CLI Commands Used
|
||||
|
||||
```bash
|
||||
openspec list --json # List changes for selection
|
||||
openspec status --change "<name>" # Check artifact completion
|
||||
openspec instructions apply --change "<name>" # Get apply instructions (NEW)
|
||||
# File reads via Read tool for proposal, specs, design, tasks
|
||||
# File edits via Edit tool for checking off tasks
|
||||
```
|
||||
|
||||
#### New CLI Command: `openspec instructions apply`
|
||||
|
||||
For consistency with artifact instructions.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" [--json]
|
||||
```
|
||||
|
||||
**Output (Markdown format):**
|
||||
```markdown
|
||||
## Apply: add-user-auth
|
||||
|
||||
### Context Files
|
||||
- proposal: openspec/changes/add-user-auth/proposal.md
|
||||
- specs: openspec/changes/add-user-auth/specs/**/*.md
|
||||
- design: openspec/changes/add-user-auth/design.md
|
||||
- tasks: openspec/changes/add-user-auth/tasks.md
|
||||
|
||||
### Progress
|
||||
2/7 complete
|
||||
|
||||
### Tasks
|
||||
- [x] Create UserAuth service class
|
||||
- [x] Add login endpoint
|
||||
- [ ] Add JWT token generation
|
||||
- [ ] Add logout endpoint
|
||||
- [ ] Add auth middleware
|
||||
- [ ] Write unit tests
|
||||
- [ ] Update API documentation
|
||||
|
||||
### Instruction
|
||||
Read context files, work through pending tasks, mark complete as you go.
|
||||
Pause if you hit blockers or need clarification.
|
||||
```
|
||||
|
||||
**Benefits of CLI command:**
|
||||
- **Consistency** - same pattern as `openspec instructions <artifact>`
|
||||
- **Structured output** - progress, tasks, context paths in one call
|
||||
- **Clean format** - markdown is readable and compact (vs verbose XML)
|
||||
- **Extensibility** - can add more sections later if needed
|
||||
- **JSON option** - `--json` flag available for programmatic use
|
||||
|
||||
#### Differences from Old `/openspec:apply`
|
||||
|
||||
| Aspect | Old `/openspec:apply` | New `openspec-apply-change` |
|
||||
|--------|----------------------|----------------------------|
|
||||
| Invocation | After all artifacts done | Anytime (if tasks.md exists) |
|
||||
| Granularity | All tasks at once | All tasks, but pauses on issues |
|
||||
| Artifact updates | Not mentioned | Encouraged when needed |
|
||||
| Progress tracking | Update all at end | Update after each task |
|
||||
| Flow control | Push through everything | Pause on blockers, resume after |
|
||||
| Context loading | Read once at start | Read context, reference as needed |
|
||||
| Issue handling | Not specified | Pause, present options, wait for guidance |
|
||||
|
||||
#### Implementation Notes
|
||||
|
||||
1. **Add CLI command**: Add `openspec instructions apply` to artifact-workflow.ts
|
||||
- Parse tasks.md for progress (count done/pending)
|
||||
- Return context paths, progress, task list, simple instruction
|
||||
2. **Add to skill-templates.ts**: Create `getApplyChangeSkillTemplate()` function
|
||||
3. **Update artifact-experimental-setup**: Generate this skill alongside new/continue
|
||||
4. **Update skills list**: Add to `.claude/skills/` directory
|
||||
5. **Test the flow**: Verify it works with existing changes that have tasks.md
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ~~Review this plan and confirm scope~~ (Done - blockers identified)
|
||||
2. ~~Design decisions~~ (Done - all 3 blockers resolved)
|
||||
3. ~~Design apply skill~~ (Done - documented above)
|
||||
4. ~~Implement proposal template change (Decision 1 - capability discovery)~~ (Done)
|
||||
5. ~~Remove `openspec next` command (Decision 2a)~~ (Done)
|
||||
6. ~~Add `openspec instructions apply` CLI command~~ (Done)
|
||||
7. ~~Create `openspec-apply-change` skill~~ (Done)
|
||||
8. Conduct E2E testing with updated workflow
|
||||
9. Write user docs (document "actions on a change" model)
|
||||
10. Release to test users
|
||||
@@ -1,211 +0,0 @@
|
||||
# Schema Customization
|
||||
|
||||
This document describes how users can customize OpenSpec schemas and templates, the current manual process, and the gap that needs to be addressed.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
OpenSpec uses a 2-level schema resolution system following the XDG Base Directory Specification:
|
||||
|
||||
1. **User override**: `${XDG_DATA_HOME}/openspec/schemas/<name>/`
|
||||
2. **Package built-in**: `<npm-package>/schemas/<name>/`
|
||||
|
||||
When a schema is requested (e.g., `spec-driven`), the resolver checks the user directory first. If found, that entire schema directory is used. Otherwise, it falls back to the package's built-in schema.
|
||||
|
||||
---
|
||||
|
||||
## Current Manual Process
|
||||
|
||||
To override the default `spec-driven` schema, a user must:
|
||||
|
||||
### 1. Determine the correct directory path
|
||||
|
||||
| Platform | Path |
|
||||
|----------|------|
|
||||
| macOS/Linux | `~/.local/share/openspec/schemas/` |
|
||||
| Windows | `%LOCALAPPDATA%\openspec\schemas\` |
|
||||
| All (if set) | `$XDG_DATA_HOME/openspec/schemas/` |
|
||||
|
||||
### 2. Create the directory structure
|
||||
|
||||
```bash
|
||||
# macOS/Linux example
|
||||
mkdir -p ~/.local/share/openspec/schemas/spec-driven/templates
|
||||
```
|
||||
|
||||
### 3. Find and copy the default schema files
|
||||
|
||||
The user must locate the installed npm package to copy the defaults:
|
||||
|
||||
```bash
|
||||
# Find the package location (varies by install method)
|
||||
npm list -g openspec --parseable
|
||||
# or
|
||||
which openspec && readlink -f $(which openspec)
|
||||
|
||||
# Copy files from the package's schemas/ directory
|
||||
cp <package-path>/schemas/spec-driven/schema.yaml ~/.local/share/openspec/schemas/spec-driven/
|
||||
cp <package-path>/schemas/spec-driven/templates/*.md ~/.local/share/openspec/schemas/spec-driven/templates/
|
||||
```
|
||||
|
||||
### 4. Modify the copied files
|
||||
|
||||
Edit `schema.yaml` to change the workflow structure:
|
||||
|
||||
```yaml
|
||||
name: spec-driven
|
||||
version: 1
|
||||
description: My custom workflow
|
||||
artifacts:
|
||||
- id: proposal
|
||||
generates: proposal.md
|
||||
description: Initial proposal
|
||||
template: proposal.md
|
||||
requires: []
|
||||
# Add, remove, or modify artifacts...
|
||||
```
|
||||
|
||||
Edit templates in `templates/` to customize the content guidance.
|
||||
|
||||
### 5. Verify the override is active
|
||||
|
||||
Currently there's no command to verify which schema is being used. Users must trust that the file exists in the right location.
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
The current process has several friction points:
|
||||
|
||||
| Issue | Impact |
|
||||
|-------|--------|
|
||||
| **Path discovery** | Users must know XDG conventions and platform-specific paths |
|
||||
| **Package location** | Finding the npm package path varies by install method (global, local, pnpm, yarn, volta, etc.) |
|
||||
| **No scaffolding** | Users must manually create directories and copy files |
|
||||
| **No verification** | No way to confirm which schema is actually being resolved |
|
||||
| **No diffing** | When upgrading openspec, users can't see what changed in built-in templates |
|
||||
| **Full copy required** | Must copy entire schema even to change one template |
|
||||
|
||||
### User Stories Not Currently Supported
|
||||
|
||||
1. *"I want to add a `research` artifact before `proposal`"* — requires manual copy and edit
|
||||
2. *"I want to customize just the proposal template"* — must copy entire schema
|
||||
3. *"I want to see what the default schema looks like"* — must find package path
|
||||
4. *"I want to revert to defaults"* — must delete files and hope paths are correct
|
||||
5. *"I upgraded openspec, did the templates change?"* — no way to diff
|
||||
|
||||
---
|
||||
|
||||
## Proposed Solution: Schema Configurator
|
||||
|
||||
A CLI command (or set of commands) that handles path resolution and file operations for users.
|
||||
|
||||
### Option A: Single `openspec schema` command
|
||||
|
||||
```bash
|
||||
# List available schemas (built-in and user overrides)
|
||||
openspec schema list
|
||||
|
||||
# Show where a schema resolves from
|
||||
openspec schema which spec-driven
|
||||
# Output: /Users/me/.local/share/openspec/schemas/spec-driven/ (user override)
|
||||
# Output: /usr/local/lib/node_modules/openspec/schemas/spec-driven/ (built-in)
|
||||
|
||||
# Copy a built-in schema to user directory for customization
|
||||
openspec schema copy spec-driven
|
||||
# Creates ~/.local/share/openspec/schemas/spec-driven/ with all files
|
||||
|
||||
# Show diff between user override and built-in
|
||||
openspec schema diff spec-driven
|
||||
|
||||
# Remove user override (revert to built-in)
|
||||
openspec schema reset spec-driven
|
||||
|
||||
# Validate a schema
|
||||
openspec schema validate spec-driven
|
||||
```
|
||||
|
||||
### Option B: Dedicated `openspec customize` command
|
||||
|
||||
```bash
|
||||
# Interactive schema customization
|
||||
openspec customize
|
||||
# Prompts: Which schema? What do you want to change? etc.
|
||||
|
||||
# Copy and open for editing
|
||||
openspec customize spec-driven
|
||||
# Copies to user dir, prints path, optionally opens in $EDITOR
|
||||
```
|
||||
|
||||
### Option C: Init-time schema selection
|
||||
|
||||
```bash
|
||||
# During project init, offer schema customization
|
||||
openspec init
|
||||
# ? Select a workflow schema:
|
||||
# > spec-driven (default)
|
||||
# tdd
|
||||
# minimal
|
||||
# custom (copy and edit)
|
||||
```
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
**Option A** provides the most flexibility and follows Unix conventions (subcommands for discrete operations). Key commands in priority order:
|
||||
|
||||
1. `openspec schema list` — see what's available
|
||||
2. `openspec schema which <name>` — debug resolution
|
||||
3. `openspec schema copy <name>` — scaffold customization
|
||||
4. `openspec schema diff <name>` — compare with built-in
|
||||
5. `openspec schema reset <name>` — revert to defaults
|
||||
|
||||
---
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Path Resolution
|
||||
|
||||
The resolver already exists in `src/core/artifact-graph/resolver.ts`:
|
||||
|
||||
```typescript
|
||||
export function getPackageSchemasDir(): string { ... }
|
||||
export function getUserSchemasDir(): string { ... }
|
||||
export function getSchemaDir(name: string): string | null { ... }
|
||||
export function listSchemas(): string[] { ... }
|
||||
```
|
||||
|
||||
New commands would leverage these existing functions.
|
||||
|
||||
### File Operations
|
||||
|
||||
- Copy should preserve file permissions
|
||||
- Copy should not overwrite existing user files without `--force`
|
||||
- Reset should prompt for confirmation
|
||||
|
||||
### Template-Only Overrides
|
||||
|
||||
A future enhancement could support overriding individual templates without copying the entire schema. This would require changes to the resolution logic:
|
||||
|
||||
```
|
||||
Current: schema dir (user) OR schema dir (built-in)
|
||||
Future: schema.yaml from user OR built-in
|
||||
+ each template from user OR built-in (independent fallback)
|
||||
```
|
||||
|
||||
This adds complexity but enables the "I just want to change one template" use case.
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Schema Workflow Gaps](./schema-workflow-gaps.md) — End-to-end workflow analysis and phased implementation plan
|
||||
|
||||
## Related Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/core/artifact-graph/resolver.ts` | Schema resolution logic |
|
||||
| `src/core/artifact-graph/instruction-loader.ts` | Template loading |
|
||||
| `src/core/global-config.ts` | XDG path helpers |
|
||||
| `schemas/spec-driven/` | Default schema and templates |
|
||||
@@ -1,378 +0,0 @@
|
||||
# Schema Workflow: End-to-End Analysis
|
||||
|
||||
This document analyzes the complete user journey for working with schemas in OpenSpec, identifies gaps, and proposes a phased solution.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
### What Exists
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Schema resolution (XDG) | 2-level: user override → package built-in |
|
||||
| Built-in schemas | `spec-driven`, `tdd` |
|
||||
| Artifact workflow commands | `status`, `next`, `instructions`, `templates` with `--schema` flag |
|
||||
| Change creation | `openspec new change <name>` — no schema binding |
|
||||
|
||||
### What's Missing
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Schema bound to change | Not stored — must pass `--schema` every time |
|
||||
| Project-local schemas | Not supported — can't version control with repo |
|
||||
| Schema management CLI | None — manual path discovery required |
|
||||
| Project default schema | None — hardcoded to `spec-driven` |
|
||||
|
||||
---
|
||||
|
||||
## User Journey Analysis
|
||||
|
||||
### Scenario 1: Using a Non-Default Schema
|
||||
|
||||
**Goal:** User wants to use TDD workflow for a new feature.
|
||||
|
||||
**Today's experience:**
|
||||
```bash
|
||||
openspec new change add-auth
|
||||
# Creates directory, no schema info stored
|
||||
|
||||
openspec status --change add-auth
|
||||
# Shows spec-driven artifacts (WRONG - user wanted TDD)
|
||||
|
||||
# User realizes mistake...
|
||||
openspec status --change add-auth --schema tdd
|
||||
# Correct, but must remember --schema every time
|
||||
|
||||
# 6 months later...
|
||||
openspec status --change add-auth
|
||||
# Wrong again - nobody remembers this was TDD
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Schema is a runtime argument, not persisted
|
||||
- Easy to forget `--schema` and get wrong results
|
||||
- No record of intended schema for future reference
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Customizing a Schema
|
||||
|
||||
**Goal:** User wants to add a "research" artifact before "proposal".
|
||||
|
||||
**Today's experience:**
|
||||
```bash
|
||||
# Step 1: Figure out where to put overrides
|
||||
# Must know XDG conventions:
|
||||
# macOS/Linux: ~/.local/share/openspec/schemas/
|
||||
# Windows: %LOCALAPPDATA%\openspec\schemas/
|
||||
|
||||
# Step 2: Create directory structure
|
||||
mkdir -p ~/.local/share/openspec/schemas/my-workflow/templates
|
||||
|
||||
# Step 3: Find the npm package to copy defaults
|
||||
npm list -g openspec --parseable
|
||||
# Output varies by package manager:
|
||||
# npm: /usr/local/lib/node_modules/openspec
|
||||
# pnpm: ~/.local/share/pnpm/global/5/node_modules/openspec
|
||||
# volta: ~/.volta/tools/image/packages/openspec/...
|
||||
# yarn: ~/.config/yarn/global/node_modules/openspec
|
||||
|
||||
# Step 4: Copy files
|
||||
cp -r <package-path>/schemas/spec-driven/* \
|
||||
~/.local/share/openspec/schemas/my-workflow/
|
||||
|
||||
# Step 5: Edit schema.yaml and templates
|
||||
# No way to verify override is active
|
||||
# No way to diff against original
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Must know XDG path conventions
|
||||
- Finding npm package path varies by install method
|
||||
- No tooling to scaffold or verify
|
||||
- No diff capability when upgrading openspec
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Team Sharing Custom Workflow
|
||||
|
||||
**Goal:** Team wants everyone to use the same custom schema.
|
||||
|
||||
**Today's options:**
|
||||
1. Everyone manually sets up XDG override — error-prone, drift risk
|
||||
2. Document setup in README — still manual, easy to miss
|
||||
3. Publish separate npm package — overkill for most teams
|
||||
4. Check schema into repo — **not supported** (no project-local resolution)
|
||||
|
||||
**Problems:**
|
||||
- No project-local schema resolution
|
||||
- Can't version control custom schemas with the codebase
|
||||
- No single source of truth for team workflow
|
||||
|
||||
---
|
||||
|
||||
## Gap Summary
|
||||
|
||||
| Gap | Impact | Workaround |
|
||||
|-----|--------|------------|
|
||||
| Schema not bound to change | Wrong results, forgotten context | Remember to pass `--schema` |
|
||||
| No project-local schemas | Can't share via repo | Manual XDG setup per machine |
|
||||
| No schema management CLI | Manual path hunting | Know XDG + find npm package |
|
||||
| No project default schema | Must specify every time | Always pass `--schema` |
|
||||
| No init-time schema selection | Missed setup opportunity | Manual config |
|
||||
|
||||
---
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
### New File Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── config.yaml # Project config (NEW)
|
||||
├── schemas/ # Project-local schemas (NEW)
|
||||
│ └── my-workflow/
|
||||
│ ├── schema.yaml
|
||||
│ └── templates/
|
||||
│ ├── research.md
|
||||
│ ├── proposal.md
|
||||
│ └── ...
|
||||
└── changes/
|
||||
└── add-auth/
|
||||
├── change.yaml # Change metadata (NEW)
|
||||
├── proposal.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
### config.yaml (Project Config)
|
||||
|
||||
```yaml
|
||||
# openspec/config.yaml
|
||||
defaultSchema: spec-driven
|
||||
```
|
||||
|
||||
Sets the project-wide default schema. Used when:
|
||||
- Creating new changes without `--schema`
|
||||
- Running commands on changes without `change.yaml`
|
||||
|
||||
### change.yaml (Change Metadata)
|
||||
|
||||
```yaml
|
||||
# openspec/changes/add-auth/change.yaml
|
||||
schema: tdd
|
||||
created: 2025-01-15T10:30:00Z
|
||||
description: Add user authentication system
|
||||
```
|
||||
|
||||
Binds a specific schema to a change. Created automatically by `openspec new change`.
|
||||
|
||||
### Schema Resolution Order
|
||||
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User global (XDG)
|
||||
3. <npm-package>/schemas/<name>/ # Built-in
|
||||
```
|
||||
|
||||
Project-local takes priority, enabling version-controlled custom schemas.
|
||||
|
||||
### Schema Selection Order (Per Command)
|
||||
|
||||
```
|
||||
1. --schema CLI flag # Explicit override
|
||||
2. change.yaml in change directory # Change-specific binding
|
||||
3. openspec/config.yaml defaultSchema # Project default
|
||||
4. "spec-driven" # Hardcoded fallback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ideal User Experience
|
||||
|
||||
### Creating a Change
|
||||
|
||||
```bash
|
||||
# Uses project default (from config.yaml, or spec-driven)
|
||||
openspec new change add-auth
|
||||
# Creates openspec/changes/add-auth/change.yaml:
|
||||
# schema: spec-driven
|
||||
# created: 2025-01-15T10:30:00Z
|
||||
|
||||
# Explicit schema for this change
|
||||
openspec new change add-auth --schema tdd
|
||||
# Creates change.yaml with schema: tdd
|
||||
```
|
||||
|
||||
### Working with Changes
|
||||
|
||||
```bash
|
||||
# Auto-reads schema from change.yaml — no --schema needed
|
||||
openspec status --change add-auth
|
||||
# Output: "Change: add-auth (schema: tdd)"
|
||||
# Shows which artifacts are ready/blocked/done
|
||||
|
||||
# Explicit override still works (with informational message)
|
||||
openspec status --change add-auth --schema spec-driven
|
||||
# "Note: change.yaml specifies 'tdd', using 'spec-driven' per --schema flag"
|
||||
```
|
||||
|
||||
### Customizing Schemas
|
||||
|
||||
```bash
|
||||
# See what's available
|
||||
openspec schema list
|
||||
# Built-in:
|
||||
# spec-driven proposal → specs → design → tasks
|
||||
# tdd spec → tests → implementation → docs
|
||||
# Project: (none)
|
||||
# User: (none)
|
||||
|
||||
# Copy to project for customization
|
||||
openspec schema copy spec-driven my-workflow
|
||||
# Created ./openspec/schemas/my-workflow/
|
||||
# Edit schema.yaml and templates/ to customize
|
||||
|
||||
# Copy to global (user-level override)
|
||||
openspec schema copy spec-driven --global
|
||||
# Created ~/.local/share/openspec/schemas/spec-driven/
|
||||
|
||||
# See where a schema resolves from
|
||||
openspec schema which spec-driven
|
||||
# ./openspec/schemas/spec-driven/ (project)
|
||||
# or: ~/.local/share/openspec/schemas/spec-driven/ (user)
|
||||
# or: /usr/local/lib/node_modules/openspec/schemas/spec-driven/ (built-in)
|
||||
|
||||
# Compare override with built-in
|
||||
openspec schema diff spec-driven
|
||||
# Shows diff between user/project version and package built-in
|
||||
|
||||
# Remove override, revert to built-in
|
||||
openspec schema reset spec-driven
|
||||
# Removes ./openspec/schemas/spec-driven/ (or --global for user dir)
|
||||
```
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
openspec init
|
||||
# ? Select default workflow schema:
|
||||
# > spec-driven (proposal → specs → design → tasks)
|
||||
# tdd (spec → tests → implementation → docs)
|
||||
# (custom schemas if detected)
|
||||
#
|
||||
# Writes to openspec/config.yaml:
|
||||
# defaultSchema: spec-driven
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Change Metadata (change.yaml)
|
||||
|
||||
**Priority:** High
|
||||
**Solves:** "Forgot --schema", lost context, wrong results
|
||||
|
||||
**Scope:**
|
||||
- Create `change.yaml` when running `openspec new change`
|
||||
- Store `schema`, `created` timestamp
|
||||
- Modify workflow commands to read schema from `change.yaml`
|
||||
- `--schema` flag overrides (with informational message)
|
||||
- Backwards compatible: missing `change.yaml` → use default
|
||||
|
||||
**change.yaml format:**
|
||||
```yaml
|
||||
schema: tdd
|
||||
created: 2025-01-15T10:30:00Z
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
- Existing changes without `change.yaml` continue to work
|
||||
- Default to `spec-driven` (current behavior)
|
||||
- Optional: `openspec migrate` to add `change.yaml` to existing changes
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Project-Local Schemas
|
||||
|
||||
**Priority:** High
|
||||
**Solves:** Team sharing, version control, no XDG knowledge needed
|
||||
|
||||
**Scope:**
|
||||
- Add `./openspec/schemas/` to resolution order (first priority)
|
||||
- `openspec schema copy <name> [new-name]` creates in project by default
|
||||
- `--global` flag for user-level XDG directory
|
||||
- Teams can commit `openspec/schemas/` to repo
|
||||
|
||||
**Resolution order:**
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local (NEW)
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User global
|
||||
3. <npm-package>/schemas/<name>/ # Built-in
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Schema Management CLI
|
||||
|
||||
**Priority:** Medium
|
||||
**Solves:** Path discovery, scaffolding, debugging
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
openspec schema list # Show available schemas with sources
|
||||
openspec schema which <name> # Show resolution path
|
||||
openspec schema copy <name> [to] # Copy for customization
|
||||
openspec schema diff <name> # Compare with built-in
|
||||
openspec schema reset <name> # Remove override
|
||||
openspec schema validate <name> # Validate schema.yaml structure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Project Config + Init Enhancement
|
||||
|
||||
**Priority:** Low
|
||||
**Solves:** Project-wide defaults, streamlined setup
|
||||
|
||||
**Scope:**
|
||||
- Add `openspec/config.yaml` with `defaultSchema` field
|
||||
- `openspec init` prompts for schema selection
|
||||
- Store selection in `config.yaml`
|
||||
- Commands use as fallback when no `change.yaml` exists
|
||||
|
||||
**config.yaml format:**
|
||||
```yaml
|
||||
defaultSchema: spec-driven
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Existing change without `change.yaml` | Uses `--schema` flag or project default or `spec-driven` |
|
||||
| Existing project without `config.yaml` | Falls back to `spec-driven` |
|
||||
| `--schema` flag provided | Overrides `change.yaml` (with info message) |
|
||||
| No project-local schemas dir | Skipped in resolution, checks user/built-in |
|
||||
|
||||
All existing functionality continues to work. New features are additive.
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Schema Customization](./schema-customization.md) — Details on manual override process and CLI gaps
|
||||
- [Artifact POC](./artifact_poc.md) — Core artifact graph architecture
|
||||
|
||||
## Related Code
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/core/artifact-graph/resolver.ts` | Schema resolution logic |
|
||||
| `src/core/artifact-graph/instruction-loader.ts` | Template loading |
|
||||
| `src/core/global-config.ts` | XDG path helpers |
|
||||
| `src/commands/artifact-workflow.ts` | CLI commands |
|
||||
| `src/utils/change-utils.ts` | Change creation utilities |
|
||||
@@ -1,26 +0,0 @@
|
||||
## Why
|
||||
|
||||
With per-change schema metadata in place (see `add-per-change-schema-metadata`), agents can now create changes with different workflow schemas. However, the agent skills are still hardcoded to `spec-driven` artifacts and don't offer schema selection to users.
|
||||
|
||||
## What Changes
|
||||
|
||||
**Scope: Experimental artifact workflow agent skills**
|
||||
|
||||
**Depends on:** `add-per-change-schema-metadata` (must be implemented first)
|
||||
|
||||
- Update `openspec-new-change` skill to prompt user for schema selection
|
||||
- Update `openspec-continue-change` skill to work with any schema's artifacts
|
||||
- Update `openspec-apply-change` skill to handle schema-specific task structures
|
||||
- Add schema descriptions to help users choose appropriate workflow
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `cli-artifact-workflow`: Agent skills support dynamic schema selection
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected code**: `src/core/templates/skill-templates.ts`
|
||||
- **User experience**: Users can choose TDD, spec-driven, or future workflows when starting a change
|
||||
- **Agent behavior**: Skills read artifact list from schema rather than hardcoding
|
||||
- **Backward compatible**: Default remains `spec-driven` if user doesn't choose
|
||||
@@ -1,32 +0,0 @@
|
||||
## Prerequisites
|
||||
|
||||
- [x] 0.1 Implement `add-per-change-schema-metadata` change first
|
||||
|
||||
## 1. Schema Discovery
|
||||
|
||||
- [x] 1.1 Add CLI command or helper to list schemas with descriptions (for agent use)
|
||||
- [x] 1.2 Ensure `openspec templates --schema <name>` returns artifact list for any schema
|
||||
|
||||
## 2. Update New Change Skill
|
||||
|
||||
- [x] 2.1 Add schema selection prompt using AskUserQuestion tool
|
||||
- [x] 2.2 Present available schemas with descriptions (spec-driven, tdd, etc.)
|
||||
- [x] 2.3 Pass selected schema to `openspec new change --schema <name>`
|
||||
- [x] 2.4 Update output to show which schema/workflow was selected
|
||||
|
||||
## 3. Update Continue Change Skill
|
||||
|
||||
- [x] 3.1 Remove hardcoded artifact references (proposal, specs, design, tasks)
|
||||
- [x] 3.2 Read artifact list dynamically from `openspec status --json`
|
||||
- [x] 3.3 Adjust artifact creation guidelines to be schema-agnostic
|
||||
- [x] 3.4 Handle schema-specific artifact types (e.g., TDD's `tests` artifact)
|
||||
|
||||
## 4. Update Apply Change Skill
|
||||
|
||||
- [x] 4.1 Make task detection work with different schema structures
|
||||
- [x] 4.2 Adjust context file reading for schema-specific artifacts
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [x] 5.1 Add schema descriptions to help text or skill instructions
|
||||
- [x] 5.2 Document when to use each schema (TDD for bug fixes, spec-driven for features, etc.)
|
||||
@@ -0,0 +1,9 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### Scenario: Generating slash commands for Antigravity
|
||||
- **WHEN** the user selects Antigravity during initialization
|
||||
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
|
||||
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
|
||||
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
|
||||
@@ -0,0 +1,8 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
|
||||
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
|
||||
@@ -1,147 +0,0 @@
|
||||
## Context
|
||||
|
||||
The experimental artifact workflow supports multiple schemas (`spec-driven`, `tdd`), but schema selection must be passed on every command. This creates friction for agents and users.
|
||||
|
||||
We need a lightweight metadata file to persist the schema choice per change.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Store schema choice once at change creation
|
||||
- Auto-detect schema in experimental workflow commands
|
||||
- Maintain backward compatibility (no metadata = default)
|
||||
- Validate metadata with Zod schema
|
||||
|
||||
**Non-Goals:**
|
||||
- Migrate existing changes (they use default)
|
||||
- Extend to legacy commands
|
||||
- Store additional metadata beyond schema (keep minimal for now)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Zod Schema Design
|
||||
|
||||
The metadata file (`.openspec.yaml`) will be validated with this Zod schema:
|
||||
|
||||
```typescript
|
||||
// src/core/artifact-graph/types.ts (or new metadata.ts)
|
||||
|
||||
import { z } from 'zod';
|
||||
import { listSchemas } from './resolver.js';
|
||||
|
||||
/**
|
||||
* Schema for per-change metadata stored in .openspec.yaml
|
||||
*/
|
||||
export const ChangeMetadataSchema = z.object({
|
||||
// Required: which workflow schema this change uses
|
||||
schema: z.string().min(1, { message: 'schema is required' }).refine(
|
||||
(val) => listSchemas().includes(val),
|
||||
(val) => ({ message: `Unknown schema '${val}'. Available: ${listSchemas().join(', ')}` })
|
||||
),
|
||||
|
||||
// Optional: creation timestamp (ISO date string)
|
||||
created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
||||
message: 'created must be YYYY-MM-DD format'
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>;
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- `schema` is required and validated against available schemas at parse time
|
||||
- `created` is optional, ISO date format for consistency
|
||||
- Minimal fields - can extend later without breaking existing files
|
||||
- Follows existing codebase pattern (see `ArtifactSchema`, `SchemaYamlSchema`)
|
||||
|
||||
### Decision: File Location and Format
|
||||
|
||||
**Location:** `openspec/changes/<name>/.openspec.yaml`
|
||||
|
||||
**Format:**
|
||||
```yaml
|
||||
schema: tdd
|
||||
created: 2025-01-05
|
||||
```
|
||||
|
||||
**Alternatives considered:**
|
||||
- `change.yaml` - less hidden, but clutters directory
|
||||
- Frontmatter in `proposal.md` - couples to proposal existence
|
||||
- `openspec.json` - YAML matches existing schema files
|
||||
|
||||
### Decision: Read/Write Functions
|
||||
|
||||
```typescript
|
||||
// src/utils/change-metadata.ts
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as yaml from 'yaml';
|
||||
import { ChangeMetadataSchema, type ChangeMetadata } from '../core/artifact-graph/types.js';
|
||||
|
||||
const METADATA_FILENAME = '.openspec.yaml';
|
||||
|
||||
export function writeChangeMetadata(
|
||||
changeDir: string,
|
||||
metadata: ChangeMetadata
|
||||
): void {
|
||||
// Validate before writing
|
||||
const validated = ChangeMetadataSchema.parse(metadata);
|
||||
const content = yaml.stringify(validated);
|
||||
fs.writeFileSync(path.join(changeDir, METADATA_FILENAME), content);
|
||||
}
|
||||
|
||||
export function readChangeMetadata(
|
||||
changeDir: string
|
||||
): ChangeMetadata | null {
|
||||
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
||||
|
||||
if (!fs.existsSync(metaPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(metaPath, 'utf-8');
|
||||
const parsed = yaml.parse(content);
|
||||
|
||||
// Validate and return (throws ZodError if invalid)
|
||||
return ChangeMetadataSchema.parse(parsed);
|
||||
}
|
||||
```
|
||||
|
||||
### Decision: Schema Resolution Order
|
||||
|
||||
When determining which schema to use:
|
||||
|
||||
1. **Explicit `--schema` flag** (highest priority - user override)
|
||||
2. **`.openspec.yaml` metadata** (persisted choice)
|
||||
3. **Default `spec-driven`** (fallback)
|
||||
|
||||
```typescript
|
||||
function resolveSchemaForChange(
|
||||
changeDir: string,
|
||||
explicitSchema?: string
|
||||
): string {
|
||||
if (explicitSchema) return explicitSchema;
|
||||
|
||||
const metadata = readChangeMetadata(changeDir);
|
||||
if (metadata?.schema) return metadata.schema;
|
||||
|
||||
return 'spec-driven';
|
||||
}
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Extra file per change** → Minimal overhead, hidden file
|
||||
- **YAML parsing dependency** → Already using `yaml` package for schema files
|
||||
- **Schema validation at read time** → Fail fast with clear error if corrupted
|
||||
|
||||
## Migration Plan
|
||||
|
||||
No migration needed:
|
||||
- Existing changes without `.openspec.yaml` continue to work (use default)
|
||||
- New changes created with `openspec new change --schema X` get metadata file
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `openspec new change` prompt for schema interactively if not specified? (Leaning no - default is fine)
|
||||
@@ -1,29 +0,0 @@
|
||||
## Why
|
||||
|
||||
Currently, the schema (workflow type) must be passed via `--schema` flag on every experimental workflow command. This is repetitive and error-prone. Agents have no way to know which schema a change uses, so they default to `spec-driven` and cannot leverage alternative workflows like `tdd`.
|
||||
|
||||
## What Changes
|
||||
|
||||
**Scope: Experimental artifact workflow only** (`openspec new change`, `openspec status`, `openspec instructions`, `openspec templates`)
|
||||
|
||||
- Store schema choice in `.openspec.yaml` metadata file when creating a change via `openspec new change`
|
||||
- Auto-detect schema from metadata in experimental workflow commands
|
||||
- Make `--schema` flag optional (override only, metadata takes precedence)
|
||||
- Add `--schema` option to `openspec new change` command
|
||||
|
||||
**Not affected**: Legacy commands (`openspec validate`, `openspec archive`, `openspec list`, `openspec show`)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `change-metadata`: Reading/writing per-change metadata files
|
||||
|
||||
### Modified Capabilities
|
||||
- `cli-artifact-workflow`: Commands auto-detect schema from change metadata
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected code**: `src/utils/change-utils.ts`, `src/core/artifact-graph/instruction-loader.ts`, `src/commands/artifact-workflow.ts`
|
||||
- **Agent skills**: Can be simplified - no longer need to pass schema explicitly
|
||||
- **Backward compatible**: Changes without `.openspec.yaml` fall back to `spec-driven` default
|
||||
- **Isolation**: All changes contained within experimental workflow code; legacy commands untouched
|
||||
@@ -1,98 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Change Metadata
|
||||
|
||||
The system SHALL store and validate per-change metadata in `.openspec.yaml` files using a Zod schema.
|
||||
|
||||
#### Scenario: Metadata file created with new change
|
||||
|
||||
- **WHEN** user runs `openspec new change add-feature --schema tdd`
|
||||
- **THEN** the system creates `.openspec.yaml` in the change directory
|
||||
- **AND** the file contains `schema: tdd` and `created: <YYYY-MM-DD>`
|
||||
|
||||
#### Scenario: Metadata validated on read
|
||||
|
||||
- **WHEN** the system reads `.openspec.yaml`
|
||||
- **AND** the `schema` field references an unknown schema
|
||||
- **THEN** the system displays a validation error listing available schemas
|
||||
|
||||
#### Scenario: Metadata schema validation
|
||||
|
||||
- **WHEN** `.openspec.yaml` contains invalid YAML or missing required fields
|
||||
- **THEN** the system displays a Zod validation error with details
|
||||
|
||||
#### Scenario: Missing metadata file
|
||||
|
||||
- **WHEN** a change directory has no `.openspec.yaml` file
|
||||
- **THEN** the system falls back to the default schema (`spec-driven`)
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: New Change Command
|
||||
|
||||
The system SHALL create new change directories with validation and optional schema metadata.
|
||||
|
||||
#### Scenario: Create valid change
|
||||
|
||||
- **WHEN** user runs `openspec new change add-feature`
|
||||
- **THEN** the system creates `openspec/changes/add-feature/` directory
|
||||
- **AND** creates `.openspec.yaml` with `schema: spec-driven` (default)
|
||||
|
||||
#### Scenario: Create change with schema
|
||||
|
||||
- **WHEN** user runs `openspec new change add-feature --schema tdd`
|
||||
- **THEN** the system creates `openspec/changes/add-feature/` directory
|
||||
- **AND** creates `.openspec.yaml` with `schema: tdd`
|
||||
|
||||
#### Scenario: Invalid schema on create
|
||||
|
||||
- **WHEN** user runs `openspec new change add-feature --schema unknown`
|
||||
- **THEN** the system displays an error listing available schemas
|
||||
- **AND** does not create the change directory
|
||||
|
||||
#### Scenario: Invalid change name
|
||||
|
||||
- **WHEN** user runs `openspec new change "Add Feature"` with invalid name
|
||||
- **THEN** the system displays validation error with guidance
|
||||
|
||||
#### Scenario: Duplicate change name
|
||||
|
||||
- **WHEN** user runs `openspec new change existing-change` for an existing change
|
||||
- **THEN** the system displays an error indicating the change already exists
|
||||
|
||||
#### Scenario: Create with description
|
||||
|
||||
- **WHEN** user runs `openspec new change add-feature --description "Add new feature"`
|
||||
- **THEN** the system creates the change directory with description in README.md
|
||||
|
||||
### Requirement: Schema Selection
|
||||
|
||||
The system SHALL support custom schema selection for workflow commands, with automatic detection from change metadata.
|
||||
|
||||
#### Scenario: Schema auto-detected from metadata
|
||||
|
||||
- **WHEN** user runs `openspec status --change <id>` without `--schema`
|
||||
- **AND** the change has `.openspec.yaml` with `schema: tdd`
|
||||
- **THEN** the system uses the `tdd` schema
|
||||
|
||||
#### Scenario: Explicit schema overrides metadata
|
||||
|
||||
- **WHEN** user runs `openspec status --change <id> --schema spec-driven`
|
||||
- **AND** the change has `.openspec.yaml` with `schema: tdd`
|
||||
- **THEN** the system uses `spec-driven` (explicit flag wins)
|
||||
|
||||
#### Scenario: Default schema fallback
|
||||
|
||||
- **WHEN** user runs workflow commands without `--schema`
|
||||
- **AND** the change has no `.openspec.yaml` file
|
||||
- **THEN** the system uses the "spec-driven" schema
|
||||
|
||||
#### Scenario: Custom schema via flag
|
||||
|
||||
- **WHEN** user runs `openspec status --change <id> --schema tdd`
|
||||
- **THEN** the system uses the specified schema for artifact graph
|
||||
|
||||
#### Scenario: Unknown schema
|
||||
|
||||
- **WHEN** user specifies an unknown schema
|
||||
- **THEN** the system displays an error listing available schemas
|
||||
@@ -1,29 +0,0 @@
|
||||
## 1. Zod Schema and Types
|
||||
|
||||
- [x] 1.1 Add `ChangeMetadataSchema` Zod schema to `src/core/artifact-graph/types.ts`
|
||||
- [x] 1.2 Export `ChangeMetadata` type inferred from schema
|
||||
|
||||
## 2. Core Metadata Functions
|
||||
|
||||
- [x] 2.1 Create `src/utils/change-metadata.ts` with `writeChangeMetadata()` function
|
||||
- [x] 2.2 Add `readChangeMetadata()` function with Zod validation
|
||||
- [x] 2.3 Update `createChange()` to accept optional `schema` param and write metadata
|
||||
|
||||
## 3. Auto-Detection in Instruction Loader
|
||||
|
||||
- [x] 3.1 Modify `loadChangeContext()` to read schema from `.openspec.yaml`
|
||||
- [x] 3.2 Make `schemaName` parameter optional (fall back to metadata, then default)
|
||||
|
||||
## 4. CLI Updates
|
||||
|
||||
- [x] 4.1 Add `--schema <name>` option to `openspec new change` command
|
||||
- [x] 4.2 Verify existing commands (`status`, `instructions`) work with auto-detection
|
||||
|
||||
## 5. Tests
|
||||
|
||||
- [x] 5.1 Test `ChangeMetadataSchema` validates correctly (valid/invalid cases)
|
||||
- [x] 5.2 Test `writeChangeMetadata()` creates valid YAML
|
||||
- [x] 5.3 Test `readChangeMetadata()` parses and validates schema
|
||||
- [x] 5.4 Test `loadChangeContext()` auto-detects schema from metadata
|
||||
- [x] 5.5 Test fallback to default when no metadata exists
|
||||
- [x] 5.6 Test `--schema` flag overrides metadata
|
||||
@@ -1,105 +0,0 @@
|
||||
# Delta for CLI Init
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### Scenario: Generating slash commands for Antigravity
|
||||
- **WHEN** the user selects Antigravity during initialization
|
||||
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
|
||||
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
|
||||
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
|
||||
|
||||
#### Scenario: Generating slash commands for Claude Code
|
||||
- **WHEN** the user selects Claude Code during initialization
|
||||
- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for CodeBuddy Code
|
||||
- **WHEN** the user selects CodeBuddy Code during initialization
|
||||
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
- **WHEN** the user selects Cline during initialization
|
||||
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Crush
|
||||
- **WHEN** the user selects Crush during initialization
|
||||
- **THEN** create `.crush/commands/openspec/proposal.md`, `.crush/commands/openspec/apply.md`, and `.crush/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cursor
|
||||
- **WHEN** the user selects Cursor during initialization
|
||||
- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-apply.md`, and `.cursor/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Factory Droid
|
||||
- **WHEN** the user selects Factory Droid during initialization
|
||||
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates that include Factory-compatible YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** include the `$ARGUMENTS` placeholder in the template body so droid receives any user-supplied input
|
||||
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
|
||||
|
||||
#### Scenario: Generating slash commands for OpenCode
|
||||
- **WHEN** the user selects OpenCode during initialization
|
||||
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Windsurf
|
||||
- **WHEN** the user selects Windsurf during initialization
|
||||
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Kilo Code
|
||||
- **WHEN** the user selects Kilo Code during initialization
|
||||
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Codex
|
||||
- **WHEN** the user selects Codex during initialization
|
||||
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
|
||||
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
|
||||
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
|
||||
|
||||
#### Scenario: Generating slash commands for GitHub Copilot
|
||||
- **WHEN** the user selects GitHub Copilot during initialization
|
||||
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
|
||||
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
|
||||
- **AND** include `$ARGUMENTS` placeholder to capture user input
|
||||
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Gemini CLI
|
||||
- **WHEN** the user selects Gemini CLI during initialization
|
||||
- **THEN** create `.gemini/commands/openspec/proposal.toml`, `.gemini/commands/openspec/apply.toml`, and `.gemini/commands/openspec/archive.toml`
|
||||
- **AND** populate each file as TOML that sets a stage-specific `description = "<summary>"` and a multi-line `prompt = """` block with the shared OpenSpec template
|
||||
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
|
||||
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
|
||||
|
||||
#### Scenario: Generating slash commands for iFlow CLI
|
||||
- **WHEN** the user selects iFlow CLI during initialization
|
||||
- **THEN** create `.iflow/commands/openspec-proposal.md`, `.iflow/commands/openspec-apply.md`, and `.iflow/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include YAML frontmatter with `name`, `id`, `category`, and `description` fields for each command
|
||||
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for RooCode
|
||||
- **WHEN** the user selects RooCode during initialization
|
||||
- **THEN** create `.roo/commands/openspec-proposal.md`, `.roo/commands/openspec-apply.md`, and `.roo/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include simple Markdown headings (e.g., `# OpenSpec: Proposal`) without YAML frontmatter
|
||||
- **AND** wrap the generated content in OpenSpec managed markers where applicable so `openspec update` can safely refresh the commands
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
@@ -1,92 +0,0 @@
|
||||
# Delta for CLI Update
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
|
||||
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
|
||||
|
||||
#### Scenario: Updating slash commands for Claude Code
|
||||
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for CodeBuddy Code
|
||||
- **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cline
|
||||
- **WHEN** `.clinerules/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Crush
|
||||
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cursor
|
||||
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Factory Droid
|
||||
- **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid
|
||||
- **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched
|
||||
- **AND** skip creating missing files during update
|
||||
|
||||
#### Scenario: Updating slash commands for OpenCode
|
||||
- **WHEN** `.opencode/command/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
|
||||
|
||||
#### Scenario: Updating slash commands for Windsurf
|
||||
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Kilo Code
|
||||
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Codex
|
||||
- **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** refresh each file using the shared slash-command templates (including placeholder guidance)
|
||||
- **AND** preserve any unmanaged content outside the OpenSpec marker block
|
||||
- **AND** skip creation when a Codex prompt file is missing
|
||||
|
||||
#### Scenario: Updating slash commands for GitHub Copilot
|
||||
- **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`
|
||||
- **THEN** refresh each file using shared templates while preserving the YAML frontmatter
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Gemini CLI
|
||||
- **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml`
|
||||
- **THEN** refresh the body of each file using the shared proposal/apply/archive templates
|
||||
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
|
||||
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
|
||||
|
||||
#### Scenario: Updating slash commands for iFlow CLI
|
||||
- **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
# Delta for CLI Init
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### Scenario: Generating slash commands for Antigravity
|
||||
- **WHEN** the user selects Antigravity during initialization
|
||||
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
|
||||
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
|
||||
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
|
||||
|
||||
#### Scenario: Generating slash commands for Claude Code
|
||||
- **WHEN** the user selects Claude Code during initialization
|
||||
- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for CodeBuddy Code
|
||||
- **WHEN** the user selects CodeBuddy Code during initialization
|
||||
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
- **WHEN** the user selects Cline during initialization
|
||||
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Crush
|
||||
- **WHEN** the user selects Crush during initialization
|
||||
- **THEN** create `.crush/commands/openspec/proposal.md`, `.crush/commands/openspec/apply.md`, and `.crush/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cursor
|
||||
- **WHEN** the user selects Cursor during initialization
|
||||
- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-apply.md`, and `.cursor/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Factory Droid
|
||||
- **WHEN** the user selects Factory Droid during initialization
|
||||
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates that include Factory-compatible YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** include the `$ARGUMENTS` placeholder in the template body so droid receives any user-supplied input
|
||||
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
|
||||
|
||||
#### Scenario: Generating slash commands for OpenCode
|
||||
- **WHEN** the user selects OpenCode during initialization
|
||||
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Windsurf
|
||||
- **WHEN** the user selects Windsurf during initialization
|
||||
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Kilo Code
|
||||
- **WHEN** the user selects Kilo Code during initialization
|
||||
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Codex
|
||||
- **WHEN** the user selects Codex during initialization
|
||||
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
|
||||
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
|
||||
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
|
||||
|
||||
#### Scenario: Generating slash commands for GitHub Copilot
|
||||
- **WHEN** the user selects GitHub Copilot during initialization
|
||||
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
|
||||
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
|
||||
- **AND** include `$ARGUMENTS` placeholder to capture user input
|
||||
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Gemini CLI
|
||||
- **WHEN** the user selects Gemini CLI during initialization
|
||||
- **THEN** create `.gemini/commands/openspec/proposal.toml`, `.gemini/commands/openspec/apply.toml`, and `.gemini/commands/openspec/archive.toml`
|
||||
- **AND** populate each file as TOML that sets a stage-specific `description = "<summary>"` and a multi-line `prompt = """` block with the shared OpenSpec template
|
||||
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
|
||||
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
|
||||
|
||||
#### Scenario: Generating slash commands for iFlow CLI
|
||||
- **WHEN** the user selects iFlow CLI during initialization
|
||||
- **THEN** create `.iflow/commands/openspec-proposal.md`, `.iflow/commands/openspec-apply.md`, and `.iflow/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include YAML frontmatter with `name`, `id`, `category`, and `description` fields for each command
|
||||
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for RooCode
|
||||
- **WHEN** the user selects RooCode during initialization
|
||||
- **THEN** create `.roo/commands/openspec-proposal.md`, `.roo/commands/openspec-apply.md`, and `.roo/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include simple Markdown headings (e.g., `# OpenSpec: Proposal`) without YAML frontmatter
|
||||
- **AND** wrap the generated content in OpenSpec managed markers where applicable so `openspec update` can safely refresh the commands
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
# Delta for CLI Update
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
|
||||
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
|
||||
|
||||
#### Scenario: Updating slash commands for Claude Code
|
||||
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for CodeBuddy Code
|
||||
- **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cline
|
||||
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Crush
|
||||
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cursor
|
||||
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Factory Droid
|
||||
- **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid
|
||||
- **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched
|
||||
- **AND** skip creating missing files during update
|
||||
|
||||
#### Scenario: Updating slash commands for OpenCode
|
||||
- **WHEN** `.opencode/command/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
|
||||
|
||||
#### Scenario: Updating slash commands for Windsurf
|
||||
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Kilo Code
|
||||
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Codex
|
||||
- **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** refresh each file using the shared slash-command templates (including placeholder guidance)
|
||||
- **AND** preserve any unmanaged content outside the OpenSpec marker block
|
||||
- **AND** skip creation when a Codex prompt file is missing
|
||||
|
||||
#### Scenario: Updating slash commands for GitHub Copilot
|
||||
- **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`
|
||||
- **THEN** refresh each file using shared templates while preserving the YAML frontmatter
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Gemini CLI
|
||||
- **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml`
|
||||
- **THEN** refresh the body of each file using the shared proposal/apply/archive templates
|
||||
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
|
||||
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
|
||||
|
||||
#### Scenario: Updating slash commands for iFlow CLI
|
||||
- **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
+1
-1
@@ -8,6 +8,6 @@ The Cline implementation was architecturally incorrect. According to Cline's off
|
||||
- **BREAKING**: Existing Cline users will need to re-run `openspec init` to get the corrected workflow files
|
||||
|
||||
## Impact
|
||||
- Affected specs: cli-init, cli-update (corrected Cline workflow paths)
|
||||
- Affected specs: cli-init (corrected Cline workflow paths)
|
||||
- Affected code: `src/core/configurators/slash/cline.ts`, test files, README.md
|
||||
- Modified files: `.clinerules/workflows/openspec-*.md` (moved from `.clinerules/openspec-*.md`)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Delta for CLI Init
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Configuration
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
- **WHEN** the user selects Cline during initialization
|
||||
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
@@ -1,138 +0,0 @@
|
||||
## Why
|
||||
|
||||
The `generateApplyInstructions` function is hardcoded to check for `spec-driven` artifacts (`proposal.md`, `specs/`, `design.md`, `tasks.md`). If a user selects a different schema like `tdd`, the apply instructions are meaningless - they check for files that don't exist in that schema.
|
||||
|
||||
This blocks the experimental workflow from supporting multiple schemas properly.
|
||||
|
||||
## What Changes
|
||||
|
||||
**Scope: Experimental artifact workflow** (`openspec instructions apply`)
|
||||
|
||||
**Depends on:** `add-per-change-schema-metadata` (to know which schema a change uses)
|
||||
|
||||
- Make `generateApplyInstructions` read artifact definitions from the schema
|
||||
- Dynamically determine which artifacts exist based on schema
|
||||
- Define when a change becomes "implementable" (see Design Decision below)
|
||||
- Generate schema-appropriate context files and instructions
|
||||
|
||||
## Design Decision: When is a change implementable?
|
||||
|
||||
This is the key question. Different approaches:
|
||||
|
||||
### Option A: Explicit `apply` artifact in schema
|
||||
|
||||
Add a field to mark which artifact is the "implementation gate":
|
||||
|
||||
```yaml
|
||||
artifacts:
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
apply: true # ← This artifact triggers apply mode
|
||||
```
|
||||
|
||||
**Pros:** Explicit, flexible
|
||||
**Cons:** Another field to maintain, what if multiple artifacts are `apply: true`?
|
||||
|
||||
### Option B: Leaf artifacts are implementable
|
||||
|
||||
The artifact(s) with no dependents (nothing depends on them) are the apply target.
|
||||
|
||||
- `spec-driven`: `tasks` is a leaf → apply = execute tasks
|
||||
- `tdd`: `docs` is a leaf → but that doesn't make sense for TDD...
|
||||
|
||||
**Pros:** No extra schema field, derived from graph
|
||||
**Cons:** Doesn't match TDD semantics (implementation is the action, not docs)
|
||||
|
||||
### Option C: Schema-level `apply_phase` definition
|
||||
|
||||
Add a top-level field to the schema:
|
||||
|
||||
```yaml
|
||||
name: spec-driven
|
||||
apply_phase:
|
||||
requires: [tasks] # Must exist before apply
|
||||
tracks: tasks.md # File with checkboxes to track
|
||||
instruction: "Work through tasks, mark complete as you go"
|
||||
```
|
||||
|
||||
```yaml
|
||||
name: tdd
|
||||
apply_phase:
|
||||
requires: [tests] # Must have tests before implementing
|
||||
tracks: null # No checkbox tracking - just make tests pass
|
||||
instruction: "Run tests, implement until green, refactor"
|
||||
```
|
||||
|
||||
**Pros:** Full flexibility, schema controls its own apply semantics
|
||||
**Cons:** More complex schema format
|
||||
|
||||
### Option D: Convention-based (artifact ID matching)
|
||||
|
||||
If artifact ID is `tasks` or `implementation`, it's the apply target.
|
||||
|
||||
**Pros:** Simple, no schema changes
|
||||
**Cons:** Brittle, doesn't work for custom schemas
|
||||
|
||||
### Option E: All artifacts complete → apply available
|
||||
|
||||
Apply becomes available when ALL schema artifacts exist. Implementation is whatever the user does after planning.
|
||||
|
||||
**Pros:** Simple, no schema changes
|
||||
**Cons:** Doesn't guide what "apply" means for different workflows
|
||||
|
||||
---
|
||||
|
||||
## Decision: Add `apply` block to schema.yaml
|
||||
|
||||
Add a top-level `apply` field to schema definitions:
|
||||
|
||||
```yaml
|
||||
name: spec-driven
|
||||
version: 1
|
||||
description: Default OpenSpec workflow
|
||||
|
||||
artifacts:
|
||||
# ... existing artifacts ...
|
||||
|
||||
apply:
|
||||
requires: [tasks] # Artifacts that must exist before apply
|
||||
tracks: tasks.md # File with checkboxes for progress (optional)
|
||||
instruction: | # Guidance shown to agent
|
||||
Read context files, work through pending tasks, mark complete as you go.
|
||||
Pause if you hit blockers or need clarification.
|
||||
```
|
||||
|
||||
```yaml
|
||||
name: tdd
|
||||
version: 1
|
||||
description: Test-driven development workflow
|
||||
|
||||
artifacts:
|
||||
# ... existing artifacts ...
|
||||
|
||||
apply:
|
||||
requires: [tests] # Must have tests before implementing
|
||||
tracks: null # No checkbox tracking
|
||||
instruction: |
|
||||
Run tests to see failures. Implement minimal code to pass each test.
|
||||
Refactor while keeping tests green.
|
||||
```
|
||||
|
||||
**Key properties:**
|
||||
- `requires`: Array of artifact IDs that must exist before apply is available
|
||||
- `tracks`: Path to file with checkboxes (relative to change dir), or `null` if no tracking
|
||||
- `instruction`: Custom guidance for the apply phase
|
||||
|
||||
**Fallback behavior:** Schemas without `apply` block default to "all artifacts must exist"
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `cli-artifact-workflow`: Apply instructions become schema-aware
|
||||
|
||||
## Impact
|
||||
|
||||
- **Affected code**: `src/commands/artifact-workflow.ts` (generateApplyInstructions)
|
||||
- **Schema format**: May need new `apply_phase` field
|
||||
- **Existing schemas**: Need to add apply_phase to `spec-driven` and `tdd`
|
||||
- **Backward compatible**: Schemas without apply_phase can use default behavior
|
||||
@@ -1,35 +0,0 @@
|
||||
## Prerequisites
|
||||
|
||||
- [x] 0.1 Implement `add-per-change-schema-metadata` first (to auto-detect schema)
|
||||
|
||||
## 1. Schema Format
|
||||
|
||||
- [x] 1.1 Add `ApplyPhaseSchema` Zod schema to `src/core/artifact-graph/types.ts`
|
||||
- [x] 1.2 Update `SchemaYamlSchema` to include optional `apply` field
|
||||
- [x] 1.3 Export `ApplyPhase` type
|
||||
|
||||
## 2. Update Existing Schemas
|
||||
|
||||
- [x] 2.1 Add `apply` block to `schemas/spec-driven/schema.yaml`
|
||||
- [x] 2.2 Add `apply` block to `schemas/tdd/schema.yaml`
|
||||
|
||||
## 3. Refactor generateApplyInstructions
|
||||
|
||||
- [x] 3.1 Load schema via `resolveSchema(schemaName)`
|
||||
- [x] 3.2 Read `apply.requires` to determine required artifacts
|
||||
- [x] 3.3 Check artifact existence dynamically (not hardcoded paths)
|
||||
- [x] 3.4 Use `apply.tracks` for progress tracking (or skip if null)
|
||||
- [x] 3.5 Use `apply.instruction` for the instruction text
|
||||
- [x] 3.6 Build `contextFiles` from all existing artifacts in schema
|
||||
|
||||
## 4. Handle Fallback
|
||||
|
||||
- [x] 4.1 If schema has no `apply` block, require all artifacts to exist
|
||||
- [x] 4.2 Default instruction: "All artifacts complete. Proceed with implementation."
|
||||
|
||||
## 5. Tests
|
||||
|
||||
- [x] 5.1 Test apply instructions with spec-driven schema
|
||||
- [x] 5.2 Test apply instructions with tdd schema
|
||||
- [x] 5.3 Test fallback when schema has no apply block
|
||||
- [x] 5.4 Test blocked state when required artifacts missing
|
||||
@@ -46,6 +46,35 @@ The system SHALL display artifact completion status for a change, including scaf
|
||||
- **AND** directory `openspec/changes/unknown-id/` does not exist
|
||||
- **THEN** the system displays an error listing all available change directories
|
||||
|
||||
### Requirement: Next Command
|
||||
|
||||
The system SHALL show which artifacts are ready to be created, including for scaffolded changes.
|
||||
|
||||
#### Scenario: Show ready artifacts
|
||||
|
||||
- **WHEN** user runs `openspec next --change <id>`
|
||||
- **THEN** the system lists artifacts whose dependencies are all satisfied
|
||||
|
||||
#### Scenario: No artifacts ready
|
||||
|
||||
- **WHEN** all artifacts are either completed or blocked
|
||||
- **THEN** the system indicates no artifacts are ready (with explanation)
|
||||
|
||||
#### Scenario: All artifacts complete
|
||||
|
||||
- **WHEN** all artifacts in the change are completed
|
||||
- **THEN** the system indicates the change is complete
|
||||
|
||||
#### Scenario: Next JSON output
|
||||
|
||||
- **WHEN** user runs `openspec next --change <id> --json`
|
||||
- **THEN** the system outputs JSON array of ready artifact IDs
|
||||
|
||||
#### Scenario: Next on scaffolded change
|
||||
|
||||
- **WHEN** user runs `openspec next --change <id>` on a change with no artifacts
|
||||
- **THEN** system shows root artifacts (e.g., "proposal") as ready to create
|
||||
|
||||
### Requirement: Instructions Command
|
||||
|
||||
The system SHALL output enriched instructions for creating an artifact, including for scaffolded changes.
|
||||
@@ -159,11 +188,3 @@ 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
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Next Command
|
||||
|
||||
**Reason**: Redundant with Status Command - `openspec status` already shows which artifacts are ready (status: "ready") vs blocked vs done.
|
||||
|
||||
**Migration**: Use `openspec status --change <id> --json` and filter artifacts with `status: "ready"` to find artifacts that can be created next.
|
||||
|
||||
|
||||
@@ -172,12 +172,6 @@ The command SHALL use consistent exit codes to indicate different failure modes.
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### Scenario: Generating slash commands for Antigravity
|
||||
- **WHEN** the user selects Antigravity during initialization
|
||||
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
|
||||
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
|
||||
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
|
||||
|
||||
#### Scenario: Generating slash commands for Claude Code
|
||||
- **WHEN** the user selects Claude Code during initialization
|
||||
- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md`
|
||||
@@ -192,7 +186,7 @@ The init command SHALL generate slash command files for supported editors using
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
- **WHEN** the user selects Cline during initialization
|
||||
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
|
||||
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
@@ -52,11 +52,6 @@ The update command SHALL always update the core OpenSpec files and display an AS
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
|
||||
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
|
||||
|
||||
#### Scenario: Updating slash commands for Claude Code
|
||||
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
@@ -68,7 +63,7 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cline
|
||||
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **WHEN** `.clinerules/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
@@ -6,143 +6,23 @@ artifacts:
|
||||
generates: proposal.md
|
||||
description: Initial proposal document outlining the change
|
||||
template: proposal.md
|
||||
instruction: |
|
||||
Create the proposal document that establishes WHY this change is needed.
|
||||
|
||||
Sections:
|
||||
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
|
||||
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
|
||||
- **Capabilities**: Identify which specs will be created or modified:
|
||||
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<name>/spec.md`. Use kebab-case names (e.g., `user-auth`, `data-export`).
|
||||
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Check `openspec/specs/` for existing spec names. Leave empty if no requirement changes.
|
||||
- **Impact**: Affected code, APIs, dependencies, or systems.
|
||||
|
||||
IMPORTANT: The Capabilities section is critical. It creates the contract between
|
||||
proposal and specs phases. Research existing specs before filling this in.
|
||||
Each capability listed here will need a corresponding spec file.
|
||||
|
||||
Keep it concise (1-2 pages). Focus on the "why" not the "how" -
|
||||
implementation details belong in design.md.
|
||||
|
||||
This is the foundation - specs, design, and tasks all build on this.
|
||||
requires: []
|
||||
|
||||
- id: specs
|
||||
generates: "specs/**/*.md"
|
||||
description: Detailed specifications for the change
|
||||
template: spec.md
|
||||
instruction: |
|
||||
Create specification files that define WHAT the system should do.
|
||||
|
||||
Create one spec file per capability/feature area in specs/<name>/spec.md.
|
||||
|
||||
Delta operations (use ## headers):
|
||||
- **ADDED Requirements**: New capabilities
|
||||
- **MODIFIED Requirements**: Changed behavior - MUST include full updated content
|
||||
- **REMOVED Requirements**: Deprecated features - MUST include **Reason** and **Migration**
|
||||
- **RENAMED Requirements**: Name changes only - use FROM:/TO: format
|
||||
|
||||
Format requirements:
|
||||
- Each requirement: `### Requirement: <name>` followed by description
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may)
|
||||
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
|
||||
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
|
||||
- Every requirement MUST have at least one scenario.
|
||||
|
||||
MODIFIED requirements workflow:
|
||||
1. Locate the existing requirement in openspec/specs/<capability>/spec.md
|
||||
2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
|
||||
3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
|
||||
4. Ensure header text matches exactly (whitespace-insensitive)
|
||||
|
||||
Common pitfall: Using MODIFIED with partial content loses detail at archive time.
|
||||
If adding new concerns without changing existing behavior, use ADDED instead.
|
||||
|
||||
Example:
|
||||
```
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: User can export data
|
||||
The system SHALL allow users to export their data in CSV format.
|
||||
|
||||
#### Scenario: Successful export
|
||||
- **WHEN** user clicks "Export" button
|
||||
- **THEN** system downloads a CSV file with all user data
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Legacy export
|
||||
**Reason**: Replaced by new export system
|
||||
**Migration**: Use new export endpoint at /api/v2/export
|
||||
```
|
||||
|
||||
Specs should be testable - each scenario is a potential test case.
|
||||
requires:
|
||||
- proposal
|
||||
|
||||
- id: design
|
||||
generates: design.md
|
||||
description: Technical design document with implementation details
|
||||
template: design.md
|
||||
instruction: |
|
||||
Create the design document that explains HOW to implement the change.
|
||||
|
||||
When to include design.md (create only if any apply):
|
||||
- Cross-cutting change (multiple services/modules) or new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Sections:
|
||||
- **Context**: Background, current state, constraints, stakeholders
|
||||
- **Goals / Non-Goals**: What this design achieves and explicitly excludes
|
||||
- **Decisions**: Key technical choices with rationale (why X over Y?). Include alternatives considered for each decision.
|
||||
- **Risks / Trade-offs**: Known limitations, things that could go wrong. Format: [Risk] → Mitigation
|
||||
- **Migration Plan**: Steps to deploy, rollback strategy (if applicable)
|
||||
- **Open Questions**: Outstanding decisions or unknowns to resolve
|
||||
|
||||
Focus on architecture and approach, not line-by-line implementation.
|
||||
Reference the proposal for motivation and specs for requirements.
|
||||
|
||||
Good design docs explain the "why" behind technical decisions.
|
||||
requires:
|
||||
- proposal
|
||||
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
description: Implementation tasks derived from specs and design
|
||||
template: tasks.md
|
||||
instruction: |
|
||||
Create the task list that breaks down the implementation work.
|
||||
|
||||
Guidelines:
|
||||
- Group related tasks under ## numbered headings
|
||||
- Each task is a checkbox: - [ ] X.Y Task description
|
||||
- Tasks should be small enough to complete in one session
|
||||
- Order tasks by dependency (what must be done first?)
|
||||
|
||||
Example:
|
||||
```
|
||||
## 1. Setup
|
||||
|
||||
- [ ] 1.1 Create new module structure
|
||||
- [ ] 1.2 Add dependencies to package.json
|
||||
|
||||
## 2. Core Implementation
|
||||
|
||||
- [ ] 2.1 Implement data export function
|
||||
- [ ] 2.2 Add CSV formatting utilities
|
||||
```
|
||||
|
||||
Reference specs for what needs to be built, design for how to build it.
|
||||
Each task should be verifiable - you know when it's done.
|
||||
requires:
|
||||
- specs
|
||||
- design
|
||||
|
||||
apply:
|
||||
requires: [tasks]
|
||||
tracks: tasks.md
|
||||
instruction: |
|
||||
Read context files, work through pending tasks, mark complete as you go.
|
||||
Pause if you hit blockers or need clarification.
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
## Why
|
||||
|
||||
<!-- Explain the motivation for this change. What problem does this solve? Why now? -->
|
||||
<!-- Explain the motivation for this change -->
|
||||
|
||||
## What Changes
|
||||
|
||||
<!-- Describe what will change. Be specific about new capabilities, modifications, or removals. -->
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- Capabilities being introduced. Replace <name> with kebab-case identifier (e.g., user-auth, data-export, api-rate-limiting). Each creates specs/<name>/spec.md -->
|
||||
- `<name>`: <brief description of what this capability covers>
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- Existing capabilities whose REQUIREMENTS are changing (not just implementation).
|
||||
Only list here if spec-level behavior changes. Each needs a delta spec file.
|
||||
Use existing spec names from openspec/specs/. Leave empty if no requirement changes. -->
|
||||
- `<existing-name>`: <what requirement is changing>
|
||||
<!-- Describe what will change -->
|
||||
|
||||
## Impact
|
||||
|
||||
<!-- Affected code, APIs, dependencies, systems -->
|
||||
<!-- List affected areas -->
|
||||
|
||||
@@ -6,208 +6,22 @@ artifacts:
|
||||
generates: spec.md
|
||||
description: Feature specification defining requirements
|
||||
template: spec.md
|
||||
instruction: |
|
||||
Create the feature specification that defines WHAT to build.
|
||||
|
||||
Sections:
|
||||
- **Feature**: Name and high-level description of the feature's purpose and user value
|
||||
- **Requirements**: List of specific requirements. Use SHALL/MUST for normative language.
|
||||
- **Acceptance Criteria**: Testable criteria in WHEN/THEN format
|
||||
|
||||
Format requirements:
|
||||
- Each requirement should be specific and testable
|
||||
- Use `#### Scenario: <name>` with WHEN/THEN format for acceptance criteria
|
||||
- Define edge cases and error scenarios explicitly
|
||||
- Every requirement MUST have at least one scenario
|
||||
|
||||
Example:
|
||||
```
|
||||
## Feature: User Authentication
|
||||
|
||||
Users can securely log into the application.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Password validation
|
||||
The system SHALL validate passwords meet minimum security requirements.
|
||||
|
||||
#### Scenario: Valid password accepted
|
||||
- **WHEN** password has 8+ chars, uppercase, lowercase, and number
|
||||
- **THEN** password is accepted
|
||||
|
||||
#### Scenario: Weak password rejected
|
||||
- **WHEN** password is less than 8 characters
|
||||
- **THEN** system displays "Password too short" error
|
||||
```
|
||||
|
||||
This spec drives test creation - each scenario becomes a test case.
|
||||
requires: []
|
||||
|
||||
- id: tests
|
||||
generates: "tests/*.test.ts"
|
||||
description: Test files written before implementation
|
||||
template: test.md
|
||||
instruction: |
|
||||
Write tests BEFORE implementation (TDD red phase).
|
||||
|
||||
File naming:
|
||||
- Create test files as `tests/<feature>.test.ts`
|
||||
- One test file per feature/capability
|
||||
- Use descriptive names matching the spec
|
||||
|
||||
Test structure:
|
||||
- Use Given/When/Then format matching spec scenarios
|
||||
- Group related tests with `describe()` blocks
|
||||
- Each scenario from spec becomes at least one `it()` test
|
||||
|
||||
Coverage requirements:
|
||||
- Cover each requirement from the spec
|
||||
- Include happy path (success cases)
|
||||
- Include edge cases (boundary conditions)
|
||||
- Include error scenarios (invalid input, failures)
|
||||
- Tests should fail initially (no implementation yet)
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
describe('Password validation', () => {
|
||||
it('accepts valid password with all requirements', () => {
|
||||
// GIVEN a password meeting all requirements
|
||||
const password = 'SecurePass1';
|
||||
// WHEN validating
|
||||
const result = validatePassword(password);
|
||||
// THEN it should be accepted
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects password shorter than 8 characters', () => {
|
||||
// GIVEN a short password
|
||||
const password = 'Short1';
|
||||
// WHEN validating
|
||||
const result = validatePassword(password);
|
||||
// THEN it should be rejected with message
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toBe('Password too short');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Follow the spec requirements exactly - tests verify the spec.
|
||||
requires:
|
||||
- spec
|
||||
|
||||
- id: implementation
|
||||
generates: "src/*.ts"
|
||||
description: Implementation code to pass the tests
|
||||
template: implementation.md
|
||||
instruction: |
|
||||
Implement the feature to make tests pass (TDD green phase).
|
||||
|
||||
TDD workflow:
|
||||
1. Run tests - confirm they fail (red)
|
||||
2. Write minimal code to pass ONE test
|
||||
3. Run tests - confirm that test passes (green)
|
||||
4. Refactor if needed while keeping tests green
|
||||
5. Repeat for next failing test
|
||||
|
||||
Implementation guidelines:
|
||||
- Write minimal code to pass each test - no more, no less
|
||||
- Run tests frequently to verify progress
|
||||
- Keep functions small and focused
|
||||
- Use clear, descriptive names
|
||||
|
||||
Code organization:
|
||||
- Create source files in `src/<feature>.ts`
|
||||
- Export public API clearly
|
||||
- Keep implementation details private
|
||||
- Add JSDoc comments for public functions
|
||||
|
||||
Example structure:
|
||||
```typescript
|
||||
/**
|
||||
* Validates a password meets security requirements.
|
||||
* @param password - The password to validate
|
||||
* @returns Validation result with valid flag and optional error
|
||||
*/
|
||||
export function validatePassword(password: string): ValidationResult {
|
||||
if (password.length < 8) {
|
||||
return { valid: false, error: 'Password too short' };
|
||||
}
|
||||
// ... additional checks
|
||||
return { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
Don't over-engineer - implement only what tests require.
|
||||
requires:
|
||||
- tests
|
||||
|
||||
- id: docs
|
||||
generates: "docs/*.md"
|
||||
description: Documentation for the implemented feature
|
||||
template: docs.md
|
||||
instruction: |
|
||||
Document the implemented feature.
|
||||
|
||||
Sections:
|
||||
- **Overview**: What the feature does and why it exists (1-2 paragraphs)
|
||||
- **Getting Started**: Quick start guide to use the feature immediately
|
||||
- **Examples**: Code examples showing common use cases
|
||||
- **Reference**: Detailed API documentation, configuration options
|
||||
|
||||
Guidelines:
|
||||
- Write for the user, not the developer
|
||||
- Start with the most common use case
|
||||
- Include copy-pasteable code examples
|
||||
- Document all configuration options with defaults
|
||||
- Note any limitations, edge cases, or gotchas
|
||||
- Link to related features or specs
|
||||
|
||||
Example structure:
|
||||
```markdown
|
||||
## Overview
|
||||
|
||||
Password validation ensures user passwords meet security requirements
|
||||
before account creation or password changes.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Import and use the validation function:
|
||||
|
||||
```typescript
|
||||
import { validatePassword } from './password';
|
||||
|
||||
const result = validatePassword('MySecurePass1');
|
||||
if (!result.valid) {
|
||||
console.error(result.error);
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic validation
|
||||
...
|
||||
|
||||
### Custom error handling
|
||||
...
|
||||
|
||||
## Reference
|
||||
|
||||
### validatePassword(password)
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| password | string | The password to validate |
|
||||
|
||||
**Returns**: `{ valid: boolean, error?: string }`
|
||||
```
|
||||
|
||||
Reference the spec for requirements, implementation for details.
|
||||
requires:
|
||||
- implementation
|
||||
|
||||
apply:
|
||||
requires: [tests]
|
||||
tracks: null
|
||||
instruction: |
|
||||
Run tests to see failures. Implement minimal code to pass each test.
|
||||
Refactor while keeping tests green.
|
||||
|
||||
+2
-5
@@ -96,14 +96,11 @@ program
|
||||
.description('List items (changes by default). Use --specs to list specs.')
|
||||
.option('--specs', 'List specs instead of changes')
|
||||
.option('--changes', 'List changes explicitly (default)')
|
||||
.option('--sort <order>', 'Sort order: "recent" (default) or "name"', 'recent')
|
||||
.option('--json', 'Output as JSON (for programmatic use)')
|
||||
.action(async (options?: { specs?: boolean; changes?: boolean; sort?: string; json?: boolean }) => {
|
||||
.action(async (options?: { specs?: boolean; changes?: boolean }) => {
|
||||
try {
|
||||
const listCommand = new ListCommand();
|
||||
const mode: 'changes' | 'specs' = options?.specs ? 'specs' : 'changes';
|
||||
const sort = options?.sort === 'name' ? 'name' : 'recent';
|
||||
await listCommand.execute('.', mode, { sort, json: options?.json });
|
||||
await listCommand.execute('.', mode);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
|
||||
+100
-605
@@ -19,43 +19,13 @@ import {
|
||||
formatChangeStatus,
|
||||
generateInstructions,
|
||||
listSchemas,
|
||||
listSchemasWithInfo,
|
||||
getSchemaDir,
|
||||
resolveSchema,
|
||||
ArtifactGraph,
|
||||
type ChangeStatus,
|
||||
type ArtifactInstructions,
|
||||
type SchemaInfo,
|
||||
} from '../core/artifact-graph/index.js';
|
||||
import { createChange, validateChangeName } from '../utils/change-utils.js';
|
||||
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate } from '../core/templates/skill-templates.js';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types for Apply Instructions
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface TaskItem {
|
||||
id: string;
|
||||
description: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
interface ApplyInstructions {
|
||||
changeName: string;
|
||||
changeDir: string;
|
||||
schemaName: string;
|
||||
contextFiles: Record<string, string>;
|
||||
progress: {
|
||||
total: number;
|
||||
complete: number;
|
||||
remaining: number;
|
||||
};
|
||||
tasks: TaskItem[];
|
||||
state: 'blocked' | 'all_done' | 'ready';
|
||||
missingArtifacts?: string[];
|
||||
instruction: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SCHEMA = 'spec-driven';
|
||||
|
||||
@@ -185,14 +155,9 @@ async function statusCommand(options: StatusOptions): Promise<void> {
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const changeName = await validateChangeExists(options.change, projectRoot);
|
||||
const schemaName = validateSchemaExists(options.schema ?? DEFAULT_SCHEMA);
|
||||
|
||||
// Validate schema if explicitly provided
|
||||
if (options.schema) {
|
||||
validateSchemaExists(options.schema);
|
||||
}
|
||||
|
||||
// loadChangeContext will auto-detect schema from metadata if not provided
|
||||
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
||||
const context = loadChangeContext(projectRoot, changeName, schemaName);
|
||||
const status = formatChangeStatus(context);
|
||||
|
||||
spinner.stop();
|
||||
@@ -236,6 +201,57 @@ function printStatusText(status: ChangeStatus): void {
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Next Command
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface NextOptions {
|
||||
change?: string;
|
||||
schema?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
async function nextCommand(options: NextOptions): Promise<void> {
|
||||
const spinner = ora('Finding next artifacts...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const changeName = await validateChangeExists(options.change, projectRoot);
|
||||
const schemaName = validateSchemaExists(options.schema ?? DEFAULT_SCHEMA);
|
||||
|
||||
const context = loadChangeContext(projectRoot, changeName, schemaName);
|
||||
const ready = context.graph.getNextArtifacts(context.completed);
|
||||
const isComplete = context.graph.isComplete(context.completed);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(ready, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
console.log(chalk.green('All artifacts are complete!'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ready.length === 0) {
|
||||
console.log('No artifacts are ready. All remaining artifacts are blocked.');
|
||||
console.log('Run `openspec status --change ' + changeName + '` to see blocked dependencies.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Artifacts ready to create:');
|
||||
for (const artifactId of ready) {
|
||||
const color = getStatusColor('ready');
|
||||
console.log(color(` ${artifactId}`));
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Instructions Command
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -255,30 +271,26 @@ async function instructionsCommand(
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const changeName = await validateChangeExists(options.change, projectRoot);
|
||||
|
||||
// Validate schema if explicitly provided
|
||||
if (options.schema) {
|
||||
validateSchemaExists(options.schema);
|
||||
}
|
||||
|
||||
// loadChangeContext will auto-detect schema from metadata if not provided
|
||||
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
||||
const schemaName = validateSchemaExists(options.schema ?? DEFAULT_SCHEMA);
|
||||
|
||||
if (!artifactId) {
|
||||
spinner.stop();
|
||||
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
||||
const schema = resolveSchema(schemaName);
|
||||
const graph = ArtifactGraph.fromSchema(schema);
|
||||
const validIds = graph.getAllArtifacts().map((a) => a.id);
|
||||
throw new Error(
|
||||
`Missing required argument <artifact>. Valid artifacts:\n ${validIds.join('\n ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const context = loadChangeContext(projectRoot, changeName, schemaName);
|
||||
const artifact = context.graph.getArtifact(artifactId);
|
||||
|
||||
if (!artifact) {
|
||||
spinner.stop();
|
||||
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
||||
throw new Error(
|
||||
`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`
|
||||
`Artifact '${artifactId}' not found in schema '${schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,372 +312,38 @@ async function instructionsCommand(
|
||||
}
|
||||
|
||||
function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void {
|
||||
const {
|
||||
artifactId,
|
||||
changeName,
|
||||
schemaName,
|
||||
changeDir,
|
||||
outputPath,
|
||||
description,
|
||||
instruction,
|
||||
template,
|
||||
dependencies,
|
||||
unlocks,
|
||||
} = instructions;
|
||||
|
||||
// Opening tag
|
||||
console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
|
||||
console.log();
|
||||
|
||||
// Warning for blocked artifacts
|
||||
if (isBlocked) {
|
||||
const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
|
||||
console.log('<warning>');
|
||||
console.log('This artifact has unmet dependencies. Complete them first or proceed with caution.');
|
||||
console.log(`Missing: ${missing.join(', ')}`);
|
||||
console.log('</warning>');
|
||||
console.log(chalk.yellow('Warning: This artifact has unmet dependencies.'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Task directive
|
||||
console.log('<task>');
|
||||
console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
|
||||
console.log(description);
|
||||
console.log('</task>');
|
||||
console.log(`Artifact: ${instructions.artifactId}`);
|
||||
console.log(`Output: ${instructions.outputPath}`);
|
||||
console.log(`Description: ${instructions.description}`);
|
||||
console.log();
|
||||
|
||||
// Context (dependencies)
|
||||
if (dependencies.length > 0) {
|
||||
console.log('<context>');
|
||||
console.log('Read these files for context before creating this artifact:');
|
||||
console.log();
|
||||
for (const dep of dependencies) {
|
||||
const status = dep.done ? 'done' : 'missing';
|
||||
const fullPath = path.join(changeDir, dep.path);
|
||||
console.log(`<dependency id="${dep.id}" status="${status}">`);
|
||||
console.log(` <path>${fullPath}</path>`);
|
||||
console.log(` <description>${dep.description}</description>`);
|
||||
console.log('</dependency>');
|
||||
}
|
||||
console.log('</context>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Output location
|
||||
console.log('<output>');
|
||||
console.log(`Write to: ${path.join(changeDir, outputPath)}`);
|
||||
console.log('</output>');
|
||||
console.log();
|
||||
|
||||
// Instruction (guidance)
|
||||
if (instruction) {
|
||||
console.log('<instruction>');
|
||||
console.log(instruction.trim());
|
||||
console.log('</instruction>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Template
|
||||
console.log('<template>');
|
||||
console.log(template.trim());
|
||||
console.log('</template>');
|
||||
console.log();
|
||||
|
||||
// Success criteria placeholder
|
||||
console.log('<success_criteria>');
|
||||
console.log('<!-- To be defined in schema validation rules -->');
|
||||
console.log('</success_criteria>');
|
||||
console.log();
|
||||
|
||||
// Unlocks
|
||||
if (unlocks.length > 0) {
|
||||
console.log('<unlocks>');
|
||||
console.log(`Completing this artifact enables: ${unlocks.join(', ')}`);
|
||||
console.log('</unlocks>');
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Closing tag
|
||||
console.log('</artifact>');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Apply Instructions Command
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface ApplyInstructionsOptions {
|
||||
change?: string;
|
||||
schema?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses tasks.md content and extracts task items with their completion status.
|
||||
*/
|
||||
function parseTasksFile(content: string): TaskItem[] {
|
||||
const tasks: TaskItem[] = [];
|
||||
const lines = content.split('\n');
|
||||
let taskIndex = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
// Match checkbox patterns: - [ ] or - [x] or - [X]
|
||||
const checkboxMatch = line.match(/^[-*]\s*\[([ xX])\]\s*(.+)$/);
|
||||
if (checkboxMatch) {
|
||||
taskIndex++;
|
||||
const done = checkboxMatch[1].toLowerCase() === 'x';
|
||||
const description = checkboxMatch[2].trim();
|
||||
tasks.push({
|
||||
id: `${taskIndex}`,
|
||||
description,
|
||||
done,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an artifact output exists in the change directory.
|
||||
* Supports glob patterns (e.g., "specs/*.md") by verifying at least one matching file exists.
|
||||
*/
|
||||
function artifactOutputExists(changeDir: string, generates: string): boolean {
|
||||
// Normalize the generates path to use platform-specific separators
|
||||
const normalizedGenerates = generates.split('/').join(path.sep);
|
||||
const fullPath = path.join(changeDir, normalizedGenerates);
|
||||
|
||||
// If it's a glob pattern (contains ** or *), check for matching files
|
||||
if (generates.includes('*')) {
|
||||
// Extract the directory part before the glob pattern
|
||||
const parts = normalizedGenerates.split(path.sep);
|
||||
const dirParts: string[] = [];
|
||||
let patternPart = '';
|
||||
for (const part of parts) {
|
||||
if (part.includes('*')) {
|
||||
patternPart = part;
|
||||
break;
|
||||
}
|
||||
dirParts.push(part);
|
||||
}
|
||||
const dirPath = path.join(changeDir, ...dirParts);
|
||||
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract expected extension from pattern (e.g., "*.md" -> ".md")
|
||||
const extMatch = patternPart.match(/\*(\.[a-zA-Z0-9]+)$/);
|
||||
const expectedExt = extMatch ? extMatch[1] : null;
|
||||
|
||||
// Recursively check for matching files
|
||||
const hasMatchingFiles = (dir: string): boolean => {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
// For ** patterns, recurse into subdirectories
|
||||
if (generates.includes('**') && hasMatchingFiles(path.join(dir, entry.name))) {
|
||||
return true;
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
// Check if file matches expected extension (or any file if no extension specified)
|
||||
if (!expectedExt || entry.name.endsWith(expectedExt)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return hasMatchingFiles(dirPath);
|
||||
}
|
||||
|
||||
return fs.existsSync(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates apply instructions for implementing tasks from a change.
|
||||
* Schema-aware: reads apply phase configuration from schema to determine
|
||||
* required artifacts, tracking file, and instruction.
|
||||
*/
|
||||
async function generateApplyInstructions(
|
||||
projectRoot: string,
|
||||
changeName: string,
|
||||
schemaName?: string
|
||||
): Promise<ApplyInstructions> {
|
||||
// loadChangeContext will auto-detect schema from metadata if not provided
|
||||
const context = loadChangeContext(projectRoot, changeName, schemaName);
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
|
||||
// Get the full schema to access the apply phase configuration
|
||||
const schema = resolveSchema(context.schemaName);
|
||||
const applyConfig = schema.apply;
|
||||
|
||||
// Determine required artifacts and tracking file from schema
|
||||
// Fallback: if no apply block, require all artifacts
|
||||
const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
|
||||
const tracksFile = applyConfig?.tracks ?? null;
|
||||
const schemaInstruction = applyConfig?.instruction ?? null;
|
||||
|
||||
// Check which required artifacts are missing
|
||||
const missingArtifacts: string[] = [];
|
||||
for (const artifactId of requiredArtifactIds) {
|
||||
const artifact = schema.artifacts.find((a) => a.id === artifactId);
|
||||
if (artifact && !artifactOutputExists(changeDir, artifact.generates)) {
|
||||
missingArtifacts.push(artifactId);
|
||||
}
|
||||
}
|
||||
|
||||
// Build context files from all existing artifacts in schema
|
||||
const contextFiles: Record<string, string> = {};
|
||||
for (const artifact of schema.artifacts) {
|
||||
if (artifactOutputExists(changeDir, artifact.generates)) {
|
||||
contextFiles[artifact.id] = path.join(changeDir, artifact.generates);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tasks if tracking file exists
|
||||
let tasks: TaskItem[] = [];
|
||||
let tracksFileExists = false;
|
||||
if (tracksFile) {
|
||||
const tracksPath = path.join(changeDir, tracksFile);
|
||||
tracksFileExists = fs.existsSync(tracksPath);
|
||||
if (tracksFileExists) {
|
||||
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
|
||||
tasks = parseTasksFile(tasksContent);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate progress
|
||||
const total = tasks.length;
|
||||
const complete = tasks.filter((t) => t.done).length;
|
||||
const remaining = total - complete;
|
||||
|
||||
// Determine state and instruction
|
||||
let state: ApplyInstructions['state'];
|
||||
let instruction: string;
|
||||
|
||||
if (missingArtifacts.length > 0) {
|
||||
state = 'blocked';
|
||||
instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nUse the openspec-continue-change skill to create the missing artifacts first.`;
|
||||
} else if (tracksFile && !tracksFileExists) {
|
||||
// Tracking file configured but doesn't exist yet
|
||||
const tracksFilename = path.basename(tracksFile);
|
||||
state = 'blocked';
|
||||
instruction = `The ${tracksFilename} file is missing and must be created.\nUse openspec-continue-change to generate the tracking file.`;
|
||||
} else if (tracksFile && tracksFileExists && total === 0) {
|
||||
// Tracking file exists but contains no tasks
|
||||
const tracksFilename = path.basename(tracksFile);
|
||||
state = 'blocked';
|
||||
instruction = `The ${tracksFilename} file exists but contains no tasks.\nAdd tasks to ${tracksFilename} or regenerate it with openspec-continue-change.`;
|
||||
} else if (tracksFile && remaining === 0 && total > 0) {
|
||||
state = 'all_done';
|
||||
instruction = 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
|
||||
} else if (!tracksFile) {
|
||||
// No tracking file (e.g., TDD schema) - ready to apply
|
||||
state = 'ready';
|
||||
instruction = schemaInstruction?.trim() ?? 'All required artifacts complete. Proceed with implementation.';
|
||||
console.log('Dependencies:');
|
||||
if (instructions.dependencies.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
state = 'ready';
|
||||
instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
|
||||
}
|
||||
|
||||
return {
|
||||
changeName,
|
||||
changeDir,
|
||||
schemaName: context.schemaName,
|
||||
contextFiles,
|
||||
progress: { total, complete, remaining },
|
||||
tasks,
|
||||
state,
|
||||
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
||||
instruction,
|
||||
};
|
||||
}
|
||||
|
||||
async function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void> {
|
||||
const spinner = ora('Generating apply instructions...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const changeName = await validateChangeExists(options.change, projectRoot);
|
||||
|
||||
// Validate schema if explicitly provided
|
||||
if (options.schema) {
|
||||
validateSchemaExists(options.schema);
|
||||
for (const dep of instructions.dependencies) {
|
||||
const status = dep.done ? chalk.green('[done]') : chalk.red('[missing]');
|
||||
console.log(` ${status} ${dep.id}`);
|
||||
}
|
||||
|
||||
// generateApplyInstructions uses loadChangeContext which auto-detects schema
|
||||
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(instructions, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
printApplyInstructionsText(instructions);
|
||||
} catch (error) {
|
||||
spinner.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function printApplyInstructionsText(instructions: ApplyInstructions): void {
|
||||
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
|
||||
|
||||
console.log(`## Apply: ${changeName}`);
|
||||
console.log(`Schema: ${schemaName}`);
|
||||
console.log();
|
||||
|
||||
// Warning for blocked state
|
||||
if (state === 'blocked' && missingArtifacts) {
|
||||
console.log('### ⚠️ Blocked');
|
||||
console.log();
|
||||
console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`);
|
||||
console.log('Use the openspec-continue-change skill to create these first.');
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Context files (dynamically from schema)
|
||||
const contextFileEntries = Object.entries(contextFiles);
|
||||
if (contextFileEntries.length > 0) {
|
||||
console.log('### Context Files');
|
||||
for (const [artifactId, filePath] of contextFileEntries) {
|
||||
console.log(`- ${artifactId}: ${filePath}`);
|
||||
if (instructions.unlocks.length > 0) {
|
||||
console.log('Unlocks:');
|
||||
for (const unlocked of instructions.unlocks) {
|
||||
console.log(` ${unlocked}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Progress (only show if we have tracking)
|
||||
if (progress.total > 0 || tasks.length > 0) {
|
||||
console.log('### Progress');
|
||||
if (state === 'all_done') {
|
||||
console.log(`${progress.complete}/${progress.total} complete ✓`);
|
||||
} else {
|
||||
console.log(`${progress.complete}/${progress.total} complete`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Tasks
|
||||
if (tasks.length > 0) {
|
||||
console.log('### Tasks');
|
||||
for (const task of tasks) {
|
||||
const checkbox = task.done ? '[x]' : '[ ]';
|
||||
console.log(`- ${checkbox} ${task.description}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Instruction
|
||||
console.log('### Instruction');
|
||||
console.log(instruction);
|
||||
console.log('Template:');
|
||||
console.log('─'.repeat(40));
|
||||
console.log(instructions.template);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -734,7 +412,6 @@ async function templatesCommand(options: TemplatesOptions): Promise<void> {
|
||||
|
||||
interface NewChangeOptions {
|
||||
description?: string;
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
async function newChangeCommand(name: string | undefined, options: NewChangeOptions): Promise<void> {
|
||||
@@ -747,17 +424,11 @@ async function newChangeCommand(name: string | undefined, options: NewChangeOpti
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
|
||||
// Validate schema if provided
|
||||
if (options.schema) {
|
||||
validateSchemaExists(options.schema);
|
||||
}
|
||||
|
||||
const schemaDisplay = options.schema ? ` with schema '${options.schema}'` : '';
|
||||
const spinner = ora(`Creating change '${name}'${schemaDisplay}...`).start();
|
||||
const spinner = ora(`Creating change '${name}'...`).start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
await createChange(projectRoot, name, { schema: options.schema });
|
||||
await createChange(projectRoot, name);
|
||||
|
||||
// If description provided, create README.md with description
|
||||
if (options.description) {
|
||||
@@ -767,171 +438,13 @@ async function newChangeCommand(name: string | undefined, options: NewChangeOpti
|
||||
await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
|
||||
}
|
||||
|
||||
const schemaUsed = options.schema ?? DEFAULT_SCHEMA;
|
||||
spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${schemaUsed})`);
|
||||
spinner.succeed(`Created change '${name}' at openspec/changes/${name}/`);
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to create change '${name}'`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Artifact Experimental Setup Command
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generates Agent Skills and slash commands for the experimental artifact workflow.
|
||||
* Creates .claude/skills/ directory with SKILL.md files following Agent Skills spec.
|
||||
* Creates .claude/commands/opsx/ directory with slash command files.
|
||||
*/
|
||||
async function artifactExperimentalSetupCommand(): Promise<void> {
|
||||
const spinner = ora('Setting up experimental artifact workflow...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const skillsDir = path.join(projectRoot, '.claude', 'skills');
|
||||
const commandsDir = path.join(projectRoot, '.claude', 'commands', 'opsx');
|
||||
|
||||
// Get skill templates
|
||||
const newChangeSkill = getNewChangeSkillTemplate();
|
||||
const continueChangeSkill = getContinueChangeSkillTemplate();
|
||||
const applyChangeSkill = getApplyChangeSkillTemplate();
|
||||
const ffChangeSkill = getFfChangeSkillTemplate();
|
||||
|
||||
// Get command templates
|
||||
const newCommand = getOpsxNewCommandTemplate();
|
||||
const continueCommand = getOpsxContinueCommandTemplate();
|
||||
const applyCommand = getOpsxApplyCommandTemplate();
|
||||
const ffCommand = getOpsxFfCommandTemplate();
|
||||
|
||||
// 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' },
|
||||
];
|
||||
|
||||
const createdSkillFiles: string[] = [];
|
||||
|
||||
for (const { template, dirName } of skills) {
|
||||
const skillDir = path.join(skillsDir, dirName);
|
||||
const skillFile = path.join(skillDir, 'SKILL.md');
|
||||
|
||||
// Generate SKILL.md content with YAML frontmatter
|
||||
const skillContent = `---
|
||||
name: ${template.name}
|
||||
description: ${template.description}
|
||||
---
|
||||
|
||||
${template.instructions}
|
||||
`;
|
||||
|
||||
// Write the skill file
|
||||
await FileSystemUtils.writeFile(skillFile, skillContent);
|
||||
createdSkillFiles.push(path.relative(projectRoot, skillFile));
|
||||
}
|
||||
|
||||
// Create slash command files
|
||||
const commands = [
|
||||
{ template: newCommand, fileName: 'new.md' },
|
||||
{ template: continueCommand, fileName: 'continue.md' },
|
||||
{ template: applyCommand, fileName: 'apply.md' },
|
||||
{ template: ffCommand, fileName: 'ff.md' },
|
||||
];
|
||||
|
||||
const createdCommandFiles: string[] = [];
|
||||
|
||||
for (const { template, fileName } of commands) {
|
||||
const commandFile = path.join(commandsDir, fileName);
|
||||
|
||||
// Generate command content with YAML frontmatter
|
||||
const commandContent = `---
|
||||
name: ${template.name}
|
||||
description: ${template.description}
|
||||
category: ${template.category}
|
||||
tags: [${template.tags.join(', ')}]
|
||||
---
|
||||
|
||||
${template.content}
|
||||
`;
|
||||
|
||||
// Write the command file
|
||||
await FileSystemUtils.writeFile(commandFile, commandContent);
|
||||
createdCommandFiles.push(path.relative(projectRoot, commandFile));
|
||||
}
|
||||
|
||||
spinner.succeed('Experimental artifact workflow setup complete!');
|
||||
|
||||
// Print success message
|
||||
console.log();
|
||||
console.log(chalk.bold('🧪 Experimental Artifact Workflow Setup Complete'));
|
||||
console.log();
|
||||
console.log(chalk.bold('Skills Created:'));
|
||||
for (const file of createdSkillFiles) {
|
||||
console.log(chalk.green(' ✓ ' + file));
|
||||
}
|
||||
console.log();
|
||||
console.log(chalk.bold('Slash Commands Created:'));
|
||||
for (const file of createdCommandFiles) {
|
||||
console.log(chalk.green(' ✓ ' + file));
|
||||
}
|
||||
console.log();
|
||||
console.log(chalk.bold('📖 Usage:'));
|
||||
console.log();
|
||||
console.log(' ' + chalk.cyan('Skills') + ' work automatically in compatible editors:');
|
||||
console.log(' • Claude Code - Auto-detected, ready to use');
|
||||
console.log(' • Cursor - Enable in Settings → Rules → Import Settings');
|
||||
console.log(' • Windsurf - Auto-imports from .claude directory');
|
||||
console.log();
|
||||
console.log(' Ask Claude naturally:');
|
||||
console.log(' • "I want to start a new OpenSpec change to add <feature>"');
|
||||
console.log(' • "Continue working on this change"');
|
||||
console.log(' • "Implement the tasks for this change"');
|
||||
console.log();
|
||||
console.log(' ' + chalk.cyan('Slash Commands') + ' for explicit invocation:');
|
||||
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();
|
||||
console.log(chalk.yellow('💡 This is an experimental feature.'));
|
||||
console.log(' Feedback welcome at: https://github.com/Fission-AI/OpenSpec/issues');
|
||||
console.log();
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to setup experimental artifact workflow');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Schemas Command
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface SchemasOptions {
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
async function schemasCommand(options: SchemasOptions): Promise<void> {
|
||||
const schemas = listSchemasWithInfo();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(schemas, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Available schemas:');
|
||||
console.log();
|
||||
|
||||
for (const schema of schemas) {
|
||||
const sourceLabel = schema.source === 'user' ? chalk.dim(' (user override)') : '';
|
||||
console.log(` ${chalk.bold(schema.name)}${sourceLabel}`);
|
||||
console.log(` ${schema.description}`);
|
||||
console.log(` Artifacts: ${schema.artifacts.join(' → ')}`);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Command Registration
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -946,7 +459,7 @@ export function registerArtifactWorkflowCommands(program: Command): void {
|
||||
.command('status')
|
||||
.description('[Experimental] Display artifact completion status for a change')
|
||||
.option('--change <id>', 'Change name to show status for')
|
||||
.option('--schema <name>', 'Schema override (auto-detected from .openspec.yaml)')
|
||||
.option('--schema <name>', `Schema to use (default: ${DEFAULT_SCHEMA})`)
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options: StatusOptions) => {
|
||||
try {
|
||||
@@ -958,21 +471,33 @@ export function registerArtifactWorkflowCommands(program: Command): void {
|
||||
}
|
||||
});
|
||||
|
||||
// Next command
|
||||
program
|
||||
.command('next')
|
||||
.description('[Experimental] Show artifacts ready to be created')
|
||||
.option('--change <id>', 'Change name to check')
|
||||
.option('--schema <name>', `Schema to use (default: ${DEFAULT_SCHEMA})`)
|
||||
.option('--json', 'Output as JSON array of ready artifact IDs')
|
||||
.action(async (options: NextOptions) => {
|
||||
try {
|
||||
await nextCommand(options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Instructions command
|
||||
program
|
||||
.command('instructions [artifact]')
|
||||
.description('[Experimental] Output enriched instructions for creating an artifact or applying tasks')
|
||||
.description('[Experimental] Output enriched instructions for creating an artifact')
|
||||
.option('--change <id>', 'Change name')
|
||||
.option('--schema <name>', 'Schema override (auto-detected from .openspec.yaml)')
|
||||
.option('--schema <name>', `Schema to use (default: ${DEFAULT_SCHEMA})`)
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (artifactId: string | undefined, options: InstructionsOptions) => {
|
||||
try {
|
||||
// Special case: "apply" is not an artifact, but a command to get apply instructions
|
||||
if (artifactId === 'apply') {
|
||||
await applyInstructionsCommand(options);
|
||||
} else {
|
||||
await instructionsCommand(artifactId, options);
|
||||
}
|
||||
await instructionsCommand(artifactId, options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
@@ -996,21 +521,6 @@ export function registerArtifactWorkflowCommands(program: Command): void {
|
||||
}
|
||||
});
|
||||
|
||||
// Schemas command
|
||||
program
|
||||
.command('schemas')
|
||||
.description('[Experimental] List available workflow schemas with descriptions')
|
||||
.option('--json', 'Output as JSON (for agent use)')
|
||||
.action(async (options: SchemasOptions) => {
|
||||
try {
|
||||
await schemasCommand(options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// New command group with change subcommand
|
||||
const newCmd = program.command('new').description('[Experimental] Create new items');
|
||||
|
||||
@@ -1018,7 +528,6 @@ export function registerArtifactWorkflowCommands(program: Command): void {
|
||||
.command('change <name>')
|
||||
.description('[Experimental] Create a new change directory')
|
||||
.option('--description <text>', 'Description to add to README.md')
|
||||
.option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`)
|
||||
.action(async (name: string, options: NewChangeOptions) => {
|
||||
try {
|
||||
await newChangeCommand(name, options);
|
||||
@@ -1028,18 +537,4 @@ export function registerArtifactWorkflowCommands(program: Command): void {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Artifact experimental setup command
|
||||
program
|
||||
.command('artifact-experimental-setup')
|
||||
.description('[Experimental] Setup Agent Skills for the experimental artifact workflow')
|
||||
.action(async () => {
|
||||
try {
|
||||
await artifactExperimentalSetupCommand();
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,12 +21,10 @@ export { detectCompleted } from './state.js';
|
||||
export {
|
||||
resolveSchema,
|
||||
listSchemas,
|
||||
listSchemasWithInfo,
|
||||
getSchemaDir,
|
||||
getPackageSchemasDir,
|
||||
getUserSchemasDir,
|
||||
SchemaLoadError,
|
||||
type SchemaInfo,
|
||||
} from './resolver.js';
|
||||
|
||||
// Instruction loading
|
||||
@@ -38,7 +36,7 @@ export {
|
||||
TemplateLoadError,
|
||||
type ChangeContext,
|
||||
type ArtifactInstructions,
|
||||
type DependencyInfo,
|
||||
type DependencyStatus,
|
||||
type ArtifactStatus,
|
||||
type ChangeStatus,
|
||||
} from './instruction-loader.js';
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as path from 'node:path';
|
||||
import { getSchemaDir, resolveSchema } from './resolver.js';
|
||||
import { ArtifactGraph } from './graph.js';
|
||||
import { detectCompleted } from './state.js';
|
||||
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
|
||||
import type { Artifact, CompletedSet } from './types.js';
|
||||
|
||||
/**
|
||||
@@ -45,34 +44,26 @@ export interface ArtifactInstructions {
|
||||
artifactId: string;
|
||||
/** Schema name */
|
||||
schemaName: string;
|
||||
/** Full path to change directory */
|
||||
changeDir: string;
|
||||
/** Output path pattern (e.g., "proposal.md") */
|
||||
outputPath: string;
|
||||
/** Artifact description */
|
||||
description: string;
|
||||
/** Guidance on how to create this artifact (from schema instruction field) */
|
||||
instruction: string | undefined;
|
||||
/** Template content (structure to follow) */
|
||||
/** Template content */
|
||||
template: string;
|
||||
/** Dependencies with completion status and paths */
|
||||
dependencies: DependencyInfo[];
|
||||
/** Dependencies with completion status */
|
||||
dependencies: DependencyStatus[];
|
||||
/** Artifacts that become available after completing this one */
|
||||
unlocks: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency information including path and description.
|
||||
* Dependency status information.
|
||||
*/
|
||||
export interface DependencyInfo {
|
||||
export interface DependencyStatus {
|
||||
/** Artifact ID */
|
||||
id: string;
|
||||
/** Whether the dependency is completed */
|
||||
done: boolean;
|
||||
/** Relative output path of the dependency (e.g., "proposal.md") */
|
||||
path: string;
|
||||
/** Description of the dependency artifact */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,8 +90,6 @@ 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[];
|
||||
}
|
||||
@@ -145,34 +134,25 @@ export function loadTemplate(schemaName: string, templatePath: string): string {
|
||||
/**
|
||||
* Loads change context combining graph and completion state.
|
||||
*
|
||||
* Schema resolution order:
|
||||
* 1. Explicit schemaName parameter (if provided)
|
||||
* 2. Schema from .openspec.yaml metadata (if exists in change directory)
|
||||
* 3. Default 'spec-driven'
|
||||
*
|
||||
* @param projectRoot - Project root directory
|
||||
* @param changeName - Change name
|
||||
* @param schemaName - Optional schema name override. If not provided, auto-detected from metadata.
|
||||
* @param schemaName - Optional schema name (defaults to "spec-driven")
|
||||
* @returns Change context with graph, completed set, and metadata
|
||||
*/
|
||||
export function loadChangeContext(
|
||||
projectRoot: string,
|
||||
changeName: string,
|
||||
schemaName?: string
|
||||
schemaName: string = 'spec-driven'
|
||||
): ChangeContext {
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
|
||||
// Resolve schema: explicit > metadata > default
|
||||
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName);
|
||||
|
||||
const schema = resolveSchema(resolvedSchemaName);
|
||||
const schema = resolveSchema(schemaName);
|
||||
const graph = ArtifactGraph.fromSchema(schema);
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
const completed = detectCompleted(graph, changeDir);
|
||||
|
||||
return {
|
||||
graph,
|
||||
completed,
|
||||
schemaName: resolvedSchemaName,
|
||||
schemaName,
|
||||
changeName,
|
||||
changeDir,
|
||||
};
|
||||
@@ -196,17 +176,15 @@ export function generateInstructions(
|
||||
}
|
||||
|
||||
const template = loadTemplate(context.schemaName, artifact.template);
|
||||
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
|
||||
const dependencies = getDependencyStatus(artifact, context.completed);
|
||||
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
||||
|
||||
return {
|
||||
changeName: context.changeName,
|
||||
artifactId: artifact.id,
|
||||
schemaName: context.schemaName,
|
||||
changeDir: context.changeDir,
|
||||
outputPath: artifact.generates,
|
||||
description: artifact.description,
|
||||
instruction: artifact.instruction,
|
||||
template,
|
||||
dependencies,
|
||||
unlocks,
|
||||
@@ -214,22 +192,16 @@ export function generateInstructions(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets dependency info including paths and descriptions.
|
||||
* Gets dependency status for an artifact.
|
||||
*/
|
||||
function getDependencyInfo(
|
||||
function getDependencyStatus(
|
||||
artifact: Artifact,
|
||||
graph: ArtifactGraph,
|
||||
completed: CompletedSet
|
||||
): DependencyInfo[] {
|
||||
return artifact.requires.map(id => {
|
||||
const depArtifact = graph.getArtifact(id);
|
||||
return {
|
||||
id,
|
||||
done: completed.has(id),
|
||||
path: depArtifact?.generates ?? id,
|
||||
description: depArtifact?.description ?? '',
|
||||
};
|
||||
});
|
||||
): DependencyStatus[] {
|
||||
return artifact.requires.map(id => ({
|
||||
id,
|
||||
done: completed.has(id),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,10 +226,6 @@ 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);
|
||||
@@ -296,7 +264,6 @@ export function formatChangeStatus(context: ChangeContext): ChangeStatus {
|
||||
changeName: context.changeName,
|
||||
schemaName: context.schemaName,
|
||||
isComplete: context.graph.isComplete(context.completed),
|
||||
applyRequires,
|
||||
artifacts: artifactStatuses,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,71 +156,3 @@ export function listSchemas(): string[] {
|
||||
|
||||
return Array.from(schemas).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema info with metadata (name, description, artifacts).
|
||||
*/
|
||||
export interface SchemaInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
artifacts: string[];
|
||||
source: 'package' | 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all available schemas with their descriptions and artifact lists.
|
||||
* Useful for agent skills to present schema selection to users.
|
||||
*/
|
||||
export function listSchemasWithInfo(): SchemaInfo[] {
|
||||
const schemas: SchemaInfo[] = [];
|
||||
const seenNames = new Set<string>();
|
||||
|
||||
// Add user override schemas first (they take precedence)
|
||||
const userDir = getUserSchemasDir();
|
||||
if (fs.existsSync(userDir)) {
|
||||
for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
|
||||
if (fs.existsSync(schemaPath)) {
|
||||
try {
|
||||
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
|
||||
schemas.push({
|
||||
name: entry.name,
|
||||
description: schema.description || '',
|
||||
artifacts: schema.artifacts.map((a) => a.id),
|
||||
source: 'user',
|
||||
});
|
||||
seenNames.add(entry.name);
|
||||
} catch {
|
||||
// Skip invalid schemas
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add package built-in schemas (if not overridden)
|
||||
const packageDir = getPackageSchemasDir();
|
||||
if (fs.existsSync(packageDir)) {
|
||||
for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && !seenNames.has(entry.name)) {
|
||||
const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
|
||||
if (fs.existsSync(schemaPath)) {
|
||||
try {
|
||||
const schema = parseSchema(fs.readFileSync(schemaPath, 'utf-8'));
|
||||
schemas.push({
|
||||
name: entry.name,
|
||||
description: schema.description || '',
|
||||
artifacts: schema.artifacts.map((a) => a.id),
|
||||
source: 'package',
|
||||
});
|
||||
} catch {
|
||||
// Skip invalid schemas
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schemas.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
@@ -6,53 +6,21 @@ export const ArtifactSchema = z.object({
|
||||
generates: z.string().min(1, { error: 'generates field is required' }),
|
||||
description: z.string(),
|
||||
template: z.string().min(1, { error: 'template field is required' }),
|
||||
instruction: z.string().optional(),
|
||||
requires: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
// Apply phase configuration for schema-aware apply instructions
|
||||
export const ApplyPhaseSchema = z.object({
|
||||
// Artifact IDs that must exist before apply is available
|
||||
requires: z.array(z.string()).min(1, { error: 'At least one required artifact' }),
|
||||
// Path to file with checkboxes for progress (relative to change dir), or null if no tracking
|
||||
tracks: z.string().nullable().optional(),
|
||||
// Custom guidance for the apply phase
|
||||
instruction: z.string().optional(),
|
||||
});
|
||||
|
||||
// Full schema YAML structure
|
||||
export const SchemaYamlSchema = z.object({
|
||||
name: z.string().min(1, { error: 'Schema name is required' }),
|
||||
version: z.number().int().positive({ error: 'Version must be a positive integer' }),
|
||||
description: z.string().optional(),
|
||||
artifacts: z.array(ArtifactSchema).min(1, { error: 'At least one artifact required' }),
|
||||
// Optional apply phase configuration (for schema-aware apply instructions)
|
||||
apply: ApplyPhaseSchema.optional(),
|
||||
});
|
||||
|
||||
// Derived TypeScript types
|
||||
export type Artifact = z.infer<typeof ArtifactSchema>;
|
||||
export type ApplyPhase = z.infer<typeof ApplyPhaseSchema>;
|
||||
export type SchemaYaml = z.infer<typeof SchemaYamlSchema>;
|
||||
|
||||
// Per-change metadata schema
|
||||
// Note: schema field is validated at parse time against available schemas
|
||||
// using a lazy import to avoid circular dependencies
|
||||
export const ChangeMetadataSchema = z.object({
|
||||
// Required: which workflow schema this change uses
|
||||
schema: z.string().min(1, { message: 'schema is required' }),
|
||||
|
||||
// Optional: creation timestamp (ISO date string)
|
||||
created: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/, {
|
||||
message: 'created must be YYYY-MM-DD format',
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>;
|
||||
|
||||
// Runtime state types (not Zod - internal only)
|
||||
|
||||
// Slice 1: Simple completion tracking via filesystem
|
||||
|
||||
+8
-98
@@ -9,78 +9,13 @@ interface ChangeInfo {
|
||||
name: string;
|
||||
completedTasks: number;
|
||||
totalTasks: number;
|
||||
lastModified: Date;
|
||||
}
|
||||
|
||||
interface ListOptions {
|
||||
sort?: 'recent' | 'name';
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent modification time of any file in a directory (recursive).
|
||||
* Falls back to the directory's own mtime if no files are found.
|
||||
*/
|
||||
async function getLastModified(dirPath: string): Promise<Date> {
|
||||
let latest: Date | null = null;
|
||||
|
||||
async function walk(dir: string): Promise<void> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else {
|
||||
const stat = await fs.stat(fullPath);
|
||||
if (latest === null || stat.mtime > latest) {
|
||||
latest = stat.mtime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dirPath);
|
||||
|
||||
// If no files found, use the directory's own modification time
|
||||
if (latest === null) {
|
||||
const dirStat = await fs.stat(dirPath);
|
||||
return dirStat.mtime;
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date as relative time (e.g., "2 hours ago", "3 days ago")
|
||||
*/
|
||||
function formatRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffSecs = Math.floor(diffMs / 1000);
|
||||
const diffMins = Math.floor(diffSecs / 60);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 30) {
|
||||
return date.toLocaleDateString();
|
||||
} else if (diffDays > 0) {
|
||||
return `${diffDays}d ago`;
|
||||
} else if (diffHours > 0) {
|
||||
return `${diffHours}h ago`;
|
||||
} else if (diffMins > 0) {
|
||||
return `${diffMins}m ago`;
|
||||
} else {
|
||||
return 'just now';
|
||||
}
|
||||
}
|
||||
|
||||
export class ListCommand {
|
||||
async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes', options: ListOptions = {}): Promise<void> {
|
||||
const { sort = 'recent', json = false } = options;
|
||||
|
||||
async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes'): Promise<void> {
|
||||
if (mode === 'changes') {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
|
||||
|
||||
// Check if changes directory exists
|
||||
try {
|
||||
await fs.access(changesDir);
|
||||
@@ -95,48 +30,24 @@ export class ListCommand {
|
||||
.map(entry => entry.name);
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
if (json) {
|
||||
console.log(JSON.stringify({ changes: [] }));
|
||||
} else {
|
||||
console.log('No active changes found.');
|
||||
}
|
||||
console.log('No active changes found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect information about each change
|
||||
const changes: ChangeInfo[] = [];
|
||||
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, changeDir);
|
||||
const changePath = path.join(changesDir, changeDir);
|
||||
const lastModified = await getLastModified(changePath);
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
completedTasks: progress.completed,
|
||||
totalTasks: progress.total,
|
||||
lastModified
|
||||
totalTasks: progress.total
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by preference (default: recent first)
|
||||
if (sort === 'recent') {
|
||||
changes.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
||||
} else {
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
// JSON output for programmatic use
|
||||
if (json) {
|
||||
const jsonOutput = changes.map(c => ({
|
||||
name: c.name,
|
||||
completedTasks: c.completedTasks,
|
||||
totalTasks: c.totalTasks,
|
||||
lastModified: c.lastModified.toISOString(),
|
||||
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress'
|
||||
}));
|
||||
console.log(JSON.stringify({ changes: jsonOutput }, null, 2));
|
||||
return;
|
||||
}
|
||||
// Sort alphabetically by name
|
||||
changes.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Display results
|
||||
console.log('Changes:');
|
||||
@@ -145,8 +56,7 @@ export class ListCommand {
|
||||
for (const change of changes) {
|
||||
const paddedName = change.name.padEnd(nameWidth);
|
||||
const status = formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
|
||||
const timeAgo = formatRelativeTime(change.lastModified);
|
||||
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
|
||||
console.log(`${padding}${paddedName} ${status}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,913 +0,0 @@
|
||||
/**
|
||||
* Agent Skill Templates
|
||||
*
|
||||
* Templates for generating Agent Skills compatible with:
|
||||
* - Claude Code
|
||||
* - Cursor (Settings → Rules → Import Settings)
|
||||
* - Windsurf
|
||||
* - Other Agent Skills-compatible editors
|
||||
*/
|
||||
|
||||
export interface SkillTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-new-change skill
|
||||
* Based on /opsx:new command
|
||||
*/
|
||||
export function getNewChangeSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-new-change',
|
||||
description: 'Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.',
|
||||
instructions: `Start a new change using the experimental artifact-driven approach.
|
||||
|
||||
**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. **Select a workflow schema**
|
||||
|
||||
Run \`openspec schemas --json\` to get available schemas with descriptions.
|
||||
|
||||
Use the **AskUserQuestion tool** to let the user choose a workflow:
|
||||
- Present each schema with its description
|
||||
- Mark \`spec-driven\` as "(default)" if it's available
|
||||
- Example options: "spec-driven - proposal → specs → design → tasks (default)", "tdd - tests → implementation → docs"
|
||||
|
||||
If user doesn't have a preference, default to \`spec-driven\`.
|
||||
|
||||
3. **Create the change directory**
|
||||
\`\`\`bash
|
||||
openspec new change "<name>" --schema "<selected-schema>"
|
||||
\`\`\`
|
||||
This creates a scaffolded change at \`openspec/changes/<name>/\` with the selected schema.
|
||||
|
||||
4. **Show the artifact status**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>"
|
||||
\`\`\`
|
||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||
|
||||
5. **Get instructions for the first artifact**
|
||||
The first artifact depends on the schema (e.g., \`proposal\` for spec-driven, \`spec\` for tdd).
|
||||
Check the status output to find the first artifact with status "ready".
|
||||
\`\`\`bash
|
||||
openspec instructions <first-artifact-id> --change "<name>"
|
||||
\`\`\`
|
||||
This outputs the template and context for creating the first artifact.
|
||||
|
||||
6. **STOP and wait for user direction**
|
||||
|
||||
**Output**
|
||||
|
||||
After completing the steps, summarize:
|
||||
- Change name and location
|
||||
- Selected schema/workflow and its artifact sequence
|
||||
- Current status (0/N artifacts complete)
|
||||
- The template for the first artifact
|
||||
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
|
||||
|
||||
**Guardrails**
|
||||
- Do NOT create any artifacts yet - just show the instructions
|
||||
- Do NOT advance beyond showing the first artifact template
|
||||
- If the name is invalid (not kebab-case), ask for a valid name
|
||||
- If a change with that name already exists, suggest continuing that change instead
|
||||
- Always pass --schema to preserve the user's workflow choice`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-continue-change skill
|
||||
* Based on /opsx:continue command
|
||||
*/
|
||||
export function getContinueChangeSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-continue-change',
|
||||
description: 'Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.',
|
||||
instructions: `Continue working on a change by creating the next artifact.
|
||||
|
||||
**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 sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||
|
||||
Present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Schema (from \`schema\` field if present, otherwise "spec-driven")
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from \`lastModified\` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check current status**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>" --json
|
||||
\`\`\`
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven", "tdd")
|
||||
- \`artifacts\`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||
- \`isComplete\`: Boolean indicating if all artifacts are complete
|
||||
|
||||
3. **Act based on status**:
|
||||
|
||||
---
|
||||
|
||||
**If all artifacts are complete (\`isComplete: true\`)**:
|
||||
- Congratulate the user
|
||||
- Show final status including the schema used
|
||||
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||
- STOP
|
||||
|
||||
---
|
||||
|
||||
**If artifacts are ready to create** (status shows artifacts with \`status: "ready"\`):
|
||||
- Pick the FIRST artifact with \`status: "ready"\` from the status output
|
||||
- Get its instructions:
|
||||
\`\`\`bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
\`\`\`
|
||||
- Parse the JSON to get template, dependencies, and what it unlocks
|
||||
- **Create the artifact file** using the template as a starting point:
|
||||
- Read any completed dependency files for context
|
||||
- Fill in the template based on context and user's goals
|
||||
- Write to the output path specified in instructions
|
||||
- Show what was created and what's now unlocked
|
||||
- STOP after creating ONE artifact
|
||||
|
||||
---
|
||||
|
||||
**If no artifacts are ready (all blocked)**:
|
||||
- This shouldn't happen with a valid schema
|
||||
- Show status and suggest checking for issues
|
||||
|
||||
4. **After creating an artifact, show progress**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>"
|
||||
\`\`\`
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifact was created
|
||||
- Schema workflow being used
|
||||
- Current progress (N/M complete)
|
||||
- What artifacts are now unlocked
|
||||
- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
The artifact types and their purpose depend on the schema. Use the \`instruction\` field from the instructions output to understand what to create.
|
||||
|
||||
Common artifact patterns:
|
||||
|
||||
**spec-driven schema** (proposal → specs → design → tasks):
|
||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||
- **specs/*.md**: Create one spec per capability listed in the proposal.
|
||||
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||
|
||||
**tdd schema** (spec → tests → implementation → docs):
|
||||
- **spec.md**: Feature specification defining what to build.
|
||||
- **tests/*.test.ts**: Write tests BEFORE implementation (TDD red phase).
|
||||
- **src/*.ts**: Implement to make tests pass (TDD green phase).
|
||||
- **docs/*.md**: Document the implemented feature.
|
||||
|
||||
For other schemas, follow the \`instruction\` field from the CLI output.
|
||||
|
||||
**Guardrails**
|
||||
- Create ONE artifact per invocation
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- Never skip artifacts or create out of order
|
||||
- If context is unclear, ask the user before creating
|
||||
- Verify the artifact file exists after writing before marking progress
|
||||
- Use the schema's artifact sequence, don't assume specific artifact names`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for openspec-apply-change skill
|
||||
* For implementing tasks from a completed (or in-progress) change
|
||||
*/
|
||||
export function getApplyChangeSkillTemplate(): SkillTemplate {
|
||||
return {
|
||||
name: 'openspec-apply-change',
|
||||
description: 'Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.',
|
||||
instructions: `Implement tasks from an OpenSpec change.
|
||||
|
||||
**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 are implementation-ready (have tasks artifact).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>" --json
|
||||
\`\`\`
|
||||
Parse the JSON to understand:
|
||||
- \`schemaName\`: The workflow being used (e.g., "spec-driven", "tdd")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
\`\`\`bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
\`\`\`
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If \`state: "blocked"\` (missing artifacts): show message, suggest using openspec-continue-change
|
||||
- If \`state: "all_done"\`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in \`contextFiles\` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- **tdd**: spec, tests, implementation, docs
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
\`\`\`
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
\`\`\`
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
\`\`\`
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
\`\`\`
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
\`\`\`
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Slash Command Templates
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export interface CommandTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
tags: string[];
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:new slash command
|
||||
*/
|
||||
export function getOpsxNewCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: New',
|
||||
description: 'Start a new change using the experimental artifact workflow (OPSX)',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'artifacts', 'experimental'],
|
||||
content: `Start a new change using the experimental artifact-driven approach.
|
||||
|
||||
**Input**: The argument after \`/opsx:new\` 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. **Select a workflow schema**
|
||||
|
||||
Run \`openspec schemas --json\` to get available schemas with descriptions.
|
||||
|
||||
Use the **AskUserQuestion tool** to let the user choose a workflow:
|
||||
- Present each schema with its description
|
||||
- Mark \`spec-driven\` as "(default)" if it's available
|
||||
- Example options: "spec-driven - proposal → specs → design → tasks (default)", "tdd - tests → implementation → docs"
|
||||
|
||||
If user doesn't have a preference, default to \`spec-driven\`.
|
||||
|
||||
3. **Create the change directory**
|
||||
\`\`\`bash
|
||||
openspec new change "<name>" --schema "<selected-schema>"
|
||||
\`\`\`
|
||||
This creates a scaffolded change at \`openspec/changes/<name>/\` with the selected schema.
|
||||
|
||||
4. **Show the artifact status**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>"
|
||||
\`\`\`
|
||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||
|
||||
5. **Get instructions for the first artifact**
|
||||
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
||||
\`\`\`bash
|
||||
openspec instructions <first-artifact-id> --change "<name>"
|
||||
\`\`\`
|
||||
This outputs the template and context for creating the first artifact.
|
||||
|
||||
6. **STOP and wait for user direction**
|
||||
|
||||
**Output**
|
||||
|
||||
After completing the steps, summarize:
|
||||
- Change name and location
|
||||
- Selected schema/workflow and its artifact sequence
|
||||
- Current status (0/N artifacts complete)
|
||||
- The template for the first artifact
|
||||
- Prompt: "Ready to create the first artifact? Run \`/opsx:continue\` or just describe what this change is about and I'll draft it."
|
||||
|
||||
**Guardrails**
|
||||
- Do NOT create any artifacts yet - just show the instructions
|
||||
- Do NOT advance beyond showing the first artifact template
|
||||
- If the name is invalid (not kebab-case), ask for a valid name
|
||||
- If a change with that name already exists, suggest using \`/opsx:continue\` instead
|
||||
- Always pass --schema to preserve the user's workflow choice`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:continue slash command
|
||||
*/
|
||||
export function getOpsxContinueCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: Continue',
|
||||
description: 'Continue working on a change - create the next artifact (Experimental)',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'artifacts', 'experimental'],
|
||||
content: `Continue working on a change by creating the next artifact.
|
||||
|
||||
**Input**: Optionally specify \`--change <name>\` after \`/opsx:continue\`. 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 sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
|
||||
|
||||
Present the top 3-4 most recently modified changes as options, showing:
|
||||
- Change name
|
||||
- Schema (from \`schema\` field if present, otherwise "spec-driven")
|
||||
- Status (e.g., "0/5 tasks", "complete", "no tasks")
|
||||
- How recently it was modified (from \`lastModified\` field)
|
||||
|
||||
Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check current status**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>" --json
|
||||
\`\`\`
|
||||
Parse the JSON to understand current state. The response includes:
|
||||
- \`schemaName\`: The workflow schema being used (e.g., "spec-driven", "tdd")
|
||||
- \`artifacts\`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||
- \`isComplete\`: Boolean indicating if all artifacts are complete
|
||||
|
||||
3. **Act based on status**:
|
||||
|
||||
---
|
||||
|
||||
**If all artifacts are complete (\`isComplete: true\`)**:
|
||||
- Congratulate the user
|
||||
- Show final status including the schema used
|
||||
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||
- STOP
|
||||
|
||||
---
|
||||
|
||||
**If artifacts are ready to create** (status shows artifacts with \`status: "ready"\`):
|
||||
- Pick the FIRST artifact with \`status: "ready"\` from the status output
|
||||
- Get its instructions:
|
||||
\`\`\`bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
\`\`\`
|
||||
- Parse the JSON to get template, dependencies, and what it unlocks
|
||||
- **Create the artifact file** using the template as a starting point:
|
||||
- Read any completed dependency files for context
|
||||
- Fill in the template based on context and user's goals
|
||||
- Write to the output path specified in instructions
|
||||
- Show what was created and what's now unlocked
|
||||
- STOP after creating ONE artifact
|
||||
|
||||
---
|
||||
|
||||
**If no artifacts are ready (all blocked)**:
|
||||
- This shouldn't happen with a valid schema
|
||||
- Show status and suggest checking for issues
|
||||
|
||||
4. **After creating an artifact, show progress**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>"
|
||||
\`\`\`
|
||||
|
||||
**Output**
|
||||
|
||||
After each invocation, show:
|
||||
- Which artifact was created
|
||||
- Schema workflow being used
|
||||
- Current progress (N/M complete)
|
||||
- What artifacts are now unlocked
|
||||
- Prompt: "Run \`/opsx:continue\` to create the next artifact"
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
The artifact types and their purpose depend on the schema. Use the \`instruction\` field from the instructions output to understand what to create.
|
||||
|
||||
Common artifact patterns:
|
||||
|
||||
**spec-driven schema** (proposal → specs → design → tasks):
|
||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||
- **specs/*.md**: Create one spec per capability listed in the proposal.
|
||||
- **design.md**: Document technical decisions, architecture, and implementation approach.
|
||||
- **tasks.md**: Break down implementation into checkboxed tasks.
|
||||
|
||||
**tdd schema** (spec → tests → implementation → docs):
|
||||
- **spec.md**: Feature specification defining what to build.
|
||||
- **tests/*.test.ts**: Write tests BEFORE implementation (TDD red phase).
|
||||
- **src/*.ts**: Implement to make tests pass (TDD green phase).
|
||||
- **docs/*.md**: Document the implemented feature.
|
||||
|
||||
For other schemas, follow the \`instruction\` field from the CLI output.
|
||||
|
||||
**Guardrails**
|
||||
- Create ONE artifact per invocation
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- Never skip artifacts or create out of order
|
||||
- If context is unclear, ask the user before creating
|
||||
- Verify the artifact file exists after writing before marking progress
|
||||
- Use the schema's artifact sequence, don't assume specific artifact names`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for /opsx:apply slash command
|
||||
*/
|
||||
export function getOpsxApplyCommandTemplate(): CommandTemplate {
|
||||
return {
|
||||
name: 'OPSX: Apply',
|
||||
description: 'Implement tasks from an OpenSpec change (Experimental)',
|
||||
category: 'Workflow',
|
||||
tags: ['workflow', 'artifacts', 'experimental'],
|
||||
content: `Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify \`--change <name>\` after \`/opsx:apply\`. 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 are implementation-ready (have tasks artifact).
|
||||
Include the schema used for each change if available.
|
||||
Mark changes with incomplete tasks as "(In Progress)".
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
\`\`\`bash
|
||||
openspec status --change "<name>" --json
|
||||
\`\`\`
|
||||
Parse the JSON to understand:
|
||||
- \`schemaName\`: The workflow being used (e.g., "spec-driven", "tdd")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
\`\`\`bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
\`\`\`
|
||||
|
||||
This returns:
|
||||
- Context file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If \`state: "blocked"\` (missing artifacts): show message, suggest using \`/opsx:continue\`
|
||||
- If \`state: "all_done"\`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read the files listed in \`contextFiles\` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- **tdd**: spec, tests, implementation, docs
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
\`\`\`
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
\`\`\`
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
\`\`\`
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
\`\`\`
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
\`\`\`
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
\`\`\`
|
||||
|
||||
**Guardrails**
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **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`
|
||||
};
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as yaml from 'yaml';
|
||||
import { ChangeMetadataSchema, type ChangeMetadata } from '../core/artifact-graph/types.js';
|
||||
import { listSchemas } from '../core/artifact-graph/resolver.js';
|
||||
|
||||
const METADATA_FILENAME = '.openspec.yaml';
|
||||
|
||||
/**
|
||||
* Error thrown when change metadata validation fails.
|
||||
*/
|
||||
export class ChangeMetadataError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly metadataPath: string,
|
||||
public readonly cause?: Error
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ChangeMetadataError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a schema name is valid (exists in available schemas).
|
||||
*
|
||||
* @param schemaName - The schema name to validate
|
||||
* @returns The validated schema name
|
||||
* @throws Error if schema is not found
|
||||
*/
|
||||
export function validateSchemaName(schemaName: string): string {
|
||||
const availableSchemas = listSchemas();
|
||||
if (!availableSchemas.includes(schemaName)) {
|
||||
throw new Error(
|
||||
`Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`
|
||||
);
|
||||
}
|
||||
return schemaName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes change metadata to .openspec.yaml in the change directory.
|
||||
*
|
||||
* @param changeDir - The path to the change directory
|
||||
* @param metadata - The metadata to write
|
||||
* @throws ChangeMetadataError if validation fails or write fails
|
||||
*/
|
||||
export function writeChangeMetadata(
|
||||
changeDir: string,
|
||||
metadata: ChangeMetadata
|
||||
): void {
|
||||
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
||||
|
||||
// Validate schema exists
|
||||
validateSchemaName(metadata.schema);
|
||||
|
||||
// Validate with Zod
|
||||
const parseResult = ChangeMetadataSchema.safeParse(metadata);
|
||||
if (!parseResult.success) {
|
||||
throw new ChangeMetadataError(
|
||||
`Invalid metadata: ${parseResult.error.message}`,
|
||||
metaPath
|
||||
);
|
||||
}
|
||||
|
||||
// Write YAML file
|
||||
const content = yaml.stringify(parseResult.data);
|
||||
try {
|
||||
fs.writeFileSync(metaPath, content, 'utf-8');
|
||||
} catch (err) {
|
||||
const ioError = err instanceof Error ? err : new Error(String(err));
|
||||
throw new ChangeMetadataError(
|
||||
`Failed to write metadata: ${ioError.message}`,
|
||||
metaPath,
|
||||
ioError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads change metadata from .openspec.yaml in the change directory.
|
||||
*
|
||||
* @param changeDir - The path to the change directory
|
||||
* @returns The validated metadata, or null if no metadata file exists
|
||||
* @throws ChangeMetadataError if the file exists but is invalid
|
||||
*/
|
||||
export function readChangeMetadata(changeDir: string): ChangeMetadata | null {
|
||||
const metaPath = path.join(changeDir, METADATA_FILENAME);
|
||||
|
||||
if (!fs.existsSync(metaPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(metaPath, 'utf-8');
|
||||
} catch (err) {
|
||||
const ioError = err instanceof Error ? err : new Error(String(err));
|
||||
throw new ChangeMetadataError(
|
||||
`Failed to read metadata: ${ioError.message}`,
|
||||
metaPath,
|
||||
ioError
|
||||
);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = yaml.parse(content);
|
||||
} catch (err) {
|
||||
const parseError = err instanceof Error ? err : new Error(String(err));
|
||||
throw new ChangeMetadataError(
|
||||
`Invalid YAML in metadata file: ${parseError.message}`,
|
||||
metaPath,
|
||||
parseError
|
||||
);
|
||||
}
|
||||
|
||||
// Validate with Zod
|
||||
const parseResult = ChangeMetadataSchema.safeParse(parsed);
|
||||
if (!parseResult.success) {
|
||||
throw new ChangeMetadataError(
|
||||
`Invalid metadata: ${parseResult.error.message}`,
|
||||
metaPath
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that the schema exists
|
||||
const availableSchemas = listSchemas();
|
||||
if (!availableSchemas.includes(parseResult.data.schema)) {
|
||||
throw new ChangeMetadataError(
|
||||
`Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`,
|
||||
metaPath
|
||||
);
|
||||
}
|
||||
|
||||
return parseResult.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the schema for a change, with explicit override taking precedence.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Explicit schema (if provided)
|
||||
* 2. Schema from .openspec.yaml metadata (if exists)
|
||||
* 3. Default 'spec-driven'
|
||||
*
|
||||
* @param changeDir - The path to the change directory
|
||||
* @param explicitSchema - Optional explicit schema override
|
||||
* @returns The resolved schema name
|
||||
*/
|
||||
export function resolveSchemaForChange(
|
||||
changeDir: string,
|
||||
explicitSchema?: string
|
||||
): string {
|
||||
// 1. Explicit override wins
|
||||
if (explicitSchema) {
|
||||
return explicitSchema;
|
||||
}
|
||||
|
||||
// 2. Try reading from metadata
|
||||
try {
|
||||
const metadata = readChangeMetadata(changeDir);
|
||||
if (metadata?.schema) {
|
||||
return metadata.schema;
|
||||
}
|
||||
} catch {
|
||||
// If metadata read fails, fall back to default
|
||||
}
|
||||
|
||||
// 3. Default
|
||||
return 'spec-driven';
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from './file-system.js';
|
||||
import { writeChangeMetadata, validateSchemaName } from './change-metadata.js';
|
||||
|
||||
const DEFAULT_SCHEMA = 'spec-driven';
|
||||
|
||||
/**
|
||||
* Options for creating a change.
|
||||
*/
|
||||
export interface CreateChangeOptions {
|
||||
/** The workflow schema to use (default: 'spec-driven') */
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of validating a change name.
|
||||
@@ -79,27 +68,20 @@ export function validateChangeName(name: string): ValidationResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new change directory with metadata file.
|
||||
* Creates a new change directory.
|
||||
*
|
||||
* @param projectRoot - The root directory of the project (where `openspec/` lives)
|
||||
* @param name - The change name (must be valid kebab-case)
|
||||
* @param options - Optional settings for the change
|
||||
* @throws Error if the change name is invalid
|
||||
* @throws Error if the schema name is invalid
|
||||
* @throws Error if the change directory already exists
|
||||
*
|
||||
* @example
|
||||
* // Creates openspec/changes/add-auth/ with default schema
|
||||
* // Creates openspec/changes/add-auth/
|
||||
* await createChange('/path/to/project', 'add-auth')
|
||||
*
|
||||
* @example
|
||||
* // Creates openspec/changes/add-auth/ with TDD schema
|
||||
* await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
|
||||
*/
|
||||
export async function createChange(
|
||||
projectRoot: string,
|
||||
name: string,
|
||||
options: CreateChangeOptions = {}
|
||||
name: string
|
||||
): Promise<void> {
|
||||
// Validate the name first
|
||||
const validation = validateChangeName(name);
|
||||
@@ -107,10 +89,6 @@ export async function createChange(
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
|
||||
// Determine schema (validate if provided)
|
||||
const schemaName = options.schema ?? DEFAULT_SCHEMA;
|
||||
validateSchemaName(schemaName);
|
||||
|
||||
// Build the change directory path
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', name);
|
||||
|
||||
@@ -121,11 +99,4 @@ export async function createChange(
|
||||
|
||||
// Create the directory (including parent directories if needed)
|
||||
await FileSystemUtils.createDirectory(changeDir);
|
||||
|
||||
// Write metadata file with schema and creation date
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
writeChangeMetadata(changeDir, {
|
||||
schema: schemaName,
|
||||
created: today,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-10
@@ -1,12 +1,3 @@
|
||||
// Shared utilities
|
||||
export { validateChangeName, createChange } from './change-utils.js';
|
||||
export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
|
||||
|
||||
// Change metadata utilities
|
||||
export {
|
||||
readChangeMetadata,
|
||||
writeChangeMetadata,
|
||||
resolveSchemaForChange,
|
||||
validateSchemaName,
|
||||
ChangeMetadataError,
|
||||
} from './change-metadata.js';
|
||||
export type { ValidationResult } from './change-utils.js';
|
||||
@@ -187,6 +187,68 @@ describe('artifact-workflow CLI commands', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('next command', () => {
|
||||
it('shows proposal as next for scaffolded change', async () => {
|
||||
// Create empty change directory (no proposal.md)
|
||||
const changeDir = path.join(changesDir, 'scaffolded-change');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
|
||||
const result = await runCLI(['next', '--change', 'scaffolded-change'], { cwd: tempDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Artifacts ready to create');
|
||||
expect(result.stdout).toContain('proposal');
|
||||
});
|
||||
|
||||
it('shows design and specs as next when proposal exists', async () => {
|
||||
// createTestChange always creates proposal.md, so design and specs are ready
|
||||
await createTestChange('minimal-change');
|
||||
|
||||
const result = await runCLI(['next', '--change', 'minimal-change'], { cwd: tempDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Artifacts ready to create');
|
||||
expect(result.stdout).toContain('design');
|
||||
expect(result.stdout).toContain('specs');
|
||||
});
|
||||
|
||||
it('shows tasks as next after proposal, design, and specs', async () => {
|
||||
await createTestChange('after-specs', ['proposal', 'design', 'specs']);
|
||||
|
||||
const result = await runCLI(['next', '--change', 'after-specs'], { cwd: tempDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('tasks');
|
||||
});
|
||||
|
||||
it('shows complete message when all done', async () => {
|
||||
await createTestChange('complete-change', ['proposal', 'design', 'specs', 'tasks']);
|
||||
|
||||
const result = await runCLI(['next', '--change', 'complete-change'], { cwd: tempDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('All artifacts are complete!');
|
||||
});
|
||||
|
||||
it('outputs JSON array of ready artifacts', async () => {
|
||||
await createTestChange('json-next', ['proposal']);
|
||||
|
||||
const result = await runCLI(['next', '--change', 'json-next', '--json'], { cwd: tempDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
expect(Array.isArray(json)).toBe(true);
|
||||
expect(json).toContain('design');
|
||||
expect(json).toContain('specs');
|
||||
});
|
||||
|
||||
it('errors when --change is missing and lists available changes', async () => {
|
||||
await createTestChange('some-change');
|
||||
|
||||
const result = await runCLI(['next'], { cwd: tempDir });
|
||||
expect(result.exitCode).toBe(1);
|
||||
const output = getOutput(result);
|
||||
expect(output).toContain('Missing required option --change');
|
||||
expect(output).toContain('some-change');
|
||||
});
|
||||
});
|
||||
|
||||
describe('instructions command', () => {
|
||||
it('shows instructions for proposal on scaffolded change', async () => {
|
||||
// Create empty change directory (no proposal.md)
|
||||
@@ -197,9 +259,9 @@ describe('artifact-workflow CLI commands', () => {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('<artifact id="proposal"');
|
||||
expect(result.stdout).toContain('Artifact: proposal');
|
||||
expect(result.stdout).toContain('proposal.md');
|
||||
expect(result.stdout).toContain('<template>');
|
||||
expect(result.stdout).toContain('Template:');
|
||||
});
|
||||
|
||||
it('shows instructions for design artifact', async () => {
|
||||
@@ -209,9 +271,9 @@ describe('artifact-workflow CLI commands', () => {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('<artifact id="design"');
|
||||
expect(result.stdout).toContain('Artifact: design');
|
||||
expect(result.stdout).toContain('design.md');
|
||||
expect(result.stdout).toContain('<template>');
|
||||
expect(result.stdout).toContain('Template:');
|
||||
});
|
||||
|
||||
it('shows blocked warning for artifact with unmet dependencies', async () => {
|
||||
@@ -222,8 +284,8 @@ describe('artifact-workflow CLI commands', () => {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('<warning>');
|
||||
expect(result.stdout).toContain('status="missing"');
|
||||
expect(result.stdout).toContain('Warning: This artifact has unmet dependencies');
|
||||
expect(result.stdout).toContain('[missing]');
|
||||
});
|
||||
|
||||
it('outputs JSON for instructions', async () => {
|
||||
@@ -348,209 +410,6 @@ describe('artifact-workflow CLI commands', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('instructions apply command', () => {
|
||||
it('shows apply instructions for spec-driven schema with tasks', async () => {
|
||||
await createTestChange('apply-change', ['proposal', 'design', 'specs', 'tasks']);
|
||||
|
||||
const result = await runCLI(['instructions', 'apply', '--change', 'apply-change'], {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('## Apply: apply-change');
|
||||
expect(result.stdout).toContain('Schema: spec-driven');
|
||||
expect(result.stdout).toContain('### Context Files');
|
||||
expect(result.stdout).toContain('### Instruction');
|
||||
});
|
||||
|
||||
it('shows blocked state when required artifacts are missing', async () => {
|
||||
// Only create proposal - missing tasks (required by spec-driven apply block)
|
||||
await createTestChange('blocked-apply', ['proposal']);
|
||||
|
||||
const result = await runCLI(['instructions', 'apply', '--change', 'blocked-apply'], {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Blocked');
|
||||
expect(result.stdout).toContain('Missing artifacts: tasks');
|
||||
});
|
||||
|
||||
it('outputs JSON for apply instructions', async () => {
|
||||
await createTestChange('json-apply', ['proposal', 'design', 'specs', 'tasks']);
|
||||
|
||||
const result = await runCLI(
|
||||
['instructions', 'apply', '--change', 'json-apply', '--json'],
|
||||
{ cwd: tempDir }
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
expect(json.changeName).toBe('json-apply');
|
||||
expect(json.schemaName).toBe('spec-driven');
|
||||
expect(json.state).toBe('ready');
|
||||
expect(json.contextFiles).toBeDefined();
|
||||
expect(typeof json.contextFiles).toBe('object');
|
||||
});
|
||||
|
||||
it('shows schema instruction from apply block', async () => {
|
||||
await createTestChange('instr-apply', ['proposal', 'design', 'specs', 'tasks']);
|
||||
|
||||
const result = await runCLI(['instructions', 'apply', '--change', 'instr-apply'], {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Should show the instruction from spec-driven schema apply block
|
||||
expect(result.stdout).toContain('work through pending tasks');
|
||||
});
|
||||
|
||||
it('shows all_done state when all tasks are complete', async () => {
|
||||
const changeDir = await createTestChange('done-apply', [
|
||||
'proposal',
|
||||
'design',
|
||||
'specs',
|
||||
'tasks',
|
||||
]);
|
||||
// Overwrite tasks with all completed
|
||||
await fs.writeFile(
|
||||
path.join(changeDir, 'tasks.md'),
|
||||
'## Tasks\n- [x] Task 1\n- [x] Task 2'
|
||||
);
|
||||
|
||||
const result = await runCLI(['instructions', 'apply', '--change', 'done-apply'], {
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('complete ✓');
|
||||
expect(result.stdout).toContain('ready to be archived');
|
||||
});
|
||||
|
||||
it('uses tdd schema apply configuration', async () => {
|
||||
// Create a TDD-style change with spec and tests
|
||||
const changeDir = path.join(changesDir, 'tdd-apply');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
await fs.writeFile(path.join(changeDir, 'spec.md'), '## Feature\nTest spec.');
|
||||
const testsDir = path.join(changeDir, 'tests');
|
||||
await fs.mkdir(testsDir, { recursive: true });
|
||||
await fs.writeFile(path.join(testsDir, 'test.test.ts'), 'test("works", () => {})');
|
||||
|
||||
const result = await runCLI(
|
||||
['instructions', 'apply', '--change', 'tdd-apply', '--schema', 'tdd'],
|
||||
{ cwd: tempDir }
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Schema: tdd');
|
||||
// TDD schema has no task tracking, so should show schema instruction
|
||||
expect(result.stdout).toContain('Run tests to see failures');
|
||||
});
|
||||
|
||||
it('spec-driven schema uses apply block configuration', async () => {
|
||||
// Verify that spec-driven schema uses its apply block (requires: [tasks])
|
||||
await createTestChange('apply-config-test', ['proposal', 'design', 'specs', 'tasks']);
|
||||
|
||||
const result = await runCLI(
|
||||
['instructions', 'apply', '--change', 'apply-config-test', '--json'],
|
||||
{ cwd: tempDir }
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
// spec-driven schema has apply block with requires: [tasks], so should be ready
|
||||
expect(json.schemaName).toBe('spec-driven');
|
||||
expect(json.state).toBe('ready');
|
||||
});
|
||||
|
||||
it('fallback: requires all artifacts when schema has no apply block', async () => {
|
||||
// Create a minimal schema without an apply block in user schemas dir
|
||||
const userDataDir = path.join(tempDir, 'user-data');
|
||||
const noApplySchemaDir = path.join(userDataDir, 'openspec', 'schemas', 'no-apply');
|
||||
const templatesDir = path.join(noApplySchemaDir, 'templates');
|
||||
await fs.mkdir(templatesDir, { recursive: true });
|
||||
|
||||
// Minimal schema with 2 artifacts, no apply block
|
||||
const schemaContent = `
|
||||
name: no-apply
|
||||
version: 1
|
||||
description: Test schema without apply block
|
||||
artifacts:
|
||||
- id: first
|
||||
generates: first.md
|
||||
description: First artifact
|
||||
template: first.md
|
||||
requires: []
|
||||
- id: second
|
||||
generates: second.md
|
||||
description: Second artifact
|
||||
template: second.md
|
||||
requires: [first]
|
||||
`;
|
||||
await fs.writeFile(path.join(noApplySchemaDir, 'schema.yaml'), schemaContent);
|
||||
await fs.writeFile(path.join(templatesDir, 'first.md'), '# First\n');
|
||||
await fs.writeFile(path.join(templatesDir, 'second.md'), '# Second\n');
|
||||
|
||||
// Create a change with only the first artifact (missing second)
|
||||
const changeDir = path.join(changesDir, 'no-apply-test');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
await fs.writeFile(path.join(changeDir, 'first.md'), '# First artifact content');
|
||||
|
||||
// Run with XDG_DATA_HOME pointing to our temp user data dir
|
||||
const result = await runCLI(
|
||||
['instructions', 'apply', '--change', 'no-apply-test', '--schema', 'no-apply', '--json'],
|
||||
{
|
||||
cwd: tempDir,
|
||||
env: { XDG_DATA_HOME: userDataDir },
|
||||
}
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
// Without apply block, fallback requires ALL artifacts - second is missing
|
||||
expect(json.schemaName).toBe('no-apply');
|
||||
expect(json.state).toBe('blocked');
|
||||
expect(json.missingArtifacts).toContain('second');
|
||||
});
|
||||
|
||||
it('fallback: ready when all artifacts exist for schema without apply block', async () => {
|
||||
// Create a minimal schema without an apply block
|
||||
const userDataDir = path.join(tempDir, 'user-data-2');
|
||||
const noApplySchemaDir = path.join(userDataDir, 'openspec', 'schemas', 'no-apply-full');
|
||||
const templatesDir = path.join(noApplySchemaDir, 'templates');
|
||||
await fs.mkdir(templatesDir, { recursive: true });
|
||||
|
||||
const schemaContent = `
|
||||
name: no-apply-full
|
||||
version: 1
|
||||
description: Test schema without apply block
|
||||
artifacts:
|
||||
- id: only
|
||||
generates: only.md
|
||||
description: Only artifact
|
||||
template: only.md
|
||||
requires: []
|
||||
`;
|
||||
await fs.writeFile(path.join(noApplySchemaDir, 'schema.yaml'), schemaContent);
|
||||
await fs.writeFile(path.join(templatesDir, 'only.md'), '# Only\n');
|
||||
|
||||
// Create a change with the artifact present
|
||||
const changeDir = path.join(changesDir, 'no-apply-full-test');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
await fs.writeFile(path.join(changeDir, 'only.md'), '# Content');
|
||||
|
||||
const result = await runCLI(
|
||||
['instructions', 'apply', '--change', 'no-apply-full-test', '--schema', 'no-apply-full', '--json'],
|
||||
{
|
||||
cwd: tempDir,
|
||||
env: { XDG_DATA_HOME: userDataDir },
|
||||
}
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
// All artifacts exist, should be ready with default instruction
|
||||
expect(json.schemaName).toBe('no-apply-full');
|
||||
expect(json.state).toBe('ready');
|
||||
expect(json.instruction).toContain('All required artifacts complete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('help text', () => {
|
||||
it('marks status command as experimental in help', async () => {
|
||||
const result = await runCLI(['status', '--help']);
|
||||
@@ -558,6 +417,12 @@ artifacts:
|
||||
expect(result.stdout).toContain('[Experimental]');
|
||||
});
|
||||
|
||||
it('marks next command as experimental in help', async () => {
|
||||
const result = await runCLI(['next', '--help']);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('[Experimental]');
|
||||
});
|
||||
|
||||
it('marks instructions command as experimental in help', async () => {
|
||||
const result = await runCLI(['instructions', '--help']);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
@@ -86,42 +86,6 @@ describe('instruction-loader', () => {
|
||||
|
||||
expect(context.completed.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should auto-detect schema from .openspec.yaml metadata', () => {
|
||||
// Create change directory with metadata file
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', 'my-change');
|
||||
fs.mkdirSync(changeDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(changeDir, '.openspec.yaml'), 'schema: tdd\ncreated: "2025-01-05"\n');
|
||||
|
||||
// Load without explicit schema - should detect from metadata
|
||||
const context = loadChangeContext(tempDir, 'my-change');
|
||||
|
||||
expect(context.schemaName).toBe('tdd');
|
||||
expect(context.graph.getName()).toBe('tdd');
|
||||
});
|
||||
|
||||
it('should use explicit schema over metadata schema', () => {
|
||||
// Create change directory with metadata file using tdd
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', 'my-change');
|
||||
fs.mkdirSync(changeDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(changeDir, '.openspec.yaml'), 'schema: tdd\n');
|
||||
|
||||
// Load with explicit schema - should override metadata
|
||||
const context = loadChangeContext(tempDir, 'my-change', 'spec-driven');
|
||||
|
||||
expect(context.schemaName).toBe('spec-driven');
|
||||
expect(context.graph.getName()).toBe('spec-driven');
|
||||
});
|
||||
|
||||
it('should fall back to default when no metadata and no explicit schema', () => {
|
||||
// Create change directory without metadata file
|
||||
const changeDir = path.join(tempDir, 'openspec', 'changes', 'my-change');
|
||||
fs.mkdirSync(changeDir, { recursive: true });
|
||||
|
||||
const context = loadChangeContext(tempDir, 'my-change');
|
||||
|
||||
expect(context.schemaName).toBe('spec-driven');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateInstructions', () => {
|
||||
|
||||
@@ -115,19 +115,19 @@ Regular text that should be ignored
|
||||
expect(logOutput.some(line => line.includes('no-tasks') && line.includes('No tasks'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should sort changes alphabetically when sort=name', async () => {
|
||||
it('should sort changes alphabetically', async () => {
|
||||
const changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
await fs.mkdir(path.join(changesDir, 'zebra'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'alpha'), { recursive: true });
|
||||
await fs.mkdir(path.join(changesDir, 'middle'), { recursive: true });
|
||||
|
||||
const listCommand = new ListCommand();
|
||||
await listCommand.execute(tempDir, 'changes', { sort: 'name' });
|
||||
await listCommand.execute(tempDir);
|
||||
|
||||
const changeLines = logOutput.filter(line =>
|
||||
const changeLines = logOutput.filter(line =>
|
||||
line.includes('alpha') || line.includes('middle') || line.includes('zebra')
|
||||
);
|
||||
|
||||
|
||||
expect(changeLines[0]).toContain('alpha');
|
||||
expect(changeLines[1]).toContain('middle');
|
||||
expect(changeLines[2]).toContain('zebra');
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { randomUUID } from 'crypto';
|
||||
import {
|
||||
writeChangeMetadata,
|
||||
readChangeMetadata,
|
||||
resolveSchemaForChange,
|
||||
validateSchemaName,
|
||||
ChangeMetadataError,
|
||||
} from '../../src/utils/change-metadata.js';
|
||||
import { ChangeMetadataSchema } from '../../src/core/artifact-graph/types.js';
|
||||
|
||||
describe('ChangeMetadataSchema', () => {
|
||||
describe('valid metadata', () => {
|
||||
it('should accept valid schema with created date', () => {
|
||||
const result = ChangeMetadataSchema.safeParse({
|
||||
schema: 'spec-driven',
|
||||
created: '2025-01-05',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.schema).toBe('spec-driven');
|
||||
expect(result.data.created).toBe('2025-01-05');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept valid schema without created date', () => {
|
||||
const result = ChangeMetadataSchema.safeParse({
|
||||
schema: 'tdd',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.schema).toBe('tdd');
|
||||
expect(result.data.created).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid metadata', () => {
|
||||
it('should reject empty schema', () => {
|
||||
const result = ChangeMetadataSchema.safeParse({
|
||||
schema: '',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject missing schema', () => {
|
||||
const result = ChangeMetadataSchema.safeParse({
|
||||
created: '2025-01-05',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject invalid date format', () => {
|
||||
const result = ChangeMetadataSchema.safeParse({
|
||||
schema: 'spec-driven',
|
||||
created: '01/05/2025', // Wrong format
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject non-ISO date format', () => {
|
||||
const result = ChangeMetadataSchema.safeParse({
|
||||
schema: 'spec-driven',
|
||||
created: '2025-1-5', // Missing leading zeros
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeChangeMetadata', () => {
|
||||
let testDir: string;
|
||||
let changeDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
|
||||
changeDir = path.join(testDir, 'openspec', 'changes', 'test-change');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should write valid YAML metadata file', async () => {
|
||||
writeChangeMetadata(changeDir, {
|
||||
schema: 'spec-driven',
|
||||
created: '2025-01-05',
|
||||
});
|
||||
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
const content = await fs.readFile(metaPath, 'utf-8');
|
||||
|
||||
expect(content).toContain('schema: spec-driven');
|
||||
expect(content).toContain('created: 2025-01-05');
|
||||
});
|
||||
|
||||
it('should throw error for unknown schema', () => {
|
||||
expect(() =>
|
||||
writeChangeMetadata(changeDir, {
|
||||
schema: 'unknown-schema',
|
||||
created: '2025-01-05',
|
||||
})
|
||||
).toThrow(/Unknown schema 'unknown-schema'/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readChangeMetadata', () => {
|
||||
let testDir: string;
|
||||
let changeDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
|
||||
changeDir = path.join(testDir, 'openspec', 'changes', 'test-change');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return null when no metadata file exists', () => {
|
||||
const result = readChangeMetadata(changeDir);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should read valid metadata', async () => {
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(
|
||||
metaPath,
|
||||
'schema: spec-driven\ncreated: "2025-01-05"\n',
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const result = readChangeMetadata(changeDir);
|
||||
expect(result).toEqual({
|
||||
schema: 'spec-driven',
|
||||
created: '2025-01-05',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw ChangeMetadataError for invalid YAML', async () => {
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(metaPath, '{ invalid yaml', 'utf-8');
|
||||
|
||||
expect(() => readChangeMetadata(changeDir)).toThrow(ChangeMetadataError);
|
||||
});
|
||||
|
||||
it('should throw ChangeMetadataError for missing schema field', async () => {
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(metaPath, 'created: "2025-01-05"\n', 'utf-8');
|
||||
|
||||
expect(() => readChangeMetadata(changeDir)).toThrow(ChangeMetadataError);
|
||||
});
|
||||
|
||||
it('should throw ChangeMetadataError for unknown schema', async () => {
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(metaPath, 'schema: unknown-schema\n', 'utf-8');
|
||||
|
||||
expect(() => readChangeMetadata(changeDir)).toThrow(/Unknown schema/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveSchemaForChange', () => {
|
||||
let testDir: string;
|
||||
let changeDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
|
||||
changeDir = path.join(testDir, 'openspec', 'changes', 'test-change');
|
||||
await fs.mkdir(changeDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return explicit schema when provided', async () => {
|
||||
// Even with metadata file, explicit schema wins
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(metaPath, 'schema: spec-driven\n', 'utf-8');
|
||||
|
||||
const result = resolveSchemaForChange(changeDir, 'tdd');
|
||||
expect(result).toBe('tdd');
|
||||
});
|
||||
|
||||
it('should return schema from metadata when no explicit schema', async () => {
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(metaPath, 'schema: spec-driven\n', 'utf-8');
|
||||
|
||||
const result = resolveSchemaForChange(changeDir);
|
||||
expect(result).toBe('spec-driven');
|
||||
});
|
||||
|
||||
it('should return default when no metadata and no explicit schema', () => {
|
||||
const result = resolveSchemaForChange(changeDir);
|
||||
expect(result).toBe('spec-driven');
|
||||
});
|
||||
|
||||
it('should return default when metadata read fails', async () => {
|
||||
// Create an invalid metadata file
|
||||
const metaPath = path.join(changeDir, '.openspec.yaml');
|
||||
await fs.writeFile(metaPath, '{ invalid yaml', 'utf-8');
|
||||
|
||||
// Should fall back to default, not throw
|
||||
const result = resolveSchemaForChange(changeDir);
|
||||
expect(result).toBe('spec-driven');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSchemaName', () => {
|
||||
it('should accept valid schema name', () => {
|
||||
expect(() => validateSchemaName('spec-driven')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for unknown schema', () => {
|
||||
expect(() => validateSchemaName('unknown-schema')).toThrow(
|
||||
/Unknown schema 'unknown-schema'/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -128,31 +128,6 @@ describe('createChange', () => {
|
||||
const stats = await fs.stat(changeDir);
|
||||
expect(stats.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it('should create .openspec.yaml metadata file with default schema', async () => {
|
||||
await createChange(testDir, 'add-auth');
|
||||
|
||||
const metaPath = path.join(testDir, 'openspec', 'changes', 'add-auth', '.openspec.yaml');
|
||||
const content = await fs.readFile(metaPath, 'utf-8');
|
||||
expect(content).toContain('schema: spec-driven');
|
||||
expect(content).toMatch(/created: \d{4}-\d{2}-\d{2}/);
|
||||
});
|
||||
|
||||
it('should create .openspec.yaml with custom schema', async () => {
|
||||
await createChange(testDir, 'add-auth', { schema: 'tdd' });
|
||||
|
||||
const metaPath = path.join(testDir, 'openspec', 'changes', 'add-auth', '.openspec.yaml');
|
||||
const content = await fs.readFile(metaPath, 'utf-8');
|
||||
expect(content).toContain('schema: tdd');
|
||||
});
|
||||
});
|
||||
|
||||
describe('schema validation', () => {
|
||||
it('should throw error for unknown schema', async () => {
|
||||
await expect(createChange(testDir, 'add-auth', { schema: 'unknown-schema' })).rejects.toThrow(
|
||||
/Unknown schema/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate change throws error', () => {
|
||||
|
||||
Reference in New Issue
Block a user