mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835c15a2f6 | ||
|
|
847aa81c0f | ||
|
|
39bebefcc4 | ||
|
|
cf8b6212c8 | ||
|
|
c157483685 | ||
|
|
f90c7c3354 | ||
|
|
9381bd3b24 | ||
|
|
ae83b4e16d | ||
|
|
d48528134b | ||
|
|
54bd3f1ccd | ||
|
|
675e870bf1 | ||
|
|
07eaf7b691 | ||
|
|
153721d14a | ||
|
|
70c2e17525 | ||
|
|
e2c333e493 | ||
|
|
e137dd3981 | ||
|
|
2beb8e77e8 | ||
|
|
3b16b13613 | ||
|
|
c4cfdc7c49 | ||
|
|
e0736807b4 | ||
|
|
fdb05a723e | ||
|
|
8332a09811 | ||
|
|
d61a49f6d5 | ||
|
|
7d1237f00d | ||
|
|
33466b1e2a | ||
|
|
6c8c778043 | ||
|
|
43b01ad374 | ||
|
|
3cdcdfca8e | ||
|
|
32fc19a60d | ||
|
|
84f372517f | ||
|
|
adda63e17a | ||
|
|
90d05b7115 | ||
|
|
20714c1c28 | ||
|
|
2e51ae26d3 | ||
|
|
dbd4ed7bfb | ||
|
|
473093f885 | ||
|
|
b5a884748b | ||
|
|
690c75225c | ||
|
|
dd53fb7736 | ||
|
|
2a441c472d | ||
|
|
ed4d965208 | ||
|
|
c86985d6ec | ||
|
|
bf4bc2426f | ||
|
|
c57e421cc2 | ||
|
|
ed2e832066 | ||
|
|
9db74aa5ac | ||
|
|
b5b7248610 | ||
|
|
322bfd455a | ||
|
|
08c349369a | ||
|
|
40afee643e | ||
|
|
05023dab43 | ||
|
|
d7a928b4e9 | ||
|
|
07dd634986 | ||
|
|
36078b1947 | ||
|
|
d0e1b076c2 | ||
|
|
2fbda520de | ||
|
|
5633556b6d | ||
|
|
2bb0ed36c5 | ||
|
|
06097f9cb7 | ||
|
|
8f5a526396 | ||
|
|
eb152eb2ca | ||
|
|
e987a5a327 | ||
|
|
4971cda812 | ||
|
|
4715138927 | ||
|
|
940898c1c5 | ||
|
|
d49a88c3bb | ||
|
|
bb9f6ce0ea | ||
|
|
ae85a7229d | ||
|
|
504c93bdf1 | ||
|
|
c4a54a8d54 | ||
|
|
38d2356836 | ||
|
|
3f67debf65 | ||
|
|
533cb0fa87 | ||
|
|
8dfd824477 | ||
|
|
3ed1270316 |
+93
-4
@@ -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
|
||||
```
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Github Workflows
|
||||
|
||||
## Testing CI Locally
|
||||
|
||||
Test GitHub Actions workflows locally using [act](https://nektosact.com/):
|
||||
|
||||
```bash
|
||||
# Test all PR checks
|
||||
act pull_request
|
||||
|
||||
# Test specific job
|
||||
act pull_request -j nix-flake-validate
|
||||
|
||||
# Dry run to see what would execute
|
||||
act pull_request --dryrun
|
||||
```
|
||||
|
||||
The `.actrc` file configures act to use the appropriate Docker image.
|
||||
|
||||
|
||||
+101
-2
@@ -15,6 +15,29 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Detect which files changed to enable path-based filtering
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
nix: ${{ steps.filter.outputs.nix }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for Nix-related changes
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
nix:
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'scripts/update-flake.sh'
|
||||
- '.github/workflows/ci.yml'
|
||||
|
||||
test_pr:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -156,6 +179,66 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
nix-flake-validate:
|
||||
name: Nix Flake Validation
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: changes
|
||||
if: needs.changes.outputs.nix == 'true'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v13
|
||||
|
||||
- name: Setup Nix cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Build with Nix
|
||||
run: nix build
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
if [ ! -e "result" ]; then
|
||||
echo "Error: Nix build output 'result' symlink not found"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "result/bin/openspec" ]; then
|
||||
echo "Error: openspec binary not found in build output"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build output verified"
|
||||
|
||||
- name: Test binary execution
|
||||
run: |
|
||||
VERSION=$(nix run . -- --version)
|
||||
echo "OpenSpec version: $VERSION"
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Version command returned empty output"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Binary execution successful"
|
||||
|
||||
- name: Validate update script
|
||||
run: |
|
||||
echo "Testing update-flake.sh script..."
|
||||
bash scripts/update-flake.sh
|
||||
echo "✅ Update script executed successfully"
|
||||
|
||||
- name: Check flake.nix modifications
|
||||
run: |
|
||||
if git diff --quiet flake.nix; then
|
||||
echo "⚠️ Warning: flake.nix was not modified by update script"
|
||||
else
|
||||
echo "✅ flake.nix was updated by script"
|
||||
git diff flake.nix
|
||||
fi
|
||||
|
||||
- name: Restore flake.nix
|
||||
if: always()
|
||||
run: git checkout -- flake.nix || true
|
||||
|
||||
validate-changesets:
|
||||
name: Validate Changesets
|
||||
runs-on: ubuntu-latest
|
||||
@@ -191,7 +274,7 @@ jobs:
|
||||
required-checks-pr:
|
||||
name: All checks passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_pr, lint]
|
||||
needs: [test_pr, lint, nix-flake-validate]
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Verify all checks passed
|
||||
@@ -204,12 +287,20 @@ jobs:
|
||||
echo "Lint job failed"
|
||||
exit 1
|
||||
fi
|
||||
# Nix validation may be skipped if no Nix-related files changed
|
||||
if [[ "${{ needs.nix-flake-validate.result }}" != "success" && "${{ needs.nix-flake-validate.result }}" != "skipped" ]]; then
|
||||
echo "Nix flake validation job failed"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${{ needs.nix-flake-validate.result }}" == "skipped" ]]; then
|
||||
echo "Nix flake validation skipped (no Nix-related changes)"
|
||||
fi
|
||||
echo "All required checks passed!"
|
||||
|
||||
required-checks-main:
|
||||
name: All checks passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_matrix, lint]
|
||||
needs: [test_matrix, lint, nix-flake-validate]
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Verify all checks passed
|
||||
@@ -222,4 +313,12 @@ jobs:
|
||||
echo "Lint job failed"
|
||||
exit 1
|
||||
fi
|
||||
# Nix validation may be skipped if no Nix-related files changed
|
||||
if [[ "${{ needs.nix-flake-validate.result }}" != "success" && "${{ needs.nix-flake-validate.result }}" != "skipped" ]]; then
|
||||
echo "Nix flake validation job failed"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${{ needs.nix-flake-validate.result }}" == "skipped" ]]; then
|
||||
echo "Nix flake validation skipped (no Nix-related changes)"
|
||||
fi
|
||||
echo "All required checks passed!"
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
name: Polish Release Notes
|
||||
|
||||
# Uses Claude to transform raw changelog into polished release notes.
|
||||
# Triggered automatically by release-prepare after publishing, or manually.
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [polish-release-notes]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'Release tag to polish (e.g., v0.18.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
# repository_dispatch passes tag via client_payload, workflow_dispatch via inputs
|
||||
TAG_NAME: ${{ github.event.client_payload.tag_name || inputs.tag_name }}
|
||||
|
||||
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 "${{ env.TAG_NAME }}" --json body -q '.body' > current-notes.md
|
||||
echo "Fetched release notes for ${{ env.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 }}
|
||||
claude_args: "--allowedTools Write,Read"
|
||||
prompt: |
|
||||
Transform the changelog in `current-notes.md` into release notes for OpenSpec ${{ env.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:
|
||||
```
|
||||
${{ env.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 ${{ env.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="${{ env.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
|
||||
@@ -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:
|
||||
@@ -36,6 +47,7 @@ jobs:
|
||||
|
||||
# Opens/updates the Version Packages PR; publishes when the Version PR merges
|
||||
- name: Create/Update Version PR
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
title: 'chore(release): version packages'
|
||||
@@ -44,5 +56,21 @@ 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)
|
||||
|
||||
# Trigger release notes polishing after a release is published
|
||||
# Uses repository_dispatch instead of workflow_dispatch because:
|
||||
# - workflow_dispatch requires actions:write permission (GitHub App doesn't have it)
|
||||
# - repository_dispatch works with contents:write (which we already have)
|
||||
- name: Polish release notes
|
||||
if: steps.changesets.outputs.published == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
# Get version from package.json (just bumped by changesets)
|
||||
TAG="v$(jq -r .version package.json)"
|
||||
echo "Triggering polish workflow for $TAG"
|
||||
gh api repos/${{ github.repository }}/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "{\"event_type\":\"polish-release-notes\",\"client_payload\":{\"tag_name\":\"$TAG\"}}"
|
||||
|
||||
@@ -148,3 +148,4 @@ CLAUDE.md
|
||||
|
||||
# Pnpm
|
||||
.pnpm-store/
|
||||
result
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Always open `@/openspec/AGENTS.md` when the request:
|
||||
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||
- Sounds ambiguous and you need the authoritative spec before coding
|
||||
|
||||
Use `@/openspec/AGENTS.md` to learn:
|
||||
- How to create and apply change proposals
|
||||
- Spec format and conventions
|
||||
- Project structure and guidelines
|
||||
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
+124
-3
@@ -1,5 +1,122 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#540](https://github.com/Fission-AI/OpenSpec/pull/540) [`c4cfdc7`](https://github.com/Fission-AI/OpenSpec/commit/c4cfdc7c499daef30d8a218f5f59b8d9e5adb754) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
|
||||
|
||||
- **Bulk archive skill** — Archive multiple completed changes in a single operation with `/opsx:bulk-archive`. Includes batch validation, spec conflict detection, and consolidated confirmation
|
||||
|
||||
### Other
|
||||
|
||||
- **Simplified setup** — Config creation now uses sensible defaults with helpful comments instead of interactive prompts
|
||||
|
||||
## 0.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#530](https://github.com/Fission-AI/OpenSpec/pull/530) [`33466b1`](https://github.com/Fission-AI/OpenSpec/commit/33466b1e2a6798bdd6d0e19149173585b0612e6f) Thanks [@TabishB](https://github.com/TabishB)! - Add project-level configuration, project-local schemas, and schema management commands
|
||||
|
||||
**New Features**
|
||||
|
||||
- **Project-level configuration** — Configure OpenSpec behavior per-project via `openspec/config.yaml`, including custom rules injection, context files, and schema resolution settings
|
||||
- **Project-local schemas** — Define custom artifact schemas within your project's `openspec/schemas/` directory for project-specific workflows
|
||||
- **Schema management commands** — New `openspec schema` commands (`list`, `show`, `export`, `validate`) for inspecting and managing artifact schemas (experimental)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- Fixed config loading to handle null `rules` field in project configuration
|
||||
|
||||
## 0.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#516](https://github.com/Fission-AI/OpenSpec/pull/516) [`b5a8847`](https://github.com/Fission-AI/OpenSpec/commit/b5a884748be6156a7bb140b4941cfec4f20a9fc8) Thanks [@TabishB](https://github.com/TabishB)! - Add feedback command and Nix flake support
|
||||
|
||||
**New Features**
|
||||
|
||||
- **Feedback command** — Submit feedback directly from the CLI with `openspec feedback`, which creates GitHub Issues with automatic metadata inclusion and graceful fallback for manual submission
|
||||
- **Nix flake support** — Install and develop openspec using Nix with the new `flake.nix`, including automated flake maintenance and CI validation
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- **Explore mode guardrails** — Explore mode now explicitly prevents implementation, keeping the focus on thinking and discovery while still allowing artifact creation
|
||||
|
||||
**Other**
|
||||
|
||||
- Improved change inference in `opsx apply` — automatically detects the target change from conversation context or prompts when ambiguous
|
||||
- Streamlined archive sync assessment with clearer delta spec location guidance
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#502](https://github.com/Fission-AI/OpenSpec/pull/502) [`9db74aa`](https://github.com/Fission-AI/OpenSpec/commit/9db74aa5ac6547efadaed795217cfa17444f2004) Thanks [@TabishB](https://github.com/TabishB)! - Add `/opsx:verify` command and fix vitest process storms
|
||||
|
||||
**New Features**
|
||||
|
||||
- **`/opsx:verify` command** — Validate that change implementations match their specifications
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- Fixed vitest process storms by capping worker parallelism
|
||||
- Fixed agent workflows to use non-interactive mode for validation commands
|
||||
- Fixed PowerShell completions generator to remove trailing commas
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- eb152eb: Add Continue IDE support, shell completions, and `/opsx:explore` command
|
||||
|
||||
**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
|
||||
|
||||
- 8dfd824: Add OPSX experimental workflow commands and enhanced artifact system
|
||||
|
||||
**New Commands:**
|
||||
|
||||
- `/opsx:ff` - Fast-forward through artifact creation, generating all needed artifacts in one go
|
||||
- `/opsx:sync` - Sync delta specs from a change to main specs
|
||||
- `/opsx:archive` - Archive completed changes with smart sync check
|
||||
|
||||
**Artifact Workflow Enhancements:**
|
||||
|
||||
- Schema-aware apply instructions with inline guidance and XML output
|
||||
- Agent schema selection for experimental artifact workflow
|
||||
- Per-change schema metadata via `.openspec.yaml` files
|
||||
- Agent Skills for experimental artifact workflow
|
||||
- Instruction loader for template loading and change context
|
||||
- Restructured schemas as directories with templates
|
||||
|
||||
**Improvements:**
|
||||
|
||||
- Enhanced list command with last modified timestamps and sorting
|
||||
- Change creation utilities for better workflow support
|
||||
|
||||
**Fixes:**
|
||||
|
||||
- Normalize paths for cross-platform glob compatibility
|
||||
- Allow REMOVED requirements when creating new spec files
|
||||
|
||||
## 0.17.2
|
||||
|
||||
### Patch Changes
|
||||
@@ -20,13 +137,15 @@
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 2e71835: ### New Features
|
||||
- 2e71835: Add `openspec config` command and Oh-my-zsh completions
|
||||
|
||||
**New Features**
|
||||
|
||||
- Add `openspec config` command for managing global configuration settings
|
||||
- Implement global config directory with XDG Base Directory specification support
|
||||
- Add Oh-my-zsh shell completions support for enhanced CLI experience
|
||||
|
||||
### Bug Fixes
|
||||
**Bug Fixes**
|
||||
|
||||
- Fix hang in pre-commit hooks by using dynamic imports
|
||||
- Respect XDG_CONFIG_HOME environment variable on all platforms
|
||||
@@ -34,7 +153,7 @@
|
||||
- Align cli-completion spec with implementation
|
||||
- Remove hardcoded agent field from slash commands
|
||||
|
||||
### Documentation
|
||||
**Documentation**
|
||||
|
||||
- Alphabetize AI tools list in README and make it collapsible
|
||||
|
||||
@@ -55,6 +174,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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Maintainers
|
||||
|
||||
People who maintain and guide OpenSpec.
|
||||
|
||||
## Core Maintainers
|
||||
|
||||
| Name | GitHub | Role |
|
||||
|------|--------|------|
|
||||
| Tabish Bidiwale | [@TabishB](https://github.com/TabishB) | Lead maintainer |
|
||||
|
||||
## Advisors
|
||||
|
||||
Advisors help shape technical direction and provide guidance to the project.
|
||||
|
||||
| Name | GitHub | Focus |
|
||||
|------|--------|-------|
|
||||
| Hari Krishnan | [@harikrishnan83](https://github.com/harikrishnan83) | Technical direction |
|
||||
@@ -26,6 +26,10 @@
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sub>🧪 <strong>OPSX Workflow</strong> — schema-driven, hackable, fluid. See <a href="docs/experimental-workflow.md">workflow docs</a> for details.</sub>
|
||||
</p>
|
||||
|
||||
# OpenSpec
|
||||
|
||||
OpenSpec aligns humans and AI coding assistants with spec-driven development so you agree on what to build before any code is written. **No API keys required.**
|
||||
@@ -85,42 +89,26 @@ See the full comparison in [How OpenSpec Compares](#how-openspec-compares).
|
||||
|
||||
### Supported AI Tools
|
||||
|
||||
OpenSpec generates **Agent Skills** and **/opsx:\* slash commands** for supported tools during `openspec init`.
|
||||
|
||||
<details>
|
||||
<summary><strong>Native Slash Commands</strong> (click to expand)</summary>
|
||||
<summary><strong>Tools with Agent Skills + Slash Commands</strong> (click to expand)</summary>
|
||||
|
||||
These tools have built-in OpenSpec commands. Select the OpenSpec integration when prompted.
|
||||
These tools support the full OpenSpec workflow with skills and commands:
|
||||
|
||||
| Tool | Commands |
|
||||
|------|----------|
|
||||
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
|
||||
| **Antigravity** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.agent/workflows/`) |
|
||||
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
|
||||
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
|
||||
| **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) |
|
||||
| **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` |
|
||||
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
|
||||
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
|
||||
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
|
||||
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/commands/`) |
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Qoder (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.qoder/commands/openspec/`) — see [docs](https://qoder.com/cli) |
|
||||
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
|
||||
| **RooCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.roo/commands/`) |
|
||||
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
|
||||
| Tool | Skills Location | Commands |
|
||||
|------|-----------------|----------|
|
||||
| **Claude Code** | `.claude/skills/` | `/opsx:new`, `/opsx:apply`, `/opsx:archive`, etc. |
|
||||
| **Cursor** | `.cursor/skills/` | `/opsx:*` commands via prompts |
|
||||
|
||||
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
|
||||
Run `openspec init` and select the tools you use. Skills and commands are generated automatically.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>AGENTS.md Compatible</strong> (click to expand)</summary>
|
||||
|
||||
These tools automatically read workflow instructions from `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
|
||||
Tools that support AGENTS.md can follow OpenSpec workflows by reading `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
|
||||
|
||||
| Tools |
|
||||
|-------|
|
||||
@@ -135,6 +123,8 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
|
||||
|
||||
#### Step 1: Install the CLI globally
|
||||
|
||||
**Option A: Using npm**
|
||||
|
||||
```bash
|
||||
npm install -g @fission-ai/openspec@latest
|
||||
```
|
||||
@@ -144,6 +134,39 @@ Verify installation:
|
||||
openspec --version
|
||||
```
|
||||
|
||||
**Option B: Using Nix (NixOS and Nix package manager)**
|
||||
|
||||
Run OpenSpec directly without installation:
|
||||
```bash
|
||||
nix run github:Fission-AI/OpenSpec -- init
|
||||
```
|
||||
|
||||
Or install to your profile:
|
||||
```bash
|
||||
nix profile install github:Fission-AI/OpenSpec
|
||||
```
|
||||
|
||||
Or add to your development environment in `flake.nix`:
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
openspec.url = "github:Fission-AI/OpenSpec";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, openspec, ... }: {
|
||||
devShells.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.mkShell {
|
||||
buildInputs = [ openspec.packages.x86_64-linux.default ];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Verify installation:
|
||||
```bash
|
||||
openspec --version
|
||||
```
|
||||
|
||||
#### Step 2: Initialize OpenSpec in your project
|
||||
|
||||
Navigate to your project directory:
|
||||
@@ -157,102 +180,139 @@ openspec init
|
||||
```
|
||||
|
||||
**What happens during initialization:**
|
||||
- You'll be prompted to pick any natively supported AI tools (Claude Code, CodeBuddy, Cursor, OpenCode, Qoder,etc.); other assistants always rely on the shared `AGENTS.md` stub
|
||||
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
|
||||
- A new `openspec/` directory structure is created in your project
|
||||
- You'll see an interactive tool selector to pick AI tools (Claude Code, Cursor, etc.)
|
||||
- OpenSpec generates **Agent Skills** in tool-specific directories (e.g., `.claude/skills/`)
|
||||
- **/opsx:\* slash commands** are created for each selected tool
|
||||
- A `openspec/config.yaml` file is created for project configuration
|
||||
- The `openspec/` directory structure is created (specs, changes, archive)
|
||||
|
||||
**After setup:**
|
||||
- Primary AI tools can trigger `/openspec` workflows without additional configuration
|
||||
- Run `openspec list` to verify the setup and view any active changes
|
||||
- If your coding assistant doesn't surface the new slash commands right away, restart it. Slash commands are loaded at startup,
|
||||
so a fresh launch ensures they appear
|
||||
**Legacy upgrade:** If you have files from an older OpenSpec version, init will detect them and offer to clean up automatically. Use `--force` to skip the confirmation prompt.
|
||||
|
||||
### Optional: Populate Project Context
|
||||
|
||||
After `openspec init` completes, you'll receive a suggested prompt to help populate your project context:
|
||||
|
||||
```text
|
||||
Populate your project context:
|
||||
"Please read openspec/project.md and help me fill it out with details about my project, tech stack, and conventions"
|
||||
**Non-interactive mode:** For CI or scripted setups:
|
||||
```bash
|
||||
openspec init --tools claude,cursor # Specific tools
|
||||
openspec init --tools all # All supported tools
|
||||
openspec init --tools none # Skip tool setup
|
||||
```
|
||||
|
||||
Use `openspec/project.md` to define project-level conventions, standards, architectural patterns, and other guidelines that should be followed across all changes.
|
||||
**After setup:**
|
||||
- Run `/opsx:new` to start your first change
|
||||
- Run `openspec list` to verify the setup and view any active changes
|
||||
- Restart your IDE for slash commands to take effect
|
||||
|
||||
### Optional: Configure Project Context
|
||||
|
||||
After `openspec init`, you can customize `openspec/config.yaml` to inject project-specific context into all artifacts:
|
||||
|
||||
```yaml
|
||||
# openspec/config.yaml
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: TypeScript, React, Node.js
|
||||
Testing: Vitest for unit tests
|
||||
Style: ESLint with Prettier
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Include rollback plan
|
||||
specs:
|
||||
- Use Given/When/Then format for scenarios
|
||||
```
|
||||
|
||||
This context is automatically included in artifact instructions, helping the AI understand your project's conventions.
|
||||
|
||||
### Create Your First Change
|
||||
|
||||
Here's a real example showing the complete OpenSpec workflow. This works with any AI tool. Those with native slash commands will recognize the shortcuts automatically.
|
||||
|
||||
#### 1. Draft the Proposal
|
||||
Start by asking your AI to create a change proposal:
|
||||
Here's a real example showing the complete OpenSpec workflow using `/opsx:*` commands.
|
||||
|
||||
#### 1. Start a New Change
|
||||
```text
|
||||
You: Create an OpenSpec change proposal for adding profile search filters by role and team
|
||||
(Shortcut for tools with slash commands: /openspec:proposal Add profile search filters)
|
||||
You: /opsx:new
|
||||
|
||||
AI: I'll create an OpenSpec change proposal for profile filters.
|
||||
*Scaffolds openspec/changes/add-profile-filters/ with proposal.md, tasks.md, spec deltas.*
|
||||
AI: What do you want to build?
|
||||
You: Add profile search filters by role and team
|
||||
|
||||
AI: I'll create a new change for profile filters.
|
||||
*Creates openspec/changes/add-profile-filters/ with proposal.md*
|
||||
```
|
||||
|
||||
#### 2. Verify & Review
|
||||
Check that the change was created correctly and review the proposal:
|
||||
#### 2. Build Out Artifacts
|
||||
Use `/opsx:continue` to create artifacts one at a time, or `/opsx:ff` to fast-forward through all planning artifacts:
|
||||
|
||||
```text
|
||||
You: /opsx:continue
|
||||
|
||||
AI: Proposal is done. Ready to create: specs, design
|
||||
Which artifact should I create?
|
||||
You: specs
|
||||
|
||||
AI: *Creates specs with requirements and scenarios*
|
||||
```
|
||||
|
||||
Or fast-forward through everything:
|
||||
```text
|
||||
You: /opsx:ff add-profile-filters
|
||||
|
||||
AI: *Creates proposal → specs → design → tasks in sequence*
|
||||
```
|
||||
|
||||
#### 3. Verify & Review
|
||||
```bash
|
||||
$ openspec list # Confirm the change folder exists
|
||||
$ openspec validate add-profile-filters # Validate spec formatting
|
||||
$ openspec show add-profile-filters # Review proposal, tasks, and spec delta
|
||||
```
|
||||
|
||||
#### 3. Refine the Specs
|
||||
Iterate on the specifications until they match your needs:
|
||||
|
||||
```text
|
||||
You: Can you add acceptance criteria for the role and team filters?
|
||||
|
||||
AI: I'll update the spec delta with scenarios for role and team filters.
|
||||
*Edits openspec/changes/add-profile-filters/specs/profile/spec.md and tasks.md.*
|
||||
openspec list # Confirm the change folder exists
|
||||
openspec validate add-profile-filters # Validate spec formatting
|
||||
openspec show add-profile-filters # Review proposal, tasks, and spec delta
|
||||
```
|
||||
|
||||
#### 4. Implement the Change
|
||||
Once specs look good, start implementation:
|
||||
|
||||
```text
|
||||
You: The specs look good. Let's implement this change.
|
||||
(Shortcut for tools with slash commands: /openspec:apply add-profile-filters)
|
||||
You: /opsx:apply
|
||||
|
||||
AI: I'll work through the tasks in the add-profile-filters change.
|
||||
*Implements tasks from openspec/changes/add-profile-filters/tasks.md*
|
||||
*Marks tasks complete: Task 1.1 ✓, Task 1.2 ✓, Task 2.1 ✓...*
|
||||
*Implements tasks, marking them complete as it goes*
|
||||
```
|
||||
|
||||
**Key feature:** If you discover issues during implementation, you can update specs, design, or tasks — then continue. No phase gates.
|
||||
|
||||
#### 5. Archive the Completed Change
|
||||
After implementation is complete, archive the change:
|
||||
|
||||
```text
|
||||
AI: All tasks are complete. The implementation is ready.
|
||||
|
||||
You: Please archive the change
|
||||
(Shortcut for tools with slash commands: /openspec:archive add-profile-filters)
|
||||
You: /opsx:archive
|
||||
|
||||
AI: I'll archive the add-profile-filters change.
|
||||
*Runs: openspec archive add-profile-filters --yes*
|
||||
✓ Change archived successfully. Specs updated. Ready for the next feature!
|
||||
*Runs: openspec archive add-profile-filters --yes*
|
||||
✓ Change archived. Specs updated. Ready for the next feature!
|
||||
```
|
||||
|
||||
Or run the command yourself in terminal:
|
||||
Or run directly in terminal:
|
||||
```bash
|
||||
$ openspec archive add-profile-filters --yes # Archive the completed change without prompts
|
||||
openspec archive add-profile-filters --yes
|
||||
```
|
||||
|
||||
**Note:** Tools with native slash commands (Claude Code, CodeBuddy, Cursor, Codex, Qoder, RooCode) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Slash Commands (in your AI tool)
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/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:apply` | Implement tasks, updating artifacts as needed |
|
||||
| `/opsx:sync` | Sync delta specs to main specs |
|
||||
| `/opsx:archive` | Archive when done |
|
||||
| `/opsx:verify` | Verify implementation matches change artifacts |
|
||||
|
||||
### CLI Commands (in terminal)
|
||||
|
||||
```bash
|
||||
openspec init # Initialize OpenSpec with skills and commands
|
||||
openspec list # View active change folders
|
||||
openspec view # Interactive dashboard of specs and changes
|
||||
openspec show <change> # Display change details (proposal, tasks, spec updates)
|
||||
openspec validate <change> # Check spec formatting and structure
|
||||
openspec archive <change> [--yes|-y] # Move a completed change into archive/ (non-interactive with --yes)
|
||||
openspec archive <change> [--yes|-y] # Move a completed change into archive/
|
||||
openspec update # Refresh skills and commands for configured tools
|
||||
```
|
||||
|
||||
## Example: How AI Creates OpenSpec Files
|
||||
@@ -352,12 +412,12 @@ Without specs, AI coding assistants generate code from vague prompts, often miss
|
||||
|
||||
## Team Adoption
|
||||
|
||||
1. **Initialize OpenSpec** – Run `openspec init` in your repo.
|
||||
2. **Start with new features** – Ask your AI to capture upcoming work as change proposals.
|
||||
1. **Initialize OpenSpec** – Run `openspec init` in your repo and select your team's tools.
|
||||
2. **Start with new features** – Use `/opsx:new` to capture upcoming work as change proposals.
|
||||
3. **Grow incrementally** – Each change archives into living specs that document your system.
|
||||
4. **Stay flexible** – Different teammates can use Claude Code, CodeBuddy, Cursor, or any AGENTS.md-compatible tool while sharing the same specs.
|
||||
4. **Stay flexible** – Different teammates can use Claude Code, Cursor, or any AGENTS.md-compatible tool while sharing the same specs.
|
||||
|
||||
Run `openspec update` whenever someone switches tools so your agents pick up the latest instructions and slash-command bindings.
|
||||
Run `openspec update` to refresh skills and commands when upgrading OpenSpec or adding new tools.
|
||||
|
||||
## Updating OpenSpec
|
||||
|
||||
@@ -365,8 +425,51 @@ Run `openspec update` whenever someone switches tools so your agents pick up the
|
||||
```bash
|
||||
npm install -g @fission-ai/openspec@latest
|
||||
```
|
||||
2. **Refresh agent instructions**
|
||||
- Run `openspec update` inside each project to regenerate AI guidance and ensure the latest slash commands are active.
|
||||
2. **Refresh skills and commands**
|
||||
```bash
|
||||
openspec update
|
||||
```
|
||||
This regenerates skills and slash commands for all configured tools.
|
||||
|
||||
3. **Restart your IDE** for slash commands to take effect.
|
||||
|
||||
## Workflow Customization
|
||||
|
||||
<details>
|
||||
<summary><strong>Custom Schemas & Templates</strong></summary>
|
||||
|
||||
OpenSpec uses a **schema-driven workflow** that you can customize:
|
||||
|
||||
**Why customize:**
|
||||
- **Hackable** — edit templates and schemas yourself, test immediately, no rebuild
|
||||
- **Granular** — each artifact has its own instructions, test and tweak individually
|
||||
- **Customizable** — define your own workflows, artifacts, and dependencies
|
||||
|
||||
**Built-in schemas:**
|
||||
- `spec-driven` (default): proposal → specs → design → tasks
|
||||
- `tdd`: tests → implementation → docs
|
||||
|
||||
**Create custom schemas:**
|
||||
```bash
|
||||
openspec schema init my-workflow # Create new schema interactively
|
||||
openspec schema fork spec-driven my-workflow # Fork existing schema
|
||||
openspec schemas # List available schemas
|
||||
```
|
||||
|
||||
Schemas are stored in `openspec/schemas/` (project) or `~/.local/share/openspec/schemas/` (global).
|
||||
|
||||
[Full documentation →](docs/experimental-workflow.md)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Telemetry</strong> – OpenSpec collects anonymous usage stats (opt-out: <code>OPENSPEC_TELEMETRY=0</code>)</summary>
|
||||
|
||||
We collect only command names and version to understand usage patterns. No arguments, paths, content, or PII. Automatically disabled in CI.
|
||||
|
||||
**Opt-out:** `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1`
|
||||
|
||||
</details>
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -376,6 +479,13 @@ Run `openspec update` whenever someone switches tools so your agents pick up the
|
||||
- Develop CLI locally: `pnpm run dev` or `pnpm run dev:cli`
|
||||
- Conventional commits (one-line): `type(scope): subject`
|
||||
|
||||
<details>
|
||||
<summary><strong>Maintainers & Advisors</strong></summary>
|
||||
|
||||
See [MAINTAINERS.md](MAINTAINERS.md) for the list of core maintainers and advisors who help guide the project.
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
+589
-31
@@ -1,18 +1,70 @@
|
||||
# Experimental Workflow (OPSX)
|
||||
|
||||
> **Status:** Experimental. Things might break. Feedback welcome on [Discord](https://discord.gg/BYjPaKbqMt).
|
||||
> **Status:** Experimental. Things might break. Feedback welcome on [Discord](https://discord.gg/YctCnvvshC).
|
||||
>
|
||||
> **Compatibility:** Claude Code only (for now)
|
||||
|
||||
## What Is It?
|
||||
|
||||
OPSX is a new way to work with OpenSpec changes. Instead of one big proposal, you build **artifacts** step-by-step:
|
||||
OPSX is a **fluid, iterative workflow** for OpenSpec changes. No more rigid phases — just actions you can take anytime.
|
||||
|
||||
## Why This Exists
|
||||
|
||||
The standard OpenSpec workflow works, but it's **locked down**:
|
||||
|
||||
- **Instructions are hardcoded** — buried in TypeScript, you can't change them
|
||||
- **All-or-nothing** — one big command creates everything, can't test individual pieces
|
||||
- **Fixed structure** — same workflow for everyone, no customization
|
||||
- **Black box** — when AI output is bad, you can't tweak the prompts
|
||||
|
||||
**OPSX opens it up.** Now anyone can:
|
||||
|
||||
1. **Experiment with instructions** — edit a template, see if the AI does better
|
||||
2. **Test granularly** — validate each artifact's instructions independently
|
||||
3. **Customize workflows** — define your own artifacts and dependencies
|
||||
4. **Iterate quickly** — change a template, test immediately, no rebuild
|
||||
|
||||
```
|
||||
proposal → specs → design → tasks → implementation → archive
|
||||
Standard workflow: OPSX:
|
||||
┌────────────────────────┐ ┌────────────────────────┐
|
||||
│ Hardcoded in package │ │ schema.yaml │◄── You edit this
|
||||
│ (can't change) │ │ templates/*.md │◄── Or this
|
||||
│ ↓ │ │ ↓ │
|
||||
│ Wait for new release │ │ Instant effect │
|
||||
│ ↓ │ │ ↓ │
|
||||
│ Hope it's better │ │ Test it yourself │
|
||||
└────────────────────────┘ └────────────────────────┘
|
||||
```
|
||||
|
||||
Each artifact has dependencies. Can't write tasks until you have specs. Can't implement until you have tasks. The system tracks what's ready and what's blocked.
|
||||
**This is for everyone:**
|
||||
- **Teams** — create workflows that match how you actually work
|
||||
- **Power users** — tweak prompts to get better AI outputs for your codebase
|
||||
- **OpenSpec contributors** — experiment with new approaches without releases
|
||||
|
||||
We're all still learning what works best. OPSX lets us learn together.
|
||||
|
||||
## The User Experience
|
||||
|
||||
**The problem with linear workflows:**
|
||||
You're "in planning phase", then "in implementation phase", then "done". But real work doesn't work that way. You implement something, realize your design was wrong, need to update specs, continue implementing. Linear phases fight against how work actually happens.
|
||||
|
||||
**OPSX approach:**
|
||||
- **Actions, not phases** — create, implement, update, archive — do any of them anytime
|
||||
- **Dependencies are enablers** — they show what's possible, not what's required next
|
||||
- **Update as you learn** — halfway through implementation? Go back and fix the design. That's normal.
|
||||
|
||||
```
|
||||
You can always go back:
|
||||
|
||||
┌────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
proposal ──→ specs ──→ design ──→ tasks ──→ implement
|
||||
▲ ▲ ▲ │
|
||||
│ │ │ │
|
||||
└───────────┴──────────┴───────────────┘
|
||||
update as you learn
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -21,68 +73,558 @@ Each artifact has dependencies. Can't write tasks until you have specs. Can't im
|
||||
openspec init
|
||||
|
||||
# 2. Generate the experimental skills
|
||||
openspec artifact-experimental-setup
|
||||
openspec experimental
|
||||
```
|
||||
|
||||
This creates skills in `.claude/skills/` that Claude Code auto-detects.
|
||||
|
||||
During setup, you'll be prompted to create a **project config** (`openspec/config.yaml`). This is optional but recommended.
|
||||
|
||||
## Project Configuration
|
||||
|
||||
Project config lets you set defaults and inject project-specific context into all artifacts.
|
||||
|
||||
### Creating Config
|
||||
|
||||
Config is created during `experimental`, or manually:
|
||||
|
||||
```yaml
|
||||
# openspec/config.yaml
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: TypeScript, React, Node.js
|
||||
API conventions: RESTful, JSON responses
|
||||
Testing: Vitest for unit tests, Playwright for e2e
|
||||
Style: ESLint with Prettier, strict TypeScript
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Include rollback plan
|
||||
- Identify affected teams
|
||||
specs:
|
||||
- Use Given/When/Then format for scenarios
|
||||
design:
|
||||
- Include sequence diagrams for complex flows
|
||||
```
|
||||
|
||||
### Config Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `schema` | string | Default schema for new changes (e.g., `spec-driven`, `tdd`) |
|
||||
| `context` | string | Project context injected into all artifact instructions |
|
||||
| `rules` | object | Per-artifact rules, keyed by artifact ID |
|
||||
|
||||
### How It Works
|
||||
|
||||
**Schema precedence** (highest to lowest):
|
||||
1. CLI flag (`--schema tdd`)
|
||||
2. Change metadata (`.openspec.yaml` in change directory)
|
||||
3. Project config (`openspec/config.yaml`)
|
||||
4. Default (`spec-driven`)
|
||||
|
||||
**Context injection:**
|
||||
- Context is prepended to every artifact's instructions
|
||||
- Wrapped in `<context>...</context>` tags
|
||||
- Helps AI understand your project's conventions
|
||||
|
||||
**Rules injection:**
|
||||
- Rules are only injected for matching artifacts
|
||||
- Wrapped in `<rules>...</rules>` tags
|
||||
- Appear after context, before the template
|
||||
|
||||
### Artifact IDs by Schema
|
||||
|
||||
**spec-driven** (default):
|
||||
- `proposal` — Change proposal
|
||||
- `specs` — Specifications
|
||||
- `design` — Technical design
|
||||
- `tasks` — Implementation tasks
|
||||
|
||||
**tdd**:
|
||||
- `spec` — Feature specification
|
||||
- `tests` — Test file
|
||||
- `implementation` — Implementation code
|
||||
- `docs` — Documentation
|
||||
|
||||
### Config Validation
|
||||
|
||||
- Unknown artifact IDs in `rules` generate warnings
|
||||
- Schema names are validated against available schemas
|
||||
- Context has a 50KB size limit
|
||||
- Invalid YAML is reported with line numbers
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**"Unknown artifact ID in rules: X"**
|
||||
- Check artifact IDs match your schema (see list above)
|
||||
- Run `openspec schemas --json` to see artifact IDs for each schema
|
||||
|
||||
**Config not being applied:**
|
||||
- Ensure file is at `openspec/config.yaml` (not `.yml`)
|
||||
- Check YAML syntax with a validator
|
||||
- Config changes take effect immediately (no restart needed)
|
||||
|
||||
**Context too large:**
|
||||
- Context is limited to 50KB
|
||||
- Summarize or link to external docs instead
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:explore` | Think through ideas, investigate problems, clarify requirements |
|
||||
| `/opsx:new` | Start a new change |
|
||||
| `/opsx:continue` | Create the next artifact |
|
||||
| `/opsx:ff` | Fast-forward (create all artifacts at once) |
|
||||
| `/opsx:apply` | Implement the tasks |
|
||||
| `/opsx:continue` | Create the next artifact (based on what's ready) |
|
||||
| `/opsx:ff` | Fast-forward — create all planning artifacts at once |
|
||||
| `/opsx:apply` | Implement tasks, updating artifacts as needed |
|
||||
| `/opsx:sync` | Sync delta specs to main specs |
|
||||
| `/opsx:archive` | Archive when done |
|
||||
|
||||
## Usage
|
||||
|
||||
### Explore an idea
|
||||
```
|
||||
/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`.
|
||||
|
||||
### Start a new change
|
||||
```
|
||||
/opsx:new
|
||||
```
|
||||
You'll be asked what you want to build and which workflow schema to use.
|
||||
|
||||
### Build artifacts step-by-step
|
||||
### Create artifacts
|
||||
```
|
||||
/opsx:continue
|
||||
```
|
||||
Creates one artifact at a time. Good for reviewing each step.
|
||||
Shows what's ready to create based on dependencies, then creates one artifact. Use repeatedly to build up your change incrementally.
|
||||
|
||||
### Or fast-forward
|
||||
```
|
||||
/opsx:ff add-dark-mode
|
||||
```
|
||||
Creates all artifacts in one go. Good when you know what you want.
|
||||
Creates all planning artifacts at once. Use when you have a clear picture of what you're building.
|
||||
|
||||
### Implement
|
||||
### Implement (the fluid part)
|
||||
```
|
||||
/opsx:apply
|
||||
```
|
||||
Works through tasks, checking them off as you go.
|
||||
Works through tasks, checking them off as you go. **Key difference:** if you discover issues during implementation, you can update your specs, design, or tasks — then continue. No phase gates. If you're juggling multiple changes, you can run `/opsx:apply <name>`; otherwise it should infer from the conversation and prompt you to choose if it can’t tell.
|
||||
|
||||
### Sync specs and archive
|
||||
### Finish up
|
||||
```
|
||||
/opsx:sync # Update main specs with your delta specs
|
||||
/opsx:archive # Move to archive when done
|
||||
```
|
||||
|
||||
## When to Update vs. Start Fresh
|
||||
|
||||
OPSX lets you update artifacts anytime. But when does "update as you learn" become "this is different work"?
|
||||
|
||||
### What a Proposal Captures
|
||||
|
||||
A proposal defines three things:
|
||||
1. **Intent** — What problem are you solving?
|
||||
2. **Scope** — What's in/out of bounds?
|
||||
3. **Approach** — How will you solve it?
|
||||
|
||||
The question is: which changed, and by how much?
|
||||
|
||||
### Update the Existing Change When:
|
||||
|
||||
**Same intent, refined execution**
|
||||
- You discover edge cases you didn't consider
|
||||
- The approach needs tweaking but the goal is unchanged
|
||||
- Implementation reveals the design was slightly off
|
||||
|
||||
**Scope narrows**
|
||||
- You realize full scope is too big, want to ship MVP first
|
||||
- "Add dark mode" → "Add dark mode toggle (system preference in v2)"
|
||||
|
||||
**Learning-driven corrections**
|
||||
- Codebase isn't structured how you thought
|
||||
- A dependency doesn't work as expected
|
||||
- "Use CSS variables" → "Use Tailwind's dark: prefix instead"
|
||||
|
||||
### Start a New Change When:
|
||||
|
||||
**Intent fundamentally changed**
|
||||
- The problem itself is different now
|
||||
- "Add dark mode" → "Add comprehensive theme system with custom colors, fonts, spacing"
|
||||
|
||||
**Scope exploded**
|
||||
- Change grew so much it's essentially different work
|
||||
- Original proposal would be unrecognizable after updates
|
||||
- "Fix login bug" → "Rewrite auth system"
|
||||
|
||||
**Original is completable**
|
||||
- The original change can be marked "done"
|
||||
- New work stands alone, not a refinement
|
||||
- Complete "Add dark mode MVP" → Archive → New change "Enhance dark mode"
|
||||
|
||||
### The Heuristics
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Is this the same work? │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Same intent? >50% overlap? Can original
|
||||
Same problem? Same scope? be "done" without
|
||||
│ │ these changes?
|
||||
│ │ │
|
||||
┌────────┴────────┐ ┌──────┴──────┐ ┌───────┴───────┐
|
||||
│ │ │ │ │ │
|
||||
YES NO YES NO NO YES
|
||||
│ │ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
UPDATE NEW UPDATE NEW UPDATE NEW
|
||||
```
|
||||
|
||||
| Test | Update | New Change |
|
||||
|------|--------|------------|
|
||||
| **Identity** | "Same thing, refined" | "Different work" |
|
||||
| **Scope overlap** | >50% overlaps | <50% overlaps |
|
||||
| **Completion** | Can't be "done" without changes | Can finish original, new work stands alone |
|
||||
| **Story** | Update chain tells coherent story | Patches would confuse more than clarify |
|
||||
|
||||
### The Principle
|
||||
|
||||
> **Update preserves context. New change provides clarity.**
|
||||
>
|
||||
> Choose update when the history of your thinking is valuable.
|
||||
> Choose new when starting fresh would be clearer than patching.
|
||||
|
||||
Think of it like git branches:
|
||||
- Keep committing while working on the same feature
|
||||
- Start a new branch when it's genuinely new work
|
||||
- Sometimes merge a partial feature and start fresh for phase 2
|
||||
|
||||
## What's Different?
|
||||
|
||||
**Standard workflow** (`/openspec:proposal`):
|
||||
- One big proposal document
|
||||
- Linear phases: plan → implement → archive
|
||||
- All-or-nothing artifact creation
|
||||
| | Standard (`/openspec:proposal`) | Experimental (`/opsx:*`) |
|
||||
|---|---|---|
|
||||
| **Structure** | One big proposal document | Discrete artifacts with dependencies |
|
||||
| **Workflow** | Linear phases: plan → implement → archive | Fluid actions — do anything anytime |
|
||||
| **Iteration** | Awkward to go back | Update artifacts as you learn |
|
||||
| **Customization** | Fixed structure | Schema-driven (define your own artifacts) |
|
||||
|
||||
**Experimental workflow** (`/opsx:*`):
|
||||
- Discrete artifacts with dependencies
|
||||
- Fluid actions (not phases) - update artifacts anytime
|
||||
- Step-by-step or fast-forward
|
||||
- Schema-driven (can customize the workflow)
|
||||
**The key insight:** work isn't linear. OPSX stops pretending it is.
|
||||
|
||||
The key insight: work isn't linear. You implement, realize the design is wrong, update it, continue. OPSX supports this.
|
||||
## Architecture Deep Dive
|
||||
|
||||
This section explains how OPSX works under the hood and how it compares to the standard workflow.
|
||||
|
||||
### Philosophy: Phases vs Actions
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ STANDARD WORKFLOW │
|
||||
│ (Phase-Locked, All-or-Nothing) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ PLANNING │ ───► │ IMPLEMENTING │ ───► │ ARCHIVING │ │
|
||||
│ │ PHASE │ │ PHASE │ │ PHASE │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ /openspec:proposal /openspec:apply /openspec:archive │
|
||||
│ │
|
||||
│ • Creates ALL artifacts at once │
|
||||
│ • Can't go back to update specs during implementation │
|
||||
│ • Phase gates enforce linear progression │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OPSX WORKFLOW │
|
||||
│ (Fluid Actions, Iterative) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ ACTIONS (not phases) │ │
|
||||
│ │ │ │
|
||||
│ │ new ◄──► continue ◄──► apply ◄──► sync │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ └──────────┴───────────┴──────────┘ │ │
|
||||
│ │ any order │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ • Create artifacts one at a time OR fast-forward │
|
||||
│ • Update specs/design/tasks during implementation │
|
||||
│ • Dependencies enable progress, phases don't exist │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Architecture
|
||||
|
||||
**Standard workflow** uses hardcoded templates in TypeScript:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ STANDARD WORKFLOW COMPONENTS │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Hardcoded Templates (TypeScript strings) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Configurators (18+ classes, one per editor) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Generated Command Files (.claude/commands/openspec/*.md) │
|
||||
│ │
|
||||
│ • Fixed structure, no artifact awareness │
|
||||
│ • Change requires code modification + rebuild │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**OPSX** uses external schemas and a dependency graph engine:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OPSX COMPONENTS │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Schema Definitions (YAML) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ name: spec-driven │ │
|
||||
│ │ artifacts: │ │
|
||||
│ │ - id: proposal │ │
|
||||
│ │ generates: proposal.md │ │
|
||||
│ │ requires: [] ◄── Dependencies │ │
|
||||
│ │ - id: specs │ │
|
||||
│ │ generates: specs/**/*.md ◄── Glob patterns │ │
|
||||
│ │ requires: [proposal] ◄── Enables after proposal │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Artifact Graph Engine │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ • Topological sort (dependency ordering) │ │
|
||||
│ │ • State detection (filesystem existence) │ │
|
||||
│ │ • Rich instruction generation (templates + context) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Skill Files (.claude/skills/openspec-*/SKILL.md) │
|
||||
│ │
|
||||
│ • Cross-editor compatible (Claude Code, Cursor, Windsurf) │
|
||||
│ • Skills query CLI for structured data │
|
||||
│ • Fully customizable via schema files │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Dependency Graph Model
|
||||
|
||||
Artifacts form a directed acyclic graph (DAG). Dependencies are **enablers**, not gates:
|
||||
|
||||
```
|
||||
proposal
|
||||
(root node)
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
specs design
|
||||
(requires: (requires:
|
||||
proposal) proposal)
|
||||
│ │
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
tasks
|
||||
(requires:
|
||||
specs, design)
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ APPLY PHASE │
|
||||
│ (requires: │
|
||||
│ tasks) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**State transitions:**
|
||||
|
||||
```
|
||||
BLOCKED ────────────────► READY ────────────────► DONE
|
||||
│ │ │
|
||||
Missing All deps File exists
|
||||
dependencies are DONE on filesystem
|
||||
```
|
||||
|
||||
### Information Flow
|
||||
|
||||
**Standard workflow** — agent receives static instructions:
|
||||
|
||||
```
|
||||
User: "/openspec:proposal"
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Static instructions: │
|
||||
│ • Create proposal.md │
|
||||
│ • Create tasks.md │
|
||||
│ • Create design.md │
|
||||
│ • Create specs/*.md │
|
||||
│ │
|
||||
│ No awareness of what exists or │
|
||||
│ dependencies between artifacts │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Agent creates ALL artifacts in one go
|
||||
```
|
||||
|
||||
**OPSX** — agent queries for rich context:
|
||||
|
||||
```
|
||||
User: "/opsx:continue"
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Step 1: Query current state │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ $ openspec status --change "add-auth" --json │ │
|
||||
│ │ │ │
|
||||
│ │ { │ │
|
||||
│ │ "artifacts": [ │ │
|
||||
│ │ {"id": "proposal", "status": "done"}, │ │
|
||||
│ │ {"id": "specs", "status": "ready"}, ◄── First ready │ │
|
||||
│ │ {"id": "design", "status": "ready"}, │ │
|
||||
│ │ {"id": "tasks", "status": "blocked", "missingDeps": ["specs"]}│ │
|
||||
│ │ ] │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Step 2: Get rich instructions for ready artifact │
|
||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ $ openspec instructions specs --change "add-auth" --json │ │
|
||||
│ │ │ │
|
||||
│ │ { │ │
|
||||
│ │ "template": "# Specification\n\n## ADDED Requirements...", │ │
|
||||
│ │ "dependencies": [{"id": "proposal", "path": "...", "done": true}│ │
|
||||
│ │ "unlocks": ["tasks"] │ │
|
||||
│ │ } │ │
|
||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Step 3: Read dependencies → Create ONE artifact → Show what's unlocked │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Iteration Model
|
||||
|
||||
**Standard workflow** — awkward to iterate:
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│/proposal│ ──► │ /apply │ ──► │/archive │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
│ │
|
||||
│ ├── "Wait, the design is wrong"
|
||||
│ │
|
||||
│ ├── Options:
|
||||
│ │ • Edit files manually (breaks context)
|
||||
│ │ • Abandon and start over
|
||||
│ │ • Push through and fix later
|
||||
│ │
|
||||
│ └── No official "go back" mechanism
|
||||
│
|
||||
└── Creates ALL artifacts at once
|
||||
```
|
||||
|
||||
**OPSX** — natural iteration:
|
||||
|
||||
```
|
||||
/opsx:new ───► /opsx:continue ───► /opsx:apply ───► /opsx:archive
|
||||
│ │ │
|
||||
│ │ ├── "The design is wrong"
|
||||
│ │ │
|
||||
│ │ ▼
|
||||
│ │ Just edit design.md
|
||||
│ │ and continue!
|
||||
│ │ │
|
||||
│ │ ▼
|
||||
│ │ /opsx:apply picks up
|
||||
│ │ where you left off
|
||||
│ │
|
||||
│ └── Creates ONE artifact, shows what's unlocked
|
||||
│
|
||||
└── Scaffolds change, waits for direction
|
||||
```
|
||||
|
||||
### Custom Schemas
|
||||
|
||||
Create custom workflows using the schema management commands:
|
||||
|
||||
```bash
|
||||
# Create a new schema from scratch (interactive)
|
||||
openspec schema init my-workflow
|
||||
|
||||
# Or fork an existing schema as a starting point
|
||||
openspec schema fork spec-driven my-workflow
|
||||
|
||||
# Validate your schema structure
|
||||
openspec schema validate my-workflow
|
||||
|
||||
# See where a schema resolves from (useful for debugging)
|
||||
openspec schema which my-workflow
|
||||
```
|
||||
|
||||
Schemas are stored in `openspec/schemas/` (project-local, version controlled) or `~/.local/share/openspec/schemas/` (user global).
|
||||
|
||||
**Schema structure:**
|
||||
```
|
||||
openspec/schemas/research-first/
|
||||
├── schema.yaml
|
||||
└── templates/
|
||||
├── research.md
|
||||
├── proposal.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
**Example schema.yaml:**
|
||||
```yaml
|
||||
name: research-first
|
||||
artifacts:
|
||||
- id: research # Added before proposal
|
||||
generates: research.md
|
||||
requires: []
|
||||
|
||||
- id: proposal
|
||||
generates: proposal.md
|
||||
requires: [research] # Now depends on research
|
||||
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
requires: [proposal]
|
||||
```
|
||||
|
||||
**Dependency Graph:**
|
||||
```
|
||||
research ──► proposal ──► tasks
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
| Aspect | Standard | OPSX |
|
||||
|--------|----------|------|
|
||||
| **Templates** | Hardcoded TypeScript | External YAML + Markdown |
|
||||
| **Dependencies** | None (all at once) | DAG with topological sort |
|
||||
| **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 |
|
||||
|
||||
## Schemas
|
||||
|
||||
@@ -91,17 +633,33 @@ Schemas define what artifacts exist and their dependencies. Currently available:
|
||||
- **spec-driven** (default): proposal → specs → design → tasks
|
||||
- **tdd**: tests → implementation → docs
|
||||
|
||||
Run `openspec schemas` to see available schemas.
|
||||
```bash
|
||||
# List available schemas
|
||||
openspec schemas
|
||||
|
||||
# See all schemas with their resolution sources
|
||||
openspec schema which --all
|
||||
|
||||
# Create a new schema interactively
|
||||
openspec schema init my-workflow
|
||||
|
||||
# Fork an existing schema for customization
|
||||
openspec schema fork spec-driven my-workflow
|
||||
|
||||
# Validate schema structure before use
|
||||
openspec schema validate my-workflow
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `/opsx:ff` when you have a clear idea, `/opsx:continue` when exploring
|
||||
- Use `/opsx:explore` to think through an idea before committing to a change
|
||||
- `/opsx:ff` when you know what you want, `/opsx:continue` when exploring
|
||||
- During `/opsx:apply`, if something's wrong — fix the artifact, then continue
|
||||
- Tasks track progress via checkboxes in `tasks.md`
|
||||
- Delta specs (in `specs/`) get synced to main specs with `/opsx:sync`
|
||||
- If you get stuck, the status command shows what's blocked: `openspec status --change "name"`
|
||||
- Check status anytime: `openspec status --change "name"`
|
||||
|
||||
## Feedback
|
||||
|
||||
This is rough. That's intentional - we're learning what works.
|
||||
This is rough. That's intentional — we're learning what works.
|
||||
|
||||
Found a bug? Have ideas? Join us on [Discord](https://discord.gg/BYjPaKbqMt) or open an issue on [GitHub](https://github.com/Fission-AI/openspec/issues).
|
||||
Found a bug? Have ideas? Join us on [Discord](https://discord.gg/YctCnvvshC) or open an issue on [GitHub](https://github.com/Fission-AI/openspec/issues).
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# Project Config Demo Guide
|
||||
|
||||
A quick-reference guide for demonstrating the `openspec/config.yaml` feature.
|
||||
|
||||
## Summary: What Project Config Does
|
||||
|
||||
The feature adds `openspec/config.yaml` as a lightweight customization layer that lets teams:
|
||||
|
||||
- **Set a default schema** - New changes automatically use this schema instead of having to specify `--schema` every time
|
||||
- **Inject project context** - Shared context (tech stack, conventions) shown to AI when creating any artifact
|
||||
- **Add per-artifact rules** - Custom rules that only apply to specific artifacts (e.g., proposal, specs)
|
||||
|
||||
## Demo Walkthrough
|
||||
|
||||
### Demo 1: Interactive Setup (Recommended Entry Point)
|
||||
|
||||
The easiest way to demo is through the experimental setup command:
|
||||
|
||||
```bash
|
||||
openspec artifact-experimental-setup
|
||||
```
|
||||
|
||||
After creating skills/commands, it will prompt:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📋 Project Configuration (Optional)
|
||||
|
||||
Configure project defaults for OpenSpec workflows.
|
||||
|
||||
? Create openspec/config.yaml? (Y/n)
|
||||
```
|
||||
|
||||
Walk through:
|
||||
|
||||
1. **Select schema** - Shows available schemas with their artifact flows
|
||||
2. **Add context** - Opens editor for multi-line project context (tech stack, conventions)
|
||||
3. **Add rules** - Checkbox to select artifacts, then line-by-line rule entry
|
||||
|
||||
This creates `openspec/config.yaml` with the user's choices.
|
||||
|
||||
### Demo 2: Manual Config Creation
|
||||
|
||||
Show that users can create the config directly:
|
||||
|
||||
```bash
|
||||
cat > openspec/config.yaml << 'EOF'
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: TypeScript, React, Node.js, PostgreSQL
|
||||
API style: RESTful, documented in docs/api.md
|
||||
Testing: Jest + React Testing Library
|
||||
We value backwards compatibility for all public APIs
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Include rollback plan
|
||||
- Identify affected teams and notify in #platform-changes
|
||||
specs:
|
||||
- Use Given/When/Then format
|
||||
- Reference existing patterns before inventing new ones
|
||||
EOF
|
||||
```
|
||||
|
||||
### Demo 3: Effect on New Changes
|
||||
|
||||
Show that creating a new change now uses the default schema:
|
||||
|
||||
```bash
|
||||
# Before config: had to specify schema
|
||||
openspec new change my-feature --schema spec-driven
|
||||
|
||||
# After config: schema is automatic
|
||||
openspec new change my-feature
|
||||
# Automatically uses spec-driven from config
|
||||
```
|
||||
|
||||
### Demo 4: Context and Rules Injection
|
||||
|
||||
The key demo moment - show how instructions are enriched:
|
||||
|
||||
```bash
|
||||
# Get instructions for an artifact
|
||||
openspec instructions proposal --change my-feature
|
||||
```
|
||||
|
||||
Output shows the XML structure:
|
||||
|
||||
```xml
|
||||
<context>
|
||||
Tech stack: TypeScript, React, Node.js, PostgreSQL
|
||||
API style: RESTful, documented in docs/api.md
|
||||
...
|
||||
</context>
|
||||
|
||||
<rules>
|
||||
- Include rollback plan
|
||||
- Identify affected teams and notify in #platform-changes
|
||||
</rules>
|
||||
|
||||
<template>
|
||||
[Schema's built-in proposal template]
|
||||
</template>
|
||||
```
|
||||
|
||||
Key points to highlight:
|
||||
|
||||
- **Context** appears in ALL artifacts (proposal, specs, design, tasks)
|
||||
- **Rules** ONLY appear for the matching artifact (proposal rules only in proposal instructions)
|
||||
|
||||
### Demo 5: Precedence Override
|
||||
|
||||
Show the schema resolution order:
|
||||
|
||||
```bash
|
||||
# Config sets schema: spec-driven
|
||||
|
||||
# 1. CLI flag wins
|
||||
openspec new change feature-a --schema tdd # Uses tdd
|
||||
|
||||
# 2. Change metadata wins over config
|
||||
# (if .openspec.yaml in change directory specifies schema)
|
||||
|
||||
# 3. Config is used as default
|
||||
openspec new change feature-b # Uses spec-driven from config
|
||||
|
||||
# 4. Hardcoded default (no config)
|
||||
# Would fall back to spec-driven anyway
|
||||
```
|
||||
|
||||
### Demo 6: Validation and Error Handling
|
||||
|
||||
Show graceful error handling:
|
||||
|
||||
```bash
|
||||
# Create config with typo
|
||||
echo "schema: spec-drivne" > openspec/config.yaml
|
||||
|
||||
# Try to use it - shows fuzzy matching suggestions
|
||||
openspec new change test
|
||||
# Schema 'spec-drivne' not found
|
||||
# Did you mean: spec-driven (built-in)
|
||||
```
|
||||
|
||||
```bash
|
||||
# Unknown artifact ID in rules - warns but doesn't halt
|
||||
cat > openspec/config.yaml << 'EOF'
|
||||
schema: spec-driven
|
||||
rules:
|
||||
testplan: # Schema doesn't have this
|
||||
- Some rule
|
||||
EOF
|
||||
|
||||
openspec instructions proposal --change test
|
||||
# ⚠️ Unknown artifact ID in rules: "testplan". Valid IDs for schema "spec-driven": ...
|
||||
# (continues working)
|
||||
```
|
||||
|
||||
## Quick Demo Script
|
||||
|
||||
Here's a quick all-in-one demo:
|
||||
|
||||
```bash
|
||||
# 1. Show there's no config initially
|
||||
cat openspec/config.yaml 2>/dev/null || echo "No config exists"
|
||||
|
||||
# 2. Create a simple config
|
||||
cat > openspec/config.yaml << 'EOF'
|
||||
schema: spec-driven
|
||||
context: |
|
||||
This is a demo project using React and TypeScript.
|
||||
We follow semantic versioning.
|
||||
rules:
|
||||
proposal:
|
||||
- Include migration steps if breaking change
|
||||
EOF
|
||||
|
||||
# 3. Show the config
|
||||
cat openspec/config.yaml
|
||||
|
||||
# 4. Create a change (uses default schema from config)
|
||||
openspec new change demo-feature
|
||||
|
||||
# 5. Show instructions with injected context/rules
|
||||
openspec instructions proposal --change demo-feature | head -30
|
||||
|
||||
# 6. Show that specs don't have proposal rules
|
||||
openspec instructions specs --change demo-feature | head -30
|
||||
```
|
||||
|
||||
## What to Emphasize in Demo
|
||||
|
||||
- **Low friction** - Teams can customize without forking schemas
|
||||
- **Shared context** - Everyone on the team gets the same project knowledge
|
||||
- **Per-artifact rules** - Targeted guidance where it matters
|
||||
- **Graceful failures** - Typos warn, don't break workflow
|
||||
- **Team sharing** - Just commit `openspec/config.yaml` and everyone benefits
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Experimental Workflow Guide](./experimental-workflow.md) - Full user guide with config section
|
||||
- [Project Config Proposal](../openspec/changes/project-config/proposal.md) - Original design proposal
|
||||
- [Project Config Design](../openspec/changes/project-config/design.md) - Technical implementation details
|
||||
@@ -10,18 +10,18 @@ This document analyzes the complete user journey for working with schemas in Ope
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Schema resolution (XDG) | 2-level: user override → package built-in |
|
||||
| Schema resolution | 3-level: project → user → package (PR #522) |
|
||||
| Built-in schemas | `spec-driven`, `tdd` |
|
||||
| Artifact workflow commands | `status`, `next`, `instructions`, `templates` with `--schema` flag |
|
||||
| Change creation | `openspec new change <name>` — no schema binding |
|
||||
| Project-local schemas | ✅ Supported via `openspec/schemas/` (PR #522) |
|
||||
| Schema management CLI | ✅ `schema which`, `validate`, `fork`, `init` (PR #525) |
|
||||
|
||||
### What's Missing
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Schema bound to change | Not stored — must pass `--schema` every time |
|
||||
| Project-local schemas | Not supported — can't version control with repo |
|
||||
| Schema management CLI | None — manual path discovery required |
|
||||
| Project default schema | None — hardcoded to `spec-driven` |
|
||||
|
||||
---
|
||||
@@ -114,13 +114,13 @@ cp -r <package-path>/schemas/spec-driven/* \
|
||||
|
||||
## Gap Summary
|
||||
|
||||
| Gap | Impact | Workaround |
|
||||
|-----|--------|------------|
|
||||
| Schema not bound to change | Wrong results, forgotten context | Remember to pass `--schema` |
|
||||
| No project-local schemas | Can't share via repo | Manual XDG setup per machine |
|
||||
| No schema management CLI | Manual path hunting | Know XDG + find npm package |
|
||||
| No project default schema | Must specify every time | Always pass `--schema` |
|
||||
| No init-time schema selection | Missed setup opportunity | Manual config |
|
||||
| Gap | Impact | Status |
|
||||
|-----|--------|--------|
|
||||
| Schema not bound to change | Wrong results, forgotten context | ⏳ Pending (Phase 1) |
|
||||
| No project-local schemas | Can't share via repo | ✅ Fixed (PR #522) |
|
||||
| No schema management CLI | Manual path hunting | ✅ Fixed (PR #525) |
|
||||
| No project default schema | Must specify every time | ⏳ Pending (Phase 4) |
|
||||
| No init-time schema selection | Missed setup opportunity | ⏳ Pending (Phase 4) |
|
||||
|
||||
---
|
||||
|
||||
@@ -296,18 +296,17 @@ created: 2025-01-15T10:30:00Z
|
||||
|
||||
### Phase 2: Project-Local Schemas
|
||||
|
||||
**Priority:** High
|
||||
**Status:** ✅ Complete (PR #522)
|
||||
**Solves:** Team sharing, version control, no XDG knowledge needed
|
||||
|
||||
**Scope:**
|
||||
- Add `./openspec/schemas/` to resolution order (first priority)
|
||||
- `openspec schema copy <name> [new-name]` creates in project by default
|
||||
- `--global` flag for user-level XDG directory
|
||||
**Implemented:**
|
||||
- `./openspec/schemas/` added to resolution order (first priority)
|
||||
- `openspec schema fork <name> [new-name]` creates in project by default
|
||||
- Teams can commit `openspec/schemas/` to repo
|
||||
|
||||
**Resolution order:**
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local (NEW)
|
||||
1. ./openspec/schemas/<name>/ # Project-local
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User global
|
||||
3. <npm-package>/schemas/<name>/ # Built-in
|
||||
```
|
||||
@@ -316,19 +315,21 @@ created: 2025-01-15T10:30:00Z
|
||||
|
||||
### Phase 3: Schema Management CLI
|
||||
|
||||
**Priority:** Medium
|
||||
**Status:** ✅ Complete (PR #525)
|
||||
**Solves:** Path discovery, scaffolding, debugging
|
||||
|
||||
**Commands:**
|
||||
**Implemented Commands:**
|
||||
```bash
|
||||
openspec schema list # Show available schemas with sources
|
||||
openspec schema which <name> # Show resolution path
|
||||
openspec schema copy <name> [to] # Copy for customization
|
||||
openspec schema diff <name> # Compare with built-in
|
||||
openspec schema reset <name> # Remove override
|
||||
openspec schema validate <name> # Validate schema.yaml structure
|
||||
openspec schema which [name] # Show resolution path, --all for all schemas
|
||||
openspec schema validate [name] # Validate schema structure and templates
|
||||
openspec schema fork <source> [name] # Copy existing schema for customization
|
||||
openspec schema init <name> # Create new project-local schema (interactive)
|
||||
```
|
||||
|
||||
**Not implemented (may add later):**
|
||||
- `schema diff` — Compare override with built-in
|
||||
- `schema reset` — Remove override, revert to built-in
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Project Config + Init Enhancement
|
||||
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767640445,
|
||||
"narHash": "sha256-UWYqmD7JFBEDBHWYcqE6s6c77pWdcU/i+bwD6XxMb8A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9f0c42f8bc7151b8e7e5840fb3bd454ad850d8c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
description = "OpenSpec - AI-native system for spec-driven development";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "openspec";
|
||||
version = "0.23.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
pnpmDeps = pkgs.fetchPnpmDeps {
|
||||
inherit (finalAttrs) pname version src;
|
||||
pnpm = pkgs.pnpm_9;
|
||||
fetcherVersion = 3;
|
||||
hash = "sha256-9s2kdvd7svK4hofnD66HkDc86WTQeayfF5y7L2dmjNg=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
nodejs_20
|
||||
npmHooks.npmInstallHook
|
||||
pnpmConfigHook
|
||||
pnpm_9
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
pnpm run build
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
dontNpmPrune = true;
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "AI-native system for spec-driven development";
|
||||
homepage = "https://github.com/Fission-AI/OpenSpec";
|
||||
license = licenses.mit;
|
||||
maintainers = [ ];
|
||||
mainProgram = "openspec";
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
apps = forAllSystems (system: {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/openspec";
|
||||
};
|
||||
});
|
||||
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_20
|
||||
pnpm_9
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "OpenSpec development environment"
|
||||
echo "Node version: $(node --version)"
|
||||
echo "pnpm version: $(pnpm --version)"
|
||||
echo "Run 'pnpm install' to install dependencies"
|
||||
'';
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
|
||||
## TL;DR Quick Checklist
|
||||
|
||||
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||
- Decide scope: new capability vs modify existing capability
|
||||
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||
- Request approval: Do not start implementation until proposal is approved
|
||||
|
||||
## Three-Stage Workflow
|
||||
|
||||
### Stage 1: Creating Changes
|
||||
Create proposal when you need to:
|
||||
- Add features or functionality
|
||||
- Make breaking changes (API, schema)
|
||||
- Change architecture or patterns
|
||||
- Optimize performance (changes behavior)
|
||||
- Update security patterns
|
||||
|
||||
Triggers (examples):
|
||||
- "Help me create a change proposal"
|
||||
- "Help me plan a change"
|
||||
- "Help me create a proposal"
|
||||
- "I want to create a spec proposal"
|
||||
- "I want to create a spec"
|
||||
|
||||
Loose matching guidance:
|
||||
- Contains one of: `proposal`, `change`, `spec`
|
||||
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||
|
||||
Skip proposal for:
|
||||
- Bug fixes (restore intended behavior)
|
||||
- Typos, formatting, comments
|
||||
- Dependency updates (non-breaking)
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||
- Update `specs/` if capabilities changed
|
||||
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
**Context Checklist:**
|
||||
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||
- [ ] Check pending changes in `changes/` for conflicts
|
||||
- [ ] Read `openspec/project.md` for conventions
|
||||
- [ ] Run `openspec list` to see active changes
|
||||
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||
|
||||
**Before Creating Specs:**
|
||||
- Always check if capability already exists
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- Use `openspec show [spec]` to review current state
|
||||
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||
|
||||
### Search Guidance
|
||||
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||
- Show details:
|
||||
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||
- Change: `openspec show <change-id> --json --deltas-only`
|
||||
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
# Essential commands
|
||||
openspec list # List active changes
|
||||
openspec list --specs # List specifications
|
||||
openspec show [item] # Display change or spec
|
||||
openspec validate [item] # Validate changes or specs
|
||||
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||
|
||||
# Project management
|
||||
openspec init [path] # Initialize OpenSpec
|
||||
openspec update [path] # Update instruction files
|
||||
|
||||
# Interactive mode
|
||||
openspec show # Prompts for selection
|
||||
openspec validate # Bulk validation mode
|
||||
|
||||
# Debugging
|
||||
openspec show [change] --json --deltas-only
|
||||
openspec validate [change] --strict
|
||||
```
|
||||
|
||||
### Command Flags
|
||||
|
||||
- `--json` - Machine-readable output
|
||||
- `--type change|spec` - Disambiguate items
|
||||
- `--strict` - Comprehensive validation
|
||||
- `--no-interactive` - Disable prompts
|
||||
- `--skip-specs` - Archive without spec updates
|
||||
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project conventions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ └── [capability]/ # Single focused capability
|
||||
│ ├── spec.md # Requirements and scenarios
|
||||
│ └── design.md # Technical patterns
|
||||
├── changes/ # Proposals - what SHOULD change
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||
│ │ └── specs/ # Delta changes
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||
│ └── archive/ # Completed changes
|
||||
```
|
||||
|
||||
## Creating Change Proposals
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
New request?
|
||||
├─ Bug fix restoring spec behavior? → Fix directly
|
||||
├─ Typo/format/comment? → Fix directly
|
||||
├─ New feature/capability? → Create proposal
|
||||
├─ Breaking change? → Create proposal
|
||||
├─ Architecture change? → Create proposal
|
||||
└─ Unclear? → Create proposal (safer)
|
||||
```
|
||||
|
||||
### Proposal Structure
|
||||
|
||||
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||
|
||||
2. **Write proposal.md:**
|
||||
```markdown
|
||||
## Why
|
||||
[1-2 sentences on problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
- [Bullet list of changes]
|
||||
- [Mark breaking changes with **BREAKING**]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities]
|
||||
- Affected code: [key files/systems]
|
||||
```
|
||||
|
||||
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
The system SHALL provide...
|
||||
|
||||
#### Scenario: Success case
|
||||
- **WHEN** user performs action
|
||||
- **THEN** expected result
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason**: [Why removing]
|
||||
**Migration**: [How to handle]
|
||||
```
|
||||
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||
|
||||
4. **Create tasks.md:**
|
||||
```markdown
|
||||
## 1. Implementation
|
||||
- [ ] 1.1 Create database schema
|
||||
- [ ] 1.2 Implement API endpoint
|
||||
- [ ] 1.3 Add frontend component
|
||||
- [ ] 1.4 Write tests
|
||||
```
|
||||
|
||||
5. **Create design.md when needed:**
|
||||
Create `design.md` if any of the following apply; otherwise omit it:
|
||||
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Minimal `design.md` skeleton:
|
||||
```markdown
|
||||
## Context
|
||||
[Background, constraints, stakeholders]
|
||||
|
||||
## Goals / Non-Goals
|
||||
- Goals: [...]
|
||||
- Non-Goals: [...]
|
||||
|
||||
## Decisions
|
||||
- Decision: [What and why]
|
||||
- Alternatives considered: [Options + rationale]
|
||||
|
||||
## Risks / Trade-offs
|
||||
- [Risk] → Mitigation
|
||||
|
||||
## Migration Plan
|
||||
[Steps, rollback]
|
||||
|
||||
## Open Questions
|
||||
- [...]
|
||||
```
|
||||
|
||||
## Spec File Format
|
||||
|
||||
### Critical: Scenario Formatting
|
||||
|
||||
**CORRECT** (use #### headers):
|
||||
```markdown
|
||||
#### Scenario: User login success
|
||||
- **WHEN** valid credentials provided
|
||||
- **THEN** return JWT token
|
||||
```
|
||||
|
||||
**WRONG** (don't use bullets or bold):
|
||||
```markdown
|
||||
- **Scenario: User login** ❌
|
||||
**Scenario**: User login ❌
|
||||
### Scenario: User login ❌
|
||||
```
|
||||
|
||||
Every requirement MUST have at least one scenario.
|
||||
|
||||
### Requirement Wording
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||
|
||||
### Delta Operations
|
||||
|
||||
- `## ADDED Requirements` - New capabilities
|
||||
- `## MODIFIED Requirements` - Changed behavior
|
||||
- `## REMOVED Requirements` - Deprecated features
|
||||
- `## RENAMED Requirements` - Name changes
|
||||
|
||||
Headers matched with `trim(header)` - whitespace ignored.
|
||||
|
||||
#### When to use ADDED vs MODIFIED
|
||||
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||
|
||||
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||
|
||||
Authoring a MODIFIED requirement correctly:
|
||||
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||
|
||||
Example for RENAMED:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Login`
|
||||
- TO: `### Requirement: User Authentication`
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
**"Change must have at least one delta"**
|
||||
- Check `changes/[name]/specs/` exists with .md files
|
||||
- Verify files have operation prefixes (## ADDED Requirements)
|
||||
|
||||
**"Requirement must have at least one scenario"**
|
||||
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||
- Don't use bullet points or bold for scenario headers
|
||||
|
||||
**Silent scenario parsing failures**
|
||||
- Exact format required: `#### Scenario: Name`
|
||||
- Debug with: `openspec show [change] --json --deltas-only`
|
||||
|
||||
### Validation Tips
|
||||
|
||||
```bash
|
||||
# Always use strict mode for comprehensive checks
|
||||
openspec validate [change] --strict
|
||||
|
||||
# Debug delta parsing
|
||||
openspec show [change] --json | jq '.deltas'
|
||||
|
||||
# Check specific requirement
|
||||
openspec show [spec] --json -r 1
|
||||
```
|
||||
|
||||
## Happy Path Script
|
||||
|
||||
```bash
|
||||
# 1) Explore current state
|
||||
openspec spec list --long
|
||||
openspec list
|
||||
# Optional full-text search:
|
||||
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||
# rg -n "^#|Requirement:" openspec/changes
|
||||
|
||||
# 2) Choose change id and scaffold
|
||||
CHANGE=add-two-factor-auth
|
||||
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||
|
||||
# 3) Add deltas (example)
|
||||
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
Users MUST provide a second factor during login.
|
||||
|
||||
#### Scenario: OTP required
|
||||
- **WHEN** valid credentials are provided
|
||||
- **THEN** an OTP challenge is required
|
||||
EOF
|
||||
|
||||
# 4) Validate
|
||||
openspec validate $CHANGE --strict
|
||||
```
|
||||
|
||||
## Multi-Capability Example
|
||||
|
||||
```
|
||||
openspec/changes/add-2fa-notify/
|
||||
├── proposal.md
|
||||
├── tasks.md
|
||||
└── specs/
|
||||
├── auth/
|
||||
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||
└── notifications/
|
||||
└── spec.md # ADDED: OTP email notification
|
||||
```
|
||||
|
||||
auth/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
...
|
||||
```
|
||||
|
||||
notifications/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: OTP Email Notification
|
||||
...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Simplicity First
|
||||
- Default to <100 lines of new code
|
||||
- Single-file implementations until proven insufficient
|
||||
- Avoid frameworks without clear justification
|
||||
- Choose boring, proven patterns
|
||||
|
||||
### Complexity Triggers
|
||||
Only add complexity with:
|
||||
- Performance data showing current solution too slow
|
||||
- Concrete scale requirements (>1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring abstraction
|
||||
|
||||
### Clear References
|
||||
- Use `file.ts:42` format for code locations
|
||||
- Reference specs as `specs/auth/spec.md`
|
||||
- Link related changes and PRs
|
||||
|
||||
### Capability Naming
|
||||
- Use verb-noun: `user-auth`, `payment-capture`
|
||||
- Single purpose per capability
|
||||
- 10-minute understandability rule
|
||||
- Split if description needs "AND"
|
||||
|
||||
### Change ID Naming
|
||||
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
| Task | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Find files by pattern | Glob | Fast pattern matching |
|
||||
| Search code content | Grep | Optimized regex search |
|
||||
| Read specific files | Read | Direct file access |
|
||||
| Explore unknown scope | Task | Multi-step investigation |
|
||||
|
||||
## Error Recovery
|
||||
|
||||
### Change Conflicts
|
||||
1. Run `openspec list` to see active changes
|
||||
2. Check for overlapping specs
|
||||
3. Coordinate with change owners
|
||||
4. Consider combining proposals
|
||||
|
||||
### Validation Failures
|
||||
1. Run with `--strict` flag
|
||||
2. Check JSON output for details
|
||||
3. Verify spec file format
|
||||
4. Ensure scenarios properly formatted
|
||||
|
||||
### Missing Context
|
||||
1. Read project.md first
|
||||
2. Check related specs
|
||||
3. Review recent archives
|
||||
4. Ask for clarification
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Stage Indicators
|
||||
- `changes/` - Proposed, not yet built
|
||||
- `specs/` - Built and deployed
|
||||
- `archive/` - Completed changes
|
||||
|
||||
### File Purposes
|
||||
- `proposal.md` - Why and what
|
||||
- `tasks.md` - Implementation steps
|
||||
- `design.md` - Technical decisions
|
||||
- `spec.md` - Requirements and behavior
|
||||
|
||||
### CLI Essentials
|
||||
```bash
|
||||
openspec list # What's in progress?
|
||||
openspec show [item] # View details
|
||||
openspec validate --strict # Is it correct?
|
||||
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||
```
|
||||
|
||||
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||
@@ -0,0 +1,20 @@
|
||||
## Why
|
||||
|
||||
Users and agents need a simple way to submit feedback about OpenSpec directly from the CLI. Currently there's no mechanism to collect user feedback, feature requests, or bug reports in a way that enables follow-up conversation. Using GitHub Issues allows us to track feedback, prevent spam via GitHub auth, and enables outreach to users.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `openspec feedback <message>` CLI command
|
||||
- Leverage `gh` CLI for GitHub authentication and issue creation
|
||||
- Add `/feedback` skill for agent-assisted feedback with context enrichment
|
||||
- Ensure cross-platform compatibility (macOS, Linux, Windows)
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New `cli-feedback` capability
|
||||
- Affected code:
|
||||
- `src/cli/index.ts` - Register feedback command
|
||||
- `src/commands/feedback.ts` - Command implementation using `gh` CLI
|
||||
- `src/core/templates/skill-templates.ts` - Feedback skill template
|
||||
- `src/core/completions/command-registry.ts` - Shell completions
|
||||
- External dependency: Requires `gh` CLI installed and authenticated
|
||||
@@ -0,0 +1,188 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Feedback command
|
||||
|
||||
The system SHALL provide an `openspec feedback` command that creates a GitHub Issue in the openspec repository using the `gh` CLI. The system SHALL use `execFileSync` with argument arrays to prevent shell injection vulnerabilities.
|
||||
|
||||
#### Scenario: Simple feedback submission
|
||||
|
||||
- **WHEN** user executes `openspec feedback "Great tool!"`
|
||||
- **THEN** the system executes `gh issue create` with title "Feedback: Great tool!"
|
||||
- **AND** the issue is created in the openspec repository
|
||||
- **AND** the issue has the `feedback` label
|
||||
- **AND** the system displays the created issue URL
|
||||
|
||||
#### Scenario: Safe command execution
|
||||
|
||||
- **WHEN** submitting feedback via `gh` CLI
|
||||
- **THEN** the system uses `execFileSync` with separate arguments array
|
||||
- **AND** user input is NOT passed through a shell
|
||||
- **AND** shell metacharacters (quotes, backticks, $(), etc.) are treated as literal text
|
||||
|
||||
#### Scenario: Feedback with body
|
||||
|
||||
- **WHEN** user executes `openspec feedback "Title here" --body "Detailed description..."`
|
||||
- **THEN** the system creates a GitHub Issue with the specified title
|
||||
- **AND** the issue body contains the detailed description
|
||||
- **AND** the issue body includes metadata (OpenSpec version, platform, timestamp)
|
||||
|
||||
### Requirement: GitHub CLI dependency
|
||||
|
||||
The system SHALL use `gh` CLI for automatic feedback submission when available, and provide a manual submission fallback when `gh` is not installed or not authenticated. The system SHALL use platform-appropriate commands to detect `gh` CLI availability.
|
||||
|
||||
#### Scenario: Missing gh CLI with fallback
|
||||
|
||||
- **WHEN** user runs `openspec feedback "message"`
|
||||
- **AND** `gh` CLI is not installed (not found in PATH)
|
||||
- **THEN** the system displays warning: "GitHub CLI not found. Manual submission required."
|
||||
- **AND** outputs structured feedback content with delimiters:
|
||||
- "--- FORMATTED FEEDBACK ---"
|
||||
- Title line
|
||||
- Labels line
|
||||
- Body content with metadata
|
||||
- "--- END FEEDBACK ---"
|
||||
- **AND** displays pre-filled GitHub issue URL for manual submission
|
||||
- **AND** exits with zero code (successful fallback)
|
||||
|
||||
#### Scenario: Cross-platform gh CLI detection on Unix
|
||||
|
||||
- **WHEN** system is running on macOS or Linux (platform is 'darwin' or 'linux')
|
||||
- **AND** checking if `gh` CLI is installed
|
||||
- **THEN** the system executes `which gh` command
|
||||
|
||||
#### Scenario: Cross-platform gh CLI detection on Windows
|
||||
|
||||
- **WHEN** system is running on Windows (platform is 'win32')
|
||||
- **AND** checking if `gh` CLI is installed
|
||||
- **THEN** the system executes `where gh` command
|
||||
|
||||
#### Scenario: Unauthenticated gh CLI with fallback
|
||||
|
||||
- **WHEN** user runs `openspec feedback "message"`
|
||||
- **AND** `gh` CLI is installed but not authenticated
|
||||
- **THEN** the system displays warning: "GitHub authentication required. Manual submission required."
|
||||
- **AND** outputs structured feedback content (same format as missing gh CLI scenario)
|
||||
- **AND** displays pre-filled GitHub issue URL for manual submission
|
||||
- **AND** displays authentication instructions: "To auto-submit in the future: gh auth login"
|
||||
- **AND** exits with zero code (successful fallback)
|
||||
|
||||
#### Scenario: Authenticated gh CLI
|
||||
|
||||
- **WHEN** user runs `openspec feedback "message"`
|
||||
- **AND** `gh auth status` returns success (authenticated)
|
||||
- **THEN** the system proceeds with feedback submission
|
||||
|
||||
### Requirement: Issue metadata
|
||||
|
||||
The system SHALL include relevant metadata in the GitHub Issue body.
|
||||
|
||||
#### Scenario: Standard metadata
|
||||
|
||||
- **WHEN** creating a GitHub Issue for feedback
|
||||
- **THEN** the issue body includes:
|
||||
- OpenSpec CLI version
|
||||
- Platform (darwin, linux, win32)
|
||||
- Submission timestamp
|
||||
- Separator line: "---\nSubmitted via OpenSpec CLI"
|
||||
|
||||
#### Scenario: Windows platform metadata
|
||||
|
||||
- **WHEN** creating a GitHub Issue for feedback on Windows
|
||||
- **THEN** the issue body includes "Platform: win32"
|
||||
- **AND** all platform detection uses Node.js `os.platform()` API
|
||||
|
||||
#### Scenario: No sensitive metadata
|
||||
|
||||
- **WHEN** creating a GitHub Issue for feedback
|
||||
- **THEN** the issue body does NOT include:
|
||||
- File paths from user's system
|
||||
- Project names or directory names
|
||||
- Environment variables
|
||||
- IP addresses
|
||||
|
||||
### Requirement: Feedback always works
|
||||
|
||||
The system SHALL allow feedback submission regardless of telemetry settings.
|
||||
|
||||
#### Scenario: Feedback with telemetry disabled
|
||||
|
||||
- **WHEN** user has disabled telemetry via `OPENSPEC_TELEMETRY=0`
|
||||
- **AND** user runs `openspec feedback "message"`
|
||||
- **THEN** the feedback is still submitted via `gh` CLI
|
||||
- **AND** telemetry events are not sent
|
||||
|
||||
#### Scenario: Feedback in CI environment
|
||||
|
||||
- **WHEN** `CI=true` is set in the environment
|
||||
- **AND** user runs `openspec feedback "message"`
|
||||
- **THEN** the feedback submission proceeds normally (if `gh` is available and authenticated)
|
||||
|
||||
### Requirement: Error handling
|
||||
|
||||
The system SHALL handle feedback submission errors gracefully.
|
||||
|
||||
#### Scenario: gh CLI execution failure
|
||||
|
||||
- **WHEN** `gh issue create` command fails
|
||||
- **THEN** the system displays the error output from `gh` CLI
|
||||
- **AND** exits with the same exit code as `gh`
|
||||
|
||||
#### Scenario: Network failure
|
||||
|
||||
- **WHEN** `gh` CLI reports network connectivity issues
|
||||
- **THEN** the system displays the error message from `gh`
|
||||
- **AND** suggests checking network connectivity
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
### Requirement: Feedback skill for agents
|
||||
|
||||
The system SHALL provide a `/feedback` skill that guides agents through collecting and submitting user feedback.
|
||||
|
||||
#### Scenario: Agent-initiated feedback
|
||||
|
||||
- **WHEN** user invokes `/feedback` in an agent conversation
|
||||
- **THEN** the agent gathers context from the conversation
|
||||
- **AND** drafts a feedback issue with enriched content
|
||||
- **AND** anonymizes sensitive information
|
||||
- **AND** presents the draft to the user for approval
|
||||
- **AND** submits via `openspec feedback` command on user confirmation
|
||||
|
||||
#### Scenario: Context enrichment
|
||||
|
||||
- **WHEN** agent drafts feedback
|
||||
- **THEN** the agent includes relevant context such as:
|
||||
- What task was being performed
|
||||
- What worked well or poorly
|
||||
- Specific friction points or praise
|
||||
|
||||
#### Scenario: Anonymization
|
||||
|
||||
- **WHEN** agent drafts feedback
|
||||
- **THEN** the agent removes or replaces:
|
||||
- File paths with `<path>` or generic descriptions
|
||||
- API keys, tokens, secrets with `<redacted>`
|
||||
- Company/organization names with `<company>`
|
||||
- Personal names with `<user>`
|
||||
- Specific URLs with `<url>` unless public/relevant
|
||||
|
||||
#### Scenario: User confirmation required
|
||||
|
||||
- **WHEN** agent has drafted feedback
|
||||
- **THEN** the agent MUST show the complete draft to the user
|
||||
- **AND** ask for explicit approval before submitting
|
||||
- **AND** allow the user to request modifications
|
||||
- **AND** only submit after user confirms
|
||||
|
||||
### Requirement: Shell completions
|
||||
|
||||
The system SHALL provide shell completions for the feedback command.
|
||||
|
||||
#### Scenario: Command completion
|
||||
|
||||
- **WHEN** user types `openspec fee<TAB>`
|
||||
- **THEN** the shell completes to `openspec feedback`
|
||||
|
||||
#### Scenario: Flag completion
|
||||
|
||||
- **WHEN** user types `openspec feedback "msg" --<TAB>`
|
||||
- **THEN** the shell suggests available flags (`--body`)
|
||||
@@ -0,0 +1,30 @@
|
||||
## 1. Feedback Command
|
||||
|
||||
- [x] 1.1 Create `src/commands/feedback.ts` with command implementation
|
||||
- [x] 1.2 Check `gh` CLI availability using platform-appropriate command (`which` on Unix/macOS, `where` on Windows)
|
||||
- [x] 1.3 Check GitHub auth status with `gh auth status`
|
||||
- [x] 1.4 Execute `gh issue create` with formatted title and body using `execFileSync` to prevent shell injection
|
||||
- [x] 1.5 Display issue URL returned by `gh` CLI
|
||||
- [x] 1.6 Register `feedback <message>` command in `src/cli/index.ts`
|
||||
- [x] 1.7 Ensure cross-platform compatibility (macOS, Linux, Windows)
|
||||
|
||||
## 2. Shell Completions
|
||||
|
||||
- [x] 2.1 Add `feedback` command to command registry
|
||||
- [x] 2.2 Regenerate completion scripts for all shells
|
||||
|
||||
## 3. Feedback Skill
|
||||
|
||||
- [x] 3.1 Create feedback skill template in `skill-templates.ts`
|
||||
- [x] 3.2 Document context gathering workflow
|
||||
- [x] 3.3 Document anonymization rules
|
||||
- [x] 3.4 Document user confirmation flow
|
||||
|
||||
## 4. Testing
|
||||
|
||||
- [x] 4.1 Add unit tests for feedback command (mock `gh` subprocess calls)
|
||||
- [x] 4.2 Add integration test for full feedback flow with mocked `gh` CLI
|
||||
- [x] 4.3 Test error handling for missing `gh` CLI
|
||||
- [x] 4.4 Test error handling for unauthenticated `gh` session
|
||||
- [x] 4.5 Test cross-platform `gh` CLI detection (verify `which` on Unix, `where` on Windows)
|
||||
- [x] 4.6 Test platform metadata includes correct value for Windows (win32)
|
||||
@@ -0,0 +1,96 @@
|
||||
# Design: Add /opsx:verify Skill
|
||||
|
||||
## Architecture Decision: Dynamic Generation via Setup Command
|
||||
|
||||
### Context
|
||||
|
||||
All existing opsx experimental skills (explore, new, continue, apply, ff, sync, archive) are dynamically generated when users run `openspec artifact-experimental-setup`. They are not manually created files checked into the repository.
|
||||
|
||||
### Decision
|
||||
|
||||
**Integrate verify into the existing artifact-experimental-setup system rather than creating static skill files.**
|
||||
|
||||
### Rationale
|
||||
|
||||
1. **Consistency**: All 7 existing opsx skills follow this pattern. Adding verify as the 8th skill should follow the same architecture.
|
||||
|
||||
2. **Maintainability**: Template functions in `skill-templates.ts` are the single source of truth. Changes to skill definitions automatically propagate to all users when they re-run setup.
|
||||
|
||||
3. **Distribution**: Users get the verify skill automatically when running `openspec artifact-experimental-setup`, just like all other opsx skills. No special installation steps needed.
|
||||
|
||||
4. **Versioning**: Skills are generated from the installed npm package version, ensuring consistency between CLI version and skill behavior.
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
#### 1. Template Functions
|
||||
|
||||
Add two template functions to `src/core/templates/skill-templates.ts`:
|
||||
|
||||
```typescript
|
||||
export function getVerifyChangeSkillTemplate(): SkillTemplate
|
||||
export function getOpsxVerifyCommandTemplate(): CommandTemplate
|
||||
```
|
||||
|
||||
These return the skill definition (for Agent Skills) and slash command definition (for explicit invocation).
|
||||
|
||||
#### 2. Setup Integration
|
||||
|
||||
Update `artifactExperimentalSetupCommand()` in `src/commands/artifact-workflow.ts`:
|
||||
|
||||
- Import both template functions
|
||||
- Add verify to the `skills` array (position 8)
|
||||
- Add verify to the `commands` array (position 8)
|
||||
- Update help text to list `/opsx:verify`
|
||||
|
||||
#### 3. Generated Artifacts
|
||||
|
||||
When users run `openspec artifact-experimental-setup`, the command creates:
|
||||
|
||||
- `.claude/skills/openspec-verify-change/SKILL.md` - Agent Skills format
|
||||
- `.claude/commands/opsx/verify.md` - Slash command format
|
||||
|
||||
Both are generated from the template functions, with YAML frontmatter automatically added.
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
**Alternative 1: Static skill files in repository**
|
||||
|
||||
Create `.claude/skills/openspec-verify-change/SKILL.md` as a static file in the OpenSpec repository.
|
||||
|
||||
**Rejected because:**
|
||||
- Inconsistent with all other opsx skills
|
||||
- Requires users to manually copy/update files
|
||||
- Versioning becomes complicated (repo version vs installed package version)
|
||||
- Breaks the established pattern
|
||||
|
||||
**Alternative 2: Separate verify setup command**
|
||||
|
||||
Add `openspec setup-verify` as a separate command.
|
||||
|
||||
**Rejected because:**
|
||||
- Fragments the setup experience
|
||||
- Users would need to run multiple commands
|
||||
- Doesn't scale if we add more skills in the future
|
||||
- Goes against the "setup once, get everything" philosophy
|
||||
|
||||
### Trade-offs
|
||||
|
||||
**Advantages:**
|
||||
- Consistent with existing architecture
|
||||
- Zero additional setup burden for users
|
||||
- Easy to update and maintain
|
||||
- Automatic version compatibility
|
||||
|
||||
**Disadvantages:**
|
||||
- Slightly more complex initial implementation (template functions + integration)
|
||||
- Requires understanding the setup system (but that's already documented)
|
||||
|
||||
### Verification
|
||||
|
||||
The implementation correctly follows this design if:
|
||||
|
||||
1. Both template functions exist in `skill-templates.ts`
|
||||
2. Verify appears in both skills and commands arrays in `artifact-workflow.ts`
|
||||
3. Help text mentions `/opsx:verify`
|
||||
4. Running `openspec artifact-experimental-setup` generates both skill and command files
|
||||
5. Build succeeds with no TypeScript errors
|
||||
@@ -0,0 +1,48 @@
|
||||
# Change: Add /opsx:verify Skill
|
||||
|
||||
## Why
|
||||
|
||||
Users need a way to validate that their implementation actually matches what was requested before archiving a change. Currently, there's no systematic way to check:
|
||||
- Whether all tasks are truly complete
|
||||
- Whether the implementation covers all spec requirements and scenarios
|
||||
- Whether the implementation follows the design decisions
|
||||
- Whether the code is coherent and makes sense
|
||||
|
||||
A user requested: "Can we get a :verify that will ensure that the implementation matches what was requested?"
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `getVerifyChangeSkillTemplate()` function to `skill-templates.ts`
|
||||
- Add `getOpsxVerifyCommandTemplate()` function to `skill-templates.ts`
|
||||
- Integrate verify skill into `artifactExperimentalSetupCommand` in `artifact-workflow.ts`
|
||||
- Add verify to the skills and commands arrays in the setup command
|
||||
- Update help text to include `/opsx:verify` in the list of available commands
|
||||
- Create `opsx-verify-skill` capability spec
|
||||
|
||||
## Verification Dimensions
|
||||
|
||||
The skill verifies across three dimensions:
|
||||
|
||||
1. **Completeness** - Are all tasks done? Are all specs addressed?
|
||||
2. **Correctness** - Does the implementation match specs? Are scenarios covered?
|
||||
3. **Coherence** - Does the implementation make sense? Does it follow design.md?
|
||||
|
||||
## Output Format
|
||||
|
||||
Produces a prioritized report with:
|
||||
- Summary scorecard (tasks, specs, design adherence)
|
||||
- Critical issues first (must fix before archive)
|
||||
- Warnings second (should fix)
|
||||
- Suggestions third (nice to have)
|
||||
- Actionable fix recommendations for each issue
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New `opsx-verify-skill` spec
|
||||
- Affected code:
|
||||
- `src/core/templates/skill-templates.ts` - Added 2 new template functions
|
||||
- `src/commands/artifact-workflow.ts` - Integrated verify into experimental setup
|
||||
- Generated artifacts: When users run `openspec artifact-experimental-setup`:
|
||||
- Creates `.claude/skills/openspec-verify-change/SKILL.md`
|
||||
- Creates `.claude/commands/opsx/verify.md`
|
||||
- Related skills: Works alongside `/opsx:apply` and before `/opsx:archive`
|
||||
@@ -0,0 +1,190 @@
|
||||
# opsx-verify-skill Specification
|
||||
|
||||
## Purpose
|
||||
Defines the agent skill for verifying that implementation matches change artifacts (specs, tasks, design).
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Verify Skill Invocation
|
||||
The system SHALL provide an `/opsx:verify` skill that validates implementation against change artifacts.
|
||||
|
||||
#### Scenario: Verify with change name provided
|
||||
- **WHEN** agent executes `/opsx:verify <change-name>`
|
||||
- **THEN** the agent verifies implementation for that specific change
|
||||
- **AND** produces a verification report
|
||||
|
||||
#### Scenario: Verify without change name
|
||||
- **WHEN** agent executes `/opsx:verify` without a change name
|
||||
- **THEN** the agent prompts user to select from available changes
|
||||
- **AND** shows only changes that have implementation tasks
|
||||
|
||||
#### Scenario: Change has no tasks
|
||||
- **WHEN** selected change has no tasks.md or tasks are empty
|
||||
- **THEN** the agent reports "No tasks to verify"
|
||||
- **AND** suggests running `/opsx:continue` to create tasks
|
||||
|
||||
### Requirement: Completeness Verification
|
||||
The agent SHALL verify that all required work has been completed.
|
||||
|
||||
#### Scenario: Task completion check
|
||||
- **WHEN** verifying completeness
|
||||
- **THEN** the agent reads tasks.md
|
||||
- **AND** counts tasks marked `- [x]` (complete) vs `- [ ]` (incomplete)
|
||||
- **AND** reports completion status with specific incomplete tasks listed
|
||||
|
||||
#### Scenario: Spec coverage check
|
||||
- **WHEN** verifying completeness
|
||||
- **AND** delta specs exist in `openspec/changes/<name>/specs/`
|
||||
- **THEN** the agent extracts all requirements from delta specs
|
||||
- **AND** searches codebase for implementation of each requirement
|
||||
- **AND** reports which requirements appear to have implementation vs which are missing
|
||||
|
||||
#### Scenario: All tasks complete
|
||||
- **WHEN** all tasks are marked complete
|
||||
- **THEN** report "Tasks: N/N complete"
|
||||
- **AND** mark completeness dimension as passed
|
||||
|
||||
#### Scenario: Incomplete tasks found
|
||||
- **WHEN** some tasks are incomplete
|
||||
- **THEN** report "Tasks: X/N complete"
|
||||
- **AND** list each incomplete task
|
||||
- **AND** mark as CRITICAL issue
|
||||
- **AND** suggest: "Complete remaining tasks or mark as done if already implemented"
|
||||
|
||||
### Requirement: Correctness Verification
|
||||
The agent SHALL verify that implementation matches the specifications.
|
||||
|
||||
#### Scenario: Requirement implementation mapping
|
||||
- **WHEN** verifying correctness
|
||||
- **THEN** for each requirement in delta specs:
|
||||
- Search codebase for implementation
|
||||
- Identify relevant files and line numbers
|
||||
- Assess whether implementation satisfies the requirement
|
||||
|
||||
#### Scenario: Scenario coverage check
|
||||
- **WHEN** verifying correctness
|
||||
- **THEN** for each scenario in delta specs:
|
||||
- Check if the scenario's conditions are handled in code
|
||||
- Check if tests exist that cover the scenario
|
||||
- Report coverage status
|
||||
|
||||
#### Scenario: Implementation matches spec
|
||||
- **WHEN** implementation appears to satisfy a requirement
|
||||
- **THEN** report which files/lines implement it
|
||||
- **AND** mark requirement as covered
|
||||
|
||||
#### Scenario: Implementation diverges from spec
|
||||
- **WHEN** implementation exists but doesn't match spec intent
|
||||
- **THEN** report the divergence as WARNING
|
||||
- **AND** explain what differs
|
||||
- **AND** suggest: either update implementation or update spec to match reality
|
||||
|
||||
#### Scenario: Missing implementation
|
||||
- **WHEN** no implementation found for a requirement
|
||||
- **THEN** report as CRITICAL issue
|
||||
- **AND** suggest: "Implement requirement X" with guidance on what's needed
|
||||
|
||||
### Requirement: Coherence Verification
|
||||
The agent SHALL verify that implementation is sensible and follows design decisions.
|
||||
|
||||
#### Scenario: Design.md adherence check
|
||||
- **WHEN** verifying coherence
|
||||
- **AND** design.md exists for the change
|
||||
- **THEN** extract key decisions from design.md
|
||||
- **AND** verify implementation follows those decisions
|
||||
- **AND** report any deviations
|
||||
|
||||
#### Scenario: No design.md
|
||||
- **WHEN** verifying coherence
|
||||
- **AND** no design.md exists
|
||||
- **THEN** skip design adherence check
|
||||
- **AND** note "No design.md to verify against"
|
||||
|
||||
#### Scenario: Design decision followed
|
||||
- **WHEN** implementation follows a design decision
|
||||
- **THEN** report as confirmed
|
||||
- **AND** cite evidence from code
|
||||
|
||||
#### Scenario: Design decision violated
|
||||
- **WHEN** implementation contradicts a design decision
|
||||
- **THEN** report as WARNING
|
||||
- **AND** explain the contradiction
|
||||
- **AND** suggest: either update implementation or update design.md
|
||||
|
||||
#### Scenario: Code pattern consistency
|
||||
- **WHEN** verifying coherence
|
||||
- **THEN** check if new code follows existing project patterns
|
||||
- **AND** flag any significant deviations as suggestions
|
||||
|
||||
### Requirement: Verification Report Format
|
||||
The agent SHALL produce a structured, prioritized report.
|
||||
|
||||
#### Scenario: Report summary
|
||||
- **WHEN** verification completes
|
||||
- **THEN** display summary scorecard:
|
||||
```
|
||||
## Verification Report: <change-name>
|
||||
|
||||
### Summary
|
||||
| Dimension | Status |
|
||||
|--------------|----------|
|
||||
| Completeness | X/Y |
|
||||
| Correctness | X/Y |
|
||||
| Coherence | Followed |
|
||||
```
|
||||
|
||||
#### Scenario: Issue prioritization
|
||||
- **WHEN** issues are found
|
||||
- **THEN** group and display in priority order:
|
||||
1. CRITICAL - Must fix before archive (missing implementation, incomplete tasks)
|
||||
2. WARNING - Should fix (divergence from spec/design, missing tests)
|
||||
3. SUGGESTION - Nice to fix (pattern inconsistencies, minor improvements)
|
||||
|
||||
#### Scenario: Actionable recommendations
|
||||
- **WHEN** reporting an issue
|
||||
- **THEN** include specific, actionable fix recommendation
|
||||
- **AND** reference relevant files and line numbers where applicable
|
||||
- **AND** avoid vague suggestions like "consider reviewing"
|
||||
|
||||
#### Scenario: All checks pass
|
||||
- **WHEN** no issues found across all dimensions
|
||||
- **THEN** display:
|
||||
```
|
||||
All checks passed. Ready for archive.
|
||||
```
|
||||
|
||||
#### Scenario: Critical issues found
|
||||
- **WHEN** CRITICAL issues exist
|
||||
- **THEN** display:
|
||||
```
|
||||
X critical issue(s) found. Fix before archiving.
|
||||
```
|
||||
- **AND** do NOT suggest running archive
|
||||
|
||||
#### Scenario: Only warnings/suggestions
|
||||
- **WHEN** no CRITICAL issues but warnings exist
|
||||
- **THEN** display:
|
||||
```
|
||||
No critical issues. Y warning(s) to consider.
|
||||
Ready for archive (with noted improvements).
|
||||
```
|
||||
|
||||
### Requirement: Flexible Artifact Handling
|
||||
The agent SHALL gracefully handle changes with varying artifact completeness.
|
||||
|
||||
#### Scenario: Minimal change (tasks only)
|
||||
- **WHEN** change has only tasks.md
|
||||
- **THEN** verify task completion only
|
||||
- **AND** skip spec and design checks
|
||||
- **AND** note which checks were skipped
|
||||
|
||||
#### Scenario: Change with specs but no design
|
||||
- **WHEN** change has tasks.md and delta specs but no design.md
|
||||
- **THEN** verify completeness and correctness
|
||||
- **AND** skip design adherence
|
||||
- **AND** still check code coherence against project patterns
|
||||
|
||||
#### Scenario: Full change (all artifacts)
|
||||
- **WHEN** change has proposal, design, specs, and tasks
|
||||
- **THEN** perform all verification checks
|
||||
- **AND** cross-reference artifacts for consistency
|
||||
@@ -0,0 +1,15 @@
|
||||
# Tasks: Add /opsx:verify Skill
|
||||
|
||||
## 1. Skill Template Functions
|
||||
- [x] 1.1 Add `getVerifyChangeSkillTemplate()` to skill-templates.ts
|
||||
- [x] 1.2 Add `getOpsxVerifyCommandTemplate()` to skill-templates.ts
|
||||
|
||||
## 2. Integration with artifact-experimental-setup
|
||||
- [x] 2.1 Import verify template functions in artifact-workflow.ts
|
||||
- [x] 2.2 Add verify to skills array in artifactExperimentalSetupCommand
|
||||
- [x] 2.3 Add verify to commands array in artifactExperimentalSetupCommand
|
||||
- [x] 2.4 Add verify to help text output
|
||||
|
||||
## 3. Verification (Build & Test)
|
||||
- [x] 3.1 Verify TypeScript compilation succeeds
|
||||
- [x] 3.2 Verify all 8 skills are now included (was 7, now 8)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Change Proposal: Extend Shell Completions
|
||||
|
||||
## Why
|
||||
|
||||
Zsh completions provide an excellent developer experience, but many developers use bash, fish, or PowerShell. Extending completion support to these shells removes friction for the majority of developers who don't use Zsh.
|
||||
|
||||
## What Changes
|
||||
|
||||
This change adds bash, fish, and PowerShell completion support following the same architectural patterns, documentation methodology, and testing rigor established for Zsh completions.
|
||||
|
||||
## Deltas
|
||||
|
||||
- **Spec:** `cli-completion`
|
||||
- **Operation:** MODIFIED
|
||||
- **Description:** Extend completion generation, installation, and testing requirements to support bash, fish, and PowerShell while maintaining the existing Zsh implementation and architectural patterns
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
# cli-completion Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Native Shell Behavior Integration
|
||||
|
||||
The completion system SHALL respect and integrate with each supported shell's native completion patterns and user interaction model.
|
||||
|
||||
#### Scenario: Zsh native completion
|
||||
|
||||
- **WHEN** generating Zsh completion scripts
|
||||
- **THEN** use Zsh completion system with `_arguments`, `_describe`, and `compadd`
|
||||
- **AND** completions SHALL trigger on single TAB (standard Zsh behavior)
|
||||
- **AND** display as an interactive menu that users navigate with TAB/arrow keys
|
||||
- **AND** support Oh My Zsh's enhanced menu styling automatically
|
||||
|
||||
#### Scenario: Bash native completion
|
||||
|
||||
- **WHEN** generating Bash completion scripts
|
||||
- **THEN** use Bash completion with `complete` builtin and `COMPREPLY` array
|
||||
- **AND** completions SHALL trigger on double TAB (standard Bash behavior)
|
||||
- **AND** display as space-separated list or column format
|
||||
- **AND** support both bash-completion v1 and v2 patterns
|
||||
|
||||
#### Scenario: Fish native completion
|
||||
|
||||
- **WHEN** generating Fish completion scripts
|
||||
- **THEN** use Fish's `complete` command with conditions
|
||||
- **AND** completions SHALL trigger on single TAB with auto-suggestion preview
|
||||
- **AND** display with Fish's native coloring and description alignment
|
||||
- **AND** leverage Fish's built-in caching automatically
|
||||
|
||||
#### Scenario: PowerShell native completion
|
||||
|
||||
- **WHEN** generating PowerShell completion scripts
|
||||
- **THEN** use `Register-ArgumentCompleter` with scriptblock
|
||||
- **AND** completions SHALL trigger on TAB with cycling behavior
|
||||
- **AND** display with PowerShell's native completion UI
|
||||
- **AND** support both Windows PowerShell 5.1 and PowerShell Core 7+
|
||||
|
||||
#### Scenario: No custom UX patterns
|
||||
|
||||
- **WHEN** implementing completion for any shell
|
||||
- **THEN** do NOT attempt to customize completion trigger behavior
|
||||
- **AND** do NOT override shell-specific navigation patterns
|
||||
- **AND** ensure completions feel native to experienced users of that shell
|
||||
|
||||
### Requirement: Shell Detection
|
||||
|
||||
The completion system SHALL automatically detect the user's current shell environment.
|
||||
|
||||
#### Scenario: Detecting Zsh from environment
|
||||
|
||||
- **WHEN** no shell is explicitly specified
|
||||
- **THEN** read the `$SHELL` environment variable
|
||||
- **AND** extract the shell name from the path (e.g., `/bin/zsh` → `zsh`)
|
||||
- **AND** validate the shell is one of: `zsh`, `bash`, `fish`, `powershell`
|
||||
- **AND** throw an error if the shell is not supported
|
||||
|
||||
#### Scenario: Detecting Bash from environment
|
||||
|
||||
- **WHEN** `$SHELL` contains `bash` in the path
|
||||
- **THEN** detect shell as `bash`
|
||||
- **AND** proceed with bash-specific completion logic
|
||||
|
||||
#### Scenario: Detecting Fish from environment
|
||||
|
||||
- **WHEN** `$SHELL` contains `fish` in the path
|
||||
- **THEN** detect shell as `fish`
|
||||
- **AND** proceed with fish-specific completion logic
|
||||
|
||||
#### Scenario: Detecting PowerShell from environment
|
||||
|
||||
- **WHEN** `$PSModulePath` environment variable is present
|
||||
- **THEN** detect shell as `powershell`
|
||||
- **AND** proceed with PowerShell-specific completion logic
|
||||
|
||||
#### Scenario: Unsupported shell detection
|
||||
|
||||
- **WHEN** shell path indicates an unsupported shell
|
||||
- **THEN** throw error: "Shell '<name>' is not supported. Supported shells: zsh, bash, fish, powershell"
|
||||
|
||||
### Requirement: Completion Generation
|
||||
|
||||
The completion command SHALL generate completion scripts for all supported shells on demand.
|
||||
|
||||
#### Scenario: Generating Zsh completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate zsh`
|
||||
- **THEN** output a complete Zsh completion script to stdout
|
||||
- **AND** include completions for all commands: init, list, show, validate, archive, view, update, change, spec, completion
|
||||
- **AND** include all command-specific flags and options
|
||||
- **AND** use Zsh's `_arguments` and `_describe` built-in functions
|
||||
- **AND** support dynamic completion for change and spec IDs
|
||||
|
||||
#### Scenario: Generating Bash completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate bash`
|
||||
- **THEN** output a complete Bash completion script to stdout
|
||||
- **AND** include completions for all commands and subcommands
|
||||
- **AND** use `complete -F` with custom completion function
|
||||
- **AND** populate `COMPREPLY` with appropriate suggestions
|
||||
- **AND** support dynamic completion for change and spec IDs via `openspec __complete`
|
||||
|
||||
#### Scenario: Generating Fish completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate fish`
|
||||
- **THEN** output a complete Fish completion script to stdout
|
||||
- **AND** use `complete -c openspec` with conditions
|
||||
- **AND** include command-specific completions with `--condition` predicates
|
||||
- **AND** support dynamic completion for change and spec IDs via `openspec __complete`
|
||||
- **AND** include descriptions for each completion option
|
||||
|
||||
#### Scenario: Generating PowerShell completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate powershell`
|
||||
- **THEN** output a complete PowerShell completion script to stdout
|
||||
- **AND** use `Register-ArgumentCompleter -CommandName openspec`
|
||||
- **AND** implement scriptblock that handles command context
|
||||
- **AND** support dynamic completion for change and spec IDs via `openspec __complete`
|
||||
- **AND** return `[System.Management.Automation.CompletionResult]` objects
|
||||
|
||||
### Requirement: Installation Automation
|
||||
|
||||
The completion command SHALL automatically install completion scripts into shell configuration files for all supported shells.
|
||||
|
||||
#### Scenario: Installing for Oh My Zsh
|
||||
|
||||
- **WHEN** user executes `openspec completion install zsh`
|
||||
- **THEN** detect if Oh My Zsh is installed by checking for `$ZSH` environment variable or `~/.oh-my-zsh/` directory
|
||||
- **AND** create custom completions directory at `~/.oh-my-zsh/custom/completions/` if it doesn't exist
|
||||
- **AND** write completion script to `~/.oh-my-zsh/custom/completions/_openspec`
|
||||
- **AND** ensure `~/.oh-my-zsh/custom/completions` is in `$fpath` by updating `~/.zshrc` if needed
|
||||
- **AND** display success message with instruction to run `exec zsh` or restart terminal
|
||||
|
||||
#### Scenario: Installing for standard Zsh
|
||||
|
||||
- **WHEN** user executes `openspec completion install zsh` and Oh My Zsh is not detected
|
||||
- **THEN** create completions directory at `~/.zsh/completions/` if it doesn't exist
|
||||
- **AND** write completion script to `~/.zsh/completions/_openspec`
|
||||
- **AND** add `fpath=(~/.zsh/completions $fpath)` to `~/.zshrc` if not already present
|
||||
- **AND** add `autoload -Uz compinit && compinit` to `~/.zshrc` if not already present
|
||||
- **AND** display success message with instruction to run `exec zsh` or restart terminal
|
||||
|
||||
#### Scenario: Installing for Bash with bash-completion
|
||||
|
||||
- **WHEN** user executes `openspec completion install bash`
|
||||
- **THEN** detect if bash-completion is installed by checking for `/usr/share/bash-completion` or `/etc/bash_completion.d`
|
||||
- **AND** if bash-completion is available, write to `/etc/bash_completion.d/openspec` (with sudo) or `~/.local/share/bash-completion/completions/openspec`
|
||||
- **AND** if bash-completion is not available, write to `~/.bash_completion.d/openspec` and source it from `~/.bashrc`
|
||||
- **AND** add sourcing line to `~/.bashrc` using marker-based updates if needed
|
||||
- **AND** display success message with instruction to run `exec bash` or restart terminal
|
||||
|
||||
#### Scenario: Installing for Fish
|
||||
|
||||
- **WHEN** user executes `openspec completion install fish`
|
||||
- **THEN** create Fish completions directory at `~/.config/fish/completions/` if it doesn't exist
|
||||
- **AND** write completion script to `~/.config/fish/completions/openspec.fish`
|
||||
- **AND** Fish automatically loads completions from this directory (no config file modification needed)
|
||||
- **AND** display success message indicating completions are immediately available
|
||||
|
||||
#### Scenario: Installing for PowerShell
|
||||
|
||||
- **WHEN** user executes `openspec completion install powershell`
|
||||
- **THEN** detect PowerShell profile location via `$PROFILE` environment variable or default paths
|
||||
- **AND** create profile directory if it doesn't exist
|
||||
- **AND** add completion script import to profile using marker-based updates
|
||||
- **AND** write completion script to PowerShell modules directory or alongside profile
|
||||
- **AND** display success message with instruction to restart PowerShell or run `. $PROFILE`
|
||||
|
||||
#### Scenario: Auto-detecting shell for installation
|
||||
|
||||
- **WHEN** user executes `openspec completion install` without specifying a shell
|
||||
- **THEN** detect current shell using shell detection logic
|
||||
- **AND** install completion for the detected shell (zsh, bash, fish, or powershell)
|
||||
- **AND** display which shell was detected
|
||||
|
||||
#### Scenario: Already installed
|
||||
|
||||
- **WHEN** completion is already installed for the target shell
|
||||
- **THEN** display message indicating completion is already installed
|
||||
- **AND** offer to reinstall/update by overwriting existing files
|
||||
- **AND** exit with code 0
|
||||
|
||||
### Requirement: Uninstallation
|
||||
|
||||
The completion command SHALL remove installed completion scripts and configuration for all supported shells.
|
||||
|
||||
#### Scenario: Uninstalling Zsh completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall zsh`
|
||||
- **THEN** prompt for confirmation before proceeding (unless `--yes` flag provided)
|
||||
- **AND** if user declines, cancel uninstall and display "Uninstall cancelled."
|
||||
- **AND** if user confirms, remove `~/.oh-my-zsh/custom/completions/_openspec` if Oh My Zsh is detected
|
||||
- **AND** remove `~/.zsh/completions/_openspec` if standard Zsh setup is detected
|
||||
- **AND** remove fpath modifications from `~/.zshrc` using marker-based removal
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Uninstalling Bash completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall bash`
|
||||
- **THEN** prompt for confirmation (unless `--yes` flag provided)
|
||||
- **AND** if user confirms, remove completion file from bash-completion directory or `~/.bash_completion.d/`
|
||||
- **AND** remove sourcing lines from `~/.bashrc` using marker-based removal
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Uninstalling Fish completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall fish`
|
||||
- **THEN** prompt for confirmation (unless `--yes` flag provided)
|
||||
- **AND** if user confirms, remove `~/.config/fish/completions/openspec.fish`
|
||||
- **AND** display success message (no config file modification needed)
|
||||
|
||||
#### Scenario: Uninstalling PowerShell completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall powershell`
|
||||
- **THEN** prompt for confirmation (unless `--yes` flag provided)
|
||||
- **AND** if user confirms, remove completion import from PowerShell profile using marker-based removal
|
||||
- **AND** remove completion script file
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Auto-detecting shell for uninstallation
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall` without specifying a shell
|
||||
- **THEN** detect current shell and uninstall completion for that shell
|
||||
|
||||
#### Scenario: Not installed
|
||||
|
||||
- **WHEN** attempting to uninstall completion that isn't installed
|
||||
- **THEN** display error message indicating completion is not installed
|
||||
- **AND** exit with code 1
|
||||
|
||||
### Requirement: Architecture Patterns
|
||||
|
||||
The completion implementation SHALL follow clean architecture principles with TypeScript best practices, supporting multiple shells through a plugin-based pattern.
|
||||
|
||||
#### Scenario: Shell-specific generators
|
||||
|
||||
- **WHEN** implementing completion generators
|
||||
- **THEN** create generator classes for each shell: `ZshGenerator`, `BashGenerator`, `FishGenerator`, `PowerShellGenerator`
|
||||
- **AND** implement a common `CompletionGenerator` interface with method:
|
||||
- `generate(commands: CommandDefinition[]): string` - Returns complete shell script
|
||||
- **AND** each generator handles shell-specific syntax, escaping, and patterns
|
||||
- **AND** all generators consume the same `CommandDefinition[]` from the command registry
|
||||
|
||||
#### Scenario: Shell-specific installers
|
||||
|
||||
- **WHEN** implementing completion installers
|
||||
- **THEN** create installer classes for each shell: `ZshInstaller`, `BashInstaller`, `FishInstaller`, `PowerShellInstaller`
|
||||
- **AND** implement a common `CompletionInstaller` interface with methods:
|
||||
- `install(script: string): Promise<InstallationResult>` - Installs completion script
|
||||
- `uninstall(): Promise<{ success: boolean; message: string }>` - Removes completion
|
||||
- **AND** each installer handles shell-specific paths, config files, and installation patterns
|
||||
|
||||
#### Scenario: Factory pattern for shell selection
|
||||
|
||||
- **WHEN** selecting shell-specific implementation
|
||||
- **THEN** use `CompletionFactory` class with static methods:
|
||||
- `createGenerator(shell: SupportedShell): CompletionGenerator`
|
||||
- `createInstaller(shell: SupportedShell): CompletionInstaller`
|
||||
- **AND** factory uses switch statements with TypeScript exhaustiveness checking
|
||||
- **AND** adding new shell requires updating `SupportedShell` type and factory cases
|
||||
|
||||
#### Scenario: Dynamic completion providers
|
||||
|
||||
- **WHEN** implementing dynamic completions
|
||||
- **THEN** create a `CompletionProvider` class that encapsulates project discovery logic
|
||||
- **AND** implement methods:
|
||||
- `getChangeIds(): Promise<string[]>` - Discovers active change IDs
|
||||
- `getSpecIds(): Promise<string[]>` - Discovers spec IDs
|
||||
- `isOpenSpecProject(): boolean` - Checks if current directory is OpenSpec-enabled
|
||||
- **AND** implement caching with 2-second TTL using class properties
|
||||
|
||||
#### Scenario: Command registry
|
||||
|
||||
- **WHEN** defining completable commands
|
||||
- **THEN** create a centralized `CommandDefinition` type with properties:
|
||||
- `name: string` - Command name
|
||||
- `description: string` - Help text
|
||||
- `flags: FlagDefinition[]` - Available flags
|
||||
- `acceptsPositional: boolean` - Whether command takes positional arguments
|
||||
- `positionalType: string` - Type of positional (change-id, spec-id, path, shell)
|
||||
- `subcommands?: CommandDefinition[]` - Nested subcommands
|
||||
- **AND** export a `COMMAND_REGISTRY` constant with all command definitions
|
||||
- **AND** all generators consume this registry to ensure consistency across shells
|
||||
|
||||
#### Scenario: Type-safe shell detection
|
||||
|
||||
- **WHEN** implementing shell detection
|
||||
- **THEN** define a `SupportedShell` type as literal type: `'zsh' | 'bash' | 'fish' | 'powershell'`
|
||||
- **AND** implement `detectShell()` function in `src/utils/shell-detection.ts`
|
||||
- **AND** return detected shell or throw error with supported shells list
|
||||
|
||||
### Requirement: Testing Support
|
||||
|
||||
The completion implementation SHALL be testable with unit and integration tests for all supported shells.
|
||||
|
||||
#### Scenario: Mock shell environment
|
||||
|
||||
- **WHEN** writing tests for shell detection
|
||||
- **THEN** allow overriding `$SHELL` and `$PSModulePath` environment variables
|
||||
- **AND** use dependency injection for file system operations
|
||||
- **AND** test detection for all four shells independently
|
||||
|
||||
#### Scenario: Generator output verification
|
||||
|
||||
- **WHEN** testing completion generators
|
||||
- **THEN** create test suite for each shell generator (zsh, bash, fish, powershell)
|
||||
- **AND** verify generated scripts contain expected patterns for that shell
|
||||
- **AND** test that command registry is properly consumed
|
||||
- **AND** ensure dynamic completion placeholders are present
|
||||
- **AND** verify shell-specific syntax and escaping
|
||||
|
||||
#### Scenario: Installer simulation
|
||||
|
||||
- **WHEN** testing installation logic
|
||||
- **THEN** create test suite for each shell installer
|
||||
- **AND** use temporary test directories instead of actual home directories
|
||||
- **AND** verify file creation without modifying real shell configurations
|
||||
- **AND** test path resolution logic independently
|
||||
- **AND** mock file system operations to avoid side effects
|
||||
|
||||
#### Scenario: Cross-shell consistency
|
||||
|
||||
- **WHEN** testing completion behavior
|
||||
- **THEN** verify all shells support the same commands and flags
|
||||
- **AND** verify dynamic completions work consistently across shells
|
||||
- **AND** ensure error messages are consistent across shells
|
||||
@@ -0,0 +1,49 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## Phase 1: Foundation and Bash Support
|
||||
|
||||
- [x] Update `SupportedShell` type in `src/utils/shell-detection.ts` to include `'bash' | 'fish' | 'powershell'`
|
||||
- [x] Extend shell detection logic to recognize bash, fish, and PowerShell from environment variables
|
||||
- [x] Create `src/core/completions/generators/bash-generator.ts` implementing `CompletionGenerator` interface
|
||||
- [x] Create `src/core/completions/installers/bash-installer.ts` implementing `CompletionInstaller` interface
|
||||
- [x] Update `CompletionFactory.createGenerator()` to support bash
|
||||
- [x] Update `CompletionFactory.createInstaller()` to support bash
|
||||
- [x] Create test file `test/core/completions/generators/bash-generator.test.ts` mirroring zsh test structure
|
||||
- [x] Create test file `test/core/completions/installers/bash-installer.test.ts` mirroring zsh test structure
|
||||
- [x] Verify bash completions work manually: `openspec completion install bash && exec bash`
|
||||
|
||||
## Phase 2: Fish Support
|
||||
|
||||
- [x] Create `src/core/completions/generators/fish-generator.ts` implementing `CompletionGenerator` interface
|
||||
- [x] Create `src/core/completions/installers/fish-installer.ts` implementing `CompletionInstaller` interface
|
||||
- [x] Update `CompletionFactory.createGenerator()` to support fish
|
||||
- [x] Update `CompletionFactory.createInstaller()` to support fish
|
||||
- [x] Create test file `test/core/completions/generators/fish-generator.test.ts`
|
||||
- [x] Create test file `test/core/completions/installers/fish-installer.test.ts`
|
||||
- [x] Verify fish completions work manually: `openspec completion install fish`
|
||||
|
||||
## Phase 3: PowerShell Support
|
||||
|
||||
- [x] Create `src/core/completions/generators/powershell-generator.ts` implementing `CompletionGenerator` interface
|
||||
- [x] Create `src/core/completions/installers/powershell-installer.ts` implementing `CompletionInstaller` interface
|
||||
- [x] Update `CompletionFactory.createGenerator()` to support powershell
|
||||
- [x] Update `CompletionFactory.createInstaller()` to support powershell
|
||||
- [x] Create test file `test/core/completions/generators/powershell-generator.test.ts`
|
||||
- [x] Create test file `test/core/completions/installers/powershell-installer.test.ts`
|
||||
- [x] Verify PowerShell completions work manually on Windows or macOS PowerShell
|
||||
|
||||
## Phase 4: Documentation and Testing
|
||||
|
||||
- [x] Update `CLAUDE.md` or relevant documentation to mention all four supported shells
|
||||
- [x] Add cross-shell consistency test verifying all shells support same commands
|
||||
- [x] Run `pnpm test` to ensure all tests pass
|
||||
- [x] Run `pnpm run build` to verify TypeScript compilation
|
||||
- [x] Test all shells on different platforms (Linux for bash/fish/zsh, Windows/macOS for PowerShell)
|
||||
|
||||
## Phase 5: Validation and Cleanup
|
||||
|
||||
- [x] Run `openspec validate extend-shell-completions --strict` and resolve all issues
|
||||
- [x] Update error messages to list all four supported shells
|
||||
- [x] Verify `openspec completion --help` documentation is current
|
||||
- [x] Test auto-detection works for all shells
|
||||
- [x] Ensure uninstall works cleanly for all shells
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-07
|
||||
@@ -0,0 +1,94 @@
|
||||
## Context
|
||||
|
||||
OpenSpec is a TypeScript CLI tool using pnpm for dependency management. The project requires Node.js ≥20.19.0. Nix uses its own build system that needs to understand how to fetch dependencies and build the project reproducibly.
|
||||
|
||||
The Nix ecosystem has specific patterns for packaging Node.js/pnpm projects that differ from the traditional npm ecosystem.
|
||||
|
||||
## Goals
|
||||
|
||||
- Enable OpenSpec to be run directly via `nix run github:Fission-AI/OpenSpec`
|
||||
- Support all major platforms (Linux x86/ARM, macOS x86/ARM)
|
||||
- Use existing pnpm-lock.yaml for reproducible builds
|
||||
- Provide development environment for Nix users
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Replace existing npm/pnpm publishing workflow
|
||||
- Publish to nixpkgs (can be done later as separate effort)
|
||||
- Support Windows (Nix doesn't run natively on Windows)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Use stdenv.mkDerivation instead of buildNpmPackage
|
||||
|
||||
**Decision**: Package OpenSpec using `stdenv.mkDerivation` with pnpm hooks.
|
||||
|
||||
**Rationale**: The zigbee2mqtt package in nixpkgs demonstrates the current best practice for pnpm projects. Using `buildNpmPackage` with pnpm requires complex configuration, while `mkDerivation` with the right hooks is more straightforward and better supported.
|
||||
|
||||
**Alternative considered**: Using `buildNpmPackage` with `npmConfigHook = pkgs.pnpmConfigHook` - this is the older pattern and causes issues with dependency fetching.
|
||||
|
||||
### Use fetchPnpmDeps with explicit pnpm version
|
||||
|
||||
**Decision**: Use `pkgs.fetchPnpmDeps` with `pnpm = pkgs.pnpm_9` and `fetcherVersion = 3`.
|
||||
|
||||
**Rationale**:
|
||||
- pnpm lockfile version 9.0 requires fetcherVersion 3
|
||||
- Explicit pnpm_9 ensures consistency between fetch and build
|
||||
- This is the documented way to handle pnpm projects in nixpkgs
|
||||
|
||||
### Multi-platform support without flake-utils
|
||||
|
||||
**Decision**: Implement multi-platform support using plain Nix with `nixpkgs.lib.genAttrs`.
|
||||
|
||||
**Rationale**: Per user request, avoid extra dependencies. The `genAttrs` pattern is simple and well-understood in the Nix community.
|
||||
|
||||
### Node.js 20 instead of latest
|
||||
|
||||
**Decision**: Pin to nodejs_20 to match package.json engines requirement.
|
||||
|
||||
**Rationale**: Ensures consistency with development environment and npm package requirements. Avoids potential compatibility issues with newer Node versions.
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Dependency Hash Management
|
||||
|
||||
The `pnpmDeps.hash` field must be updated whenever dependencies change. The workflow:
|
||||
1. Set hash to fake value (all zeros)
|
||||
2. Run `nix build`
|
||||
3. Nix fails with actual hash
|
||||
4. Update flake.nix with correct hash
|
||||
|
||||
This is standard Nix workflow for fixed-output derivations.
|
||||
|
||||
### Build Inputs
|
||||
|
||||
Required nativeBuildInputs:
|
||||
- `nodejs_20` - runtime
|
||||
- `npmHooks.npmInstallHook` - handles installation phase
|
||||
- `pnpmConfigHook` - configures pnpm environment
|
||||
- `pnpm_9` - pnpm executable
|
||||
|
||||
The `dontNpmPrune = true` is important to keep all dependencies after build.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk]** Hash needs updating when dependencies change → **Mitigation**: Document this clearly; error message from Nix provides correct hash
|
||||
|
||||
**[Risk]** Nix builds might lag behind npm releases → **Mitigation**: This is fine; Nix users can still use npm if they need bleeding edge
|
||||
|
||||
**[Trade-off]** Additional maintenance burden for hash updates → **Benefit**: Better experience for Nix ecosystem users
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Add flake.nix to repository
|
||||
2. Test builds on multiple platforms (can use GitHub Actions with Nix)
|
||||
3. Update README with Nix installation instructions
|
||||
4. Optionally add to CI pipeline to catch hash mismatches early
|
||||
|
||||
No breaking changes - this is purely additive.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should we add automatic hash updating to CI? (Could use nix-update-script)
|
||||
- Should we submit to nixpkgs after validation? (Separate decision)
|
||||
- Do we want to support older Node versions in flake? (Probably no - stick to package.json requirement)
|
||||
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
|
||||
OpenSpec users on NixOS or using the Nix package manager cannot easily install or run OpenSpec without going through npm. Adding a Nix flake makes OpenSpec a first-class citizen in the Nix ecosystem, enabling users to run `nix run github:Fission-AI/OpenSpec -- init` or include OpenSpec in their development environments declaratively.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `flake.nix` to repository root with multi-platform support (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin)
|
||||
- Package uses pnpm for dependency management (matching existing development workflow)
|
||||
- Support both direct execution via `nix run` and installation via `nix profile install`
|
||||
- Provide dev shell for contributors using Nix
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `nix-flake-support`: Nix flake configuration for building and running OpenSpec
|
||||
|
||||
### Modified Capabilities
|
||||
- None
|
||||
|
||||
## Impact
|
||||
|
||||
- **New files**: `flake.nix` in repository root
|
||||
- **Documentation**: Should add installation instructions for Nix users
|
||||
- **CI/CD**: Could add flake checking to CI pipeline (optional)
|
||||
- **Maintenance**: Requires updating pnpmDeps hash when dependencies change
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Multi-platform Nix flake
|
||||
The system SHALL provide a Nix flake that builds OpenSpec for multiple platforms.
|
||||
|
||||
#### Scenario: Build on Linux x86_64
|
||||
- **WHEN** user runs `nix build` on x86_64-linux system
|
||||
- **THEN** system builds OpenSpec package successfully
|
||||
- **AND** package includes the `openspec` binary
|
||||
|
||||
#### Scenario: Build on macOS ARM
|
||||
- **WHEN** user runs `nix build` on aarch64-darwin system
|
||||
- **THEN** system builds OpenSpec package successfully
|
||||
- **AND** package includes the `openspec` binary
|
||||
|
||||
#### Scenario: Build on Linux ARM
|
||||
- **WHEN** user runs `nix build` on aarch64-linux system
|
||||
- **THEN** system builds OpenSpec package successfully
|
||||
|
||||
#### Scenario: Build on macOS x86_64
|
||||
- **WHEN** user runs `nix build` on x86_64-darwin system
|
||||
- **THEN** system builds OpenSpec package successfully
|
||||
|
||||
### Requirement: Direct execution via nix run
|
||||
The system SHALL allow users to run OpenSpec directly from GitHub without installing.
|
||||
|
||||
#### Scenario: Run init command from GitHub
|
||||
- **WHEN** user runs `nix run github:Fission-AI/OpenSpec -- init`
|
||||
- **THEN** system downloads and builds OpenSpec
|
||||
- **AND** executes `openspec init` command
|
||||
|
||||
#### Scenario: Run any OpenSpec command
|
||||
- **WHEN** user runs `nix run github:Fission-AI/OpenSpec -- <command> <args>`
|
||||
- **THEN** system executes `openspec <command> <args>`
|
||||
|
||||
### Requirement: pnpm dependency management
|
||||
The system SHALL use pnpm for building OpenSpec in the Nix flake.
|
||||
|
||||
#### Scenario: Fetch dependencies with pnpm
|
||||
- **WHEN** Nix builds the package
|
||||
- **THEN** system uses `fetchPnpmDeps` to download dependencies
|
||||
- **AND** uses pnpm-lock.yaml for reproducible builds
|
||||
- **AND** uses fetcherVersion 3 for lockfile version 9.0
|
||||
|
||||
#### Scenario: Build with pnpm
|
||||
- **WHEN** Nix runs the build phase
|
||||
- **THEN** system executes `pnpm run build`
|
||||
- **AND** produces dist directory with compiled TypeScript
|
||||
|
||||
### Requirement: Node.js version compatibility
|
||||
The system SHALL use Node.js 20 as specified in package.json engines field.
|
||||
|
||||
#### Scenario: Build with correct Node version
|
||||
- **WHEN** Nix builds OpenSpec
|
||||
- **THEN** system uses nodejs_20 from nixpkgs
|
||||
- **AND** build succeeds without version compatibility errors
|
||||
|
||||
### Requirement: Development shell
|
||||
The system SHALL provide a Nix development shell for contributors.
|
||||
|
||||
#### Scenario: Enter dev shell
|
||||
- **WHEN** user runs `nix develop` in OpenSpec repository
|
||||
- **THEN** system provides shell with nodejs_20 and pnpm_9
|
||||
- **AND** displays welcome message with versions
|
||||
- **AND** provides instructions to run `pnpm install`
|
||||
|
||||
### Requirement: Proper binary installation
|
||||
The system SHALL install the openspec binary correctly.
|
||||
|
||||
#### Scenario: Binary in PATH
|
||||
- **WHEN** package is built or installed
|
||||
- **THEN** `openspec` binary is available in `$out/bin/openspec`
|
||||
- **AND** binary is executable
|
||||
- **AND** binary can be invoked without full path when installed
|
||||
|
||||
#### Scenario: Binary executes correctly
|
||||
- **WHEN** user runs the installed `openspec` command
|
||||
- **THEN** system executes the CLI entry point
|
||||
- **AND** all subcommands work correctly
|
||||
@@ -0,0 +1,65 @@
|
||||
## 1. Create Flake Structure
|
||||
|
||||
- [x] 1.1 Create flake.nix in repository root
|
||||
- [x] 1.2 Define inputs (nixpkgs only, no flake-utils)
|
||||
- [x] 1.3 Set up supportedSystems list (4 platforms)
|
||||
- [x] 1.4 Create forAllSystems helper function
|
||||
|
||||
## 2. Configure Package Build
|
||||
|
||||
- [x] 2.1 Set up stdenv.mkDerivation with finalAttrs pattern
|
||||
- [x] 2.2 Configure pnpmDeps with fetchPnpmDeps
|
||||
- [x] 2.3 Set pnpm = pnpm_9 and fetcherVersion = 3
|
||||
- [x] 2.4 Add placeholder hash (all zeros)
|
||||
- [x] 2.5 Configure nativeBuildInputs (nodejs_20, hooks, pnpm_9)
|
||||
- [x] 2.6 Set dontNpmPrune = true
|
||||
|
||||
## 3. Define Build Phase
|
||||
|
||||
- [x] 3.1 Add buildPhase with runHook preBuild
|
||||
- [x] 3.2 Add pnpm run build command
|
||||
- [x] 3.3 Add runHook postBuild
|
||||
|
||||
## 4. Configure Installation
|
||||
|
||||
- [x] 4.1 Let npmInstallHook handle installation automatically
|
||||
- [x] 4.2 Verify binary ends up in $out/bin/openspec
|
||||
|
||||
## 5. Add Metadata
|
||||
|
||||
- [x] 5.1 Set meta.description
|
||||
- [x] 5.2 Set meta.homepage
|
||||
- [x] 5.3 Set meta.license (MIT)
|
||||
- [x] 5.4 Set meta.mainProgram = "openspec"
|
||||
|
||||
## 6. Configure App Entry Point
|
||||
|
||||
- [x] 6.1 Add apps output with forAllSystems
|
||||
- [x] 6.2 Set default app to openspec binary
|
||||
- [x] 6.3 Test that nix run works
|
||||
|
||||
## 7. Add Development Shell
|
||||
|
||||
- [x] 7.1 Add devShells output with forAllSystems
|
||||
- [x] 7.2 Include nodejs_20 and pnpm_9 in buildInputs
|
||||
- [x] 7.3 Add shellHook with welcome message and instructions
|
||||
|
||||
## 8. Get Correct Dependency Hash
|
||||
|
||||
- [x] 8.1 Run nix build to trigger hash mismatch
|
||||
- [x] 8.2 Copy correct hash from error message
|
||||
- [x] 8.3 Update pnpmDeps.hash in flake.nix
|
||||
- [x] 8.4 Verify build succeeds
|
||||
|
||||
## 9. Testing
|
||||
|
||||
- [x] 9.1 Test `nix build` on x86_64-linux
|
||||
- [x] 9.2 Test `nix run . -- --version` works
|
||||
- [x] 9.3 Test `nix develop` provides correct environment
|
||||
- [ ] 9.4 Test on macOS if available
|
||||
- [ ] 9.5 Test `nix run github:Fission-AI/OpenSpec -- init` after merge to main
|
||||
|
||||
## 10. Documentation
|
||||
|
||||
- [x] 10.1 Add Nix installation section to README
|
||||
- [x] 10.2 Include example commands for common Nix workflows in README
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-09
|
||||
@@ -0,0 +1,117 @@
|
||||
## Context
|
||||
|
||||
The Nix flake added in the previous change requires manual maintenance when:
|
||||
1. Package version changes (must update flake.nix version field)
|
||||
2. Dependencies change (must update pnpmDeps hash)
|
||||
|
||||
Currently this requires maintainers to:
|
||||
- Manually edit flake.nix version
|
||||
- Set placeholder hash
|
||||
- Run nix build to get error
|
||||
- Copy hash from error message
|
||||
- Update flake.nix again
|
||||
- Verify build works
|
||||
|
||||
This is tedious and error-prone, especially for maintainers unfamiliar with Nix.
|
||||
|
||||
## Goals
|
||||
|
||||
- Automate version and hash updates for flake.nix
|
||||
- Make script idempotent and safe to run multiple times
|
||||
- Provide clear feedback during execution
|
||||
- Integrate easily into release workflow
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Automatically commit changes (maintainer decides when to commit)
|
||||
- Support non-pnpm package managers
|
||||
- Handle complex Nix configurations beyond OpenSpec's use case
|
||||
|
||||
## Decisions
|
||||
|
||||
### Use Bash instead of Node.js script
|
||||
|
||||
**Decision**: Implement as bash script rather than Node.js.
|
||||
|
||||
**Rationale**:
|
||||
- Needs to call Nix commands which are bash-native
|
||||
- Parsing Nix output is simpler in bash with grep/sed
|
||||
- Maintainers updating flake.nix likely have Nix installed (bash environment)
|
||||
- Node.js would add unnecessary complexity for shell operations
|
||||
|
||||
**Alternative considered**: Node.js script with child_process - adds dependency on extra npm packages for shell operations, less natural for Nix tooling.
|
||||
|
||||
### Extract hash from build error output
|
||||
|
||||
**Decision**: Trigger intentional build failure with placeholder hash to get correct hash.
|
||||
|
||||
**Rationale**: This is the standard Nix workflow for updating fixed-output derivations. No API exists to compute the hash without building.
|
||||
|
||||
**Alternative considered**: Pre-compute hash from pnpm-lock.yaml - would require understanding Nix's hash algorithm and pnpm's lockfile structure, fragile and non-standard.
|
||||
|
||||
### Use sed for in-place file editing
|
||||
|
||||
**Decision**: Use `sed -i` for updating flake.nix in place.
|
||||
|
||||
**Rationale**: Simple, available on all Unix-like systems, handles the specific replacement patterns needed.
|
||||
|
||||
**Alternative considered**:
|
||||
- Using Node.js to parse/modify: Overkill for simple string replacement
|
||||
- Manual `sed` without `-i`: Requires temp files, more complex
|
||||
|
||||
### Verify build after hash update
|
||||
|
||||
**Decision**: Always run verification build after updating hash.
|
||||
|
||||
**Rationale**: Catches errors immediately, gives maintainer confidence the update worked.
|
||||
|
||||
**Trade-off**: Takes extra time (~30s) but prevents broken flake.nix commits.
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Path Resolution
|
||||
|
||||
Script calculates paths relative to its own location:
|
||||
```bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
```
|
||||
|
||||
This allows running from any working directory.
|
||||
|
||||
### Error Handling
|
||||
|
||||
Uses `set -euo pipefail` for strict error handling:
|
||||
- `-e`: Exit on any command failure
|
||||
- `-u`: Exit on undefined variable access
|
||||
- `-o pipefail`: Catch failures in pipes
|
||||
|
||||
### Hash Extraction Pattern
|
||||
|
||||
Uses grep with Perl regex to extract hash:
|
||||
```bash
|
||||
grep -oP 'got:\s+\Ksha256-[A-Za-z0-9+/=]+'
|
||||
```
|
||||
|
||||
This reliably extracts the hash regardless of surrounding text.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk]** Script assumes standard Nix error message format → **Mitigation**: If extraction fails, script exits with error and shows full output
|
||||
|
||||
**[Risk]** Build might fail for reasons other than hash mismatch → **Mitigation**: Script checks for hash in output before proceeding
|
||||
|
||||
**[Trade-off]** Requires Nix installed to run → **Benefit**: Only maintainers updating flake need to run this, and they have Nix
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Add script to scripts directory
|
||||
2. Document in scripts/README.md
|
||||
3. Use in next version bump to verify workflow
|
||||
4. Update CONTRIBUTING.md if needed to mention script
|
||||
|
||||
No breaking changes - purely additive tooling.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None - straightforward automation script.
|
||||
@@ -0,0 +1,23 @@
|
||||
## Why
|
||||
|
||||
Maintaining the Nix flake requires manual updates to version and dependency hash when releasing new versions or updating dependencies. This is error-prone and requires maintainers to understand Nix internals. Automating this process ensures consistency and reduces friction for releases.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `scripts/update-flake.sh` to automatically update flake.nix version and dependency hash
|
||||
- Add `scripts/README.md` documenting all maintenance scripts
|
||||
- Script extracts version from package.json and determines correct pnpm dependency hash automatically
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `flake-update-script`: Automation script for maintaining flake.nix
|
||||
|
||||
### Modified Capabilities
|
||||
- None
|
||||
|
||||
## Impact
|
||||
|
||||
- **New files**: `scripts/update-flake.sh`, `scripts/README.md`
|
||||
- **Maintainer workflow**: Version bumps now include running `./scripts/update-flake.sh`
|
||||
- **Dependencies**: Script requires Node.js (already a dependency) and Nix (for maintainers using Nix)
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Automatic Version Update
|
||||
The script SHALL automatically update the version in flake.nix to match package.json.
|
||||
|
||||
#### Scenario: Version extraction from package.json
|
||||
- **WHEN** script runs
|
||||
- **THEN** version is read from package.json using Node.js
|
||||
- **AND** version field in flake.nix is updated to match
|
||||
|
||||
#### Scenario: Version already up-to-date
|
||||
- **WHEN** script runs and flake.nix version already matches package.json
|
||||
- **THEN** script reports version is up-to-date
|
||||
- **AND** continues to hash update
|
||||
|
||||
### Requirement: Automatic Hash Determination
|
||||
The script SHALL automatically determine and update the correct pnpm dependency hash.
|
||||
|
||||
#### Scenario: Trigger build to get hash
|
||||
- **WHEN** script needs to determine correct hash
|
||||
- **THEN** script sets placeholder hash in flake.nix
|
||||
- **AND** runs nix build which fails with correct hash
|
||||
- **AND** extracts correct hash from build error output
|
||||
|
||||
#### Scenario: Hash extraction from build output
|
||||
- **WHEN** nix build fails with hash mismatch
|
||||
- **THEN** script parses "got: sha256-..." from error output
|
||||
- **AND** updates flake.nix with correct hash
|
||||
|
||||
#### Scenario: Hash update failure
|
||||
- **WHEN** script cannot extract hash from build output
|
||||
- **THEN** script exits with error
|
||||
- **AND** displays build output for debugging
|
||||
|
||||
### Requirement: Build Verification
|
||||
The script SHALL verify that flake.nix builds successfully after updates.
|
||||
|
||||
#### Scenario: Successful verification
|
||||
- **WHEN** hash has been updated
|
||||
- **THEN** script runs nix build to verify
|
||||
- **AND** reports success if build completes
|
||||
|
||||
#### Scenario: Dirty git tree warning
|
||||
- **WHEN** build succeeds but git tree is dirty
|
||||
- **THEN** script reports warning about dirty tree
|
||||
- **AND** still indicates build success
|
||||
|
||||
### Requirement: User Feedback
|
||||
The script SHALL provide clear progress information and next steps.
|
||||
|
||||
#### Scenario: Progress reporting
|
||||
- **WHEN** script runs
|
||||
- **THEN** each step is reported with descriptive message
|
||||
- **AND** detected version and hash are displayed
|
||||
|
||||
#### Scenario: Success summary
|
||||
- **WHEN** script completes successfully
|
||||
- **THEN** summary shows updated version and hash
|
||||
- **AND** next steps are displayed (test, commit, etc.)
|
||||
|
||||
### Requirement: Script Safety
|
||||
The script SHALL fail fast on errors and use safe defaults.
|
||||
|
||||
#### Scenario: Bash error handling
|
||||
- **WHEN** script encounters an error
|
||||
- **THEN** script exits immediately (set -e)
|
||||
- **AND** undefined variables cause exit (set -u)
|
||||
- **AND** pipe failures are caught (set -o pipefail)
|
||||
|
||||
#### Scenario: File path resolution
|
||||
- **WHEN** script determines file locations
|
||||
- **THEN** paths are calculated relative to script location
|
||||
- **AND** script works regardless of working directory
|
||||
|
||||
### Requirement: Documentation
|
||||
The system SHALL provide documentation for the update script.
|
||||
|
||||
#### Scenario: Script usage documentation
|
||||
- **WHEN** maintainer needs to use update script
|
||||
- **THEN** scripts/README.md explains when and how to use it
|
||||
- **AND** example workflow is provided
|
||||
|
||||
#### Scenario: Script listing
|
||||
- **WHEN** maintainer views scripts/README.md
|
||||
- **THEN** all maintenance scripts are documented
|
||||
- **AND** purpose of each script is clear
|
||||
@@ -0,0 +1,55 @@
|
||||
## 1. Create Update Script
|
||||
|
||||
- [x] 1.1 Create scripts/update-flake.sh file
|
||||
- [x] 1.2 Add shebang and error handling (set -euo pipefail)
|
||||
- [x] 1.3 Add path resolution for project root and files
|
||||
- [x] 1.4 Make script executable (chmod +x)
|
||||
|
||||
## 2. Implement Version Update Logic
|
||||
|
||||
- [x] 2.1 Extract version from package.json using Node.js
|
||||
- [x] 2.2 Use sed to update version in flake.nix
|
||||
- [x] 2.3 Report if version already up-to-date
|
||||
- [x] 2.4 Display detected version to user
|
||||
|
||||
## 3. Implement Hash Update Logic
|
||||
|
||||
- [x] 3.1 Set placeholder hash in flake.nix
|
||||
- [x] 3.2 Run nix build and capture output (allow failure)
|
||||
- [x] 3.3 Extract correct hash from build error using grep
|
||||
- [x] 3.4 Handle case where hash extraction fails
|
||||
- [x] 3.5 Update flake.nix with correct hash
|
||||
- [x] 3.6 Display detected hash to user
|
||||
|
||||
## 4. Add Build Verification
|
||||
|
||||
- [x] 4.1 Run nix build after hash update
|
||||
- [x] 4.2 Check for dirty git tree warning
|
||||
- [x] 4.3 Report success or failure clearly
|
||||
|
||||
## 5. Add User Feedback
|
||||
|
||||
- [x] 5.1 Add progress messages for each step
|
||||
- [x] 5.2 Add success summary with version and hash
|
||||
- [x] 5.3 Add next steps instructions (test, commit)
|
||||
- [x] 5.4 Add error messages with context
|
||||
|
||||
## 6. Create Documentation
|
||||
|
||||
- [x] 6.1 Create scripts/README.md
|
||||
- [x] 6.2 Document update-flake.sh purpose and usage
|
||||
- [x] 6.3 Add example workflow
|
||||
- [x] 6.4 Document other existing scripts
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- [x] 7.1 Test script runs successfully
|
||||
- [x] 7.2 Verify version is extracted correctly
|
||||
- [x] 7.3 Verify hash is updated correctly
|
||||
- [x] 7.4 Verify build succeeds after update
|
||||
- [x] 7.5 Test idempotency (running twice works)
|
||||
|
||||
## 8. Integration
|
||||
|
||||
- [ ] 8.1 Add note to release process documentation
|
||||
- [ ] 8.2 Use in next actual version bump to validate workflow
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-10
|
||||
@@ -0,0 +1,175 @@
|
||||
## Context
|
||||
|
||||
OpenSpec needs usage analytics to understand adoption and inform product decisions. PostHog provides a privacy-conscious analytics platform suitable for open source projects.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Track daily/weekly/monthly active usage
|
||||
- Understand command usage patterns
|
||||
- Keep implementation minimal and privacy-respecting
|
||||
- Enable opt-out with minimal friction
|
||||
|
||||
**Non-Goals:**
|
||||
- Detailed error tracking or diagnostics
|
||||
- User identification or profiling
|
||||
- Complex event hierarchies
|
||||
- Full CLI command for telemetry management (env var sufficient for now)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Opt-Out Model
|
||||
|
||||
**Decision:** Telemetry enabled by default, opt-out via environment variable.
|
||||
|
||||
```bash
|
||||
OPENSPEC_TELEMETRY=0 # Disable telemetry
|
||||
DO_NOT_TRACK=1 # Industry standard, also respected
|
||||
```
|
||||
|
||||
Auto-disabled when `CI=true` is detected.
|
||||
|
||||
**Rationale:**
|
||||
- Opt-in typically yields ~3% participation—not enough for meaningful data
|
||||
- Understanding usage patterns requires statistically significant sample sizes
|
||||
- Environment variable opt-out is simple and immediate
|
||||
- Respecting `DO_NOT_TRACK` follows industry convention
|
||||
|
||||
**Alternatives considered:**
|
||||
- Opt-in only - Insufficient data for product decisions
|
||||
- Config file setting - More complex, env var sufficient for MVP
|
||||
- Full `openspec telemetry` command - Can add later if users request
|
||||
|
||||
### Event Design
|
||||
|
||||
**Decision:** Single event type with minimal properties.
|
||||
|
||||
```typescript
|
||||
{
|
||||
event: 'command_executed',
|
||||
properties: {
|
||||
command: 'init', // Command name only
|
||||
version: '1.2.3' // OpenSpec version
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Answers the core questions: how much usage, which commands are popular
|
||||
- PostHog derives DAU/WAU/MAU from anonymous user counts over time
|
||||
- No arguments, paths, or content—clean privacy story
|
||||
- Easy to explain in disclosure notice
|
||||
|
||||
**Not tracked:**
|
||||
- Command arguments
|
||||
- File paths or contents
|
||||
- Error messages or stack traces
|
||||
- Project names or spec content
|
||||
- IP addresses (`$ip: null` explicitly set)
|
||||
|
||||
### Anonymous ID
|
||||
|
||||
**Decision:** Random UUID, lazily generated on first telemetry send, stored in global config.
|
||||
|
||||
```typescript
|
||||
// ~/.config/openspec/config.json
|
||||
{
|
||||
"telemetry": {
|
||||
"anonymousId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Random UUID has no relation to the person—can't be reversed
|
||||
- Stored in config so same user = same ID across sessions (needed for DAU/WAU/MAU)
|
||||
- Lazy generation means no ID created if user opts out before first command
|
||||
- User can delete config to reset identity
|
||||
|
||||
**Alternatives considered:**
|
||||
- Machine-derived hash (hostname, MAC) - Feels invasive, fingerprint-like
|
||||
- Per-session UUID - Breaks user counting metrics entirely
|
||||
|
||||
### SDK Configuration
|
||||
|
||||
**Decision:** PostHog Node SDK with immediate flush, shutdown on exit.
|
||||
|
||||
```typescript
|
||||
const posthog = new PostHog(API_KEY, {
|
||||
flushAt: 1, // Send immediately, don't batch
|
||||
flushInterval: 0 // No timer-based flushing
|
||||
});
|
||||
|
||||
// Before CLI exits
|
||||
await posthog.shutdown();
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- CLI processes are short-lived; batching would lose events
|
||||
- `flushAt: 1` ensures each event sends immediately
|
||||
- `shutdown()` guarantees flush before process exit
|
||||
- Adds ~100-300ms to exit—negligible for typical CLI workflows
|
||||
|
||||
**Error handling:**
|
||||
- Network failures silently ignored (telemetry shouldn't break CLI)
|
||||
- `shutdown()` wrapped in try/catch
|
||||
|
||||
### Hook Location
|
||||
|
||||
**Decision:** Commander.js `preAction` and `postAction` hooks.
|
||||
|
||||
```typescript
|
||||
program
|
||||
.hook('preAction', (thisCommand) => {
|
||||
maybeShowTelemetryNotice();
|
||||
trackCommand(thisCommand.name(), VERSION);
|
||||
})
|
||||
.hook('postAction', async () => {
|
||||
await shutdown();
|
||||
});
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Centralized—one place for all telemetry logic
|
||||
- Automatic—new commands get tracked without code changes
|
||||
- Clean separation—command handlers don't know about telemetry
|
||||
|
||||
**Subcommand handling:**
|
||||
- Track full command path for nested commands (e.g., `change:apply`)
|
||||
|
||||
### First-Run Notice
|
||||
|
||||
**Decision:** One-liner on first command ever, stored "seen" flag in config.
|
||||
|
||||
```
|
||||
Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- First command (not just `init`) ensures notice is always seen
|
||||
- Non-blocking—no prompt, just informational
|
||||
- One-liner is visible but not intrusive
|
||||
- Storing "seen" in config prevents repeated display
|
||||
|
||||
**Config after first run:**
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"anonymousId": "...",
|
||||
"noticeSeen": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Users prefer opt-in | Clear disclosure, trivial opt-out, transparent about what's collected |
|
||||
| GDPR concerns | No personal data, no IP, user can delete config |
|
||||
| Slows CLI exit by ~200ms | Negligible for most workflows; can optimize if needed |
|
||||
| PostHog outage affects CLI | Fire-and-forget with timeout; failures are silent |
|
||||
|
||||
## Open Questions
|
||||
|
||||
None—design is intentionally minimal. Future enhancements (dedicated command, workflow tracking) can be added based on user feedback.
|
||||
@@ -0,0 +1,37 @@
|
||||
## Why
|
||||
|
||||
OpenSpec currently has no visibility into how the tool is being used. Without analytics, we cannot:
|
||||
- Understand which commands and features are most valuable to users
|
||||
- Measure adoption and usage patterns
|
||||
- Make data-driven decisions about product development
|
||||
|
||||
Adding PostHog analytics enables product insights while respecting user privacy through transparent, opt-out telemetry.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add PostHog Node.js SDK as a dependency
|
||||
- Implement telemetry system with environment variable opt-out
|
||||
- Track command usage (command name and version only)
|
||||
- Show first-run notice informing users about telemetry
|
||||
- Store anonymous ID in global config (`~/.config/openspec/config.json`)
|
||||
- Respect `DO_NOT_TRACK` and `OPENSPEC_TELEMETRY=0` environment variables
|
||||
- Auto-disable in CI environments
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `telemetry`: Anonymous usage analytics using PostHog. Covers command tracking, opt-out controls, and first-run disclosure notice.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `global-config`: Add telemetry state storage (anonymous ID, notice seen flag)
|
||||
|
||||
## Impact
|
||||
|
||||
- **Dependencies**: Add `posthog-node` package
|
||||
- **Privacy**: Opt-out via env var, no personal data collected, clear disclosure
|
||||
- **Configuration**: New global config fields for telemetry state
|
||||
- **Network**: Async event sending with flush on exit (~100-300ms added)
|
||||
- **CI/CD**: Telemetry auto-disabled when `CI=true`
|
||||
- **Documentation**: Update README with telemetry disclosure
|
||||
@@ -0,0 +1,21 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Global configuration storage
|
||||
The system SHALL store global configuration in `~/.config/openspec/config.json`, including telemetry state with `anonymousId` and `noticeSeen` fields.
|
||||
|
||||
#### Scenario: Initial config creation
|
||||
- **WHEN** no global config file exists
|
||||
- **AND** the first telemetry event is about to be sent
|
||||
- **THEN** the system creates `~/.config/openspec/config.json` with telemetry configuration
|
||||
|
||||
#### Scenario: Telemetry config structure
|
||||
- **WHEN** reading or writing telemetry configuration
|
||||
- **THEN** the config contains a `telemetry` object with `anonymousId` (string UUID) and `noticeSeen` (boolean) fields
|
||||
|
||||
#### Scenario: Config file format
|
||||
- **WHEN** storing configuration
|
||||
- **THEN** the system writes valid JSON that can be read and modified by users
|
||||
|
||||
#### Scenario: Existing config preservation
|
||||
- **WHEN** adding telemetry fields to an existing config file
|
||||
- **THEN** the system preserves all existing configuration fields
|
||||
@@ -0,0 +1,116 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Command execution tracking
|
||||
The system SHALL send a `command_executed` event to PostHog when any CLI command executes, including only the command name and OpenSpec version as properties.
|
||||
|
||||
#### Scenario: Standard command execution
|
||||
- **WHEN** a user runs any openspec command
|
||||
- **THEN** the system sends a `command_executed` event with `command` and `version` properties
|
||||
|
||||
#### Scenario: Subcommand execution
|
||||
- **WHEN** a user runs a nested command like `openspec change apply`
|
||||
- **THEN** the system sends a `command_executed` event with the full command path (e.g., `change:apply`)
|
||||
|
||||
### Requirement: Privacy-preserving event design
|
||||
The system SHALL NOT include command arguments, file paths, project names, spec content, error messages, or IP addresses in telemetry events.
|
||||
|
||||
#### Scenario: Command with arguments
|
||||
- **WHEN** a user runs `openspec init my-project --force`
|
||||
- **THEN** the telemetry event contains only `command: "init"` and `version: "<version>"` without arguments
|
||||
|
||||
#### Scenario: IP address exclusion
|
||||
- **WHEN** the system sends a telemetry event
|
||||
- **THEN** the event explicitly sets `$ip: null` to prevent IP tracking
|
||||
|
||||
### Requirement: Environment variable opt-out
|
||||
The system SHALL disable telemetry when `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` environment variables are set.
|
||||
|
||||
#### Scenario: OPENSPEC_TELEMETRY opt-out
|
||||
- **WHEN** `OPENSPEC_TELEMETRY=0` is set in the environment
|
||||
- **THEN** the system sends no telemetry events
|
||||
|
||||
#### Scenario: DO_NOT_TRACK opt-out
|
||||
- **WHEN** `DO_NOT_TRACK=1` is set in the environment
|
||||
- **THEN** the system sends no telemetry events
|
||||
|
||||
#### Scenario: Environment variable takes precedence
|
||||
- **WHEN** the user has previously used the CLI (config exists)
|
||||
- **AND** the user sets `OPENSPEC_TELEMETRY=0`
|
||||
- **THEN** telemetry is disabled regardless of config state
|
||||
|
||||
### Requirement: CI environment auto-disable
|
||||
The system SHALL automatically disable telemetry when `CI=true` environment variable is detected.
|
||||
|
||||
#### Scenario: CI environment detection
|
||||
- **WHEN** `CI=true` is set in the environment
|
||||
- **THEN** the system sends no telemetry events
|
||||
|
||||
#### Scenario: CI with explicit enable
|
||||
- **WHEN** `CI=true` is set
|
||||
- **AND** `OPENSPEC_TELEMETRY=1` is explicitly set
|
||||
- **THEN** telemetry remains disabled (CI takes precedence for privacy)
|
||||
|
||||
### Requirement: First-run telemetry notice
|
||||
The system SHALL display a one-line telemetry disclosure notice on the first command execution, before any telemetry is sent.
|
||||
|
||||
#### Scenario: First command execution
|
||||
- **WHEN** a user runs their first openspec command
|
||||
- **AND** telemetry is enabled
|
||||
- **THEN** the system displays: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0"
|
||||
|
||||
#### Scenario: Subsequent command execution
|
||||
- **WHEN** a user has already seen the notice (noticeSeen: true in config)
|
||||
- **THEN** the system does not display the notice
|
||||
|
||||
#### Scenario: Notice before telemetry
|
||||
- **WHEN** displaying the first-run notice
|
||||
- **THEN** the notice appears before any telemetry event is sent
|
||||
|
||||
### Requirement: Anonymous user identification
|
||||
The system SHALL generate a random UUID as an anonymous identifier on first telemetry send, stored in global config.
|
||||
|
||||
#### Scenario: First telemetry event
|
||||
- **WHEN** the first telemetry event is sent
|
||||
- **AND** no anonymousId exists in config
|
||||
- **THEN** the system generates a random UUID v4 and stores it in config
|
||||
|
||||
#### Scenario: Persistent identity
|
||||
- **WHEN** a user runs multiple commands across sessions
|
||||
- **THEN** the same anonymousId is used for all events
|
||||
|
||||
#### Scenario: Lazy generation with opt-out
|
||||
- **WHEN** a user opts out before running any command
|
||||
- **THEN** no anonymousId is ever generated or stored
|
||||
|
||||
### Requirement: Immediate event sending
|
||||
The system SHALL send telemetry events immediately without batching, using `flushAt: 1` and `flushInterval: 0` configuration.
|
||||
|
||||
#### Scenario: Event transmission timing
|
||||
- **WHEN** a command executes
|
||||
- **THEN** the telemetry event is sent immediately, not queued for batch transmission
|
||||
|
||||
### Requirement: Graceful shutdown
|
||||
The system SHALL call `posthog.shutdown()` before CLI exit to ensure pending events are flushed.
|
||||
|
||||
#### Scenario: Normal exit
|
||||
- **WHEN** a command completes successfully
|
||||
- **THEN** the system awaits `shutdown()` before exiting
|
||||
|
||||
#### Scenario: Error exit
|
||||
- **WHEN** a command fails with an error
|
||||
- **THEN** the system still awaits `shutdown()` before exiting
|
||||
|
||||
### Requirement: Silent failure handling
|
||||
The system SHALL silently ignore telemetry failures without affecting CLI functionality.
|
||||
|
||||
#### Scenario: Network failure
|
||||
- **WHEN** the telemetry request fails due to network error
|
||||
- **THEN** the CLI command completes normally without error message
|
||||
|
||||
#### Scenario: PostHog outage
|
||||
- **WHEN** PostHog service is unavailable
|
||||
- **THEN** the CLI command completes normally without error message
|
||||
|
||||
#### Scenario: Shutdown failure
|
||||
- **WHEN** `shutdown()` fails or times out
|
||||
- **THEN** the CLI exits normally without error message
|
||||
@@ -0,0 +1,47 @@
|
||||
## 1. Setup
|
||||
|
||||
- [x] 1.1 Add `posthog-node` package as a dependency
|
||||
- [x] 1.2 Create `src/telemetry/` module directory
|
||||
- [x] 1.3 Add PostHog API key configuration (environment variable or embedded)
|
||||
|
||||
## 2. Global Config
|
||||
|
||||
- [x] 2.1 Create or extend global config module for `~/.config/openspec/config.json`
|
||||
- [x] 2.2 Implement read/write functions that preserve existing config fields
|
||||
- [x] 2.3 Define telemetry config structure (`anonymousId`, `noticeSeen`)
|
||||
|
||||
## 3. Core Telemetry Module
|
||||
|
||||
- [x] 3.1 Implement `isTelemetryEnabled()` checking `OPENSPEC_TELEMETRY`, `DO_NOT_TRACK`, and `CI` env vars
|
||||
- [x] 3.2 Implement `getOrCreateAnonymousId()` with lazy UUID generation
|
||||
- [x] 3.3 Initialize PostHog client with `flushAt: 1` and `flushInterval: 0`
|
||||
- [x] 3.4 Implement `trackCommand(commandName, version)` with `$ip: null`
|
||||
- [x] 3.5 Implement `shutdown()` with try/catch for silent failure handling
|
||||
|
||||
## 4. First-Run Notice
|
||||
|
||||
- [x] 4.1 Implement `maybeShowTelemetryNotice()` function
|
||||
- [x] 4.2 Check `noticeSeen` flag before displaying notice
|
||||
- [x] 4.3 Display notice text: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0"
|
||||
- [x] 4.4 Update `noticeSeen` in config after first display
|
||||
|
||||
## 5. CLI Integration
|
||||
|
||||
- [x] 5.1 Add Commander.js `preAction` hook to show notice and track command
|
||||
- [x] 5.2 Add Commander.js `postAction` hook to call shutdown
|
||||
- [x] 5.3 Handle subcommand path extraction (e.g., `change:apply`)
|
||||
|
||||
## 6. Testing
|
||||
|
||||
- [x] 6.1 Test opt-out via `OPENSPEC_TELEMETRY=0`
|
||||
- [x] 6.2 Test opt-out via `DO_NOT_TRACK=1`
|
||||
- [x] 6.3 Test auto-disable in CI environment
|
||||
- [x] 6.4 Test first-run notice display and noticeSeen persistence
|
||||
- [x] 6.5 Test anonymous ID generation and persistence
|
||||
- [x] 6.6 Test silent failure on network error (mock PostHog)
|
||||
|
||||
## 7. Documentation
|
||||
|
||||
- [x] 7.1 Add telemetry disclosure section to README
|
||||
- [x] 7.2 Document opt-out methods (`OPENSPEC_TELEMETRY=0`, `DO_NOT_TRACK=1`)
|
||||
- [x] 7.3 Document what data is collected and not collected
|
||||
@@ -0,0 +1,16 @@
|
||||
## Why
|
||||
|
||||
CodeBuddy slash command configurator currently uses inconsistent frontmatter fields compared to other tools. It uses `category` and `tags` fields (like Crush) but should use `argument-hint` field (like Factory, Auggie, and Codex) for better consistency. Additionally, the `proposal` command is missing frontmatter fields entirely. After reviewing CodeBuddy's official documentation, the correct format should use `description` and `argument-hint` fields with square bracket parameter format.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Replace `category` and `tags` fields with `argument-hint` field in CodeBuddy frontmatter
|
||||
- Add missing frontmatter fields to the `proposal` command
|
||||
- Use correct square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
|
||||
- Ensure consistency with CodeBuddy's official documentation
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: cli-init, cli-update
|
||||
- Affected code: `src/core/configurators/slash/codebuddy.ts`
|
||||
- CodeBuddy users will get proper argument hints in the correct format for slash commands
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### Scenario: Generating slash commands for Antigravity
|
||||
- **WHEN** the user selects Antigravity during initialization
|
||||
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
|
||||
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
|
||||
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
|
||||
|
||||
#### Scenario: Generating slash commands for Claude Code
|
||||
- **WHEN** the user selects Claude Code during initialization
|
||||
- **THEN** create `.claude/commands/openspec/proposal.md`, `.claude/commands/openspec/apply.md`, and `.claude/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for CodeBuddy Code
|
||||
- **WHEN** the user selects CodeBuddy Code during initialization
|
||||
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates that include CodeBuddy-compatible YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
- **WHEN** the user selects Cline during initialization
|
||||
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Crush
|
||||
- **WHEN** the user selects Crush during initialization
|
||||
- **THEN** create `.crush/commands/openspec/proposal.md`, `.crush/commands/openspec/apply.md`, and `.crush/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cursor
|
||||
- **WHEN** the user selects Cursor during initialization
|
||||
- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-apply.md`, and `.cursor/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Factory Droid
|
||||
- **WHEN** the user selects Factory Droid during initialization
|
||||
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates that include Factory-compatible YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** include the `$ARGUMENTS` placeholder in the template body so droid receives any user-supplied input
|
||||
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
|
||||
|
||||
#### Scenario: Generating slash commands for OpenCode
|
||||
- **WHEN** the user selects OpenCode during initialization
|
||||
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Windsurf
|
||||
- **WHEN** the user selects Windsurf during initialization
|
||||
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Kilo Code
|
||||
- **WHEN** the user selects Kilo Code during initialization
|
||||
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Codex
|
||||
- **WHEN** the user selects Codex during initialization
|
||||
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
|
||||
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
|
||||
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Slash Command Updates
|
||||
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
|
||||
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
|
||||
|
||||
#### Scenario: Updating slash commands for Claude Code
|
||||
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for CodeBuddy Code
|
||||
- **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using the shared CodeBuddy templates that include YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
|
||||
- **AND** preserve any user customizations outside the OpenSpec managed markers
|
||||
|
||||
#### Scenario: Updating slash commands for Cline
|
||||
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Crush
|
||||
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** include Crush-specific frontmatter with OpenSpec category and tags
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Cursor
|
||||
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Factory Droid
|
||||
- **WHEN** `.factory/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using the shared Factory templates that include YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** ensure the template body retains the `$ARGUMENTS` placeholder so user input keeps flowing into droid
|
||||
- **AND** update only the content inside the OpenSpec managed markers, leaving any unmanaged notes untouched
|
||||
- **AND** skip creating missing files during update
|
||||
|
||||
#### Scenario: Updating slash commands for OpenCode
|
||||
- **WHEN** `.opencode/command/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** ensure the archive command includes `$ARGUMENTS` placeholder in frontmatter for accepting change ID arguments
|
||||
|
||||
#### Scenario: Updating slash commands for Windsurf
|
||||
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
@@ -0,0 +1,6 @@
|
||||
## 1. Implementation
|
||||
|
||||
- [x] 1.1 Update CodeBuddy frontmatter to use `argument-hint` instead of `category` and `tags`
|
||||
- [x] 1.2 Add missing frontmatter fields to the `proposal` command
|
||||
- [x] 1.3 Ensure all three commands (proposal, apply, archive) have consistent frontmatter structure
|
||||
- [x] 1.4 Test the changes by running `openspec init` and `openspec update`
|
||||
@@ -0,0 +1,206 @@
|
||||
# Design: Nix CI Validation
|
||||
|
||||
## Context
|
||||
|
||||
OpenSpec recently added Nix flake support to enable Nix users to install the tool. This includes:
|
||||
- `flake.nix`: Nix package definition with pnpm dependency fetching
|
||||
- `scripts/update-flake.sh`: Automation script to update version and hash when releasing
|
||||
|
||||
Currently, there is no CI validation ensuring these Nix artifacts remain functional. The existing CI workflow (.github/workflows/ci.yml) validates Node.js builds, tests, and linting across multiple platforms (Linux, macOS, Windows) but does not validate Nix builds.
|
||||
|
||||
**Stakeholders**: Nix users, maintainers, contributors who need confidence that Nix support works.
|
||||
|
||||
**Constraints**:
|
||||
- Must work in GitHub Actions Linux runners
|
||||
- Should minimize CI runtime impact (<5 minutes added)
|
||||
- Should support local testing with `act` for rapid iteration
|
||||
- Must integrate with existing required checks
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals**:
|
||||
- Validate `nix build` succeeds on every PR/push
|
||||
- Validate `scripts/update-flake.sh` executes without errors
|
||||
- Ensure Nix support doesn't regress silently
|
||||
- Support local testing with `act`
|
||||
- Optimize with caching to minimize CI time
|
||||
|
||||
**Non-Goals**:
|
||||
- Testing on macOS (GitHub-hosted macOS runners are slower and more expensive; Nix flake already declares macOS support)
|
||||
- Building for all declared systems (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin) - focus on most common platform
|
||||
- Validating Nix flake quality/style (nixpkgs-fmt, etc.) - can be added later if needed
|
||||
- Running OpenSpec's full test suite through Nix build - existing CI already does this
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Use DeterminateSystems nix-installer-action
|
||||
|
||||
**What**: Use `determinatesystems/nix-installer-action` for installing Nix in CI.
|
||||
|
||||
**Why**:
|
||||
- Official GitHub Action maintained by Determinate Systems (Nix experts)
|
||||
- Handles GitHub Actions environment quirks automatically
|
||||
- Includes automatic caching configuration
|
||||
- More reliable than curl | sh installation script
|
||||
- Better error messages and diagnostics
|
||||
|
||||
**Alternatives considered**:
|
||||
- Official Nix installer (`curl -L https://nixos.org/nix/install | sh`): Works but requires manual setup of flakes, caching, and CI-specific configuration
|
||||
- `cachix/install-nix-action`: Popular alternative but determinatesystems is more actively maintained and has better GHA integration
|
||||
|
||||
### Decision 2: Use Magic Nix Cache for performance
|
||||
|
||||
**What**: Use `determinatesystems/magic-nix-cache-action` for automatic binary caching.
|
||||
|
||||
**Why**:
|
||||
- Zero-configuration caching for Nix store
|
||||
- Significantly reduces CI time on subsequent runs (from ~5min to ~1-2min)
|
||||
- Free for public repositories
|
||||
- Handles cache keys automatically
|
||||
|
||||
**Alternatives considered**:
|
||||
- Manual Nix store caching with GitHub Actions cache: More complex, requires manual cache key management
|
||||
- Cachix: Excellent tool but requires account setup and token management
|
||||
- No caching: Acceptable for initial implementation, but poor developer experience
|
||||
|
||||
### Decision 3: Separate job for Nix validation
|
||||
|
||||
**What**: Create a dedicated `nix-validate` job in .github/workflows/ci.yml that runs in parallel with other jobs.
|
||||
|
||||
**Why**:
|
||||
- Keeps Nix validation isolated from Node.js validation
|
||||
- Allows parallel execution for faster CI
|
||||
- Easier to debug when Nix-specific issues occur
|
||||
- Can be marked as required check independently
|
||||
|
||||
**Alternatives considered**:
|
||||
- Add Nix steps to existing jobs: Creates coupling between Node.js and Nix validation, harder to maintain
|
||||
- Separate workflow file: Overkill for a single job, harder to manage required checks
|
||||
|
||||
### Decision 4: Validate update script by executing it
|
||||
|
||||
**What**: Run `scripts/update-flake.sh` as part of CI validation.
|
||||
|
||||
**Why**:
|
||||
- Ensures the script doesn't break due to changes in package.json format, nix build output, or dependencies
|
||||
- Tests the full workflow users will follow when releasing
|
||||
- Catches errors early
|
||||
|
||||
**Implementation approach**:
|
||||
- Execute script in a way that doesn't modify git state (or discard changes after)
|
||||
- Verify script exits with code 0
|
||||
- Optionally validate that flake.nix contains expected patterns after execution
|
||||
|
||||
**Alternatives considered**:
|
||||
- Mock/dry-run mode: Would require modifying the script significantly
|
||||
- Skip validation: Risky - script could break and only be discovered at release time
|
||||
- Only run on release branches: Misses issues early in development
|
||||
|
||||
### Decision 5: Run on pull_request and push to main
|
||||
|
||||
**What**: Configure Nix validation job to run on:
|
||||
- `pull_request` events (any PR to main)
|
||||
- `push` events (direct pushes to main)
|
||||
- `workflow_dispatch` (manual trigger for testing)
|
||||
|
||||
**Why**:
|
||||
- Catches issues before merge (pull_request)
|
||||
- Validates main branch stays healthy (push)
|
||||
- Allows manual testing without creating PRs (workflow_dispatch)
|
||||
|
||||
### Decision 6: Support act for local testing
|
||||
|
||||
**What**: Ensure workflow is compatible with `act` tool for local CI testing.
|
||||
|
||||
**Why**:
|
||||
- Faster iteration when developing CI changes
|
||||
- Allows testing without pushing to GitHub
|
||||
- Reduces commit noise from CI debugging
|
||||
|
||||
**Requirements**:
|
||||
- Use standard GitHub Actions syntax
|
||||
- Document any act-specific configuration needed
|
||||
- Test that Nix can be installed in act's Docker containers
|
||||
|
||||
**Limitations**:
|
||||
- act may not perfectly replicate GitHub's runners, but close enough for validation
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk: CI runtime increase
|
||||
|
||||
**Impact**: Adding Nix validation will increase total CI time by 2-5 minutes per run.
|
||||
|
||||
**Mitigation**:
|
||||
- Run Nix job in parallel with existing jobs (no blocking delay)
|
||||
- Use magic-nix-cache for subsequent runs (~1-2 min with cache)
|
||||
- Configure appropriate timeout (10 minutes max)
|
||||
|
||||
**Acceptance**: The benefit of preventing Nix regressions outweighs the cost.
|
||||
|
||||
### Risk: Nix installer failures in CI
|
||||
|
||||
**Impact**: Transient failures in Nix installation could block PRs.
|
||||
|
||||
**Mitigation**:
|
||||
- Use determinatesystems action which has retry logic
|
||||
- Monitor for flaky failures and adjust if needed
|
||||
- Document troubleshooting steps
|
||||
|
||||
**Acceptance**: Nix installation is generally stable in GHA; this is low risk.
|
||||
|
||||
### Risk: Update script modifies git state
|
||||
|
||||
**Impact**: Running update-flake.sh modifies flake.nix, which could cause CI to fail if git state is checked.
|
||||
|
||||
**Mitigation**:
|
||||
- Run script in isolation without committing changes
|
||||
- Add `git checkout -- flake.nix` after validation
|
||||
- Or accept dirty git state in CI (doesn't affect build validation)
|
||||
|
||||
**Acceptance**: Script validation is important enough to handle this carefully.
|
||||
|
||||
### Risk: act compatibility issues
|
||||
|
||||
**Impact**: Workflow might not work perfectly with act due to Docker environment differences.
|
||||
|
||||
**Mitigation**:
|
||||
- Document known limitations
|
||||
- Focus on GitHub Actions as primary validation target
|
||||
- Use act as best-effort local testing
|
||||
|
||||
**Acceptance**: act support is nice-to-have, not required.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Add Nix job (new, non-required)
|
||||
1. Add `nix-validate` job to .github/workflows/ci.yml
|
||||
2. Configure to run in parallel with existing jobs
|
||||
3. Do NOT mark as required check initially
|
||||
4. Monitor for ~1 week to ensure stability
|
||||
|
||||
### Phase 2: Make required
|
||||
1. After validation is stable, add to required checks
|
||||
2. Update branch protection rules in GitHub settings
|
||||
3. Document in CONTRIBUTING.md or README
|
||||
|
||||
### Rollback Plan
|
||||
If Nix validation causes issues:
|
||||
1. Remove job from required checks in GitHub settings (immediate)
|
||||
2. Comment out or remove job from workflow (permanent fix)
|
||||
3. Investigate and fix issues
|
||||
4. Re-enable following same phased approach
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Q**: Should we test update-flake.sh on every CI run, or only when package.json or pnpm-lock.yaml changes?
|
||||
- **A**: Test on every run for simplicity. The script is fast (<30 seconds) and catching regressions is valuable.
|
||||
|
||||
- **Q**: Should we validate on macOS as well?
|
||||
- **A**: No for initial implementation. Linux validation is sufficient and macOS runners are slower/more expensive. Can add later if users report macOS-specific issues.
|
||||
|
||||
- **Q**: Should we run full OpenSpec tests through the Nix build?
|
||||
- **A**: No. The Nix build already runs `pnpm test` as part of its build phase. Existing CI jobs cover testing thoroughly. Nix validation focuses on build success.
|
||||
|
||||
- **Q**: What timeout should we use for the Nix validation job?
|
||||
- **A**: Start with 10 minutes. With caching, jobs should complete in 1-3 minutes. Without cache (first run), 5-7 minutes is expected.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add Nix CI Validation
|
||||
|
||||
## Why
|
||||
|
||||
The project recently added Nix flake support (flake.nix) and an automated update script (scripts/update-flake.sh) to enable Nix users to install OpenSpec. However, there is no CI validation ensuring these Nix artifacts continue to work as the project evolves. This creates risk that breaking changes could be merged without detection.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a new GitHub Actions workflow job to validate Nix flake builds successfully
|
||||
- Add validation that the update-flake.sh script executes without errors
|
||||
- Test on Linux (where Nix support is most common)
|
||||
- Ensure CI fails if Nix build or update script breaks
|
||||
- Enable local testing with `act` for developers
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: New capability `ci-nix-validation`
|
||||
- Affected code: `.github/workflows/ci.yml` (add new job)
|
||||
- Affected infrastructure: GitHub Actions runners with Nix installed
|
||||
- Benefits: Prevents regressions in Nix support, gives confidence to Nix users
|
||||
- Trade-offs: Adds ~2-3 minutes to CI runtime
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
# CI Nix Validation Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Nix Flake Build Validation
|
||||
|
||||
The CI system SHALL validate that the Nix flake builds successfully on every pull request and push to main.
|
||||
|
||||
#### Scenario: Successful flake build
|
||||
|
||||
- **WHEN** a pull request or push to main is made
|
||||
- **THEN** the CI SHALL execute `nix build` and verify it completes with exit code 0
|
||||
- **AND** the build output SHALL contain the openspec binary
|
||||
|
||||
#### Scenario: Flake build failure
|
||||
|
||||
- **WHEN** the Nix flake configuration is broken
|
||||
- **THEN** the CI job SHALL fail with a non-zero exit code
|
||||
- **AND** the CI SHALL prevent merging of the pull request
|
||||
|
||||
#### Scenario: Multi-platform support check
|
||||
|
||||
- **WHEN** the flake declares support for multiple systems
|
||||
- **THEN** the CI SHALL validate the flake builds on at least Linux (x86_64-linux)
|
||||
|
||||
### Requirement: Update Script Validation
|
||||
|
||||
The CI system SHALL validate that the update-flake.sh script executes successfully and produces valid output.
|
||||
|
||||
#### Scenario: Update script execution
|
||||
|
||||
- **WHEN** the CI runs the update script validation
|
||||
- **THEN** the script SHALL execute without errors
|
||||
- **AND** the script SHALL correctly extract the version from package.json
|
||||
- **AND** the script SHALL update flake.nix with the correct version
|
||||
|
||||
#### Scenario: Update script with mock hash
|
||||
|
||||
- **WHEN** validating the update script in CI
|
||||
- **THEN** the script SHALL be able to detect and extract the correct pnpm dependency hash
|
||||
- **AND** the flake.nix SHALL be updated with a valid sha256 hash
|
||||
|
||||
### Requirement: CI Job Integration
|
||||
|
||||
The Nix validation jobs SHALL be integrated into the existing GitHub Actions workflow and required for merge.
|
||||
|
||||
#### Scenario: PR merge requirements
|
||||
|
||||
- **WHEN** a pull request is created
|
||||
- **THEN** the Nix validation job SHALL be included in required checks
|
||||
- **AND** the PR SHALL NOT be mergeable until Nix validation passes
|
||||
|
||||
#### Scenario: Job execution triggers
|
||||
|
||||
- **WHEN** code is pushed to a pull request OR pushed to main OR manually triggered
|
||||
- **THEN** the Nix validation job SHALL execute automatically
|
||||
|
||||
### Requirement: Local Testing Support
|
||||
|
||||
The CI workflow SHALL be testable locally using the `act` tool to enable rapid iteration.
|
||||
|
||||
#### Scenario: Local CI execution with act
|
||||
|
||||
- **WHEN** a developer runs `act` with the Nix validation workflow
|
||||
- **THEN** the workflow SHALL execute in the local Docker environment
|
||||
- **AND** the developer SHALL receive feedback on Nix build status without pushing to GitHub
|
||||
|
||||
#### Scenario: Act configuration compatibility
|
||||
|
||||
- **WHEN** the workflow is designed
|
||||
- **THEN** it SHALL use standard GitHub Actions syntax compatible with `act`
|
||||
- **AND** any Nix-specific setup SHALL work in the act Docker environment
|
||||
|
||||
### Requirement: Nix Installation in CI
|
||||
|
||||
The CI environment SHALL have Nix properly installed and configured before running validation.
|
||||
|
||||
#### Scenario: Nix installation step
|
||||
|
||||
- **WHEN** the Nix validation job starts
|
||||
- **THEN** Nix SHALL be installed using the official Nix installer or determinatesystems/nix-installer-action
|
||||
- **AND** the Nix installation SHALL be cached for subsequent runs to improve performance
|
||||
|
||||
#### Scenario: Nix configuration for CI
|
||||
|
||||
- **WHEN** Nix is installed in CI
|
||||
- **THEN** it SHALL be configured to work in the GitHub Actions environment
|
||||
- **AND** experimental features (flakes, nix-command) SHALL be enabled
|
||||
|
||||
### Requirement: CI Performance Optimization
|
||||
|
||||
The Nix validation SHALL be optimized to minimize CI runtime impact.
|
||||
|
||||
#### Scenario: Acceptable runtime
|
||||
|
||||
- **WHEN** the Nix validation job runs
|
||||
- **THEN** it SHALL complete in under 5 minutes on a clean run
|
||||
- **AND** with caching, it SHALL complete in under 3 minutes on subsequent runs
|
||||
|
||||
#### Scenario: Parallel execution
|
||||
|
||||
- **WHEN** multiple CI jobs are running
|
||||
- **THEN** the Nix validation job SHALL run in parallel with other validation jobs (tests, lint)
|
||||
- **AND** SHALL NOT block other independent checks
|
||||
@@ -0,0 +1,49 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Add Nix Installation to CI
|
||||
|
||||
- [x] 1.1 Research Nix installation options for GitHub Actions (nix-installer-action vs manual install)
|
||||
- [x] 1.2 Add Nix installation step to .github/workflows/ci.yml
|
||||
- [x] 1.3 Configure Nix with experimental features enabled (flakes, nix-command)
|
||||
- [x] 1.4 Add Nix store caching to improve CI performance
|
||||
|
||||
## 2. Create Nix Build Validation Job
|
||||
|
||||
- [x] 2.1 Add new `nix-flake-validate` job to .github/workflows/ci.yml
|
||||
- [x] 2.2 Implement `nix build` step with proper error handling
|
||||
- [x] 2.3 Add verification step to confirm binary exists in build output
|
||||
- [x] 2.4 Add step to test binary execution (`nix run . -- --version`)
|
||||
|
||||
## 3. Add Update Script Validation
|
||||
|
||||
- [x] 3.1 Add job step to run scripts/update-flake.sh in dry-run or test mode
|
||||
- [x] 3.2 Verify script executes without errors
|
||||
- [x] 3.3 Add validation that version is correctly extracted from package.json
|
||||
- [x] 3.4 Verify flake.nix is updated with correct format (version and hash)
|
||||
|
||||
## 4. Configure Job Dependencies and Requirements
|
||||
|
||||
- [x] 4.1 Configure Nix validation job to run on pull_request and push events
|
||||
- [x] 4.2 Add Nix validation to required checks list
|
||||
- [x] 4.3 Configure job to run in parallel with existing test/lint jobs
|
||||
- [x] 4.4 Set appropriate timeout (5-10 minutes)
|
||||
|
||||
## 5. Test with act Locally
|
||||
|
||||
- [x] 5.1 Install act locally if not already available
|
||||
- [x] 5.2 Test Nix validation job using `act pull_request`
|
||||
- [x] 5.3 Verify act can run the workflow with Nix installed
|
||||
- [x] 5.4 Document any act-specific configuration needed in .actrc or README
|
||||
|
||||
## 6. Documentation and Finalization
|
||||
|
||||
- [x] 6.1 Add documentation about Nix CI validation to README or CONTRIBUTING.md
|
||||
- [x] 6.2 Document how to test CI locally with act
|
||||
- [ ] 6.3 Update CI badge or status indicators if needed
|
||||
- [ ] 6.4 Test end-to-end by creating a test PR
|
||||
|
||||
## 7. Archive Change
|
||||
|
||||
- [x] 7.1 After merge and verification, create new spec file at openspec/specs/ci-nix-validation/spec.md
|
||||
- [x] 7.2 Move change directory to openspec/changes/archive/[date]-add-nix-ci-validation/
|
||||
- [x] 7.3 Run `openspec validate --strict` to confirm archived change passes
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-23
|
||||
@@ -0,0 +1,193 @@
|
||||
## Context
|
||||
|
||||
Currently `openspec init` and `openspec experimental` are separate commands with distinct purposes:
|
||||
|
||||
- **init**: Creates `openspec/` directory, generates `AGENTS.md`/`project.md`, configures tool config files (`CLAUDE.md`, etc.), generates old slash commands (`/openspec:proposal`, etc.)
|
||||
- **experimental**: Generates skills (9 per tool), generates opsx slash commands (`/opsx:new`, etc.), creates `config.yaml`
|
||||
|
||||
The skill-based workflow (experimental) is the direction we're going, so we're making it the default by merging into `init`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Single `openspec init` command that sets up the complete skill-based workflow
|
||||
- Clean migration path for existing users with legacy artifacts
|
||||
- Remove all code related to config files and old slash commands
|
||||
- Keep the polished UX from experimental (animated welcome, searchable multi-select)
|
||||
|
||||
**Non-Goals:**
|
||||
- Supporting both workflows simultaneously
|
||||
- Providing options to use the old workflow
|
||||
- Backward compatibility for `/openspec:*` commands (breaking change)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Merge into init, not into experimental
|
||||
|
||||
**Choice**: Rewrite `init` to do what `experimental` does, then delete `experimental`.
|
||||
|
||||
**Rationale**: `init` is the canonical setup command. Users expect `init` to set up their project. `experimental` was always meant to be temporary.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Keep `experimental` as the main command → confusing name for default behavior
|
||||
- Create new command → unnecessary, `init` already exists
|
||||
|
||||
### Decision 2: Legacy cleanup with Y/N prompt
|
||||
|
||||
**Choice**: Detect legacy artifacts, show what was found, prompt `"Legacy files detected. Upgrade and clean up? [Y/n]"`, then remove if confirmed.
|
||||
|
||||
**Rationale**: Users should know what's being removed. A single Y/N is simple and decisive. No need for multiple options.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Multiple options (keep/remove/cancel) → overcomplicated
|
||||
- Silent removal → users might be surprised
|
||||
- Just warn without removing → leaves cruft
|
||||
|
||||
### Decision 3: Surgical removal of legacy content
|
||||
|
||||
**Choice**: For files with mixed content (OpenSpec markers + user content), only remove the OpenSpec marker block. For files that are 100% OpenSpec content, delete the entire file.
|
||||
|
||||
**Rationale**: Respects user customizations. CLAUDE.md might have other instructions beyond OpenSpec.
|
||||
|
||||
**Edge cases**:
|
||||
- **Config files with mixed content**: Remove only `<!-- OPENSPEC:START -->` to `<!-- OPENSPEC:END -->` block
|
||||
- **Config files that are 100% OpenSpec**: Delete file entirely (check if content outside markers is empty/whitespace)
|
||||
- **Old slash command directories** (`.claude/commands/openspec/`): Delete entire directory (ours)
|
||||
- **`openspec/AGENTS.md`**: Delete (ours)
|
||||
- **Root `AGENTS.md`**: Only remove OpenSpec marker block, preserve rest
|
||||
|
||||
### Decision 6: Preserve project.md with migration hint
|
||||
|
||||
**Choice**: Do NOT auto-delete `openspec/project.md`. Preserve it and show a message directing users to manually migrate content to `config.yaml`'s `context:` field.
|
||||
|
||||
**Rationale**:
|
||||
- `project.md` may contain valuable user-written project documentation
|
||||
- The new workflow uses `config.yaml.context` for the same purpose (auto-injected into artifacts)
|
||||
- Auto-deleting would lose user content; auto-migrating is complex (needs LLM to compress)
|
||||
- Users can migrate manually or use `/opsx:explore` to get AI assistance
|
||||
|
||||
**Migration path**:
|
||||
1. During legacy cleanup, detect `openspec/project.md` but do not delete
|
||||
2. Show in output: "openspec/project.md still exists - migrate content to config.yaml's context: field, then delete"
|
||||
3. User migrates manually or asks Claude in explore mode: "help me migrate project.md to config.yaml"
|
||||
4. User deletes project.md when ready
|
||||
|
||||
**Why not auto-migrate?**
|
||||
- `project.md` is verbose (sections, headers, placeholders)
|
||||
- `config.yaml.context` should be concise and dense
|
||||
- LLM compression would be ideal but adds complexity and non-determinism to init
|
||||
- Manual migration lets users decide what's actually important
|
||||
|
||||
### Decision 4: Hidden alias for experimental
|
||||
|
||||
**Choice**: Keep `openspec experimental` as a hidden command that delegates to `init`.
|
||||
|
||||
**Rationale**: Users who learned `experimental` can still use it during transition. Hidden means it won't show in help.
|
||||
|
||||
### Decision 5: Reuse existing infrastructure
|
||||
|
||||
**Choice**: Reuse skill templates, command adapters, welcome screen, and multi-select from experimental.
|
||||
|
||||
**Rationale**: Already built and working. Just needs to be called from init instead of experimental.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Users with custom `/openspec:*` commands lose them | Document in release notes; old commands are in git history |
|
||||
| Mixed-content detection might be imperfect | Conservative approach: if unsure, preserve the file and warn |
|
||||
| Users confused by missing config files | Clear messaging in init output about what changed |
|
||||
| `openspec update` might break | Review and update `update` command to work with new structure |
|
||||
|
||||
## Architecture
|
||||
|
||||
### What init creates (after merge)
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── config.yaml # Schema settings (from experimental)
|
||||
├── specs/ # Empty, for user's specs
|
||||
└── changes/ # Empty, for user's changes
|
||||
└── archive/
|
||||
|
||||
.<tool>/skills/ # 9 skills per selected tool
|
||||
├── openspec-explore/SKILL.md
|
||||
├── openspec-new-change/SKILL.md
|
||||
├── openspec-continue-change/SKILL.md
|
||||
├── openspec-apply-change/SKILL.md
|
||||
├── openspec-ff-change/SKILL.md
|
||||
├── openspec-verify-change/SKILL.md
|
||||
├── openspec-sync-specs/SKILL.md
|
||||
├── openspec-archive-change/SKILL.md
|
||||
└── openspec-bulk-archive-change/SKILL.md
|
||||
|
||||
.<tool>/commands/opsx/ # 9 slash commands per selected tool
|
||||
├── explore.md
|
||||
├── new.md
|
||||
├── continue.md
|
||||
├── apply.md
|
||||
├── ff.md
|
||||
├── verify.md
|
||||
├── sync.md
|
||||
├── archive.md
|
||||
└── bulk-archive.md
|
||||
```
|
||||
|
||||
### What init no longer creates
|
||||
|
||||
- `CLAUDE.md`, `.cursorrules`, `.windsurfrules`, etc. (config files)
|
||||
- `openspec/AGENTS.md`
|
||||
- `openspec/project.md`
|
||||
- Root `AGENTS.md` stub
|
||||
- `.claude/commands/openspec/` (old slash commands)
|
||||
|
||||
### Legacy detection targets
|
||||
|
||||
| Artifact Type | Detection Method | Removal Method |
|
||||
|--------------|------------------|----------------|
|
||||
| Config files (CLAUDE.md, etc.) | File exists AND contains OpenSpec markers | Remove marker block; delete file if empty after |
|
||||
| Old slash command dirs | Directory exists at `.<tool>/commands/openspec/` | Delete entire directory |
|
||||
| openspec/AGENTS.md | File exists at `openspec/AGENTS.md` | Delete file |
|
||||
| openspec/project.md | File exists at `openspec/project.md` | **Preserve** - show migration hint only |
|
||||
| Root AGENTS.md | File exists at `AGENTS.md` AND contains OpenSpec markers | Remove marker block; delete file if empty after |
|
||||
|
||||
### Code to remove
|
||||
|
||||
- `src/core/configurators/` - entire directory (ToolRegistry, all config generators)
|
||||
- `src/core/configurators/slash/` - entire directory (SlashCommandRegistry, old command generators)
|
||||
- `src/core/templates/slash-command-templates.ts` - old `/openspec:*` content
|
||||
- `src/core/templates/claude-template.ts`
|
||||
- `src/core/templates/cline-template.ts`
|
||||
- `src/core/templates/costrict-template.ts`
|
||||
- `src/core/templates/agents-template.ts`
|
||||
- `src/core/templates/agents-root-stub.ts`
|
||||
- `src/core/templates/project-template.ts`
|
||||
- `src/commands/experimental/` - entire directory (merged into init)
|
||||
- Related test files
|
||||
|
||||
### Code to migrate into init
|
||||
|
||||
- Animated welcome screen (`src/ui/welcome-screen.ts`) - keep, call from init
|
||||
- Searchable multi-select (`src/prompts/searchable-multi-select.ts`) - keep, call from init
|
||||
- Skill templates (`src/core/templates/skill-templates.ts`) - keep
|
||||
- Command generation (`src/core/command-generation/`) - keep
|
||||
- Tool states detection (from `experimental/setup.ts`) - move to init
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **What happens to `openspec update`?** - RESOLVED
|
||||
|
||||
**Current behavior**: Updates `openspec/AGENTS.md`, config files (`CLAUDE.md`, etc.) via `ToolRegistry`, and old slash commands (`/openspec:*`) via `SlashCommandRegistry`.
|
||||
|
||||
**New behavior**: Rewrite to refresh skills and opsx commands instead:
|
||||
- Detect which tools have skills installed (check for `.claude/skills/openspec-*/`, etc.)
|
||||
- Refresh all 9 skill files per installed tool using `skill-templates.ts`
|
||||
- Refresh all 9 opsx command files per installed tool using `command-generation/` adapters
|
||||
- Remove imports of `ToolRegistry`, `SlashCommandRegistry`, `agentsTemplate`
|
||||
- Update output messaging to reflect skills/commands instead of config files
|
||||
|
||||
**Key principle**: Same as current update - only refresh existing tools, don't add new ones.
|
||||
|
||||
2. **Should we keep `openspec schemas` and other experimental subcommands?** - RESOLVED
|
||||
|
||||
**Decision**: Yes, keep them. Remove "[Experimental]" label from all subcommands (status, instructions, schemas, etc.). See task 4.3.
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
The current setup has two separate commands (`openspec init` and `openspec experimental`) that configure different parts of the OpenSpec workflow. This creates confusion about which command to run, results in partial setups, and maintains two parallel systems (config files + old slash commands vs skills + opsx commands). Making the skill-based workflow the default simplifies onboarding and establishes a single, consistent way to use OpenSpec.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: `openspec init` now generates skills and `/opsx:*` commands instead of config files and `/openspec:*` commands
|
||||
- **BREAKING**: Config files (`CLAUDE.md`, `.cursorrules`, etc.) are no longer generated
|
||||
- **BREAKING**: Old slash commands (`/openspec:proposal`, `/openspec:apply`, `/openspec:archive`) are no longer generated
|
||||
- **BREAKING**: `openspec/AGENTS.md` and `openspec/project.md` are no longer generated
|
||||
- Merge `experimental` command functionality into `init`
|
||||
- Add legacy detection and auto-cleanup with Y/N confirmation
|
||||
- Keep `openspec experimental` as hidden alias for backward compatibility
|
||||
- Use the animated welcome screen from experimental for the unified init
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `legacy-cleanup`: Detect and remove legacy OpenSpec artifacts (config files, old slash commands, AGENTS.md) during init
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `cli-init`: Complete rewrite - generates skills and opsx commands instead of config files and old slash commands; removes AGENTS.md/project.md generation; adds legacy cleanup; uses experimental's animated welcome screen
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code removal**: `ToolRegistry`, `SlashCommandRegistry`, config file generators, old slash command templates, AGENTS.md/project.md templates
|
||||
- **Code migration**: Move skill generation and command adapter logic from `experimental/setup.ts` into `init.ts`
|
||||
- **Commands affected**: `init` (rewritten), `experimental` (becomes hidden alias), `update` (may need adjustment)
|
||||
- **User migration**: Existing users running `init` will be prompted to clean up legacy files
|
||||
- **Breaking for**: Users relying on config files for passive triggering, users using `/openspec:*` commands
|
||||
@@ -0,0 +1,176 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Directory Creation
|
||||
|
||||
The command SHALL create the OpenSpec directory structure with config file.
|
||||
|
||||
#### Scenario: Creating OpenSpec structure
|
||||
|
||||
- **WHEN** `openspec init` is executed
|
||||
- **THEN** create the following directory structure:
|
||||
```
|
||||
openspec/
|
||||
├── config.yaml
|
||||
├── specs/
|
||||
└── changes/
|
||||
└── archive/
|
||||
```
|
||||
|
||||
### Requirement: AI Tool Configuration
|
||||
|
||||
The command SHALL configure AI coding assistants with skills and slash commands using a searchable multi-select experience.
|
||||
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
|
||||
- **WHEN** run interactively
|
||||
- **THEN** display animated welcome screen with OpenSpec logo
|
||||
- **AND** present a searchable multi-select that shows all available tools
|
||||
- **AND** mark already configured tools with "(configured ✓)" indicator
|
||||
- **AND** pre-select configured tools for easy refresh
|
||||
- **AND** sort configured tools to appear first in the list
|
||||
- **AND** allow filtering by typing to search
|
||||
|
||||
#### Scenario: Selecting tools to configure
|
||||
|
||||
- **WHEN** user selects tools and confirms
|
||||
- **THEN** generate skills in `.<tool>/skills/` directory for each selected tool
|
||||
- **AND** generate slash commands in `.<tool>/commands/opsx/` directory for each selected tool
|
||||
- **AND** create `openspec/config.yaml` with default schema setting
|
||||
|
||||
### Requirement: Skill Generation
|
||||
|
||||
The command SHALL generate Agent Skills for selected AI tools.
|
||||
|
||||
#### Scenario: Generating skills for a tool
|
||||
|
||||
- **WHEN** a tool is selected during initialization
|
||||
- **THEN** create 9 skill directories under `.<tool>/skills/`:
|
||||
- `openspec-explore/SKILL.md`
|
||||
- `openspec-new-change/SKILL.md`
|
||||
- `openspec-continue-change/SKILL.md`
|
||||
- `openspec-apply-change/SKILL.md`
|
||||
- `openspec-ff-change/SKILL.md`
|
||||
- `openspec-verify-change/SKILL.md`
|
||||
- `openspec-sync-specs/SKILL.md`
|
||||
- `openspec-archive-change/SKILL.md`
|
||||
- `openspec-bulk-archive-change/SKILL.md`
|
||||
- **AND** each SKILL.md SHALL contain YAML frontmatter with name and description
|
||||
- **AND** each SKILL.md SHALL contain the skill instructions
|
||||
|
||||
### Requirement: Slash Command Generation
|
||||
|
||||
The command SHALL generate opsx slash commands for selected AI tools.
|
||||
|
||||
#### Scenario: Generating slash commands for a tool
|
||||
|
||||
- **WHEN** a tool is selected during initialization
|
||||
- **THEN** create 9 slash command files using the tool's command adapter:
|
||||
- `/opsx:explore`
|
||||
- `/opsx:new`
|
||||
- `/opsx:continue`
|
||||
- `/opsx:apply`
|
||||
- `/opsx:ff`
|
||||
- `/opsx:verify`
|
||||
- `/opsx:sync`
|
||||
- `/opsx:archive`
|
||||
- `/opsx:bulk-archive`
|
||||
- **AND** use tool-specific path conventions (e.g., `.claude/commands/opsx/` for Claude)
|
||||
- **AND** include tool-specific frontmatter format
|
||||
|
||||
### Requirement: Success Output
|
||||
|
||||
The command SHALL provide clear, actionable next steps upon successful initialization.
|
||||
|
||||
#### Scenario: Displaying success message
|
||||
|
||||
- **WHEN** initialization completes successfully
|
||||
- **THEN** display categorized summary:
|
||||
- "Created: <tools>" for newly configured tools
|
||||
- "Refreshed: <tools>" for already-configured tools that were updated
|
||||
- Count of skills and commands generated
|
||||
- **AND** display getting started section with:
|
||||
- `/opsx:new` - Start a new change
|
||||
- `/opsx:continue` - Create the next artifact
|
||||
- `/opsx:apply` - Implement tasks
|
||||
- **AND** display links to documentation and feedback
|
||||
|
||||
#### Scenario: Displaying restart instruction
|
||||
|
||||
- **WHEN** initialization completes successfully and tools were created or refreshed
|
||||
- **THEN** display instruction to restart IDE for slash commands to take effect
|
||||
|
||||
### Requirement: Config File Generation
|
||||
|
||||
The command SHALL create an OpenSpec config file with schema settings.
|
||||
|
||||
#### Scenario: Creating config.yaml
|
||||
|
||||
- **WHEN** initialization completes
|
||||
- **AND** config.yaml does not exist
|
||||
- **THEN** create `openspec/config.yaml` with default schema setting
|
||||
- **AND** display config location in output
|
||||
|
||||
#### Scenario: Preserving existing config.yaml
|
||||
|
||||
- **WHEN** initialization runs in extend mode
|
||||
- **AND** `openspec/config.yaml` already exists
|
||||
- **THEN** preserve the existing config file
|
||||
- **AND** display "(exists)" indicator in output
|
||||
|
||||
### Requirement: Non-Interactive Mode
|
||||
|
||||
The command SHALL support non-interactive operation through command-line options.
|
||||
|
||||
#### Scenario: Select all tools non-interactively
|
||||
|
||||
- **WHEN** run with `--tools all`
|
||||
- **THEN** automatically select every available AI tool without prompting
|
||||
- **AND** proceed with skill and command generation
|
||||
|
||||
#### Scenario: Select specific tools non-interactively
|
||||
|
||||
- **WHEN** run with `--tools claude,cursor`
|
||||
- **THEN** parse the comma-separated tool IDs
|
||||
- **AND** generate skills and commands for specified tools only
|
||||
|
||||
#### Scenario: Skip tool configuration non-interactively
|
||||
|
||||
- **WHEN** run with `--tools none`
|
||||
- **THEN** create only the openspec directory structure and config.yaml
|
||||
- **AND** skip skill and command generation
|
||||
|
||||
### Requirement: Experimental Command Alias
|
||||
|
||||
The command SHALL maintain backward compatibility with the experimental command.
|
||||
|
||||
#### Scenario: Running openspec experimental
|
||||
|
||||
- **WHEN** user runs `openspec experimental`
|
||||
- **THEN** delegate to `openspec init`
|
||||
- **AND** the command SHALL be hidden from help output
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: File Generation
|
||||
|
||||
**Reason**: AGENTS.md and project.md are no longer generated. Skills contain all necessary instructions.
|
||||
|
||||
**Migration**: Skills in `.<tool>/skills/` provide all OpenSpec workflow instructions. No manual file needed.
|
||||
|
||||
### Requirement: AI Tool Configuration Details
|
||||
|
||||
**Reason**: Config files (CLAUDE.md, .cursorrules, etc.) are replaced by skills.
|
||||
|
||||
**Migration**: Use skills in `.<tool>/skills/` instead of config files. Skills provide richer, tool-specific instructions.
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
|
||||
**Reason**: Old `/openspec:*` slash commands are replaced by `/opsx:*` commands with richer functionality.
|
||||
|
||||
**Migration**: Use `/opsx:new`, `/opsx:continue`, `/opsx:apply` instead of `/openspec:proposal`, `/openspec:apply`, `/openspec:archive`.
|
||||
|
||||
### Requirement: Root instruction stub
|
||||
|
||||
**Reason**: Root AGENTS.md stub is no longer needed. Skills provide tool-specific instructions.
|
||||
|
||||
**Migration**: Skills are loaded automatically by supporting tools. No root stub needed.
|
||||
@@ -0,0 +1,158 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Legacy artifact detection
|
||||
|
||||
The system SHALL detect legacy OpenSpec artifacts from previous init versions.
|
||||
|
||||
#### Scenario: Detecting legacy config files
|
||||
|
||||
- **WHEN** running `openspec init` on an existing project
|
||||
- **THEN** the system SHALL check for config files with OpenSpec markers:
|
||||
- `CLAUDE.md`
|
||||
- `.cursorrules`
|
||||
- `.windsurfrules`
|
||||
- `.clinerules`
|
||||
- `.kilocode_rules`
|
||||
- `.github/copilot-instructions.md`
|
||||
- `.amazonq/instructions.md`
|
||||
- `CODEBUDDY.md`
|
||||
- `IFLOW.md`
|
||||
- And all other tool config files from the legacy ToolRegistry
|
||||
|
||||
#### Scenario: Detecting legacy slash command directories
|
||||
|
||||
- **WHEN** running `openspec init` on an existing project
|
||||
- **THEN** the system SHALL check for old slash command directories:
|
||||
- `.claude/commands/openspec/`
|
||||
- `.cursor/commands/openspec/` (note: old format used `openspec-*.md` in commands root)
|
||||
- `.windsurf/workflows/openspec-*.md`
|
||||
- And equivalent directories for all tools in the legacy SlashCommandRegistry
|
||||
|
||||
#### Scenario: Detecting legacy OpenSpec structure files
|
||||
|
||||
- **WHEN** running `openspec init` on an existing project
|
||||
- **THEN** the system SHALL check for:
|
||||
- `openspec/AGENTS.md`
|
||||
- `openspec/project.md` (for migration messaging only, not deleted)
|
||||
- Root `AGENTS.md` with OpenSpec markers
|
||||
|
||||
### Requirement: Legacy cleanup confirmation
|
||||
|
||||
The system SHALL prompt for confirmation before removing legacy artifacts.
|
||||
|
||||
#### Scenario: Prompting for cleanup when legacy detected
|
||||
|
||||
- **WHEN** legacy artifacts are detected
|
||||
- **THEN** the system SHALL display what was found
|
||||
- **AND** prompt: "Legacy files detected. Upgrade and clean up? [Y/n]"
|
||||
- **AND** default to Yes if user presses Enter
|
||||
|
||||
#### Scenario: User confirms cleanup
|
||||
|
||||
- **WHEN** user responds Y or presses Enter
|
||||
- **THEN** the system SHALL remove legacy artifacts
|
||||
- **AND** proceed with skill-based setup
|
||||
|
||||
#### Scenario: User declines cleanup
|
||||
|
||||
- **WHEN** user responds N
|
||||
- **THEN** the system SHALL abort initialization
|
||||
- **AND** display message suggesting manual cleanup or using `--force` flag
|
||||
|
||||
#### Scenario: Non-interactive mode
|
||||
|
||||
- **WHEN** running with `--no-interactive` or in CI environment
|
||||
- **AND** legacy artifacts are detected
|
||||
- **THEN** the system SHALL abort with exit code 1
|
||||
- **AND** display detected legacy artifacts
|
||||
- **AND** suggest running interactively or using `--force` flag
|
||||
|
||||
### Requirement: Surgical removal of config file content
|
||||
|
||||
The system SHALL preserve user content when removing OpenSpec markers from config files.
|
||||
|
||||
#### Scenario: Config file with only OpenSpec content
|
||||
|
||||
- **WHEN** a config file contains only OpenSpec marker block (whitespace outside is acceptable)
|
||||
- **THEN** the system SHALL remove the OpenSpec marker block
|
||||
- **AND** preserve the file (even if empty or whitespace-only)
|
||||
- **AND** NOT delete the file (config files belong to the user's project root)
|
||||
|
||||
#### Scenario: Config file with mixed content
|
||||
|
||||
- **WHEN** a config file contains content outside OpenSpec markers
|
||||
- **THEN** the system SHALL remove only the `<!-- OPENSPEC:START -->` to `<!-- OPENSPEC:END -->` block
|
||||
- **AND** preserve all content before and after the markers
|
||||
- **AND** clean up any resulting double blank lines
|
||||
|
||||
#### Scenario: Root AGENTS.md with mixed content
|
||||
|
||||
- **WHEN** root `AGENTS.md` contains OpenSpec markers AND other content
|
||||
- **THEN** the system SHALL remove only the OpenSpec marker block
|
||||
- **AND** preserve the rest of the file
|
||||
|
||||
### Requirement: Legacy directory removal
|
||||
|
||||
The system SHALL remove legacy slash command directories entirely.
|
||||
|
||||
#### Scenario: Removing old slash command directory
|
||||
|
||||
- **WHEN** a legacy slash command directory exists (e.g., `.claude/commands/openspec/`)
|
||||
- **THEN** the system SHALL delete the entire directory and its contents
|
||||
- **AND** NOT delete the parent directory (e.g., `.claude/commands/` remains)
|
||||
|
||||
#### Scenario: Removing legacy AGENTS.md
|
||||
|
||||
- **WHEN** `openspec/AGENTS.md` exists
|
||||
- **THEN** the system SHALL delete the file
|
||||
- **AND** NOT delete the `openspec/` directory itself
|
||||
|
||||
### Requirement: project.md migration hint
|
||||
|
||||
The system SHALL preserve project.md and display a migration hint instead of deleting it.
|
||||
|
||||
#### Scenario: project.md exists during upgrade
|
||||
|
||||
- **WHEN** `openspec/project.md` exists during legacy cleanup
|
||||
- **THEN** the system SHALL NOT delete the file
|
||||
- **AND** the system SHALL display a migration hint in the output:
|
||||
```
|
||||
Manual migration needed:
|
||||
→ openspec/project.md still exists
|
||||
Move useful content to config.yaml's "context:" field, then delete
|
||||
```
|
||||
|
||||
#### Scenario: project.md migration rationale
|
||||
|
||||
- **GIVEN** project.md may contain user-written project documentation
|
||||
- **AND** config.yaml's context field serves the same purpose (auto-injected into artifacts)
|
||||
- **WHEN** displaying the migration hint
|
||||
- **THEN** users can migrate manually or use `/opsx:explore` to get AI assistance
|
||||
|
||||
### Requirement: Cleanup reporting
|
||||
|
||||
The system SHALL report what was cleaned up.
|
||||
|
||||
#### Scenario: Displaying cleanup summary
|
||||
|
||||
- **WHEN** legacy cleanup completes
|
||||
- **THEN** the system SHALL display a summary section:
|
||||
```
|
||||
Cleaned up legacy files:
|
||||
✓ Removed OpenSpec markers from CLAUDE.md
|
||||
✓ Removed .claude/commands/openspec/ (replaced by /opsx:*)
|
||||
✓ Removed openspec/AGENTS.md (no longer needed)
|
||||
```
|
||||
- **AND IF** `openspec/project.md` exists
|
||||
- **THEN** the system SHALL display a separate migration section:
|
||||
```
|
||||
Manual migration needed:
|
||||
→ openspec/project.md still exists
|
||||
Move useful content to config.yaml's "context:" field, then delete
|
||||
```
|
||||
|
||||
#### Scenario: No legacy detected
|
||||
|
||||
- **WHEN** no legacy artifacts are found
|
||||
- **THEN** the system SHALL NOT display the cleanup section
|
||||
- **AND** proceed directly with skill setup
|
||||
@@ -0,0 +1,67 @@
|
||||
## 1. Legacy Detection & Cleanup Module
|
||||
|
||||
- [x] 1.1 Create `src/core/legacy-cleanup.ts` with detection functions for all legacy artifact types
|
||||
- [x] 1.2 Implement `detectLegacyConfigFiles()` - check for config files with OpenSpec markers
|
||||
- [x] 1.3 Implement `detectLegacySlashCommands()` - check for old `/openspec:*` command directories
|
||||
- [x] 1.4 Implement `detectLegacyStructureFiles()` - check for AGENTS.md (project.md detected separately for messaging)
|
||||
- [x] 1.5 Implement `removeMarkerBlock()` - surgically remove OpenSpec marker blocks from files
|
||||
- [x] 1.6 Implement `cleanupLegacyArtifacts()` - orchestrate removal with proper edge case handling (preserves project.md)
|
||||
- [x] 1.7 Implement migration hint output for project.md - show message directing users to migrate to config.yaml
|
||||
- [x] 1.8 Add unit tests for legacy detection and cleanup functions
|
||||
|
||||
## 2. Rewrite Init Command
|
||||
|
||||
- [x] 2.1 Replace `src/core/init.ts` with new implementation using experimental's approach
|
||||
- [x] 2.2 Import and use animated welcome screen from `src/ui/welcome-screen.ts`
|
||||
- [x] 2.3 Import and use searchable multi-select from `src/prompts/searchable-multi-select.ts`
|
||||
- [x] 2.4 Integrate legacy detection at start of init flow
|
||||
- [x] 2.5 Add Y/N prompt for legacy cleanup confirmation
|
||||
- [x] 2.6 Generate skills using existing `skill-templates.ts`
|
||||
- [x] 2.7 Generate slash commands using existing `command-generation/` adapters
|
||||
- [x] 2.8 Create `openspec/config.yaml` with default schema
|
||||
- [x] 2.9 Update success output to match new workflow (skills, /opsx:* commands)
|
||||
- [x] 2.10 Add `--force` flag to skip legacy cleanup prompt in non-interactive mode
|
||||
|
||||
## 3. Remove Legacy Code
|
||||
|
||||
- [x] 3.1 Delete `src/core/configurators/` directory (ToolRegistry, all config generators)
|
||||
- [x] 3.2 Delete `src/core/templates/slash-command-templates.ts`
|
||||
- [x] 3.3 Delete `src/core/templates/claude-template.ts`
|
||||
- [x] 3.4 Delete `src/core/templates/cline-template.ts`
|
||||
- [x] 3.5 Delete `src/core/templates/costrict-template.ts`
|
||||
- [x] 3.6 Delete `src/core/templates/agents-template.ts`
|
||||
- [x] 3.7 Delete `src/core/templates/agents-root-stub.ts`
|
||||
- [x] 3.8 Delete `src/core/templates/project-template.ts`
|
||||
- [x] 3.9 Delete `src/commands/experimental/` directory
|
||||
- [x] 3.10 Update `src/core/templates/index.ts` to remove deleted exports
|
||||
- [x] 3.11 Delete related test files for removed modules (wizard.ts)
|
||||
|
||||
## 4. Update CLI Registration
|
||||
|
||||
- [x] 4.1 Update `src/cli/index.ts` to remove `registerArtifactWorkflowCommands()` call
|
||||
- [x] 4.2 Keep experimental subcommands (status, instructions, schemas, etc.) but register directly
|
||||
- [x] 4.3 Remove "[Experimental]" labels from kept subcommands
|
||||
- [x] 4.4 Add hidden `experimental` command as alias to `init`
|
||||
|
||||
## 5. Update Related Commands
|
||||
|
||||
- [x] 5.1 Update `openspec update` command to refresh skills/commands instead of config files
|
||||
- [x] 5.2 Remove config file refresh logic from update
|
||||
- [x] 5.3 Add skill refresh logic to update
|
||||
|
||||
## 6. Testing & Verification
|
||||
|
||||
- [x] 6.1 Add integration tests for new init flow (fresh install)
|
||||
- [x] 6.2 Add integration tests for legacy detection and cleanup
|
||||
- [x] 6.3 Add integration tests for extend mode (re-running init)
|
||||
- [x] 6.4 Test non-interactive mode with `--tools` flag
|
||||
- [x] 6.5 Test `--force` flag for CI environments
|
||||
- [x] 6.6 Verify cross-platform path handling (use path.join throughout)
|
||||
- [x] 6.7 Run full test suite and fix any broken tests
|
||||
|
||||
## 7. Documentation & Cleanup
|
||||
|
||||
- [x] 7.1 Update README with new init behavior (skill-based workflow is self-documenting)
|
||||
- [x] 7.2 Document breaking changes for release notes (in tasks file)
|
||||
- [x] 7.3 Remove any orphaned imports/references to deleted modules (verified none exist)
|
||||
- [x] 7.4 Run linter and fix any issues (passed)
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-22
|
||||
@@ -0,0 +1,144 @@
|
||||
## Context
|
||||
|
||||
The `artifact-experimental-setup` command generates skill files and opsx slash commands for AI coding assistants. Currently it hardcodes paths to `.claude/skills` and `.claude/commands/opsx`.
|
||||
|
||||
The existing `AI_TOOLS` array in `config.ts` lists 22 AI tools but lacks path information. There's also an existing `SlashCommandConfigurator` system for the old workflow commands, but it's tightly coupled to the old 3 commands (proposal, apply, archive) and can't be easily extended for the 9 opsx commands.
|
||||
|
||||
Each AI tool has:
|
||||
- Different skill directory conventions (`.claude/skills/`, `.cursor/skills/`, etc.)
|
||||
- Different command file paths (`.claude/commands/opsx/`, `.cursor/commands/`, etc.)
|
||||
- Different frontmatter formats (YAML keys, structure varies by tool)
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Support skill generation for any AI tool following the Agent Skills spec
|
||||
- Support command generation with tool-specific formatting via adapters
|
||||
- Require explicit tool selection (no defaults)
|
||||
- Create a generic, extensible command generation system
|
||||
|
||||
**Non-Goals:**
|
||||
- Global path installation (deferred to future work)
|
||||
- Multi-tool generation in single command (future enhancement)
|
||||
- Unifying with existing SlashCommandConfigurator (separate systems for now)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Add `skillsDir` to `AIToolOption` interface
|
||||
|
||||
**Decision**: Add single `skillsDir` field to existing interface. No `commandsDir` or `globalSkillsDir`.
|
||||
|
||||
```typescript
|
||||
interface AIToolOption {
|
||||
name: string;
|
||||
value: string;
|
||||
available: boolean;
|
||||
successLabel?: string;
|
||||
skillsDir?: string; // e.g., '.claude' - /skills suffix per Agent Skills spec
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
- Skills follow Agent Skills spec: `<toolDir>/skills/` - suffix is standard
|
||||
- Commands need per-tool formatting, handled by adapters (not a simple path)
|
||||
- Global paths deferred - can extend interface later
|
||||
|
||||
### 2. Strategy/Adapter pattern for command generation
|
||||
|
||||
**Decision**: Create generic command generation with tool-specific adapters.
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CommandContent │
|
||||
│ (tool-agnostic: id, name, description, category, tags, body) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ generateCommand(content, adapter) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Claude │ │ Cursor │ │ Windsurf │
|
||||
│ Adapter │ │ Adapter │ │ Adapter │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
```typescript
|
||||
// Tool-agnostic command data
|
||||
interface CommandContent {
|
||||
id: string; // e.g., 'explore', 'new', 'apply'
|
||||
name: string; // e.g., 'OpenSpec Explore'
|
||||
description: string; // e.g., 'Enter explore mode...'
|
||||
category: string; // e.g., 'OpenSpec'
|
||||
tags: string[]; // e.g., ['openspec', 'explore']
|
||||
body: string; // The command instructions
|
||||
}
|
||||
|
||||
// Per-tool formatting strategy
|
||||
interface ToolCommandAdapter {
|
||||
toolId: string;
|
||||
getFilePath(commandId: string): string;
|
||||
formatFile(content: CommandContent): string;
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
- Separates "what to generate" from "how to format it"
|
||||
- Each tool's frontmatter quirks encapsulated in its adapter
|
||||
- Easy to add new tools by implementing adapter interface
|
||||
- Body content shared across all tools
|
||||
|
||||
**Alternative considered**: Extend existing SlashCommandConfigurator
|
||||
- Rejected: Tightly coupled to old 3 commands, significant refactor needed
|
||||
|
||||
### 3. Adapter registry pattern
|
||||
|
||||
**Decision**: Create `CommandAdapterRegistry` similar to existing `SlashCommandRegistry`.
|
||||
|
||||
```typescript
|
||||
class CommandAdapterRegistry {
|
||||
private static adapters: Map<string, ToolCommandAdapter> = new Map();
|
||||
|
||||
static get(toolId: string): ToolCommandAdapter | undefined;
|
||||
static getAll(): ToolCommandAdapter[];
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
- Consistent with existing codebase patterns
|
||||
- Easy lookup by tool ID
|
||||
- Centralized registration
|
||||
|
||||
### 4. Required tool flag
|
||||
|
||||
**Decision**: Require `--tool` flag - error if omitted.
|
||||
|
||||
**Rationale**:
|
||||
- Explicit tool selection avoids assumptions
|
||||
- Consistent with project convention of not providing defaults
|
||||
- Users must consciously choose their target tool
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk] Adapter maintenance burden** → Each new tool needs an adapter. Mitigated by simple interface - most adapters are ~20 lines.
|
||||
|
||||
**[Risk] Frontmatter format drift** → Tools may change their formats. Mitigated by encapsulating format in adapter - single place to update.
|
||||
|
||||
**[Trade-off] Two command systems** → Old SlashCommandConfigurator and new CommandAdapterRegistry coexist. Acceptable for now - can unify later if needed.
|
||||
|
||||
**[Trade-off] skillsDir optional** → Tools without skillsDir configured will error. Acceptable - we add paths as tools are tested.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
1. Add `skillsDir` to `AIToolOption` and populate for known tools
|
||||
2. Create `CommandContent` and `ToolCommandAdapter` interfaces
|
||||
3. Implement adapters for Claude, Cursor, Windsurf (start with 3)
|
||||
4. Create `CommandAdapterRegistry`
|
||||
5. Create `generateCommand()` function
|
||||
6. Update `artifact-experimental-setup` to use new system
|
||||
7. Add `--tool` flag with validation
|
||||
@@ -0,0 +1,36 @@
|
||||
## Why
|
||||
|
||||
The `artifact-experimental-setup` command currently hardcodes skill output paths to `.claude/skills` and `.claude/commands/opsx`. This prevents users of other AI coding tools (Cursor, Windsurf, Codex, etc.) from using OpenSpec's skill generation. We need to support the diverse ecosystem of AI coding assistants, each with their own conventions for skill/instruction file locations and command frontmatter formats.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add `skillsDir` path configuration to the existing `AIToolOption` interface in `config.ts`
|
||||
- Add required `--tool <tool-id>` flag to the `artifact-experimental-setup` command
|
||||
- Create a generic command generation system using Strategy/Adapter pattern:
|
||||
- `CommandContent`: tool-agnostic command data (id, name, description, body)
|
||||
- `ToolCommandAdapter`: per-tool formatting (file paths, frontmatter format)
|
||||
- `CommandGenerator`: orchestrates generation using content + adapter
|
||||
- Require explicit tool selection (no default) for clarity
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `ai-tool-paths`: Configuration mapping AI tool IDs to their project-local skill directory paths
|
||||
- `command-generation`: Generic command generation system with tool adapters for formatting differences
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `cli-artifact-workflow`: Adding `--tool` flag to setup command for provider selection
|
||||
|
||||
## Impact
|
||||
|
||||
- **Files Modified**:
|
||||
- `src/core/config.ts` - Extend `AIToolOption` interface with `skillsDir` field
|
||||
- `src/commands/artifact-workflow.ts` - Add `--tool` flag, use provider paths and adapters
|
||||
- **New Files**:
|
||||
- `src/core/command-generation/types.ts` - CommandContent, ToolCommandAdapter interfaces
|
||||
- `src/core/command-generation/generator.ts` - Generic command generator
|
||||
- `src/core/command-generation/adapters/*.ts` - Per-tool adapters
|
||||
- **Backward Compatibility**: Existing workflows unaffected - this is a new command setup feature
|
||||
- **User-Facing**: Required `--tool` flag on `artifact-experimental-setup` command for explicit tool selection
|
||||
@@ -0,0 +1,63 @@
|
||||
# ai-tool-paths Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the path configuration for AI coding tool skill directories, enabling skill generation to target different tools following the Agent Skills spec.
|
||||
|
||||
## Requirements
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: AIToolOption skillsDir field
|
||||
|
||||
The `AIToolOption` interface SHALL include an optional `skillsDir` field for skill generation path configuration.
|
||||
|
||||
#### Scenario: Interface includes skillsDir field
|
||||
|
||||
- **WHEN** a tool entry is defined in `AI_TOOLS` that supports skill generation
|
||||
- **THEN** it SHALL include a `skillsDir` field specifying the project-local base directory (e.g., `.claude`)
|
||||
|
||||
#### Scenario: Skills path follows Agent Skills spec
|
||||
|
||||
- **WHEN** generating skills for a tool with `skillsDir: '.claude'`
|
||||
- **THEN** skills SHALL be written to `<projectRoot>/<skillsDir>/skills/`
|
||||
- **AND** the `/skills` suffix is appended per Agent Skills specification
|
||||
|
||||
### Requirement: Path configuration for supported tools
|
||||
|
||||
The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent Skills specification.
|
||||
|
||||
#### Scenario: Claude Code paths defined
|
||||
|
||||
- **WHEN** looking up the `claude` tool
|
||||
- **THEN** `skillsDir` SHALL be `.claude`
|
||||
|
||||
#### Scenario: Cursor paths defined
|
||||
|
||||
- **WHEN** looking up the `cursor` tool
|
||||
- **THEN** `skillsDir` SHALL be `.cursor`
|
||||
|
||||
#### Scenario: Windsurf paths defined
|
||||
|
||||
- **WHEN** looking up the `windsurf` tool
|
||||
- **THEN** `skillsDir` SHALL be `.windsurf`
|
||||
|
||||
#### Scenario: Tools without skillsDir
|
||||
|
||||
- **WHEN** a tool has no `skillsDir` defined
|
||||
- **THEN** skill generation SHALL error with message indicating the tool is not supported
|
||||
|
||||
### Requirement: Cross-platform path handling
|
||||
|
||||
The system SHALL handle paths correctly across operating systems.
|
||||
|
||||
#### Scenario: Path construction on Windows
|
||||
|
||||
- **WHEN** constructing skill paths on Windows
|
||||
- **THEN** the system SHALL use `path.join()` for all path construction
|
||||
- **AND** SHALL NOT hardcode forward slashes
|
||||
|
||||
#### Scenario: Path construction on Unix
|
||||
|
||||
- **WHEN** constructing skill paths on macOS or Linux
|
||||
- **THEN** the system SHALL use `path.join()` for consistency
|
||||
@@ -0,0 +1,60 @@
|
||||
# cli-artifact-workflow Delta Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Add `--tool` flag to the `artifact-experimental-setup` command for multi-provider support.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Tool selection flag
|
||||
|
||||
The `artifact-experimental-setup` command SHALL accept a `--tool <tool-id>` flag to specify the target AI tool.
|
||||
|
||||
#### Scenario: Specify tool via flag
|
||||
|
||||
- **WHEN** user runs `openspec artifact-experimental-setup --tool cursor`
|
||||
- **THEN** skill files are generated in `.cursor/skills/`
|
||||
- **AND** command files are generated using Cursor's frontmatter format
|
||||
|
||||
#### Scenario: Missing tool flag
|
||||
|
||||
- **WHEN** user runs `openspec artifact-experimental-setup` without `--tool`
|
||||
- **THEN** the system displays an error requiring the `--tool` flag
|
||||
- **AND** lists valid tool IDs in the error message
|
||||
|
||||
#### Scenario: Unknown tool ID
|
||||
|
||||
- **WHEN** user runs `openspec artifact-experimental-setup --tool unknown-tool`
|
||||
- **AND** the tool ID is not in `AI_TOOLS`
|
||||
- **THEN** the system displays an error listing valid tool IDs
|
||||
|
||||
#### Scenario: Tool without skillsDir
|
||||
|
||||
- **WHEN** user specifies a tool that has no `skillsDir` configured
|
||||
- **THEN** the system displays an error indicating skill generation is not supported for that tool
|
||||
|
||||
#### Scenario: Tool without command adapter
|
||||
|
||||
- **WHEN** user specifies a tool that has `skillsDir` but no command adapter registered
|
||||
- **THEN** skill files are generated successfully
|
||||
- **AND** command generation is skipped with informational message
|
||||
|
||||
### Requirement: Output messaging
|
||||
|
||||
The setup command SHALL display clear output about what was generated.
|
||||
|
||||
#### Scenario: Show target tool in output
|
||||
|
||||
- **WHEN** setup command runs successfully
|
||||
- **THEN** output includes the target tool name (e.g., "Setting up for Cursor...")
|
||||
|
||||
#### Scenario: Show generated paths
|
||||
|
||||
- **WHEN** setup command completes
|
||||
- **THEN** output lists all generated skill file paths
|
||||
- **AND** lists all generated command file paths (if applicable)
|
||||
|
||||
#### Scenario: Show skipped commands message
|
||||
|
||||
- **WHEN** command generation is skipped due to missing adapter
|
||||
- **THEN** output includes message: "Command generation skipped - no adapter for <tool>"
|
||||
@@ -0,0 +1,98 @@
|
||||
# command-generation Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Define a generic command generation system that supports multiple AI tools through a Strategy/Adapter pattern, separating command content from tool-specific formatting.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: CommandContent interface
|
||||
|
||||
The system SHALL define a tool-agnostic `CommandContent` interface for command data.
|
||||
|
||||
#### Scenario: CommandContent structure
|
||||
|
||||
- **WHEN** defining a command to generate
|
||||
- **THEN** `CommandContent` SHALL include:
|
||||
- `id`: string identifier (e.g., 'explore', 'apply')
|
||||
- `name`: human-readable name (e.g., 'OpenSpec Explore')
|
||||
- `description`: brief description of command purpose
|
||||
- `category`: grouping category (e.g., 'OpenSpec')
|
||||
- `tags`: array of tag strings
|
||||
- `body`: the command instruction content
|
||||
|
||||
### 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 relative file path for command
|
||||
- `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/commands/opsx/<id>.md`
|
||||
|
||||
### Requirement: Command generator function
|
||||
|
||||
The system SHALL provide a `generateCommand` function that combines content with adapter.
|
||||
|
||||
#### Scenario: Generate command file
|
||||
|
||||
- **WHEN** calling `generateCommand(content, adapter)`
|
||||
- **THEN** it SHALL return an object with:
|
||||
- `path`: the file path from `adapter.getFilePath(content.id)`
|
||||
- `fileContent`: the formatted content from `adapter.formatFile(content)`
|
||||
|
||||
#### Scenario: Generate multiple commands
|
||||
|
||||
- **WHEN** generating all opsx commands for a tool
|
||||
- **THEN** the system SHALL iterate over command contents and generate each using the tool's adapter
|
||||
|
||||
### Requirement: CommandAdapterRegistry
|
||||
|
||||
The system SHALL provide a registry for looking up tool adapters.
|
||||
|
||||
#### Scenario: Get adapter by tool ID
|
||||
|
||||
- **WHEN** calling `CommandAdapterRegistry.get('cursor')`
|
||||
- **THEN** it SHALL return the Cursor adapter or undefined if not registered
|
||||
|
||||
#### Scenario: Get all adapters
|
||||
|
||||
- **WHEN** calling `CommandAdapterRegistry.getAll()`
|
||||
- **THEN** it SHALL return array of all registered adapters
|
||||
|
||||
#### Scenario: Adapter not found
|
||||
|
||||
- **WHEN** looking up an adapter for unregistered tool
|
||||
- **THEN** `CommandAdapterRegistry.get()` SHALL return undefined
|
||||
- **AND** caller SHALL handle missing adapter appropriately
|
||||
|
||||
### Requirement: Shared command body content
|
||||
|
||||
The body content of commands SHALL be shared across all tools.
|
||||
|
||||
#### Scenario: Same instructions across tools
|
||||
|
||||
- **WHEN** generating the 'explore' command for Claude and Cursor
|
||||
- **THEN** both SHALL use the same `body` content
|
||||
- **AND** only the frontmatter and file path SHALL differ
|
||||
@@ -0,0 +1,55 @@
|
||||
## 1. Extend AIToolOption Interface
|
||||
|
||||
- [x] 1.1 Add `skillsDir?: string` field to `AIToolOption` interface in `src/core/config.ts`
|
||||
|
||||
## 2. Add skillsDir to AI_TOOLS
|
||||
|
||||
- [x] 2.1 Add `skillsDir: '.claude'` to Claude Code tool entry
|
||||
- [x] 2.2 Add `skillsDir: '.cursor'` to Cursor tool entry
|
||||
- [x] 2.3 Add `skillsDir: '.windsurf'` to Windsurf tool entry
|
||||
- [x] 2.4 Add skillsDir for other tools with known Agent Skills spec support (codex, opencode, roocode, kilocode, gemini, factory, github-copilot)
|
||||
|
||||
## 3. Create Command Generation Types
|
||||
|
||||
- [x] 3.1 Create `src/core/command-generation/types.ts` with `CommandContent` interface
|
||||
- [x] 3.2 Add `ToolCommandAdapter` interface to types.ts
|
||||
- [x] 3.3 Export types from module index
|
||||
|
||||
## 4. Implement Tool Command Adapters
|
||||
|
||||
- [x] 4.1 Create `src/core/command-generation/adapters/claude.ts` with Claude frontmatter format
|
||||
- [x] 4.2 Create `src/core/command-generation/adapters/cursor.ts` with Cursor frontmatter format
|
||||
- [x] 4.3 Create `src/core/command-generation/adapters/windsurf.ts` with Windsurf frontmatter format
|
||||
- [x] 4.4 Create base adapter or utility for shared YAML formatting logic (if applicable)
|
||||
|
||||
## 5. Create Command Adapter Registry
|
||||
|
||||
- [x] 5.1 Create `src/core/command-generation/registry.ts` with `CommandAdapterRegistry` class
|
||||
- [x] 5.2 Register Claude, Cursor, Windsurf adapters in static initializer
|
||||
- [x] 5.3 Add `get(toolId)` and `getAll()` methods
|
||||
|
||||
## 6. Create Command Generator
|
||||
|
||||
- [x] 6.1 Create `src/core/command-generation/generator.ts` with `generateCommand()` function
|
||||
- [x] 6.2 Add `generateCommands()` function for batch generation
|
||||
- [x] 6.3 Create module index `src/core/command-generation/index.ts` exporting public API
|
||||
|
||||
## 7. Update artifact-experimental-setup Command
|
||||
|
||||
- [x] 7.1 Add `--tool <tool-id>` option (required) to command in `src/commands/artifact-workflow.ts`
|
||||
- [x] 7.2 Add validation: `--tool` flag is required (error if missing with list of valid tools)
|
||||
- [x] 7.3 Add validation: tool exists in AI_TOOLS
|
||||
- [x] 7.4 Add validation: tool has skillsDir configured
|
||||
- [x] 7.5 Replace hardcoded `.claude` skill paths with `tool.skillsDir`
|
||||
- [x] 7.6 Replace hardcoded command generation with `CommandAdapterRegistry.get()` + `generateCommands()`
|
||||
- [x] 7.7 Handle missing adapter gracefully (skip commands with message)
|
||||
- [x] 7.8 Update output messages to show target tool name and paths
|
||||
|
||||
## 8. Testing
|
||||
|
||||
- [x] 8.1 Add unit tests for `CommandContent` and `ToolCommandAdapter` contracts
|
||||
- [x] 8.2 Add unit tests for Claude adapter (path + frontmatter format)
|
||||
- [x] 8.3 Add unit tests for Cursor adapter (path + frontmatter format)
|
||||
- [x] 8.4 Add unit tests for `CommandAdapterRegistry.get()` and missing adapter case
|
||||
- [x] 8.5 Add integration test for `--tool` flag validation
|
||||
- [x] 8.6 Verify cross-platform path handling uses `path.join()` throughout
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: "2025-01-13"
|
||||
@@ -0,0 +1,665 @@
|
||||
# Design: Project Config
|
||||
|
||||
## Context
|
||||
|
||||
OpenSpec currently has a fixed schema resolution order:
|
||||
1. `--schema` CLI flag
|
||||
2. `.openspec.yaml` in change directory
|
||||
3. Hardcoded default: `"spec-driven"`
|
||||
|
||||
This forces users who want project-level customization to fork entire schemas, even for simple additions like injecting tech stack context or adding artifact-specific rules.
|
||||
|
||||
The proposal introduces `openspec/config.yaml` as a lightweight customization layer that sits between preset schemas and full forking. It allows teams to:
|
||||
- Set a default schema
|
||||
- Inject project context into all artifacts
|
||||
- Add per-artifact rules
|
||||
|
||||
**Constraints:**
|
||||
- Must not break existing changes that lack config
|
||||
- Must maintain clean separation between "configure" (this) and "fork" (project-local-schemas)
|
||||
- Config is project-level only (no global/user-level config)
|
||||
|
||||
**Key stakeholders:**
|
||||
- OpenSpec users who need light customization without forking
|
||||
- Teams sharing workflow conventions via committed config
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Load and parse `openspec/config.yaml` using Zod schema
|
||||
- Use config's `schema` field as default in schema resolution
|
||||
- Inject `context` into all artifact instructions
|
||||
- Inject `rules` into matching artifact instructions only
|
||||
- Gracefully handle missing or invalid config (fallback to defaults)
|
||||
|
||||
**Non-Goals:**
|
||||
- Structural changes to schemas (`skip`, `add`, inheritance) - those belong in fork path
|
||||
- File references for context (`context: ./file.md`) - start with strings
|
||||
- Global user-level config (XDG dirs, etc.)
|
||||
- Config management commands (`openspec config init`) - manual creation for now
|
||||
- Migration from old setups (no existing config to migrate from)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Config File Format: YAML vs JSON
|
||||
|
||||
**Decision:** Use YAML (`.yaml` extension, support `.yml` alias)
|
||||
|
||||
**Rationale:**
|
||||
- YAML supports multi-line strings naturally (`context: |`)
|
||||
- More readable for documentation-heavy content
|
||||
- Consistent with `.openspec.yaml` used in changes
|
||||
- Easy to parse with existing `yaml` library
|
||||
|
||||
**Alternatives considered:**
|
||||
- JSON: More strict, but poor multi-line string UX
|
||||
- TOML: Less familiar to most users
|
||||
|
||||
### 2. Config Location: Project Root vs openspec/ Directory
|
||||
|
||||
**Decision:** `./openspec/config.yaml` (inside openspec directory)
|
||||
|
||||
**Rationale:**
|
||||
- Co-located with `openspec/schemas/` (project-local-schemas)
|
||||
- Keeps project root clean
|
||||
- Natural namespace for OpenSpec configuration
|
||||
- Mirrors structure used by other tools (e.g., `.github/`)
|
||||
|
||||
**Alternatives considered:**
|
||||
- `./openspec.config.yaml` in root: Pollutes root, less clear ownership
|
||||
- XDG config directories: Out of scope, no global config yet
|
||||
|
||||
### 3. Context Injection: XML Tags vs Markdown Sections
|
||||
|
||||
**Decision:** Use XML-style tags `<context>` and `<rules>`
|
||||
|
||||
**Rationale:**
|
||||
- Clear delimiters that don't conflict with Markdown
|
||||
- Agents can easily parse structure
|
||||
- Matches existing patterns in the codebase for special sections
|
||||
|
||||
**Example:**
|
||||
```xml
|
||||
<context>
|
||||
Tech stack: TypeScript, React
|
||||
</context>
|
||||
|
||||
<rules>
|
||||
- Include rollback plan
|
||||
</rules>
|
||||
|
||||
<template>
|
||||
## Summary
|
||||
...
|
||||
</template>
|
||||
```
|
||||
|
||||
**Alternatives considered:**
|
||||
- Markdown headers: Conflicts with template content
|
||||
- Comments: Less visible to agents
|
||||
|
||||
### 4. Schema Resolution: Insert Position
|
||||
|
||||
**Decision:** Config's `schema` field goes between change metadata and hardcoded default
|
||||
|
||||
**New resolution order:**
|
||||
1. `--schema` CLI flag (explicit override)
|
||||
2. `.openspec.yaml` in change directory (change-specific binding)
|
||||
3. **`openspec/config.yaml` schema field** (NEW - project default)
|
||||
4. `"spec-driven"` (hardcoded fallback)
|
||||
|
||||
**Rationale:**
|
||||
- Preserves CLI and change-level overrides (most specific wins)
|
||||
- Makes config act as a "project default"
|
||||
- Backwards compatible (no existing configs to conflict with)
|
||||
|
||||
### 5. Rules Validation: Strict vs Permissive
|
||||
|
||||
**Decision:** Warn on unknown artifact IDs, don't error
|
||||
|
||||
**Rationale:**
|
||||
- Future-proof: If schema adds new artifacts, old configs don't break
|
||||
- Dev experience: Typos show warnings, but don't halt workflow
|
||||
- User can fix incrementally
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
rules:
|
||||
proposal: [...]
|
||||
testplan: [...] # Schema doesn't have this artifact → WARN, not ERROR
|
||||
```
|
||||
|
||||
### 6. Error Handling: Config Parse Failures
|
||||
|
||||
**Decision:** Log warning and fall back to defaults (don't halt commands)
|
||||
|
||||
**Rationale:**
|
||||
- Syntax errors in config shouldn't break all of OpenSpec
|
||||
- User can fix config incrementally
|
||||
- Commands remain usable during config development
|
||||
|
||||
**Warning message:**
|
||||
```
|
||||
⚠️ Failed to parse openspec/config.yaml: [error details]
|
||||
Falling back to default schema (spec-driven)
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Core Types and Loading
|
||||
|
||||
**File: `src/core/project-config.ts` (NEW)**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { findProjectRoot } from '../utils/path-utils';
|
||||
|
||||
/**
|
||||
* Zod schema for project configuration.
|
||||
*
|
||||
* Purpose:
|
||||
* 1. Documentation - clearly defines the config file structure
|
||||
* 2. Type safety - TypeScript infers ProjectConfig type from schema
|
||||
* 3. Runtime validation - uses safeParse() for resilient field-by-field validation
|
||||
*
|
||||
* Why Zod over manual validation:
|
||||
* - Helps understand OpenSpec's data interfaces at a glance
|
||||
* - Single source of truth for type and validation
|
||||
* - Consistent with other OpenSpec schemas
|
||||
*/
|
||||
export const ProjectConfigSchema = z.object({
|
||||
schema: z.string().min(1).describe('The workflow schema to use (e.g., "spec-driven", "tdd")'),
|
||||
context: z.string().optional().describe('Project context injected into all artifact instructions'),
|
||||
rules: z.record(
|
||||
z.string(),
|
||||
z.array(z.string())
|
||||
).optional().describe('Per-artifact rules, keyed by artifact ID'),
|
||||
});
|
||||
|
||||
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
||||
|
||||
const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit
|
||||
|
||||
/**
|
||||
* Read and parse openspec/config.yaml from project root.
|
||||
* Uses resilient parsing - validates each field independently using Zod safeParse.
|
||||
* Returns null if file doesn't exist.
|
||||
* Returns partial config if some fields are invalid (with warnings).
|
||||
*/
|
||||
export function readProjectConfig(): ProjectConfig | null {
|
||||
const projectRoot = findProjectRoot();
|
||||
|
||||
// Try both .yaml and .yml, prefer .yaml
|
||||
let configPath = path.join(projectRoot, 'openspec', 'config.yaml');
|
||||
if (!existsSync(configPath)) {
|
||||
configPath = path.join(projectRoot, 'openspec', 'config.yml');
|
||||
if (!existsSync(configPath)) {
|
||||
return null; // No config is OK
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(configPath, 'utf-8');
|
||||
const raw = parseYaml(content);
|
||||
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
console.warn(`⚠️ openspec/config.yaml is not a valid YAML object`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
|
||||
// Parse schema field using Zod
|
||||
const schemaField = z.string().min(1);
|
||||
const schemaResult = schemaField.safeParse(raw.schema);
|
||||
if (schemaResult.success) {
|
||||
config.schema = schemaResult.data;
|
||||
} else if (raw.schema !== undefined) {
|
||||
console.warn(`⚠️ Invalid 'schema' field in config (must be non-empty string)`);
|
||||
}
|
||||
|
||||
// Parse context field with size limit
|
||||
if (raw.context !== undefined) {
|
||||
const contextField = z.string();
|
||||
const contextResult = contextField.safeParse(raw.context);
|
||||
|
||||
if (contextResult.success) {
|
||||
const contextSize = Buffer.byteLength(contextResult.data, 'utf-8');
|
||||
if (contextSize > MAX_CONTEXT_SIZE) {
|
||||
console.warn(
|
||||
`⚠️ Context too large (${(contextSize / 1024).toFixed(1)}KB, limit: ${MAX_CONTEXT_SIZE / 1024}KB)`
|
||||
);
|
||||
console.warn(` Ignoring context field`);
|
||||
} else {
|
||||
config.context = contextResult.data;
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ Invalid 'context' field in config (must be string)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse rules field using Zod
|
||||
if (raw.rules !== undefined) {
|
||||
const rulesField = z.record(z.string(), z.array(z.string()));
|
||||
|
||||
// First check if it's an object structure
|
||||
if (typeof raw.rules === 'object' && !Array.isArray(raw.rules)) {
|
||||
const parsedRules: Record<string, string[]> = {};
|
||||
let hasValidRules = false;
|
||||
|
||||
for (const [artifactId, rules] of Object.entries(raw.rules)) {
|
||||
const rulesArrayResult = z.array(z.string()).safeParse(rules);
|
||||
|
||||
if (rulesArrayResult.success) {
|
||||
// Filter out empty strings
|
||||
const validRules = rulesArrayResult.data.filter(r => r.length > 0);
|
||||
if (validRules.length > 0) {
|
||||
parsedRules[artifactId] = validRules;
|
||||
hasValidRules = true;
|
||||
}
|
||||
if (validRules.length < rulesArrayResult.data.length) {
|
||||
console.warn(
|
||||
`⚠️ Some rules for '${artifactId}' are empty strings, ignoring them`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`⚠️ Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidRules) {
|
||||
config.rules = parsedRules;
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ Invalid 'rules' field in config (must be object)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Return partial config even if some fields failed
|
||||
return Object.keys(config).length > 0 ? (config as ProjectConfig) : null;
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to parse openspec/config.yaml:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate artifact IDs in rules against a schema's artifacts.
|
||||
* Called during instruction loading (when schema is known).
|
||||
* Returns warnings for unknown artifact IDs.
|
||||
*/
|
||||
export function validateConfigRules(
|
||||
rules: Record<string, string[]>,
|
||||
validArtifactIds: Set<string>,
|
||||
schemaName: string
|
||||
): string[] {
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const artifactId of Object.keys(rules)) {
|
||||
if (!validArtifactIds.has(artifactId)) {
|
||||
const validIds = Array.from(validArtifactIds).sort().join(', ');
|
||||
warnings.push(
|
||||
`Unknown artifact ID in rules: "${artifactId}". ` +
|
||||
`Valid IDs for schema "${schemaName}": ${validIds}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest valid schema names when user provides invalid schema.
|
||||
* Uses fuzzy matching to find similar names.
|
||||
*/
|
||||
export function suggestSchemas(
|
||||
invalidSchemaName: string,
|
||||
availableSchemas: { name: string; isBuiltIn: boolean }[]
|
||||
): string {
|
||||
// Simple fuzzy match: Levenshtein distance
|
||||
function levenshtein(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
for (let i = 0; i <= b.length; i++) {
|
||||
matrix[i] = [i];
|
||||
}
|
||||
for (let j = 0; j <= a.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
for (let i = 1; i <= b.length; i++) {
|
||||
for (let j = 1; j <= a.length; j++) {
|
||||
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
||||
matrix[i][j] = matrix[i - 1][j - 1];
|
||||
} else {
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j - 1] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j] + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matrix[b.length][a.length];
|
||||
}
|
||||
|
||||
// Find closest matches (distance <= 3)
|
||||
const suggestions = availableSchemas
|
||||
.map(s => ({ ...s, distance: levenshtein(invalidSchemaName, s.name) }))
|
||||
.filter(s => s.distance <= 3)
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
.slice(0, 3);
|
||||
|
||||
const builtIn = availableSchemas.filter(s => s.isBuiltIn).map(s => s.name);
|
||||
const projectLocal = availableSchemas.filter(s => !s.isBuiltIn).map(s => s.name);
|
||||
|
||||
let message = `❌ Schema '${invalidSchemaName}' not found in openspec/config.yaml\n\n`;
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
message += `Did you mean one of these?\n`;
|
||||
suggestions.forEach(s => {
|
||||
const type = s.isBuiltIn ? 'built-in' : 'project-local';
|
||||
message += ` - ${s.name} (${type})\n`;
|
||||
});
|
||||
message += '\n';
|
||||
}
|
||||
|
||||
message += `Available schemas:\n`;
|
||||
if (builtIn.length > 0) {
|
||||
message += ` Built-in: ${builtIn.join(', ')}\n`;
|
||||
}
|
||||
if (projectLocal.length > 0) {
|
||||
message += ` Project-local: ${projectLocal.join(', ')}\n`;
|
||||
} else {
|
||||
message += ` Project-local: (none found)\n`;
|
||||
}
|
||||
|
||||
message += `\nFix: Edit openspec/config.yaml and change 'schema: ${invalidSchemaName}' to a valid schema name`;
|
||||
|
||||
return message;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Schema Resolution
|
||||
|
||||
**File: `src/utils/change-metadata.ts`**
|
||||
|
||||
Update `resolveSchemaForChange()` to check config:
|
||||
|
||||
```typescript
|
||||
export function resolveSchemaForChange(
|
||||
changeName: string,
|
||||
cliSchema?: string
|
||||
): string {
|
||||
// 1. CLI flag wins
|
||||
if (cliSchema) {
|
||||
return cliSchema;
|
||||
}
|
||||
|
||||
// 2. Change metadata (.openspec.yaml)
|
||||
const metadata = readChangeMetadata(changeName);
|
||||
if (metadata?.schema) {
|
||||
return metadata.schema;
|
||||
}
|
||||
|
||||
// 3. Project config (NEW)
|
||||
const projectConfig = readProjectConfig();
|
||||
if (projectConfig?.schema) {
|
||||
return projectConfig.schema;
|
||||
}
|
||||
|
||||
// 4. Hardcoded default
|
||||
return 'spec-driven';
|
||||
}
|
||||
```
|
||||
|
||||
**File: `src/utils/change-utils.ts`**
|
||||
|
||||
Update `createNewChange()` to use config schema:
|
||||
|
||||
```typescript
|
||||
export function createNewChange(
|
||||
changeName: string,
|
||||
schema?: string
|
||||
): void {
|
||||
// Use schema from config if not specified
|
||||
const resolvedSchema = schema ?? readProjectConfig()?.schema ?? 'spec-driven';
|
||||
|
||||
// ... rest of change creation logic
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Instruction Injection and Validation
|
||||
|
||||
**File: `src/core/artifact-graph/instruction-loader.ts`**
|
||||
|
||||
Update `loadInstructions()` to inject context, rules, and validate artifact IDs:
|
||||
|
||||
```typescript
|
||||
// Session-level cache for validation warnings (avoid repeating same warnings)
|
||||
const shownWarnings = new Set<string>();
|
||||
|
||||
export function loadInstructions(
|
||||
changeName: string,
|
||||
artifactId: string
|
||||
): InstructionOutput {
|
||||
const projectConfig = readProjectConfig();
|
||||
|
||||
// Load base instructions from schema
|
||||
const baseInstructions = loadSchemaInstructions(changeName, artifactId);
|
||||
const schema = getSchemaForChange(changeName); // Assumes we have schema loaded
|
||||
|
||||
// Validate rules artifact IDs (only once per session)
|
||||
if (projectConfig?.rules) {
|
||||
const validArtifactIds = new Set(schema.artifacts.map(a => a.id));
|
||||
const warnings = validateConfigRules(
|
||||
projectConfig.rules,
|
||||
validArtifactIds,
|
||||
schema.name
|
||||
);
|
||||
|
||||
// Show each unique warning only once per session
|
||||
for (const warning of warnings) {
|
||||
if (!shownWarnings.has(warning)) {
|
||||
console.warn(`⚠️ ${warning}`);
|
||||
shownWarnings.add(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build enriched instruction with XML sections
|
||||
let enrichedInstruction = '';
|
||||
|
||||
// Add context (all artifacts)
|
||||
if (projectConfig?.context) {
|
||||
enrichedInstruction += `<context>\n${projectConfig.context}\n</context>\n\n`;
|
||||
}
|
||||
|
||||
// Add rules (only for matching artifact)
|
||||
const rulesForArtifact = projectConfig?.rules?.[artifactId];
|
||||
if (rulesForArtifact && rulesForArtifact.length > 0) {
|
||||
enrichedInstruction += `<rules>\n`;
|
||||
for (const rule of rulesForArtifact) {
|
||||
enrichedInstruction += `- ${rule}\n`;
|
||||
}
|
||||
enrichedInstruction += `</rules>\n\n`;
|
||||
}
|
||||
|
||||
// Add original template
|
||||
enrichedInstruction += `<template>\n${baseInstructions.template}\n</template>`;
|
||||
|
||||
return {
|
||||
...baseInstructions,
|
||||
instruction: enrichedInstruction,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Note on validation timing:** Rules are validated lazily during instruction loading (not at config load time) because:
|
||||
1. Schema isn't known at config load time (circular dependency)
|
||||
2. Warnings shown when user actually uses the feature (better UX)
|
||||
3. Validation warnings cached per session to avoid spam
|
||||
|
||||
### Phase 4: Performance and Caching
|
||||
|
||||
**Why config is read multiple times:**
|
||||
|
||||
```typescript
|
||||
// Example: "openspec instructions proposal --change my-feature"
|
||||
|
||||
// 1. Schema resolution (to know which schema to use)
|
||||
resolveSchemaForChange('my-feature')
|
||||
→ readProjectConfig() // Read #1
|
||||
|
||||
// 2. Instruction loading (to inject context and rules)
|
||||
loadInstructions('my-feature', 'proposal')
|
||||
→ readProjectConfig() // Read #2
|
||||
|
||||
// Result: Config read twice per command
|
||||
// More complex commands may read 3-5 times
|
||||
```
|
||||
|
||||
**Performance Strategy:**
|
||||
|
||||
V1 approach: No caching, read config fresh each time
|
||||
- Simpler implementation
|
||||
- No cache invalidation complexity
|
||||
- Acceptable if config reads are fast enough
|
||||
|
||||
**Benchmark targets:**
|
||||
- Typical config (1KB context, 5 artifact rules): **< 10ms** per read (imperceptible even 5x)
|
||||
- Large config (50KB context limit): **< 50ms** per read (acceptable for rare case)
|
||||
|
||||
**If benchmarks fail:** Add simple caching:
|
||||
|
||||
```typescript
|
||||
// Simple in-memory cache with no invalidation
|
||||
let cachedConfig: { mtime: number; config: ProjectConfig | null } | null = null;
|
||||
|
||||
export function readProjectConfig(): ProjectConfig | null {
|
||||
const projectRoot = findProjectRoot();
|
||||
const configPath = path.join(projectRoot, 'openspec', 'config.yaml');
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = statSync(configPath);
|
||||
const mtime = stats.mtimeMs;
|
||||
|
||||
// Return cached config if file hasn't changed
|
||||
if (cachedConfig && cachedConfig.mtime === mtime) {
|
||||
return cachedConfig.config;
|
||||
}
|
||||
|
||||
// Read and parse config
|
||||
const config = parseConfigFile(configPath); // Extracted logic
|
||||
|
||||
// Cache result
|
||||
cachedConfig = { mtime, config };
|
||||
return config;
|
||||
}
|
||||
```
|
||||
|
||||
**Performance testing task:** Add to Phase 6 (Testing)
|
||||
- Measure typical config read time (1KB context)
|
||||
- Measure large config read time (50KB context limit)
|
||||
- Measure repeated reads within single command
|
||||
- Document results, add caching only if needed
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ User runs: openspec instructions proposal --change foo │
|
||||
│ │
|
||||
└────────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ resolveSchemaForChange("foo") │
|
||||
│ │
|
||||
│ 1. Check CLI flag ✗ │
|
||||
│ 2. Check .openspec.yaml ✗ │
|
||||
│ 3. Check openspec/config.yaml ✓ → "spec-driven" │
|
||||
│ │
|
||||
└────────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ loadInstructions("foo", "proposal") │
|
||||
│ │
|
||||
│ 1. Load spec-driven/artifacts/proposal.yaml │
|
||||
│ 2. Read openspec/config.yaml │
|
||||
│ 3. Build enriched instruction: │
|
||||
│ - <context>...</context> │
|
||||
│ - <rules>...</rules> (if rules.proposal exists) │
|
||||
│ - <template>...</template> │
|
||||
│ │
|
||||
└────────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Return InstructionOutput with enriched content │
|
||||
│ │
|
||||
│ Agent sees project context + rules + schema template │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Risk]** Config typos silently ignored (e.g., wrong artifact ID in rules)
|
||||
→ **Mitigation:** Validate and warn on unknown artifact IDs during config load. Don't error to allow forward compatibility.
|
||||
|
||||
**[Risk]** Context grows too large, pollutes all artifact instructions
|
||||
→ **Mitigation:** Document recommended size (< 500 chars). If this becomes an issue, add per-artifact context override later.
|
||||
|
||||
**[Risk]** YAML parsing errors break OpenSpec commands
|
||||
→ **Mitigation:** Catch parse errors, log warning, fall back to defaults. Commands remain functional.
|
||||
|
||||
**[Risk]** Config cached incorrectly across commands
|
||||
→ **Mitigation:** Read config fresh on each `readProjectConfig()` call. No caching layer for v1 (simplicity over perf).
|
||||
|
||||
**[Trade-off]** Context is injected into ALL artifacts
|
||||
→ **Benefit:** Consistent project knowledge across workflow
|
||||
→ **Cost:** Can't scope context to specific artifacts (yet)
|
||||
→ **Future:** Add `context: { global: "...", proposal: "..." }` if needed
|
||||
|
||||
**[Trade-off]** Rules use artifact IDs, not human names
|
||||
→ **Benefit:** Stable identifiers (IDs don't change)
|
||||
→ **Cost:** User needs to know artifact IDs from schema
|
||||
→ **Mitigation:** Document common artifact IDs, show in `openspec status` output
|
||||
|
||||
## Migration Plan
|
||||
|
||||
**No migration needed** - this is a new feature with no existing state.
|
||||
|
||||
**Rollout steps:**
|
||||
1. Deploy with config loading behind feature flag (optional, for safety)
|
||||
2. Test with internal project (this repo)
|
||||
3. Document in README with examples
|
||||
4. Remove feature flag if used
|
||||
|
||||
**Rollback strategy:**
|
||||
- Config is additive only (doesn't break existing changes)
|
||||
- If bugs found, config parsing can be disabled with env var
|
||||
- Users can delete config file to restore old behavior
|
||||
|
||||
## Open Questions
|
||||
|
||||
**Q: Should context support file references (`context: ./CONTEXT.md`)?**
|
||||
**A (deferred):** Start with string-only. Add file reference later if users request it. Keeps v1 simple.
|
||||
|
||||
**Q: Should we support `.yml` alias in addition to `.yaml`?**
|
||||
**A:** Yes, check both extensions. Prefer `.yaml` in docs, but accept `.yml` for users who prefer it.
|
||||
|
||||
**Q: What if config's schema field references a non-existent schema?**
|
||||
**A:** Schema resolution will fail downstream. Show error when trying to load schema, suggest valid schema names.
|
||||
|
||||
**Q: Should rules be validated against the resolved schema's artifact IDs?**
|
||||
**A:** Yes, validate and warn, but don't halt. This allows forward compatibility if schema evolves.
|
||||
@@ -0,0 +1,774 @@
|
||||
# Project Config
|
||||
|
||||
## Summary
|
||||
|
||||
Add `openspec/config.yaml` support for project-level configuration. This enables teams to customize OpenSpec behavior without forking schemas, by providing context and rules that are injected into artifact generation.
|
||||
|
||||
## Motivation
|
||||
|
||||
Currently, customizing OpenSpec requires forking entire schemas:
|
||||
- Must copy all files even to add one rule
|
||||
- Lose updates when openspec upgrades
|
||||
- High friction for simple customizations
|
||||
|
||||
Most users don't need different workflow structure. They need to:
|
||||
- Provide project context (tech stack, conventions, constraints)
|
||||
- Add rules for specific artifacts (requirements, formatting preferences)
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Two-Path Model
|
||||
|
||||
OpenSpec customization follows two distinct paths:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ CONFIGURE (this change) FORK (project-local-schemas) │
|
||||
│ ───────────────────── ──────────────────────────── │
|
||||
│ │
|
||||
│ Use a preset schema Define your own schema │
|
||||
│ + add context from scratch │
|
||||
│ + add rules │
|
||||
│ │
|
||||
│ openspec/config.yaml openspec/schemas/my-flow/ │
|
||||
│ │
|
||||
│ ✓ Simple ✓ Full control │
|
||||
│ ✓ Get updates ✗ You maintain everything │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Config Schema
|
||||
|
||||
```yaml
|
||||
# openspec/config.yaml
|
||||
|
||||
# Required: which workflow schema to use
|
||||
schema: spec-driven
|
||||
|
||||
# Optional: project context injected into all artifact prompts
|
||||
context: |
|
||||
Tech stack: TypeScript, React, Node.js, PostgreSQL
|
||||
API style: RESTful, documented in docs/api-conventions.md
|
||||
Testing: Jest + React Testing Library
|
||||
We value backwards compatibility for all public APIs
|
||||
|
||||
# Optional: per-artifact rules (additive)
|
||||
rules:
|
||||
proposal:
|
||||
- Include rollback plan
|
||||
- Identify affected teams and notify in #platform-changes
|
||||
specs:
|
||||
- Use Given/When/Then format
|
||||
- Reference existing patterns before inventing new ones
|
||||
tasks:
|
||||
- Each task should be completable in < 2 hours
|
||||
- Include acceptance criteria
|
||||
```
|
||||
|
||||
### What's NOT in Config
|
||||
|
||||
The following were explicitly excluded to keep the model simple:
|
||||
|
||||
| Feature | Decision | Rationale |
|
||||
|---------|----------|-----------|
|
||||
| `skip: [artifact]` | Not supported | Structural changes belong in fork path |
|
||||
| `add: [{...}]` | Not supported | Structural changes belong in fork path |
|
||||
| `extends: base` | Not supported | No inheritance, fork is full copy |
|
||||
| `context: ./file.md` | Not supported (yet) | Start with string, add file reference later if needed |
|
||||
|
||||
### Field Definitions
|
||||
|
||||
#### `schema` (required)
|
||||
|
||||
Which workflow schema to use. Can be:
|
||||
- Built-in name: `spec-driven`, `tdd`
|
||||
- Project-local schema name: `my-workflow` (requires project-local-schemas change)
|
||||
|
||||
This becomes the default schema for:
|
||||
- New changes created without `--schema` flag
|
||||
- Commands run on changes without `.openspec.yaml` metadata
|
||||
|
||||
#### `context` (optional)
|
||||
|
||||
A string containing project context. Injected into ALL artifact prompts.
|
||||
|
||||
Use cases:
|
||||
- Tech stack description
|
||||
- Link to conventions/style guides
|
||||
- Team constraints or preferences
|
||||
- Domain-specific context
|
||||
|
||||
#### `rules` (optional)
|
||||
|
||||
Per-artifact rules, keyed by artifact ID. Additive to schema's built-in guidance.
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
<artifact-id>:
|
||||
- Rule 1
|
||||
- Rule 2
|
||||
```
|
||||
|
||||
Rules are injected into the specific artifact's prompt, not all prompts.
|
||||
|
||||
### Injection Format
|
||||
|
||||
When generating instructions for an artifact:
|
||||
|
||||
```xml
|
||||
<context>
|
||||
Tech stack: TypeScript, React, Node.js, PostgreSQL
|
||||
API style: RESTful, documented in docs/api-conventions.md
|
||||
...
|
||||
</context>
|
||||
|
||||
<rules>
|
||||
- Include rollback plan
|
||||
- Identify affected teams and notify in #platform-changes
|
||||
</rules>
|
||||
|
||||
<template>
|
||||
[Schema's built-in template content]
|
||||
</template>
|
||||
```
|
||||
|
||||
Context appears for all artifacts. Rules only appear for the matching artifact.
|
||||
|
||||
### Config Creation Strategy
|
||||
|
||||
**Why integrate with `artifact-experimental-setup`?**
|
||||
|
||||
This feature targets **experimental workflow users**. The decision to create config during experimental setup (rather than providing standalone commands) is intentional:
|
||||
|
||||
**Rationale:**
|
||||
1. **Single entry point** - Users setting up experimental features are already in "configuration mode"
|
||||
2. **Contextual timing** - Natural to configure project defaults when setting up workflow
|
||||
3. **Avoids premature API surface** - No standalone `openspec config init` until feature graduates
|
||||
4. **Experimental scope** - Keeps config as experimental feature, not stable API
|
||||
5. **Progressive disclosure** - Users can skip and create manually later if needed
|
||||
|
||||
**Evolution path:**
|
||||
|
||||
```
|
||||
Today (Experimental):
|
||||
openspec artifact-experimental-setup
|
||||
→ prompts for config creation
|
||||
→ creates .claude/skills/
|
||||
→ creates openspec/config.yaml
|
||||
|
||||
Future (When graduating):
|
||||
openspec init
|
||||
→ prompts for config creation
|
||||
→ creates openspec/ directory
|
||||
→ creates openspec/config.yaml
|
||||
|
||||
+ standalone commands:
|
||||
openspec config init
|
||||
openspec config validate
|
||||
openspec config set <key> <value>
|
||||
```
|
||||
|
||||
**Why optional?**
|
||||
|
||||
Config is **additive**, not required:
|
||||
- OpenSpec works without config (uses defaults)
|
||||
- Users can skip during setup and add manually later
|
||||
- Teams can start simple and add config when they feel friction
|
||||
- No config file in git = no problem, everyone gets defaults
|
||||
|
||||
**Design principle:** The system never *requires* config, but makes it easy to create when users want customization.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
**Core Config System:**
|
||||
- Define `ProjectConfig` type with Zod schema
|
||||
- Add `readProjectConfig()` function with graceful error handling
|
||||
- Update instruction generation to inject context (all artifacts)
|
||||
- Update instruction generation to inject rules (per-artifact)
|
||||
- Update schema resolution to use config's `schema` field as default
|
||||
- Update `openspec new change` to use config's schema as default
|
||||
|
||||
**Config Creation (Experimental Setup):**
|
||||
- Extend `artifact-experimental-setup` command to optionally create config
|
||||
- Interactive prompts for schema selection (with description of each schema)
|
||||
- Interactive prompts for project context (optional multi-line input)
|
||||
- Interactive prompts for per-artifact rules (optional)
|
||||
- Validate config immediately after creation
|
||||
- Show clear "skip" option for users who want to create config manually later
|
||||
- Display created config location and usage examples
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- `skip` / `add` for structural changes (use fork path for structural changes)
|
||||
- File reference for context (`context: ./CONTEXT.md`) - start with string, add later if needed
|
||||
- Global user-level config (XDG directories, etc.)
|
||||
- Integration with standard `openspec init` (will add when experimental graduates)
|
||||
- Standalone `openspec config init` command (may add in future change)
|
||||
- `openspec config validate` command (may add in future change)
|
||||
- Config editing/updating commands (users edit YAML directly)
|
||||
|
||||
## User Experience
|
||||
|
||||
### Setting Up Config (Experimental Workflow)
|
||||
|
||||
When users set up the experimental workflow, they're prompted to optionally create config:
|
||||
|
||||
```bash
|
||||
$ openspec artifact-experimental-setup
|
||||
|
||||
Setting up experimental artifact workflow...
|
||||
|
||||
✓ Created .claude/skills/openspec-explore/SKILL.md
|
||||
✓ Created .claude/skills/openspec-new-change/SKILL.md
|
||||
✓ Created .claude/skills/openspec-continue-change/SKILL.md
|
||||
✓ Created .claude/skills/openspec-apply-change/SKILL.md
|
||||
✓ Created .claude/skills/openspec-ff-change/SKILL.md
|
||||
✓ Created .claude/skills/openspec-sync-specs/SKILL.md
|
||||
✓ Created .claude/skills/openspec-archive-change/SKILL.md
|
||||
|
||||
✓ Created .claude/commands/opsx/explore.md
|
||||
✓ Created .claude/commands/opsx/new.md
|
||||
✓ Created .claude/commands/opsx/continue.md
|
||||
✓ Created .claude/commands/opsx/apply.md
|
||||
✓ Created .claude/commands/opsx/ff.md
|
||||
✓ Created .claude/commands/opsx/sync.md
|
||||
✓ Created .claude/commands/opsx/archive.md
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📋 Project Configuration (Optional)
|
||||
|
||||
Configure project defaults for OpenSpec workflows.
|
||||
|
||||
? Create openspec/config.yaml? (Y/n) Y
|
||||
|
||||
? Default schema for new changes?
|
||||
❯ spec-driven (proposal → specs → design → tasks)
|
||||
tdd (spec → tests → implementation → docs)
|
||||
|
||||
? Add project context? (optional)
|
||||
Context is shown to AI when creating artifacts.
|
||||
Examples: tech stack, conventions, style guides, domain knowledge
|
||||
|
||||
Press Enter to skip, or type/paste context:
|
||||
│ Tech stack: TypeScript, React, Node.js, PostgreSQL
|
||||
│ API style: RESTful, documented in docs/api-conventions.md
|
||||
│ Testing: Jest + React Testing Library
|
||||
│ We value backwards compatibility for all public APIs
|
||||
│
|
||||
[Press Enter when done]
|
||||
|
||||
? Add per-artifact rules? (optional) (Y/n) Y
|
||||
|
||||
Which artifacts should have custom rules?
|
||||
[Space to select, Enter when done]
|
||||
◯ proposal
|
||||
◉ specs
|
||||
◯ design
|
||||
◯ tasks
|
||||
|
||||
? Rules for specs artifact:
|
||||
Enter rules one per line, press Enter on empty line to finish:
|
||||
│ Use Given/When/Then format for scenarios
|
||||
│ Reference existing patterns before inventing new ones
|
||||
│
|
||||
[Empty line to finish]
|
||||
|
||||
✓ Created openspec/config.yaml
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
🎉 Setup Complete!
|
||||
|
||||
📖 Config created at: openspec/config.yaml
|
||||
• Default schema: spec-driven
|
||||
• Project context: Added (4 lines)
|
||||
• Rules: 1 artifact configured
|
||||
|
||||
Usage:
|
||||
• New changes automatically use 'spec-driven' schema
|
||||
• Context injected into all artifact instructions
|
||||
• Rules applied to matching artifacts
|
||||
|
||||
To share with team:
|
||||
git add openspec/config.yaml .claude/
|
||||
git commit -m "Setup OpenSpec experimental workflow with project config"
|
||||
|
||||
[Rest of experimental setup output...]
|
||||
```
|
||||
|
||||
**Key UX decisions:**
|
||||
|
||||
1. **Prompted during setup** - Natural place since users are already configuring experimental features
|
||||
2. **Optional at every step** - Clear skip options, no forced configuration
|
||||
3. **Guided prompts** - Schema descriptions, example context, artifact selection
|
||||
4. **Immediate validation** - Config is validated after creation, errors shown immediately
|
||||
5. **Clear output** - Shows exactly what was created and how it affects workflow
|
||||
|
||||
### Setting Up Config (Manual Creation)
|
||||
|
||||
Users can also create config manually (or skip during setup and add later):
|
||||
|
||||
```bash
|
||||
# Create config file manually
|
||||
cat > openspec/config.yaml << 'EOF'
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: TypeScript, React, Node.js
|
||||
We follow REST conventions documented in docs/api.md
|
||||
All changes require backwards compatibility consideration
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Must include rollback plan
|
||||
- Must identify affected teams
|
||||
specs:
|
||||
- Use Given/When/Then format
|
||||
EOF
|
||||
```
|
||||
|
||||
### Effect on Workflow
|
||||
|
||||
Once config is created, it affects the experimental workflow in three ways:
|
||||
|
||||
**1. Default Schema Selection**
|
||||
|
||||
```bash
|
||||
# Before config: must specify schema
|
||||
/opsx:new my-feature --schema spec-driven
|
||||
|
||||
# After config (with schema: spec-driven): schema is automatic
|
||||
/opsx:new my-feature
|
||||
# Automatically uses spec-driven from config
|
||||
|
||||
# Override still works
|
||||
/opsx:new my-feature --schema tdd
|
||||
# Uses tdd, ignoring config
|
||||
```
|
||||
|
||||
**2. Context Injection (All Artifacts)**
|
||||
|
||||
```bash
|
||||
# Get instructions for any artifact
|
||||
openspec instructions proposal --change my-feature
|
||||
|
||||
# Output now includes project context:
|
||||
<context>
|
||||
Tech stack: TypeScript, React, Node.js, PostgreSQL
|
||||
API style: RESTful, documented in docs/api-conventions.md
|
||||
Testing: Jest + React Testing Library
|
||||
We value backwards compatibility for all public APIs
|
||||
</context>
|
||||
|
||||
<template>
|
||||
[Schema's proposal template]
|
||||
</template>
|
||||
```
|
||||
|
||||
Context appears in instructions for **all artifacts** (proposal, specs, design, tasks).
|
||||
|
||||
**3. Rules Injection (Per-Artifact)**
|
||||
|
||||
```bash
|
||||
# Get instructions for artifact with rules configured
|
||||
openspec instructions specs --change my-feature
|
||||
|
||||
# Output includes artifact-specific rules:
|
||||
<context>
|
||||
[Project context]
|
||||
</context>
|
||||
|
||||
<rules>
|
||||
- Use Given/When/Then format for scenarios
|
||||
- Reference existing patterns before inventing new ones
|
||||
</rules>
|
||||
|
||||
<template>
|
||||
[Schema's specs template]
|
||||
</template>
|
||||
```
|
||||
|
||||
Rules only appear for the **specific artifact** they're configured for.
|
||||
|
||||
**Artifacts without rules** (e.g., design, tasks) don't get a `<rules>` section:
|
||||
|
||||
```bash
|
||||
openspec instructions design --change my-feature
|
||||
# Output: <context> then <template> only (no rules)
|
||||
```
|
||||
|
||||
### Team Sharing
|
||||
|
||||
```bash
|
||||
# Commit config
|
||||
git add openspec/config.yaml
|
||||
git commit -m "Add project config with context and rules"
|
||||
|
||||
# Everyone gets the same context and rules automatically
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Files to Modify/Create
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/core/project-config.ts` | **NEW FILE:** Types, parsing, reading, validation helpers |
|
||||
| `src/core/artifact-graph/instruction-loader.ts` | Inject context (all artifacts) and rules (per-artifact) |
|
||||
| `src/utils/change-utils.ts` | Use config schema as default in `createChange()` |
|
||||
| `src/utils/change-metadata.ts` | Update `resolveSchemaForChange()` to check config |
|
||||
| `src/commands/artifact-workflow.ts` | Extend `artifactExperimentalSetupCommand()` to prompt for config creation |
|
||||
| `src/core/config-prompts.ts` | **NEW FILE:** Interactive prompts for config creation (reusable) |
|
||||
|
||||
### Config Location
|
||||
|
||||
Always at `./openspec/config.yaml` relative to project root. No XDG/global config for simplicity.
|
||||
|
||||
### Resolution Order Update
|
||||
|
||||
Schema selection order becomes:
|
||||
|
||||
```
|
||||
1. --schema CLI flag # Explicit override
|
||||
2. .openspec.yaml in change directory # Change-specific binding
|
||||
3. openspec/config.yaml schema field # Project default (NEW)
|
||||
4. "spec-driven" # Hardcoded fallback
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
- `schema` must be a valid schema name (exists in resolution)
|
||||
- `context` must be string
|
||||
- `rules` must be object with string keys (artifact IDs) and array values
|
||||
- Unknown artifact IDs in `rules` should warn, not error (allows forward compat)
|
||||
|
||||
### Experimental Setup Integration
|
||||
|
||||
**Changes to `artifactExperimentalSetupCommand()` in `src/commands/artifact-workflow.ts`:**
|
||||
|
||||
After creating skills and commands, the setup command will:
|
||||
|
||||
1. **Display section header:**
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📋 Project Configuration (Optional)
|
||||
Configure project defaults for OpenSpec workflows.
|
||||
```
|
||||
|
||||
2. **Prompt: Create config?**
|
||||
- Yes/No prompt with default "Yes"
|
||||
- If No → skip entire config section, show usage instructions
|
||||
- If Yes → continue to detailed prompts
|
||||
|
||||
3. **Prompt: Schema selection**
|
||||
- Use `listSchemasWithInfo()` to get available schemas
|
||||
- Display each with description and artifact flow
|
||||
- Default to first schema (likely "spec-driven")
|
||||
|
||||
4. **Prompt: Project context**
|
||||
- Multi-line input (or editor if available)
|
||||
- Show examples: "tech stack, conventions, style guides"
|
||||
- Allow empty (skip)
|
||||
|
||||
5. **Prompt: Per-artifact rules**
|
||||
- Yes/No prompt, default "No" (rules are less common)
|
||||
- If Yes:
|
||||
- Show checklist of artifacts from selected schema
|
||||
- For each selected artifact, prompt for rules (line-by-line input)
|
||||
- Allow empty line to finish each artifact's rules
|
||||
|
||||
6. **Create and validate config:**
|
||||
- Build `ProjectConfig` object from inputs
|
||||
- Validate with Zod schema
|
||||
- Write to `openspec/config.yaml` using YAML serializer
|
||||
- If validation fails, show error and ask to retry or skip
|
||||
|
||||
7. **Display success summary:**
|
||||
- Path to created config
|
||||
- Summary: schema used, context added (line count), rules count
|
||||
- Usage examples showing how config affects workflow
|
||||
- Suggestion to commit config to git
|
||||
|
||||
**Error handling:**
|
||||
- Invalid schema selection → show available schemas with fuzzy match suggestions, retry
|
||||
- Context too large (>50KB) → reject with error, ask to reduce size
|
||||
- Rules reference invalid artifact → warn but continue (forward compat)
|
||||
- File write fails → show error, suggest manual creation
|
||||
- Config already exists → show message, skip config section, continue with setup
|
||||
- User cancellation (Ctrl+C) → log "Config creation cancelled", continue with rest of setup (skills/commands already created)
|
||||
|
||||
**If config already exists:**
|
||||
|
||||
When `openspec/config.yaml` already exists:
|
||||
|
||||
```bash
|
||||
$ openspec artifact-experimental-setup
|
||||
|
||||
[Skills and commands created...]
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📋 Project Configuration
|
||||
|
||||
ℹ️ openspec/config.yaml already exists. Skipping config creation.
|
||||
|
||||
To update config, edit openspec/config.yaml manually or:
|
||||
1. Delete openspec/config.yaml
|
||||
2. Run openspec artifact-experimental-setup again
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
[Rest of setup output...]
|
||||
```
|
||||
|
||||
This prevents accidentally overwriting user's config.
|
||||
|
||||
**Implementation approach:**
|
||||
|
||||
Create separate `src/core/config-prompts.ts` module:
|
||||
|
||||
```typescript
|
||||
export interface ConfigPromptResult {
|
||||
createConfig: boolean;
|
||||
schema?: string;
|
||||
context?: string;
|
||||
rules?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export async function promptForConfig(): Promise<ConfigPromptResult> {
|
||||
// Prompt logic using inquirer or similar
|
||||
// Returns structured result for config creation
|
||||
// Throws ExitPromptError on Ctrl+C (handled by caller)
|
||||
}
|
||||
```
|
||||
|
||||
**Ctrl+C handling in setup command:**
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const configResult = await promptForConfig();
|
||||
if (configResult.createConfig) {
|
||||
writeConfigFile(configResult);
|
||||
console.log('✓ Created openspec/config.yaml');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'ExitPromptError') {
|
||||
console.log('\nℹ️ Config creation cancelled');
|
||||
console.log(' Skills and commands already created');
|
||||
console.log(' Run setup again to create config later');
|
||||
// Continue with rest of setup (not a fatal error)
|
||||
} else {
|
||||
throw error; // Re-throw unexpected errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This keeps prompts reusable and testable separately from the setup command.
|
||||
|
||||
### Dependencies
|
||||
|
||||
**Interactive Prompting Library:**
|
||||
|
||||
The experimental setup command will need an interactive prompting library for the config creation flow. Options:
|
||||
|
||||
1. **@inquirer/prompts** (recommended)
|
||||
- Modern, tree-shakeable, TypeScript-first
|
||||
- Individual imports: `@inquirer/input`, `@inquirer/confirm`, `@inquirer/checkbox`, `@inquirer/editor`
|
||||
- Already used in OpenSpec (if not, lightweight addition)
|
||||
|
||||
2. **inquirer** (classic)
|
||||
- More established, larger ecosystem
|
||||
- Heavier bundle size
|
||||
- Single package with all prompt types
|
||||
|
||||
**Prompts needed:**
|
||||
- `confirm` - "Create config?" "Add rules?"
|
||||
- `select` - Schema selection with descriptions
|
||||
- `editor` or multi-line `input` - Project context
|
||||
- `checkbox` - Artifact selection for rules
|
||||
- `input` (repeated) - Rule entry (line-by-line)
|
||||
|
||||
**Alternative (no dependency):**
|
||||
|
||||
Use Node's built-in `readline` for basic prompts:
|
||||
- More code to write
|
||||
- Less polished UX (no arrow key navigation, checkbox selection)
|
||||
- Zero dependency cost
|
||||
|
||||
**Recommendation:** Use `@inquirer/prompts` for best UX. Config setup is a one-time operation where UX matters.
|
||||
|
||||
### YAML Serialization
|
||||
|
||||
Config creation needs YAML serialization:
|
||||
|
||||
- **yaml** package (already a dependency)
|
||||
- Use `yaml.stringify()` to write config
|
||||
- Preserve multi-line strings with `|` literal style
|
||||
- Format: 2-space indent, no quotes unless needed
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
import { stringify } from 'yaml';
|
||||
|
||||
const config = {
|
||||
schema: 'spec-driven',
|
||||
context: 'Multi-line\ncontext\nhere',
|
||||
rules: { proposal: ['Rule 1', 'Rule 2'] }
|
||||
};
|
||||
|
||||
const yamlContent = stringify(config, {
|
||||
indent: 2,
|
||||
defaultStringType: 'QUOTE_DOUBLE',
|
||||
defaultKeyType: 'PLAIN',
|
||||
});
|
||||
// context will use | literal style automatically for multi-line
|
||||
```
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
**Core Config Functionality:**
|
||||
- Create config with all fields (schema, context, rules), verify parsing
|
||||
- Create minimal config (schema only), verify parsing
|
||||
- Verify context appears in instruction output for all artifacts
|
||||
- Verify rules appear only for matching artifact (not all artifacts)
|
||||
- Verify schema from config is used for new changes
|
||||
- Verify CLI `--schema` flag overrides config
|
||||
- Verify change's `.openspec.yaml` overrides config
|
||||
- Verify graceful handling of missing config (fallback to defaults)
|
||||
- Verify graceful handling of invalid YAML syntax (warning, fallback)
|
||||
- Verify graceful handling of invalid schema (warning, show valid schemas)
|
||||
- Verify unknown artifact IDs in rules emit warnings but don't halt
|
||||
|
||||
**Schema Resolution Precedence:**
|
||||
- Test all four levels of schema resolution:
|
||||
1. CLI flag `--schema` (highest priority)
|
||||
2. Change metadata `.openspec.yaml`
|
||||
3. Project config `openspec/config.yaml`
|
||||
4. Hardcoded default "spec-driven" (lowest priority)
|
||||
- Verify each level correctly overrides lower levels
|
||||
|
||||
**Context and Rules Injection:**
|
||||
- Verify context injection uses `<context>` XML-style tags
|
||||
- Verify rules injection uses `<rules>` XML-style tags with bullets
|
||||
- Verify injection order: `<context>` → `<rules>` → `<template>`
|
||||
- Verify multi-line context is preserved
|
||||
- Verify special characters in context/rules are not escaped
|
||||
- Verify empty context/rules don't create tags
|
||||
|
||||
**Experimental Setup Integration:**
|
||||
- Test `artifact-experimental-setup` with user skipping config creation
|
||||
- Test `artifact-experimental-setup` with minimal config (schema only)
|
||||
- Test `artifact-experimental-setup` with full config (schema + context + rules)
|
||||
- Test schema selection from available schemas
|
||||
- Test multi-line context input
|
||||
- Test per-artifact rules prompts
|
||||
- Test artifact selection (checkboxes)
|
||||
- Test validation errors during config creation
|
||||
- Test file write errors (permissions, etc.)
|
||||
- Verify created config can be parsed by `readProjectConfig()`
|
||||
- Verify success summary shows correct information
|
||||
|
||||
**Edge Cases:**
|
||||
- Config file exists but is empty → treat as invalid, warn
|
||||
- Config has `.yml` extension instead of `.yaml` → accept both
|
||||
- Both `.yaml` and `.yml` exist → prefer `.yaml`
|
||||
- Context contains YAML-significant characters → properly escape in output
|
||||
- Rules array contains empty strings → filter out or warn
|
||||
- Schema references non-existent schema → error with suggestions
|
||||
- Config in subdirectory (not project root) → not found, use defaults
|
||||
|
||||
**Backward Compatibility:**
|
||||
- Existing projects without config continue to work
|
||||
- Existing changes with `.openspec.yaml` metadata aren't affected by config
|
||||
- Adding config to existing project doesn't break in-progress changes
|
||||
|
||||
**Integration Tests:**
|
||||
- Create config → create change → verify schema used
|
||||
- Create config → get instructions → verify context injected
|
||||
- Create config → get instructions → verify rules injected
|
||||
- Update config → verify changes reflected immediately (no caching)
|
||||
- Run `artifact-experimental-setup` → create config → create change → verify flow
|
||||
|
||||
## Related Changes
|
||||
|
||||
- **project-local-schemas**: Enables `schema: my-workflow` to reference project-local schemas
|
||||
|
||||
## Appendix: Full Config Schema
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Zod schema serves as both runtime validation and documentation
|
||||
// Type is inferred from schema for type safety
|
||||
export const ProjectConfigSchema = z.object({
|
||||
// Required: which schema to use (e.g., "spec-driven", "tdd", or project-local schema name)
|
||||
schema: z.string().min(1).describe('The workflow schema to use (e.g., "spec-driven", "tdd")'),
|
||||
|
||||
// Optional: project context (injected into all artifact instructions)
|
||||
// Max size: 50KB (enforced during parsing)
|
||||
context: z.string().optional().describe('Project context injected into all artifact instructions'),
|
||||
|
||||
// Optional: per-artifact rules (additive to schema's built-in guidance)
|
||||
rules: z.record(
|
||||
z.string(), // artifact ID
|
||||
z.array(z.string()) // list of rules
|
||||
).optional().describe('Per-artifact rules, keyed by artifact ID'),
|
||||
});
|
||||
|
||||
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
||||
|
||||
// Note: Parsing uses safeParse() on individual fields for resilient error handling
|
||||
// Invalid fields are warned about but don't prevent other fields from being loaded
|
||||
```
|
||||
|
||||
## Appendix: Visual Summary
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ User provides: │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ openspec/config.yaml │ │
|
||||
│ │ │ │
|
||||
│ │ schema: spec-driven │ │
|
||||
│ │ context: "We use React, TypeScript..." │ │
|
||||
│ │ rules: │ │
|
||||
│ │ proposal: [...] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ OpenSpec merges: │ │
|
||||
│ │ │ │
|
||||
│ │ Schema (spec-driven) │ │
|
||||
│ │ + User's context │ │
|
||||
│ │ + User's rules │ │
|
||||
│ │ ───────────────────────── │ │
|
||||
│ │ = Enriched instructions │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Agent sees (for proposal artifact): │ │
|
||||
│ │ │ │
|
||||
│ │ <context> │ │
|
||||
│ │ We use React, TypeScript... │ │
|
||||
│ │ </context> │ │
|
||||
│ │ │ │
|
||||
│ │ <rules> │ │
|
||||
│ │ - Include rollback plan │ │
|
||||
│ │ - Identify affected teams │ │
|
||||
│ │ </rules> │ │
|
||||
│ │ │ │
|
||||
│ │ <template> │ │
|
||||
│ │ [Built-in proposal template] │ │
|
||||
│ │ </template> │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Spec: Config Loading
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Load project config from openspec/config.yaml
|
||||
|
||||
The system SHALL read and parse the project configuration file located at `openspec/config.yaml` relative to the project root.
|
||||
|
||||
#### Scenario: Valid config file exists
|
||||
- **WHEN** `openspec/config.yaml` exists with valid YAML content
|
||||
- **THEN** system parses the file and returns a ProjectConfig object
|
||||
|
||||
#### Scenario: Config file does not exist
|
||||
- **WHEN** `openspec/config.yaml` does not exist
|
||||
- **THEN** system returns null without error
|
||||
|
||||
#### Scenario: Config file has invalid YAML syntax
|
||||
- **WHEN** `openspec/config.yaml` contains malformed YAML
|
||||
- **THEN** system logs a warning message and returns null
|
||||
|
||||
#### Scenario: Config file has valid YAML but invalid schema
|
||||
- **WHEN** `openspec/config.yaml` contains valid YAML that fails Zod schema validation
|
||||
- **THEN** system logs a warning message with validation details and returns null
|
||||
|
||||
### Requirement: Support .yml file extension alias
|
||||
|
||||
The system SHALL accept both `.yaml` and `.yml` file extensions for the config file.
|
||||
|
||||
#### Scenario: Config file uses .yml extension
|
||||
- **WHEN** `openspec/config.yml` exists and `openspec/config.yaml` does not exist
|
||||
- **THEN** system reads from `openspec/config.yml`
|
||||
|
||||
#### Scenario: Both .yaml and .yml exist
|
||||
- **WHEN** both `openspec/config.yaml` and `openspec/config.yml` exist
|
||||
- **THEN** system prefers `openspec/config.yaml`
|
||||
|
||||
### Requirement: Use resilient field-by-field parsing
|
||||
|
||||
The system SHALL parse each config field independently, collecting valid fields and warning about invalid ones without rejecting the entire config.
|
||||
|
||||
#### Scenario: Schema field is valid
|
||||
- **WHEN** config contains `schema: "spec-driven"`
|
||||
- **THEN** schema field is included in returned config
|
||||
|
||||
#### Scenario: Schema field is missing
|
||||
- **WHEN** config lacks the `schema` field
|
||||
- **THEN** no warning is logged (field is optional at parse level)
|
||||
|
||||
#### Scenario: Schema field is empty string
|
||||
- **WHEN** config contains `schema: ""`
|
||||
- **THEN** warning is logged and schema field is not included in returned config
|
||||
|
||||
#### Scenario: Schema field is invalid type
|
||||
- **WHEN** config contains `schema: 123` (number instead of string)
|
||||
- **THEN** warning is logged and schema field is not included in returned config
|
||||
|
||||
#### Scenario: Context field is valid
|
||||
- **WHEN** config contains `context: "Tech stack: TypeScript"`
|
||||
- **THEN** context field is included in returned config
|
||||
|
||||
#### Scenario: Context field is invalid type
|
||||
- **WHEN** config contains `context: 123` (number instead of string)
|
||||
- **THEN** warning is logged and context field is not included in returned config
|
||||
|
||||
#### Scenario: Rules field has valid structure
|
||||
- **WHEN** config contains `rules: { proposal: ["Rule 1"], specs: ["Rule 2"] }`
|
||||
- **THEN** rules field is included in returned config with valid rules
|
||||
|
||||
#### Scenario: Rules field has non-array value for artifact
|
||||
- **WHEN** config contains `rules: { proposal: "not an array", specs: ["Valid"] }`
|
||||
- **THEN** warning is logged for proposal, but specs rules are still included in returned config
|
||||
|
||||
#### Scenario: Rules array contains non-string elements
|
||||
- **WHEN** config contains `rules: { proposal: ["Valid rule", 123, ""] }`
|
||||
- **THEN** only "Valid rule" is included, warning logged about invalid elements
|
||||
|
||||
#### Scenario: Mix of valid and invalid fields
|
||||
- **WHEN** config contains valid schema, invalid context type, valid rules
|
||||
- **THEN** config is returned with schema and rules fields, warning logged about context
|
||||
|
||||
### Requirement: Enforce context size limit
|
||||
|
||||
The system SHALL reject context fields exceeding 50KB and log a warning.
|
||||
|
||||
#### Scenario: Context within size limit
|
||||
- **WHEN** config contains context of 1KB
|
||||
- **THEN** context is included in returned config
|
||||
|
||||
#### Scenario: Context at size limit
|
||||
- **WHEN** config contains context of exactly 50KB
|
||||
- **THEN** context is included in returned config
|
||||
|
||||
#### Scenario: Context exceeds size limit
|
||||
- **WHEN** config contains context of 51KB
|
||||
- **THEN** warning is logged with size and limit, context field is not included in returned config
|
||||
|
||||
### Requirement: Defer artifact ID validation to instruction loading
|
||||
|
||||
The system SHALL NOT validate artifact IDs in rules during config load time. Validation happens during instruction loading when schema is known.
|
||||
|
||||
#### Scenario: Config with rules is loaded
|
||||
- **WHEN** config contains `rules: { unknownartifact: [...] }`
|
||||
- **THEN** config is loaded successfully without validation errors
|
||||
|
||||
#### Scenario: Validation happens at instruction load time
|
||||
- **WHEN** instructions are loaded for any artifact and config has unknown artifact IDs in rules
|
||||
- **THEN** warnings are emitted about unknown artifact IDs (see rules-injection spec for details)
|
||||
|
||||
### Requirement: Gracefully handle config errors without halting
|
||||
|
||||
The system SHALL continue operation with default values when config loading or parsing fails.
|
||||
|
||||
#### Scenario: Config parse failure during command execution
|
||||
- **WHEN** config file has syntax errors and user runs `openspec new change`
|
||||
- **THEN** command executes using default schema "spec-driven"
|
||||
|
||||
#### Scenario: Warning is visible to user
|
||||
- **WHEN** config loading fails
|
||||
- **THEN** system outputs warning message to stderr with details about the failure
|
||||
@@ -0,0 +1,51 @@
|
||||
# Spec: Context Injection
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Inject context into all artifact instructions
|
||||
|
||||
The system SHALL inject the context field from project config into instructions for all artifacts, wrapped in XML-style `<context>` tags.
|
||||
|
||||
#### Scenario: Config has context field
|
||||
- **WHEN** config contains `context: "Tech stack: TypeScript, React"`
|
||||
- **THEN** instruction output includes `<context>\nTech stack: TypeScript, React\n</context>`
|
||||
|
||||
#### Scenario: Config has no context field
|
||||
- **WHEN** config omits the context field or context is undefined
|
||||
- **THEN** instruction output does not include `<context>` tags
|
||||
|
||||
#### Scenario: Context is multi-line string
|
||||
- **WHEN** config contains context with multiple lines
|
||||
- **THEN** instruction output preserves line breaks within `<context>` tags
|
||||
|
||||
#### Scenario: Context applied to all artifacts
|
||||
- **WHEN** instructions are loaded for any artifact (proposal, specs, design, tasks)
|
||||
- **THEN** context section appears in all instruction outputs
|
||||
|
||||
### Requirement: Format context with XML-style tags
|
||||
|
||||
The system SHALL wrap context content in `<context>` opening and `</context>` closing tags with content on separate lines.
|
||||
|
||||
#### Scenario: Context tag structure
|
||||
- **WHEN** context is injected into instructions
|
||||
- **THEN** format is exactly `<context>\n{content}\n</context>\n\n`
|
||||
|
||||
#### Scenario: Context appears before template
|
||||
- **WHEN** instructions are generated with context
|
||||
- **THEN** `<context>` section appears before the `<template>` section
|
||||
|
||||
### Requirement: Preserve context content exactly as provided
|
||||
|
||||
The system SHALL inject context content without modification, escaping, or interpretation.
|
||||
|
||||
#### Scenario: Context contains special characters
|
||||
- **WHEN** context includes characters like `<`, `>`, `&`, quotes
|
||||
- **THEN** characters are preserved exactly as written in the config
|
||||
|
||||
#### Scenario: Context contains URLs
|
||||
- **WHEN** context includes URLs like "docs at https://example.com"
|
||||
- **THEN** URLs are preserved exactly in the injected content
|
||||
|
||||
#### Scenario: Context contains markdown
|
||||
- **WHEN** context includes markdown formatting like `**bold**` or `[links](url)`
|
||||
- **THEN** markdown is preserved without rendering or escaping
|
||||
@@ -0,0 +1,99 @@
|
||||
# Spec: Rules Injection
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Inject rules only for matching artifact
|
||||
|
||||
The system SHALL inject rules from config into instructions only when the artifact ID matches a key in the rules object.
|
||||
|
||||
#### Scenario: Rules exist for the artifact
|
||||
- **WHEN** loading instructions for "proposal" and config has `rules: { proposal: ["Rule 1", "Rule 2"] }`
|
||||
- **THEN** instruction output includes rules section with both rules
|
||||
|
||||
#### Scenario: No rules for the artifact
|
||||
- **WHEN** loading instructions for "design" and config has `rules: { proposal: [...] }`
|
||||
- **THEN** instruction output does not include `<rules>` tags
|
||||
|
||||
#### Scenario: Rules object is undefined
|
||||
- **WHEN** config omits the rules field or rules is undefined
|
||||
- **THEN** instruction output does not include `<rules>` tags for any artifact
|
||||
|
||||
#### Scenario: Rules array is empty for artifact
|
||||
- **WHEN** config has `rules: { proposal: [] }`
|
||||
- **THEN** instruction output does not include `<rules>` tags
|
||||
|
||||
### Requirement: Format rules with XML-style tags and bullet list
|
||||
|
||||
The system SHALL wrap rules in `<rules>` tags with each rule as a bulleted list item.
|
||||
|
||||
#### Scenario: Single rule for artifact
|
||||
- **WHEN** config has `rules: { proposal: ["Include rollback plan"] }`
|
||||
- **THEN** instruction output includes `<rules>\n- Include rollback plan\n</rules>\n\n`
|
||||
|
||||
#### Scenario: Multiple rules for artifact
|
||||
- **WHEN** config has `rules: { proposal: ["Rule 1", "Rule 2", "Rule 3"] }`
|
||||
- **THEN** instruction output includes each rule as separate bullet point
|
||||
|
||||
#### Scenario: Rules appear after context and before template
|
||||
- **WHEN** instructions are generated with both context and rules
|
||||
- **THEN** order is `<context>` then `<rules>` then `<template>`
|
||||
|
||||
### Requirement: Preserve rule text exactly as provided
|
||||
|
||||
The system SHALL inject rule text without modification, escaping, or interpretation.
|
||||
|
||||
#### Scenario: Rule contains markdown
|
||||
- **WHEN** rule includes markdown like "Use **Given/When/Then** format"
|
||||
- **THEN** markdown is preserved in the injected content
|
||||
|
||||
#### Scenario: Rule contains special characters
|
||||
- **WHEN** rule includes characters like `<`, `>`, quotes
|
||||
- **THEN** characters are preserved exactly as written
|
||||
|
||||
#### Scenario: Rule is multi-line string
|
||||
- **WHEN** rule text contains line breaks
|
||||
- **THEN** line breaks are preserved within the bullet point
|
||||
|
||||
### Requirement: Support multiple artifacts with different rules
|
||||
|
||||
The system SHALL allow different rule sets for different artifacts in the same config.
|
||||
|
||||
#### Scenario: Multiple artifacts have rules
|
||||
- **WHEN** config has `rules: { proposal: ["P1"], specs: ["S1", "S2"], tasks: ["T1"] }`
|
||||
- **THEN** proposal instructions show only ["P1"], specs show only ["S1", "S2"], tasks show only ["T1"]
|
||||
|
||||
#### Scenario: Some artifacts have rules, others do not
|
||||
- **WHEN** config has rules for proposal and specs only
|
||||
- **THEN** design and tasks instructions have no `<rules>` section
|
||||
|
||||
### Requirement: Rules are additive to schema guidance
|
||||
|
||||
The system SHALL add config rules to the schema's built-in artifact instruction, not replace it.
|
||||
|
||||
#### Scenario: Artifact has schema instruction and config rules
|
||||
- **WHEN** artifact has built-in instruction from schema and config provides rules
|
||||
- **THEN** final instruction contains both schema guidance and config rules
|
||||
|
||||
#### Scenario: Rules provide additional constraints
|
||||
- **WHEN** schema says "create proposal" and config rules say "include rollback plan"
|
||||
- **THEN** agent sees both the schema template and the additional rule
|
||||
|
||||
### Requirement: Validate artifact IDs during instruction loading
|
||||
|
||||
The system SHALL validate artifact IDs in rules against the schema when instructions are loaded and emit warnings for unknown IDs.
|
||||
|
||||
#### Scenario: All artifact IDs are valid
|
||||
- **WHEN** instructions loaded and config has `rules: { proposal: [...], specs: [...] }` for schema with those artifacts
|
||||
- **THEN** no validation warnings are emitted
|
||||
|
||||
#### Scenario: Unknown artifact ID in rules
|
||||
- **WHEN** instructions loaded and config has `rules: { unknownartifact: [...] }`
|
||||
- **THEN** warning emitted: "Unknown artifact ID in rules: 'unknownartifact'. Valid IDs for schema 'spec-driven': design, proposal, specs, tasks"
|
||||
|
||||
#### Scenario: Multiple unknown artifact IDs
|
||||
- **WHEN** instructions loaded and config has multiple unknown artifact IDs
|
||||
- **THEN** separate warning emitted for each unknown artifact ID
|
||||
|
||||
#### Scenario: Validation warnings shown once per session
|
||||
- **WHEN** instructions loaded multiple times in same CLI session
|
||||
- **THEN** each unique validation warning is shown only once (cached)
|
||||
@@ -0,0 +1,83 @@
|
||||
# Spec: Schema Resolution with Config
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Use config schema as default for new changes
|
||||
|
||||
The system SHALL use the schema field from `openspec/config.yaml` as the default when creating new changes without explicit `--schema` flag.
|
||||
|
||||
#### Scenario: Create change without --schema flag and config exists
|
||||
- **WHEN** user runs `openspec new change foo` and config contains `schema: "tdd"`
|
||||
- **THEN** system creates change with schema "tdd"
|
||||
|
||||
#### Scenario: Create change without --schema flag and no config
|
||||
- **WHEN** user runs `openspec new change foo` and no config file exists
|
||||
- **THEN** system creates change with default schema "spec-driven"
|
||||
|
||||
#### Scenario: Create change with explicit --schema flag
|
||||
- **WHEN** user runs `openspec new change foo --schema custom` and config contains `schema: "tdd"`
|
||||
- **THEN** system creates change with schema "custom" (CLI flag overrides config)
|
||||
|
||||
### Requirement: Resolve schema with updated precedence order
|
||||
|
||||
The system SHALL resolve the schema for a change using the following precedence order: CLI flag, change metadata, project config, hardcoded default.
|
||||
|
||||
#### Scenario: CLI flag is provided
|
||||
- **WHEN** user runs command with `--schema custom`
|
||||
- **THEN** system uses "custom" regardless of change metadata or config
|
||||
|
||||
#### Scenario: Change metadata specifies schema
|
||||
- **WHEN** change has `.openspec.yaml` with `schema: bound` and config has `schema: tdd`
|
||||
- **THEN** system uses "bound" from change metadata
|
||||
|
||||
#### Scenario: Only project config specifies schema
|
||||
- **WHEN** no CLI flag or change metadata, but config has `schema: tdd`
|
||||
- **THEN** system uses "tdd" from project config
|
||||
|
||||
#### Scenario: No schema specified anywhere
|
||||
- **WHEN** no CLI flag, change metadata, or project config
|
||||
- **THEN** system uses hardcoded default "spec-driven"
|
||||
|
||||
### Requirement: Support project-local schema names in config
|
||||
|
||||
The system SHALL allow the config schema field to reference project-local schemas defined in `openspec/schemas/`.
|
||||
|
||||
#### Scenario: Config references project-local schema
|
||||
- **WHEN** config contains `schema: "my-workflow"` and `openspec/schemas/my-workflow/` exists
|
||||
- **THEN** system resolves to the project-local schema
|
||||
|
||||
#### Scenario: Config references non-existent schema
|
||||
- **WHEN** config contains `schema: "nonexistent"` and that schema does not exist
|
||||
- **THEN** system shows error when attempting to load the schema with fuzzy match suggestions and list of all valid schemas
|
||||
|
||||
### Requirement: Provide helpful error message for invalid schema
|
||||
|
||||
The system SHALL display schema error with fuzzy match suggestions, list of available schemas, and fix instructions.
|
||||
|
||||
#### Scenario: Schema name with typo (close match)
|
||||
- **WHEN** config contains `schema: "spce-driven"` (typo)
|
||||
- **THEN** error message includes "Did you mean: spec-driven (built-in)" as suggestion
|
||||
|
||||
#### Scenario: Schema name with no close matches
|
||||
- **WHEN** config contains `schema: "completely-wrong"`
|
||||
- **THEN** error message shows list of all available built-in and project-local schemas
|
||||
|
||||
#### Scenario: Error message includes fix instructions
|
||||
- **WHEN** config references invalid schema
|
||||
- **THEN** error message includes "Fix: Edit openspec/config.yaml and change 'schema: X' to a valid schema name"
|
||||
|
||||
#### Scenario: Error distinguishes built-in vs project-local schemas
|
||||
- **WHEN** error lists available schemas
|
||||
- **THEN** output clearly labels each as "built-in" or "project-local"
|
||||
|
||||
### Requirement: Maintain backwards compatibility for existing changes
|
||||
|
||||
The system SHALL continue to work with existing changes that do not have project config.
|
||||
|
||||
#### Scenario: Existing change without config
|
||||
- **WHEN** change was created before config feature and no config file exists
|
||||
- **THEN** system resolves schema using existing logic (change metadata or hardcoded default)
|
||||
|
||||
#### Scenario: Existing change with config added later
|
||||
- **WHEN** config file is added to project with existing changes
|
||||
- **THEN** existing changes continue to use their bound schema from `.openspec.yaml`
|
||||
@@ -0,0 +1,72 @@
|
||||
## 1. Core Config System
|
||||
|
||||
- [x] 1.1 Create `src/core/project-config.ts` with ProjectConfigSchema using Zod (for docs and type inference)
|
||||
- [x] 1.2 Implement `readProjectConfig()` with resilient field-by-field parsing using Zod's `safeParse()`
|
||||
- [x] 1.3 Add support for both .yaml and .yml extensions (prefer .yaml)
|
||||
- [x] 1.4 Add 50KB hard limit for context field with size check and warning
|
||||
- [x] 1.5 Implement `validateConfigRules()` to validate artifact IDs against schema (called during instruction loading)
|
||||
- [x] 1.6 Implement `suggestSchemas()` with Levenshtein distance fuzzy matching for helpful error messages
|
||||
- [x] 1.7 Add unit tests for resilient parsing (partial configs, field-level errors with Zod safeParse)
|
||||
- [x] 1.8 Add unit tests for context size limit enforcement
|
||||
- [x] 1.9 Add unit tests for .yml/.yaml precedence
|
||||
- [x] 1.10 Add unit tests for fuzzy schema matching with typos
|
||||
|
||||
## 2. Schema Resolution Integration
|
||||
|
||||
- [x] 2.1 Update `resolveSchemaForChange()` in `src/utils/change-metadata.ts` to check project config (3rd in precedence)
|
||||
- [x] 2.2 Update `createNewChange()` in `src/utils/change-utils.ts` to use config schema as default
|
||||
- [x] 2.3 Add integration tests for schema resolution precedence (CLI → change metadata → config → default)
|
||||
- [x] 2.4 Add test for project-local schema names in config
|
||||
- [x] 2.5 Add test for non-existent schema error handling with suggestions
|
||||
|
||||
## 3. Context and Rules Injection
|
||||
|
||||
- [x] 3.1 Update `loadInstructions()` in `src/core/artifact-graph/instruction-loader.ts` to inject context for all artifacts
|
||||
- [x] 3.2 Add rules injection logic for matching artifacts only with XML tags and bullet formatting
|
||||
- [x] 3.3 Add validation call during instruction loading to check artifact IDs in rules
|
||||
- [x] 3.4 Implement session-level warning cache to avoid repeating same validation warnings
|
||||
- [x] 3.5 Implement proper ordering: `<context>` → `<rules>` → `<template>`
|
||||
- [x] 3.6 Preserve multi-line strings and special characters without escaping
|
||||
- [x] 3.7 Add unit tests for context injection (present, absent, multi-line, special chars)
|
||||
- [x] 3.8 Add unit tests for rules injection (matching artifact, non-matching, empty array, multiple artifacts)
|
||||
- [x] 3.9 Add unit tests for validation timing (warnings during instruction load, not config load)
|
||||
- [x] 3.10 Add unit tests for warning deduplication (same warning shown once per session)
|
||||
- [x] 3.11 Add integration test verifying full instruction output with context + rules + template
|
||||
|
||||
## 4. Interactive Config Creation
|
||||
|
||||
- [x] 4.1 Add @inquirer/prompts dependency to package.json
|
||||
- [x] 4.2 Create `src/core/config-prompts.ts` with ConfigPromptResult interface
|
||||
- [x] 4.3 Implement `promptForConfig()` function with schema selection prompt
|
||||
- [x] 4.4 Add multi-line context input prompt with examples and skip option
|
||||
- [x] 4.5 Add per-artifact rules prompts with checkbox selection and line-by-line input
|
||||
- [x] 4.6 Implement YAML serialization with proper multi-line string formatting
|
||||
- [x] 4.7 Add validation and retry logic for prompt errors
|
||||
|
||||
## 5. Experimental Setup Integration
|
||||
|
||||
- [x] 5.1 Update `artifactExperimentalSetupCommand()` in `src/commands/artifact-workflow.ts` to check for existing config
|
||||
- [x] 5.2 Add config creation section after skills/commands creation with header and description
|
||||
- [x] 5.3 Integrate `promptForConfig()` calls with proper flow control
|
||||
- [x] 5.4 Add Ctrl+C (ExitPromptError) handling - log cancellation message, continue with setup (non-fatal)
|
||||
- [x] 5.5 Write created config to `openspec/config.yaml` using YAML stringify
|
||||
- [x] 5.6 Display success summary showing path, schema, context lines, rules count
|
||||
- [x] 5.7 Show usage examples and git commit suggestion
|
||||
- [x] 5.8 Handle existing config case with skip message and manual update instructions
|
||||
- [x] 5.9 Add error handling for file write failures with fallback suggestions
|
||||
- [x] 5.10 Add test for cancellation behavior (skills/commands preserved, config not created)
|
||||
|
||||
## 6. Testing and Documentation
|
||||
|
||||
- [x] 6.1 Add end-to-end test: run experimental setup → create config → create change → verify schema used
|
||||
- [x] 6.2 Add end-to-end test: create config → get instructions → verify context and rules injected
|
||||
- [x] 6.3 Test backwards compatibility: existing changes work without config
|
||||
- [x] 6.4 Test config changes are reflected immediately (no stale cache)
|
||||
- [x] 6.5 Add performance benchmark: measure config read time with typical config (1KB context)
|
||||
- [x] 6.6 Add performance benchmark: measure config read time with large config (50KB context)
|
||||
- [x] 6.7 Add performance benchmark: measure repeated reads within single command
|
||||
- [x] 6.8 Document benchmark results and decide if caching is needed (target: <10ms typical, <50ms acceptable)
|
||||
- [x] 6.9 If benchmarks fail: implement mtime-based caching with cache invalidation
|
||||
- [x] 6.10 Update README or docs with config feature examples and schema
|
||||
- [x] 6.11 Document common artifact IDs for different schemas
|
||||
- [x] 6.12 Add troubleshooting section for config validation errors
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: "2025-01-13"
|
||||
@@ -0,0 +1,117 @@
|
||||
## Context
|
||||
|
||||
OpenSpec currently resolves schemas from two locations:
|
||||
1. User override: `~/.local/share/openspec/schemas/<name>/`
|
||||
2. Package built-in: `<npm-package>/schemas/<name>/`
|
||||
|
||||
This change adds a third, highest-priority level: project-local schemas at `./openspec/schemas/<name>/`.
|
||||
|
||||
The resolver functions in `src/core/artifact-graph/resolver.ts` currently don't take a `projectRoot` parameter because user and package paths are absolute. To support project-local schemas, we need to pass project root context into the resolver.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Enable version-controlled custom workflow schemas
|
||||
- Allow teams to share schemas via git without per-machine setup
|
||||
- Maintain backward compatibility with existing resolver API
|
||||
- Integrate with `config.yaml`'s `schema` field (from project-config change)
|
||||
|
||||
**Non-Goals:**
|
||||
- Schema inheritance or `extends` keyword
|
||||
- Template-level overrides (partial forks)
|
||||
- Schema management CLI commands (`openspec schema copy/which/diff/reset`)
|
||||
- Validation that project-local schema names don't conflict with built-ins (shadowing is intentional)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: Add optional `projectRoot` parameter to resolver functions
|
||||
|
||||
**Choice:** Add optional `projectRoot?: string` parameter to resolver functions rather than using `process.cwd()` internally.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Use `process.cwd()` internally: Simpler API but implicit, harder to test, doesn't match existing codebase patterns
|
||||
- Create separate project-aware functions: No breaking changes but awkward API, callers must compose
|
||||
|
||||
**Rationale:** The codebase already follows a pattern where CLI commands get project root via `process.cwd()` and pass it down to functions that need it. Adding an optional parameter maintains backward compatibility while enabling explicit, testable behavior.
|
||||
|
||||
**Affected functions:**
|
||||
```typescript
|
||||
getSchemaDir(name: string, projectRoot?: string): string | null
|
||||
listSchemas(projectRoot?: string): string[]
|
||||
listSchemasWithInfo(projectRoot?: string): SchemaInfo[]
|
||||
resolveSchema(name: string, projectRoot?: string): SchemaYaml
|
||||
```
|
||||
|
||||
### Decision 2: Resolution order is project → user → package
|
||||
|
||||
**Choice:** Project-local schemas have highest priority, then user overrides, then package built-ins.
|
||||
|
||||
**Rationale:**
|
||||
- Project-local should win because it represents team intent (version controlled, shared)
|
||||
- User overrides still useful for personal experimentation without affecting team
|
||||
- Package built-ins are the fallback defaults
|
||||
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local (highest)
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User override
|
||||
3. <npm-package>/schemas/<name>/ # Package built-in (lowest)
|
||||
```
|
||||
|
||||
### Decision 3: Add `getProjectSchemasDir()` helper function
|
||||
|
||||
**Choice:** Create a dedicated function to get the project schemas directory path.
|
||||
|
||||
```typescript
|
||||
function getProjectSchemasDir(projectRoot: string): string {
|
||||
return path.join(projectRoot, 'openspec', 'schemas');
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** Matches existing pattern with `getPackageSchemasDir()` and `getUserSchemasDir()`. Keeps path logic centralized.
|
||||
|
||||
### Decision 4: Extend `SchemaInfo.source` to include `'project'`
|
||||
|
||||
**Choice:** Update the source type from `'package' | 'user'` to `'project' | 'user' | 'package'`.
|
||||
|
||||
**Rationale:** Consumers need to distinguish project-local schemas for display purposes (e.g., `schemasCommand` output).
|
||||
|
||||
### Decision 5: No special handling for schema name conflicts
|
||||
|
||||
**Choice:** If a project-local schema has the same name as a built-in (e.g., `spec-driven`), the project-local version wins. No warning, no error.
|
||||
|
||||
**Rationale:** This is intentional shadowing. Teams may want to customize a built-in schema while keeping the same name for familiarity.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk: Confusion when project schema shadows built-in
|
||||
A team could create `openspec/schemas/spec-driven/` that shadows the built-in, causing confusion when someone expects default behavior.
|
||||
|
||||
**Mitigation:** The `openspec schemas` command shows the source of each schema. Users can see `spec-driven (project)` vs `spec-driven (package)`.
|
||||
|
||||
### Risk: Missing projectRoot parameter
|
||||
If callers forget to pass `projectRoot`, project-local schemas won't be found.
|
||||
|
||||
**Mitigation:**
|
||||
- Make the change incrementally, updating call sites that need project-local support
|
||||
- Existing behavior (user + package only) is preserved when `projectRoot` is undefined
|
||||
|
||||
### Trade-off: Optional parameter vs required
|
||||
Making `projectRoot` optional maintains backward compatibility but means some code paths may silently skip project-local resolution.
|
||||
|
||||
**Accepted:** Backward compatibility is more important. The main entry points (CLI commands) will always pass `projectRoot`.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
1. **Update `resolver.ts`:**
|
||||
- Add `getProjectSchemasDir(projectRoot: string)` function
|
||||
- Update `getSchemaDir()` to check project-local first when `projectRoot` provided
|
||||
- Update `listSchemas()` to include project schemas when `projectRoot` provided
|
||||
- Update `listSchemasWithInfo()` to return `source: 'project'` for project schemas
|
||||
- Update `SchemaInfo` type to include `'project'` in source union
|
||||
|
||||
2. **Update `artifact-workflow.ts`:**
|
||||
- Update `schemasCommand` to pass `projectRoot` and display source labels
|
||||
|
||||
3. **Update call sites:**
|
||||
- Any existing code that needs project-local resolution should pass `projectRoot`
|
||||
- `config.yaml` schema resolution already has access to `projectRoot`
|
||||
@@ -0,0 +1,167 @@
|
||||
# Project-Local Schemas
|
||||
|
||||
## Summary
|
||||
|
||||
Add project-local schema resolution (`./openspec/schemas/`) as the highest priority in the schema lookup chain. This enables teams to version control custom workflow schemas with their repository.
|
||||
|
||||
## Motivation
|
||||
|
||||
Currently, schema resolution is 2-level:
|
||||
1. User override: `~/.local/share/openspec/schemas/<name>/`
|
||||
2. Package built-in: `<npm-package>/schemas/<name>/`
|
||||
|
||||
This creates friction for teams:
|
||||
- Custom schemas must be set up per-machine via XDG paths
|
||||
- Cannot share schemas via version control
|
||||
- No single source of truth for team workflows
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### 3-Level Resolution Order
|
||||
|
||||
```
|
||||
1. ./openspec/schemas/<name>/ # Project-local (NEW)
|
||||
2. ~/.local/share/openspec/schemas/<name>/ # User global (XDG)
|
||||
3. <npm-package>/schemas/<name>/ # Package built-in
|
||||
```
|
||||
|
||||
Project-local takes highest priority, enabling:
|
||||
- Version-controlled custom workflows
|
||||
- Automatic team sharing via git
|
||||
- No per-machine setup required
|
||||
|
||||
### Fork Model (Not Inheritance)
|
||||
|
||||
Custom schemas are complete definitions, not extensions. There is no `extends` keyword.
|
||||
|
||||
**Rationale:** Simplicity. Inheritance adds complexity (conflict resolution, partial overrides, debugging "where did this come from?"). Users who need custom workflows can define them fully. This keeps the mental model simple:
|
||||
- Use a preset → Configure path (see project-config change)
|
||||
- Need different structure → Fork path (define your own)
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── schemas/ # Project-local schemas
|
||||
│ └── my-workflow/
|
||||
│ ├── schema.yaml # Full schema definition
|
||||
│ └── templates/
|
||||
│ ├── artifact1.md
|
||||
│ ├── artifact2.md
|
||||
│ └── ...
|
||||
└── changes/
|
||||
```
|
||||
|
||||
### Schema Naming
|
||||
|
||||
Project-local schemas are referenced by their directory name:
|
||||
- `openspec/schemas/my-workflow/` → referenced as `my-workflow`
|
||||
- Works with `--schema my-workflow` flag
|
||||
- Works with `schema: my-workflow` in config.yaml (see project-config change)
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Add `getProjectSchemasDir()` function to resolver
|
||||
- Update `getSchemaDir()` to check project-local first
|
||||
- Update `listSchemas()` to include project schemas
|
||||
- Update `listSchemasWithInfo()` to include `source: 'project'`
|
||||
- Update `schemasCommand` output to show project schemas
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Schema management CLI (`openspec schema copy/which/diff/reset`) - future enhancement
|
||||
- Schema inheritance/extends - explicitly not supported
|
||||
- Template-level overrides (partial fork) - explicitly not supported
|
||||
|
||||
## User Experience
|
||||
|
||||
### Creating a Custom Schema
|
||||
|
||||
```bash
|
||||
# Create schema directory
|
||||
mkdir -p openspec/schemas/my-workflow/templates
|
||||
|
||||
# Define schema
|
||||
cat > openspec/schemas/my-workflow/schema.yaml << 'EOF'
|
||||
name: my-workflow
|
||||
version: 1
|
||||
description: Our team's planning workflow
|
||||
|
||||
artifacts:
|
||||
- id: research
|
||||
generates: research.md
|
||||
template: research.md
|
||||
description: Background research
|
||||
requires: []
|
||||
|
||||
- id: proposal
|
||||
generates: proposal.md
|
||||
template: proposal.md
|
||||
description: Change proposal
|
||||
requires: [research]
|
||||
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
template: tasks.md
|
||||
description: Implementation tasks
|
||||
requires: [proposal]
|
||||
EOF
|
||||
|
||||
# Create templates
|
||||
echo "# Research\n\n..." > openspec/schemas/my-workflow/templates/research.md
|
||||
# ... etc
|
||||
```
|
||||
|
||||
### Using the Custom Schema
|
||||
|
||||
```bash
|
||||
# Via CLI flag
|
||||
openspec new change add-feature --schema my-workflow
|
||||
openspec status --change add-feature --schema my-workflow
|
||||
|
||||
# Via config.yaml (requires project-config change)
|
||||
# schema: my-workflow
|
||||
```
|
||||
|
||||
### Team Sharing
|
||||
|
||||
```bash
|
||||
# Commit to repo
|
||||
git add openspec/schemas/
|
||||
git commit -m "Add custom workflow schema"
|
||||
git push
|
||||
|
||||
# Team members get it automatically
|
||||
git pull
|
||||
openspec status --change add-feature --schema my-workflow # Just works
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/core/artifact-graph/resolver.ts` | Add `getProjectSchemasDir()`, update resolution order |
|
||||
| `src/commands/artifact-workflow.ts` | Update `schemasCommand` to show source |
|
||||
|
||||
### Project Root Detection
|
||||
|
||||
Use existing `findProjectRoot()` pattern or current working directory. The project-local schemas directory is always `./openspec/schemas/` relative to project root.
|
||||
|
||||
### Source Indication
|
||||
|
||||
`listSchemasWithInfo()` returns `source: 'project' | 'user' | 'package'`. Update type definition and implementation.
|
||||
|
||||
## Testing Considerations
|
||||
|
||||
- Create temp project with local schema, verify resolution priority
|
||||
- Verify local schema overrides user override with same name
|
||||
- Verify `listSchemas()` includes project schemas
|
||||
- Verify `schemasCommand` shows correct source labels
|
||||
|
||||
## Related Changes
|
||||
|
||||
- **project-config**: Adds `config.yaml` with `schema` field that can reference project-local schemas
|
||||
@@ -0,0 +1,88 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Project-local schema resolution
|
||||
|
||||
The system SHALL resolve schemas from the project-local directory (`./openspec/schemas/<name>/`) with highest priority when a `projectRoot` is provided.
|
||||
|
||||
#### Scenario: Project-local schema takes precedence over user override
|
||||
- **WHEN** a schema named "my-workflow" exists at `./openspec/schemas/my-workflow/schema.yaml`
|
||||
- **AND** a schema named "my-workflow" exists at `~/.local/share/openspec/schemas/my-workflow/schema.yaml`
|
||||
- **AND** `getSchemaDir("my-workflow", projectRoot)` is called
|
||||
- **THEN** the system SHALL return the project-local path
|
||||
|
||||
#### Scenario: Project-local schema takes precedence over package built-in
|
||||
- **WHEN** a schema named "spec-driven" exists at `./openspec/schemas/spec-driven/schema.yaml`
|
||||
- **AND** "spec-driven" is a package built-in schema
|
||||
- **AND** `getSchemaDir("spec-driven", projectRoot)` is called
|
||||
- **THEN** the system SHALL return the project-local path
|
||||
|
||||
#### Scenario: Falls back to user override when no project-local schema
|
||||
- **WHEN** no schema named "my-workflow" exists at `./openspec/schemas/my-workflow/`
|
||||
- **AND** a schema named "my-workflow" exists at `~/.local/share/openspec/schemas/my-workflow/schema.yaml`
|
||||
- **AND** `getSchemaDir("my-workflow", projectRoot)` is called
|
||||
- **THEN** the system SHALL return the user override path
|
||||
|
||||
#### Scenario: Falls back to package built-in when no project-local or user schema
|
||||
- **WHEN** no schema named "spec-driven" exists at `./openspec/schemas/spec-driven/`
|
||||
- **AND** no schema named "spec-driven" exists at `~/.local/share/openspec/schemas/spec-driven/`
|
||||
- **AND** "spec-driven" is a package built-in schema
|
||||
- **AND** `getSchemaDir("spec-driven", projectRoot)` is called
|
||||
- **THEN** the system SHALL return the package built-in path
|
||||
|
||||
#### Scenario: Backward compatibility when projectRoot not provided
|
||||
- **WHEN** `getSchemaDir("my-workflow")` is called without a `projectRoot` parameter
|
||||
- **THEN** the system SHALL only check user override and package built-in locations
|
||||
- **AND** the system SHALL NOT check project-local location
|
||||
|
||||
### Requirement: Project schemas directory helper
|
||||
|
||||
The system SHALL provide a `getProjectSchemasDir(projectRoot)` function that returns the project-local schemas directory path.
|
||||
|
||||
#### Scenario: Returns correct path
|
||||
- **WHEN** `getProjectSchemasDir("/path/to/project")` is called
|
||||
- **THEN** the system SHALL return `/path/to/project/openspec/schemas`
|
||||
|
||||
### Requirement: List schemas includes project-local
|
||||
|
||||
The system SHALL include project-local schemas when listing available schemas if `projectRoot` is provided.
|
||||
|
||||
#### Scenario: Project-local schemas appear in list
|
||||
- **WHEN** a schema named "team-flow" exists at `./openspec/schemas/team-flow/schema.yaml`
|
||||
- **AND** `listSchemas(projectRoot)` is called
|
||||
- **THEN** the returned list SHALL include "team-flow"
|
||||
|
||||
#### Scenario: Project-local schema shadows same-named user schema in list
|
||||
- **WHEN** a schema named "custom" exists at both project-local and user override locations
|
||||
- **AND** `listSchemas(projectRoot)` is called
|
||||
- **THEN** the returned list SHALL include "custom" exactly once
|
||||
|
||||
#### Scenario: Backward compatibility for listSchemas
|
||||
- **WHEN** `listSchemas()` is called without a `projectRoot` parameter
|
||||
- **THEN** the system SHALL only include user override and package built-in schemas
|
||||
|
||||
### Requirement: Schema info includes project source
|
||||
|
||||
The system SHALL indicate `source: 'project'` for project-local schemas in `listSchemasWithInfo()` results.
|
||||
|
||||
#### Scenario: Project-local schema shows project source
|
||||
- **WHEN** a schema named "team-flow" exists at `./openspec/schemas/team-flow/schema.yaml`
|
||||
- **AND** `listSchemasWithInfo(projectRoot)` is called
|
||||
- **THEN** the schema info for "team-flow" SHALL have `source: 'project'`
|
||||
|
||||
#### Scenario: User override schema shows user source
|
||||
- **WHEN** a schema named "my-custom" exists only at `~/.local/share/openspec/schemas/my-custom/`
|
||||
- **AND** `listSchemasWithInfo(projectRoot)` is called
|
||||
- **THEN** the schema info for "my-custom" SHALL have `source: 'user'`
|
||||
|
||||
#### Scenario: Package built-in schema shows package source
|
||||
- **WHEN** "spec-driven" exists only as a package built-in
|
||||
- **AND** `listSchemasWithInfo(projectRoot)` is called
|
||||
- **THEN** the schema info for "spec-driven" SHALL have `source: 'package'`
|
||||
|
||||
### Requirement: Schemas command shows source
|
||||
|
||||
The `openspec schemas` command SHALL display the source of each schema.
|
||||
|
||||
#### Scenario: Display format includes source
|
||||
- **WHEN** user runs `openspec schemas`
|
||||
- **THEN** the output SHALL show each schema with its source label (project, user, or package)
|
||||
@@ -0,0 +1,28 @@
|
||||
## 1. Update Resolver Types and Helpers
|
||||
|
||||
- [x] 1.1 Update `SchemaInfo.source` type to include `'project'` in `src/core/artifact-graph/resolver.ts`
|
||||
- [x] 1.2 Add `getProjectSchemasDir(projectRoot: string): string` function
|
||||
|
||||
## 2. Update Schema Resolution Functions
|
||||
|
||||
- [x] 2.1 Update `getSchemaDir(name, projectRoot?)` to check project-local first when projectRoot provided
|
||||
- [x] 2.2 Update `resolveSchema(name, projectRoot?)` to pass projectRoot to getSchemaDir
|
||||
- [x] 2.3 Update `listSchemas(projectRoot?)` to include project-local schemas
|
||||
- [x] 2.4 Update `listSchemasWithInfo(projectRoot?)` to include project schemas with `source: 'project'`
|
||||
|
||||
## 3. Update CLI Commands
|
||||
|
||||
- [x] 3.1 Update `schemasCommand` to pass projectRoot and display source labels in output
|
||||
|
||||
## 4. Update Call Sites
|
||||
|
||||
- [x] 4.1 Review and update call sites that need project-local schema support to pass projectRoot
|
||||
|
||||
## 5. Testing
|
||||
|
||||
- [x] 5.1 Add unit tests for `getProjectSchemasDir()`
|
||||
- [x] 5.2 Add unit tests for project-local schema resolution priority
|
||||
- [x] 5.3 Add unit tests for backward compatibility (no projectRoot = user + package only)
|
||||
- [x] 5.4 Add unit tests for `listSchemas()` including project schemas
|
||||
- [x] 5.5 Add unit tests for `listSchemasWithInfo()` with `source: 'project'`
|
||||
- [x] 5.6 Add integration test with temp project containing local schema
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-20
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
We want to rename `spec-driven` to `openspec-default` to better reflect that it's the standard/default workflow. However, renaming directly would break existing projects that have `schema: spec-driven` in their `openspec/config.yaml`. Adding alias support allows both names to work interchangeably, enabling a smooth transition with no breaking changes.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add schema alias resolution in the schema resolver
|
||||
- `openspec-default` and `spec-driven` will both resolve to the same schema
|
||||
- The physical directory remains `schemas/spec-driven/` (or could be renamed to `schemas/openspec-default/` with `spec-driven` as the alias)
|
||||
- All CLI commands and config files accept either name
|
||||
- No changes required to existing user configs
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `schema-aliases`: Support for schema name aliases so multiple names can resolve to the same schema directory
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- No existing spec-level behavior is changing - this is purely additive -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/core/artifact-graph/resolver.ts` - Add alias resolution logic
|
||||
- `schemas/` directory - Potentially rename `spec-driven` to `openspec-default`
|
||||
- Documentation - Update to prefer `openspec-default` while noting `spec-driven` still works
|
||||
- Default schema constants - Update `DEFAULT_SCHEMA` to `openspec-default`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-20
|
||||
@@ -0,0 +1,113 @@
|
||||
## Context
|
||||
|
||||
OpenSpec uses workflow schemas to define artifact sequences for change proposals. Currently, schemas are resolved from three locations (project → user → package), but managing custom schemas requires manual file creation with no tooling support. The resolver infrastructure exists (`src/core/artifact-graph/resolver.ts`) but there's no CLI exposure for schema management operations.
|
||||
|
||||
Users who want to customize workflows must:
|
||||
1. Manually create directory structures under `openspec/schemas/<name>/`
|
||||
2. Copy and modify `schema.yaml` files without validation
|
||||
3. Debug resolution issues by inspecting the filesystem directly
|
||||
|
||||
This creates friction for schema customization and leads to runtime errors when schemas are malformed.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Provide CLI commands for common schema management operations
|
||||
- Enable interactive schema creation with guided prompts
|
||||
- Allow forking existing schemas as customization starting points
|
||||
- Surface schema validation errors before runtime
|
||||
- Help debug schema resolution order when shadowing occurs
|
||||
|
||||
**Non-Goals:**
|
||||
- Schema editing (users edit YAML directly or via `$EDITOR`)
|
||||
- Schema publishing or sharing mechanisms
|
||||
- Schema versioning or migration tooling
|
||||
- Validation of template file contents (only checks existence)
|
||||
- Schema inheritance or composition beyond simple forking
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Command Structure: `openspec schema <subcommand>`
|
||||
|
||||
Add a new command group following the existing pattern used by `openspec config` and `openspec completion`.
|
||||
|
||||
**Rationale:** Grouping related commands under a noun (schema) matches the established CLI patterns and provides a natural namespace for future schema operations.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Flat commands (`openspec schema-init`, `openspec schema-fork`): Rejected because it pollutes the top-level namespace and doesn't scale well.
|
||||
- Extending existing commands (`openspec init --schema`): Rejected because schema management is distinct from project initialization.
|
||||
|
||||
### 2. Implementation Location
|
||||
|
||||
New file `src/commands/schema.ts` with a `registerSchemaCommand(program: Command)` function that registers the `schema` command group and all subcommands.
|
||||
|
||||
**Rationale:** Follows the pattern established by `config.ts` and matches how other command groups are organized.
|
||||
|
||||
### 3. Schema Validation Approach
|
||||
|
||||
Validation checks:
|
||||
1. `schema.yaml` exists and is valid YAML
|
||||
2. Parses successfully against the Zod schema in `types.ts`
|
||||
3. All referenced template files exist in the schema directory
|
||||
4. Artifact dependency graph has no cycles (use existing topological sort)
|
||||
|
||||
**Rationale:** Reuse existing validation infrastructure (`parseSchema` from `schema.ts`) and extend with template existence checks. This catches the most common errors without duplicating validation logic.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Deep template validation (check frontmatter, syntax): Rejected as over-engineering. Template contents are free-form markdown.
|
||||
|
||||
### 4. Interactive Prompts for `schema init`
|
||||
|
||||
Use `@inquirer/prompts` (already a dependency) for:
|
||||
- Schema name input with kebab-case validation
|
||||
- Schema description input
|
||||
- Multi-select for artifact selection with descriptions
|
||||
- Optional: set as project default
|
||||
|
||||
**Rationale:** Matches the UX established by `openspec init` and `openspec config reset`. Provides a guided experience while keeping the wizard lightweight.
|
||||
|
||||
### 5. Fork Source Resolution
|
||||
|
||||
`schema fork <source>` resolves the source schema using the existing `getSchemaDir()` function, respecting the full resolution order (project → user → package). This allows forking from any accessible schema.
|
||||
|
||||
The destination is always project-local: `openspec/schemas/<name>/`
|
||||
|
||||
**Rationale:** Forking to project scope makes sense because:
|
||||
- Custom schemas are project-specific decisions
|
||||
- User-global schemas can be added manually if needed
|
||||
- Keeps the command simple with a clear default
|
||||
|
||||
### 6. Output Format Consistency
|
||||
|
||||
All commands support `--json` flag for machine-readable output:
|
||||
- `schema init`: Outputs `{ "created": true, "path": "...", "schema": "..." }`
|
||||
- `schema fork`: Outputs `{ "forked": true, "source": "...", "destination": "..." }`
|
||||
- `schema validate`: Outputs validation report matching existing validate command format
|
||||
- `schema which`: Outputs `{ "name": "...", "source": "project|user|package", "path": "..." }`
|
||||
|
||||
Text output uses ora spinners for progress and clear success/error messaging.
|
||||
|
||||
**Rationale:** Consistent with existing OpenSpec commands and enables scripting/automation.
|
||||
|
||||
### 7. Schema `which` Command Design
|
||||
|
||||
Shows resolution details for a schema name:
|
||||
- Which location it resolves from (project/user/package)
|
||||
- Full path to the schema directory
|
||||
- Whether it shadows other schemas at lower priority levels
|
||||
|
||||
**Rationale:** Essential for debugging "why isn't my schema being used?" scenarios when multiple schemas with the same name exist.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Template scaffolding may become stale]** → The `schema init` command will scaffold a default set of artifacts (proposal, specs, design, tasks). If the built-in schema patterns evolve, these templates may not reflect best practices.
|
||||
- *Mitigation*: Document that `init` creates a minimal starting point. Users can `fork` built-in schemas for the latest patterns.
|
||||
|
||||
**[Interactive prompts in CI environments]** → `schema init` with prompts may hang in non-interactive environments.
|
||||
- *Mitigation*: Support `--name`, `--description`, and `--artifacts` flags for non-interactive use. Detect TTY and show helpful error if prompts would hang.
|
||||
|
||||
**[Validation doesn't catch all errors]** → Schema validation checks structure but can't verify semantic correctness (e.g., a template that doesn't match its artifact purpose).
|
||||
- *Mitigation*: This is acceptable. Full semantic validation would require understanding template intent, which is out of scope.
|
||||
|
||||
**[Fork overwrites without warning]** → If target schema already exists, `fork` could overwrite it.
|
||||
- *Mitigation*: Check for existing schema and require `--force` flag or interactive confirmation before overwriting.
|
||||
@@ -0,0 +1,55 @@
|
||||
## Why
|
||||
|
||||
Creating and managing project-local schemas currently requires manual directory creation, copying files, and hoping the structure is correct. Users only discover structural errors at runtime when commands fail. This friction discourages schema customization and makes it harder to tailor OpenSpec workflows to specific project needs.
|
||||
|
||||
Key pain points:
|
||||
- **Manual scaffolding**: Users must manually create `openspec/schemas/<name>/` with correct structure
|
||||
- **No validation feedback**: Schema errors aren't caught until a command tries to use the schema
|
||||
- **Starting from scratch is hard**: No easy way to base a custom schema on an existing one
|
||||
- **Debugging resolution**: When a schema doesn't resolve as expected, there's no way to see the resolution path
|
||||
|
||||
## What Changes
|
||||
|
||||
Add a new `openspec schema` command group with subcommands for creating, forking, validating, and inspecting schemas.
|
||||
|
||||
### Commands
|
||||
|
||||
1. **`openspec schema init <name>`** - Interactive wizard to scaffold a new project schema
|
||||
- Prompts for schema description
|
||||
- Prompts for artifacts to include (with explanations)
|
||||
- Creates valid directory structure with `schema.yaml` and template files
|
||||
- Optionally sets as project default in `openspec/config.yaml`
|
||||
|
||||
2. **`openspec schema fork <source> [name]`** - Copy an existing schema as a starting point
|
||||
- Copies from user override or package built-in
|
||||
- Allows renaming (defaults to `<source>-custom`)
|
||||
- Preserves all templates and configuration
|
||||
|
||||
3. **`openspec schema validate [name]`** - Validate schema structure and templates
|
||||
- Checks `schema.yaml` is valid
|
||||
- Verifies all referenced templates exist
|
||||
- Reports missing or malformed files
|
||||
- Run without name to validate all project schemas
|
||||
|
||||
4. **`openspec schema which <name>`** - Show schema resolution path
|
||||
- Displays which location the schema resolves from (project/user/package)
|
||||
- Shows full path to schema directory
|
||||
- Useful for debugging shadowing issues
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `schema-init-command`: Interactive wizard for creating new project schemas with guided prompts
|
||||
- `schema-fork-command`: Copy existing schemas to project for customization
|
||||
- `schema-validate-command`: Validate schema structure and report errors before runtime
|
||||
- `schema-which-command`: Debug schema resolution by showing which location is used
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- None - these are additive commands -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Code**: New command implementations in `src/commands/` using existing resolver infrastructure
|
||||
- **CLI**: New `schema` command group with 4 subcommands
|
||||
- **Dependencies**: May use `enquirer` or similar for interactive prompts in `schema init`
|
||||
- **Documentation**: Need to update CLI reference and schema customization guide
|
||||
@@ -0,0 +1,66 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Schema fork copies existing schema
|
||||
The CLI SHALL provide an `openspec schema fork <source> [name]` command that copies an existing schema to the project's `openspec/schemas/` directory.
|
||||
|
||||
#### Scenario: Fork with explicit name
|
||||
- **WHEN** user runs `openspec schema fork spec-driven my-custom`
|
||||
- **THEN** system locates `spec-driven` schema using resolution order (project → user → package)
|
||||
- **AND** copies all files to `openspec/schemas/my-custom/`
|
||||
- **AND** updates `name` field in `schema.yaml` to `my-custom`
|
||||
- **AND** displays success message with source and destination paths
|
||||
|
||||
#### Scenario: Fork with default name
|
||||
- **WHEN** user runs `openspec schema fork spec-driven` without specifying a name
|
||||
- **THEN** system copies to `openspec/schemas/spec-driven-custom/`
|
||||
- **AND** updates `name` field in `schema.yaml` to `spec-driven-custom`
|
||||
|
||||
#### Scenario: Source schema not found
|
||||
- **WHEN** user runs `openspec schema fork nonexistent`
|
||||
- **THEN** system displays error that schema was not found
|
||||
- **AND** lists available schemas
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
### Requirement: Schema fork prevents accidental overwrites
|
||||
The CLI SHALL require confirmation or `--force` flag when the destination schema already exists.
|
||||
|
||||
#### Scenario: Destination exists without force
|
||||
- **WHEN** user runs `openspec schema fork spec-driven my-custom` and `openspec/schemas/my-custom/` exists
|
||||
- **THEN** system displays error that destination already exists
|
||||
- **AND** suggests using `--force` to overwrite
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
#### Scenario: Destination exists with force flag
|
||||
- **WHEN** user runs `openspec schema fork spec-driven my-custom --force` and destination exists
|
||||
- **THEN** system removes existing destination directory
|
||||
- **AND** copies source schema to destination
|
||||
- **AND** displays success message
|
||||
|
||||
#### Scenario: Interactive confirmation for overwrite
|
||||
- **WHEN** user runs `openspec schema fork spec-driven my-custom` in interactive mode and destination exists
|
||||
- **THEN** system prompts for confirmation to overwrite
|
||||
- **AND** proceeds based on user response
|
||||
|
||||
### Requirement: Schema fork preserves all schema files
|
||||
The CLI SHALL copy the complete schema directory including templates, configuration, and any additional files.
|
||||
|
||||
#### Scenario: Copy includes template files
|
||||
- **WHEN** user forks a schema with template files (e.g., `proposal.md`, `design.md`)
|
||||
- **THEN** all template files are copied to the destination
|
||||
- **AND** template file contents are unchanged
|
||||
|
||||
#### Scenario: Copy includes nested directories
|
||||
- **WHEN** user forks a schema with nested directories (e.g., `templates/specs/`)
|
||||
- **THEN** nested directory structure is preserved
|
||||
- **AND** all nested files are copied
|
||||
|
||||
### Requirement: Schema fork outputs JSON format
|
||||
The CLI SHALL support `--json` flag for machine-readable output.
|
||||
|
||||
#### Scenario: JSON output on success
|
||||
- **WHEN** user runs `openspec schema fork spec-driven my-custom --json`
|
||||
- **THEN** system outputs JSON with `forked: true`, `source`, `destination`, and `sourcePath` fields
|
||||
|
||||
#### Scenario: JSON output shows source location
|
||||
- **WHEN** user runs `openspec schema fork spec-driven --json`
|
||||
- **THEN** JSON output includes `sourceLocation` field indicating "project", "user", or "package"
|
||||
@@ -0,0 +1,71 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Schema init command creates project-local schema
|
||||
The CLI SHALL provide an `openspec schema init <name>` command that creates a new schema directory under `openspec/schemas/<name>/` with a valid `schema.yaml` file and default template files.
|
||||
|
||||
#### Scenario: Create schema with valid name
|
||||
- **WHEN** user runs `openspec schema init my-workflow`
|
||||
- **THEN** system creates directory `openspec/schemas/my-workflow/`
|
||||
- **AND** creates `schema.yaml` with name, version, description, and artifacts array
|
||||
- **AND** creates template files referenced by artifacts
|
||||
- **AND** displays success message with created path
|
||||
|
||||
#### Scenario: Reject invalid schema name
|
||||
- **WHEN** user runs `openspec schema init "My Workflow"` (contains space)
|
||||
- **THEN** system displays error about invalid schema name
|
||||
- **AND** suggests using kebab-case format
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
#### Scenario: Schema name already exists
|
||||
- **WHEN** user runs `openspec schema init existing-schema` and `openspec/schemas/existing-schema/` already exists
|
||||
- **THEN** system displays error that schema already exists
|
||||
- **AND** suggests using `--force` to overwrite or `schema fork` to copy
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
### Requirement: Schema init supports interactive mode
|
||||
The CLI SHALL prompt for schema configuration when run in an interactive terminal without explicit flags.
|
||||
|
||||
#### Scenario: Interactive prompts for description
|
||||
- **WHEN** user runs `openspec schema init my-workflow` in an interactive terminal
|
||||
- **THEN** system prompts for schema description
|
||||
- **AND** uses provided description in generated `schema.yaml`
|
||||
|
||||
#### Scenario: Interactive prompts for artifact selection
|
||||
- **WHEN** user runs `openspec schema init my-workflow` in an interactive terminal
|
||||
- **THEN** system displays multi-select prompt with common artifacts (proposal, specs, design, tasks)
|
||||
- **AND** each option includes a brief description
|
||||
- **AND** uses selected artifacts in generated `schema.yaml`
|
||||
|
||||
#### Scenario: Non-interactive mode with flags
|
||||
- **WHEN** user runs `openspec schema init my-workflow --description "My workflow" --artifacts proposal,tasks`
|
||||
- **THEN** system creates schema without prompting
|
||||
- **AND** uses flag values for configuration
|
||||
|
||||
### Requirement: Schema init supports setting project default
|
||||
The CLI SHALL offer to set the newly created schema as the project default.
|
||||
|
||||
#### Scenario: Set as default interactively
|
||||
- **WHEN** user runs `openspec schema init my-workflow` in interactive mode
|
||||
- **AND** user confirms setting as default
|
||||
- **THEN** system updates `openspec/config.yaml` with `defaultSchema: my-workflow`
|
||||
|
||||
#### Scenario: Set as default via flag
|
||||
- **WHEN** user runs `openspec schema init my-workflow --default`
|
||||
- **THEN** system creates schema and updates `openspec/config.yaml` with `defaultSchema: my-workflow`
|
||||
|
||||
#### Scenario: Skip setting default
|
||||
- **WHEN** user runs `openspec schema init my-workflow --no-default`
|
||||
- **THEN** system creates schema without modifying `openspec/config.yaml`
|
||||
|
||||
### Requirement: Schema init outputs JSON format
|
||||
The CLI SHALL support `--json` flag for machine-readable output.
|
||||
|
||||
#### Scenario: JSON output on success
|
||||
- **WHEN** user runs `openspec schema init my-workflow --json --description "Test" --artifacts proposal`
|
||||
- **THEN** system outputs JSON with `created: true`, `path`, and `schema` fields
|
||||
- **AND** does not display interactive prompts or spinners
|
||||
|
||||
#### Scenario: JSON output on error
|
||||
- **WHEN** user runs `openspec schema init "invalid name" --json`
|
||||
- **THEN** system outputs JSON with `error` field describing the issue
|
||||
- **AND** exits with non-zero code
|
||||
@@ -0,0 +1,86 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Schema validate checks schema structure
|
||||
The CLI SHALL provide an `openspec schema validate [name]` command that validates schema configuration and reports errors.
|
||||
|
||||
#### Scenario: Validate specific schema
|
||||
- **WHEN** user runs `openspec schema validate my-workflow`
|
||||
- **THEN** system locates schema using resolution order
|
||||
- **AND** validates `schema.yaml` against the schema Zod type
|
||||
- **AND** displays validation result (valid or list of errors)
|
||||
|
||||
#### Scenario: Validate all project schemas
|
||||
- **WHEN** user runs `openspec schema validate` without a name
|
||||
- **THEN** system validates all schemas in `openspec/schemas/`
|
||||
- **AND** displays results for each schema
|
||||
- **AND** exits with non-zero code if any schema is invalid
|
||||
|
||||
#### Scenario: Schema not found
|
||||
- **WHEN** user runs `openspec schema validate nonexistent`
|
||||
- **THEN** system displays error that schema was not found
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
### Requirement: Schema validate checks YAML syntax
|
||||
The CLI SHALL report YAML parsing errors with line numbers when possible.
|
||||
|
||||
#### Scenario: Invalid YAML syntax
|
||||
- **WHEN** user runs `openspec schema validate my-workflow` and `schema.yaml` has syntax errors
|
||||
- **THEN** system displays YAML parse error with line number
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
#### Scenario: Valid YAML but missing required fields
|
||||
- **WHEN** `schema.yaml` is valid YAML but missing `name` field
|
||||
- **THEN** system displays Zod validation error for missing required field
|
||||
- **AND** identifies the specific missing field
|
||||
|
||||
### Requirement: Schema validate checks template existence
|
||||
The CLI SHALL verify that all template files referenced by artifacts exist.
|
||||
|
||||
#### Scenario: Missing template file
|
||||
- **WHEN** artifact references `template: proposal.md` but file doesn't exist in schema directory
|
||||
- **THEN** system reports error: "Template file 'proposal.md' not found for artifact 'proposal'"
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
#### Scenario: All templates exist
|
||||
- **WHEN** all artifact templates exist
|
||||
- **THEN** system reports that templates are valid
|
||||
- **AND** template existence is included in validation summary
|
||||
|
||||
### Requirement: Schema validate checks dependency graph
|
||||
The CLI SHALL verify that artifact dependencies form a valid directed acyclic graph.
|
||||
|
||||
#### Scenario: Valid dependency graph
|
||||
- **WHEN** artifact dependencies form a valid DAG (e.g., tasks → specs → proposal)
|
||||
- **THEN** system reports dependency graph is valid
|
||||
|
||||
#### Scenario: Circular dependency detected
|
||||
- **WHEN** artifact A requires B and artifact B requires A
|
||||
- **THEN** system reports circular dependency error
|
||||
- **AND** identifies the artifacts involved in the cycle
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
#### Scenario: Unknown dependency reference
|
||||
- **WHEN** artifact requires `nonexistent-artifact`
|
||||
- **THEN** system reports error: "Artifact 'x' requires unknown artifact 'nonexistent-artifact'"
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
### Requirement: Schema validate outputs JSON format
|
||||
The CLI SHALL support `--json` flag for machine-readable validation results.
|
||||
|
||||
#### Scenario: JSON output for valid schema
|
||||
- **WHEN** user runs `openspec schema validate my-workflow --json` and schema is valid
|
||||
- **THEN** system outputs JSON with `valid: true`, `name`, and `path` fields
|
||||
|
||||
#### Scenario: JSON output for invalid schema
|
||||
- **WHEN** user runs `openspec schema validate my-workflow --json` and schema has errors
|
||||
- **THEN** system outputs JSON with `valid: false` and `issues` array
|
||||
- **AND** each issue includes `level`, `path`, and `message` fields
|
||||
- **AND** format matches existing `openspec validate` output structure
|
||||
|
||||
### Requirement: Schema validate supports verbose mode
|
||||
The CLI SHALL support `--verbose` flag for detailed validation information.
|
||||
|
||||
#### Scenario: Verbose output shows all checks
|
||||
- **WHEN** user runs `openspec schema validate my-workflow --verbose`
|
||||
- **THEN** system displays each validation check as it runs
|
||||
- **AND** shows pass/fail status for: YAML parsing, Zod validation, template existence, dependency graph
|
||||
@@ -0,0 +1,65 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Schema which shows resolution result
|
||||
The CLI SHALL provide an `openspec schema which <name>` command that displays where a schema resolves from.
|
||||
|
||||
#### Scenario: Schema resolves from project
|
||||
- **WHEN** user runs `openspec schema which my-workflow` and schema exists in `openspec/schemas/my-workflow/`
|
||||
- **THEN** system displays source as "project"
|
||||
- **AND** displays full path to schema directory
|
||||
|
||||
#### Scenario: Schema resolves from user directory
|
||||
- **WHEN** user runs `openspec schema which my-workflow` and schema exists only in user data directory
|
||||
- **THEN** system displays source as "user"
|
||||
- **AND** displays full path including XDG data directory
|
||||
|
||||
#### Scenario: Schema resolves from package
|
||||
- **WHEN** user runs `openspec schema which spec-driven` and no override exists
|
||||
- **THEN** system displays source as "package"
|
||||
- **AND** displays full path to package's schemas directory
|
||||
|
||||
#### Scenario: Schema not found
|
||||
- **WHEN** user runs `openspec schema which nonexistent`
|
||||
- **THEN** system displays error that schema was not found
|
||||
- **AND** lists available schemas
|
||||
- **AND** exits with non-zero code
|
||||
|
||||
### Requirement: Schema which shows shadowing information
|
||||
The CLI SHALL indicate when a schema shadows another schema at a lower priority level.
|
||||
|
||||
#### Scenario: Project schema shadows package
|
||||
- **WHEN** user runs `openspec schema which spec-driven` and both project and package have `spec-driven`
|
||||
- **THEN** system displays that project schema is active
|
||||
- **AND** indicates it shadows the package version
|
||||
- **AND** shows path to shadowed package schema
|
||||
|
||||
#### Scenario: No shadowing
|
||||
- **WHEN** schema exists only in one location
|
||||
- **THEN** system does not display shadowing information
|
||||
|
||||
#### Scenario: Multiple shadows
|
||||
- **WHEN** project schema shadows both user and package schemas
|
||||
- **THEN** system lists all shadowed locations in priority order
|
||||
|
||||
### Requirement: Schema which outputs JSON format
|
||||
The CLI SHALL support `--json` flag for machine-readable output.
|
||||
|
||||
#### Scenario: JSON output basic
|
||||
- **WHEN** user runs `openspec schema which spec-driven --json`
|
||||
- **THEN** system outputs JSON with `name`, `source`, and `path` fields
|
||||
|
||||
#### Scenario: JSON output with shadows
|
||||
- **WHEN** user runs `openspec schema which spec-driven --json` and schema has shadows
|
||||
- **THEN** JSON includes `shadows` array with `source` and `path` for each shadowed schema
|
||||
|
||||
### Requirement: Schema which supports list mode
|
||||
The CLI SHALL support listing all schemas with their resolution sources.
|
||||
|
||||
#### Scenario: List all schemas
|
||||
- **WHEN** user runs `openspec schema which --all`
|
||||
- **THEN** system displays all available schemas grouped by source
|
||||
- **AND** indicates which schemas shadow others
|
||||
|
||||
#### Scenario: List in JSON format
|
||||
- **WHEN** user runs `openspec schema which --all --json`
|
||||
- **THEN** system outputs JSON array with resolution info for each schema
|
||||
@@ -0,0 +1,67 @@
|
||||
## 1. Setup and Command Structure
|
||||
|
||||
- [x] 1.1 Create `src/commands/schema.ts` with `registerSchemaCommand(program: Command)` function
|
||||
- [x] 1.2 Register schema command in `src/cli/index.ts` (import and call `registerSchemaCommand`)
|
||||
- [x] 1.3 Add schema command group with description: "Manage workflow schemas"
|
||||
|
||||
## 2. Schema Which Command
|
||||
|
||||
- [x] 2.1 Add `schema which <name>` subcommand with `--json` and `--all` options
|
||||
- [x] 2.2 Implement resolution lookup using `getSchemaDir()` with project root
|
||||
- [x] 2.3 Implement shadow detection by checking all three locations (project, user, package)
|
||||
- [x] 2.4 Add text output: show source, path, and shadowing info
|
||||
- [x] 2.5 Add JSON output: `{ name, source, path, shadows: [] }`
|
||||
- [x] 2.6 Add `--all` mode to list all schemas with their resolution sources
|
||||
|
||||
## 3. Schema Validate Command
|
||||
|
||||
- [x] 3.1 Add `schema validate [name]` subcommand with `--json` and `--verbose` options
|
||||
- [x] 3.2 Implement single-schema validation using existing `parseSchema()` from `schema.ts`
|
||||
- [x] 3.3 Add template existence check for each artifact's template file
|
||||
- [x] 3.4 Add dependency graph cycle detection (reuse topological sort logic)
|
||||
- [x] 3.5 Add validate-all mode when no name provided (scan `openspec/schemas/`)
|
||||
- [x] 3.6 Add text output with pass/fail indicators and error messages
|
||||
- [x] 3.7 Add JSON output matching existing `openspec validate` format: `{ valid, issues: [] }`
|
||||
- [x] 3.8 Add verbose mode showing each validation step
|
||||
|
||||
## 4. Schema Fork Command
|
||||
|
||||
- [x] 4.1 Add `schema fork <source> [name]` subcommand with `--json` and `--force` options
|
||||
- [x] 4.2 Implement source resolution using `getSchemaDir()` with project root
|
||||
- [x] 4.3 Implement default destination naming: `<source>-custom`
|
||||
- [x] 4.4 Implement directory copy with recursive file copy
|
||||
- [x] 4.5 Update `name` field in copied `schema.yaml`
|
||||
- [x] 4.6 Add overwrite protection: check destination exists, require `--force` or confirmation
|
||||
- [x] 4.7 Add text output with source/destination paths
|
||||
- [x] 4.8 Add JSON output: `{ forked, source, destination, sourceLocation }`
|
||||
|
||||
## 5. Schema Init Command
|
||||
|
||||
- [x] 5.1 Add `schema init <name>` subcommand with `--json`, `--description`, `--artifacts`, `--default`, `--no-default`, `--force` options
|
||||
- [x] 5.2 Implement schema name validation (kebab-case, no spaces)
|
||||
- [x] 5.3 Implement interactive prompts for description using `@inquirer/prompts`
|
||||
- [x] 5.4 Implement interactive artifact selection with descriptions (multi-select)
|
||||
- [x] 5.5 Create schema directory and `schema.yaml` with selected configuration
|
||||
- [x] 5.6 Create default template files for selected artifacts
|
||||
- [x] 5.7 Add `--default` flag to update `openspec/config.yaml` with new schema as default
|
||||
- [x] 5.8 Add overwrite protection: check if schema exists, require `--force`
|
||||
- [x] 5.9 Add text output with created path and next steps
|
||||
- [x] 5.10 Add JSON output: `{ created, path, schema }`
|
||||
- [x] 5.11 Add non-interactive mode with `--description` and `--artifacts` flags
|
||||
|
||||
## 6. Testing
|
||||
|
||||
- [x] 6.1 Add unit tests for `schema which` command in `test/commands/schema.test.ts`
|
||||
- [x] 6.2 Add unit tests for `schema validate` command
|
||||
- [x] 6.3 Add unit tests for `schema fork` command
|
||||
- [x] 6.4 Add unit tests for `schema init` command
|
||||
- [x] 6.5 Test interactive mode mocking with `@inquirer/prompts`
|
||||
- [x] 6.6 Test JSON output format for all commands
|
||||
- [x] 6.7 Test error cases: invalid name, not found, already exists, cycle detection
|
||||
|
||||
## 7. Documentation and Polish
|
||||
|
||||
- [x] 7.1 Add CLI help text for all schema subcommands
|
||||
- [x] 7.2 Update shell completion to include schema commands
|
||||
- [x] 7.3 Run linting and fix any issues (`npm run lint`)
|
||||
- [x] 7.4 Run full test suite (`npm test`)
|
||||
@@ -0,0 +1,24 @@
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: TypeScript, Node.js (≥20.19.0), ESM modules
|
||||
Package manager: pnpm
|
||||
CLI framework: Commander.js
|
||||
|
||||
Cross-platform requirements:
|
||||
- This tool runs on macOS, Linux, AND Windows
|
||||
- Always use path.join() or path.resolve() for file paths - never hardcode slashes
|
||||
- Never assume forward-slash path separators
|
||||
- Tests must use path.join() for expected path values, not hardcoded strings
|
||||
- Consider case sensitivity differences in file systems
|
||||
|
||||
rules:
|
||||
specs:
|
||||
- Include scenarios for Windows path handling when dealing with file paths
|
||||
- Requirements involving paths must specify cross-platform behavior
|
||||
tasks:
|
||||
- Add Windows CI verification as a task when changes involve file paths
|
||||
- Include cross-platform testing considerations
|
||||
design:
|
||||
- Document any platform-specific behavior or limitations
|
||||
- Prefer Node.js path module over string manipulation for paths
|
||||
@@ -0,0 +1,107 @@
|
||||
# ci-nix-validation Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Validates Nix flake builds and maintenance scripts in CI to ensure Nix users can reliably install and use OpenSpec. Prevents regressions in Nix support by testing builds and the update-flake.sh script on every pull request and push to main.
|
||||
## Requirements
|
||||
### Requirement: Nix Flake Build Validation
|
||||
|
||||
The CI system SHALL validate that the Nix flake builds successfully on every pull request and push to main.
|
||||
|
||||
#### Scenario: Successful flake build
|
||||
|
||||
- **WHEN** a pull request or push to main is made
|
||||
- **THEN** the CI SHALL execute `nix build` and verify it completes with exit code 0
|
||||
- **AND** the build output SHALL contain the openspec binary
|
||||
|
||||
#### Scenario: Flake build failure
|
||||
|
||||
- **WHEN** the Nix flake configuration is broken
|
||||
- **THEN** the CI job SHALL fail with a non-zero exit code
|
||||
- **AND** the CI SHALL prevent merging of the pull request
|
||||
|
||||
#### Scenario: Multi-platform support check
|
||||
|
||||
- **WHEN** the flake declares support for multiple systems
|
||||
- **THEN** the CI SHALL validate the flake builds on at least Linux (x86_64-linux)
|
||||
|
||||
### Requirement: Update Script Validation
|
||||
|
||||
The CI system SHALL validate that the update-flake.sh script executes successfully and produces valid output.
|
||||
|
||||
#### Scenario: Update script execution
|
||||
|
||||
- **WHEN** the CI runs the update script validation
|
||||
- **THEN** the script SHALL execute without errors
|
||||
- **AND** the script SHALL correctly extract the version from package.json
|
||||
- **AND** the script SHALL update flake.nix with the correct version
|
||||
|
||||
#### Scenario: Update script with mock hash
|
||||
|
||||
- **WHEN** validating the update script in CI
|
||||
- **THEN** the script SHALL be able to detect and extract the correct pnpm dependency hash
|
||||
- **AND** the flake.nix SHALL be updated with a valid sha256 hash
|
||||
|
||||
### Requirement: CI Job Integration
|
||||
|
||||
The Nix validation jobs SHALL be integrated into the existing GitHub Actions workflow and required for merge.
|
||||
|
||||
#### Scenario: PR merge requirements
|
||||
|
||||
- **WHEN** a pull request is created
|
||||
- **THEN** the Nix validation job SHALL be included in required checks
|
||||
- **AND** the PR SHALL NOT be mergeable until Nix validation passes
|
||||
|
||||
#### Scenario: Job execution triggers
|
||||
|
||||
- **WHEN** code is pushed to a pull request OR pushed to main OR manually triggered
|
||||
- **THEN** the Nix validation job SHALL execute automatically
|
||||
|
||||
### Requirement: Local Testing Support
|
||||
|
||||
The CI workflow SHALL be testable locally using the `act` tool to enable rapid iteration.
|
||||
|
||||
#### Scenario: Local CI execution with act
|
||||
|
||||
- **WHEN** a developer runs `act` with the Nix validation workflow
|
||||
- **THEN** the workflow SHALL execute in the local Docker environment
|
||||
- **AND** the developer SHALL receive feedback on Nix build status without pushing to GitHub
|
||||
|
||||
#### Scenario: Act configuration compatibility
|
||||
|
||||
- **WHEN** the workflow is designed
|
||||
- **THEN** it SHALL use standard GitHub Actions syntax compatible with `act`
|
||||
- **AND** any Nix-specific setup SHALL work in the act Docker environment
|
||||
|
||||
### Requirement: Nix Installation in CI
|
||||
|
||||
The CI environment SHALL have Nix properly installed and configured before running validation.
|
||||
|
||||
#### Scenario: Nix installation step
|
||||
|
||||
- **WHEN** the Nix validation job starts
|
||||
- **THEN** Nix SHALL be installed using the official Nix installer or determinatesystems/nix-installer-action
|
||||
- **AND** the Nix installation SHALL be cached for subsequent runs to improve performance
|
||||
|
||||
#### Scenario: Nix configuration for CI
|
||||
|
||||
- **WHEN** Nix is installed in CI
|
||||
- **THEN** it SHALL be configured to work in the GitHub Actions environment
|
||||
- **AND** experimental features (flakes, nix-command) SHALL be enabled
|
||||
|
||||
### Requirement: CI Performance Optimization
|
||||
|
||||
The Nix validation SHALL be optimized to minimize CI runtime impact.
|
||||
|
||||
#### Scenario: Acceptable runtime
|
||||
|
||||
- **WHEN** the Nix validation job runs
|
||||
- **THEN** it SHALL complete in under 5 minutes on a clean run
|
||||
- **AND** with caching, it SHALL complete in under 3 minutes on subsequent runs
|
||||
|
||||
#### Scenario: Parallel execution
|
||||
|
||||
- **WHEN** multiple CI jobs are running
|
||||
- **THEN** the Nix validation job SHALL run in parallel with other validation jobs (tests, lint)
|
||||
- **AND** SHALL NOT block other independent checks
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# cli-completion Specification
|
||||
|
||||
## Purpose
|
||||
Provide shell completion scripts for the OpenSpec CLI, enabling tab-completion for commands, flags, and dynamic values (change IDs, spec IDs) in supported shells. Currently supports Zsh with architecture designed for future shell expansion.
|
||||
Provide shell completion scripts for the OpenSpec CLI, enabling tab-completion for commands, flags, and dynamic values (change IDs, spec IDs) across multiple shells. Supports Zsh, Bash, Fish, and PowerShell.
|
||||
## Requirements
|
||||
### Requirement: Native Shell Behavior Integration
|
||||
|
||||
The completion system SHALL respect and integrate with Zsh's native completion patterns and user interaction model.
|
||||
The completion system SHALL respect and integrate with each supported shell's native completion patterns and user interaction model.
|
||||
|
||||
#### Scenario: Zsh native completion
|
||||
|
||||
@@ -15,12 +15,36 @@ The completion system SHALL respect and integrate with Zsh's native completion p
|
||||
- **AND** display as an interactive menu that users navigate with TAB/arrow keys
|
||||
- **AND** support Oh My Zsh's enhanced menu styling automatically
|
||||
|
||||
#### Scenario: Bash native completion
|
||||
|
||||
- **WHEN** generating Bash completion scripts
|
||||
- **THEN** use Bash completion with `complete` builtin and `COMPREPLY` array
|
||||
- **AND** completions SHALL trigger on double TAB (standard Bash behavior)
|
||||
- **AND** display as space-separated list or column format
|
||||
- **AND** support both bash-completion v1 and v2 patterns
|
||||
|
||||
#### Scenario: Fish native completion
|
||||
|
||||
- **WHEN** generating Fish completion scripts
|
||||
- **THEN** use Fish's `complete` command with conditions
|
||||
- **AND** completions SHALL trigger on single TAB with auto-suggestion preview
|
||||
- **AND** display with Fish's native coloring and description alignment
|
||||
- **AND** leverage Fish's built-in caching automatically
|
||||
|
||||
#### Scenario: PowerShell native completion
|
||||
|
||||
- **WHEN** generating PowerShell completion scripts
|
||||
- **THEN** use `Register-ArgumentCompleter` with scriptblock
|
||||
- **AND** completions SHALL trigger on TAB with cycling behavior
|
||||
- **AND** display with PowerShell's native completion UI
|
||||
- **AND** support both Windows PowerShell 5.1 and PowerShell Core 7+
|
||||
|
||||
#### Scenario: No custom UX patterns
|
||||
|
||||
- **WHEN** implementing Zsh completion
|
||||
- **WHEN** implementing completion for any shell
|
||||
- **THEN** do NOT attempt to customize completion trigger behavior
|
||||
- **AND** do NOT override Zsh-specific navigation patterns
|
||||
- **AND** ensure completions feel native to experienced Zsh users
|
||||
- **AND** do NOT override shell-specific navigation patterns
|
||||
- **AND** ensure completions feel native to experienced users of that shell
|
||||
|
||||
### Requirement: Command Structure
|
||||
|
||||
@@ -43,17 +67,35 @@ The completion system SHALL automatically detect the user's current shell enviro
|
||||
- **WHEN** no shell is explicitly specified
|
||||
- **THEN** read the `$SHELL` environment variable
|
||||
- **AND** extract the shell name from the path (e.g., `/bin/zsh` → `zsh`)
|
||||
- **AND** validate the shell is `zsh`
|
||||
- **AND** throw an error if the shell is not `zsh`, with message indicating only Zsh is currently supported
|
||||
- **AND** validate the shell is one of: `zsh`, `bash`, `fish`, `powershell`
|
||||
- **AND** throw an error if the shell is not supported
|
||||
|
||||
#### Scenario: Non-Zsh shell detection
|
||||
#### Scenario: Detecting Bash from environment
|
||||
|
||||
- **WHEN** shell path indicates bash, fish, powershell, or other non-Zsh shell
|
||||
- **THEN** throw error: "Shell '<name>' is not supported yet. Currently supported: zsh"
|
||||
- **WHEN** `$SHELL` contains `bash` in the path
|
||||
- **THEN** detect shell as `bash`
|
||||
- **AND** proceed with bash-specific completion logic
|
||||
|
||||
#### Scenario: Detecting Fish from environment
|
||||
|
||||
- **WHEN** `$SHELL` contains `fish` in the path
|
||||
- **THEN** detect shell as `fish`
|
||||
- **AND** proceed with fish-specific completion logic
|
||||
|
||||
#### Scenario: Detecting PowerShell from environment
|
||||
|
||||
- **WHEN** `$PSModulePath` environment variable is present
|
||||
- **THEN** detect shell as `powershell`
|
||||
- **AND** proceed with PowerShell-specific completion logic
|
||||
|
||||
#### Scenario: Unsupported shell detection
|
||||
|
||||
- **WHEN** shell path indicates an unsupported shell
|
||||
- **THEN** throw error: "Shell '<name>' is not supported. Supported shells: zsh, bash, fish, powershell"
|
||||
|
||||
### Requirement: Completion Generation
|
||||
|
||||
The completion command SHALL generate Zsh completion scripts on demand.
|
||||
The completion command SHALL generate completion scripts for all supported shells on demand.
|
||||
|
||||
#### Scenario: Generating Zsh completion
|
||||
|
||||
@@ -64,6 +106,33 @@ The completion command SHALL generate Zsh completion scripts on demand.
|
||||
- **AND** use Zsh's `_arguments` and `_describe` built-in functions
|
||||
- **AND** support dynamic completion for change and spec IDs
|
||||
|
||||
#### Scenario: Generating Bash completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate bash`
|
||||
- **THEN** output a complete Bash completion script to stdout
|
||||
- **AND** include completions for all commands and subcommands
|
||||
- **AND** use `complete -F` with custom completion function
|
||||
- **AND** populate `COMPREPLY` with appropriate suggestions
|
||||
- **AND** support dynamic completion for change and spec IDs via `openspec __complete`
|
||||
|
||||
#### Scenario: Generating Fish completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate fish`
|
||||
- **THEN** output a complete Fish completion script to stdout
|
||||
- **AND** use `complete -c openspec` with conditions
|
||||
- **AND** include command-specific completions with `--condition` predicates
|
||||
- **AND** support dynamic completion for change and spec IDs via `openspec __complete`
|
||||
- **AND** include descriptions for each completion option
|
||||
|
||||
#### Scenario: Generating PowerShell completion
|
||||
|
||||
- **WHEN** user executes `openspec completion generate powershell`
|
||||
- **THEN** output a complete PowerShell completion script to stdout
|
||||
- **AND** use `Register-ArgumentCompleter -CommandName openspec`
|
||||
- **AND** implement scriptblock that handles command context
|
||||
- **AND** support dynamic completion for change and spec IDs via `openspec __complete`
|
||||
- **AND** return `[System.Management.Automation.CompletionResult]` objects
|
||||
|
||||
### Requirement: Dynamic Completions
|
||||
|
||||
The completion system SHALL provide context-aware dynamic completions for project-specific values.
|
||||
@@ -98,7 +167,7 @@ The completion system SHALL provide context-aware dynamic completions for projec
|
||||
|
||||
### Requirement: Installation Automation
|
||||
|
||||
The completion command SHALL automatically install completion scripts into shell configuration files.
|
||||
The completion command SHALL automatically install completion scripts into shell configuration files for all supported shells.
|
||||
|
||||
#### Scenario: Installing for Oh My Zsh
|
||||
|
||||
@@ -118,12 +187,37 @@ The completion command SHALL automatically install completion scripts into shell
|
||||
- **AND** add `autoload -Uz compinit && compinit` to `~/.zshrc` if not already present
|
||||
- **AND** display success message with instruction to run `exec zsh` or restart terminal
|
||||
|
||||
#### Scenario: Auto-detecting Zsh for installation
|
||||
#### Scenario: Installing for Bash with bash-completion
|
||||
|
||||
- **WHEN** user executes `openspec completion install bash`
|
||||
- **THEN** detect if bash-completion is installed by checking for `/usr/share/bash-completion` or `/etc/bash_completion.d`
|
||||
- **AND** if bash-completion is available, write to `/etc/bash_completion.d/openspec` (with sudo) or `~/.local/share/bash-completion/completions/openspec`
|
||||
- **AND** if bash-completion is not available, write to `~/.bash_completion.d/openspec` and source it from `~/.bashrc`
|
||||
- **AND** add sourcing line to `~/.bashrc` using marker-based updates if needed
|
||||
- **AND** display success message with instruction to run `exec bash` or restart terminal
|
||||
|
||||
#### Scenario: Installing for Fish
|
||||
|
||||
- **WHEN** user executes `openspec completion install fish`
|
||||
- **THEN** create Fish completions directory at `~/.config/fish/completions/` if it doesn't exist
|
||||
- **AND** write completion script to `~/.config/fish/completions/openspec.fish`
|
||||
- **AND** Fish automatically loads completions from this directory (no config file modification needed)
|
||||
- **AND** display success message indicating completions are immediately available
|
||||
|
||||
#### Scenario: Installing for PowerShell
|
||||
|
||||
- **WHEN** user executes `openspec completion install powershell`
|
||||
- **THEN** detect PowerShell profile location via `$PROFILE` environment variable or default paths
|
||||
- **AND** create profile directory if it doesn't exist
|
||||
- **AND** add completion script import to profile using marker-based updates
|
||||
- **AND** write completion script to PowerShell modules directory or alongside profile
|
||||
- **AND** display success message with instruction to restart PowerShell or run `. $PROFILE`
|
||||
|
||||
#### Scenario: Auto-detecting shell for installation
|
||||
|
||||
- **WHEN** user executes `openspec completion install` without specifying a shell
|
||||
- **THEN** detect current shell using shell detection logic
|
||||
- **AND** install completion if detected shell is Zsh
|
||||
- **AND** throw error if detected shell is not Zsh
|
||||
- **AND** install completion for the detected shell (zsh, bash, fish, or powershell)
|
||||
- **AND** display which shell was detected
|
||||
|
||||
#### Scenario: Already installed
|
||||
@@ -135,23 +229,45 @@ The completion command SHALL automatically install completion scripts into shell
|
||||
|
||||
### Requirement: Uninstallation
|
||||
|
||||
The completion command SHALL remove installed completion scripts and configuration.
|
||||
The completion command SHALL remove installed completion scripts and configuration for all supported shells.
|
||||
|
||||
#### Scenario: Uninstalling Oh My Zsh completion
|
||||
#### Scenario: Uninstalling Zsh completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall zsh`
|
||||
- **THEN** prompt for confirmation before proceeding (unless `--yes` flag provided)
|
||||
- **AND** if user declines, cancel uninstall and display "Uninstall cancelled."
|
||||
- **AND** if user confirms, remove `~/.oh-my-zsh/custom/completions/_openspec` if Oh My Zsh is detected
|
||||
- **AND** remove `~/.zsh/completions/_openspec` if standard Zsh setup is detected
|
||||
- **AND** remove fpath modifications from `~/.zshrc`
|
||||
- **AND** remove fpath modifications from `~/.zshrc` using marker-based removal
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Auto-detecting Zsh for uninstallation
|
||||
#### Scenario: Uninstalling Bash completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall bash`
|
||||
- **THEN** prompt for confirmation (unless `--yes` flag provided)
|
||||
- **AND** if user confirms, remove completion file from bash-completion directory or `~/.bash_completion.d/`
|
||||
- **AND** remove sourcing lines from `~/.bashrc` using marker-based removal
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Uninstalling Fish completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall fish`
|
||||
- **THEN** prompt for confirmation (unless `--yes` flag provided)
|
||||
- **AND** if user confirms, remove `~/.config/fish/completions/openspec.fish`
|
||||
- **AND** display success message (no config file modification needed)
|
||||
|
||||
#### Scenario: Uninstalling PowerShell completion
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall powershell`
|
||||
- **THEN** prompt for confirmation (unless `--yes` flag provided)
|
||||
- **AND** if user confirms, remove completion import from PowerShell profile using marker-based removal
|
||||
- **AND** remove completion script file
|
||||
- **AND** display success message
|
||||
|
||||
#### Scenario: Auto-detecting shell for uninstallation
|
||||
|
||||
- **WHEN** user executes `openspec completion uninstall` without specifying a shell
|
||||
- **THEN** detect current shell and uninstall completion if shell is Zsh
|
||||
- **AND** throw error if detected shell is not Zsh
|
||||
- **THEN** detect current shell and uninstall completion for that shell
|
||||
|
||||
#### Scenario: Not installed
|
||||
|
||||
@@ -161,17 +277,34 @@ The completion command SHALL remove installed completion scripts and configurati
|
||||
|
||||
### Requirement: Architecture Patterns
|
||||
|
||||
The completion implementation SHALL follow clean architecture principles with TypeScript best practices.
|
||||
The completion implementation SHALL follow clean architecture principles with TypeScript best practices, supporting multiple shells through a plugin-based pattern.
|
||||
|
||||
#### Scenario: Shell-specific generators
|
||||
|
||||
- **WHEN** implementing completion generators
|
||||
- **THEN** create `ZshCompletionGenerator` class for Zsh
|
||||
- **AND** implement a common `CompletionGenerator` interface with methods:
|
||||
- `generate(): string` - Returns complete shell script
|
||||
- `getInstallPath(): string` - Returns target installation path
|
||||
- `getConfigFile(): string` - Returns shell configuration file path
|
||||
- **AND** design interface to be extensible for future shells (bash, fish, powershell)
|
||||
- **THEN** create generator classes for each shell: `ZshGenerator`, `BashGenerator`, `FishGenerator`, `PowerShellGenerator`
|
||||
- **AND** implement a common `CompletionGenerator` interface with method:
|
||||
- `generate(commands: CommandDefinition[]): string` - Returns complete shell script
|
||||
- **AND** each generator handles shell-specific syntax, escaping, and patterns
|
||||
- **AND** all generators consume the same `CommandDefinition[]` from the command registry
|
||||
|
||||
#### Scenario: Shell-specific installers
|
||||
|
||||
- **WHEN** implementing completion installers
|
||||
- **THEN** create installer classes for each shell: `ZshInstaller`, `BashInstaller`, `FishInstaller`, `PowerShellInstaller`
|
||||
- **AND** implement a common `CompletionInstaller` interface with methods:
|
||||
- `install(script: string): Promise<InstallationResult>` - Installs completion script
|
||||
- `uninstall(): Promise<{ success: boolean; message: string }>` - Removes completion
|
||||
- **AND** each installer handles shell-specific paths, config files, and installation patterns
|
||||
|
||||
#### Scenario: Factory pattern for shell selection
|
||||
|
||||
- **WHEN** selecting shell-specific implementation
|
||||
- **THEN** use `CompletionFactory` class with static methods:
|
||||
- `createGenerator(shell: SupportedShell): CompletionGenerator`
|
||||
- `createInstaller(shell: SupportedShell): CompletionInstaller`
|
||||
- **AND** factory uses switch statements with TypeScript exhaustiveness checking
|
||||
- **AND** adding new shell requires updating `SupportedShell` type and factory cases
|
||||
|
||||
#### Scenario: Dynamic completion providers
|
||||
|
||||
@@ -190,18 +323,18 @@ The completion implementation SHALL follow clean architecture principles with Ty
|
||||
- `name: string` - Command name
|
||||
- `description: string` - Help text
|
||||
- `flags: FlagDefinition[]` - Available flags
|
||||
- `acceptsChangeId: boolean` - Whether command takes change ID argument
|
||||
- `acceptsSpecId: boolean` - Whether command takes spec ID argument
|
||||
- `acceptsPositional: boolean` - Whether command takes positional arguments
|
||||
- `positionalType: string` - Type of positional (change-id, spec-id, path, shell)
|
||||
- `subcommands?: CommandDefinition[]` - Nested subcommands
|
||||
- **AND** export a `COMMAND_REGISTRY` constant with all command definitions
|
||||
- **AND** generators consume this registry to ensure consistency
|
||||
- **AND** all generators consume this registry to ensure consistency across shells
|
||||
|
||||
#### Scenario: Type-safe shell detection
|
||||
|
||||
- **WHEN** implementing shell detection
|
||||
- **THEN** define a `SupportedShell` type as literal type: `'zsh'`
|
||||
- **AND** implement `detectShell()` function that returns 'zsh' or throws error
|
||||
- **AND** design type to be extensible (e.g., future: `'bash' | 'zsh' | 'fish' | 'powershell'`)
|
||||
- **THEN** define a `SupportedShell` type as literal type: `'zsh' | 'bash' | 'fish' | 'powershell'`
|
||||
- **AND** implement `detectShell()` function in `src/utils/shell-detection.ts`
|
||||
- **AND** return detected shell or throw error with supported shells list
|
||||
|
||||
### Requirement: Error Handling
|
||||
|
||||
@@ -209,8 +342,8 @@ The completion command SHALL provide clear error messages for common failure sce
|
||||
|
||||
#### Scenario: Unsupported shell
|
||||
|
||||
- **WHEN** user requests completion for unsupported shell (bash, fish, powershell, etc.)
|
||||
- **THEN** display error message: "Shell '<name>' is not supported yet. Currently supported: zsh"
|
||||
- **WHEN** user requests completion for unsupported shell (e.g., ksh, csh, tcsh)
|
||||
- **THEN** display error message: "Shell '<name>' is not supported yet. Currently supported: zsh, bash, fish, powershell"
|
||||
- **AND** exit with code 1
|
||||
|
||||
#### Scenario: Permission errors during installation
|
||||
@@ -228,7 +361,7 @@ The completion command SHALL provide clear error messages for common failure sce
|
||||
|
||||
#### Scenario: Shell not detected
|
||||
|
||||
- **WHEN** `openspec completion install` cannot detect current shell or detects non-Zsh shell
|
||||
- **WHEN** `openspec completion install` cannot detect current shell
|
||||
- **THEN** display error: "Could not auto-detect shell. Please specify shell explicitly."
|
||||
- **AND** display usage hint: "Usage: openspec completion <operation> [shell]"
|
||||
- **AND** exit with code 1
|
||||
@@ -263,25 +396,37 @@ The completion command SHALL provide machine-parseable and human-readable output
|
||||
|
||||
### Requirement: Testing Support
|
||||
|
||||
The completion implementation SHALL be testable with unit and integration tests.
|
||||
The completion implementation SHALL be testable with unit and integration tests for all supported shells.
|
||||
|
||||
#### Scenario: Mock shell environment
|
||||
|
||||
- **WHEN** writing tests for shell detection
|
||||
- **THEN** allow overriding `$SHELL` environment variable
|
||||
- **THEN** allow overriding `$SHELL` and `$PSModulePath` environment variables
|
||||
- **AND** use dependency injection for file system operations
|
||||
- **AND** test detection for all four shells independently
|
||||
|
||||
#### Scenario: Generator output verification
|
||||
|
||||
- **WHEN** testing completion generators
|
||||
- **THEN** verify generated scripts contain expected patterns
|
||||
- **THEN** create test suite for each shell generator (zsh, bash, fish, powershell)
|
||||
- **AND** verify generated scripts contain expected patterns for that shell
|
||||
- **AND** test that command registry is properly consumed
|
||||
- **AND** ensure dynamic completion placeholders are present
|
||||
- **AND** verify shell-specific syntax and escaping
|
||||
|
||||
#### Scenario: Installation simulation
|
||||
#### Scenario: Installer simulation
|
||||
|
||||
- **WHEN** testing installation logic
|
||||
- **THEN** use temporary test directories instead of actual home directories
|
||||
- **THEN** create test suite for each shell installer
|
||||
- **AND** use temporary test directories instead of actual home directories
|
||||
- **AND** verify file creation without modifying real shell configurations
|
||||
- **AND** test path resolution logic independently
|
||||
- **AND** mock file system operations to avoid side effects
|
||||
|
||||
#### Scenario: Cross-shell consistency
|
||||
|
||||
- **WHEN** testing completion behavior
|
||||
- **THEN** verify all shells support the same commands and flags
|
||||
- **AND** verify dynamic completions work consistently across shells
|
||||
- **AND** ensure error messages are consistent across shells
|
||||
|
||||
|
||||
@@ -187,7 +187,8 @@ The init command SHALL generate slash command files for supported editors using
|
||||
#### Scenario: Generating slash commands for CodeBuddy Code
|
||||
- **WHEN** the user selects CodeBuddy Code during initialization
|
||||
- **THEN** create `.codebuddy/commands/openspec/proposal.md`, `.codebuddy/commands/openspec/apply.md`, and `.codebuddy/commands/openspec/archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** populate each file from shared templates that include CodeBuddy-compatible YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Cline
|
||||
@@ -210,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`
|
||||
|
||||
@@ -50,6 +50,7 @@ The update command SHALL always update the core OpenSpec files and display an AS
|
||||
- **AND** if a root-level stub exists, refresh it so it still directs contributors to `@/openspec/AGENTS.md`
|
||||
|
||||
### Requirement: Slash Command Updates
|
||||
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
@@ -64,8 +65,9 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
|
||||
#### Scenario: Updating slash commands for CodeBuddy Code
|
||||
- **WHEN** `.codebuddy/commands/openspec/` contains `proposal.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **THEN** refresh each file using the shared CodeBuddy templates that include YAML frontmatter for the `description` and `argument-hint` fields
|
||||
- **AND** use square bracket format for `argument-hint` parameters (e.g., `[change-id]`)
|
||||
- **AND** preserve any user customizations outside the OpenSpec managed markers
|
||||
|
||||
#### Scenario: Updating slash commands for Cline
|
||||
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
@@ -73,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
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
|
||||
This spec defines how OpenSpec resolves, reads, and writes user-level global configuration. It governs the `src/core/global-config.ts` module, which provides the foundation for storing user preferences, feature flags, and settings that persist across projects. The spec ensures cross-platform compatibility by following XDG Base Directory Specification with platform-specific fallbacks, and guarantees forward/backward compatibility through schema evolution rules.
|
||||
## Requirements
|
||||
### Requirement: Global configuration storage
|
||||
The system SHALL store global configuration in `~/.config/openspec/config.json`, including telemetry state with `anonymousId` and `noticeSeen` fields.
|
||||
|
||||
#### Scenario: Initial config creation
|
||||
- **WHEN** no global config file exists
|
||||
- **AND** the first telemetry event is about to be sent
|
||||
- **THEN** the system creates `~/.config/openspec/config.json` with telemetry configuration
|
||||
|
||||
#### Scenario: Telemetry config structure
|
||||
- **WHEN** reading or writing telemetry configuration
|
||||
- **THEN** the config contains a `telemetry` object with `anonymousId` (string UUID) and `noticeSeen` (boolean) fields
|
||||
|
||||
#### Scenario: Config file format
|
||||
- **WHEN** storing configuration
|
||||
- **THEN** the system writes valid JSON that can be read and modified by users
|
||||
|
||||
#### Scenario: Existing config preservation
|
||||
- **WHEN** adding telemetry fields to an existing config file
|
||||
- **THEN** the system preserves all existing configuration fields
|
||||
|
||||
### Requirement: Global Config Directory Path
|
||||
|
||||
The system SHALL resolve the global configuration directory path following XDG Base Directory Specification with platform-specific fallbacks.
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# telemetry Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
This spec defines how OpenSpec collects anonymous usage telemetry to help improve the tool. It governs the `src/telemetry/` module, which handles PostHog integration, privacy-preserving event design, user opt-out mechanisms, and first-run notice display. The spec ensures telemetry is minimal, transparent, and respects user privacy.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Command execution tracking
|
||||
The system SHALL send a `command_executed` event to PostHog when any CLI command executes, including only the command name and OpenSpec version as properties.
|
||||
|
||||
#### Scenario: Standard command execution
|
||||
- **WHEN** a user runs any openspec command
|
||||
- **THEN** the system sends a `command_executed` event with `command` and `version` properties
|
||||
|
||||
#### Scenario: Subcommand execution
|
||||
- **WHEN** a user runs a nested command like `openspec change apply`
|
||||
- **THEN** the system sends a `command_executed` event with the full command path (e.g., `change:apply`)
|
||||
|
||||
### Requirement: Privacy-preserving event design
|
||||
The system SHALL NOT include command arguments, file paths, project names, spec content, error messages, or IP addresses in telemetry events.
|
||||
|
||||
#### Scenario: Command with arguments
|
||||
- **WHEN** a user runs `openspec init my-project --force`
|
||||
- **THEN** the telemetry event contains only `command: "init"` and `version: "<version>"` without arguments
|
||||
|
||||
#### Scenario: IP address exclusion
|
||||
- **WHEN** the system sends a telemetry event
|
||||
- **THEN** the event explicitly sets `$ip: null` to prevent IP tracking
|
||||
|
||||
### Requirement: Environment variable opt-out
|
||||
The system SHALL disable telemetry when `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` environment variables are set.
|
||||
|
||||
#### Scenario: OPENSPEC_TELEMETRY opt-out
|
||||
- **WHEN** `OPENSPEC_TELEMETRY=0` is set in the environment
|
||||
- **THEN** the system sends no telemetry events
|
||||
|
||||
#### Scenario: DO_NOT_TRACK opt-out
|
||||
- **WHEN** `DO_NOT_TRACK=1` is set in the environment
|
||||
- **THEN** the system sends no telemetry events
|
||||
|
||||
#### Scenario: Environment variable takes precedence
|
||||
- **WHEN** the user has previously used the CLI (config exists)
|
||||
- **AND** the user sets `OPENSPEC_TELEMETRY=0`
|
||||
- **THEN** telemetry is disabled regardless of config state
|
||||
|
||||
### Requirement: CI environment auto-disable
|
||||
The system SHALL automatically disable telemetry when `CI=true` environment variable is detected.
|
||||
|
||||
#### Scenario: CI environment detection
|
||||
- **WHEN** `CI=true` is set in the environment
|
||||
- **THEN** the system sends no telemetry events
|
||||
|
||||
#### Scenario: CI with explicit enable
|
||||
- **WHEN** `CI=true` is set
|
||||
- **AND** `OPENSPEC_TELEMETRY=1` is explicitly set
|
||||
- **THEN** telemetry remains disabled (CI takes precedence for privacy)
|
||||
|
||||
### Requirement: First-run telemetry notice
|
||||
The system SHALL display a one-line telemetry disclosure notice on the first command execution, before any telemetry is sent.
|
||||
|
||||
#### Scenario: First command execution
|
||||
- **WHEN** a user runs their first openspec command
|
||||
- **AND** telemetry is enabled
|
||||
- **THEN** the system displays: "Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0"
|
||||
|
||||
#### Scenario: Subsequent command execution
|
||||
- **WHEN** a user has already seen the notice (noticeSeen: true in config)
|
||||
- **THEN** the system does not display the notice
|
||||
|
||||
#### Scenario: Notice before telemetry
|
||||
- **WHEN** displaying the first-run notice
|
||||
- **THEN** the notice appears before any telemetry event is sent
|
||||
|
||||
### Requirement: Anonymous user identification
|
||||
The system SHALL generate a random UUID as an anonymous identifier on first telemetry send, stored in global config.
|
||||
|
||||
#### Scenario: First telemetry event
|
||||
- **WHEN** the first telemetry event is sent
|
||||
- **AND** no anonymousId exists in config
|
||||
- **THEN** the system generates a random UUID v4 and stores it in config
|
||||
|
||||
#### Scenario: Persistent identity
|
||||
- **WHEN** a user runs multiple commands across sessions
|
||||
- **THEN** the same anonymousId is used for all events
|
||||
|
||||
#### Scenario: Lazy generation with opt-out
|
||||
- **WHEN** a user opts out before running any command
|
||||
- **THEN** no anonymousId is ever generated or stored
|
||||
|
||||
### Requirement: Immediate event sending
|
||||
The system SHALL send telemetry events immediately without batching, using `flushAt: 1` and `flushInterval: 0` configuration.
|
||||
|
||||
#### Scenario: Event transmission timing
|
||||
- **WHEN** a command executes
|
||||
- **THEN** the telemetry event is sent immediately, not queued for batch transmission
|
||||
|
||||
### Requirement: Graceful shutdown
|
||||
The system SHALL call `posthog.shutdown()` before CLI exit to ensure pending events are flushed.
|
||||
|
||||
#### Scenario: Normal exit
|
||||
- **WHEN** a command completes successfully
|
||||
- **THEN** the system awaits `shutdown()` before exiting
|
||||
|
||||
#### Scenario: Error exit
|
||||
- **WHEN** a command fails with an error
|
||||
- **THEN** the system still awaits `shutdown()` before exiting
|
||||
|
||||
### Requirement: Silent failure handling
|
||||
The system SHALL silently ignore telemetry failures without affecting CLI functionality.
|
||||
|
||||
#### Scenario: Network failure
|
||||
- **WHEN** the telemetry request fails due to network error
|
||||
- **THEN** the CLI command completes normally without error message
|
||||
|
||||
#### Scenario: PostHog outage
|
||||
- **WHEN** PostHog service is unavailable
|
||||
- **THEN** the CLI command completes normally without error message
|
||||
|
||||
#### Scenario: Shutdown failure
|
||||
- **WHEN** `shutdown()` fails or times out
|
||||
- **THEN** the CLI exits normally without error message
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.17.2",
|
||||
"version": "0.23.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",
|
||||
@@ -76,6 +76,7 @@
|
||||
"commander": "^14.0.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"ora": "^8.2.0",
|
||||
"posthog-node": "^5.20.0",
|
||||
"yaml": "^2.8.2",
|
||||
"zod": "^4.0.17"
|
||||
}
|
||||
|
||||
Generated
+98
-14
@@ -26,6 +26,9 @@ importers:
|
||||
ora:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
posthog-node:
|
||||
specifier: ^5.20.0
|
||||
version: 5.20.0
|
||||
yaml:
|
||||
specifier: ^2.8.2
|
||||
version: 2.8.2
|
||||
@@ -33,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)
|
||||
@@ -70,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
|
||||
@@ -83,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==}
|
||||
|
||||
@@ -451,8 +463,8 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.8':
|
||||
resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==}
|
||||
'@inquirer/type@3.0.10':
|
||||
resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
@@ -484,6 +496,9 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@posthog/core@1.9.1':
|
||||
resolution: {integrity: sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||
resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
|
||||
cpu: [arm]
|
||||
@@ -823,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'}
|
||||
@@ -847,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==}
|
||||
|
||||
@@ -1192,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'}
|
||||
@@ -1284,6 +1315,10 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
posthog-node@5.20.0:
|
||||
resolution: {integrity: sha512-LkR5KfrvEQTnUtNKN97VxFB00KcYG1Iz8iKg8r0e/i7f1eQhg1WSZO+Jp1B4bvtHCmdpIE4HwYbvCCzFoCyjVg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1455,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'}
|
||||
@@ -1564,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'}
|
||||
@@ -1631,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
|
||||
@@ -1685,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
|
||||
@@ -1887,7 +1946,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
ansi-escapes: 4.3.2
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
@@ -1896,7 +1955,7 @@ snapshots:
|
||||
'@inquirer/confirm@5.1.14(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
@@ -1904,7 +1963,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.0
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
@@ -1916,7 +1975,7 @@ snapshots:
|
||||
'@inquirer/editor@4.2.15(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
external-editor: 3.1.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
@@ -1924,7 +1983,7 @@ snapshots:
|
||||
'@inquirer/expand@4.0.17(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
@@ -1941,21 +2000,21 @@ snapshots:
|
||||
'@inquirer/input@4.2.1(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
'@inquirer/number@3.0.17(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
'@inquirer/password@4.0.17(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
ansi-escapes: 4.3.2
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
@@ -1978,7 +2037,7 @@ snapshots:
|
||||
'@inquirer/rawlist@4.1.5(@types/node@24.2.0)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
@@ -1987,7 +2046,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
@@ -1996,13 +2055,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@inquirer/core': 10.2.2(@types/node@24.2.0)
|
||||
'@inquirer/figures': 1.0.13
|
||||
'@inquirer/type': 3.0.8(@types/node@24.2.0)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.2.0)
|
||||
ansi-escapes: 4.3.2
|
||||
yoctocolors-cjs: 2.1.2
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
'@inquirer/type@3.0.8(@types/node@24.2.0)':
|
||||
'@inquirer/type@3.0.10(@types/node@24.2.0)':
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.0
|
||||
|
||||
@@ -2038,6 +2097,10 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@posthog/core@1.9.1':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.46.2':
|
||||
optional: true
|
||||
|
||||
@@ -2365,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
|
||||
@@ -2379,6 +2444,8 @@ snapshots:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
dotenv@8.6.0: {}
|
||||
|
||||
emoji-regex@10.4.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
@@ -2723,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
|
||||
@@ -2808,6 +2879,10 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
posthog-node@5.20.0:
|
||||
dependencies:
|
||||
'@posthog/core': 1.9.1
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@2.8.8: {}
|
||||
@@ -2967,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
|
||||
@@ -3074,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
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# OpenSpec Scripts
|
||||
|
||||
Utility scripts for OpenSpec maintenance and development.
|
||||
|
||||
## update-flake.sh
|
||||
|
||||
Updates `flake.nix` version and dependency hash automatically.
|
||||
|
||||
**When to use**: After updating dependencies or releasing a new version.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
./scripts/update-flake.sh
|
||||
```
|
||||
|
||||
**What it does**:
|
||||
1. Extracts version from `package.json`
|
||||
2. Updates version in `flake.nix`
|
||||
3. Automatically determines the correct pnpm dependency hash
|
||||
4. Updates the hash in `flake.nix`
|
||||
5. Verifies the build succeeds
|
||||
|
||||
**Example workflow**:
|
||||
```bash
|
||||
# After version bump and dependency updates
|
||||
pnpm install
|
||||
./scripts/update-flake.sh
|
||||
git add flake.nix
|
||||
git commit -m "chore: update flake.nix for v0.18.0"
|
||||
```
|
||||
|
||||
## postinstall.js
|
||||
|
||||
Post-installation script that runs after package installation.
|
||||
|
||||
## pack-version-check.mjs
|
||||
|
||||
Validates package version consistency before publishing.
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Script to update flake.nix version and dependency hash
|
||||
# Run this after updating package.json version
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
FLAKE_FILE="$PROJECT_ROOT/flake.nix"
|
||||
PACKAGE_JSON="$PROJECT_ROOT/package.json"
|
||||
|
||||
# Detect OS and set sed in-place flag
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS (BSD sed) requires empty string argument for -i
|
||||
SED_INPLACE=(-i '')
|
||||
else
|
||||
# Linux (GNU sed)
|
||||
SED_INPLACE=(-i)
|
||||
fi
|
||||
|
||||
echo "==> Updating flake.nix..."
|
||||
|
||||
# Extract version from package.json
|
||||
VERSION=$(node -p "require('$PACKAGE_JSON').version")
|
||||
echo " Detected version: $VERSION"
|
||||
|
||||
# Update version in flake.nix
|
||||
if ! grep -q "version = \"$VERSION\"" "$FLAKE_FILE"; then
|
||||
echo " Updating version in flake.nix..."
|
||||
sed "${SED_INPLACE[@]}" "s|version = \"[^\"]*\"|version = \"$VERSION\"|" "$FLAKE_FILE"
|
||||
else
|
||||
echo " Version already up-to-date in flake.nix"
|
||||
fi
|
||||
|
||||
# Set placeholder hash to trigger error
|
||||
echo " Setting placeholder hash..."
|
||||
PLACEHOLDER="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
sed "${SED_INPLACE[@]}" "s|hash = \"sha256-[^\"]*\"|hash = \"$PLACEHOLDER\"|" "$FLAKE_FILE"
|
||||
|
||||
# Try to build and capture the correct hash
|
||||
echo " Building to get correct hash (this will fail)..."
|
||||
BUILD_OUTPUT=$(nix build 2>&1 || true)
|
||||
|
||||
# Extract the correct hash from error output (portable - works on macOS and Linux)
|
||||
CORRECT_HASH=$(echo "$BUILD_OUTPUT" | grep -o 'got:[[:space:]]*sha256-[A-Za-z0-9+/=]*' | head -1 | sed 's/got:[[:space:]]*//')
|
||||
|
||||
if [ -z "$CORRECT_HASH" ]; then
|
||||
echo "❌ Error: Could not extract hash from build output"
|
||||
echo "Build output:"
|
||||
echo "$BUILD_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Detected hash: $CORRECT_HASH"
|
||||
|
||||
# Update flake.nix with correct hash
|
||||
sed "${SED_INPLACE[@]}" "s|hash = \"$PLACEHOLDER\"|hash = \"$CORRECT_HASH\"|" "$FLAKE_FILE"
|
||||
|
||||
# Verify the build works
|
||||
echo " Verifying build..."
|
||||
if nix build 2>&1 | grep -q "warning: Git tree.*is dirty"; then
|
||||
echo "⚠️ Warning: Git tree is dirty, but build succeeded"
|
||||
else
|
||||
echo "✅ Build successful"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ flake.nix updated successfully!"
|
||||
echo " Version: $VERSION"
|
||||
echo " Hash: $CORRECT_HASH"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Test: nix run . -- --version"
|
||||
echo " 2. Commit: git add flake.nix"
|
||||
echo " 3. Include in version bump commit"
|
||||
+193
-11
@@ -13,13 +13,49 @@ import { ChangeCommand } from '../commands/change.js';
|
||||
import { ValidateCommand } from '../commands/validate.js';
|
||||
import { ShowCommand } from '../commands/show.js';
|
||||
import { CompletionCommand } from '../commands/completion.js';
|
||||
import { FeedbackCommand } from '../commands/feedback.js';
|
||||
import { registerConfigCommand } from '../commands/config.js';
|
||||
import { registerArtifactWorkflowCommands } from '../commands/artifact-workflow.js';
|
||||
import { registerSchemaCommand } from '../commands/schema.js';
|
||||
import {
|
||||
statusCommand,
|
||||
instructionsCommand,
|
||||
applyInstructionsCommand,
|
||||
templatesCommand,
|
||||
schemasCommand,
|
||||
newChangeCommand,
|
||||
DEFAULT_SCHEMA,
|
||||
type StatusOptions,
|
||||
type InstructionsOptions,
|
||||
type TemplatesOptions,
|
||||
type SchemasOptions,
|
||||
type NewChangeOptions,
|
||||
} from '../commands/workflow/index.js';
|
||||
import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
|
||||
|
||||
const program = new Command();
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version } = require('../../package.json');
|
||||
|
||||
/**
|
||||
* Get the full command path for nested commands.
|
||||
* For example: 'change show' -> 'change:show'
|
||||
*/
|
||||
function getCommandPath(command: Command): string {
|
||||
const names: string[] = [];
|
||||
let current: Command | null = command;
|
||||
|
||||
while (current) {
|
||||
const name = current.name();
|
||||
// Skip the root 'openspec' command
|
||||
if (name && name !== 'openspec') {
|
||||
names.unshift(name);
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return names.join(':') || 'openspec';
|
||||
}
|
||||
|
||||
program
|
||||
.name('openspec')
|
||||
.description('AI-native system for spec-driven development')
|
||||
@@ -28,26 +64,42 @@ program
|
||||
// Global options
|
||||
program.option('--no-color', 'Disable color output');
|
||||
|
||||
// Apply global flags before any command runs
|
||||
program.hook('preAction', (thisCommand) => {
|
||||
// Apply global flags and telemetry before any command runs
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Show first-run telemetry notice (if not seen)
|
||||
await maybeShowTelemetryNotice();
|
||||
|
||||
// Track command execution (use actionCommand to get the actual subcommand)
|
||||
const commandPath = getCommandPath(actionCommand);
|
||||
await trackCommand(commandPath, version);
|
||||
});
|
||||
|
||||
const availableToolIds = AI_TOOLS.filter((tool) => tool.available).map((tool) => tool.value);
|
||||
// Shutdown telemetry after command completes
|
||||
program.hook('postAction', async () => {
|
||||
await shutdown();
|
||||
});
|
||||
|
||||
const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
|
||||
const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
|
||||
|
||||
program
|
||||
.command('init [path]')
|
||||
.description('Initialize OpenSpec in your project')
|
||||
.option('--tools <tools>', toolsOptionDescription)
|
||||
.action(async (targetPath = '.', options?: { tools?: string }) => {
|
||||
.option('--force', 'Auto-cleanup legacy files without prompting')
|
||||
.action(async (targetPath = '.', options?: { tools?: string; force?: boolean }) => {
|
||||
try {
|
||||
// Validate that the path is a valid directory
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(resolvedPath);
|
||||
if (!stats.isDirectory()) {
|
||||
@@ -63,10 +115,11 @@ program
|
||||
throw new Error(`Cannot access path "${targetPath}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const { InitCommand } = await import('../core/init.js');
|
||||
const initCommand = new InitCommand({
|
||||
tools: options?.tools,
|
||||
force: options?.force,
|
||||
});
|
||||
await initCommand.execute(targetPath);
|
||||
} catch (error) {
|
||||
@@ -76,13 +129,36 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// Hidden alias: 'experimental' -> 'init' for backwards compatibility
|
||||
program
|
||||
.command('experimental', { hidden: true })
|
||||
.description('Alias for init (deprecated)')
|
||||
.option('--tool <tool-id>', 'Target AI tool (maps to --tools)')
|
||||
.option('--no-interactive', 'Disable interactive prompts')
|
||||
.action(async (options?: { tool?: string; noInteractive?: boolean }) => {
|
||||
try {
|
||||
console.log('Note: "openspec experimental" is deprecated. Use "openspec init" instead.');
|
||||
const { InitCommand } = await import('../core/init.js');
|
||||
const initCommand = new InitCommand({
|
||||
tools: options?.tool,
|
||||
interactive: options?.noInteractive === true ? false : undefined,
|
||||
});
|
||||
await initCommand.execute('.');
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('update [path]')
|
||||
.description('Update OpenSpec instruction files')
|
||||
.action(async (targetPath = '.') => {
|
||||
.option('--force', 'Force update even when tools are up to date')
|
||||
.action(async (targetPath = '.', options?: { force?: boolean }) => {
|
||||
try {
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
const updateCommand = new UpdateCommand();
|
||||
const updateCommand = new UpdateCommand({ force: options?.force });
|
||||
await updateCommand.execute(resolvedPath);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
@@ -206,6 +282,7 @@ program
|
||||
|
||||
registerSpecCommand(program);
|
||||
registerConfigCommand(program);
|
||||
registerSchemaCommand(program);
|
||||
|
||||
// Top-level validate command
|
||||
program
|
||||
@@ -257,6 +334,22 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// Feedback command
|
||||
program
|
||||
.command('feedback <message>')
|
||||
.description('Submit feedback about OpenSpec')
|
||||
.option('--body <text>', 'Detailed description for the feedback')
|
||||
.action(async (message: string, options?: { body?: string }) => {
|
||||
try {
|
||||
const feedbackCommand = new FeedbackCommand();
|
||||
await feedbackCommand.execute(message, options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Completion command with subcommands
|
||||
const completionCmd = program
|
||||
.command('completion')
|
||||
@@ -320,7 +413,96 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// Register artifact workflow commands (experimental)
|
||||
registerArtifactWorkflowCommands(program);
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Workflow Commands (formerly experimental)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// Status command
|
||||
program
|
||||
.command('status')
|
||||
.description('Display artifact completion status for a change')
|
||||
.option('--change <id>', 'Change name to show status for')
|
||||
.option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options: StatusOptions) => {
|
||||
try {
|
||||
await statusCommand(options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Instructions command
|
||||
program
|
||||
.command('instructions [artifact]')
|
||||
.description('Output enriched instructions for creating an artifact or applying tasks')
|
||||
.option('--change <id>', 'Change name')
|
||||
.option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (artifactId: string | undefined, options: InstructionsOptions) => {
|
||||
try {
|
||||
// Special case: "apply" is not an artifact, but a command to get apply instructions
|
||||
if (artifactId === 'apply') {
|
||||
await applyInstructionsCommand(options);
|
||||
} else {
|
||||
await instructionsCommand(artifactId, options);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Templates command
|
||||
program
|
||||
.command('templates')
|
||||
.description('Show resolved template paths for all artifacts in a schema')
|
||||
.option('--schema <name>', `Schema to use (default: ${DEFAULT_SCHEMA})`)
|
||||
.option('--json', 'Output as JSON mapping artifact IDs to template paths')
|
||||
.action(async (options: TemplatesOptions) => {
|
||||
try {
|
||||
await templatesCommand(options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Schemas command
|
||||
program
|
||||
.command('schemas')
|
||||
.description('List available workflow schemas with descriptions')
|
||||
.option('--json', 'Output as JSON (for agent use)')
|
||||
.action(async (options: SchemasOptions) => {
|
||||
try {
|
||||
await schemasCommand(options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// New command group with change subcommand
|
||||
const newCmd = program.command('new').description('Create new items');
|
||||
|
||||
newCmd
|
||||
.command('change <name>')
|
||||
.description('Create a new change directory')
|
||||
.option('--description <text>', 'Description to add to README.md')
|
||||
.option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`)
|
||||
.action(async (name: string, options: NewChangeOptions) => {
|
||||
try {
|
||||
await newChangeCommand(name, options);
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user