mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c70681b38 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
Add Windsurf support.
|
||||
@@ -8,13 +8,8 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository == 'Fission-AI/OpenSpec'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -29,20 +24,14 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@fission-ai'
|
||||
always-auth: true
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# Opens/updates the Version Packages PR; publishes when the Version PR merges
|
||||
# Opens/updates the Version Packages PR; no publishing here
|
||||
- name: Create/Update Version PR
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
title: 'chore(release): version packages'
|
||||
createGithubReleases: true
|
||||
publish: pnpm run release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Publish to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@fission-ai'
|
||||
always-auth: true
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
|
||||
- name: Ensure running from a tag
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
||||
echo "This workflow must run from a tag (got: $GITHUB_REF)";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
- name: Verify release tag matches package.json
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME#v}"
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
if [ "$TAG" != "$PKG_VERSION" ]; then
|
||||
echo "Tag v$TAG does not match package.json $PKG_VERSION"; exit 1
|
||||
fi
|
||||
|
||||
- name: Debug npm auth and context
|
||||
run: |
|
||||
test -n "$NODE_AUTH_TOKEN" || (echo "NODE_AUTH_TOKEN is missing" && exit 1)
|
||||
echo "NODE_AUTH_TOKEN present"
|
||||
npm --version
|
||||
pnpm --version
|
||||
node --version
|
||||
npm config get registry
|
||||
npm whoami
|
||||
npm ping
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --provenance --no-git-checks
|
||||
@@ -1,12 +1,5 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add native Kilo Code workflow integration so `openspec init` and `openspec update` manage `.kilocode/workflows/openspec-*.md` files.
|
||||
- Always scaffold the managed root `AGENTS.md` hand-off stub and regroup the AI tool prompts during init/update to keep instructions consistent.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -84,10 +84,6 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
|
||||
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
|
||||
|
||||
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`.
|
||||
|
||||
#### AGENTS.md Compatible
|
||||
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/).
|
||||
@@ -125,8 +121,8 @@ openspec init
|
||||
```
|
||||
|
||||
**What happens during initialization:**
|
||||
- You'll be prompted to pick any natively supported AI tools (Claude Code, Cursor, OpenCode, 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
|
||||
- You'll be prompted to select your AI tool (Claude Code, Cursor, etc.)
|
||||
- OpenSpec automatically configures slash commands or `AGENTS.md` based on your selection
|
||||
- A new `openspec/` directory structure is created in your project
|
||||
|
||||
**After setup:**
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
## Why
|
||||
- Kilo Code executes \"slash commands\" by loading markdown workflows from `.kilocode/workflows/` (or the global `~/.kilocode/workflows/`) and running them when a user types `/workflow-name.md`, making project-local workflow files the analogue to the slash-command files we already ship for other tools.\\
|
||||
([Workflows | Kilo Code Docs](https://kilocode.ai/docs/features/slash-commands/workflows))
|
||||
- Those workflows are plain markdown with step-by-step instructions that can call built-in tools and MCP integrations, so reusing OpenSpec's shared proposal/apply/archive bodies keeps behaviour aligned across assistants without inventing new content.
|
||||
- OpenSpec already detects configured tools and refreshes marker-wrapped files during `init`/`update`; extending the same mechanism to `.kilocode/workflows/openspec-*.md` ensures Kilo Code stays in sync with one source of truth.
|
||||
|
||||
## What Changes
|
||||
- Add Kilo Code to the `openspec init` tool picker with \"already configured\" detection, including wiring for extend mode so teams can refresh Kilo Code assets.
|
||||
- Implement a `KiloCodeSlashCommandConfigurator` that creates `.kilocode/workflows/openspec-{proposal,apply,archive}.md`, ensuring the workflow directory exists and wrapping shared content in OpenSpec markers (no front matter required).
|
||||
- Teach `openspec update` to refresh existing Kilo Code workflows (and only those that already exist) using the shared slash-command templates.
|
||||
- Update documentation, release notes, and integration tests so the new workflow support is covered alongside Claude, Cursor, OpenCode, and Windsurf.
|
||||
|
||||
## Impact
|
||||
- Specs: `cli-init`, `cli-update`
|
||||
- Code: `src/core/config.ts`, `src/core/configurators/(registry|slash/*)`, `src/core/templates/slash-command-templates.ts`, CLI wiring for tool summaries
|
||||
- Tests: init/update workflow coverage, regression for marker preservation in `.kilocode/workflows/`
|
||||
- Docs: README / CHANGELOG updates advertising Kilo Code workflow support
|
||||
@@ -1,24 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: AI Tool Configuration
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system.
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
- **WHEN** run interactively
|
||||
- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu
|
||||
- **AND** list every available tool with a checkbox:
|
||||
- Claude Code (creates or refreshes CLAUDE.md and slash commands)
|
||||
- Cursor (creates or refreshes `.cursor/commands/*` slash commands)
|
||||
- OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands)
|
||||
- Windsurf (creates or refreshes `.windsurf/workflows/openspec-*.md` workflows)
|
||||
- Kilo Code (creates or refreshes `.kilocode/workflows/openspec-*.md` workflows)
|
||||
- AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers)
|
||||
- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content
|
||||
- **AND** treat disabled tools as "coming soon" and keep them unselectable
|
||||
- **AND** allow confirming with Enter after selecting one or more tools
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
#### 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
|
||||
@@ -1,8 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones.
|
||||
#### Scenario: Updating slash commands for Kilo Code
|
||||
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
@@ -1,15 +0,0 @@
|
||||
## 1. CLI wiring
|
||||
- [x] 1.1 Add Kilo Code to the selectable AI tools in `openspec init`, including "already configured" detection and success summaries.
|
||||
- [x] 1.2 Register a `KiloCodeSlashCommandConfigurator` alongside other slash-command tools.
|
||||
|
||||
## 2. Workflow generation
|
||||
- [x] 2.1 Implement the configurator so it creates `.kilocode/workflows/` (if needed) and writes `openspec-{proposal,apply,archive}.md` with OpenSpec markers.
|
||||
- [x] 2.2 Reuse the shared slash-command bodies without front matter; verify resulting files stay Markdown-only with no extra metadata.
|
||||
|
||||
## 3. Update support
|
||||
- [x] 3.1 Ensure `openspec update` refreshes existing Kilo Code workflows while skipping ones that are absent.
|
||||
- [x] 3.2 Add regression coverage confirming marker content is replaced (not duplicated) during updates.
|
||||
|
||||
## 4. Documentation
|
||||
- [x] 4.1 Update README / docs to note Kilo Code workflow support and path (`.kilocode/workflows/`).
|
||||
- [x] 4.2 Mention the integration in CHANGELOG or release notes if applicable.
|
||||
@@ -1,11 +0,0 @@
|
||||
## Why
|
||||
Manual setup for new changes leads to formatting mistakes in spec deltas and slows agents who must recreate the same file skeletons for every proposal. A built-in scaffold command will generate compliant templates so assistants can focus on the change content instead of structure.
|
||||
|
||||
## What Changes
|
||||
- Add an `openspec scaffold <change-id>` CLI command that creates a change directory with validated `proposal.md`, `tasks.md`, and spec delta templates.
|
||||
- Update CLI documentation and quick-reference guidance so agents discover the scaffold workflow before drafting files manually.
|
||||
- Add automated coverage (unit/integ tests) to ensure the command respects existing naming rules and generated Markdown passes validation.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/cli-scaffold`
|
||||
- Affected code: `src/cli/index.ts`, `src/commands`, `docs/`
|
||||
@@ -1,44 +0,0 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Scaffolding Command Registration
|
||||
The CLI SHALL expose an `openspec scaffold <change-id>` command that validates the change identifier before generating files.
|
||||
|
||||
#### Scenario: Registering scaffold command
|
||||
- **WHEN** a user runs `openspec scaffold add-user-notifications`
|
||||
- **THEN** the CLI SHALL reject invalid identifiers (non kebab-case) before proceeding
|
||||
- **AND** display usage documentation via `openspec scaffold --help`
|
||||
- **AND** exit with code 0 after successful scaffolding
|
||||
|
||||
### Requirement: Change Directory Structure
|
||||
The scaffold command SHALL create the standard change workspace with proposal, tasks, optional design, and delta directories laid out according to OpenSpec conventions.
|
||||
|
||||
#### Scenario: Generating change workspace
|
||||
- **WHEN** scaffolding a new change with id `add-user-notifications`
|
||||
- **THEN** create `openspec/changes/add-user-notifications/`
|
||||
- **AND** generate `proposal.md`, `tasks.md`, and `design.md` (commented placeholder content) in that directory when missing
|
||||
- **AND** create `openspec/changes/add-user-notifications/specs/` ready for capability-specific deltas
|
||||
|
||||
### Requirement: Template Content Guidance
|
||||
The scaffold command SHALL populate generated Markdown files with OpenSpec-compliant templates so authors can copy, edit, and pass validation without reformatting.
|
||||
|
||||
#### Scenario: Populating proposal and tasks templates
|
||||
- **WHEN** the scaffold command writes `proposal.md`
|
||||
- **THEN** include the `## Why`, `## What Changes`, and `## Impact` headings with placeholder guidance text
|
||||
- **AND** ensure `tasks.md` starts with `## 1. Implementation` and numbered checklist items using `- [ ]` syntax
|
||||
- **AND** annotate optional sections (like `design.md`) with inline TODO comments so users understand when to keep or delete them
|
||||
|
||||
### Requirement: Delta Spec Creation
|
||||
The scaffold command SHALL create at least one capability delta file with correctly formatted requirement and scenario placeholders that guide authors to enter the actual behavior.
|
||||
|
||||
#### Scenario: Creating spec delta skeleton
|
||||
- **WHEN** scaffolding a change and the capability `cli-scaffold` is provided interactively or via flags
|
||||
- **THEN** generate `openspec/changes/add-user-notifications/specs/cli-scaffold/spec.md`
|
||||
- **AND** include `## ADDED Requirements` with at least one `### Requirement:` block and matching `#### Scenario:` entries that remind the author to replace placeholder text
|
||||
- **AND** ensure the generated delta passes `openspec validate add-user-notifications --strict` until the author edits it
|
||||
|
||||
### Requirement: Idempotent Execution
|
||||
The scaffold command SHALL be safe to rerun, preserving user edits while filling in any missing managed sections.
|
||||
|
||||
#### Scenario: Rerunning scaffold on existing change
|
||||
- **WHEN** the command is executed again for an existing change directory containing user-edited files
|
||||
- **THEN** leave existing content untouched except for managed placeholder regions or missing files that need creation
|
||||
- **AND** update the filesystem summary to highlight which files were skipped, created, or refreshed
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1. CLI scaffolding command
|
||||
- [ ] 1.1 Register an `openspec scaffold` command in the CLI entrypoint with `change-id` argument validation.
|
||||
- [ ] 1.2 Implement generator logic that creates the change directory structure plus default `proposal.md`, `tasks.md`, and delta spec skeletons without overwriting existing populated files.
|
||||
|
||||
## 2. Templates and documentation
|
||||
- [ ] 2.1 Surface copy/paste templates and scaffold usage in the top-level quick reference for `openspec/AGENTS.md`.
|
||||
- [ ] 2.2 Refresh other CLI docs (`docs/`, README) to mention the scaffold workflow and link to instructions.
|
||||
|
||||
## 3. Test coverage
|
||||
- [ ] 3.1 Add unit tests covering name validation, file generation, and idempotent reruns.
|
||||
- [ ] 3.2 Add integration coverage ensuring generated files pass `openspec validate --strict` without manual edits.
|
||||
@@ -1,17 +0,0 @@
|
||||
## Why
|
||||
- Windsurf exposes "Workflows" as the vehicle for slash-like automation: saved Markdown files under `.windsurf/workflows/` that Cascade discovers across the workspace (including subdirectories and up to the git root), then executes when a user types `/workflow-name`. These files can be team-authored, must stay under 12k characters, and can call other workflows, making them the natural place to publish OpenSpec guidance for Windsurf users.\
|
||||
([Windsurf Workflows documentation](https://docs.windsurf.com/windsurf/cascade/workflows))
|
||||
- The Wave 12 changelog reiterates that workflows are invoked via slash commands and that Windsurf stores them in `.windsurf/workflows`, so the OpenSpec CLI just needs to generate Markdown there to participate in Windsurf's command palette.\
|
||||
("Custom Workflows" section, [Windsurf changelog](https://windsurf.com/changelog))
|
||||
- OpenSpec already ships shared command bodies for proposal/apply/archive and uses markers so commands stay up to date. Extending the same templates to Windsurf keeps behaviour consistent with Claude, Cursor, and OpenCode without inventing new content flows.
|
||||
|
||||
## What Changes
|
||||
- Add Windsurf to the CLI tool picker (`openspec init`) and the slash-command registry so selecting it scaffolds `.windsurf/workflows/openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` with marker-managed bodies.
|
||||
- Shape each Windsurf workflow with a short heading/description plus the existing OpenSpec guardrails/steps wrapped in markers, ensuring the total payload remains well below the 12,000 character limit.
|
||||
- Ensure `openspec update` refreshes existing Windsurf workflows (and only those that already exist) in-place, mirroring current behaviour for other editors.
|
||||
- Extend unit tests for init/update to cover Windsurf generation and updates, and update the README/tooling docs to advertise Windsurf support.
|
||||
|
||||
## Impact
|
||||
- Specs: `cli-init`, `cli-update`
|
||||
- Code: `src/core/configurators/slash/*`, `src/core/templates/slash-command-templates.ts`, CLI prompts, README
|
||||
- Tests: init/update integration coverage for Windsurf workflows
|
||||
@@ -1,23 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: AI Tool Configuration
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system.
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
- **WHEN** run interactively
|
||||
- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu
|
||||
- **AND** list every available tool with a checkbox:
|
||||
- Claude Code (creates or refreshes CLAUDE.md and slash commands)
|
||||
- Cursor (creates or refreshes `.cursor/commands/*` slash commands)
|
||||
- OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands)
|
||||
- Windsurf (creates or refreshes `.windsurf/workflows/openspec-*.md` workflows)
|
||||
- AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers)
|
||||
- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content
|
||||
- **AND** treat disabled tools as "coming soon" and keep them unselectable
|
||||
- **AND** allow confirming with Enter after selecting one or more tools
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
#### 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
|
||||
@@ -1,8 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones.
|
||||
#### Scenario: Updating slash commands for Windsurf
|
||||
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
@@ -1,17 +0,0 @@
|
||||
## 1. CLI wiring
|
||||
- [ ] 1.1 Add Windsurf to the selectable AI tools in `openspec init`, including "already configured" detection.
|
||||
- [ ] 1.2 Register a `WindsurfSlashCommandConfigurator` that writes workflows to `.windsurf/workflows/` and ensures the directory exists.
|
||||
- [ ] 1.3 Ensure `openspec update` pulls the Windsurf configurator when winds is selected and skips creation when files are absent.
|
||||
|
||||
## 2. Workflow templates
|
||||
- [ ] 2.1 Reuse the shared proposal/apply/archive bodies, adding Windsurf-specific headings/description before the OpenSpec markers.
|
||||
- [ ] 2.2 Confirm generated Markdown (per file) stays comfortably under the 12k character ceiling noted in the Windsurf docs.
|
||||
|
||||
## 3. Tests & safeguards
|
||||
- [ ] 3.1 Extend init tests to assert creation of `.windsurf/workflows/openspec-*.md` when Windsurf is chosen.
|
||||
- [ ] 3.2 Extend update tests to assert existing Windsurf workflows are refreshed and non-existent files are ignored.
|
||||
- [ ] 3.3 Add regression coverage for marker preservation inside Windsurf workflow files.
|
||||
|
||||
## 4. Documentation
|
||||
- [ ] 4.1 Update README (and any user-facing docs) to list Windsurf under native slash/workflow integrations.
|
||||
- [ ] 4.2 Call out Windsurf workflow support in release notes or CHANGELOG if applicable.
|
||||
@@ -1,12 +0,0 @@
|
||||
## Why
|
||||
Validation errors like "no deltas found" or "missing requirement text" do not tell agents how to recover, leading to repeated failures. Making error output specific about headers, required text, and next actions will help assistants fix issues in a single pass.
|
||||
|
||||
## What Changes
|
||||
- Extend `openspec validate` error reporting so each failure names the exact header, file, and expected structure, including concrete examples of compliant Markdown.
|
||||
- Tailor messages for the most common mistakes (missing delta sections, absent descriptive requirement text, missing scenarios) with actionable fixes and suggested debug commands.
|
||||
- Update docs/help output so the improved messaging is discoverable (e.g., `--help`, troubleshooting section).
|
||||
- Add regression coverage to lock in the richer messaging for the top validation paths.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/cli-validate`
|
||||
- Affected code: `src/commands/validate.ts`, `src/core/validation`, `docs/`
|
||||
@@ -1,39 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Validation SHALL provide actionable remediation steps
|
||||
Validation output SHALL include specific guidance to fix each error, including expected structure, example headers, and suggested commands to verify fixes.
|
||||
|
||||
#### Scenario: No deltas found in change
|
||||
- **WHEN** validating a change with zero parsed deltas
|
||||
- **THEN** show error "No deltas found" with guidance:
|
||||
- Explain that change specs must include `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, or `## RENAMED Requirements`
|
||||
- Remind authors that files must live under `openspec/changes/{id}/specs/<capability>/spec.md`
|
||||
- Include an explicit note: "Spec delta files cannot start with titles before the operation headers"
|
||||
- Suggest running `openspec change show {id} --json --deltas-only` for debugging
|
||||
|
||||
#### Scenario: Missing required sections
|
||||
- **WHEN** a required section is missing
|
||||
- **THEN** include expected header names and a minimal skeleton:
|
||||
- For Spec: `## Purpose`, `## Requirements`
|
||||
- For Change: `## Why`, `## What Changes`
|
||||
- Provide an example snippet of the missing section with placeholder prose ready to copy
|
||||
- Mention the quick-reference section in `openspec/AGENTS.md` as the authoritative template
|
||||
|
||||
#### Scenario: Missing requirement descriptive text
|
||||
- **WHEN** a requirement header lacks descriptive text before scenarios
|
||||
- **THEN** emit an error explaining that `### Requirement:` lines must be followed by narrative text before any `#### Scenario:` headers
|
||||
- Show compliant example: "### Requirement: Foo" followed by "The system SHALL ..."
|
||||
- Suggest adding 1-2 sentences describing the normative behavior prior to listing scenarios
|
||||
- Reference the pre-validation checklist in `openspec/AGENTS.md`
|
||||
|
||||
### Requirement: Validator SHALL detect likely misformatted scenarios and warn with a fix
|
||||
The validator SHALL recognize bulleted lines that look like scenarios (e.g., lines beginning with WHEN/THEN/AND) and emit a targeted warning with a conversion example to `#### Scenario:`.
|
||||
|
||||
#### Scenario: Bulleted WHEN/THEN under a Requirement
|
||||
- **WHEN** bullets that start with WHEN/THEN/AND are found under a requirement without any `#### Scenario:` headers
|
||||
- **THEN** emit warning: "Scenarios must use '#### Scenario:' headers", and show a conversion template:
|
||||
```
|
||||
#### Scenario: Short name
|
||||
- **WHEN** ...
|
||||
- **THEN** ...
|
||||
- **AND** ...
|
||||
```
|
||||
@@ -1,12 +0,0 @@
|
||||
## 1. Messaging enhancements
|
||||
- [ ] 1.1 Inventory current validation failures and map each to the desired message improvements.
|
||||
- [ ] 1.2 Implement structured error builders that include file paths, normalized header names, and example fixes.
|
||||
- [ ] 1.3 Ensure `openspec validate --help` and troubleshooting docs mention the richer messages and debug tips.
|
||||
|
||||
## 2. Tests
|
||||
- [ ] 2.1 Add unit tests for representative errors (no deltas, missing requirement body, missing scenarios) asserting the new wording.
|
||||
- [ ] 2.2 Add integration coverage verifying the Next steps footer reflects contextual guidance.
|
||||
|
||||
## 3. Documentation
|
||||
- [ ] 3.1 Update troubleshooting sections and CLI docs with sample output from the enhanced errors.
|
||||
- [ ] 3.2 Note the change in CHANGELOG or release notes if applicable.
|
||||
@@ -1,12 +0,0 @@
|
||||
## Why
|
||||
Agents fumble proposal formatting because the essential Markdown templates and formatting rules are buried mid-document. Reorganizing `openspec/AGENTS.md` with a prominent quick-reference and embedded examples will help assistants follow the process without guesswork.
|
||||
|
||||
## What Changes
|
||||
- Restructure `openspec/AGENTS.md` so file formats and scaffold templates appear in a top-level quick-reference section before workflow prose.
|
||||
- Embed copy/paste templates for `proposal.md`, `tasks.md`, `design.md`, and spec deltas alongside inline examples within the workflow steps.
|
||||
- Add a pre-validation checklist that highlights the most common formatting pitfalls before running `openspec validate`.
|
||||
- Split content into beginner vs. advanced sections to progressively disclose complexity while keeping advanced guidance accessible.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/docs-agent-instructions`
|
||||
- Affected code: `openspec/AGENTS.md`, `docs/`
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Quick Reference Placement
|
||||
The AI instructions SHALL begin with a quick-reference section that surfaces required file structures, templates, and formatting rules before any narrative guidance.
|
||||
|
||||
#### Scenario: Loading templates at the top
|
||||
- **WHEN** `openspec/AGENTS.md` is regenerated or updated
|
||||
- **THEN** the first substantive section after the title SHALL provide copy-ready headings for `proposal.md`, `tasks.md`, spec deltas, and scenario formatting
|
||||
- **AND** link each template to the corresponding workflow step for deeper reading
|
||||
|
||||
### Requirement: Embedded Templates and Examples
|
||||
`openspec/AGENTS.md` SHALL include complete copy/paste templates and inline examples exactly where agents make corresponding edits.
|
||||
|
||||
#### Scenario: Providing file templates
|
||||
- **WHEN** authors reach the workflow guidance for drafting proposals and deltas
|
||||
- **THEN** provide fenced Markdown templates that match the required structure (`## Why`, `## ADDED Requirements`, `#### Scenario:` etc.)
|
||||
- **AND** accompany each template with a brief example showing correct header usage and scenario bullets
|
||||
|
||||
### Requirement: Pre-validation Checklist
|
||||
`openspec/AGENTS.md` SHALL offer a concise pre-validation checklist that highlights common formatting mistakes before running `openspec validate`.
|
||||
|
||||
#### Scenario: Highlighting common validation failures
|
||||
- **WHEN** a reader reaches the validation guidance
|
||||
- **THEN** present a checklist reminding them to verify requirement headers, scenario formatting, and delta sections
|
||||
- **AND** include reminders about at least `#### Scenario:` usage and descriptive requirement text before scenarios
|
||||
|
||||
### Requirement: Progressive Disclosure of Workflow Guidance
|
||||
The documentation SHALL separate beginner essentials from advanced topics so newcomers can focus on core steps without losing access to advanced workflows.
|
||||
|
||||
#### Scenario: Organizing beginner and advanced sections
|
||||
- **WHEN** reorganizing `openspec/AGENTS.md`
|
||||
- **THEN** keep an introductory section limited to the minimum steps (scaffold, draft, validate, request review)
|
||||
- **AND** move advanced topics (multi-capability changes, archiving details, tooling deep dives) into clearly labeled later sections
|
||||
- **AND** provide anchor links from the quick-reference to those advanced sections
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1. Instruction redesign
|
||||
- [ ] 1.1 Draft a quick-reference section that surfaces file templates and formatting rules at the top of `openspec/AGENTS.md`.
|
||||
- [ ] 1.2 Reorganize the workflow narrative with inline examples and progressive disclosure for advanced topics.
|
||||
|
||||
## 2. Templates and checklists
|
||||
- [ ] 2.1 Add copy/paste templates for proposal, tasks, design, and spec delta files.
|
||||
- [ ] 2.2 Insert a pre-validation checklist capturing common lint failures before running `openspec validate`.
|
||||
|
||||
## 3. Documentation updates
|
||||
- [ ] 3.1 Update supporting docs or README pointers so contributors find the redesigned instructions.
|
||||
- [ ] 3.2 Confirm examples and references stay in sync with the new scaffold command guidance.
|
||||
@@ -1,15 +0,0 @@
|
||||
## Why
|
||||
OpenSpec currently creates the root-level `AGENTS.md` stub only when teams explicitly select the "AGENTS.md standard" tool during `openspec init`. Projects that skip that checkbox never get a managed stub, so non-native assistants (Copilot, Codeium, etc.) have no entry point and later `openspec update` runs silently create the file without any context. We need to bake the stub into initialization, clarify the tool selection experience, and keep the update workflow aligned so every teammate lands on the right instructions from day one.
|
||||
|
||||
## What Changes
|
||||
- Update `openspec init` so the root `AGENTS.md` stub is always generated (first run and extend mode) and refreshed from a shared utility instead of being tied to a tool selection.
|
||||
- Redesign the AI tool selection wizard to split options into "Natively supported" (Claude, Cursor, OpenCode, …) and an informational "Other tools" section that explains the always-on `AGENTS.md` hand-off.
|
||||
- Adjust CLI specs, prompts, and success messaging to reflect the new categories while keeping extend-mode behaviour consistent.
|
||||
- Update automated tests and fixtures to cover the unconditional stub creation and the reworked prompt flow.
|
||||
- Refresh documentation and onboarding snippets so they no longer describe the stub as opt-in and instead call out the new grouping.
|
||||
- Ensure `openspec update` continues to reconcile both `openspec/AGENTS.md` and the root stub, documenting the expected behaviour so mismatched setups self-heal.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `cli-init`, `cli-update`
|
||||
- Affected code: `src/core/init.ts`, `src/core/config.ts`, `src/core/configurators/agents.ts`, `src/core/templates/agents-root-stub.ts`, `src/core/update.ts`, related tests under `test/core/`
|
||||
- Docs & assets: README, CHANGELOG, any setup guides that reference choosing the "AGENTS.md standard" option
|
||||
@@ -1,32 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: AI Tool Configuration
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions using a grouped selection experience so teams can enable native integrations while always provisioning guidance for other assistants.
|
||||
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
- **WHEN** run interactively
|
||||
- **THEN** present a multi-select wizard that separates options into two headings:
|
||||
- **Natively supported providers** shows each available first-party integration (Claude Code, Cursor, OpenCode, …) with checkboxes
|
||||
- **Other tools** explains that the root-level `AGENTS.md` stub is always generated for AGENTS-compatible assistants and cannot be deselected
|
||||
- **AND** mark already configured native tools with "(already configured)" to signal that choosing them will refresh managed content
|
||||
- **AND** keep disabled or unavailable providers labelled as "coming soon" so users know they cannot opt in yet
|
||||
- **AND** allow confirming the selection even when no native provider is chosen because the root stub remains enabled by default
|
||||
- **AND** change the base prompt copy in extend mode to "Which natively supported AI tools would you like to add or refresh?"
|
||||
|
||||
### Requirement: Exit Code Adjustments
|
||||
`openspec init` SHALL treat extend mode without new native tool selections as a successful refresh.
|
||||
|
||||
#### Scenario: Allowing empty extend runs
|
||||
- **WHEN** OpenSpec is already initialized and the user selects no additional natively supported tools
|
||||
- **THEN** complete successfully while refreshing the root `AGENTS.md` stub
|
||||
- **AND** exit with code 0
|
||||
|
||||
## ADDED Requirements
|
||||
### Requirement: Root instruction stub
|
||||
`openspec init` SHALL always scaffold the root-level `AGENTS.md` hand-off so every teammate finds the primary OpenSpec instructions.
|
||||
|
||||
#### Scenario: Creating root `AGENTS.md`
|
||||
- **GIVEN** the project may or may not already contain an `AGENTS.md` file
|
||||
- **WHEN** initialization completes in fresh or extend mode
|
||||
- **THEN** create or refresh `AGENTS.md` at the repository root using the managed marker block from `TemplateManager.getAgentsStandardTemplate()`
|
||||
- **AND** preserve any existing content outside the managed markers while replacing the stub text inside them
|
||||
- **AND** create the stub regardless of which native AI tools are selected
|
||||
@@ -1,10 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Tool-Agnostic Updates
|
||||
The update command SHALL refresh OpenSpec-managed files in a predictable manner while respecting each team's chosen tooling.
|
||||
|
||||
#### Scenario: Updating files
|
||||
- **WHEN** updating files
|
||||
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** create or refresh the root-level `AGENTS.md` stub using the managed marker block, even if the file was previously absent
|
||||
- **AND** update only the OpenSpec-managed sections inside existing AI tool files, leaving user-authored content untouched
|
||||
- **AND** avoid creating new native-tool configuration files (slash commands, CLAUDE.md, etc.) unless they already exist
|
||||
@@ -1,11 +0,0 @@
|
||||
## 1. Implementation
|
||||
- [ ] 1.1 Refactor `openspec init` to always generate the root `AGENTS.md` stub (initial run and extend mode) via shared helper logic.
|
||||
- [ ] 1.2 Rework the AI tool selection wizard to surface "Natively supported" vs "Other tools" groupings and make the stub non-optional.
|
||||
- [ ] 1.3 Update CLI messaging, templates, and configurators so the new flow stays in sync across init and update commands.
|
||||
- [ ] 1.4 Refresh unit/integration tests to cover the unconditional stub and the regrouped prompt layout.
|
||||
- [ ] 1.5 Update documentation, README snippets, and CHANGELOG entries that mention the opt-in `AGENTS.md` experience.
|
||||
|
||||
## 2. Validation
|
||||
- [ ] 2.1 Run `pnpm test` targeting CLI init/update suites.
|
||||
- [ ] 2.2 Execute `openspec validate update-cli-init-root-agents --strict`.
|
||||
- [ ] 2.3 Perform a manual smoke test: run `openspec init` in a temp directory, confirm stub + grouped prompts, rerun in extend mode.
|
||||
@@ -1,49 +0,0 @@
|
||||
## Why
|
||||
Today’s process requires maintainers to merge the Changesets PR, cut a tag, and draft the GitHub release by hand. npm publish then runs from our existing workflow after the GitHub release is published. The human-in-the-loop steps (versioning, tagging, release notes) slow us down and risk drift between npm, tags, and changelog.
|
||||
|
||||
## What Changes
|
||||
- Use the single `changesets/action` on pushes to `main` to either open/update the version PR or, when the release PR is merged, run our publish command automatically using repository secrets.
|
||||
- Add a `release` script that builds and runs `changeset publish` so the action handles version bumps, changelog commits, npm publish, and GitHub releases end-to-end.
|
||||
- Enable `createGithubReleases: true` so GitHub releases are created from the changeset data right after publishing.
|
||||
- Document the automated flow, required secrets, guardrails, and recovery steps (rollback, hotfixes).
|
||||
|
||||
## Two-Phase Rollout (Two PRs)
|
||||
1) Phase 1 — Dry run (no publish)
|
||||
- Update the existing `release-prepare.yml` to wire up `changesets/action` with `createGithubReleases: true` and a no-op `publish` command (e.g., `echo 'dry run'`).
|
||||
- Keep `.github/workflows/release-publish.yml` intact. This avoids any publish path changes while we verify that the version PR behavior and permissions are correct.
|
||||
- Add a repository guard (`if: github.repository == 'Fission-AI/OpenSpec'`) and a concurrency group for safety.
|
||||
|
||||
2) Phase 2 — Enable publish and consolidate
|
||||
- Add `"release": "pnpm run build && pnpm exec changeset publish"` to `package.json`.
|
||||
- Change `release-prepare.yml` to use `with: publish: pnpm run release` and `env: NPM_TOKEN: \\${{ secrets.NPM_TOKEN }}` plus the default `GITHUB_TOKEN`.
|
||||
- Remove `.github/workflows/release-publish.yml` to avoid double-publish. Publishing now happens when the version PR is merged.
|
||||
|
||||
## Guardrails
|
||||
- Concurrency: `concurrency: { group: release-\\${{ github.ref }}, cancel-in-progress: false }` on the workflow to serialize releases.
|
||||
- Repository/branch guard: run publish logic only on upstream `main` (`if: github.repository == 'Fission-AI/OpenSpec' && github.ref == 'refs/heads/main'`).
|
||||
- Permissions: ensure `contents: write` and `pull-requests: write` for opening/updating the version PR; `packages: read` optional.
|
||||
|
||||
## Rollback and Hotfixes
|
||||
- Rollback: revert the release PR merge (which reverts version bumps/changelog); if a tag or GitHub release was created, delete the tag and release; deprecate the npm version if necessary (`npm deprecate @fission-ai/openspec@x.y.z 'reason'`).
|
||||
- Hotfix (urgent, no pending changesets): create a changeset for the fix and merge the release PR; in emergencies, run a manual bump/publish but reconcile with Changesets by adding a follow-up changeset to align versions.
|
||||
|
||||
## Required Secrets
|
||||
- `NPM_TOKEN` with publish rights for the `@fission-ai` scope.
|
||||
- Default `GITHUB_TOKEN` (provided by GitHub) for opening/updating the version PR and creating GitHub releases.
|
||||
|
||||
## How the Maintainer Flow Changes
|
||||
| Step | Current process | Future process |
|
||||
| --- | --- | --- |
|
||||
| Prepare release | Merge changeset PR, then manually draft release notes and tags | Merge release PR; action updates versions and handles changelog automatically |
|
||||
| Publish npm package | Happens automatically after GitHub release | Happens automatically via `changeset publish` invoked by the action |
|
||||
| GitHub release | Draft manually and sync with changelog | Action creates GitHub releases from changeset data |
|
||||
| Docs/process | Follow manual tagging/release steps | Docs describe automated flow + recovery and hotfix paths |
|
||||
|
||||
## Impact
|
||||
- Automation: reuse `.github/workflows/release-prepare.yml` (phase 1: dry-run, phase 2: publish) and remove `.github/workflows/release-publish.yml` in phase 2.
|
||||
- Package metadata: add `release` script to `package.json`.
|
||||
- Docs: update README or `/docs` to show the automated flow, secrets, guardrails, and recovery steps.
|
||||
|
||||
## Acceptance Criteria
|
||||
- Phase 1: merges to `main` open/update a version PR; on merge, the action’s `publish` step is a no-op; no npm publish occurs; logs confirm intended behavior; GitHub releases creation is wired but inert due to no publish.
|
||||
- Phase 2: merges to `main` run `pnpm run release` from the action; npm package publishes successfully; GitHub release is created automatically; `.github/workflows/release-publish.yml` is removed; no duplicate publishes occur.
|
||||
@@ -1,12 +0,0 @@
|
||||
## 1. Release workflow automation
|
||||
- [ ] 1.1 Add a `.github/workflows/release.yml` that runs on pushes to `main`, sets up pnpm + Node 20, installs dependencies, and invokes `changesets/action@v1` with `publish: pnpm run release`.
|
||||
- [ ] 1.2 Configure the action with `createGithubReleases: true` and document required secrets (`NPM_TOKEN`, default `GITHUB_TOKEN`) plus recommended concurrency safeguards.
|
||||
- [ ] 1.3 Validate the workflow using `act` or a dry-run push to confirm the action opens release PRs when changesets exist and publishes when the release PR merge lands.
|
||||
|
||||
## 2. Package release script
|
||||
- [ ] 2.1 Add a `release` script to `package.json` that builds the project and runs `changeset publish` using pnpm.
|
||||
- [ ] 2.2 Ensure the script respects the existing `prepare`/`prepublishOnly` hooks to avoid duplicate builds and update documentation or scripts if adjustments are needed.
|
||||
|
||||
## 3. Documentation and recovery steps
|
||||
- [ ] 3.1 Update maintainer docs (e.g., README or `/docs`) with the end-to-end automated release flow, explicitly removing the manual tag/release steps that are no longer required and explaining how changesets drive the release PR.
|
||||
- [ ] 3.2 Document fallback steps for failed publishes (rerun workflow, manual publish) and the hotfix path when a release must be cut without pending changesets.
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
@@ -46,7 +46,6 @@
|
||||
"test:coverage": "vitest --coverage",
|
||||
"prepare": "pnpm run build",
|
||||
"prepublishOnly": "pnpm run build",
|
||||
"release": "pnpm run build && pnpm exec changeset publish",
|
||||
"changeset": "changeset"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+4
-6
@@ -17,10 +17,8 @@ export interface AIToolOption {
|
||||
}
|
||||
|
||||
export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
||||
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
|
||||
{ name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
{ name: 'Claude Code (✅ OpenSpec custom slash commands available)', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cursor (✅ OpenSpec custom slash commands available)', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'OpenCode (✅ OpenSpec custom slash commands available)', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'AGENTS.md (works with Codex, Amp, Copilot, …)', value: 'agents', available: true, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
];
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { SlashCommandConfigurator } from "./base.js";
|
||||
import { SlashCommandId } from "../../templates/index.js";
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: ".kilocode/workflows/openspec-proposal.md",
|
||||
apply: ".kilocode/workflows/openspec-apply.md",
|
||||
archive: ".kilocode/workflows/openspec-archive.md"
|
||||
};
|
||||
|
||||
export class KiloCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = "kilocode";
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(_id: SlashCommandId): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
||||
import { CursorSlashCommandConfigurator } from './cursor.js';
|
||||
import { WindsurfSlashCommandConfigurator } from './windsurf.js';
|
||||
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
||||
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
@@ -11,14 +9,10 @@ export class SlashCommandRegistry {
|
||||
static {
|
||||
const claude = new ClaudeSlashCommandConfigurator();
|
||||
const cursor = new CursorSlashCommandConfigurator();
|
||||
const windsurf = new WindsurfSlashCommandConfigurator();
|
||||
const kilocode = new KiloCodeSlashCommandConfigurator();
|
||||
const opencode = new OpenCodeSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
this.configurators.set(windsurf.toolId, windsurf);
|
||||
this.configurators.set(kilocode.toolId, kilocode);
|
||||
this.configurators.set(opencode.toolId, opencode);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.windsurf/workflows/openspec-proposal.md',
|
||||
apply: '.windsurf/workflows/openspec-apply.md',
|
||||
archive: '.windsurf/workflows/openspec-archive.md'
|
||||
};
|
||||
|
||||
export class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'windsurf';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string | undefined {
|
||||
const descriptions: Record<SlashCommandId, string> = {
|
||||
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
|
||||
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
const description = descriptions[id];
|
||||
return `---\ndescription: ${description}\nauto_execution_mode: 3\n---`;
|
||||
}
|
||||
}
|
||||
+69
-238
@@ -59,24 +59,11 @@ const parseToolLabel = (raw: string): ToolLabel => {
|
||||
};
|
||||
};
|
||||
|
||||
const isSelectableChoice = (
|
||||
choice: ToolWizardChoice
|
||||
): choice is Extract<ToolWizardChoice, { selectable: true }> => choice.selectable;
|
||||
|
||||
type ToolWizardChoice =
|
||||
| {
|
||||
kind: 'heading' | 'info';
|
||||
value: string;
|
||||
label: ToolLabel;
|
||||
selectable: false;
|
||||
}
|
||||
| {
|
||||
kind: 'option';
|
||||
value: string;
|
||||
label: ToolLabel;
|
||||
configured: boolean;
|
||||
selectable: true;
|
||||
};
|
||||
type ToolWizardChoice = {
|
||||
value: string;
|
||||
label: ToolLabel;
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
type ToolWizardConfig = {
|
||||
extendMode: boolean;
|
||||
@@ -89,41 +76,21 @@ type WizardStep = 'intro' | 'select' | 'review';
|
||||
|
||||
type ToolSelectionPrompt = (config: ToolWizardConfig) => Promise<string[]>;
|
||||
|
||||
type RootStubStatus = 'created' | 'updated' | 'skipped';
|
||||
|
||||
const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
|
||||
|
||||
const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
|
||||
const LIST_SPACER_VALUE = '__list-spacer__';
|
||||
|
||||
const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
(config, done) => {
|
||||
const totalSteps = 3;
|
||||
const [step, setStep] = useState<WizardStep>('intro');
|
||||
const selectableChoices = config.choices.filter(isSelectableChoice);
|
||||
const initialCursorIndex = config.choices.findIndex((choice) =>
|
||||
choice.selectable
|
||||
const [cursor, setCursor] = useState<number>(0);
|
||||
const [selected, setSelected] = useState<string[]>(
|
||||
() => config.initialSelected ?? []
|
||||
);
|
||||
const [cursor, setCursor] = useState<number>(
|
||||
initialCursorIndex === -1 ? 0 : initialCursorIndex
|
||||
);
|
||||
const [selected, setSelected] = useState<string[]>(() => {
|
||||
const initial = new Set(
|
||||
(config.initialSelected ?? []).filter((value) =>
|
||||
selectableChoices.some((choice) => choice.value === value)
|
||||
)
|
||||
);
|
||||
return selectableChoices
|
||||
.map((choice) => choice.value)
|
||||
.filter((value) => initial.has(value));
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const selectedSet = new Set(selected);
|
||||
const pageSize = Math.max(config.choices.length, 1);
|
||||
const pageSize = Math.max(Math.min(config.choices.length, 7), 1);
|
||||
|
||||
const updateSelected = (next: Set<string>) => {
|
||||
const ordered = selectableChoices
|
||||
const ordered = config.choices
|
||||
.map((choice) => choice.value)
|
||||
.filter((value) => next.has(value));
|
||||
setSelected(ordered);
|
||||
@@ -133,17 +100,8 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
items: config.choices,
|
||||
active: cursor,
|
||||
pageSize,
|
||||
loop: false,
|
||||
loop: config.choices.length > 1,
|
||||
renderItem: ({ item, isActive }) => {
|
||||
if (!item.selectable) {
|
||||
const prefix = item.kind === 'info' ? ' ' : '';
|
||||
const textColor =
|
||||
item.kind === 'heading' ? PALETTE.lightGray : PALETTE.midGray;
|
||||
return `${PALETTE.midGray(' ')} ${PALETTE.midGray(' ')} ${textColor(
|
||||
`${prefix}${item.label.primary}`
|
||||
)}`;
|
||||
}
|
||||
|
||||
const isSelected = selectedSet.has(item.value);
|
||||
const cursorSymbol = isActive
|
||||
? PALETTE.white('›')
|
||||
@@ -152,36 +110,13 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
? PALETTE.white('◉')
|
||||
: PALETTE.midGray('○');
|
||||
const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
|
||||
const annotation = item.label.annotation
|
||||
? PALETTE.midGray(` (${item.label.annotation})`)
|
||||
: '';
|
||||
const configuredNote = item.configured
|
||||
? PALETTE.midGray(' (already configured)')
|
||||
: '';
|
||||
const label = `${nameColor(item.label.primary)}${annotation}${configuredNote}`;
|
||||
const label = `${nameColor(item.label.primary)}${
|
||||
item.configured ? PALETTE.midGray(' (already configured)') : ''
|
||||
}`;
|
||||
return `${cursorSymbol} ${indicator} ${label}`;
|
||||
},
|
||||
});
|
||||
|
||||
const moveCursor = (direction: 1 | -1) => {
|
||||
if (selectableChoices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextIndex = cursor;
|
||||
while (true) {
|
||||
nextIndex = nextIndex + direction;
|
||||
if (nextIndex < 0 || nextIndex >= config.choices.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.choices[nextIndex]?.selectable) {
|
||||
setCursor(nextIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useKeypress((key) => {
|
||||
if (step === 'intro') {
|
||||
if (isEnterKey(key)) {
|
||||
@@ -192,20 +127,24 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
|
||||
if (step === 'select') {
|
||||
if (isUpKey(key)) {
|
||||
moveCursor(-1);
|
||||
const previousIndex =
|
||||
cursor <= 0 ? config.choices.length - 1 : cursor - 1;
|
||||
setCursor(previousIndex);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownKey(key)) {
|
||||
moveCursor(1);
|
||||
const nextIndex =
|
||||
cursor >= config.choices.length - 1 ? 0 : cursor + 1;
|
||||
setCursor(nextIndex);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSpaceKey(key)) {
|
||||
const current = config.choices[cursor];
|
||||
if (!current || !current.selectable) return;
|
||||
if (!current) return;
|
||||
|
||||
const next = new Set(selected);
|
||||
if (next.has(current.value)) {
|
||||
@@ -220,14 +159,17 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
}
|
||||
|
||||
if (isEnterKey(key)) {
|
||||
if (selected.length === 0) {
|
||||
setError('Select at least one AI tool to continue.');
|
||||
return;
|
||||
}
|
||||
setStep('review');
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.name === 'escape') {
|
||||
const next = new Set<string>();
|
||||
updateSelected(next);
|
||||
setSelected([]);
|
||||
setError(null);
|
||||
}
|
||||
return;
|
||||
@@ -237,10 +179,7 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
if (isEnterKey(key)) {
|
||||
const finalSelection = config.choices
|
||||
.map((choice) => choice.value)
|
||||
.filter(
|
||||
(value) =>
|
||||
selectedSet.has(value) && value !== ROOT_STUB_CHOICE_VALUE
|
||||
);
|
||||
.filter((value) => selectedSet.has(value));
|
||||
done(finalSelection);
|
||||
return;
|
||||
}
|
||||
@@ -252,30 +191,9 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
}
|
||||
});
|
||||
|
||||
const rootStubChoice = selectableChoices.find(
|
||||
(choice) => choice.value === ROOT_STUB_CHOICE_VALUE
|
||||
);
|
||||
const rootStubSelected = rootStubChoice
|
||||
? selectedSet.has(ROOT_STUB_CHOICE_VALUE)
|
||||
: false;
|
||||
const nativeChoices = selectableChoices.filter(
|
||||
(choice) => choice.value !== ROOT_STUB_CHOICE_VALUE
|
||||
);
|
||||
const selectedNativeChoices = nativeChoices.filter((choice) =>
|
||||
selectedSet.has(choice.value)
|
||||
);
|
||||
|
||||
const formatSummaryLabel = (
|
||||
choice: Extract<ToolWizardChoice, { selectable: true }>
|
||||
) => {
|
||||
const annotation = choice.label.annotation
|
||||
? PALETTE.midGray(` (${choice.label.annotation})`)
|
||||
: '';
|
||||
const configuredNote = choice.configured
|
||||
? PALETTE.midGray(' (already configured)')
|
||||
: '';
|
||||
return `${PALETTE.white(choice.label.primary)}${annotation}${configuredNote}`;
|
||||
};
|
||||
const selectedNames = config.choices
|
||||
.filter((choice) => selectedSet.has(choice.value))
|
||||
.map((choice) => choice.label.primary);
|
||||
|
||||
const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
|
||||
const lines: string[] = [];
|
||||
@@ -304,21 +222,16 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
lines.push('');
|
||||
lines.push(page);
|
||||
lines.push('');
|
||||
lines.push(PALETTE.midGray('Selected configuration:'));
|
||||
if (rootStubSelected && rootStubChoice) {
|
||||
if (selectedNames.length === 0) {
|
||||
lines.push(
|
||||
` ${PALETTE.white('-')} ${formatSummaryLabel(rootStubChoice)}`
|
||||
);
|
||||
}
|
||||
if (selectedNativeChoices.length === 0) {
|
||||
lines.push(
|
||||
` ${PALETTE.midGray('- No natively supported providers selected')}`
|
||||
`${PALETTE.midGray('Selected')}: ${PALETTE.midGray(
|
||||
'None selected yet'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
selectedNativeChoices.forEach((choice) => {
|
||||
lines.push(
|
||||
` ${PALETTE.white('-')} ${formatSummaryLabel(choice)}`
|
||||
);
|
||||
lines.push(PALETTE.midGray('Selected:'));
|
||||
selectedNames.forEach((name) => {
|
||||
lines.push(` ${PALETTE.white('-')} ${PALETTE.white(name)}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -328,23 +241,13 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
);
|
||||
lines.push('');
|
||||
|
||||
if (rootStubSelected && rootStubChoice) {
|
||||
if (selectedNames.length === 0) {
|
||||
lines.push(
|
||||
`${PALETTE.white('▌')} ${formatSummaryLabel(rootStubChoice)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedNativeChoices.length === 0) {
|
||||
lines.push(
|
||||
PALETTE.midGray(
|
||||
'No natively supported providers selected. Universal instructions will still be applied.'
|
||||
)
|
||||
PALETTE.midGray('No tools selected. Press Backspace to return.')
|
||||
);
|
||||
} else {
|
||||
selectedNativeChoices.forEach((choice) => {
|
||||
lines.push(
|
||||
`${PALETTE.white('▌')} ${formatSummaryLabel(choice)}`
|
||||
);
|
||||
selectedNames.forEach((name) => {
|
||||
lines.push(`${PALETTE.white('▌')} ${PALETTE.white(name)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -382,6 +285,17 @@ export class InitCommand {
|
||||
// Get configuration (after validation to avoid prompts if validation fails)
|
||||
const config = await this.getConfiguration(existingToolStates, extendMode);
|
||||
|
||||
if (config.aiTools.length === 0) {
|
||||
if (extendMode) {
|
||||
throw new Error(
|
||||
`OpenSpec seems to already be initialized at ${openspecPath}.\n` +
|
||||
`Use 'openspec update' to update the structure.`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('You must select at least one AI tool to configure.');
|
||||
}
|
||||
|
||||
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
||||
const selectedIds = new Set(config.aiTools);
|
||||
const selectedTools = availableTools.filter((tool) =>
|
||||
@@ -421,11 +335,7 @@ export class InitCommand {
|
||||
|
||||
// Step 2: Configure AI tools
|
||||
const toolSpinner = this.startSpinner('Configuring AI tools...');
|
||||
const rootStubStatus = await this.configureAITools(
|
||||
projectPath,
|
||||
openspecDir,
|
||||
config.aiTools
|
||||
);
|
||||
await this.configureAITools(projectPath, openspecDir, config.aiTools);
|
||||
toolSpinner.stopAndPersist({
|
||||
symbol: PALETTE.white('▌'),
|
||||
text: PALETTE.white('AI tools configured'),
|
||||
@@ -438,8 +348,7 @@ export class InitCommand {
|
||||
refreshed,
|
||||
skippedExisting,
|
||||
skipped,
|
||||
extendMode,
|
||||
rootStubStatus
|
||||
extendMode
|
||||
);
|
||||
}
|
||||
|
||||
@@ -473,69 +382,27 @@ export class InitCommand {
|
||||
): Promise<string[]> {
|
||||
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
||||
|
||||
if (availableTools.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const baseMessage = extendMode
|
||||
? 'Which natively supported AI tools would you like to add or refresh?'
|
||||
: 'Which natively supported AI tools do you use?';
|
||||
const initialNativeSelection = extendMode
|
||||
? 'Which AI tools would you like to add or refresh?'
|
||||
: 'Which AI tools do you use?';
|
||||
const initialSelected = extendMode
|
||||
? availableTools
|
||||
.filter((tool) => existingTools[tool.value])
|
||||
.map((tool) => tool.value)
|
||||
: [];
|
||||
|
||||
const initialSelected = Array.from(new Set(initialNativeSelection));
|
||||
|
||||
const choices: ToolWizardChoice[] = [
|
||||
{
|
||||
kind: 'heading',
|
||||
value: '__heading-native__',
|
||||
label: {
|
||||
primary:
|
||||
'Natively supported providers (✔ OpenSpec custom slash commands available)',
|
||||
},
|
||||
selectable: false,
|
||||
},
|
||||
...availableTools.map<ToolWizardChoice>((tool) => ({
|
||||
kind: 'option',
|
||||
value: tool.value,
|
||||
label: parseToolLabel(tool.name),
|
||||
configured: Boolean(existingTools[tool.value]),
|
||||
selectable: true,
|
||||
})),
|
||||
...(availableTools.length
|
||||
? ([
|
||||
{
|
||||
kind: 'info' as const,
|
||||
value: LIST_SPACER_VALUE,
|
||||
label: { primary: '' },
|
||||
selectable: false,
|
||||
},
|
||||
] as ToolWizardChoice[])
|
||||
: []),
|
||||
{
|
||||
kind: 'heading',
|
||||
value: OTHER_TOOLS_HEADING_VALUE,
|
||||
label: {
|
||||
primary:
|
||||
'Other tools (use Universal AGENTS.md for Codex, Amp, VS Code, GitHub Copilot, …)',
|
||||
},
|
||||
selectable: false,
|
||||
},
|
||||
{
|
||||
kind: 'option',
|
||||
value: ROOT_STUB_CHOICE_VALUE,
|
||||
label: {
|
||||
primary: 'Universal AGENTS.md',
|
||||
annotation: 'always available',
|
||||
},
|
||||
configured: extendMode,
|
||||
selectable: true,
|
||||
},
|
||||
];
|
||||
|
||||
return this.prompt({
|
||||
extendMode,
|
||||
baseMessage,
|
||||
choices,
|
||||
choices: availableTools.map((tool) => ({
|
||||
value: tool.value,
|
||||
label: parseToolLabel(tool.name),
|
||||
configured: Boolean(existingTools[tool.value]),
|
||||
})),
|
||||
initialSelected,
|
||||
});
|
||||
}
|
||||
@@ -608,12 +475,7 @@ export class InitCommand {
|
||||
projectPath: string,
|
||||
openspecDir: string,
|
||||
toolIds: string[]
|
||||
): Promise<RootStubStatus> {
|
||||
const rootStubStatus = await this.configureRootAgentsStub(
|
||||
projectPath,
|
||||
openspecDir
|
||||
);
|
||||
|
||||
): Promise<void> {
|
||||
for (const toolId of toolIds) {
|
||||
const configurator = ToolRegistry.get(toolId);
|
||||
if (configurator && configurator.isAvailable) {
|
||||
@@ -625,25 +487,6 @@ export class InitCommand {
|
||||
await slashConfigurator.generateAll(projectPath, openspecDir);
|
||||
}
|
||||
}
|
||||
|
||||
return rootStubStatus;
|
||||
}
|
||||
|
||||
private async configureRootAgentsStub(
|
||||
projectPath: string,
|
||||
openspecDir: string
|
||||
): Promise<RootStubStatus> {
|
||||
const configurator = ToolRegistry.get('agents');
|
||||
if (!configurator || !configurator.isAvailable) {
|
||||
return 'skipped';
|
||||
}
|
||||
|
||||
const stubPath = path.join(projectPath, configurator.configFileName);
|
||||
const existed = await FileSystemUtils.fileExists(stubPath);
|
||||
|
||||
await configurator.configure(projectPath, openspecDir);
|
||||
|
||||
return existed ? 'updated' : 'created';
|
||||
}
|
||||
|
||||
private displaySuccessMessage(
|
||||
@@ -652,8 +495,7 @@ export class InitCommand {
|
||||
refreshed: AIToolOption[],
|
||||
skippedExisting: AIToolOption[],
|
||||
skipped: AIToolOption[],
|
||||
extendMode: boolean,
|
||||
rootStubStatus: RootStubStatus
|
||||
extendMode: boolean
|
||||
): void {
|
||||
console.log(); // Empty line for spacing
|
||||
const successHeadline = extendMode
|
||||
@@ -664,16 +506,6 @@ export class InitCommand {
|
||||
console.log();
|
||||
console.log(PALETTE.lightGray('Tool summary:'));
|
||||
const summaryLines = [
|
||||
rootStubStatus === 'created'
|
||||
? `${PALETTE.white('▌')} ${PALETTE.white(
|
||||
'Root AGENTS.md stub created for other assistants'
|
||||
)}`
|
||||
: null,
|
||||
rootStubStatus === 'updated'
|
||||
? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray(
|
||||
'Root AGENTS.md stub refreshed for other assistants'
|
||||
)}`
|
||||
: null,
|
||||
created.length
|
||||
? `${PALETTE.white('▌')} ${PALETTE.white(
|
||||
'Created:'
|
||||
@@ -755,8 +587,7 @@ export class InitCommand {
|
||||
.map((tool) => tool.successLabel ?? tool.name)
|
||||
.filter((name): name is string => Boolean(name));
|
||||
|
||||
if (names.length === 0)
|
||||
return PALETTE.lightGray('your AGENTS.md-compatible assistant');
|
||||
if (names.length === 0) return PALETTE.lightGray('your AI assistant');
|
||||
if (names.length === 1) return PALETTE.white(names[0]);
|
||||
|
||||
const base = names.slice(0, -1).map((name) => PALETTE.white(name));
|
||||
|
||||
@@ -3,7 +3,7 @@ export type SlashCommandId = 'proposal' | 'apply' | 'archive';
|
||||
const baseGuardrails = `**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`;
|
||||
- Refer to \`openspec/AGENTS.md\` if you need additional OpenSpec conventions or clarifications.`;
|
||||
|
||||
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
|
||||
|
||||
@@ -12,7 +12,7 @@ const proposalSteps = `**Steps**
|
||||
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in \`changes/<id>/specs/<capability>/spec.md\` (one folder per capability) using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
|
||||
5. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ export const VALIDATION_MESSAGES = {
|
||||
|
||||
// Guidance snippets (appended to primary messages for remediation)
|
||||
GUIDE_NO_DELTAS:
|
||||
'No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
||||
'No deltas found. Ensure your change has a specs/ directory with .md files using delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
||||
GUIDE_MISSING_SPEC_SECTIONS:
|
||||
'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
|
||||
GUIDE_MISSING_CHANGE_SECTIONS:
|
||||
'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
|
||||
GUIDE_SCENARIO_FORMAT:
|
||||
'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
|
||||
} as const;
|
||||
} as const;
|
||||
+8
-110
@@ -129,52 +129,8 @@ describe('InitCommand', () => {
|
||||
expect(updatedContent).toContain('Custom instructions here');
|
||||
});
|
||||
|
||||
it('should create Windsurf workflows when Windsurf is selected', async () => {
|
||||
queueSelections('windsurf', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const wsProposal = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
const wsApply = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
const wsArchive = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(wsProposal)).toBe(true);
|
||||
expect(await fileExists(wsApply)).toBe(true);
|
||||
expect(await fileExists(wsArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(wsProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('auto_execution_mode: 3');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(wsApply, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('auto_execution_mode: 3');
|
||||
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(wsArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('auto_execution_mode: 3');
|
||||
expect(archiveContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
|
||||
});
|
||||
|
||||
it('should always create AGENTS.md in project root', async () => {
|
||||
queueSelections(DONE);
|
||||
it('should create AGENTS.md in project root when AGENTS standard is selected', async () => {
|
||||
queueSelections('agents', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
@@ -309,42 +265,6 @@ describe('InitCommand', () => {
|
||||
expect(archiveContent).toContain('openspec list --specs');
|
||||
});
|
||||
|
||||
it('should create Kilo Code workflows with templates', async () => {
|
||||
queueSelections('kilocode', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-proposal.md'
|
||||
);
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-apply.md'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(proposalPath)).toBe(true);
|
||||
expect(await fileExists(applyPath)).toBe(true);
|
||||
expect(await fileExists(archivePath)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
expect(proposalContent).not.toContain('---\n');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
expect(applyContent).not.toContain('---\n');
|
||||
|
||||
const archiveContent = await fs.readFile(archivePath, 'utf-8');
|
||||
expect(archiveContent).toContain('openspec list --specs');
|
||||
expect(archiveContent).not.toContain('---\n');
|
||||
});
|
||||
|
||||
it('should add new tool when OpenSpec already exists', async () => {
|
||||
queueSelections('claude', DONE, 'cursor', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
@@ -357,10 +277,12 @@ describe('InitCommand', () => {
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow extend mode with no additional native tools', async () => {
|
||||
it('should error when extend mode selects no tools', async () => {
|
||||
queueSelections('claude', DONE, DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await expect(initCommand.execute(testDir)).resolves.toBeUndefined();
|
||||
await expect(initCommand.execute(testDir)).rejects.toThrow(
|
||||
/OpenSpec seems to already be initialized/
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-existent target directory', async () => {
|
||||
@@ -384,7 +306,7 @@ describe('InitCommand', () => {
|
||||
});
|
||||
|
||||
it('should reference AGENTS compatible assistants in success message', async () => {
|
||||
queueSelections(DONE);
|
||||
queueSelections('agents', DONE);
|
||||
const logSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
@@ -404,9 +326,7 @@ describe('InitCommand', () => {
|
||||
|
||||
expect(mockPrompt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseMessage: expect.stringContaining(
|
||||
'Which natively supported AI tools do you use?'
|
||||
),
|
||||
baseMessage: expect.stringContaining('Which AI tools do you use?'),
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -433,28 +353,6 @@ describe('InitCommand', () => {
|
||||
);
|
||||
expect(claudeChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should preselect Kilo Code when workflows already exist', async () => {
|
||||
queueSelections('kilocode', DONE, 'kilocode', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const preselected = secondRunArgs.initialSelected ?? [];
|
||||
expect(preselected).toContain('kilocode');
|
||||
});
|
||||
|
||||
it('should mark Windsurf as already configured during extend mode', async () => {
|
||||
queueSelections('windsurf', DONE, 'windsurf', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const wsChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'windsurf'
|
||||
);
|
||||
expect(wsChoice.configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
||||
@@ -192,109 +192,6 @@ Old body
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Kilo Code workflows', async () => {
|
||||
const kilocodePath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(kilocodePath), { recursive: true });
|
||||
const initialContent = `<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(kilocodePath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(kilocodePath, 'utf-8');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated.startsWith('<!-- OPENSPEC:START -->')).toBe(true);
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .kilocode/workflows/openspec-apply.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Windsurf workflows', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(wsPath), { recursive: true });
|
||||
const initialContent = `## OpenSpec: Apply (Windsurf)
|
||||
Intro
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(wsPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(wsPath, 'utf-8');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated).toContain('## OpenSpec: Apply (Windsurf)');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should preserve Windsurf content outside markers during update', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(wsPath), { recursive: true });
|
||||
const initialContent = `## Custom Intro Title\nSome intro text\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->\n\nFooter stays`;
|
||||
await fs.writeFile(wsPath, initialContent);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(wsPath, 'utf-8');
|
||||
expect(updated).toContain('## Custom Intro Title');
|
||||
expect(updated).toContain('Footer stays');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated).toContain('Validate with `openspec validate <id> --strict`');
|
||||
});
|
||||
|
||||
it('should not create missing Windsurf workflows on update', async () => {
|
||||
const wsApply = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-apply.md'
|
||||
);
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(wsApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
wsApply,
|
||||
'<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const wsProposal = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
const wsArchive = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(wsProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(wsArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should handle no AI tool files present', async () => {
|
||||
// Execute update command with no AI tool files
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
Reference in New Issue
Block a user