Compare commits

..
Author SHA1 Message Date
TabishB e6728e13a3 ci: retrigger PR checks 2026-04-09 17:21:32 +10:00
1code[bot]andClaude Opus 4.6 e8b3bfb00b test: update template parity hashes for formatting changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 06:37:38 +00:00
Tabish Bidiwale 50185b94ed Merge branch 'main' into fix-formatting-explore 2026-04-09 16:25:45 +10:00
Gregor Albrecht fd7ad273c7 Fix formatting in concepts.md (#882) 2026-04-09 03:34:41 +00:00
Harry James Hall ea6f380fea feat: add ForgeCode tool support (#941) 2026-04-09 03:13:30 +00:00
XingxingandClaude Opus 4.6 765df47ad3 fix(init): prevent false GitHub Copilot auto-detection from bare .github/ directory (#917)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 03:13:04 +00:00
Alfred 64d476f8b9 ci: add merge_group support for merge queue (#918) 2026-04-05 06:56:59 +00:00
Martin Kunz d8d93cbed7 style: Fix formatting of user facing and agent facing diagrams and markdown tables 2026-03-30 23:40:33 +02:00
afdca0d5da fix(status): exit gracefully when no changes exist (#759)
* fix(status): exit gracefully when no changes exist (#714)

Extract `getAvailableChanges` as a public function from `validateChangeExists`
and use it in `statusCommand` to detect the no-changes case early. Returns a
friendly message (text and JSON modes) with exit code 0 instead of a fatal error.

Generated with Claude Code using claude-opus-4-6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: fix design risk description and proposal accuracy

Address CodeRabbit review feedback:
- Fix contradictory risk description in design.md (double-read happens
  when changes exist, not when they don't)
- Clarify in proposal.md that validateChangeExists was internally
  refactored to delegate to getAvailableChanges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(status): narrow catch in getAvailableChanges to ENOENT only

Return [] only when the changes directory doesn't exist (ENOENT).
Rethrow other errors (EACCES, etc.) so real filesystem issues
surface instead of being silently masked as "no changes".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-27 00:52:22 -08:00
61eb999f7c fix(opencode): use plural commands/ directory to match OpenCode convention (#760)
* fix(opencode): use plural `commands/` directory to match OpenCode convention

The OpenCode adapter was using `.opencode/command/` (singular) but OpenCode's
official documentation specifies `.opencode/commands/` (plural). This aligns
with every other adapter in the codebase. Legacy cleanup updated to detect
old singular-path artifacts. Fixes #748.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(legacy): detect both opsx-* and openspec-* patterns, auto-cleanup in CI

- Extend LegacySlashCommandPattern.pattern to accept string | string[]
- OpenCode legacy entry now detects both opsx-*.md and openspec-*.md
- Auto-cleanup legacy artifacts in non-interactive mode instead of
  aborting with exit 1 (safe: slash commands are OpenSpec-managed,
  config cleanup only removes markers)
- Add 7 tests (6 legacy detection + 1 non-interactive init)
- Update spec with array pattern support and auto-cleanup scenario

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update task description to reflect dual-pattern support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-27 00:08:22 -08:00
3d3bf96061 docs: fix openspec status examples in cli.md (#761)
* docs: fix `openspec status` examples in cli.md to match actual CLI output

The text and JSON output examples for the status command used incorrect
field names, indicators, and structure. Updated to match real CLI output,
validated against a test project with partial artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove spec change and changeset for docs-only fix

Per reviewer feedback — docs fixes don't need a spec change or
version bump.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2026-02-26 23:37:28 -08:00
JabinGP d199dfa407 docs: fix docs/concepts nested code-block format (#763) 2026-02-26 10:15:27 +00:00
Tabish Bidiwale d7d186088e docs: realign defaults, profile workflows, and tool references (#746)
* docs: realign defaults, workflows, and tool references

* docs: resolve Trae wording and opsx diagram alignment

* chore: ignore codex workspace directory
2026-02-23 18:27:23 -08:00
openspec-release-bot[bot]andgithub-actions[bot] 6a3a1263fe Version Packages (#751)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-23 15:43:08 -08:00
Tabish Bidiwale 1e94443a35 Add changeset for profiles, Pi, Kiro, and bug fixes (#747) 2026-02-23 15:37:30 -08:00
Tabish Bidiwale a0608d0bab Sync update to prune deselected workflows (#741) 2026-02-22 05:35:01 -08:00
48 changed files with 1087 additions and 390 deletions
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
fix: OpenCode adapter now uses `.opencode/commands/` (plural) to match OpenCode's official directory convention. Fixes #748.
+5
View File
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
fix: `openspec status` now exits gracefully when no changes exist instead of throwing a fatal error. Fixes #714.
+7 -5
View File
@@ -3,6 +3,8 @@ name: CI
on:
pull_request:
branches: [main]
merge_group:
branches: [main]
push:
branches: [main]
workflow_dispatch:
@@ -42,7 +44,7 @@ jobs:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
steps:
- name: Checkout code
@@ -81,7 +83,7 @@ jobs:
name: Test (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: github.event_name != 'pull_request'
if: github.event_name == 'push'
strategy:
fail-fast: false
matrix:
@@ -242,7 +244,7 @@ jobs:
validate-changesets:
name: Validate Changesets
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -275,7 +277,7 @@ jobs:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_pr, lint, nix-flake-validate]
if: always() && github.event_name == 'pull_request'
if: always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group')
steps:
- name: Verify all checks passed
run: |
@@ -301,7 +303,7 @@ jobs:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_matrix, lint, nix-flake-validate]
if: always() && github.event_name != 'pull_request'
if: always() && github.event_name == 'push'
steps:
- name: Verify all checks passed
run: |
+3
View File
@@ -153,3 +153,6 @@ result
# OpenCode
.opencode/
opencode.json
# Codex
.codex/
+20
View File
@@ -1,5 +1,25 @@
# @fission-ai/openspec
## 1.2.0
### Minor Changes
- [#747](https://github.com/Fission-AI/OpenSpec/pull/747) [`1e94443`](https://github.com/Fission-AI/OpenSpec/commit/1e94443a3551b228eecbc89e95d96d3b9600a192) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
- **Profile system** — Choose between `core` (4 essential workflows) and `custom` (pick any subset) profiles to control which skills get installed. Manage profiles with the new `openspec config profile` command
- **Propose workflow** — New one-step workflow creates a complete change proposal with design, specs, and tasks from a single request — no need to run `new` then `ff` separately
- **AI tool auto-detection** — `openspec init` now scans your project for existing tool directories (`.claude/`, `.cursor/`, etc.) and pre-selects detected tools
- **Pi (pi.dev) support** — Pi coding agent is now a supported tool with prompt and skill generation
- **Kiro support** — AWS Kiro IDE is now a supported tool with prompt and skill generation
- **Sync prunes deselected workflows** — `openspec update` now removes command files and skill directories for workflows you've deselected, keeping your project clean
- **Config drift warning** — `openspec config list` warns when global config is out of sync with the current project
### Bug Fixes
- Fixed onboard preflight giving a false "not initialized" error on freshly initialized projects
- Fixed archive workflow stopping mid-way when syncing — it now properly resumes after sync completes
- Added Windows PowerShell alternatives for onboard shell commands
## 1.1.1
### Patch Changes
+7 -8
View File
@@ -36,7 +36,7 @@ Our philosophy:
> [!TIP]
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
>
> Run `/opsx:onboard` to get started. → [Learn more here](docs/opsx.md)
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
<p align="center">
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
@@ -46,17 +46,14 @@ Our philosophy:
Using OpenSpec in a team? [Email here](mailto:teams@openspec.dev) for access to our Slack channel.
<!-- TODO: Add GIF demo of /opsx:new → /opsx:archive workflow -->
<!-- TODO: Add GIF demo of /opsx:propose → /opsx:archive workflow -->
## See it in action
```text
You: /opsx:new add-dark-mode
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
Ready to create: proposal
You: /opsx:ff # "fast-forward" - generate all planning docs
AI: ✓ proposal.md — why we're doing this, what's changing
✓ proposal.md — why we're doing this, what's changing
✓ specs/ — requirements and scenarios
✓ design.md — technical approach
✓ tasks.md — implementation checklist
@@ -101,7 +98,9 @@ cd your-project
openspec init
```
Now tell your AI: `/opsx:new <what-you-want-to-build>`
Now tell your AI: `/opsx:propose <what-you-want-to-build>`
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
+29 -21
View File
@@ -1,6 +1,6 @@
# CLI Reference
The OpenSpec CLI (`openspec`) provides terminal commands for project setup, validation, status inspection, and management. These commands complement the AI slash commands (like `/opsx:new`) documented in [Commands](commands.md).
The OpenSpec CLI (`openspec`) provides terminal commands for project setup, validation, status inspection, and management. These commands complement the AI slash commands (like `/opsx:propose`) documented in [Commands](commands.md).
## Summary
@@ -67,6 +67,8 @@ These options work with all commands:
Initialize OpenSpec in your project. Creates the folder structure and configures AI tool integrations.
Default behavior uses global config defaults: profile `core`, delivery `both`, workflows `propose, explore, apply, archive`.
```
openspec init [path] [options]
```
@@ -83,8 +85,11 @@ openspec init [path] [options]
|--------|-------------|
| `--tools <list>` | Configure AI tools non-interactively. Use `all`, `none`, or comma-separated list |
| `--force` | Auto-cleanup legacy files without prompting |
| `--profile <profile>` | Override global profile for this init run (`core` or `custom`) |
**Supported tools:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `opencode`, `qoder`, `qwen`, `roocode`, `windsurf`
`--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`).
**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
**Examples:**
@@ -101,6 +106,9 @@ openspec init --tools claude,cursor
# Configure for all supported tools
openspec init --tools all
# Override profile for this run
openspec init --profile core
# Skip prompts and auto-cleanup legacy files
openspec init --force
```
@@ -113,8 +121,9 @@ openspec/
├── changes/ # Proposed changes
└── config.yaml # Project configuration
.claude/skills/ # Claude Code skill files (if claude selected)
.cursor/rules/ # Cursor rules (if cursor selected)
.claude/skills/ # Claude Code skills (if claude selected)
.cursor/skills/ # Cursor skills (if cursor selected)
.cursor/commands/ # Cursor OPSX commands (if delivery includes commands)
... (other tool configs)
```
@@ -122,7 +131,7 @@ openspec/
### `openspec update`
Update OpenSpec instruction files after upgrading the CLI. Re-generates AI tool configuration files.
Update OpenSpec instruction files after upgrading the CLI. Re-generates AI tool configuration files using your current global profile, selected workflows, and delivery mode.
```
openspec update [path] [options]
@@ -428,29 +437,28 @@ openspec status --change add-dark-mode --json
```
Change: add-dark-mode
Schema: spec-driven
Progress: 2/4 artifacts complete
Artifacts:
✓ proposal proposal.md exists
✓ specs specs/ exists
◆ design ready (requires: specs)
○ tasks blocked (requires: design)
Next: Create design using /opsx:continue
[x] proposal
[ ] design
[x] specs
[-] tasks (blocked by: design)
```
**Output (JSON):**
```json
{
"change": "add-dark-mode",
"schema": "spec-driven",
"changeName": "add-dark-mode",
"schemaName": "spec-driven",
"isComplete": false,
"applyRequires": ["tasks"],
"artifacts": [
{"id": "proposal", "status": "complete", "path": "proposal.md"},
{"id": "specs", "status": "complete", "path": "specs/"},
{"id": "design", "status": "ready", "requires": ["specs"]},
{"id": "tasks", "status": "blocked", "requires": ["design"]}
],
"next": "design"
{"id": "proposal", "outputPath": "proposal.md", "status": "done"},
{"id": "design", "outputPath": "design.md", "status": "ready"},
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done"},
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked", "missingDeps": ["design"]}
]
}
```
@@ -920,7 +928,7 @@ openspec completion uninstall
## Related Documentation
- [Commands](commands.md) - AI slash commands (`/opsx:new`, `/opsx:apply`, etc.)
- [Commands](commands.md) - AI slash commands (`/opsx:propose`, `/opsx:apply`, etc.)
- [Workflows](workflows.md) - Common patterns and when to use each command
- [Customization](customization.md) - Create custom schemas and templates
- [Getting Started](getting-started.md) - First-time setup guide
+61 -12
View File
@@ -6,23 +6,70 @@ For workflow patterns and when to use each command, see [Workflows](workflows.md
## Quick Reference
### Default Quick Path (`core` profile)
| Command | Purpose |
|---------|---------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas before committing to a change |
| `/opsx:new` | Start a new change |
| `/opsx:apply` | Implement tasks from the change |
| `/opsx:archive` | Archive a completed change |
### Expanded Workflow Commands (custom workflow selection)
| Command | Purpose |
|---------|---------|
| `/opsx:new` | Start a new change scaffold |
| `/opsx:continue` | Create the next artifact based on dependencies |
| `/opsx:ff` | Fast-forward: create all planning artifacts at once |
| `/opsx:apply` | Implement tasks from the change |
| `/opsx:verify` | Validate implementation matches artifacts |
| `/opsx:sync` | Merge delta specs into main specs |
| `/opsx:archive` | Archive a completed change |
| `/opsx:bulk-archive` | Archive multiple changes at once |
| `/opsx:onboard` | Guided tutorial through the complete workflow |
The default global profile is `core`. To enable expanded workflow commands, run `openspec config profile`, select workflows, then run `openspec update` in your project.
---
## Command Reference
### `/opsx:propose`
Create a new change and generate planning artifacts in one step. This is the default start command in the `core` profile.
**Syntax:**
```text
/opsx:propose [change-name-or-description]
```
**Arguments:**
| Argument | Required | Description |
|----------|----------|-------------|
| `change-name-or-description` | No | Kebab-case name or plain-language change description |
**What it does:**
- Creates `openspec/changes/<change-name>/`
- Generates artifacts needed before implementation (for `spec-driven`: proposal, specs, design, tasks)
- Stops when the change is ready for `/opsx:apply`
**Example:**
```text
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
✓ proposal.md
✓ specs/ui/spec.md
✓ design.md
✓ tasks.md
Ready for implementation. Run /opsx:apply.
```
**Tips:**
- Use this for the fastest end-to-end path
- If you want step-by-step artifact control, enable expanded workflows and use `/opsx:new` + `/opsx:continue`
---
### `/opsx:explore`
Think through ideas, investigate problems, and clarify requirements before committing to a change.
@@ -42,7 +89,7 @@ Think through ideas, investigate problems, and clarify requirements before commi
- Investigates the codebase to answer questions
- Compares options and approaches
- Creates visual diagrams to clarify thinking
- Can transition to `/opsx:new` when insights crystallize
- Can transition to `/opsx:propose` (default) or `/opsx:new` (expanded workflow) when insights crystallize
**Example:**
```text
@@ -66,7 +113,7 @@ AI: Let me investigate your current auth setup...
You: Let's go with JWT. Can we start a change for that?
AI: Ready when you are. Run /opsx:new add-jwt-auth to begin.
AI: Ready when you are. Run /opsx:propose add-jwt-auth to begin.
```
**Tips:**
@@ -79,7 +126,9 @@ AI: Ready when you are. Run /opsx:new add-jwt-auth to begin.
### `/opsx:new`
Start a new change. Creates the change folder structure and scaffolds it with the selected schema.
Start a new change scaffold. Creates the change folder and waits for you to generate artifacts with `/opsx:continue` or `/opsx:ff`.
This command is part of the expanded workflow set (not included in the default `core` profile).
**Syntax:**
```
@@ -565,13 +614,13 @@ Different AI tools use slightly different command syntax. Use the format that ma
| Tool | Syntax Example |
|------|----------------|
| Claude Code | `/opsx:new`, `/opsx:apply` |
| Cursor | `/opsx-new`, `/opsx-apply` |
| Windsurf | `/opsx-new`, `/opsx-apply` |
| Copilot (IDE) | `/opsx-new`, `/opsx-apply` |
| Trae | `/openspec-new-change`, `/openspec-apply-change` |
| Claude Code | `/opsx:propose`, `/opsx:apply` |
| Cursor | `/opsx-propose`, `/opsx-apply` |
| Windsurf | `/opsx-propose`, `/opsx-apply` |
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
| Trae | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) |
The functionality is identical regardless of syntax.
The intent is the same across tools, but how commands are surfaced can differ by integration.
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files — see [Supported Tools](supported-tools.md) for details and workarounds.
+27 -27
View File
@@ -7,10 +7,10 @@ This guide explains the core ideas behind OpenSpec and how they fit together. Fo
OpenSpec is built around four principles:
```
fluid not rigid — no phase gates, work on what makes sense
fluid not rigid — no phase gates, work on what makes sense
iterative not waterfall — learn as you build, refine as you go
easy not complex — lightweight setup, minimal ceremony
brownfield-first — works with existing codebases, not just greenfield
easy not complex — lightweight setup, minimal ceremony
brownfield-first — works with existing codebases, not just greenfield
```
### Why These Principles Matter
@@ -28,19 +28,19 @@ brownfield-first — works with existing codebases, not just greenfield
OpenSpec organizes your work into two main areas:
```
┌─────────────────────────────────────────────────────────────────┐
│ openspec/ │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────────┐ │
│ │ specs/ │ │ changes/ │ │
│ │ │ │ │ │
│ │ Source of truth │◄─────│ Proposed modifications │ │
│ │ How your system │ merge│ Each change = one folder │ │
│ │ currently works │ │ Contains artifacts + deltas │ │
│ │ │ │ │ │
│ └─────────────────────┘ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ openspec/ │
│ │
│ ┌─────────────────────┐ ┌───────────────────────────────┐ │
│ │ specs/ │ │ changes/ │ │
│ │ │ │ │ │
│ │ Source of truth │◄─────│ Proposed modifications │ │
│ │ How your system │ merge│ Each change = one folder │ │
│ │ currently works │ │ Contains artifacts + deltas │ │
│ │ │ │ │ │
│ └─────────────────────┘ └───────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
```
**Specs** are the source of truth — they describe how your system currently behaves.
@@ -270,7 +270,7 @@ Delta specs describe **what's changing** relative to the current specs. See [Del
The design captures **technical approach** and **architecture decisions**.
```markdown
````markdown
# Design: Add Dark Mode
## Technical Approach
@@ -306,7 +306,7 @@ CSS Variables (applied to :root)
- `src/contexts/ThemeContext.tsx` (new)
- `src/components/ThemeToggle.tsx` (new)
- `src/styles/globals.css` (modified)
```
````
**When to update the design:**
- Implementation reveals the approach won't work
@@ -558,17 +558,17 @@ openspec/
## How It All Fits Together
```
┌─────────────────────────────────────────────────────────────────────────────┐
┌──────────────────────────────────────────────────────────────────────────────┐
│ OPENSPEC FLOW │
│ │
│ ┌────────────────┐ │
│ │ 1. START │ /opsx:new creates a change folder │
│ │ 1. START │ /opsx:propose (core) or /opsx:new (expanded) │
│ │ CHANGE │ │
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ 2. CREATE │ /opsx:ff or /opsx:continue │
│ │ 2. CREATE │ /opsx:ff or /opsx:continue (expanded workflow) │
│ │ ARTIFACTS │ Creates proposal → specs → design → tasks │
│ │ │ (based on schema dependencies) │
│ └───────┬────────┘ │
@@ -587,13 +587,13 @@ openspec/
│ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ ┌──────────────────────────────────────────────┐ │
│ │ 5. ARCHIVE │────►│ Delta specs merge into main specs │ │
│ │ CHANGE │ │ Change folder moves to archive/ │ │
│ └────────────────┘ │ Specs are now the updated source of truth │ │
│ └──────────────────────────────────────────────┘ │
│ ┌────────────────┐ ┌──────────────────────────────────────────────┐ │
│ │ 5. ARCHIVE │────►│ Delta specs merge into main specs │ │
│ │ CHANGE │ │ Change folder moves to archive/ │ │
│ └────────────────┘ │ Specs are now the updated source of truth │ │
│ └──────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
└──────────────────────────────────────────────────────────────────────────────┘
```
**The virtuous cycle:**
+20 -40
View File
@@ -4,33 +4,22 @@ This guide explains how OpenSpec works after you've installed and initialized it
## How It Works
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written. The workflow follows a simple pattern:
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written.
**Default quick path (core profile):**
```text
/opsx:propose ──► /opsx:apply ──► /opsx:archive
```
┌────────────────────┐
│ Start a Change │ /opsx:new
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Create Artifacts │ /opsx:ff or /opsx:continue
│ (proposal, specs, │
│ design, tasks) │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Implement Tasks │ /opsx:apply
│ (AI writes code) │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Archive & Merge │ /opsx:archive
│ Specs │
└────────────────────┘
**Expanded path (custom workflow selection):**
```text
/opsx:new ──► /opsx:ff or /opsx:continue ──► /opsx:apply ──► /opsx:verify ──► /opsx:archive
```
The default global profile is `core`, which includes `propose`, `explore`, `apply`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`.
## What OpenSpec Creates
After running `openspec init`, your project has this structure:
@@ -131,23 +120,12 @@ The change folder moves to `openspec/changes/archive/` for audit history.
Let's walk through adding dark mode to an application.
### 1. Start the Change
### 1. Start the Change (Default)
```
You: /opsx:new add-dark-mode
```text
You: /opsx:propose add-dark-mode
AI: Created openspec/changes/add-dark-mode/
Ready to create: proposal
```
### 2. Create Artifacts
Use `/opsx:ff` (fast-forward) to create all planning artifacts at once:
```
You: /opsx:ff
AI: Creating artifacts for add-dark-mode...
✓ proposal.md — why we're doing this, what's changing
✓ specs/ — requirements and scenarios
✓ design.md — technical approach
@@ -155,7 +133,9 @@ AI: Creating artifacts for add-dark-mode...
Ready for implementation!
```
### 3. What Gets Created
If you've enabled the expanded workflow profile, you can also do this as two steps: `/opsx:new` then `/opsx:ff` (or `/opsx:continue` incrementally).
### 2. What Gets Created
**proposal.md** - Captures the intent:
@@ -218,7 +198,7 @@ The system SHALL allow users to choose between light and dark themes.
- [ ] 3.2 Update components to use CSS variables
```
### 4. Implement
### 3. Implement
```
You: /opsx:apply
@@ -234,7 +214,7 @@ AI: Working through tasks...
During implementation, if you discover the design needs adjustment, just update the artifact and continue.
### 5. Archive
### 4. Archive
```
You: /opsx:archive
+35 -15
View File
@@ -8,7 +8,7 @@ OPSX replaces the old phase-locked workflow with a fluid, action-based approach.
| Aspect | Legacy | OPSX |
|--------|--------|------|
| **Commands** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` | `/opsx:new`, `/opsx:continue`, `/opsx:apply`, and more |
| **Commands** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` | Default: `/opsx:propose`, `/opsx:apply`, `/opsx:archive` (expanded workflow commands optional) |
| **Workflow** | Create all artifacts at once | Create incrementally or all at once—your choice |
| **Going back** | Awkward phase gates | Natural—update any artifact anytime |
| **Customization** | Fixed structure | Schema-driven, fully hackable |
@@ -84,6 +84,9 @@ Don't worry about getting it perfect. We're still learning what works best here,
Both `openspec init` and `openspec update` detect legacy files and guide you through the same cleanup process. Use whichever fits your situation:
- New installs default to profile `core` (`propose`, `explore`, `apply`, `archive`).
- Migrated installs preserve your previously installed workflows by writing a `custom` profile when needed.
### Using `openspec init`
Run this if you want to add new tools or reconfigure which tools are set up:
@@ -141,7 +144,7 @@ Run this if you just want to migrate and refresh your existing tools to the late
openspec update
```
The update command also detects and cleans up legacy artifacts, then refreshes your skills to the latest version.
The update command also detects and cleans up legacy artifacts, then refreshes generated skills/commands to match your current profile and delivery settings.
### Non-Interactive / CI Environments
@@ -275,30 +278,43 @@ The AI will help you identify what's essential vs. what can be trimmed.
## The New Commands
After migration, you have 9 OPSX commands instead of 3:
Command availability is profile-dependent:
**Default (`core` profile):**
| Command | Purpose |
|---------|---------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
| `/opsx:explore` | Think through ideas with no structure |
| `/opsx:new` | Start a new change |
| `/opsx:continue` | Create the next artifact (one at a time) |
| `/opsx:ff` | Fast-forward—create all planning artifacts at once |
| `/opsx:apply` | Implement tasks from tasks.md |
| `/opsx:verify` | Validate implementation matches specs |
| `/opsx:sync` | Preview spec merge (optional—archive prompts if needed) |
| `/opsx:archive` | Finalize and archive the change |
**Expanded workflow (custom selection):**
| Command | Purpose |
|---------|---------|
| `/opsx:new` | Start a new change scaffold |
| `/opsx:continue` | Create the next artifact (one at a time) |
| `/opsx:ff` | Fast-forward—create planning artifacts at once |
| `/opsx:verify` | Validate implementation matches specs |
| `/opsx:sync` | Preview/spec-merge without archiving |
| `/opsx:bulk-archive` | Archive multiple changes at once |
| `/opsx:onboard` | Guided end-to-end onboarding workflow |
Enable expanded commands with `openspec config profile`, then run `openspec update`.
### Command Mapping from Legacy
| Legacy | OPSX Equivalent |
|--------|-----------------|
| `/openspec:proposal` | `/opsx:new` then `/opsx:ff` |
| `/openspec:proposal` | `/opsx:propose` (default) or `/opsx:new` then `/opsx:ff` (expanded) |
| `/openspec:apply` | `/opsx:apply` |
| `/openspec:archive` | `/opsx:archive` |
### New Capabilities
These capabilities are part of the expanded workflow command set.
**Granular artifact creation:**
```
/opsx:continue
@@ -542,9 +558,10 @@ project/
│ └── config.yaml # NEW: Project configuration
├── .claude/
│ └── skills/ # NEW: OPSX skills
│ ├── openspec-propose/ # default core profile
│ ├── openspec-explore/
│ ├── openspec-new-change/
│ └── ...
│ ├── openspec-apply-change/
│ └── ... # expanded profile adds new/continue/ff/etc.
├── CLAUDE.md # OpenSpec markers removed, your content preserved
└── AGENTS.md # OpenSpec markers removed, your content preserved
```
@@ -558,12 +575,15 @@ project/
### Command Cheatsheet
```
/opsx:new Start a change
/opsx:continue Create next artifact
/opsx:ff Create all planning artifacts
```text
/opsx:propose Start quickly (default core profile)
/opsx:apply Implement tasks
/opsx:archive Finish and archive
# Expanded workflow (if enabled):
/opsx:new Scaffold a change
/opsx:continue Create next artifact
/opsx:ff Create planning artifacts
```
---
+24 -9
View File
@@ -65,6 +65,8 @@ openspec init
This creates skills in `.claude/skills/` (or equivalent) that AI coding assistants auto-detect.
By default, OpenSpec uses the `core` workflow profile (`propose`, `explore`, `apply`, `archive`). If you want the expanded workflow commands (`new`, `continue`, `ff`, `verify`, `sync`, `bulk-archive`, `onboard`), configure them with `openspec config profile` and apply with `openspec update`.
During setup, you'll be prompted to create a **project config** (`openspec/config.yaml`). This is optional but recommended.
## Project Configuration
@@ -155,13 +157,17 @@ rules:
| Command | What it does |
|---------|--------------|
| `/opsx:propose` | Create a change and generate planning artifacts in one step (default quick path) |
| `/opsx:explore` | Think through ideas, investigate problems, clarify requirements |
| `/opsx:new` | Start a new change |
| `/opsx:continue` | Create the next artifact (based on what's ready) |
| `/opsx:ff` | Fast-forward — create all planning artifacts at once |
| `/opsx:new` | Start a new change scaffold (expanded workflow) |
| `/opsx:continue` | Create the next artifact (expanded workflow) |
| `/opsx:ff` | Fast-forward planning artifacts (expanded workflow) |
| `/opsx:apply` | Implement tasks, updating artifacts as needed |
| `/opsx:sync` | Sync delta specs to main (optional—archive prompts if needed) |
| `/opsx:verify` | Validate implementation against artifacts (expanded workflow) |
| `/opsx:sync` | Sync delta specs to main (expanded workflow, optional) |
| `/opsx:archive` | Archive when done |
| `/opsx:bulk-archive` | Archive multiple completed changes (expanded workflow) |
| `/opsx:onboard` | Guided walkthrough of an end-to-end change (expanded workflow) |
## Usage
@@ -169,13 +175,21 @@ rules:
```
/opsx:explore
```
Think through ideas, investigate problems, compare options. No structure required - just a thinking partner. When insights crystallize, transition to `/opsx:new` or `/opsx:ff`.
Think through ideas, investigate problems, compare options. No structure required - just a thinking partner. When insights crystallize, transition to `/opsx:propose` (default) or `/opsx:new`/`/opsx:ff` (expanded).
### Start a new change
```
/opsx:new
/opsx:propose
```
Creates the change and generates planning artifacts needed before implementation.
If you've enabled expanded workflows, you can instead use:
```text
/opsx:new # scaffold only
/opsx:continue # create one artifact at a time
/opsx:ff # create all planning artifacts at once
```
You'll be asked what you want to build and which workflow schema to use.
### Create artifacts
```
@@ -299,6 +313,7 @@ Think of it like git branches:
## Architecture Deep Dive
This section explains how OPSX works under the hood and how it compares to the legacy workflow.
Examples in this section use the expanded command set (`new`, `continue`, etc.); default `core` users can map the same flow to `propose → apply → archive`.
### Philosophy: Phases vs Actions
@@ -356,7 +371,7 @@ This section explains how OPSX works under the hood and how it compares to the l
│ Hardcoded Templates (TypeScript strings) │
│ │ │
│ ▼ │
│ Configurators (18+ classes, one per editor) │
│ Tool-specific configurators/adapters │
│ │ │
│ ▼ │
│ Generated Command Files (.claude/commands/openspec/*.md) │
@@ -604,7 +619,7 @@ artifacts:
| **State** | Phase-based mental model | Filesystem existence |
| **Customization** | Edit source, rebuild | Create schema.yaml |
| **Iteration** | Phase-locked | Fluid, edit anything |
| **Editor Support** | 18+ configurator classes | Single skills directory |
| **Editor Support** | Tool-specific configurator/adapters | Single skills directory |
## Schemas
+67 -52
View File
@@ -1,50 +1,59 @@
# Supported Tools
OpenSpec works with 20+ AI coding assistants. When you run `openspec init`, you'll be prompted to select which tools you use, and OpenSpec will configure the appropriate integrations.
OpenSpec works with many AI coding assistants. When you run `openspec init`, OpenSpec configures selected tools using your active profile/workflow selection and delivery mode.
## How It Works
For each tool you select, OpenSpec installs:
For each selected tool, OpenSpec can install:
1. **Skills** — Reusable instruction files that power the `/opsx:*` workflow commands
2. **Commands** — Tool-specific slash command bindings
1. **Skills** (if delivery includes skills): `.../skills/openspec-*/SKILL.md`
2. **Commands** (if delivery includes commands): tool-specific `opsx-*` command files
By default, OpenSpec uses the `core` profile, which includes:
- `propose`
- `explore`
- `apply`
- `archive`
You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `bulk-archive`, `onboard`) via `openspec config profile`, then run `openspec update`.
## Tool Directory Reference
| Tool | Skills Location | Commands Location |
|------|-----------------|-------------------|
| Amazon Q Developer | `.amazonq/skills/` | `.amazonq/prompts/` |
| Antigravity | `.agent/skills/` | `.agent/workflows/` |
| Auggie (Augment CLI) | `.augment/skills/` | `.augment/commands/` |
| Claude Code | `.claude/skills/` | `.claude/commands/opsx/` |
| Cline | `.cline/skills/` | `.clinerules/workflows/` |
| CodeBuddy | `.codebuddy/skills/` | `.codebuddy/commands/opsx/` |
| Codex | `.codex/skills/` | `~/.codex/prompts/`\* |
| Continue | `.continue/skills/` | `.continue/prompts/` |
| CoStrict | `.cospec/skills/` | `.cospec/openspec/commands/` |
| Crush | `.crush/skills/` | `.crush/commands/opsx/` |
| Cursor | `.cursor/skills/` | `.cursor/commands/` |
| Factory Droid | `.factory/skills/` | `.factory/commands/` |
| Gemini CLI | `.gemini/skills/` | `.gemini/commands/opsx/` |
| GitHub Copilot | `.github/skills/` | `.github/prompts/`\*\* |
| iFlow | `.iflow/skills/` | `.iflow/commands/` |
| Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` |
| Kiro | `.kiro/skills/` | `.kiro/prompts/` |
| OpenCode | `.opencode/skills/` | `.opencode/command/` |
| Pi | `.pi/skills/` | `.pi/prompts/` |
| Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` |
| Qwen Code | `.qwen/skills/` | `.qwen/commands/` |
| RooCode | `.roo/skills/` | `.roo/commands/` |
| Trae | `.trae/skills/` | `.trae/skills/` (via `/openspec-*`) |
| Windsurf | `.windsurf/skills/` | `.windsurf/workflows/` |
| Tool (ID) | Skills path pattern | Command path pattern |
|-----------|---------------------|----------------------|
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | `$CODEX_HOME/prompts/opsx-<id>.md`\* |
| ForgeCode (`forgecode`) | `.forge/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Continue (`continue`) | `.continue/skills/openspec-*/SKILL.md` | `.continue/prompts/opsx-<id>.prompt` |
| CoStrict (`costrict`) | `.cospec/skills/openspec-*/SKILL.md` | `.cospec/openspec/commands/opsx-<id>.md` |
| Crush (`crush`) | `.crush/skills/openspec-*/SKILL.md` | `.crush/commands/opsx/<id>.md` |
| Cursor (`cursor`) | `.cursor/skills/openspec-*/SKILL.md` | `.cursor/commands/opsx-<id>.md` |
| Factory Droid (`factory`) | `.factory/skills/openspec-*/SKILL.md` | `.factory/commands/opsx-<id>.md` |
| Gemini CLI (`gemini`) | `.gemini/skills/openspec-*/SKILL.md` | `.gemini/commands/opsx/<id>.toml` |
| GitHub Copilot (`github-copilot`) | `.github/skills/openspec-*/SKILL.md` | `.github/prompts/opsx-<id>.prompt.md`\*\* |
| iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-<id>.md` |
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilocode/workflows/opsx-<id>.md` |
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
| OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-<id>.md` |
| Pi (`pi`) | `.pi/skills/openspec-*/SKILL.md` | `.pi/prompts/opsx-<id>.md` |
| Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/<id>.md` |
| Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-<id>.toml` |
| RooCode (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-<id>.md` |
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
| Windsurf (`windsurf`) | `.windsurf/skills/openspec-*/SKILL.md` | `.windsurf/workflows/opsx-<id>.md` |
\* Codex commands are installed to the global home directory (`~/.codex/prompts/` or `$CODEX_HOME/prompts/`), not the project directory.
\* Codex commands are installed in the global Codex home (`$CODEX_HOME/prompts/` if set, otherwise `~/.codex/prompts/`), not your project directory.
\*\* GitHub Copilot's `.github/prompts/*.prompt.md` files are recognized as custom slash commands in **IDE extensions only** (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompts from this directory — see [github/copilot-cli#618](https://github.com/github/copilot-cli/issues/618). If you use Copilot CLI, you may need to manually set up [custom agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents) in `.github/agents/` as a workaround.
\*\* GitHub Copilot prompt files are recognized as custom slash commands in IDE extensions (VS Code, JetBrains, Visual Studio). Copilot CLI does not currently consume `.github/prompts/*.prompt.md` directly.
## Non-Interactive Setup
For CI/CD or scripted setup, use the `--tools` flag:
For CI/CD or scripted setup, use `--tools` (and optionally `--profile`):
```bash
# Configure specific tools
@@ -55,34 +64,40 @@ openspec init --tools all
# Skip tool configuration
openspec init --tools none
# Override profile for this init run
openspec init --profile core
```
**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `forgecode`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
## What Gets Installed
## Workflow-Dependent Installation
For each tool, OpenSpec generates 10 skill files that power the OPSX workflow:
OpenSpec installs workflow artifacts based on selected workflows:
| Skill | Purpose |
|-------|---------|
| `openspec-explore` | Thinking partner for exploring ideas |
| `openspec-new-change` | Start a new change |
| `openspec-continue-change` | Create the next artifact |
| `openspec-ff-change` | Fast-forward through all planning artifacts |
| `openspec-apply-change` | Implement tasks |
| `openspec-verify-change` | Verify implementation completeness |
| `openspec-sync-specs` | Sync delta specs to main (optional—archive prompts if needed) |
| `openspec-archive-change` | Archive a completed change |
| `openspec-bulk-archive-change` | Archive multiple changes at once |
| `openspec-onboard` | Guided onboarding through a complete workflow cycle |
- **Core profile (default):** `propose`, `explore`, `apply`, `archive`
- **Custom selection:** any subset of all workflow IDs:
`propose`, `explore`, `new`, `continue`, `apply`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard`
These skills are invoked via slash commands like `/opsx:new`, `/opsx:apply`, etc. See [Commands](commands.md) for the full list.
In other words, skill/command counts are profile-dependent and delivery-dependent, not fixed.
## Adding a New Tool
## Generated Skill Names
Want to add support for another AI coding assistant? Check out the [command adapter pattern](../CONTRIBUTING.md) or open an issue on GitHub.
When selected by profile/workflow config, OpenSpec generates these skills:
---
- `openspec-propose`
- `openspec-explore`
- `openspec-new-change`
- `openspec-continue-change`
- `openspec-apply-change`
- `openspec-ff-change`
- `openspec-sync-specs`
- `openspec-archive-change`
- `openspec-bulk-archive-change`
- `openspec-verify-change`
- `openspec-onboard`
See [Commands](commands.md) for command behavior and [CLI](cli.md) for `init`/`update` options.
## Related
+33 -7
View File
@@ -28,7 +28,32 @@ OPSX (fluid actions):
> **Customization:** OPSX workflows are driven by schemas that define artifact sequences. See [Customization](customization.md) for details on creating custom schemas.
## Workflow Patterns
## Two Modes
### Default Quick Path (`core` profile)
New installs default to `core`, which provides:
- `/opsx:propose`
- `/opsx:explore`
- `/opsx:apply`
- `/opsx:archive`
Typical flow:
```text
/opsx:propose ──► /opsx:apply ──► /opsx:archive
```
### Expanded/Full Workflow (custom selection)
If you want explicit scaffold-and-build commands (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), enable them with:
```bash
openspec config profile
openspec update
```
## Workflow Patterns (Expanded Mode)
### Quick Feature
@@ -408,15 +433,16 @@ For full command details and options, see [Commands](commands.md).
| Command | Purpose | When to Use |
|---------|---------|-------------|
| `/opsx:propose` | Create change + planning artifacts | Fast default path (`core` profile) |
| `/opsx:explore` | Think through ideas | Unclear requirements, investigation |
| `/opsx:new` | Start a change | Beginning any new work |
| `/opsx:continue` | Create next artifact | Step-by-step artifact creation |
| `/opsx:ff` | Create all planning artifacts | Clear scope, ready to build |
| `/opsx:new` | Start a change scaffold | Expanded mode, explicit artifact control |
| `/opsx:continue` | Create next artifact | Expanded mode, step-by-step artifact creation |
| `/opsx:ff` | Create all planning artifacts | Expanded mode, clear scope |
| `/opsx:apply` | Implement tasks | Ready to write code |
| `/opsx:verify` | Validate implementation | Before archiving, catch mismatches |
| `/opsx:sync` | Merge delta specs | Optional—archive prompts if needed |
| `/opsx:verify` | Validate implementation | Expanded mode, before archiving |
| `/opsx:sync` | Merge delta specs | Expanded mode, optional |
| `/opsx:archive` | Complete the change | All work finished |
| `/opsx:bulk-archive` | Archive multiple changes | Parallel work, batch completion |
| `/opsx:bulk-archive` | Archive multiple changes | Expanded mode, parallel work |
## Next Steps
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-25
@@ -0,0 +1,48 @@
## Context
The OpenCode adapter in `src/core/command-generation/adapters/opencode.ts` currently generates command files at `.opencode/command/opsx-<id>.md` (singular `command`). OpenCode's official documentation uses `.opencode/commands/` (plural), and every other adapter in the codebase follows the plural convention for commands directories. The legacy cleanup module in `src/core/legacy-cleanup.ts` also references the singular form for detecting old artifacts.
## Goals / Non-Goals
**Goals:**
- Align the OpenCode adapter path with OpenCode's official `.opencode/commands/` convention
- Add the old singular path `.opencode/command/` to legacy cleanup so existing installations are properly cleaned
- Update documentation to reflect the corrected path
- Update test assertions to match the new path
**Non-Goals:**
- Changing the OpenCode skill path (`.opencode/skills/`) — already correct
- Modifying any other adapter's directory structure
- Adding migration prompts or interactive upgrade flows
## Decisions
### 1. Direct path rename in adapter
**Decision:** Change `path.join('.opencode', 'command', ...)` to `path.join('.opencode', 'commands', ...)` in the adapter's `getFilePath` method.
**Rationale:** This is a single-line change that aligns with the established pattern across all other adapters. No abstraction or indirection needed.
**Alternatives considered:**
- Add a configuration option for the directory name — rejected as over-engineering for a bug fix
- Keep singular and add plural as alias — rejected as it creates ambiguity about which is canonical
### 2. Legacy cleanup via existing constant map
**Decision:** Update the `LEGACY_SLASH_COMMAND_PATHS` entry for `'opencode'` from `'.opencode/command/openspec-*.md'` to `'.opencode/command/opsx-*.md'` (the old singular path becomes the legacy pattern) and ensure the new path is handled by the current command generation pipeline.
**Rationale:** The existing legacy cleanup infrastructure uses `LEGACY_SLASH_COMMAND_PATHS` as an explicit lookup. The old singular-path pattern already matches the legacy format (`openspec-*` prefix from the old SlashCommandRegistry era). The current command generation uses the `opsx-*` prefix, so we also need to add a legacy pattern for `opsx-*` files in the old singular directory.
**Alternatives considered:**
- Add a separate migration script — rejected; the existing legacy cleanup mechanism handles this scenario
### 3. Documentation update
**Decision:** Update the `docs/supported-tools.md` table entry for OpenCode from `.opencode/command/opsx-<id>.md` to `.opencode/commands/opsx-<id>.md`.
**Rationale:** Documentation must match the actual generated paths.
## Risks / Trade-offs
- **[Existing installations have files at old path]** → Mitigated by legacy cleanup detecting `.opencode/command/` artifacts. On next `openspec init`, old files are cleaned up and new files written to `.opencode/commands/`.
- **[Users referencing old path in custom scripts]** → Low risk. The old path was incorrect per OpenCode's specification, so custom references were already misaligned.
@@ -0,0 +1,26 @@
## Why
The OpenCode adapter uses `.opencode/command/` (singular) for its commands directory, but OpenCode's official documentation specifies `.opencode/commands/` (plural). Every other adapter in the codebase also uses plural directory names (`.claude/commands/`, `.cursor/commands/`, `.factory/commands/`, etc.). This inconsistency was introduced in Oct 2025 without documented rationale. Fixes [#748](https://github.com/Fission-AI/OpenSpec/issues/748).
## What Changes
- OpenCode adapter path changes from `.opencode/command/` to `.opencode/commands/`
- Legacy cleanup adds `.opencode/command/` (old singular path) for backward compatibility
- Documentation updated to reflect the new plural path
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
- `command-generation`: OpenCode adapter path changes from singular `command/` to plural `commands/` to match OpenCode's official directory convention
## Impact
- `src/core/command-generation/adapters/opencode.ts` — adapter path
- `src/core/legacy-cleanup.ts` — legacy cleanup pattern + add old singular path
- `docs/supported-tools.md` — documentation table
- `test/core/command-generation/adapters.test.ts` — test assertion
@@ -0,0 +1,63 @@
## MODIFIED Requirements
### Requirement: ToolCommandAdapter interface
The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting.
#### Scenario: Adapter interface structure
- **WHEN** implementing a tool adapter
- **THEN** `ToolCommandAdapter` SHALL require:
- `toolId`: string identifier matching `AIToolOption.value`
- `getFilePath(commandId: string)`: returns file path for command (relative from project root, or absolute for global-scoped tools like Codex)
- `formatFile(content: CommandContent)`: returns complete file content with frontmatter
#### Scenario: Claude adapter formatting
- **WHEN** formatting a command for Claude Code
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
- **AND** file path SHALL follow pattern `.claude/commands/opsx/<id>.md`
#### Scenario: Cursor adapter formatting
- **WHEN** formatting a command for Cursor
- **THEN** the adapter SHALL output YAML frontmatter with `name` as `/opsx-<id>`, `id`, `category`, `description` fields
- **AND** file path SHALL follow pattern `.cursor/commands/opsx-<id>.md`
#### Scenario: Windsurf adapter formatting
- **WHEN** formatting a command for Windsurf
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
- **AND** file path SHALL follow pattern `.windsurf/workflows/opsx-<id>.md`
#### Scenario: OpenCode adapter formatting
- **WHEN** formatting a command for OpenCode
- **THEN** the adapter SHALL output YAML frontmatter with `description` field
- **AND** file path SHALL follow pattern `.opencode/commands/opsx-<id>.md` using `path.join('.opencode', 'commands', ...)` for cross-platform compatibility
- **AND** the adapter SHALL transform colon-based command references (`/opsx:name`) to hyphen-based (`/opsx-name`) in the body
## ADDED Requirements
### Requirement: Legacy cleanup for renamed OpenCode command directory
The legacy cleanup module SHALL detect and remove old OpenCode command files from the previous singular `.opencode/command/` directory path.
#### Scenario: Detect old singular-path OpenCode command files
- **WHEN** running legacy artifact detection on a project with files matching `.opencode/command/opsx-*.md` or `.opencode/command/openspec-*.md`
- **THEN** the system SHALL include those files in the legacy slash command files list via `LEGACY_SLASH_COMMAND_PATHS`
- **AND** `LegacySlashCommandPattern.pattern` SHALL accept `string | string[]` to support multiple glob patterns per tool
#### Scenario: Clean up old OpenCode command files on init
- **WHEN** a user runs `openspec init` in a project with old `.opencode/command/` artifacts
- **THEN** the system SHALL remove the old files
- **AND** generate new command files at `.opencode/commands/`
#### Scenario: Auto-cleanup legacy artifacts in non-interactive mode
- **WHEN** a user runs `openspec init` in non-interactive mode (e.g., CI) and legacy artifacts are detected
- **THEN** the system SHALL auto-cleanup legacy artifacts without requiring `--force`
- **AND** legacy slash command files (100% OpenSpec-managed) SHALL be removed
- **AND** config file cleanup SHALL only remove OpenSpec markers (never delete user files)
@@ -0,0 +1,19 @@
## 1. Adapter Fix
- [x] 1.1 Update `src/core/command-generation/adapters/opencode.ts`: change `path.join('.opencode', 'command', ...)` to `path.join('.opencode', 'commands', ...)` and update the JSDoc comment
## 2. Legacy Cleanup
- [x] 2.1 Update `src/core/legacy-cleanup.ts`: update the `'opencode'` entry in `LEGACY_SLASH_COMMAND_PATHS` to detect both `opsx-*.md` and `openspec-*.md` patterns at `.opencode/command/` for backward compatibility
## 3. Documentation
- [x] 3.1 Update `docs/supported-tools.md`: change OpenCode command path from `.opencode/command/opsx-<id>.md` to `.opencode/commands/opsx-<id>.md`
## 4. Tests
- [x] 4.1 Update `test/core/command-generation/adapters.test.ts`: change the OpenCode file path assertion from `path.join('.opencode', 'command', 'opsx-explore.md')` to `path.join('.opencode', 'commands', 'opsx-explore.md')`
## 5. Changeset
- [x] 5.1 Create a changeset file (`.changeset/fix-opencode-commands-directory.md`) with a patch bump describing the path fix
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-25
@@ -0,0 +1,38 @@
## Context
`statusCommand` in `src/commands/workflow/status.ts` calls `validateChangeExists()` from `shared.ts` as its first operation. When no `--change` option is provided and no change directories exist, `validateChangeExists` throws: `No changes found. Create one with: openspec new change <name>`. This error propagates up as a fatal CLI error (non-zero exit code).
This is correct behavior for commands like `apply` and `show` that require a change to operate on. However, `status` is an informational command — it should report the current state, even when that state is "no changes exist."
The error surfaces during onboarding (issue #714) when AI agents call `openspec status` before any change has been created.
## Goals / Non-Goals
**Goals:**
- Make `openspec status` exit with code 0 and a friendly message when no changes exist
- Support both text and JSON output modes for the no-changes case
- Keep all other commands' validation behavior unchanged
**Non-Goals:**
- Changing the behavior of `validateChangeExists` (keep it strict for all consumers; only extract its internal helper)
- Changing the onboard template or skill instructions
- Handling the case where `--change` is provided but the specific change doesn't exist (this should remain an error)
## Decisions
### Extract `getAvailableChanges` and check before validation
**Rationale**: Extract the private `getAvailableChanges` closure from `validateChangeExists` into a public exported function in `shared.ts`. Then, in `statusCommand`, call `getAvailableChanges` *before* `validateChangeExists` to detect the no-changes case early and handle it gracefully. This avoids using try/catch for control flow and eliminates any coupling to error message strings.
**Alternative considered**: Catching the error from `validateChangeExists` by matching `error.message.startsWith('No changes found')`. Rejected because string coupling is fragile — if the error message changes, the catch silently stops working.
**Alternative considered**: Adding a `throwOnEmpty` parameter to `validateChangeExists`. Rejected because it adds complexity to a shared function for a single consumer's needs and mixes UX concerns into a validation utility.
### Keep `validateChangeExists` strict
**Rationale**: `validateChangeExists` remains unchanged in behavior — it still throws for all error cases. The graceful handling lives entirely in `statusCommand`, which is the appropriate layer for UX decisions. Other commands (`apply`, `show`, `instructions`) are unaffected.
## Risks / Trade-offs
- [Risk] Extra filesystem read when no `--change` is provided and changes *do* exist (`getAvailableChanges` is called first, then `validateChangeExists` performs its own read) → Mitigation: `statusCommand` returns early before reaching `validateChangeExists` when no changes exist, so the double-read only occurs when changes are present — minimal overhead.
- [Risk] Other commands may also benefit from graceful no-changes handling in the future → Mitigation: `getAvailableChanges` is now public and reusable, making it easy to apply the same pattern elsewhere.
@@ -0,0 +1,25 @@
## Why
When `openspec status` is called without `--change` and no changes exist (e.g., during onboarding on a freshly initialized project), the CLI throws a fatal error: `No changes found. Create one with: openspec new change <name>`. This breaks the onboarding flow because AI agents may call `openspec status` before any change has been created, causing the agent to halt or report failure. Fixes [#714](https://github.com/Fission-AI/OpenSpec/issues/714).
## What Changes
- `openspec status` will exit gracefully (code 0) with a friendly message when no changes exist, instead of throwing a fatal error
- `openspec status --json` will return a valid JSON object with an empty changes array when no changes exist
- Other commands (`apply`, `show`, etc.) retain their current strict validation behavior
## Capabilities
### New Capabilities
- `graceful-status-empty`: Graceful handling of `openspec status` when no changes exist, covering both text and JSON output modes
### Modified Capabilities
_None — `validateChangeExists` was internally refactored to delegate to the newly exported `getAvailableChanges`, but its behavior and public contract are unchanged. Other consumers are unaffected._
## Impact
- `src/commands/workflow/shared.ts` — extract `getAvailableChanges` as a public function (validation behavior unchanged)
- `src/commands/workflow/status.ts` — check for available changes before validation, handle empty case gracefully
- Tests for the status command need to cover the new graceful behavior
@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: Status command exits gracefully when no changes exist
The `statusCommand` function SHALL check for available changes via `getAvailableChanges` before calling `validateChangeExists`. When no `--change` option is provided and no change directories exist, it SHALL print a friendly informational message and exit with code 0, instead of reaching `validateChangeExists` and propagating a fatal error.
#### Scenario: No changes exist, text mode
- **WHEN** user runs `openspec status` without `--change` and no change directories exist under `openspec/changes/`
- **THEN** the CLI prints `No active changes. Create one with: openspec new change <name>` to stdout and exits with code 0
#### Scenario: No changes exist, JSON mode
- **WHEN** user runs `openspec status --json` without `--change` and no change directories exist
- **THEN** the CLI outputs `{"changes":[],"message":"No active changes."}` as valid JSON to stdout and exits with code 0
### Requirement: Existing status validation behavior is preserved
Other error paths in `validateChangeExists` that apply to the status command SHALL continue to throw errors as before. Commands other than `status` that use `validateChangeExists` SHALL NOT be affected.
#### Scenario: Changes exist but --change not specified
- **WHEN** user runs `openspec status` without `--change` and one or more change directories exist
- **THEN** the CLI throws an error listing available changes with the message `Missing required option --change. Available changes: ...`
#### Scenario: Specified change does not exist
- **WHEN** user runs `openspec status --change non-existent`
- **THEN** the CLI throws an error with message `Change 'non-existent' not found`
#### Scenario: Other commands unaffected
- **WHEN** user runs `openspec show` or `openspec instructions` without `--change` and no changes exist
- **THEN** the CLI throws the original `No changes found` error (no behavior change)
@@ -0,0 +1,16 @@
## 1. Implementation
- [x] 1.1 Extract `getAvailableChanges` in `shared.ts` and use it in `statusCommand` to check for changes before calling `validateChangeExists`
- [x] 1.2 In text mode: print `No active changes. Create one with: openspec new change <name>` and return (exit 0)
- [x] 1.3 In JSON mode: output `{"changes":[],"message":"No active changes."}` and return (exit 0)
## 2. Tests
- [x] 2.1 Add test: `openspec status` with no changes exits gracefully with friendly message (text mode)
- [x] 2.2 Add test: `openspec status --json` with no changes returns valid JSON with empty changes array
- [x] 2.3 Verify existing behavior: `openspec status` without `--change` when changes exist still throws missing option error
- [x] 2.4 Verify cross-platform: tests use `path.join()` for any path assertions
## 3. Release
- [x] 3.1 Add changeset describing the fix
@@ -160,15 +160,14 @@ The update command SHALL only run inside an initialized OpenSpec project.
- **THEN** the system SHALL display: "No OpenSpec project found. Run 'openspec init' to set up."
- **THEN** the system SHALL exit with code 1
### Requirement: Extra workflows preserved
The update command SHALL NOT remove workflow files that aren't in the current profile.
### Requirement: Extra workflows synchronized to active profile
The update command SHALL remove workflow files that are no longer selected in the current profile.
#### Scenario: Extra workflows from previous profile
#### Scenario: Deselected workflows from previous profile
- **WHEN** user runs `openspec update`
- **AND** project has workflows not in current profile (e.g., user switched from custom to core)
- **THEN** the system SHALL NOT delete those extra workflow files
- **THEN** the system SHALL only add/update workflows in the current profile
- **THEN** the system SHALL display a note: "Note: <count> extra workflows not in profile (use `openspec config profile` to manage)"
- **AND** project has workflows not in current profile (e.g., user switched from custom to core or deselected workflows via `openspec config profile`)
- **THEN** the system SHALL delete skill and command workflow files for deselected workflows (respecting active delivery mode)
- **THEN** the system SHALL keep only workflows currently selected in profile
#### Scenario: Delivery change with extra workflows
- **WHEN** user runs `openspec update`
@@ -15,21 +15,19 @@ The design goal is to preserve current behavior while making extension points ex
- Define one canonical source for workflow content and metadata
- Make tool/agent-specific behavior explicit and centrally discoverable
- Keep command adapters as the formatting boundary for tool syntax differences
- Represent tool-specific command surfaces and terminology explicitly (not as scattered string rewrites)
- Consolidate artifact generation/write orchestration into one reusable engine
- Improve correctness with enforceable validation and parity tests
**Non-Goals:**
- Redesigning command semantics or workflow instruction content
- Changing user-facing CLI command names/flags in this proposal
- Guaranteeing fully accurate literal slash-command strings for every supported tool on day one
- Merging unrelated legacy cleanup behavior beyond artifact generation reuse
## Decisions
### 1. Canonical `WorkflowManifest`
**Decision**: Represent each workflow once in a manifest entry containing canonical skill and command definitions plus metadata defaults. Canonical text uses semantic tokens for tool-specific references.
**Decision**: Represent each workflow once in a manifest entry containing canonical skill and command definitions plus metadata defaults.
Suggested shape:
@@ -43,12 +41,6 @@ interface WorkflowManifestEntry {
tags: string[];
compatibility: string;
}
// Examples in canonical workflow text:
// - {{cmd.apply}}
// - {{cmd.continue.withArg}}
// - {{term.change}}
// - {{term.workflow}}
```
**Rationale**:
@@ -58,7 +50,7 @@ interface WorkflowManifestEntry {
### 2. `ToolProfileRegistry` for capability wiring
**Decision**: Add a tool profile layer that maps tool IDs to generation capabilities, command-surface rendering, and terminology.
**Decision**: Add a tool profile layer that maps tool IDs to generation capabilities and behavior.
Suggested shape:
@@ -67,16 +59,6 @@ interface ToolProfile {
toolId: string;
skillsDir?: string;
commandAdapterId?: string;
commandSurface: {
pattern: 'opsx-colon' | 'opsx-hyphen' | 'opsx-slash' | 'openspec-hyphen' | 'custom';
verified: boolean;
aliases?: string[];
};
terminology: {
change: string;
workflow: string;
command: string;
};
transforms: string[];
}
```
@@ -85,12 +67,10 @@ interface ToolProfile {
- Prevents capability drift between `AI_TOOLS`, adapter registry, and detection logic
- Allows intentional "skills-only" tools without implicit special casing
- Provides one place to answer "what does this tool support?"
- Makes command rendering decisions explicit and testable
- Supports future terminology tailoring without copy/paste template forks
### 3. First-class transform pipeline
**Decision**: Model transforms as ordered plugins with scope + phase + applicability. Include token rendering in the transform pipeline instead of hardcoding literal command strings in templates.
**Decision**: Model transforms as ordered plugins with scope + phase + applicability.
Suggested shape:
@@ -107,32 +87,17 @@ interface ArtifactTransform {
Execution order:
1. Render canonical content from manifest
2. Apply token-render transform (`{{cmd.*}}`, `{{term.*}}`) using tool profile
3. Apply matching `preAdapter` transforms
4. For commands, run adapter formatting
5. Apply matching `postAdapter` transforms
6. Validate and write
2. Apply matching `preAdapter` transforms
3. For commands, run adapter formatting
4. Apply matching `postAdapter` transforms
5. Validate and write
**Rationale**:
- Keeps adapters focused on tool formatting, not scattered behavioral rewrites
- Makes agent-specific modifications explicit and testable
- Replaces ad-hoc transform calls in `init`/`update`
- Enables neutral fallback rendering when a tool profile is not verified for literal command syntax
### 4. Fallback policy for unverified command surfaces
**Decision**: When a tool profile has `commandSurface.verified === false`, command tokens SHALL render to neutral workflow guidance instead of literal slash-command strings.
Examples:
- Literal (verified): `Run {{cmd.apply}}`
- Neutral (unverified): `Run the Apply workflow` or `use the apply skill`
**Rationale**:
- Prevents confidently wrong guidance in generated artifacts
- Allows incremental tool-surface verification without blocking rollout
- Keeps templates stable while rendering policy evolves
### 5. Shared `ArtifactSyncEngine`
### 4. Shared `ArtifactSyncEngine`
**Decision**: Introduce a single orchestration engine used by all generation entry points.
@@ -147,15 +112,13 @@ Responsibilities:
- Enables dry-run and future preview features without re-implementing logic
- Improves reliability of updates and legacy migrations
### 6. Validation + parity guardrails
### 5. Validation + parity guardrails
**Decision**: Add strict checks in tests (and optional runtime assertions in dev builds) for:
- Required skill metadata fields (`license`, `compatibility`, `metadata`) present for all manifest entries
- Projection consistency (skills, commands, detection names derived from manifest)
- Tool profile consistency (adapter existence, expected capabilities)
- Token coverage checks (no unresolved `{{...}}` tokens in rendered outputs)
- Tool command-surface verification matrix and fallback expectations
- Golden/parity output for key workflows/tools
**Rationale**:
@@ -180,8 +143,7 @@ Adding manifest/profile/transform registries increases conceptual surface area.
## Implementation Approach
1. Build manifest + profile + transform types and registries behind current public API
2. Tokenize command/terminology references in workflow templates
3. Rewire `getSkillTemplates`/`getCommandContents` to derive from manifest
4. Introduce `ArtifactSyncEngine` and switch `init` to use it with parity checks
5. Switch `update` and legacy upgrade flows to same engine
6. Remove duplicate/hardcoded lists after parity is green
2. Rewire `getSkillTemplates`/`getCommandContents` to derive from manifest
3. Introduce `ArtifactSyncEngine` and switch `init` to use it with parity checks
4. Switch `update` and legacy upgrade flows to same engine
5. Remove duplicate/hardcoded lists after parity is green
@@ -4,8 +4,7 @@ The recent split of `skill-templates.ts` into workflow modules improved readabil
- Workflow definitions are split from projection logic (`getSkillTemplates`, `getCommandTemplates`, `getCommandContents`)
- Tool capability and compatibility are spread across `AI_TOOLS`, `CommandAdapterRegistry`, and hardcoded lists like `SKILL_NAMES`
- Agent/tool-specific transformations are applied in different places (`init`, `update`, and adapter code)
- Command and terminology references are currently hardcoded in workflow text, but tool invocation surfaces vary (`/opsx:apply`, `/opsx-apply`, `/opsx/apply`, and tool-specific naming)
- Agent/tool-specific transformations (for example OpenCode command reference rewrites) are applied in different places (`init`, `update`, and adapter code)
- Artifact writing logic is duplicated across `init`, `update`, and legacy-upgrade flow
This fragmentation creates drift risk (missing exports, missing metadata parity, mismatched counts/support) and makes future workflow/tool additions slower and less predictable.
@@ -16,16 +15,13 @@ This fragmentation creates drift risk (missing exports, missing metadata parity,
- Introduce a `ToolProfileRegistry` to centralize tool capabilities (skills path, command adapter, transforms)
- Introduce a first-class transform pipeline with explicit phases (`preAdapter`, `postAdapter`) and scopes (`skill`, `command`, `both`)
- Introduce a shared `ArtifactSyncEngine` used by `init`, `update`, and legacy upgrade paths
- Add tokenized workflow text rendering so command references and tool terminology are resolved per tool profile at generation time
- Add explicit command-surface profiles per tool (pattern, namespace/path style, alias support, verification status)
- Add safe fallback behavior: when a tool command surface is not verified, render neutral workflow guidance (for example skill/workflow names) instead of potentially wrong literal command strings
- Add strict validation and test guardrails to preserve fidelity during migration and future changes
## Capabilities
### New Capabilities
- `template-artifact-pipeline`: Unified workflow manifest, tool profile registry, token-aware transform pipeline, and sync engine for skill/command generation
- `template-artifact-pipeline`: Unified workflow manifest, tool profile registry, transform pipeline, and sync engine for skill/command generation
### Modified Capabilities
@@ -45,9 +41,7 @@ This fragmentation creates drift risk (missing exports, missing metadata parity,
- **Testing additions**:
- Manifest completeness tests (workflows, required metadata, projection parity)
- Transform ordering and applicability tests
- Tool command-surface/terminology profile validation tests
- End-to-end parity tests for generated skill/command outputs across tools
- **User-facing behavior**:
- No new CLI surface area required
- Generated text may become more tool-accurate for verified tool profiles
- Generated text may intentionally use neutral workflow wording for unverified tools to avoid incorrect slash-command guidance
- Existing generated artifacts remain behaviorally equivalent unless explicitly changed in future deltas
@@ -10,18 +10,14 @@
- [ ] 2.1 Add `ToolProfile` types and `ToolProfileRegistry`
- [ ] 2.2 Map all currently supported tools to explicit profile entries
- [ ] 2.3 Wire profile lookups to command adapter resolution and skills path resolution
- [ ] 2.4 Add per-tool `commandSurface` metadata (pattern, aliases, `verified` flag)
- [ ] 2.5 Add per-tool terminology metadata (for example change/workflow/command labels)
- [ ] 2.6 Replace hardcoded detection arrays (for example `SKILL_NAMES`) with manifest-derived values
- [ ] 2.4 Replace hardcoded detection arrays (for example `SKILL_NAMES`) with manifest-derived values
## 3. Transform Pipeline
- [ ] 3.1 Introduce transform interfaces (`scope`, `phase`, `priority`, `applies`, `transform`)
- [ ] 3.2 Implement transform runner with deterministic ordering
- [ ] 3.3 Add token renderer transform for command + terminology tokens (`{{cmd.*}}`, `{{term.*}}`)
- [ ] 3.4 Implement neutral fallback rendering for tools with unverified command surfaces
- [ ] 3.5 Migrate OpenCode command reference rewrite to transform pipeline
- [ ] 3.6 Remove ad-hoc transform invocation from `init` and `update`
- [ ] 3.3 Migrate OpenCode command reference rewrite to transform pipeline
- [ ] 3.4 Remove ad-hoc transform invocation from `init` and `update`
## 4. Artifact Sync Engine
@@ -33,12 +29,10 @@
## 5. Validation and Tests
- [ ] 5.1 Add manifest completeness tests (metadata required fields, command IDs, dir names)
- [ ] 5.2 Add tool-profile consistency tests (skillsDir support, adapter/profile alignment, command-surface metadata)
- [ ] 5.3 Add token rendering tests (all tokens resolved, per-tool rendering correctness)
- [ ] 5.4 Add fallback tests for unverified tool command surfaces
- [ ] 5.5 Add transform applicability/order tests
- [ ] 5.6 Expand parity tests for representative workflow/tool matrix
- [ ] 5.7 Run full test suite and verify generated artifacts remain stable
- [ ] 5.2 Add tool-profile consistency tests (skillsDir support and adapter/profile alignment)
- [ ] 5.3 Add transform applicability/order tests
- [ ] 5.4 Expand parity tests for representative workflow/tool matrix
- [ ] 5.5 Run full test suite and verify generated artifacts remain stable
## 6. Cleanup and Documentation
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "1.1.1",
"version": "1.2.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+20 -17
View File
@@ -86,6 +86,23 @@ export function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string
}
}
/**
* Returns the list of available change directory names under openspec/changes/.
* Excludes the archive directory and hidden directories.
*/
export async function getAvailableChanges(projectRoot: string): Promise<string[]> {
const changesPath = path.join(projectRoot, 'openspec', 'changes');
try {
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
return entries
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
.map((e) => e.name);
} catch (error: unknown) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
throw error;
}
}
/**
* Validates that a change exists and returns available changes if not.
* Checks directory existence directly to support scaffolded changes (without proposal.md).
@@ -94,22 +111,8 @@ export async function validateChangeExists(
changeName: string | undefined,
projectRoot: string
): Promise<string> {
const changesPath = path.join(projectRoot, 'openspec', 'changes');
// Get all change directories (not just those with proposal.md)
const getAvailableChanges = async (): Promise<string[]> => {
try {
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
return entries
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
.map((e) => e.name);
} catch {
return [];
}
};
if (!changeName) {
const available = await getAvailableChanges();
const available = await getAvailableChanges(projectRoot);
if (available.length === 0) {
throw new Error('No changes found. Create one with: openspec new change <name>');
}
@@ -125,11 +128,11 @@ export async function validateChangeExists(
}
// Check directory existence directly
const changePath = path.join(changesPath, changeName);
const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory();
if (!exists) {
const available = await getAvailableChanges();
const available = await getAvailableChanges(projectRoot);
if (available.length === 0) {
throw new Error(
`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`
+22
View File
@@ -14,6 +14,7 @@ import {
import {
validateChangeExists,
validateSchemaExists,
getAvailableChanges,
getStatusIndicator,
getStatusColor,
} from './shared.js';
@@ -37,6 +38,27 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
try {
const projectRoot = process.cwd();
// Handle no-changes case gracefully — status is informational,
// so "no changes" is a valid state, not an error.
if (!options.change) {
const available = await getAvailableChanges(projectRoot);
if (available.length === 0) {
spinner.stop();
if (options.json) {
console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
return;
}
console.log('No active changes. Create one with: openspec new change <name>');
return;
}
// Changes exist but --change not provided
spinner.stop();
throw new Error(
`Missing required option --change. Available changes:\n ${available.join('\n ')}`
);
}
const changeName = await validateChangeExists(options.change, projectRoot);
// Validate schema if explicitly provided
+16 -2
View File
@@ -13,12 +13,26 @@ import { AI_TOOLS, type AIToolOption } from './config.js';
* Scans the project path for AI tool configuration directories and returns
* the tools that are present.
*
* Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the
* project root. Only tools with a `skillsDir` property are considered.
* For tools with `detectionPaths`, checks those specific paths (files or
* directories). Otherwise checks for the tool's `skillsDir` directory at
* the project root. Only tools with a `skillsDir` property are considered.
*/
export function getAvailableTools(projectPath: string): AIToolOption[] {
return AI_TOOLS.filter((tool) => {
if (!tool.skillsDir) return false;
if (tool.detectionPaths && tool.detectionPaths.length > 0) {
// statSync without .isDirectory() — detection paths can be files or directories
return tool.detectionPaths.some((p) => {
try {
fs.statSync(path.join(projectPath, p));
return true;
} catch {
return false;
}
});
}
const dirPath = path.join(projectPath, tool.skillsDir);
try {
return fs.statSync(dirPath).isDirectory();
@@ -10,14 +10,14 @@ import { transformToHyphenCommands } from '../../../utils/command-references.js'
/**
* OpenCode adapter for command generation.
* File path: .opencode/command/opsx-<id>.md
* File path: .opencode/commands/opsx-<id>.md
* Frontmatter: description
*/
export const opencodeAdapter: ToolCommandAdapter = {
toolId: 'opencode',
getFilePath(commandId: string): string {
return path.join('.opencode', 'command', `opsx-${commandId}.md`);
return path.join('.opencode', 'commands', `opsx-${commandId}.md`);
},
formatFile(content: CommandContent): string {
+3 -1
View File
@@ -15,6 +15,7 @@ export interface AIToolOption {
available: boolean;
successLabel?: string;
skillsDir?: string; // e.g., '.claude' - /skills suffix per Agent Skills spec
detectionPaths?: string[]; // Override skillsDir for auto-detection; any path existing triggers detection
}
export const AI_TOOLS: AIToolOption[] = [
@@ -24,6 +25,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
{ name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' },
@@ -31,7 +33,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' },
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] },
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
+4 -9
View File
@@ -208,19 +208,14 @@ export class InitCommand {
const canPrompt = this.canPromptInteractively();
if (this.force) {
// --force flag: proceed with cleanup automatically
if (this.force || !canPrompt) {
// --force flag or non-interactive mode: proceed with cleanup automatically.
// Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
// only removes markers (never deletes files), so auto-cleanup is safe.
await this.performLegacyCleanup(projectPath, detection);
return;
}
if (!canPrompt) {
// Non-interactive mode without --force: abort
console.log(chalk.red('Legacy files detected in non-interactive mode.'));
console.log(chalk.dim('Run interactively to upgrade, or use --force to auto-cleanup.'));
process.exit(1);
}
// Interactive mode: prompt for confirmation
const { confirm } = await import('@inquirer/prompts');
const shouldCleanup = await confirm({
+20 -11
View File
@@ -49,7 +49,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPatter
'roocode': { type: 'files', pattern: '.roo/commands/openspec-*.md' },
'auggie': { type: 'files', pattern: '.augment/commands/openspec-*.md' },
'factory': { type: 'files', pattern: '.factory/commands/openspec-*.md' },
'opencode': { type: 'files', pattern: '.opencode/command/openspec-*.md' },
'opencode': { type: 'files', pattern: ['.opencode/command/opsx-*.md', '.opencode/command/openspec-*.md'] },
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
@@ -63,7 +63,7 @@ export const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPatter
export interface LegacySlashCommandPattern {
type: 'directory' | 'files';
path?: string; // For directory type
pattern?: string; // For files type (glob pattern)
pattern?: string | string[]; // For files type (glob pattern or array of patterns)
}
/**
@@ -192,8 +192,11 @@ export async function detectLegacySlashCommands(
}
} else if (pattern.type === 'files' && pattern.pattern) {
// For file-based patterns, check for individual files
const foundFiles = await findLegacySlashCommandFiles(projectPath, pattern.pattern);
files.push(...foundFiles);
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
for (const p of patterns) {
const foundFiles = await findLegacySlashCommandFiles(projectPath, p);
files.push(...foundFiles);
}
}
}
@@ -604,14 +607,20 @@ export function getToolsFromLegacyArtifacts(detection: LegacyDetectionResult): s
if (pattern.type === 'files' && pattern.pattern) {
// Convert glob pattern to regex for matching
// e.g., '.cursor/commands/openspec-*.md' -> /^\.cursor\/commands\/openspec-.*\.md$/
const regexPattern = pattern.pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
.replace(/\*/g, '.*'); // Replace * with .*
const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(normalizedFile)) {
tools.add(toolId);
break;
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
let matched = false;
for (const p of patterns) {
const regexPattern = p
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
.replace(/\*/g, '.*'); // Replace * with .*
const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(normalizedFile)) {
tools.add(toolId);
matched = true;
break;
}
}
if (matched) break;
}
}
}
+25 -5
View File
@@ -80,11 +80,10 @@ export function getConfiguredToolsForProfileSync(projectPath: string): string[]
/**
* Detects if a single tool has profile/delivery drift against the desired state.
*
* Note: this function is intentionally scoped to "required artifacts missing"
* and "artifacts that should not exist for the selected delivery mode".
* Extra workflows that are outside the desired profile are handled by
* `hasProjectConfigDrift`, which compares installed workflow IDs against
* the desired workflow set.
* This function covers:
* - required artifacts missing for selected workflows
* - artifacts that should not exist for the selected delivery mode
* - artifacts for workflows that were deselected from the current profile
*/
export function hasToolProfileOrDeliveryDrift(
projectPath: string,
@@ -96,6 +95,7 @@ export function hasToolProfileOrDeliveryDrift(
if (!tool?.skillsDir) return false;
const knownDesiredWorkflows = toKnownWorkflows(desiredWorkflows);
const desiredWorkflowSet = new Set<WorkflowId>(knownDesiredWorkflows);
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
const adapter = CommandAdapterRegistry.get(toolId);
const shouldGenerateSkills = delivery !== 'commands';
@@ -109,6 +109,16 @@ export function hasToolProfileOrDeliveryDrift(
return true;
}
}
// Deselecting workflows in a profile should trigger sync.
for (const workflow of ALL_WORKFLOWS) {
if (desiredWorkflowSet.has(workflow)) continue;
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
const skillDir = path.join(skillsDir, dirName);
if (fs.existsSync(skillDir)) {
return true;
}
}
} else {
for (const workflow of ALL_WORKFLOWS) {
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
@@ -127,6 +137,16 @@ export function hasToolProfileOrDeliveryDrift(
return true;
}
}
// Deselecting workflows in a profile should trigger sync.
for (const workflow of ALL_WORKFLOWS) {
if (desiredWorkflowSet.has(workflow)) continue;
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
if (fs.existsSync(fullPath)) {
return true;
}
}
} else if (!shouldGenerateCommands && adapter) {
for (const workflow of ALL_WORKFLOWS) {
const cmdPath = adapter.getFilePath(workflow);
@@ -85,7 +85,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
Display a table summarizing all changes:
\`\`\`
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
| project-config | Done | 3/3 | 1 delta | None | Ready |
@@ -332,7 +332,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
Display a table summarizing all changes:
\`\`\`
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
| Change | Artifacts | Tasks | Specs | Conflicts | Status |
|---------------------|-----------|-------|---------|-----------|--------|
| schema-management | Done | 5/5 | 2 delta | None | Ready |
| project-config | Done | 3/3 | 1 delta | None | Ready |
+25 -25
View File
@@ -57,10 +57,10 @@ Depending on what the user brings, you might:
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
@@ -115,14 +115,14 @@ If the user mentions a change or you detect one is relevant:
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | \`specs/<capability>/spec.md\` |
| Requirement changed | \`specs/<capability>/spec.md\` |
| Design decision made | \`design.md\` |
| Scope changed | \`proposal.md\` |
| New work identified | \`tasks.md\` |
| Assumption invalidated | Relevant artifact |
| Insight Type | Where to Capture |
|----------------------------|--------------------------------|
| New requirement discovered | \`specs/<capability>/spec.md\` |
| Requirement changed | \`specs/<capability>/spec.md\` |
| Design decision made | \`design.md\` |
| Scope changed | \`proposal.md\` |
| New work identified | \`tasks.md\` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
@@ -228,7 +228,7 @@ User: A CLI tool that tracks local dev environments
You: That changes everything.
┌─────────────────────────────────────────────────┐
│ CLI TOOL DATA STORAGE │
│ CLI TOOL DATA STORAGE │
└─────────────────────────────────────────────────┘
Key constraints:
@@ -353,10 +353,10 @@ Depending on what the user brings, you might:
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
@@ -413,14 +413,14 @@ If the user mentions a change or you detect one is relevant:
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| New requirement discovered | \`specs/<capability>/spec.md\` |
| Requirement changed | \`specs/<capability>/spec.md\` |
| Design decision made | \`design.md\` |
| Scope changed | \`proposal.md\` |
| New work identified | \`tasks.md\` |
| Assumption invalidated | Relevant artifact |
| Insight Type | Where to Capture |
|----------------------------|--------------------------------|
| New requirement discovered | \`specs/<capability>/spec.md\` |
| Requirement changed | \`specs/<capability>/spec.md\` |
| Design decision made | \`design.md\` |
| Scope changed | \`proposal.md\` |
| New work identified | \`tasks.md\` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
+24 -24
View File
@@ -477,21 +477,21 @@ This same rhythm works for any size change—a small fix or a major feature.
**Core workflow:**
| Command | What it does |
|---------|--------------|
| \`/opsx:propose\` | Create a change and generate all artifacts |
| \`/opsx:explore\` | Think through problems before/during work |
| \`/opsx:apply\` | Implement tasks from a change |
| \`/opsx:archive\` | Archive a completed change |
| Command | What it does |
|-------------------|--------------------------------------------|
| \`/opsx:propose\` | Create a change and generate all artifacts |
| \`/opsx:explore\` | Think through problems before/during work |
| \`/opsx:apply\` | Implement tasks from a change |
| \`/opsx:archive\` | Archive a completed change |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| \`/opsx:new\` | Start a new change, step through artifacts one at a time |
| \`/opsx:continue\` | Continue working on an existing change |
| \`/opsx:ff\` | Fast-forward: create all artifacts at once |
| \`/opsx:verify\` | Verify implementation matches artifacts |
| Command | What it does |
|--------------------|----------------------------------------------------------|
| \`/opsx:new\` | Start a new change, step through artifacts one at a time |
| \`/opsx:continue\` | Continue working on an existing change |
| \`/opsx:ff\` | Fast-forward: create all artifacts at once |
| \`/opsx:verify\` | Verify implementation matches artifacts |
---
@@ -529,21 +529,21 @@ If the user says they just want to see the commands or skip the tutorial:
**Core workflow:**
| Command | What it does |
|---------|--------------|
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
| \`/opsx:explore\` | Think through problems (no code changes) |
| \`/opsx:apply <name>\` | Implement tasks |
| \`/opsx:archive <name>\` | Archive when done |
| Command | What it does |
|--------------------------|--------------------------------------------|
| \`/opsx:propose <name>\` | Create a change and generate all artifacts |
| \`/opsx:explore\` | Think through problems (no code changes) |
| \`/opsx:apply <name>\` | Implement tasks |
| \`/opsx:archive <name>\` | Archive when done |
**Additional commands:**
| Command | What it does |
|---------|--------------|
| \`/opsx:new <name>\` | Start a new change, step by step |
| \`/opsx:continue <name>\` | Continue an existing change |
| \`/opsx:ff <name>\` | Fast-forward: all artifacts at once |
| \`/opsx:verify <name>\` | Verify implementation |
| Command | What it does |
|---------------------------|-------------------------------------|
| \`/opsx:new <name>\` | Start a new change, step by step |
| \`/opsx:continue <name>\` | Continue an existing change |
| \`/opsx:ff <name>\` | Fast-forward: all artifacts at once |
| \`/opsx:verify <name>\` | Verify implementation |
Try \`/opsx:propose\` to start your first change.
\`\`\`
+80
View File
@@ -176,6 +176,8 @@ export class UpdateCommand {
const failedTools: Array<{ name: string; error: string }> = [];
let removedCommandCount = 0;
let removedSkillCount = 0;
let removedDeselectedCommandCount = 0;
let removedDeselectedSkillCount = 0;
for (const toolId of toolsToUpdate) {
const tool = AI_TOOLS.find((t) => t.value === toolId);
@@ -197,6 +199,8 @@ export class UpdateCommand {
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
await FileSystemUtils.writeFile(skillFile, skillContent);
}
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows);
}
// Delete skill directories if delivery is commands-only
@@ -214,6 +218,12 @@ export class UpdateCommand {
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
}
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(
resolvedProjectPath,
toolId,
desiredWorkflows
);
}
}
@@ -247,6 +257,12 @@ export class UpdateCommand {
if (removedSkillCount > 0) {
console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
}
if (removedDeselectedCommandCount > 0) {
console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
}
if (removedDeselectedSkillCount > 0) {
console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
}
// 12. Show onboarding message for newly configured tools from legacy upgrade
if (newlyConfiguredTools.length > 0) {
@@ -378,6 +394,36 @@ export class UpdateCommand {
return removed;
}
/**
* Removes skill directories for workflows that are no longer selected in the active profile.
* Returns the number of directories removed.
*/
private async removeUnselectedSkillDirs(
skillsDir: string,
desiredWorkflows: readonly (typeof ALL_WORKFLOWS)[number][]
): Promise<number> {
const desiredSet = new Set(desiredWorkflows);
let removed = 0;
for (const workflow of ALL_WORKFLOWS) {
if (desiredSet.has(workflow)) continue;
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
if (!dirName) continue;
const skillDir = path.join(skillsDir, dirName);
try {
if (fs.existsSync(skillDir)) {
await fs.promises.rm(skillDir, { recursive: true, force: true });
removed++;
}
} catch {
// Ignore errors
}
}
return removed;
}
/**
* Removes command files for workflows when delivery changed to skills-only.
* Returns the number of files removed.
@@ -408,6 +454,40 @@ export class UpdateCommand {
return removed;
}
/**
* Removes command files for workflows that are no longer selected in the active profile.
* Returns the number of files removed.
*/
private async removeUnselectedCommandFiles(
projectPath: string,
toolId: string,
desiredWorkflows: readonly (typeof ALL_WORKFLOWS)[number][]
): Promise<number> {
let removed = 0;
const adapter = CommandAdapterRegistry.get(toolId);
if (!adapter) return 0;
const desiredSet = new Set(desiredWorkflows);
for (const workflow of ALL_WORKFLOWS) {
if (desiredSet.has(workflow)) continue;
const cmdPath = adapter.getFilePath(workflow);
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
try {
if (fs.existsSync(fullPath)) {
await fs.promises.unlink(fullPath);
removed++;
}
} catch {
// Ignore errors
}
}
return removed;
}
/**
* Detect and handle legacy OpenSpec artifacts.
* Unlike init, update warns but continues if legacy files found in non-interactive mode.
+16
View File
@@ -131,6 +131,22 @@ describe('artifact-workflow CLI commands', () => {
expect(result.stdout).toContain('All artifacts complete!');
});
it('exits gracefully when no changes exist', async () => {
const result = await runCLI(['status'], { cwd: tempDir });
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('No active changes');
expect(result.stdout).toContain('openspec new change');
});
it('exits gracefully with JSON when no changes exist', async () => {
const result = await runCLI(['status', '--json'], { cwd: tempDir });
expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.changes).toEqual([]);
expect(json.message).toBe('No active changes.');
});
it('errors when --change is missing and lists available changes', async () => {
await createTestChange('some-change');
+61
View File
@@ -87,5 +87,66 @@ describe('available-tools', () => {
expect(tools).toHaveLength(1);
expect(tools[0].value).toBe('claude');
});
it('should not detect GitHub Copilot from bare .github directory', async () => {
// .github/ exists in virtually every GitHub repo (for workflows, issue templates, etc.)
// A bare .github/ directory should NOT trigger Copilot detection
await fs.mkdir(path.join(testDir, '.github'), { recursive: true });
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).not.toContain('github-copilot');
});
it('should detect GitHub Copilot when copilot-instructions.md exists', async () => {
await fs.mkdir(path.join(testDir, '.github'), { recursive: true });
await fs.writeFile(path.join(testDir, '.github', 'copilot-instructions.md'), '');
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('github-copilot');
});
it('should detect GitHub Copilot when .github/prompts directory exists', async () => {
await fs.mkdir(path.join(testDir, '.github', 'prompts'), { recursive: true });
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('github-copilot');
});
it('should detect GitHub Copilot when .github/agents directory exists', async () => {
await fs.mkdir(path.join(testDir, '.github', 'agents'), { recursive: true });
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('github-copilot');
});
it('should detect GitHub Copilot when .github/skills directory exists', async () => {
await fs.mkdir(path.join(testDir, '.github', 'skills'), { recursive: true });
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('github-copilot');
});
it('should detect GitHub Copilot when copilot-setup-steps.yml exists', async () => {
await fs.mkdir(path.join(testDir, '.github', 'workflows'), { recursive: true });
await fs.writeFile(path.join(testDir, '.github', 'workflows', 'copilot-setup-steps.yml'), '');
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('github-copilot');
});
it('should still use skillsDir detection for tools without detectionPaths', async () => {
// Claude Code has no detectionPaths, so .claude/ directory should still work
await fs.mkdir(path.join(testDir, '.claude'), { recursive: true });
const tools = getAvailableTools(testDir);
const toolValues = tools.map((t) => t.value);
expect(toolValues).toContain('claude');
});
});
});
@@ -444,7 +444,7 @@ describe('command-generation/adapters', () => {
it('should generate correct file path', () => {
const filePath = opencodeAdapter.getFilePath('explore');
expect(filePath).toBe(path.join('.opencode', 'command', 'opsx-explore.md'));
expect(filePath).toBe(path.join('.opencode', 'commands', 'opsx-explore.md'));
});
it('should format file with description frontmatter', () => {
+20
View File
@@ -536,6 +536,24 @@ describe('InitCommand - profile and detection features', () => {
expect(await fileExists(skillFile)).toBe(true);
});
it('should auto-cleanup legacy artifacts in non-interactive mode without --force', async () => {
// Create legacy OpenCode command files (singular 'command' path)
const legacyDir = path.join(testDir, '.opencode', 'command');
await fs.mkdir(legacyDir, { recursive: true });
await fs.writeFile(path.join(legacyDir, 'opsx-propose.md'), 'legacy content');
// Run init in non-interactive mode without --force
const initCommand = new InitCommand({ tools: 'opencode' });
await initCommand.execute(testDir);
// Legacy files should be cleaned up automatically
expect(await fileExists(path.join(legacyDir, 'opsx-propose.md'))).toBe(false);
// New commands should be at the correct plural path
const newCommandsDir = path.join(testDir, '.opencode', 'commands');
expect(await directoryExists(newCommandsDir)).toBe(true);
});
it('should preselect configured tools but not directory-detected tools in extend mode', async () => {
// Simulate existing OpenSpec project (extend mode).
await fs.mkdir(path.join(testDir, 'openspec'), { recursive: true });
@@ -547,6 +565,7 @@ describe('InitCommand - profile and detection features', () => {
// Directory detected only (not configured with OpenSpec)
await fs.mkdir(path.join(testDir, '.github'), { recursive: true });
await fs.writeFile(path.join(testDir, '.github', 'copilot-instructions.md'), '');
searchableMultiSelectMock.mockResolvedValue(['claude']);
@@ -569,6 +588,7 @@ describe('InitCommand - profile and detection features', () => {
it('should preselect detected tools for first-time interactive setup', async () => {
// First-time init: no openspec/ directory and no configured OpenSpec skills.
await fs.mkdir(path.join(testDir, '.github'), { recursive: true });
await fs.writeFile(path.join(testDir, '.github', 'copilot-instructions.md'), '');
searchableMultiSelectMock.mockResolvedValue(['github-copilot']);
+83
View File
@@ -335,6 +335,35 @@ ${OPENSPEC_MARKERS.end}`);
const result = await detectLegacySlashCommands(testDir);
expect(result.files).toContain('.continue/prompts/openspec-apply.prompt');
});
it('should detect legacy OpenCode opsx-* command files', async () => {
const dirPath = path.join(testDir, '.opencode', 'command');
await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(path.join(dirPath, 'opsx-propose.md'), 'content');
const result = await detectLegacySlashCommands(testDir);
expect(result.files).toContain('.opencode/command/opsx-propose.md');
});
it('should detect legacy OpenCode openspec-* command files', async () => {
const dirPath = path.join(testDir, '.opencode', 'command');
await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(path.join(dirPath, 'openspec-new.md'), 'content');
const result = await detectLegacySlashCommands(testDir);
expect(result.files).toContain('.opencode/command/openspec-new.md');
});
it('should detect both opsx-* and openspec-* OpenCode command files', async () => {
const dirPath = path.join(testDir, '.opencode', 'command');
await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(path.join(dirPath, 'opsx-propose.md'), 'content');
await fs.writeFile(path.join(dirPath, 'openspec-new.md'), 'content');
const result = await detectLegacySlashCommands(testDir);
expect(result.files).toContain('.opencode/command/opsx-propose.md');
expect(result.files).toContain('.opencode/command/openspec-new.md');
});
});
describe('detectLegacyStructureFiles', () => {
@@ -1058,6 +1087,60 @@ ${OPENSPEC_MARKERS.end}`);
expect(tools).toHaveLength(1);
});
it('should handle opencode opsx-* legacy files', () => {
const detection = {
configFiles: [],
configFilesToUpdate: [],
slashCommandDirs: [],
slashCommandFiles: ['.opencode/command/opsx-propose.md'],
hasOpenspecAgents: false,
hasProjectMd: false,
hasRootAgentsWithMarkers: false,
hasLegacyArtifacts: true,
};
const tools = getToolsFromLegacyArtifacts(detection);
expect(tools).toContain('opencode');
expect(tools).toHaveLength(1);
});
it('should handle opencode openspec-* legacy files', () => {
const detection = {
configFiles: [],
configFilesToUpdate: [],
slashCommandDirs: [],
slashCommandFiles: ['.opencode/command/openspec-new.md'],
hasOpenspecAgents: false,
hasProjectMd: false,
hasRootAgentsWithMarkers: false,
hasLegacyArtifacts: true,
};
const tools = getToolsFromLegacyArtifacts(detection);
expect(tools).toContain('opencode');
expect(tools).toHaveLength(1);
});
it('should deduplicate opencode when both opsx-* and openspec-* files exist', () => {
const detection = {
configFiles: [],
configFilesToUpdate: [],
slashCommandDirs: [],
slashCommandFiles: [
'.opencode/command/opsx-propose.md',
'.opencode/command/openspec-new.md',
],
hasOpenspecAgents: false,
hasProjectMd: false,
hasRootAgentsWithMarkers: false,
hasLegacyArtifacts: true,
};
const tools = getToolsFromLegacyArtifacts(detection);
expect(tools).toContain('opencode');
expect(tools).toHaveLength(1);
});
it('should not extract tools from config files only', () => {
// Config files don't indicate which tools were configured
// Only slash command dirs/files tell us which tools to upgrade
@@ -30,25 +30,25 @@ import {
import { generateSkillContent } from '../../../src/core/shared/skill-generation.js';
const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
getExploreSkillTemplate: '55a2a1afcba0af88c638e77e4e3870f65ed82c030b4a2056d39812ae13a616be',
getExploreSkillTemplate: '3f73b4d7ab189ef6367fccc9d99308bee35c6a89dae4c8044582a01cb01b335b',
getNewChangeSkillTemplate: '5989672758eccf54e3bb554ab97f2c129a192b12bbb7688cc1ffcf6bccb1ae9d',
getContinueChangeSkillTemplate: 'f2e413f0333dfd6641cc2bd1a189273fdea5c399eecdde98ef528b5216f097b3',
getApplyChangeSkillTemplate: '26e52e67693e93fbcdd40dcd3e20949c07ce019183d55a8149d0260c791cd7f4',
getFfChangeSkillTemplate: 'a7332fb14c8dc3f9dec71f5d332790b4a8488191e7db4ab6132ccbefecf9ded9',
getSyncSpecsSkillTemplate: 'bded184e4c345619148de2c0ad80a5b527d4ffe45c87cc785889b9329e0f465b',
getOnboardSkillTemplate: '819a2d117ad1386187975686839cb0584b41484013d0ca6a6691f7a439a11a4a',
getOpsxExploreCommandTemplate: '91353d9e8633a3a9ce7339e796f1283478fca279153f3807c92f4f8ece246b19',
getOnboardSkillTemplate: 'c9e719a02d2ae7f74a0e978f9ad4e767c1921248a9e3724c3321c58a15c38ba9',
getOpsxExploreCommandTemplate: 'b421b88c7a532385f7b1404736d7893eb35a05573b4a04a96f72379ac1bbf148',
getOpsxNewCommandTemplate: '62eee32d6d81a376e7be845d0891e28e6262ad07482f9bfe6af12a9f0366c364',
getOpsxContinueCommandTemplate: '8bbaedcc95287f9e822572608137df4f49ad54cedfb08d3342d0d1c4e9716caa',
getOpsxApplyCommandTemplate: 'a9d631a07fcd832b67d263ff3800b98604ab8d378baf1b0d545907ef3affa3b5',
getOpsxFfCommandTemplate: 'cdebe872cc8e0fcc25c8864b98ffd66a93484c0657db94bd1285b8113092702a',
getArchiveChangeSkillTemplate: '6f8ca383fdb5a4eb9872aca81e07bf0ba7f25e4de8617d7a047ca914ca7f14b9',
getBulkArchiveChangeSkillTemplate: 'b40fc44ea4e420bdc9c803985b10e5c091fc472cdfc69153b962be6be303bddd',
getBulkArchiveChangeSkillTemplate: '8049897ce1ddb2ff6c0d4b72e22636f9ecfd083b5f2c2a30cf3bb1cb828a2f93',
getOpsxSyncCommandTemplate: '378d035fe7cc30be3e027b66dcc4b8afc78ef1c8369c39479c9b05a582fb5ccf',
getVerifyChangeSkillTemplate: '63a213ba3b42af54a1cd56f5072234a03b265c3fe4a1da12cd6fbbef5ee46c4b',
getOpsxArchiveCommandTemplate: 'b44cc9748109f61687f9f596604b037bc3ea803abc143b22f09a76aebd98b493',
getOpsxOnboardCommandTemplate: '10052d05a4e2cdade7fdfa549b3444f7a92f55a39bf81ddd6af7e0e9e83a7302',
getOpsxBulkArchiveCommandTemplate: 'eaaba253a950b9e681d8427a5cbc6b50c4e91137fb37fd2360859e08f63a0c14',
getOpsxOnboardCommandTemplate: 'fce531f952e939ee85a41848fc21e4cc720b0f3eb62737adc3a51ee6ad2dfc57',
getOpsxBulkArchiveCommandTemplate: '0d77c82de43840a28c74f5181cb21e33b9a9d00454adf4bc92bdc9e69817d6f5',
getOpsxVerifyCommandTemplate: '9b4d3ca422553b7534764eb3a009da87a051612c5238e9baab294c7b1233e9a2',
getOpsxProposeSkillTemplate: 'd67f937d44650e9c61d2158c865309fbab23cb3f50a3d4868a640a97776e3999',
getOpsxProposeCommandTemplate: '41ad59b37eafd7a161bab5c6e41997a37368f9c90b194451295ede5cd42e4d46',
@@ -56,16 +56,16 @@ const EXPECTED_FUNCTION_HASHES: Record<string, string> = {
};
const EXPECTED_GENERATED_SKILL_CONTENT_HASHES: Record<string, string> = {
'openspec-explore': '90463d00761417dfbca5cb09361adcf8bbdbbb24000b86dd03647869a4104479',
'openspec-explore': '08e1ec9958eb04653707dd3e198c3fd69cf1b3acd3cf95a1022693cca83c60fc',
'openspec-new-change': 'c324a7ace1f244aa3f534ac8e3370a2c11190d6d1b85a315f26a211398310f0f',
'openspec-continue-change': '463cf0b980ec9c3c24774414ef2a3e48e9faa8577bc8748990f45ab3d5efe960',
'openspec-apply-change': 'a0084442b59be9d7e22a0382a279d470501e1ecf74bdd5347e169951c9be191c',
'openspec-ff-change': '672c3a5b8df152d959b15bd7ae2be7a75ab7b8eaa2ec1e0daa15c02479b27937',
'openspec-sync-specs': 'b8859cf454379a19ca35dbf59eedca67306607f44a355327f9dc851114e50bde',
'openspec-archive-change': 'f83c85452bd47de0dee6b8efbcea6a62534f8a175480e9044f3043f887cebf0f',
'openspec-bulk-archive-change': 'a235a539f7729ab7669e45256905808789240ecd02820e044f4d0eef67b0c2ab',
'openspec-bulk-archive-change': '10477399bb07c7ba67f78e315bd68fb1901af8866720545baf4c62a6a679493b',
'openspec-verify-change': '30d07c6f7051965f624f5964db51844ec17c7dfd05f0da95281fe0ca73616326',
'openspec-onboard': 'dbce376cf895f3fe4f63b4bce66d258c35b7b8884ac746670e5e35fabcefd255',
'openspec-onboard': 'c1444e026028210efd699110f7e9079bcb486d85ccf27f743213a81cb1084303',
'openspec-propose': '20e36dabefb90e232bad0667292bd5007ec280f8fc4fc995dbc4282bf45a22e7',
};
+15 -5
View File
@@ -1567,7 +1567,7 @@ content
consoleSpy.mockRestore();
});
it('should display extra workflows note when workflows outside profile exist', async () => {
it('should remove workflows outside profile during update sync', async () => {
// Set core profile (propose, explore, apply, archive)
setMockConfig({
featureFlags: {},
@@ -1583,19 +1583,28 @@ content
// Add a non-core workflow
await fs.mkdir(path.join(skillsDir, 'openspec-new-change'), { recursive: true });
await fs.writeFile(path.join(skillsDir, 'openspec-new-change', 'SKILL.md'), 'old');
const extraCommandFile = path.join(testDir, '.claude', 'commands', 'opsx', 'new.md');
await fs.mkdir(path.dirname(extraCommandFile), { recursive: true });
await fs.writeFile(extraCommandFile, 'old');
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
// Should display note about extra workflows
// Deselected workflow artifacts should be removed for both delivery surfaces.
expect(await FileSystemUtils.fileExists(
path.join(skillsDir, 'openspec-new-change', 'SKILL.md')
)).toBe(false);
expect(await FileSystemUtils.fileExists(extraCommandFile)).toBe(false);
// Should report deselected workflow cleanup.
const calls = consoleSpy.mock.calls.map(call =>
call.map(arg => String(arg)).join(' ')
);
const hasExtraNote = calls.some(call =>
call.includes('extra workflows not in profile')
const hasDeselectedRemovalNote = calls.some(call =>
call.includes('deselected workflows')
);
expect(hasExtraNote).toBe(true);
expect(hasDeselectedRemovalNote).toBe(true);
consoleSpy.mockRestore();
});
@@ -1635,6 +1644,7 @@ content
// Create two unconfigured tool directories
await fs.mkdir(path.join(testDir, '.github'), { recursive: true });
await fs.writeFile(path.join(testDir, '.github', 'copilot-instructions.md'), '');
await fs.mkdir(path.join(testDir, '.windsurf'), { recursive: true });
const consoleSpy = vi.spyOn(console, 'log');