Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale e5cd35ae29 docs: expand OPSX workflow section with detailed examples
Significantly expanded the versioning/archiving guide to better
address user concerns about parallel development conflicts:

- Added TL;DR with jump link to solution
- Expanded OPSX section from 8 lines to ~120 lines
- Added step-by-step authentication conflict example
- Added three-team concrete example showing all merge scenarios
- Added two workflow options (Friday batch vs sync-as-you-go)
- Added "What cases does this handle" section
- Updated quick reference card to lead with OPSX
- Restructured flow: Problem → OPSX Solution → Manual fallback
- Moved manual practices to "If You Can't Use OPSX Yet" section

The guide now clearly positions OPSX as the solution with
actionable examples instead of being buried as a light mention.
2026-01-13 13:12:11 -08:00
Tabish Bidiwale 33d8b9d96f docs: add versioning and archiving best practices guide
Add comprehensive guide covering:
- Silent data loss issue when multiple branches modify same requirements
- Best practices for batch archiving workflow
- Archive ordering strategies to prevent conflicts
- CodeRabbit configuration recommendations
- OPSX workflow as safer alternative
- Roadmap for upcoming conflict resolution features

Addresses common pain points teams encounter with parallel development
and weekly batch archiving workflows.
2026-01-13 10:21:18 -08:00
Tabish Bidiwale 36078b1947 chore: improve release notes pipeline (#481)
* chore: improve changelog generation with GitHub integration

- Switch to @changesets/changelog-github for PR/author links in CHANGELOG.md
- Add comprehensive changeset README with template and contributor guidance
- Remove release:local script (CI-only releases)

* ci: add AI-powered release notes polishing

Transforms raw changelog into user-friendly release notes when a
GitHub Release is published. Uses Claude Code Action to:
- Generate concise release title (e.g., "v0.18.0 - OPSX Workflow")
- Rewrite changelog as developer-friendly release notes
- Remove noise (commit hashes, PR numbers, internal changes)

Requires CLAUDE_CODE_OAUTH_TOKEN secret (from claude setup-token).
2026-01-10 22:33:43 -08:00
Tabish Bidiwale d0e1b076c2 chore: trigger release workflow for v0.19.0 (#480) 2026-01-10 18:20:30 -08:00
Tabish Bidiwale 2fbda520de ci: remove auto-merge to fix release triggering (#479)
GitHub's auto-merge feature uses an internal token that cannot trigger
workflows. Even with manual approval, the merge performed by auto-merge
doesn't trigger the release workflow.

Removing auto-merge means:
1. Version PR is created with CI running
2. User manually merges the PR (clicking "Merge")
3. The merge triggers the release workflow
4. Package is published

This aligns with industry standard practice for changesets.
2026-01-10 18:18:09 -08:00
github-actions[bot] 5633556b6d Version Packages (#475)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-10 18:04:55 -08:00
Tabish Bidiwale 2bb0ed36c5 ci: pass GitHub App token to checkout for CI triggering (#478)
Move the GitHub App token generation before checkout and pass it
to actions/checkout. This configures git to use the App's identity
for all operations, allowing pushes to trigger CI workflows.

Removes commitMode: github-api which doesn't support executable files.
2026-01-10 18:01:05 -08:00
Tabish Bidiwale 06097f9cb7 ci: add commitMode github-api to trigger CI on version PR (#477)
* ci: use GitHub App token to trigger CI on version PR

Replace GITHUB_TOKEN with a GitHub App token so that the version PR
can trigger CI workflows. GITHUB_TOKEN cannot trigger workflows by
design (to prevent infinite loops).

Requires APP_ID variable and APP_PRIVATE_KEY secret to be configured.

* ci: upgrade create-github-app-token to v2

* ci: add commitMode github-api to trigger CI on version PR
2026-01-10 17:50:59 -08:00
Tabish Bidiwale 8f5a526396 ci: use GitHub App token to trigger CI on version PR (#476)
* ci: use GitHub App token to trigger CI on version PR

Replace GITHUB_TOKEN with a GitHub App token so that the version PR
can trigger CI workflows. GITHUB_TOKEN cannot trigger workflows by
design (to prevent infinite loops).

Requires APP_ID variable and APP_PRIVATE_KEY secret to be configured.

* ci: upgrade create-github-app-token to v2
2026-01-10 17:38:57 -08:00
Tabish Bidiwale eb152eb2ca ci: auto-merge version PR to streamline releases (#474)
* Add changeset for Continue support, shell completions, and explore command

* ci: auto-merge version PR to streamline releases

Enable auto-merge on the changesets version PR so it merges
automatically once CI passes. This reduces the release process
from 2 manual PR merges to effectively 1.

Requires enabling "Allow auto-merge" in repository settings
and branch protection rules on main.
2026-01-10 16:22:25 -08:00
e987a5a327 Add Continue support (#402)
* OpenSpec 支持 Continue 插件

* add update

* Update spec.md

* fix: correct Continue frontmatter format and README ordering

- Fix Continue frontmatter to use proper YAML format with opening `---`
  and required `invokable: true` field for slash command availability
- Fix README table ordering: Continue should be after Codex alphabetically
- Remove unused TemplateManager import from continue.ts
- Fix trailing whitespace in update.test.ts
- Fix double blank line in init.test.ts
- Fix extra blank line in cli-init/spec.md
- Update tests to verify correct frontmatter format

---------

Co-authored-by: ajuanli <ajuanli@tencent.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2026-01-10 15:54:09 -08:00
Tabish Bidiwale 4971cda812 fix: use actionCommand for telemetry command tracking (#472)
The preAction hook receives (thisCommand, actionCommand) where thisCommand
is the root program and actionCommand is the actual subcommand being run.
Using thisCommand was incorrectly tracking the root command instead of
the actual subcommand executed by the user.
2026-01-10 15:10:12 -08:00
17 changed files with 867 additions and 11 deletions
+93 -4
View File
@@ -1,6 +1,95 @@
This directory is managed by Changesets.
# Changesets
- Add a changeset locally with `pnpm changeset`.
- The CI "Release (prepare)" workflow opens/updates a Version Packages PR.
- Publishing happens from a GitHub Release via the "Publish to npm" workflow.
This directory is managed by [Changesets](https://github.com/changesets/changesets).
## Quick Start
```bash
pnpm changeset
```
Follow the prompts to select version bump type and describe your changes.
## Workflow
1. **Add a changeset** — Run `pnpm changeset` locally before or after your PR
2. **Version PR** — CI opens/updates a "Version Packages" PR when changesets merge to main
3. **Release** — Merging the Version PR triggers npm publish and GitHub Release
> **Note:** Contributors only need to run `pnpm changeset`. Versioning (`changeset version`) and publishing happen automatically in CI.
## Template
Use this structure for your changeset content:
```markdown
---
"@fission-ai/openspec": patch
---
### New Features
- **Feature name** — What users can now do
### Bug Fixes
- Fixed issue where X happened when Y
### Breaking Changes
- `oldMethod()` has been removed, use `newMethod()` instead
### Deprecations
- `legacyOption` is deprecated and will be removed in v2.0
### Other
- Internal refactoring of X for better performance
```
Include only the sections relevant to your change.
## Version Bump Guide
| Type | When to use | Example |
|------|-------------|---------|
| `patch` | Bug fixes, small improvements | Fixed crash when config missing |
| `minor` | New features, non-breaking additions | Added `--verbose` flag |
| `major` | Breaking changes, removed features | Renamed `init` to `setup` |
## When to Create a Changeset
**Create one for:**
- New features or commands
- Bug fixes that affect users
- Breaking changes or deprecations
- Performance improvements users would notice
**Skip for:**
- Documentation-only changes
- Test additions/fixes
- Internal refactoring with no user impact
- CI/tooling changes
## Writing Good Descriptions
**Do:** Write for users, not developers
```markdown
- **Shell completions** — Tab completion now available for Bash, Fish, and PowerShell
```
**Don't:** Write implementation details
```markdown
- Added ShellCompletionGenerator class with Bash/Fish/PowerShell subclasses
```
**Do:** Explain the impact
```markdown
- Fixed config loading to respect `XDG_CONFIG_HOME` on Linux
```
**Don't:** Just reference the fix
```markdown
- Fixed #123
```
+4 -1
View File
@@ -1,6 +1,9 @@
{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"changelog": [
"@changesets/changelog-github",
{ "repo": "Fission-AI/OpenSpec" }
],
"commit": false,
"fixed": [],
"linked": [],
+137
View File
@@ -0,0 +1,137 @@
name: Polish Release Notes
# Triggers when changesets creates a release
on:
release:
types: [published]
permissions:
contents: write
jobs:
polish:
# Only run on the main repo, not forks
if: github.repository == 'Fission-AI/OpenSpec'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get current release body
id: get-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release view "${{ github.event.release.tag_name }}" --json body -q '.body' > current-notes.md
echo "Fetched release notes for ${{ github.event.release.tag_name }}"
- name: Transform release notes with Claude
uses: anthropics/claude-code-action@v1
id: claude
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
direct_prompt: |
Transform the changelog in `current-notes.md` into release notes for OpenSpec ${{ github.event.release.tag_name }}.
## Voice
OpenSpec is a developer tool. Write like you're talking to a peer:
- Direct and practical, not marketing copy
- Focus on what changed and why it matters
- Skip the hype, keep it real
## Output
Create two files:
### 1. `release-title.txt`
A short title in this format:
```
${{ github.event.release.tag_name }} - [1-4 words describing the release]
```
Examples:
- `v0.18.0 - OPSX Experimental Workflow`
- `v0.16.0 - Antigravity, iFlow Support`
- `v0.15.0 - Gemini CLI, RooCode`
Rules for title:
- Lead with the most notable addition
- 1-4 words after the dash, no fluff
- If multiple features, comma-separate the top 2
- For bugfix-only releases, use something like `v0.17.2 - Pre-commit Hook Fix`
### 2. `polished-notes.md`
```markdown
## What's New in ${{ github.event.release.tag_name }}
[One sentence: what's the theme of this release?]
### New
- **Feature name** - What it does and why you'd use it
### Improved
- **Area** - What got better
### Fixed
- What was broken, now works
```
Omit empty sections.
## Rules
1. Write for developers using OpenSpec with AI coding assistants
2. Remove commit hashes (like `eb152eb:`), PR numbers, and changesets wrappers (`### Minor Changes`)
3. Lead with what users can do, not implementation details
4. One to two sentences per item, max
5. Use **bold** for feature/area names
6. Skip internal changes (CI, refactors, tests) unless they affect users
7. If the input is already well-formatted, just clean up structure and remove noise
## Example
Before:
```
### Minor Changes
- 8dfd824: Add OPSX experimental workflow commands and enhanced artifact system
**New Commands:**
- `/opsx:ff` - Fast-forward through artifact creation
```
After (polished-notes.md):
```
### New
- **Fast-forward mode** - Generate all planning artifacts at once with `/opsx:ff`. Useful when you already know what you're building.
```
After (release-title.txt):
```
v0.18.0 - OPSX Experimental Workflow
```
Write both files. No other output.
- name: Update release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ github.event.release.tag_name }}"
if [ -f "polished-notes.md" ] && [ -f "release-title.txt" ]; then
TITLE=$(cat release-title.txt)
gh release edit "$TAG" --title "$TITLE" --notes-file polished-notes.md
echo "Updated: $TITLE"
elif [ -f "polished-notes.md" ]; then
gh release edit "$TAG" --notes-file polished-notes.md
echo "Updated notes (title unchanged)"
else
echo "No changes generated, keeping original"
fi
+12 -1
View File
@@ -18,9 +18,20 @@ jobs:
if: github.repository == 'Fission-AI/OpenSpec'
runs-on: ubuntu-latest
steps:
# Generate GitHub App token first - used for checkout and changesets
# This allows git operations to trigger CI workflows on the version PR
# (GITHUB_TOKEN cannot trigger workflows by design)
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- uses: pnpm/action-setup@v4
with:
@@ -44,5 +55,5 @@ jobs:
# so package.json already contains the bumped version.
publish: pnpm run release:ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# npm authentication handled via OIDC trusted publishing (no token needed)
+22
View File
@@ -1,5 +1,25 @@
# @fission-ai/openspec
## 0.19.0
### Minor Changes
- eb152eb: ### New Features
- **Continue IDE support** – OpenSpec now generates slash commands for [Continue](https://continue.dev/), expanding editor integration options alongside Cursor, Windsurf, Claude Code, and others
- **Shell completions for Bash, Fish, and PowerShell** – Run `openspec completion install` to set up tab completion in your preferred shell
- **`/opsx:explore` command** – A new thinking partner mode for exploring ideas and investigating problems before committing to changes
- **Codebuddy slash command improvements** – Updated frontmatter format for better compatibility
### Bug Fixes
- Shell completions now correctly offer parent-level flags (like `--help`) when a command has subcommands
- Fixed Windows compatibility issues in tests
### Other
- Added optional anonymous usage statistics to help understand how OpenSpec is used. This is **opt-out** by default – set `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` to disable. Only command names and version are collected; no arguments, file paths, or content. Automatically disabled in CI environments.
## 0.18.0
### Minor Changes
@@ -86,6 +106,8 @@
### Minor Changes
- Add Continue slash command support so `openspec init` can generate `.continue/prompts/openspec-*.prompt` files with MARKDOWN frontmatter and `$ARGUMENTS` placeholder, and refresh them on `openspec update`.
- Add Antigravity slash command support so `openspec init` can generate `.agent/workflows/openspec-*.md` files with description-only frontmatter and `openspec update` refreshes existing workflows alongside Windsurf.
## 0.15.0
+1
View File
@@ -103,6 +103,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
| **Continue** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.continue/prompts/`) |
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
+329
View File
@@ -0,0 +1,329 @@
# OpenSpec Versioning & Archiving: Quick Guide
> **TL;DR:** When multiple branches modify the same spec, the last archive wins (data loss). **Solution:** Use the OPSX workflow (`/opsx:sync`) for AI-driven intelligent merging. Setup: `openspec artifact-experimental-setup`. [Jump to solution](#the-solution-opsx-workflow)
## The Core Problem
**OpenSpec can silently lose changes when multiple branches modify the same requirement:**
```
Week 1:
┌─────────────┐
│ Branch A │ Adds: "Support SSO login"
│ (Feature) │ To requirement: "Authentication Methods"
└──────┬──────┘
│ Archives Monday ✓
▼
Main Spec now has: [Password, OAuth, SSO]
Week 1:
┌─────────────┐
│ Branch B │ Adds: "Support biometric login"
│ (Feature) │ To requirement: "Authentication Methods"
└──────┬──────┘ (Started from pre-SSO spec)
│ Archives Friday
▼
Main Spec now has: [Password, OAuth, Biometric] ← SSO disappeared! 😱
```
**Why?** OpenSpec replaces entire requirement blocks. No Git-like merging. No conflict detection.
## Workflow Risk
```
Multiple feature branches
│
├─── Change X: updates "Auth" requirement
├─── Change Y: updates "Auth" requirement
├─── Change Z: updates "Logging" requirement
│
Weekly batch archive (Friday)
│
└─── Last archive wins, earlier scenarios may vanish
```
## The Solution: OPSX Workflow
> **Requirements:** Claude Code, experimental workflow (stable and actively used)
The **OPSX workflow** solves the conflict problem with **AI-driven intelligent merging**. Instead of mechanical replacement, Claude reads your delta specs and understands intent.
**Available today.** Setup takes 30 seconds.
### Quick Comparison
```
Standard Archive: OPSX Sync:
┌──────────────────┐ ┌──────────────────┐
│ Replace entire │ │ AI-driven merge │
│ requirement │ │ Preserves │
│ block │ │ unmentioned │
│ │ │ scenarios │
│ Last write wins │ │ Intelligent │
│ ⚠️ Data loss │ │ ✓ Safe │
└──────────────────┘ └──────────────────┘
```
### How It Works: Your Authentication Example
Let's revisit the SSO/Biometric conflict with OPSX:
```
Week 1:
┌─────────────┐
│ Branch A │ Delta: "ADDED Requirements: Authentication Methods with SSO"
│ (Feature) │
└──────┬──────┘
│ Friday: /opsx:sync feature-a
▼
Main Spec: [Password, OAuth, SSO] ✓
┌─────────────┐
│ Branch B │ Delta: "ADDED Requirements: Authentication Methods with Biometric"
│ (Feature) │ (Delta created from pre-SSO spec)
└──────┬──────┘
│ Friday: /opsx:sync feature-b
│
│ What Claude does:
│ 1. Reads delta B: "Add Authentication Methods with Biometric"
│ 2. Reads main: "Already has Authentication Methods with SSO"
│ 3. Applies rule: "Preserve existing content not mentioned in delta"
│ 4. Merges: Keep SSO + OAuth, add Biometric
▼
Main Spec: [Password, OAuth, SSO, Biometric] ✓✓ ← Both changes preserved!
```
**Key principle:** Claude treats deltas as **instructions** ("add this scenario"), not **replacements** ("make it look exactly like this").
### Your Workflow Options
**Option 1: Friday Batch (Keeps Your Current Rhythm)**
```
Throughout week:
├─ Features complete on branches
└─ Code merges to main
Friday:
┌─────────────────────────────────────────────────┐
│ /opsx:sync feature-a │
│ ↓ Main updated with A's changes │
│ │
│ /opsx:sync feature-b │
│ ↓ Claude preserves A's changes, adds B │
│ │
│ /opsx:sync feature-c │
│ ↓ Claude preserves A+B, adds C │
│ │
│ git diff openspec/specs/ (review all) │
│ │
│ /opsx:archive feature-a │
│ /opsx:archive feature-b │
│ /opsx:archive feature-c │
└─────────────────────────────────────────────────┘
```
**Option 2: Sync-as-you-go (Even Safer)**
```
Monday: Feature A merges → /opsx:sync feature-a
Wednesday: Feature B merges → /opsx:sync feature-b (auto-merges with A)
Friday: Just /opsx:archive (specs already synced)
```
### Concrete Example: Three Teams, One Requirement
**Setup:**
- Change A: Adds SSO login
- Change B: Adds Biometric login
- Change C: Updates OAuth token expiry 1h → 2h
**Main spec starts with:**
```markdown
### Requirement: Authentication Methods
#### Scenario: Password login
...
#### Scenario: OAuth login (expires in 1h)
...
```
**Friday batch sync:**
```
/opsx:sync feature-a
├─ Delta A: ADDED "SSO login" scenario
└─ Main spec: [Password, OAuth, SSO]
/opsx:sync feature-b
├─ Delta B: ADDED "Biometric login" scenario
├─ Claude sees: Main already has SSO (not in delta B)
├─ Claude preserves: SSO
└─ Main spec: [Password, OAuth, SSO, Biometric]
/opsx:sync feature-c
├─ Delta C: MODIFIED "OAuth login" scenario (2h expiry)
├─ Claude sees: Main has SSO + Biometric (not in delta C)
├─ Claude preserves: SSO + Biometric
├─ Claude updates: OAuth expiry
└─ Main spec: [Password, OAuth (2h), SSO, Biometric]
```
**Result:** All three changes merged successfully! 🎉
### Getting Started
**1. One-time setup:**
```bash
openspec artifact-experimental-setup
# Creates /opsx:* skills in .claude/skills/
```
**2. Try it this Friday:**
```
# In Claude Code:
/opsx:sync
→ Select a change
→ Claude merges intelligently
git diff openspec/specs/
→ Review what changed
/opsx:archive
→ Move to archive when satisfied
```
**3. For new features:**
```
/opsx:new feature-name # Start new change
/opsx:ff # Create planning artifacts
/opsx:apply # Implement
/opsx:sync # Merge specs to main
/opsx:archive # Archive when done
```
### What Cases Does This Handle?
```
✓ Multiple changes adding different scenarios to same requirement
✓ One change adds, another modifies same requirement
✓ Changes affecting different parts of same requirement
✓ Mix of ADDED/MODIFIED/REMOVED operations
✓ Most real-world parallel development
⚠️ Two changes modifying exact same scenario in conflicting ways
⚠️ One removes what another modifies
Best practice: Always review git diff after each sync
```
### Why This Works
The `/opsx:sync` skill instructs Claude:
> "Preserve scenarios/content not mentioned in the delta. The delta represents *intent*, not a wholesale replacement. Apply changes intelligently - add new scenarios without removing existing ones, update only what's explicitly mentioned."
This is like having a human do the merge instead of a mechanical diff tool.
## If You Can't Use OPSX Yet
If you're not using Claude Code or can't adopt OPSX yet, here are manual workarounds:
### Before Creating Changes
```bash
# Check for conflicts
openspec list
ls openspec/changes/*/specs/your-capability/
# If overlap exists:
→ Coordinate with that team
→ Consider combining changes
→ Or sequence them (one after another)
```
### Archive Order Strategy
```
Friday Archive Session:
Step 1: Group by overlap
┌─────────────────────────────────────┐
│ No Overlap: [Z, W, V] │ ← Archive these first
│ Low Overlap: [A touches auth only] │ ← Then these
│ High Overlap:[X, Y both touch auth] │ ← Coordinate these last
└─────────────────────────────────────┘
Step 2: Archive in order
Z → W → V → A → (manually check X vs Y) → X → fix Y's delta → Y
↑
Key step: update Y's delta spec
to include X's changes before
archiving Y
```
### Safety Checklist
```bash
□ git diff main -- openspec/specs/your-capability/
□ openspec validate --strict
□ Manual review: did another change land on this spec?
□ If yes: update your delta spec to include their scenarios
```
## Your Action Plan
```
This Week:
├─ Run: openspec artifact-experimental-setup
└─ Try /opsx:sync on one change
Next Sprint:
├─ Adopt /opsx:sync for all Friday archives
└─ Switch to sync-as-you-go if it works well
Ongoing:
└─ Always run git diff after /opsx:sync to review
```
## Quick Reference Card
```
┌─────────────────────────────────────────────────────────────┐
│ OPENSPEC CONFLICT PREVENTION CHEAT SHEET │
├─────────────────────────────────────────────────────────────┤
│ │
│ RECOMMENDED: Use OPSX Workflow │
│ ✓ openspec artifact-experimental-setup (one-time) │
│ ✓ /opsx:sync after each feature │
│ ✓ git diff openspec/specs/ (review) │
│ ✓ /opsx:archive when done │
│ │
│ FRIDAY BATCH WORKFLOW: │
│ 1. /opsx:sync feature-a │
│ 2. /opsx:sync feature-b (AI merges with A) │
│ 3. /opsx:sync feature-c (AI merges with A+B) │
│ 4. git diff openspec/specs/ (review all) │
│ 5. /opsx:archive each │
│ │
│ IF NOT USING OPSX: │
│ □ openspec list (check overlaps) │
│ □ Coordinate with other teams │
│ □ Archive non-overlapping first │
│ □ Manually merge conflicts in delta specs │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Key Insight
> **OpenSpec delta specs are instructions, not files.** Standard archiving does mechanical replacement (last write wins). OPSX workflow uses AI to understand intent and merge intelligently (all writes preserved). Use OPSX for parallel development.
## Additional Resources
- **Detailed Technical Analysis:** [openspec-parallel-merge-plan.md](../openspec-parallel-merge-plan.md)
- **OPSX Workflow Guide:** [experimental-workflow.md](./experimental-workflow.md)
- **Join Discussion:** [OpenSpec Discord](https://discord.gg/BYjPaKbqMt)
- **Report Issues:** [GitHub Issues](https://github.com/Fission-AI/openspec/issues)
+6
View File
@@ -211,6 +211,12 @@ The init command SHALL generate slash command files for supported editors using
- **AND** populate each file from shared templates so command text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Continue
- **WHEN** the user selects Continue during initialization
- **THEN** create `.continue/prompts/openspec-proposal.prompt`, `.continue/prompts/openspec-apply.prompt`, and `.continue/prompts/openspec-archive.prompt`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Factory Droid
- **WHEN** the user selects Factory Droid during initialization
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
+5
View File
@@ -75,6 +75,11 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Continue
- **WHEN** `.continue/prompts/` contains `openspec-proposal.prompt`, `openspec-apply.prompt`, and `openspec-archive.prompt`
- **THEN** refresh each file using shared templates
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Crush
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
- **THEN** refresh each file using shared templates
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.18.0",
"version": "0.19.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
@@ -54,13 +54,13 @@
"check:pack-version": "node scripts/pack-version-check.mjs",
"release": "pnpm run release:ci",
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
"changeset": "changeset"
},
"engines": {
"node": ">=20.19.0"
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.2",
"@changesets/cli": "^2.27.7",
"@types/node": "^24.2.0",
"@vitest/ui": "^3.2.4",
+66
View File
@@ -36,6 +36,9 @@ importers:
specifier: ^4.0.17
version: 4.0.17
devDependencies:
'@changesets/changelog-github':
specifier: ^0.5.2
version: 0.5.2
'@changesets/cli':
specifier: ^2.27.7
version: 2.29.6(@types/node@24.2.0)
@@ -73,6 +76,9 @@ packages:
'@changesets/changelog-git@0.2.1':
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
'@changesets/changelog-github@0.5.2':
resolution: {integrity: sha512-HeGeDl8HaIGj9fQHo/tv5XKQ2SNEi9+9yl1Bss1jttPqeiASRXhfi0A2wv8yFKCp07kR1gpOI5ge6+CWNm1jPw==}
'@changesets/cli@2.29.6':
resolution: {integrity: sha512-6qCcVsIG1KQLhpQ5zE8N0PckIx4+9QlHK3z6/lwKnw7Tir71Bjw8BeOZaxA/4Jt00pcgCnCSWZnyuZf5Il05QQ==}
hasBin: true
@@ -86,6 +92,9 @@ packages:
'@changesets/get-dependents-graph@2.1.3':
resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
'@changesets/get-github-info@0.7.0':
resolution: {integrity: sha512-+i67Bmhfj9V4KfDeS1+Tz3iF32btKZB2AAx+cYMqDSRFP7r3/ZdGbjCo+c6qkyViN9ygDuBjzageuPGJtKGe5A==}
'@changesets/get-release-plan@4.0.13':
resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==}
@@ -829,6 +838,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
dataloader@1.4.0:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -853,6 +865,10 @@ packages:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
dotenv@8.6.0:
resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==}
engines: {node: '>=10'}
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
@@ -1198,6 +1214,15 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
@@ -1465,6 +1490,9 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
@@ -1574,6 +1602,12 @@ packages:
jsdom:
optional: true
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -1641,6 +1675,14 @@ snapshots:
dependencies:
'@changesets/types': 6.1.0
'@changesets/changelog-github@0.5.2':
dependencies:
'@changesets/get-github-info': 0.7.0
'@changesets/types': 6.1.0
dotenv: 8.6.0
transitivePeerDependencies:
- encoding
'@changesets/cli@2.29.6(@types/node@24.2.0)':
dependencies:
'@changesets/apply-release-plan': 7.0.12
@@ -1695,6 +1737,13 @@ snapshots:
picocolors: 1.1.1
semver: 7.7.2
'@changesets/get-github-info@0.7.0':
dependencies:
dataloader: 1.4.0
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
'@changesets/get-release-plan@4.0.13':
dependencies:
'@changesets/assemble-release-plan': 6.0.9
@@ -2379,6 +2428,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
dataloader@1.4.0: {}
debug@4.4.1:
dependencies:
ms: 2.1.3
@@ -2393,6 +2444,8 @@ snapshots:
dependencies:
path-type: 4.0.0
dotenv@8.6.0: {}
emoji-regex@10.4.0: {}
emoji-regex@8.0.0: {}
@@ -2737,6 +2790,10 @@ snapshots:
natural-compare@1.4.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
@@ -2985,6 +3042,8 @@ snapshots:
totalist@3.0.1: {}
tr46@0.0.3: {}
ts-api-utils@2.1.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -3092,6 +3151,13 @@ snapshots:
- tsx
- yaml
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2:
dependencies:
isexe: 2.0.0
+6 -3
View File
@@ -50,7 +50,10 @@ program
program.option('--no-color', 'Disable color output');
// Apply global flags and telemetry before any command runs
program.hook('preAction', async (thisCommand) => {
// Note: preAction receives (thisCommand, actionCommand) where:
// - thisCommand: the command where hook was added (root program)
// - actionCommand: the command actually being executed (subcommand)
program.hook('preAction', async (thisCommand, actionCommand) => {
const opts = thisCommand.opts();
if (opts.color === false) {
process.env.NO_COLOR = '1';
@@ -59,8 +62,8 @@ program.hook('preAction', async (thisCommand) => {
// Show first-run telemetry notice (if not seen)
await maybeShowTelemetryNotice();
// Track command execution
const commandPath = getCommandPath(thisCommand);
// Track command execution (use actionCommand to get the actual subcommand)
const commandPath = getCommandPath(actionCommand);
await trackCommand(commandPath, version);
});
+1
View File
@@ -24,6 +24,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)' },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
+51
View File
@@ -0,0 +1,51 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.continue/prompts/openspec-proposal.prompt',
apply: '.continue/prompts/openspec-apply.prompt',
archive: '.continue/prompts/openspec-archive.prompt'
};
/*
* Continue .prompt format requires YAML frontmatter:
* ---
* name: commandName
* description: description
* invokable: true
* ---
* Body...
*
* The 'invokable: true' field is required to make the prompt available as a slash command.
* We use 'openspec-proposal' as the name so the command becomes /openspec-proposal.
*/
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: openspec-proposal
description: Scaffold a new OpenSpec change and validate strictly.
invokable: true
---`,
apply: `---
name: openspec-apply
description: Implement an approved OpenSpec change and keep tasks in sync.
invokable: true
---`,
archive: `---
name: openspec-archive
description: Archive a deployed OpenSpec change and update specs.
invokable: true
---`
};
export class ContinueSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'continue';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
+3
View File
@@ -19,6 +19,7 @@ import { QwenSlashCommandConfigurator } from './qwen.js';
import { RooCodeSlashCommandConfigurator } from './roocode.js';
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
import { IflowSlashCommandConfigurator } from './iflow.js';
import { ContinueSlashCommandConfigurator } from './continue.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -44,6 +45,7 @@ export class SlashCommandRegistry {
const roocode = new RooCodeSlashCommandConfigurator();
const antigravity = new AntigravitySlashCommandConfigurator();
const iflow = new IflowSlashCommandConfigurator();
const continueTool = new ContinueSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -65,6 +67,7 @@ export class SlashCommandRegistry {
this.configurators.set(roocode.toolId, roocode);
this.configurators.set(antigravity.toolId, antigravity);
this.configurators.set(iflow.toolId, iflow);
this.configurators.set(continueTool.toolId, continueTool);
}
static register(configurator: SlashCommandConfigurator): void {
+55
View File
@@ -1151,6 +1151,61 @@ describe('InitCommand', () => {
expect(codeBuddyChoice.configured).toBe(true);
});
it('should create Continue slash command files with templates', async () => {
queueSelections('continue', DONE);
await initCommand.execute(testDir);
const continueProposal = path.join(
testDir,
'.continue/prompts/openspec-proposal.prompt'
);
const continueApply = path.join(
testDir,
'.continue/prompts/openspec-apply.prompt'
);
const continueArchive = path.join(
testDir,
'.continue/prompts/openspec-archive.prompt'
);
expect(await fileExists(continueProposal)).toBe(true);
expect(await fileExists(continueApply)).toBe(true);
expect(await fileExists(continueArchive)).toBe(true);
const proposalContent = await fs.readFile(continueProposal, 'utf-8');
expect(proposalContent).toContain('---');
expect(proposalContent).toContain('name: openspec-proposal');
expect(proposalContent).toContain('invokable: true');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
const applyContent = await fs.readFile(continueApply, 'utf-8');
expect(applyContent).toContain('---');
expect(applyContent).toContain('name: openspec-apply');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('invokable: true');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(continueArchive, 'utf-8');
expect(archiveContent).toContain('---');
expect(archiveContent).toContain('name: openspec-archive');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('invokable: true');
expect(archiveContent).toContain('openspec archive <id> --yes');
});
it('should mark Continue as already configured during extend mode', async () => {
queueSelections('continue', DONE, 'continue', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);
const secondRunArgs = mockPrompt.mock.calls[1][0];
const continueChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'continue'
);
expect(continueChoice.configured).toBe(true);
});
it('should create CODEBUDDY.md when CodeBuddy is selected', async () => {
queueSelections('codebuddy', DONE);
+74
View File
@@ -368,6 +368,80 @@ Old body
consoleSpy.mockRestore();
});
it('should refresh existing Continue prompt files', async () => {
const continuePath = path.join(
testDir,
'.continue/prompts/openspec-apply.prompt'
);
await fs.mkdir(path.dirname(continuePath), { recursive: true });
const initialContent = `---
name: openspec-apply
description: Old description
invokable: true
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(continuePath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(continuePath, 'utf-8');
expect(updated).toContain('name: openspec-apply');
expect(updated).toContain('invokable: true');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated OpenSpec instructions (openspec/AGENTS.md'
);
expect(logMessage).toContain('AGENTS.md (created)');
expect(logMessage).toContain(
'Updated slash commands: .continue/prompts/openspec-apply.prompt'
);
consoleSpy.mockRestore();
});
it('should not create missing Continue prompt files on update', async () => {
const continueApply = path.join(
testDir,
'.continue/prompts/openspec-apply.prompt'
);
// Only create apply; leave proposal and archive missing
await fs.mkdir(path.dirname(continueApply), { recursive: true });
await fs.writeFile(
continueApply,
`---
name: openspec-apply
description: Old description
invokable: true
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`
);
await updateCommand.execute(testDir);
const continueProposal = path.join(
testDir,
'.continue/prompts/openspec-proposal.prompt'
);
const continueArchive = path.join(
testDir,
'.continue/prompts/openspec-archive.prompt'
);
// Confirm they weren't created by update
await expect(FileSystemUtils.fileExists(continueProposal)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(continueArchive)).resolves.toBe(false);
});
it('should refresh existing OpenCode slash command files', async () => {
const openCodePath = path.join(
testDir,