mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42de5d86e | ||
|
|
4a863285b0 | ||
|
|
fe83be5d61 | ||
|
|
345f9dbb45 | ||
|
|
cc9d5402ff | ||
|
|
108bcd66d8 | ||
|
|
a50105e03c | ||
|
|
312e1d6d7c | ||
|
|
56d57da119 | ||
|
|
f56189a8f7 | ||
|
|
d7e0ce85e5 | ||
|
|
eb0d50c094 | ||
|
|
c482f1b47a | ||
|
|
2ae0484ac7 | ||
|
|
8c65b47abe | ||
|
|
9c9e57daa1 | ||
|
|
c7ca76cb4f | ||
|
|
06bd3999bf | ||
|
|
821097079a | ||
|
|
42e3118b0c | ||
|
|
a785c2a99a | ||
|
|
af513191eb | ||
|
|
efbbf3b9f1 | ||
|
|
6105211163 | ||
|
|
b3d31d224d |
@@ -1,5 +1,41 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 312e1d6: Add Amazon Q Developer CLI integration. OpenSpec now supports Amazon Q Developer with automatic prompt generation in `.amazonq/prompts/` directory, allowing you to use OpenSpec slash commands with Amazon Q's @-syntax.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d7e0ce8: Improve init wizard Enter key behavior to allow proceeding through prompts more naturally
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2ae0484: Fix cross-platform path handling issues. This release includes fixes for joinPath behavior and slash command path resolution to ensure OpenSpec works correctly across all platforms.
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8210970: Fix OpenSpec not working on Windows when Codex integration is selected. This release includes fixes for cross-platform path handling and normalization to ensure OpenSpec works correctly on Windows systems.
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- efbbf3b: Add support for Codex and GitHub Copilot slash commands with YAML frontmatter and $ARGUMENTS
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add GitHub Copilot slash command support. OpenSpec now writes prompts to `.github/prompts/openspec-{proposal,apply,archive}.prompt.md` with YAML frontmatter and `$ARGUMENTS` placeholder, and refreshes them on `openspec update`.
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -96,6 +96,8 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
|
||||
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
|
||||
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
|
||||
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/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`.
|
||||
|
||||
@@ -104,7 +106,7 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
|
||||
|
||||
| Tools |
|
||||
|-------|
|
||||
| Amp • Jules • Gemini CLI • GitHub Copilot • Others |
|
||||
| Amp • Jules • Gemini CLI • Others |
|
||||
|
||||
### Install & Initialize
|
||||
|
||||
@@ -143,6 +145,20 @@ openspec init
|
||||
- Primary AI tools can trigger `/openspec` workflows without additional configuration
|
||||
- Run `openspec list` to verify the setup and view any active changes
|
||||
|
||||
### Factory Droid custom commands
|
||||
|
||||
Factory Droid turns every file in `.factory/commands` into a slash command. Repos can ship shared commands under `<repo>/.factory/commands` while private shortcuts live in `~/.factory/commands`; the workspace copy wins when both exist.【F:openspec/changes/add-factory-slash-scaffold/specs/factory-cli/spec.md†L5-L16】 Only Markdown files and executables with a shebang are registered, and filenames are slugged to lowercase with spaces converted to hyphens so `/Code Review.mdx` becomes `/code-review`.【F:openspec/changes/add-factory-slash-scaffold/specs/factory-cli/spec.md†L8-L23】
|
||||
|
||||
Use the OpenSpec CLI to scaffold those files without memorizing Factory’s frontmatter:
|
||||
|
||||
```bash
|
||||
openspec factory slash "Code Review" \
|
||||
--description "Send a code review checklist" \
|
||||
--argument-hint "<branch-name>"
|
||||
```
|
||||
|
||||
The command creates `.factory/commands/code-review.md` with YAML frontmatter, `$ARGUMENTS` placeholders, and helpful TODOs. Add `--personal` to target `~/.factory/commands`, `--executable` to generate a `code-review.sh` script with a ready-to-edit Bash template, or `--force` to overwrite an existing file. Executable templates include a proper shebang and `set -euo pipefail` so they run inside Factory just like the docs describe.【F:openspec/changes/add-factory-slash-scaffold/specs/factory-cli/spec.md†L8-L47】
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# OpenSpec Parallel Delta Remediation Plan
|
||||
|
||||
## Problem Summary
|
||||
- Active changes apply requirement-level replacements when archiving. When two changes touch the same requirement, the second archive overwrites the first and silently drops scenarios (e.g., Windsurf vs. Kilo Code slash command updates).
|
||||
- The archive workflow (`src/core/archive.ts:191` and `src/core/archive.ts:501`) rebuilds main specs by replacing entire requirement blocks with the content contained in the change delta. The delta format (`src/core/parsers/requirement-blocks.ts:113`) has no notion of base versions or scenario-level operations.
|
||||
- The tooling cannot detect divergence between the change author’s starting point and the live spec, so parallel development corrupts the source of truth without warning.
|
||||
|
||||
## Observed Failure Mode
|
||||
- Change A (`add-windsurf-workflows`) adds a Windsurf scenario under `Slash Command Configuration`.
|
||||
- Change B (`add-kilocode-workflows`) adds a Kilo Code scenario to the same requirement, starting from the pre-Windsurf spec.
|
||||
- After Change A archives, the main spec contains both scenarios.
|
||||
- When Change B archives, `buildUpdatedSpec` sees a `MODIFIED` block for `Slash Command Configuration` and replaces the requirement with the four-scenario variant shipped in that change. Because that file never learned about Windsurf, the Windsurf scenario disappears.
|
||||
- There is no warning, diff, or conflict indicator—the archive completes successfully, and the source-of-truth spec now omits a shipped scenario.
|
||||
|
||||
## Root Causes
|
||||
1. **Replace-only semantics.** `buildUpdatedSpec` performs hash-map substitution of requirement blocks and cannot merge or compare individual scenarios (`src/core/archive.ts:455`-`src/core/archive.ts:526`).
|
||||
2. **Missing base fingerprint.** Changes do not persist the requirement content they were authored against, so the archive step cannot tell if the live spec diverged.
|
||||
3. **Single-level granularity.** The delta language only understands requirements. Even if we introduced scenario-level parsing, we would still lose sibling edits without an accompanying merge strategy.
|
||||
4. **Lack of conflict UX.** The CLI never forces contributors to reconcile parallel updates. There is no equivalent of `git merge`, `git rebase`, or conflict markers.
|
||||
|
||||
## Design Objectives
|
||||
- Preserve every approved scenario regardless of archive order.
|
||||
- Detect and block speculative archives when the live spec diverges from the author’s base.
|
||||
- Provide a deterministic, reviewable conflict resolution flow that mirrors source-control best practices.
|
||||
- Keep the authoring experience ergonomic: deltas should remain human-editable markdown.
|
||||
- Support incremental adoption so existing repositories can roll forward without breaking active work.
|
||||
|
||||
## Proposed Fix: Layered Remediation
|
||||
|
||||
### Phase 0 – Stop the Bleeding (Detection & Guardrails)
|
||||
1. **Persist requirement fingerprints alongside each change.**
|
||||
- When scaffolding or validating a change, capture the current requirement body for every `MODIFIED`/`REMOVED`/`RENAMED` entry and write it to `changes/<id>/meta.json`.
|
||||
- Store a stable hash (e.g., SHA-256) of the base requirement content and the raw text itself for later merges.
|
||||
2. **Validate fingerprints during archive.**
|
||||
- Before `buildUpdatedSpec` mutates specs, recompute the requirement hash from the live spec.
|
||||
- If the hash differs from the stored base, abort and instruct the user to rebase. This makes the destructive path impossible.
|
||||
3. **Surface intent in CLI output.**
|
||||
- Show which requirements are stale, when they diverged, and which change last touched them.
|
||||
4. **Document interim manual mitigation.**
|
||||
- Update `openspec/AGENTS.md` and docs so contributors know to rerun `openspec change sync` (see Phase 1) whenever another change lands.
|
||||
|
||||
_Outcome:_ We prevent data loss immediately while we work on a richer merge story.
|
||||
|
||||
### Phase 1 – Add a Rebase Workflow (Author-Side Merge)
|
||||
1. **Introduce `openspec change sync <id>` (or `rebase`).**
|
||||
- Reads the stored base snapshot, the current spec, and the author’s delta.
|
||||
- Performs a 3-way merge per requirement. A naive diff3 on markdown lines is acceptable initially because we already operate on requirement-sized chunks.
|
||||
- If the merge is clean, rewrite the `MODIFIED` block with the merged text and refresh the stored fingerprint.
|
||||
- On conflict, write conflict markers inside the change delta (similar to Git) and require the author to hand-edit before re-running validation.
|
||||
2. **Enrich validator messages.**
|
||||
- `openspec validate` should flag unresolved conflict markers or fingerprint mismatches so errors appear early in the workflow.
|
||||
3. **Improve diff tooling.**
|
||||
- Extend `openspec diff` to compare change deltas against the live spec and highlight pending merges.
|
||||
4. **Optional:** Offer a `--rewrite-scenarios` helper that merges bullet lists of scenarios to reduce manual editing noise.
|
||||
|
||||
_Outcome:_ Contributors can safely reconcile their work with the latest spec before archiving, restoring true parallel development.
|
||||
|
||||
### Phase 2 – Increase Delta Granularity
|
||||
1. **Extend the delta language with scenario-level directives.**
|
||||
- Allow `## MODIFIED Requirements` + `## ADDED Scenarios` / `## MODIFIED Scenarios` sections nested under the requirement header.
|
||||
- Backed by stable scenario identifiers (explicit IDs or generated hashes) stored in `meta.json`. This lets the system reason about individual scenarios.
|
||||
2. **Teach the parser to understand nested operations.**
|
||||
- Update `parseDeltaSpec` to emit scenario-level operations in addition to requirement blocks.
|
||||
- Update `buildUpdatedSpec` (or its replacement) to merge scenario lists, preserving order while inserting new entries in a deterministic fashion.
|
||||
3. **Automate migration.**
|
||||
- Provide a one-time command that inspects each existing spec, injects scenario IDs, and rewrites in-flight change deltas into the richer format.
|
||||
4. **Continue to rely on the Phase 1 rebase flow for conflicts when two changes edit the same scenario body or description.**
|
||||
|
||||
_Outcome:_ Most concurrent updates become commutative, drastically reducing the odds of human merges.
|
||||
|
||||
### Phase 3 – Structured Spec Graph (Long-Term)
|
||||
1. **Define stable requirement IDs.**
|
||||
- Embed `Requirement ID: <uuid>` markers in specs so renames and moves are trackable.
|
||||
- This enables future features like cross-capability references and better diff visualizations.
|
||||
2. **Model spec edits as operations over an AST.**
|
||||
- Build an intermediate representation (IR) for requirements/scenarios/metadata.
|
||||
- Use operational transforms or CRDT-like techniques to guarantee merge associativity.
|
||||
3. **Integrate with Git directly.**
|
||||
- Offer optional `openspec branch` scaffolding that aligns spec changes with Git branches, letting teams leverage Git’s conflict editor for the markdown IR.
|
||||
|
||||
_Outcome:_ OpenSpec graduates from replace-based updates to a resilient, intent-preserving spec management platform.
|
||||
|
||||
## Migration & Product Impacts
|
||||
- **Backfill metadata:** add hashes for all active changes and the current main specs during the initial rollout.
|
||||
- **CLI UX:** new commands (`change sync`, enhanced `archive`) require documentation, help text, and release notes.
|
||||
- **Docs & AGENTS updates:** reinforce the rebase workflow and explain conflict resolution to AI assistants.
|
||||
- **Testing:** introduce fixtures covering divergent requirement fingerprints and merge resolution logic.
|
||||
- **Telemetry (optional):** log fingerprint mismatches so we can see how often teams hit conflicts after the rollout.
|
||||
|
||||
## Open Questions / Risks
|
||||
- How should we order scenarios when multiple changes insert at different points? (Consider optional `position` metadata or deterministic alphabetical fallbacks.)
|
||||
- What is the graceful failure mode if contributors delete the `meta.json` file? (CLI should recreate fingerprints on demand.)
|
||||
- Do we need to support offline authors who cannot easily re-run the sync command before archiving? (Potential `--accept-outdated` escape hatch for emergencies.)
|
||||
- How will archived historical changes be handled? We may need a migration script to embed fingerprints retroactively so re-validation succeeds.
|
||||
|
||||
## Immediate Next Steps
|
||||
1. Prototype fingerprint capture during `openspec change validate` and block archive on mismatches.
|
||||
2. Ship `openspec change sync` with line-based diff3 merging and conflict markers.
|
||||
3. Update contributor docs and AI instructions to mandate running `sync` before archiving.
|
||||
4. Plan the scenario-level delta extension and migration path as a follow-up RFC.
|
||||
@@ -0,0 +1,11 @@
|
||||
## Why
|
||||
Factory Droid lets engineers speed up reviews and deployment rituals by loading custom slash commands from `.factory/commands`. Without tooling, every team member has to remember naming rules, directory locations, and Markdown frontmatter details from the Factory docs. This friction keeps the workflow underused and risks inconsistently formatted prompts.
|
||||
|
||||
## What Changes
|
||||
- Add an `openspec factory slash <name>` command that scaffolds Factory Droid custom commands as Markdown prompts or executable scripts using the conventions from the Factory documentation.
|
||||
- Provide options for setting description text, argument hints, personal vs workspace scope, and executable mode so teams can generate the files they need without manual boilerplate.
|
||||
- Update documentation/specs to describe the Factory integration and ensure the generated files respect Factory’s slugging and directory rules.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/factory-cli`
|
||||
- Affected code: `src/cli/index.ts`, `src/commands/factory.ts`, `src/utils/slug.ts`, `README.md`
|
||||
@@ -0,0 +1,47 @@
|
||||
## ADDED Requirements
|
||||
### Requirement: Slash command scaffolding
|
||||
The CLI MUST provide `openspec factory slash <name>` to scaffold Factory Droid custom slash commands following the documented conventions.
|
||||
|
||||
#### Scenario: Create workspace markdown command
|
||||
- **GIVEN** the current directory has no `.factory/commands` folder
|
||||
- **WHEN** `openspec factory slash "Code Review" --description "Send a code review checklist" --argument-hint "<branch-name>"` runs without additional flags
|
||||
- **THEN** the CLI creates `.factory/commands/code-review.md`
|
||||
- **AND** the file contains YAML frontmatter with `description` and `argument-hint`
|
||||
- **AND** the body includes a placeholder referencing `$ARGUMENTS`
|
||||
- **AND** the command slug is lowercase with spaces converted to hyphens and non URL-safe characters removed.
|
||||
|
||||
#### Scenario: Personal command directory
|
||||
- **WHEN** `openspec factory slash deploy --personal` is executed
|
||||
- **THEN** the CLI creates (or reuses) the `~/.factory/commands` directory
|
||||
- **AND** writes `deploy.md` into that directory instead of the workspace folder.
|
||||
|
||||
#### Scenario: Executable command template
|
||||
- **WHEN** `openspec factory slash deploy --executable` runs
|
||||
- **THEN** the CLI writes `.factory/commands/deploy.sh`
|
||||
- **AND** the file begins with a shebang line (e.g., `#!/usr/bin/env bash`)
|
||||
- **AND** the template includes `set -euo pipefail`
|
||||
- **AND** `$1` receives the first argument passed to the slash command.
|
||||
|
||||
#### Scenario: Prevent accidental overwrite
|
||||
- **GIVEN** `.factory/commands/checklist.md` already exists
|
||||
- **WHEN** `openspec factory slash checklist` runs without `--force`
|
||||
- **THEN** the CLI aborts with an explanatory error instead of overwriting the file.
|
||||
|
||||
#### Scenario: Force overwrite of existing command
|
||||
- **WHEN** `openspec factory slash checklist --force` runs and the file exists
|
||||
- **THEN** the CLI replaces the file contents with the new template.
|
||||
|
||||
### Requirement: Option handling and feedback
|
||||
The CLI MUST provide helpful options and output when generating commands.
|
||||
|
||||
#### Scenario: Display success message
|
||||
- **WHEN** a command file is generated successfully
|
||||
- **THEN** the CLI prints the absolute path and how to trigger the slash command (e.g., `/code-review`).
|
||||
|
||||
#### Scenario: Argument validation
|
||||
- **WHEN** the provided name is empty or resolves to an empty slug
|
||||
- **THEN** the CLI exits with a validation error explaining that only letters, numbers, spaces, dashes, and underscores are supported.
|
||||
|
||||
#### Scenario: Description and argument hint defaults
|
||||
- **WHEN** description or argument hint values are omitted
|
||||
- **THEN** the CLI still emits valid frontmatter and leaves missing fields blank so Factory can register the command.
|
||||
@@ -0,0 +1,12 @@
|
||||
## 1. Research Factory command conventions
|
||||
- [x] 1.1 Review Factory docs for `.factory/commands` discovery, slug rules, Markdown frontmatter, and executable requirements.
|
||||
|
||||
## 2. CLI implementation
|
||||
- [x] 2.1 Add a `factory` command group with a `slash <name>` subcommand in `src/cli/index.ts`.
|
||||
- [x] 2.2 Implement generator logic that slugs names, creates the correct target directory (`.factory/commands` or `~/.factory/commands`), and writes Markdown or executable templates without clobbering existing files unless `--force` is used.
|
||||
- [x] 2.3 Support options for description, argument hints, executable mode, personal scope, and force overwrites.
|
||||
|
||||
## 3. Documentation and validation
|
||||
- [x] 3.1 Add a new spec describing the Factory CLI scaffold behavior with scenarios for Markdown and executable outputs.
|
||||
- [x] 3.2 Update docs/quickstart material so agents understand how to generate Factory slash commands with the new CLI command.
|
||||
- [x] 3.3 Run `openspec validate add-factory-slash-scaffold --strict` and ensure checklists are updated.
|
||||
@@ -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,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.
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
## Why
|
||||
- Codex (the VS Code extension formerly known as Codeium Chat) exposes "slash commands" by reading Markdown prompt files from `~/.codex/prompts/`. Each file name becomes the `/command` users can run, with numbered placeholders (`$1`, `$2`, …) bound to the arguments they supply. The workflow screenshot shared by Kevin Kern ("Codex problem analyzer") shows the format OpenSpec should target so teams can invoke curated workflows straight from the chat palette.
|
||||
- Codex (the VS Code extension formerly known as Codeium Chat) exposes "slash commands" by reading Markdown prompt files from `~/.codex/prompts/`. Each file name becomes the `/command` users can run, with YAML frontmatter for metadata (`description`, `argument-hint`) and `$ARGUMENTS` to capture user input. The workflow screenshot shared by Kevin Kern ("Codex problem analyzer") shows the format OpenSpec should target so teams can invoke curated workflows straight from the chat palette.
|
||||
- Teams already rely on OpenSpec to manage the slash-command surface area for Claude, Cursor, OpenCode, Kilo Code, and Windsurf. Leaving Codex out forces them to manually copy/paste OpenSpec guardrails into `~/.codex/prompts/*.md`, which drifts quickly and undermines the "single source of truth" promise of the CLI.
|
||||
- Codex commands live outside the repository (under the user's home directory), so shipping an automated configurator that both scaffolds the prompts and keeps them refreshed via `openspec update` eliminates error-prone manual steps and keeps OpenSpec instructions synchronized across assistants.
|
||||
|
||||
## What Changes
|
||||
- Add Codex to the `openspec init` tool picker with the same "already configured" detection we use for other editors, wiring an implementation that writes managed Markdown prompts directly to Codex's global directory (`~/.codex/prompts` or `$CODEX_HOME/prompts`) with OpenSpec marker blocks.
|
||||
- Produce three Codex prompt files—`openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`—whose content mirrors the shared slash-command templates while adapting to Codex's numbered argument placeholders (e.g., `$1` for the change identifier or follow-up question text).
|
||||
- Produce three Codex prompt files—`openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`—whose content mirrors the shared slash-command templates while using YAML frontmatter (`description` and `argument-hint` fields) and `$ARGUMENTS` to capture all arguments as a single string (matching the GitHub Copilot pattern and official Codex specification).
|
||||
- Document Codex's global-only discovery and that OpenSpec writes prompts directly to `~/.codex/prompts` (or `$CODEX_HOME/prompts`).
|
||||
- Teach `openspec update` to refresh existing Codex prompts in-place (and only when they already exist) in the global directory.
|
||||
- Teach `openspec update` to refresh existing Codex prompts in-place (and only when they already exist) in the global directory, updating both frontmatter and body.
|
||||
- Document Codex support alongside other slash-command integrations and add regression coverage that exercises init/update behaviour against a temporary global prompts directory via `CODEX_HOME`.
|
||||
|
||||
## Impact
|
||||
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
- GitHub Copilot supports custom slash commands through markdown files in `.github/prompts/<name>.prompt.md`. Each file includes YAML frontmatter with a `description` label and uses `$ARGUMENTS` to capture user input. This format allows teams to expose curated workflows directly in Copilot's chat interface.
|
||||
- Teams already rely on OpenSpec to manage slash-command configurations for Claude Code, Cursor, OpenCode, Codex, Kilo Code, and Windsurf. Excluding GitHub Copilot forces developers to manually maintain OpenSpec prompts in `.github/prompts/`, which leads to drift and undermines OpenSpec's "single source of truth" promise.
|
||||
- GitHub Copilot discovers prompts from the repository's `.github/prompts/` directory, making it straightforward to version control and share across the team. Adding automated generation and refresh through `openspec init` and `openspec update` eliminates manual synchronization and keeps OpenSpec instructions consistent across all AI assistants.
|
||||
|
||||
## What Changes
|
||||
- Add GitHub Copilot to the `openspec init` tool picker with "already configured" detection similar to other editors, wiring an implementation that writes managed Markdown prompt files to `.github/prompts/` with OpenSpec marker blocks.
|
||||
- Generate three GitHub Copilot prompt files—`openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`—whose content mirrors shared slash-command templates while conforming to Copilot's frontmatter and `$ARGUMENTS` placeholder convention.
|
||||
- Document GitHub Copilot's repository-based discovery and that OpenSpec writes prompts to `.github/prompts/` with managed blocks.
|
||||
- Teach `openspec update` to refresh existing GitHub Copilot prompts in-place (only when they already exist) in the repository's `.github/prompts/` directory.
|
||||
- Document GitHub Copilot support alongside other slash-command integrations and add test coverage that exercises init/update behavior for `.github/prompts/` files.
|
||||
|
||||
## Impact
|
||||
- Specs: `cli-init`, `cli-update`
|
||||
- Code: `src/core/configurators/slash/github-copilot.ts` (new), `src/core/configurators/slash/registry.ts`, `src/core/templates/slash-command-templates.ts`, CLI tool summaries, docs
|
||||
- Tests: integration coverage for GitHub Copilot prompt scaffolding and refresh logic
|
||||
- Docs: README and CHANGELOG entries announcing GitHub Copilot slash-command support
|
||||
|
||||
## Current Spec Reference
|
||||
- `specs/cli-init/spec.md`
|
||||
- Requirements cover init UX, directory scaffolding, AI tool configuration, and existing slash-command support for Claude Code, Cursor, OpenCode, Codex, Kilo Code, and Windsurf.
|
||||
- Our `## MODIFIED` delta in `changes/.../specs/cli-init/spec.md` will copy the full "Slash Command Configuration" requirement (header, description, and all scenarios) before appending the new GitHub Copilot scenario so archiving retains every prior scenario.
|
||||
- `specs/cli-update/spec.md`
|
||||
- Requirements define update preconditions, template refresh behavior, and slash-command refresh logic for existing tools.
|
||||
- The corresponding delta preserves the entire "Slash Command Updates" requirement while adding the GitHub Copilot refresh scenario, ensuring the archive workflow replaces the block without losing existing scenarios or the "Missing slash command file" guardrail.
|
||||
@@ -0,0 +1,48 @@
|
||||
## 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 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 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 OpenCode
|
||||
- **WHEN** the user selects OpenCode during initialization
|
||||
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-apply.md`, and `.opencode/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Windsurf
|
||||
- **WHEN** the user selects Windsurf during initialization
|
||||
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Kilo Code
|
||||
- **WHEN** the user selects Kilo Code during initialization
|
||||
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Codex
|
||||
- **WHEN** the user selects Codex during initialization
|
||||
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
|
||||
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
|
||||
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
|
||||
|
||||
#### Scenario: Generating slash commands for GitHub Copilot
|
||||
- **WHEN** the user selects GitHub Copilot during initialization
|
||||
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
|
||||
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
|
||||
- **AND** include `$ARGUMENTS` placeholder to capture user input
|
||||
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
## 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 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 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 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
|
||||
|
||||
#### Scenario: Updating slash commands for Windsurf
|
||||
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Kilo Code
|
||||
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Codex
|
||||
- **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** refresh each file using the shared slash-command templates (including placeholder guidance)
|
||||
- **AND** preserve any unmanaged content outside the OpenSpec marker block
|
||||
- **AND** skip creation when a Codex prompt file is missing
|
||||
|
||||
#### Scenario: Updating slash commands for GitHub Copilot
|
||||
- **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`
|
||||
- **THEN** refresh each file using shared templates while preserving the YAML frontmatter
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
@@ -0,0 +1,30 @@
|
||||
## Implementation Tasks
|
||||
|
||||
- [x] Create `src/core/configurators/slash/github-copilot.ts` implementing `SlashCommandConfigurator` base class
|
||||
- Implement `getRelativePath()` to return `.github/prompts/openspec-{proposal,apply,archive}.prompt.md`
|
||||
- Implement `getFrontmatter()` to generate YAML frontmatter with `description` field and include `$ARGUMENTS` placeholder
|
||||
- Implement `generateAll()` to create `.github/prompts/` directory and write three prompt files with frontmatter, markers, and shared template bodies
|
||||
- Implement `updateExisting()` to refresh only the managed block between markers while preserving frontmatter
|
||||
- Set `toolId = "github-copilot"` and `isAvailable = true`
|
||||
|
||||
- [x] Register GitHub Copilot configurator in `src/core/configurators/slash/registry.ts`
|
||||
- Import `GitHubCopilotSlashCommandConfigurator`
|
||||
- Add to `SLASH_COMMAND_CONFIGURATORS` array
|
||||
- Update tool picker display name to "GitHub Copilot"
|
||||
|
||||
- [x] Update `src/core/init.ts` to include GitHub Copilot in the AI tool selection prompt
|
||||
- Add GitHub Copilot to the available tools list with detection for existing `.github/prompts/openspec-*.prompt.md` files
|
||||
- Display "(already configured)" when prompt files exist
|
||||
|
||||
- [x] Update `src/core/update.ts` to refresh GitHub Copilot prompts when they exist
|
||||
- Call `updateExisting()` for GitHub Copilot configurator when `.github/prompts/` contains OpenSpec prompt files
|
||||
|
||||
- [x] Add integration tests for GitHub Copilot slash command generation
|
||||
- Test `generateAll()` creates three prompt files with correct structure (frontmatter + markers + body)
|
||||
- Test `updateExisting()` preserves frontmatter and only updates managed blocks
|
||||
- Test that missing prompt files are not created during update
|
||||
|
||||
- [x] Update documentation
|
||||
- Add GitHub Copilot to README slash-command support table
|
||||
- Document `.github/prompts/` as the discovery location
|
||||
- Add CHANGELOG entry for GitHub Copilot support
|
||||
+19
@@ -17,6 +17,25 @@ The command SHALL configure AI coding assistants with OpenSpec instructions usin
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### 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 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 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 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`
|
||||
@@ -0,0 +1,27 @@
|
||||
## 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 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 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 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
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
@@ -0,0 +1,12 @@
|
||||
## Why
|
||||
The current `openspec init` command requires interactive prompts, preventing automation in CI/CD pipelines and scripted setups. Adding non-interactive options will enable programmatic initialization for automated workflows while maintaining the existing interactive experience as the default.
|
||||
|
||||
## What Changes
|
||||
- Replace the multiple flag design with a single `--tools` option that accepts `all`, `none`, or a comma-separated list of tool IDs
|
||||
- Update InitCommand to bypass interactive prompts when `--tools` is supplied and apply single-flag validation rules
|
||||
- Document the non-interactive behavior via the CLI init spec delta (scenarios for `all`, `none`, list parsing, and invalid entries)
|
||||
- Generate CLI help text dynamically from `AI_TOOLS` so supported tools stay in sync
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/cli-init/spec.md`
|
||||
- Affected code: `src/cli/index.ts`, `src/core/init.ts`
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Delta for CLI Init Specification
|
||||
|
||||
## ADDED Requirements
|
||||
### Requirement: Non-Interactive Mode
|
||||
The command SHALL support non-interactive operation through command-line options for automation and CI/CD use cases.
|
||||
|
||||
#### Scenario: Select all tools non-interactively
|
||||
- **WHEN** run with `--tools all`
|
||||
- **THEN** automatically select every available AI tool without prompting
|
||||
- **AND** proceed with initialization using the selected tools
|
||||
|
||||
#### Scenario: Select specific tools non-interactively
|
||||
- **WHEN** run with `--tools claude,cursor`
|
||||
- **THEN** parse the comma-separated tool IDs and validate against available tools
|
||||
- **AND** proceed with initialization using only the specified valid tools
|
||||
|
||||
#### Scenario: Skip tool configuration non-interactively
|
||||
- **WHEN** run with `--tools none`
|
||||
- **THEN** skip AI tool configuration entirely
|
||||
- **AND** only create the OpenSpec directory structure and template files
|
||||
|
||||
#### Scenario: Invalid tool specification
|
||||
- **WHEN** run with `--tools` containing any IDs not present in the AI tool registry
|
||||
- **THEN** exit with code 1 and display available values (`all`, `none`, or the supported tool IDs)
|
||||
|
||||
#### Scenario: Help text lists available tool IDs
|
||||
- **WHEN** displaying CLI help for `openspec init`
|
||||
- **THEN** show the `--tools` option description with the valid values derived from the AI tool registry
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Interactive Mode
|
||||
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
|
||||
|
||||
#### Scenario: Displaying interactive menu
|
||||
- **WHEN** run in fresh or extend mode without non-interactive options
|
||||
- **THEN** present a looping select menu that lets users toggle tools with Enter and finish via a "Done" option
|
||||
- **AND** label already configured tools with "(already configured)" while keeping disabled options marked "coming soon"
|
||||
- **AND** change the prompt copy in extend mode to "Which AI tools would you like to add or refresh?"
|
||||
- **AND** display inline instructions clarifying that Enter toggles a tool and selecting "Done" confirms the list
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. CLI Option Registration
|
||||
- [x] 1.1 Replace the multiple flag design with a single `--tools <value>` option supporting `all|none|a,b,c` and keep strict argument validation.
|
||||
- [x] 1.2 Populate the `--tools` help text dynamically from the `AI_TOOLS` registry.
|
||||
|
||||
## 2. InitCommand Modifications
|
||||
- [x] 2.1 Accept the single tools option in the InitCommand constructor and plumb it through existing flows.
|
||||
- [x] 2.2 Update tool selection logic to shortcut prompts for `all`, `none`, and explicit lists.
|
||||
- [x] 2.3 Fail fast with exit code 1 and a helpful message when the parsed list contains unsupported tool IDs.
|
||||
|
||||
## 3. Specification Updates
|
||||
- [x] 3.1 Capture the non-interactive scenarios (`all`, `none`, list, invalid) in the change delta without modifying `specs/cli-init/spec.md` directly.
|
||||
- [x] 3.2 Document that CLI help reflects the available tool IDs managed by `AI_TOOLS`.
|
||||
|
||||
## 4. Testing
|
||||
- [x] 4.1 Add unit coverage for parsing `--tools` values, including invalid entries.
|
||||
- [x] 4.2 Add integration coverage ensuring non-interactive runs generate the expected files and exit codes.
|
||||
- [x] 4.3 Verify the interactive flow remains unchanged when `--tools` is omitted.
|
||||
+19
@@ -16,6 +16,25 @@ The command SHALL configure AI coding assistants with OpenSpec instructions usin
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### 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 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 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`
|
||||
@@ -0,0 +1,27 @@
|
||||
## 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 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 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 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
|
||||
|
||||
#### 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
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. CLI wiring
|
||||
- [x] 1.1 Add Windsurf to the selectable AI tools in `openspec init`, including "already configured" detection.
|
||||
- [x] 1.2 Register a `WindsurfSlashCommandConfigurator` that writes workflows to `.windsurf/workflows/` and ensures the directory exists.
|
||||
- [x] 1.3 Ensure `openspec update` pulls the Windsurf configurator when winds is selected and skips creation when files are absent.
|
||||
|
||||
## 2. Workflow templates
|
||||
- [x] 2.1 Reuse the shared proposal/apply/archive bodies, adding Windsurf-specific headings/description before the OpenSpec markers.
|
||||
- [x] 2.2 Confirm generated Markdown (per file) stays comfortably under the 12k character ceiling noted in the Windsurf docs.
|
||||
|
||||
## 3. Tests & safeguards
|
||||
- [x] 3.1 Extend init tests to assert creation of `.windsurf/workflows/openspec-*.md` when Windsurf is chosen.
|
||||
- [x] 3.2 Extend update tests to assert existing Windsurf workflows are refreshed and non-existent files are ignored.
|
||||
- [x] 3.3 Add regression coverage for marker preservation inside Windsurf workflow files.
|
||||
|
||||
## 4. Documentation
|
||||
- [x] 4.1 Update README (and any user-facing docs) to list Windsurf under native slash/workflow integrations.
|
||||
- [x] 4.2 Call out Windsurf workflow support in release notes or CHANGELOG if applicable.
|
||||
@@ -0,0 +1,12 @@
|
||||
## 1. Messaging enhancements
|
||||
- [x] 1.1 Inventory current validation failures and map each to the desired message improvements.
|
||||
- [x] 1.2 Implement structured error builders that include file paths, normalized header names, and example fixes.
|
||||
- [x] 1.3 Ensure `openspec validate --help` and troubleshooting docs mention the richer messages and debug tips.
|
||||
|
||||
## 2. Tests
|
||||
- [x] 2.1 Add unit tests for representative errors (no deltas, missing requirement body, missing scenarios) asserting the new wording.
|
||||
- [x] 2.2 Add integration coverage verifying the Next steps footer reflects contextual guidance.
|
||||
|
||||
## 3. Documentation
|
||||
- [x] 3.1 Update troubleshooting sections and CLI docs with sample output from the enhanced errors.
|
||||
- [x] 3.2 Note the change in CHANGELOG or release notes if applicable.
|
||||
@@ -0,0 +1,11 @@
|
||||
## 1. Instruction redesign
|
||||
- [x] 1.1 Draft a quick-reference section that surfaces file templates and formatting rules at the top of `openspec/AGENTS.md`.
|
||||
- [x] 1.2 Reorganize the workflow narrative with inline examples and progressive disclosure for advanced topics.
|
||||
|
||||
## 2. Templates and checklists
|
||||
- [x] 2.1 Add copy/paste templates for proposal, tasks, design, and spec delta files.
|
||||
- [x] 2.2 Insert a pre-validation checklist capturing common lint failures before running `openspec validate`.
|
||||
|
||||
## 3. Documentation updates
|
||||
- [x] 3.1 Update supporting docs or README pointers so contributors find the redesigned instructions.
|
||||
- [x] 3.2 Confirm examples and references stay in sync with the new scaffold command guidance.
|
||||
@@ -0,0 +1,14 @@
|
||||
## Why
|
||||
- Users frequently scroll to a tool and press Enter without toggling it, resulting in no configuration changes.
|
||||
- The current workflow deviates from common CLI expectations where Enter confirms the highlighted item.
|
||||
- Aligning behavior with user expectations reduces friction during onboarding.
|
||||
|
||||
## What Changes
|
||||
- Update the init wizard so pressing Enter on a highlighted tool selects it before moving to the review step.
|
||||
- Adjust interactive instructions to clarify Enter selects the current tool and Space still toggles selections.
|
||||
- Refresh specs to capture the clarified behavior for the interactive menu.
|
||||
|
||||
## Impact
|
||||
- Users who press Enter without toggling now configure the highlighted tool instead of exiting with no selections.
|
||||
- Spacebar multi-select support remains unchanged for power users.
|
||||
- Documentation better reflects how the wizard behaves.
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Interactive Mode
|
||||
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
|
||||
#### Scenario: Displaying interactive menu
|
||||
- **WHEN** run in fresh or extend mode
|
||||
- **THEN** present a looping select menu that lets users toggle tools with Space and review selections with Enter
|
||||
- **AND** when Enter is pressed on a highlighted selectable tool that is not already selected, automatically add it to the selection before moving to review so the highlighted tool is configured
|
||||
- **AND** label already configured tools with "(already configured)" while keeping disabled options marked "coming soon"
|
||||
- **AND** change the prompt copy in extend mode to "Which AI tools would you like to add or refresh?"
|
||||
- **AND** display inline instructions clarifying that Space toggles tools and Enter selects the highlighted tool before reviewing selections
|
||||
@@ -0,0 +1,8 @@
|
||||
## 1. Implementation
|
||||
- [x] Update the tool selection wizard to auto-select the highlighted tool when Enter is pressed without prior toggles.
|
||||
- [x] Refresh inline instructions copy so Enter behavior is clear.
|
||||
- [x] Adjust or add tests if needed to cover the new selection flow.
|
||||
|
||||
## 2. Validation
|
||||
- [x] Run `pnpm run build`.
|
||||
- [x] Run `pnpm test` (or targeted suite) if applicable.
|
||||
@@ -0,0 +1,11 @@
|
||||
## 1. Implementation
|
||||
- [x] 1.1 Refactor `openspec init` to always generate the root `AGENTS.md` stub (initial run and extend mode) via shared helper logic.
|
||||
- [x] 1.2 Rework the AI tool selection wizard to surface "Natively supported" vs "Other tools" groupings and make the stub non-optional.
|
||||
- [x] 1.3 Update CLI messaging, templates, and configurators so the new flow stays in sync across init and update commands.
|
||||
- [x] 1.4 Refresh unit/integration tests to cover the unconditional stub and the regrouped prompt layout.
|
||||
- [x] 1.5 Update documentation, README snippets, and CHANGELOG entries that mention the opt-in `AGENTS.md` experience.
|
||||
|
||||
## 2. Validation
|
||||
- [x] 2.1 Run `pnpm test` targeting CLI init/update suites.
|
||||
- [x] 2.2 Execute `openspec validate update-cli-init-root-agents --strict`.
|
||||
- [x] 2.3 Perform a manual smoke test: run `openspec init` in a temp directory, confirm stub + grouped prompts, rerun in extend mode.
|
||||
+7
-7
@@ -1,12 +1,12 @@
|
||||
## 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.
|
||||
- [x] 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`.
|
||||
- [x] 1.2 Configure the action with `createGithubReleases: true` and document required secrets (`NPM_TOKEN`, default `GITHUB_TOKEN`) plus recommended concurrency safeguards.
|
||||
- [x] 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.
|
||||
- [x] 2.1 Add a `release` script to `package.json` that builds the project and runs `changeset publish` using pnpm.
|
||||
- [x] 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.
|
||||
- [x] 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.
|
||||
- [x] 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,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,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,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.
|
||||
@@ -42,20 +42,17 @@ The command SHALL generate required template files with appropriate content for
|
||||
- **AND** generate `project.md` with project context template
|
||||
|
||||
### Requirement: AI Tool Configuration
|
||||
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions based on user selection.
|
||||
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** 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)
|
||||
- AGENTS.md standard (creates or refreshes AGENTS.md stub 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
|
||||
- **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: AI Tool Configuration Details
|
||||
|
||||
@@ -84,13 +81,13 @@ This project uses OpenSpec to manage AI assistant workflows.
|
||||
|
||||
### Requirement: Interactive Mode
|
||||
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
|
||||
|
||||
#### Scenario: Displaying interactive menu
|
||||
- **WHEN** run in fresh or extend mode
|
||||
- **THEN** present a looping select menu that lets users toggle tools with Enter and finish via a "Done" option
|
||||
- **THEN** present a looping select menu that lets users toggle tools with Space and review selections with Enter
|
||||
- **AND** when Enter is pressed on a highlighted selectable tool that is not already selected, automatically add it to the selection before moving to review so the highlighted tool is configured
|
||||
- **AND** label already configured tools with "(already configured)" while keeping disabled options marked "coming soon"
|
||||
- **AND** change the prompt copy in extend mode to "Which AI tools would you like to add or refresh?"
|
||||
- **AND** display inline instructions clarifying that Enter toggles a tool and selecting "Done" confirms the list
|
||||
- **AND** display inline instructions clarifying that Space toggles tools and Enter selects the highlighted tool before reviewing selections
|
||||
|
||||
### Requirement: Safety Checks
|
||||
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
|
||||
@@ -141,11 +138,12 @@ The command SHALL use consistent exit codes to indicate different failure modes.
|
||||
- **AND** personalize the "Next steps" header using the names of the selected tools, defaulting to a generic label when none remain
|
||||
|
||||
### Requirement: Exit Code Adjustments
|
||||
`openspec init` SHALL treat extend mode with no selected tools as a guarded error.
|
||||
`openspec init` SHALL treat extend mode without new native tool selections as a successful refresh.
|
||||
|
||||
#### Scenario: Preventing empty extend runs
|
||||
- **WHEN** OpenSpec is already initialized and the user selects no additional tools
|
||||
- **THEN** exit with code 1 after showing the existing-initialization guidance message
|
||||
#### 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
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
@@ -168,6 +166,68 @@ 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 Windsurf
|
||||
- **WHEN** the user selects Windsurf during initialization
|
||||
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Kilo Code
|
||||
- **WHEN** the user selects Kilo Code during initialization
|
||||
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Codex
|
||||
- **WHEN** the user selects Codex during initialization
|
||||
- **THEN** create global prompt files at `~/.codex/prompts/openspec-proposal.md`, `~/.codex/prompts/openspec-apply.md`, and `~/.codex/prompts/openspec-archive.md` (or under `$CODEX_HOME/prompts` if set)
|
||||
- **AND** populate each file from shared templates that map the first numbered placeholder (`$1`) to the primary user input (e.g., change identifier or question text)
|
||||
- **AND** wrap the generated content in OpenSpec markers so `openspec update` can refresh the prompts without touching surrounding custom notes
|
||||
|
||||
#### Scenario: Generating slash commands for GitHub Copilot
|
||||
- **WHEN** the user selects GitHub Copilot during initialization
|
||||
- **THEN** create `.github/prompts/openspec-proposal.prompt.md`, `.github/prompts/openspec-apply.prompt.md`, and `.github/prompts/openspec-archive.prompt.md`
|
||||
- **AND** populate each file with YAML frontmatter containing a `description` field that summarizes the workflow stage
|
||||
- **AND** include `$ARGUMENTS` placeholder to capture user input
|
||||
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
### Requirement: Non-Interactive Mode
|
||||
The command SHALL support non-interactive operation through command-line options for automation and CI/CD use cases.
|
||||
|
||||
#### Scenario: Select all tools non-interactively
|
||||
- **WHEN** run with `--tools all`
|
||||
- **THEN** automatically select every available AI tool without prompting
|
||||
- **AND** proceed with initialization using the selected tools
|
||||
|
||||
#### Scenario: Select specific tools non-interactively
|
||||
- **WHEN** run with `--tools claude,cursor`
|
||||
- **THEN** parse the comma-separated tool IDs and validate against available tools
|
||||
- **AND** proceed with initialization using only the specified valid tools
|
||||
|
||||
#### Scenario: Skip tool configuration non-interactively
|
||||
- **WHEN** run with `--tools none`
|
||||
- **THEN** skip AI tool configuration entirely
|
||||
- **AND** only create the OpenSpec directory structure and template files
|
||||
|
||||
#### Scenario: Invalid tool specification
|
||||
- **WHEN** run with `--tools` containing any IDs not present in the AI tool registry
|
||||
- **THEN** exit with code 1 and display available values (`all`, `none`, or the supported tool IDs)
|
||||
|
||||
#### Scenario: Help text lists available tool IDs
|
||||
- **WHEN** displaying CLI help for `openspec init`
|
||||
- **THEN** show the `--tools` option description with the valid values derived from the AI tool registry
|
||||
|
||||
### 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
|
||||
|
||||
## Why
|
||||
|
||||
Manual creation of OpenSpec structure is error-prone and creates adoption friction. A standardized init command ensures:
|
||||
|
||||
@@ -32,18 +32,14 @@ The update command SHALL handle file updates in a predictable and safe manner.
|
||||
- **AND** if a root-level stub exists, update the managed block content so it keeps directing teammates to `@/openspec/AGENTS.md`
|
||||
|
||||
### Requirement: Tool-Agnostic Updates
|
||||
The update command SHALL handle file updates in a predictable and safe manner while respecting team tool choices.
|
||||
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** update the root-level `AGENTS.md` using the OpenSpec markers only when that file already exists, keeping the stub content that links to `@/openspec/AGENTS.md`
|
||||
- **AND** update only the OpenSpec-managed blocks in **existing** AI tool files using markers
|
||||
- **AND** use the default directory name `openspec`
|
||||
- **AND** be idempotent (repeated runs have no additional effect)
|
||||
- **AND** respect team members' AI tool choices by not creating additional tool files beyond the root `AGENTS.md`
|
||||
- **AND** do not create new root-level stub files when none are present
|
||||
- **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
|
||||
|
||||
### Requirement: Core Files Always Updated
|
||||
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
|
||||
@@ -71,6 +67,31 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Windsurf
|
||||
- **WHEN** `.windsurf/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Kilo Code
|
||||
- **WHEN** `.kilocode/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates wrapped in OpenSpec markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
- **AND** skip creating missing files (the update command only refreshes what already exists)
|
||||
|
||||
#### Scenario: Updating slash commands for Codex
|
||||
- **GIVEN** the global Codex prompt directory contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** refresh each file using the shared slash-command templates (including placeholder guidance)
|
||||
- **AND** preserve any unmanaged content outside the OpenSpec marker block
|
||||
- **AND** skip creation when a Codex prompt file is missing
|
||||
|
||||
#### Scenario: Updating slash commands for GitHub Copilot
|
||||
- **WHEN** `.github/prompts/` contains `openspec-proposal.prompt.md`, `openspec-apply.prompt.md`, and `openspec-archive.prompt.md`
|
||||
- **THEN** refresh each file using shared templates while preserving the YAML frontmatter
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
|
||||
@@ -9,17 +9,25 @@ Validation output SHALL include specific guidance to fix each error, including e
|
||||
#### Scenario: No deltas found in change
|
||||
- **WHEN** validating a change with zero parsed deltas
|
||||
- **THEN** show error "No deltas found" with guidance:
|
||||
- Ensure `openspec/changes/{id}/specs/` exists with `.md` files
|
||||
- Use delta headers: `## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`
|
||||
- Each requirement must include at least one `#### Scenario:` block
|
||||
- Try: `openspec change show {id} --json --deltas-only` to inspect what was parsed
|
||||
- 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** the validator SHALL include expected header names and a minimal skeleton:
|
||||
- **THEN** include expected header names and a minimal skeleton:
|
||||
- For Spec: `## Purpose`, `## Requirements`
|
||||
- For Change: `## Why`, `## What Changes`
|
||||
- Show an example snippet of the missing section
|
||||
- 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:`.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# docs-agent-instructions Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change improve-agent-instruction-usability. Update Purpose after archive.
|
||||
## 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
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.8.1",
|
||||
"version": "0.11.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
+39
-2
@@ -4,6 +4,7 @@ import ora from 'ora';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { InitCommand } from '../core/init.js';
|
||||
import { AI_TOOLS } from '../core/config.js';
|
||||
import { UpdateCommand } from '../core/update.js';
|
||||
import { ListCommand } from '../core/list.js';
|
||||
import { ArchiveCommand } from '../core/archive.js';
|
||||
@@ -12,6 +13,7 @@ import { registerSpecCommand } from '../commands/spec.js';
|
||||
import { ChangeCommand } from '../commands/change.js';
|
||||
import { ValidateCommand } from '../commands/validate.js';
|
||||
import { ShowCommand } from '../commands/show.js';
|
||||
import { FactoryCommand } from '../commands/factory.js';
|
||||
|
||||
const program = new Command();
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -33,10 +35,14 @@ program.hook('preAction', (thisCommand) => {
|
||||
}
|
||||
});
|
||||
|
||||
const availableToolIds = AI_TOOLS.filter((tool) => tool.available).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')
|
||||
.action(async (targetPath = '.') => {
|
||||
.option('--tools <tools>', toolsOptionDescription)
|
||||
.action(async (targetPath = '.', options?: { tools?: string }) => {
|
||||
try {
|
||||
// Validate that the path is a valid directory
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
@@ -57,7 +63,9 @@ program
|
||||
}
|
||||
}
|
||||
|
||||
const initCommand = new InitCommand();
|
||||
const initCommand = new InitCommand({
|
||||
tools: options?.tools,
|
||||
});
|
||||
await initCommand.execute(targetPath);
|
||||
} catch (error) {
|
||||
console.log(); // Empty line for spacing
|
||||
@@ -112,6 +120,35 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
const factoryCmd = program
|
||||
.command('factory')
|
||||
.description('Manage Factory Droid custom slash commands');
|
||||
|
||||
factoryCmd
|
||||
.command('slash <name>')
|
||||
.description('Scaffold a Factory slash command in .factory/commands')
|
||||
.option('--description <text>', 'Override the command description shown in slash suggestions')
|
||||
.option('--argument-hint <hint>', 'Append usage hints after the command name')
|
||||
.option('--personal', 'Create the command in ~/.factory/commands instead of the workspace directory')
|
||||
.option('--executable', 'Generate an executable script instead of Markdown')
|
||||
.option('--force', 'Overwrite the command if it already exists')
|
||||
.action(async (name: string, options?: {
|
||||
description?: string;
|
||||
argumentHint?: string;
|
||||
personal?: boolean;
|
||||
executable?: boolean;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const factoryCommand = new FactoryCommand();
|
||||
await factoryCommand.slash(name, options ?? {});
|
||||
} catch (error) {
|
||||
console.log();
|
||||
ora().fail(`Error: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Change command with subcommands
|
||||
const changeCmd = program
|
||||
.command('change')
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { slugifyFactoryCommand } from '../utils/slug.js';
|
||||
|
||||
export interface FactorySlashOptions {
|
||||
description?: string;
|
||||
argumentHint?: string;
|
||||
executable?: boolean;
|
||||
personal?: boolean;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export class FactoryCommand {
|
||||
async slash(name: string, options: FactorySlashOptions = {}): Promise<void> {
|
||||
const slug = slugifyFactoryCommand(name ?? '');
|
||||
if (!slug) {
|
||||
throw new Error('Factory command names must use letters, numbers, spaces, dashes, or underscores.');
|
||||
}
|
||||
|
||||
const baseDir = options.personal
|
||||
? path.join(os.homedir(), '.factory', 'commands')
|
||||
: path.join(process.cwd(), '.factory', 'commands');
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
|
||||
const extension = options.executable ? '.sh' : '.md';
|
||||
const filePath = path.join(baseDir, `${slug}${extension}`);
|
||||
|
||||
const fileExists = await this.pathExists(filePath);
|
||||
if (fileExists && !options.force) {
|
||||
throw new Error(`Factory command "/${slug}" already exists at ${filePath}. Use --force to overwrite.`);
|
||||
}
|
||||
|
||||
const content = options.executable
|
||||
? this.buildExecutableTemplate(slug)
|
||||
: this.buildMarkdownTemplate(options.description, options.argumentHint);
|
||||
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
if (options.executable) {
|
||||
await fs.chmod(filePath, 0o755);
|
||||
}
|
||||
|
||||
const hint = options.argumentHint ? ` ${options.argumentHint}` : '';
|
||||
console.log(`Created Factory slash command at ${filePath}`);
|
||||
console.log(`Trigger with /${slug}${hint}`.trim());
|
||||
}
|
||||
|
||||
private async pathExists(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(targetPath);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private buildMarkdownTemplate(description?: string, argumentHint?: string): string {
|
||||
const frontmatter = [
|
||||
'---',
|
||||
`description: ${description ?? ''}`,
|
||||
`argument-hint: ${argumentHint ?? ''}`,
|
||||
'---',
|
||||
'',
|
||||
];
|
||||
|
||||
const body = [
|
||||
'Replace this text with the instructions you want droid to follow.',
|
||||
'',
|
||||
'Arguments provided when invoking the slash command will appear as `$ARGUMENTS`.',
|
||||
'',
|
||||
'- Highlight important TODOs for the assistant.',
|
||||
'- Document any prerequisites the script should assume.',
|
||||
'',
|
||||
'Thanks!'
|
||||
];
|
||||
|
||||
return [...frontmatter, ...body, ''].join('\n');
|
||||
}
|
||||
|
||||
private buildExecutableTemplate(slug: string): string {
|
||||
const lines = [
|
||||
'#!/usr/bin/env bash',
|
||||
'',
|
||||
'set -euo pipefail',
|
||||
'',
|
||||
'command_name="' + slug + '"',
|
||||
'target="$1"',
|
||||
'',
|
||||
'echo "Running /${command_name} for ${target:-your task}"',
|
||||
'',
|
||||
'# Add your automation steps here',
|
||||
'# npm install',
|
||||
'# npm run lint',
|
||||
'',
|
||||
'echo "Finished /${command_name}"',
|
||||
'',
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -23,5 +23,7 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
||||
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
|
||||
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
|
||||
{ name: 'AGENTS.md (works with Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
|
||||
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
|
||||
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.amazonq/prompts/openspec-proposal.md',
|
||||
apply: '.amazonq/prompts/openspec-apply.md',
|
||||
archive: '.amazonq/prompts/openspec-archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---
|
||||
|
||||
The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
|
||||
|
||||
<UserRequest>
|
||||
$ARGUMENTS
|
||||
</UserRequest>`,
|
||||
apply: `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
|
||||
|
||||
<ChangeId>
|
||||
$ARGUMENTS
|
||||
</ChangeId>`,
|
||||
archive: `---
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---
|
||||
|
||||
The user wants to archive the following deployed change. Use the openspec instructions to archive the change and update specs.
|
||||
|
||||
<ChangeId>
|
||||
$ARGUMENTS
|
||||
</ChangeId>`
|
||||
};
|
||||
|
||||
export class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'amazon-q';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../../../utils/file-system.js';
|
||||
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
|
||||
import { OPENSPEC_MARKERS } from '../../config.js';
|
||||
@@ -28,7 +27,7 @@ export abstract class SlashCommandConfigurator {
|
||||
|
||||
for (const target of this.getTargets()) {
|
||||
const body = TemplateManager.getSlashCommandBody(target.id).trim();
|
||||
const filePath = path.join(projectPath, target.path);
|
||||
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
|
||||
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
await this.updateBody(filePath, body);
|
||||
@@ -53,7 +52,7 @@ export abstract class SlashCommandConfigurator {
|
||||
const updated: string[] = [];
|
||||
|
||||
for (const target of this.getTargets()) {
|
||||
const filePath = path.join(projectPath, target.path);
|
||||
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
const body = TemplateManager.getSlashCommandBody(target.id).trim();
|
||||
await this.updateBody(filePath, body);
|
||||
@@ -71,7 +70,7 @@ export abstract class SlashCommandConfigurator {
|
||||
// to redirect to tool-specific locations (e.g., global directories).
|
||||
resolveAbsolutePath(projectPath: string, id: SlashCommandId): string {
|
||||
const rel = this.getRelativePath(id);
|
||||
return path.join(projectPath, rel);
|
||||
return FileSystemUtils.joinPath(projectPath, rel);
|
||||
}
|
||||
|
||||
protected async updateBody(filePath: string, body: string): Promise<void> {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SlashCommandId, TemplateManager } from "../../templates/index.js";
|
||||
import { FileSystemUtils } from "../../../utils/file-system.js";
|
||||
import { OPENSPEC_MARKERS } from "../../config.js";
|
||||
|
||||
// Use POSIX-style paths for consistent logging across platforms.
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: ".codex/prompts/openspec-proposal.md",
|
||||
apply: ".codex/prompts/openspec-apply.md",
|
||||
@@ -20,21 +21,36 @@ export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string | undefined {
|
||||
// Codex does not use YAML front matter. Provide a heading-style
|
||||
// preface that captures the first numbered placeholder `$1`.
|
||||
const headers: Record<SlashCommandId, string> = {
|
||||
proposal: "Request: $1",
|
||||
apply: "Change ID: $1",
|
||||
archive: "Change ID: $1",
|
||||
// Codex supports YAML frontmatter with description and argument-hint fields,
|
||||
// plus $ARGUMENTS to capture all arguments as a single string.
|
||||
const frontmatter: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
argument-hint: request or feature description
|
||||
---
|
||||
|
||||
$ARGUMENTS`,
|
||||
apply: `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
argument-hint: change-id
|
||||
---
|
||||
|
||||
$ARGUMENTS`,
|
||||
archive: `---
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
argument-hint: change-id
|
||||
---
|
||||
|
||||
$ARGUMENTS`,
|
||||
};
|
||||
return headers[id];
|
||||
return frontmatter[id];
|
||||
}
|
||||
|
||||
private getGlobalPromptsDir(): string {
|
||||
const home = (process.env.CODEX_HOME && process.env.CODEX_HOME.trim())
|
||||
? process.env.CODEX_HOME.trim()
|
||||
: path.join(os.homedir(), ".codex");
|
||||
return path.join(home, "prompts");
|
||||
: FileSystemUtils.joinPath(os.homedir(), ".codex");
|
||||
return FileSystemUtils.joinPath(home, "prompts");
|
||||
}
|
||||
|
||||
// Codex discovers prompts globally. Generate directly in the global directory
|
||||
@@ -44,16 +60,19 @@ export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
for (const target of this.getTargets()) {
|
||||
const body = TemplateManager.getSlashCommandBody(target.id).trim();
|
||||
const promptsDir = this.getGlobalPromptsDir();
|
||||
const filePath = path.join(promptsDir, path.basename(target.path));
|
||||
const filePath = FileSystemUtils.joinPath(
|
||||
promptsDir,
|
||||
path.basename(target.path)
|
||||
);
|
||||
|
||||
await FileSystemUtils.createDirectory(path.dirname(filePath));
|
||||
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
await this.updateBody(filePath, body);
|
||||
await this.updateFullFile(filePath, target.id, body);
|
||||
} else {
|
||||
const header = this.getFrontmatter(target.id);
|
||||
const frontmatter = this.getFrontmatter(target.id);
|
||||
const sections: string[] = [];
|
||||
if (header) sections.push(header.trim());
|
||||
if (frontmatter) sections.push(frontmatter.trim());
|
||||
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
|
||||
await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
|
||||
}
|
||||
@@ -67,20 +86,41 @@ export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
const updated: string[] = [];
|
||||
for (const target of this.getTargets()) {
|
||||
const promptsDir = this.getGlobalPromptsDir();
|
||||
const filePath = path.join(promptsDir, path.basename(target.path));
|
||||
const filePath = FileSystemUtils.joinPath(
|
||||
promptsDir,
|
||||
path.basename(target.path)
|
||||
);
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
const body = TemplateManager.getSlashCommandBody(target.id).trim();
|
||||
await this.updateBody(filePath, body);
|
||||
await this.updateFullFile(filePath, target.id, body);
|
||||
updated.push(target.path);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
// Update both frontmatter and body in an existing file
|
||||
private async updateFullFile(filePath: string, id: SlashCommandId, body: string): Promise<void> {
|
||||
const content = await FileSystemUtils.readFile(filePath);
|
||||
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
|
||||
|
||||
if (startIndex === -1) {
|
||||
throw new Error(`Missing OpenSpec start marker in ${filePath}`);
|
||||
}
|
||||
|
||||
// Replace everything before the start marker with the new frontmatter
|
||||
const frontmatter = this.getFrontmatter(id);
|
||||
const sections: string[] = [];
|
||||
if (frontmatter) sections.push(frontmatter.trim());
|
||||
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
|
||||
|
||||
await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
|
||||
}
|
||||
|
||||
// Resolve to the global prompts location for configuration detection
|
||||
resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string {
|
||||
const promptsDir = this.getGlobalPromptsDir();
|
||||
const fileName = path.basename(FILE_PATHS[id]);
|
||||
return path.join(promptsDir, fileName);
|
||||
return FileSystemUtils.joinPath(promptsDir, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.github/prompts/openspec-proposal.prompt.md',
|
||||
apply: '.github/prompts/openspec-apply.prompt.md',
|
||||
archive: '.github/prompts/openspec-archive.prompt.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---
|
||||
|
||||
$ARGUMENTS`,
|
||||
apply: `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
$ARGUMENTS`,
|
||||
archive: `---
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---
|
||||
|
||||
$ARGUMENTS`
|
||||
};
|
||||
|
||||
export class GitHubCopilotSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'github-copilot';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { WindsurfSlashCommandConfigurator } from './windsurf.js';
|
||||
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
||||
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
||||
import { CodexSlashCommandConfigurator } from './codex.js';
|
||||
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
|
||||
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
@@ -16,6 +18,8 @@ export class SlashCommandRegistry {
|
||||
const kilocode = new KiloCodeSlashCommandConfigurator();
|
||||
const opencode = new OpenCodeSlashCommandConfigurator();
|
||||
const codex = new CodexSlashCommandConfigurator();
|
||||
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
|
||||
const amazonQ = new AmazonQSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
@@ -23,6 +27,8 @@ export class SlashCommandRegistry {
|
||||
this.configurators.set(kilocode.toolId, kilocode);
|
||||
this.configurators.set(opencode.toolId, opencode);
|
||||
this.configurators.set(codex.toolId, codex);
|
||||
this.configurators.set(githubCopilot.toolId, githubCopilot);
|
||||
this.configurators.set(amazonQ.toolId, amazonQ);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
|
||||
+91
-5
@@ -220,6 +220,16 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
}
|
||||
|
||||
if (isEnterKey(key)) {
|
||||
const current = config.choices[cursor];
|
||||
if (
|
||||
current &&
|
||||
current.selectable &&
|
||||
!selectedSet.has(current.value)
|
||||
) {
|
||||
const next = new Set(selected);
|
||||
next.add(current.value);
|
||||
updateSelected(next);
|
||||
}
|
||||
setStep('review');
|
||||
setError(null);
|
||||
return;
|
||||
@@ -298,7 +308,7 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
lines.push(PALETTE.white(config.baseMessage));
|
||||
lines.push(
|
||||
PALETTE.midGray(
|
||||
'Use ↑/↓ to move · Space to toggle · Enter to review selections.'
|
||||
'Use ↑/↓ to move · Space to toggle · Enter selects highlighted tool and reviews.'
|
||||
)
|
||||
);
|
||||
lines.push('');
|
||||
@@ -359,13 +369,16 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
|
||||
type InitCommandOptions = {
|
||||
prompt?: ToolSelectionPrompt;
|
||||
tools?: string;
|
||||
};
|
||||
|
||||
export class InitCommand {
|
||||
private readonly prompt: ToolSelectionPrompt;
|
||||
private readonly toolsArg?: string;
|
||||
|
||||
constructor(options: InitCommandOptions = {}) {
|
||||
this.prompt = options.prompt ?? ((config) => toolSelectionWizard(config));
|
||||
this.toolsArg = options.tools;
|
||||
}
|
||||
|
||||
async execute(targetPath: string): Promise<void> {
|
||||
@@ -460,13 +473,86 @@ export class InitCommand {
|
||||
existingTools: Record<string, boolean>,
|
||||
extendMode: boolean
|
||||
): Promise<OpenSpecConfig> {
|
||||
const selectedTools = await this.promptForAITools(
|
||||
existingTools,
|
||||
extendMode
|
||||
);
|
||||
const selectedTools = await this.getSelectedTools(existingTools, extendMode);
|
||||
return { aiTools: selectedTools };
|
||||
}
|
||||
|
||||
private async getSelectedTools(
|
||||
existingTools: Record<string, boolean>,
|
||||
extendMode: boolean
|
||||
): Promise<string[]> {
|
||||
const nonInteractiveSelection = this.resolveToolsArg();
|
||||
if (nonInteractiveSelection !== null) {
|
||||
return nonInteractiveSelection;
|
||||
}
|
||||
|
||||
// Fall back to interactive mode
|
||||
return this.promptForAITools(existingTools, extendMode);
|
||||
}
|
||||
|
||||
private resolveToolsArg(): string[] | null {
|
||||
if (typeof this.toolsArg === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = this.toolsArg.trim();
|
||||
if (raw.length === 0) {
|
||||
throw new Error(
|
||||
'The --tools option requires a value. Use "all", "none", or a comma-separated list of tool IDs.'
|
||||
);
|
||||
}
|
||||
|
||||
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
||||
const availableValues = availableTools.map((tool) => tool.value);
|
||||
const availableSet = new Set(availableValues);
|
||||
const availableList = ['all', 'none', ...availableValues].join(', ');
|
||||
|
||||
const lowerRaw = raw.toLowerCase();
|
||||
if (lowerRaw === 'all') {
|
||||
return availableValues;
|
||||
}
|
||||
|
||||
if (lowerRaw === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tokens = raw
|
||||
.split(',')
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
throw new Error(
|
||||
'The --tools option requires at least one tool ID when not using "all" or "none".'
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedTokens = tokens.map((token) => token.toLowerCase());
|
||||
|
||||
if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
|
||||
throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
|
||||
}
|
||||
|
||||
const invalidTokens = tokens.filter(
|
||||
(_token, index) => !availableSet.has(normalizedTokens[index])
|
||||
);
|
||||
|
||||
if (invalidTokens.length > 0) {
|
||||
throw new Error(
|
||||
`Invalid tool(s): ${invalidTokens.join(', ')}. Available values: ${availableList}`
|
||||
);
|
||||
}
|
||||
|
||||
const deduped: string[] = [];
|
||||
for (const token of normalizedTokens) {
|
||||
if (!deduped.includes(token)) {
|
||||
deduped.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
private async promptForAITools(
|
||||
existingTools: Record<string, boolean>,
|
||||
extendMode: boolean
|
||||
|
||||
+3
-3
@@ -106,9 +106,9 @@ export class UpdateCommand {
|
||||
}
|
||||
|
||||
if (updatedSlashFiles.length > 0) {
|
||||
summaryParts.push(
|
||||
`Updated slash commands: ${updatedSlashFiles.join(', ')}`
|
||||
);
|
||||
// Normalize to forward slashes for cross-platform log consistency
|
||||
const normalized = updatedSlashFiles.map((p) => p.replace(/\\/g, '/'));
|
||||
summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
|
||||
}
|
||||
|
||||
const failedItems = [
|
||||
|
||||
@@ -42,6 +42,33 @@ function findMarkerIndex(
|
||||
}
|
||||
|
||||
export class FileSystemUtils {
|
||||
private static isWindowsBasePath(basePath: string): boolean {
|
||||
return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
|
||||
}
|
||||
|
||||
private static normalizeSegments(segments: string[]): string[] {
|
||||
return segments
|
||||
.flatMap((segment) => segment.split(/[\\/]+/u))
|
||||
.filter((part) => part.length > 0);
|
||||
}
|
||||
|
||||
static joinPath(basePath: string, ...segments: string[]): string {
|
||||
const normalizedSegments = this.normalizeSegments(segments);
|
||||
|
||||
if (this.isWindowsBasePath(basePath)) {
|
||||
const normalizedBasePath = path.win32.normalize(basePath);
|
||||
return normalizedSegments.length
|
||||
? path.win32.join(normalizedBasePath, ...normalizedSegments)
|
||||
: normalizedBasePath;
|
||||
}
|
||||
|
||||
const posixBasePath = basePath.replace(/\\/g, '/');
|
||||
|
||||
return normalizedSegments.length
|
||||
? path.posix.join(posixBasePath, ...normalizedSegments)
|
||||
: path.posix.normalize(posixBasePath);
|
||||
}
|
||||
|
||||
static async createDirectory(dirPath: string): Promise<void> {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
// Shared utilities will be implemented here
|
||||
export {};
|
||||
export { slugifyFactoryCommand } from './slug.js';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function slugifyFactoryCommand(name: string): string {
|
||||
const normalized = name
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^A-Za-z0-9 _-]/g, '')
|
||||
.trim();
|
||||
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const slug = normalized
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.toLowerCase();
|
||||
|
||||
return slug;
|
||||
}
|
||||
@@ -3,6 +3,16 @@ import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { runCLI, cliProjectRoot } from '../helpers/run-cli.js';
|
||||
import { AI_TOOLS } from '../../src/core/config.js';
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
@@ -26,6 +36,20 @@ describe('openspec CLI e2e basics', () => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Usage: openspec');
|
||||
expect(result.stderr).toBe('');
|
||||
|
||||
});
|
||||
|
||||
it('shows dynamic tool ids in init help', async () => {
|
||||
const result = await runCLI(['init', '--help']);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const expectedTools = AI_TOOLS.filter((tool) => tool.available)
|
||||
.map((tool) => tool.value)
|
||||
.join(', ');
|
||||
const normalizedOutput = result.stdout.replace(/\s+/g, ' ').trim();
|
||||
expect(normalizedOutput).toContain(
|
||||
`Use "all", "none", or a comma-separated list of: ${expectedTools}`
|
||||
);
|
||||
});
|
||||
|
||||
it('reports the package version', async () => {
|
||||
@@ -53,4 +77,76 @@ describe('openspec CLI e2e basics', () => {
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr).toContain("Unknown item 'does-not-exist'");
|
||||
});
|
||||
|
||||
describe('init command non-interactive options', () => {
|
||||
it('initializes with --tools all option', async () => {
|
||||
const projectDir = await prepareFixture('tmp-init');
|
||||
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
||||
await fs.mkdir(emptyProjectDir, { recursive: true });
|
||||
|
||||
const result = await runCLI(['init', '--tools', 'all'], { cwd: emptyProjectDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Tool summary:');
|
||||
|
||||
// Check that tool configurations were created
|
||||
const claudePath = path.join(emptyProjectDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(emptyProjectDir, '.cursor/commands/openspec-proposal.md');
|
||||
expect(await fileExists(claudePath)).toBe(true);
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes with --tools list option', async () => {
|
||||
const projectDir = await prepareFixture('tmp-init');
|
||||
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
||||
await fs.mkdir(emptyProjectDir, { recursive: true });
|
||||
|
||||
const result = await runCLI(['init', '--tools', 'claude'], { cwd: emptyProjectDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Tool summary:');
|
||||
|
||||
const claudePath = path.join(emptyProjectDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(emptyProjectDir, '.cursor/commands/openspec-proposal.md');
|
||||
expect(await fileExists(claudePath)).toBe(true);
|
||||
expect(await fileExists(cursorProposal)).toBe(false); // Not selected
|
||||
});
|
||||
|
||||
it('initializes with --tools none option', async () => {
|
||||
const projectDir = await prepareFixture('tmp-init');
|
||||
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
||||
await fs.mkdir(emptyProjectDir, { recursive: true });
|
||||
|
||||
const result = await runCLI(['init', '--tools', 'none'], { cwd: emptyProjectDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Tool summary:');
|
||||
|
||||
const claudePath = path.join(emptyProjectDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(emptyProjectDir, '.cursor/commands/openspec-proposal.md');
|
||||
const rootAgentsPath = path.join(emptyProjectDir, 'AGENTS.md');
|
||||
|
||||
expect(await fileExists(rootAgentsPath)).toBe(true);
|
||||
expect(await fileExists(claudePath)).toBe(false);
|
||||
expect(await fileExists(cursorProposal)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns error for invalid tool names', async () => {
|
||||
const projectDir = await prepareFixture('tmp-init');
|
||||
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
||||
await fs.mkdir(emptyProjectDir, { recursive: true });
|
||||
|
||||
const result = await runCLI(['init', '--tools', 'invalid-tool'], { cwd: emptyProjectDir });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr).toContain('Invalid tool(s): invalid-tool');
|
||||
expect(result.stderr).toContain('Available values:');
|
||||
});
|
||||
|
||||
it('returns error when combining reserved keywords with explicit ids', async () => {
|
||||
const projectDir = await prepareFixture('tmp-init');
|
||||
const emptyProjectDir = path.join(projectDir, '..', 'empty-project');
|
||||
await fs.mkdir(emptyProjectDir, { recursive: true });
|
||||
|
||||
const result = await runCLI(['init', '--tools', 'all,claude'], { cwd: emptyProjectDir });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr).toContain('Cannot combine reserved values "all" or "none" with specific tool IDs');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+203
-3
@@ -339,16 +339,22 @@ describe('InitCommand', () => {
|
||||
expect(await fileExists(archivePath)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(proposalContent).toContain('Request: $1');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('argument-hint: request or feature description');
|
||||
expect(proposalContent).toContain('$ARGUMENTS');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('Change ID: $1');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('argument-hint: change-id');
|
||||
expect(applyContent).toContain('$ARGUMENTS');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(archivePath, 'utf-8');
|
||||
expect(archiveContent).toContain('Change ID: $1');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('argument-hint: change-id');
|
||||
expect(archiveContent).toContain('$ARGUMENTS');
|
||||
expect(archiveContent).toContain('openspec archive <id> --yes');
|
||||
});
|
||||
|
||||
@@ -388,6 +394,48 @@ describe('InitCommand', () => {
|
||||
expect(archiveContent).not.toContain('---\n');
|
||||
});
|
||||
|
||||
it('should create GitHub Copilot prompt files with templates', async () => {
|
||||
queueSelections('github-copilot', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-proposal.prompt.md'
|
||||
);
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-apply.prompt.md'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-archive.prompt.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('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('$ARGUMENTS');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('$ARGUMENTS');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(archivePath, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('$ARGUMENTS');
|
||||
expect(archiveContent).toContain('openspec archive <id> --yes');
|
||||
});
|
||||
|
||||
it('should add new tool when OpenSpec already exists', async () => {
|
||||
queueSelections('claude', DONE, 'cursor', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
@@ -510,6 +558,158 @@ describe('InitCommand', () => {
|
||||
);
|
||||
expect(codexChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark GitHub Copilot as already configured during extend mode', async () => {
|
||||
queueSelections('github-copilot', DONE, 'github-copilot', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const githubCopilotChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'github-copilot'
|
||||
);
|
||||
expect(githubCopilotChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create Amazon Q Developer prompt files with templates', async () => {
|
||||
queueSelections('amazon-q', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-proposal.md'
|
||||
);
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-apply.md'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/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('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('$ARGUMENTS');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('$ARGUMENTS');
|
||||
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
|
||||
});
|
||||
|
||||
it('should mark Amazon Q Developer as already configured during extend mode', async () => {
|
||||
queueSelections('amazon-q', DONE, 'amazon-q', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const amazonQChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'amazon-q'
|
||||
);
|
||||
expect(amazonQChoice.configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-interactive mode', () => {
|
||||
it('should select all available tools with --tools all option', async () => {
|
||||
const nonInteractiveCommand = new InitCommand({ tools: 'all' });
|
||||
|
||||
await nonInteractiveCommand.execute(testDir);
|
||||
|
||||
// Should create configurations for all available tools
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(
|
||||
testDir,
|
||||
'.cursor/commands/openspec-proposal.md'
|
||||
);
|
||||
const windsurfProposal = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(claudePath)).toBe(true);
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
expect(await fileExists(windsurfProposal)).toBe(true);
|
||||
});
|
||||
|
||||
it('should select specific tools with --tools option', async () => {
|
||||
const nonInteractiveCommand = new InitCommand({ tools: 'claude,cursor' });
|
||||
|
||||
await nonInteractiveCommand.execute(testDir);
|
||||
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(
|
||||
testDir,
|
||||
'.cursor/commands/openspec-proposal.md'
|
||||
);
|
||||
const windsurfProposal = path.join(
|
||||
testDir,
|
||||
'.windsurf/workflows/openspec-proposal.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(claudePath)).toBe(true);
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
expect(await fileExists(windsurfProposal)).toBe(false); // Not selected
|
||||
});
|
||||
|
||||
it('should skip tool configuration with --tools none option', async () => {
|
||||
const nonInteractiveCommand = new InitCommand({ tools: 'none' });
|
||||
|
||||
await nonInteractiveCommand.execute(testDir);
|
||||
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(
|
||||
testDir,
|
||||
'.cursor/commands/openspec-proposal.md'
|
||||
);
|
||||
|
||||
// Should still create AGENTS.md but no tool-specific files
|
||||
const rootAgentsPath = path.join(testDir, 'AGENTS.md');
|
||||
expect(await fileExists(rootAgentsPath)).toBe(true);
|
||||
expect(await fileExists(claudePath)).toBe(false);
|
||||
expect(await fileExists(cursorProposal)).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw error for invalid tool names', async () => {
|
||||
const nonInteractiveCommand = new InitCommand({ tools: 'invalid-tool' });
|
||||
|
||||
await expect(nonInteractiveCommand.execute(testDir)).rejects.toThrow(
|
||||
/Invalid tool\(s\): invalid-tool\. Available values: /
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle comma-separated tool names with spaces', async () => {
|
||||
const nonInteractiveCommand = new InitCommand({ tools: 'claude, cursor' });
|
||||
|
||||
await nonInteractiveCommand.execute(testDir);
|
||||
|
||||
const claudePath = path.join(testDir, 'CLAUDE.md');
|
||||
const cursorProposal = path.join(
|
||||
testDir,
|
||||
'.cursor/commands/openspec-proposal.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(claudePath)).toBe(true);
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject combining reserved keywords with explicit tool ids', async () => {
|
||||
const nonInteractiveCommand = new InitCommand({ tools: 'all,claude' });
|
||||
|
||||
await expect(nonInteractiveCommand.execute(testDir)).rejects.toThrow(
|
||||
/Cannot combine reserved values "all" or "none" with specific tool IDs/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
||||
+135
-3
@@ -262,7 +262,7 @@ Old body
|
||||
'.codex/prompts/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(codexPath), { recursive: true });
|
||||
const initialContent = `Change ID: $1\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
|
||||
const initialContent = `---\ndescription: Old description\nargument-hint: old-hint\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(codexPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
@@ -270,9 +270,12 @@ Old body
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(codexPath, 'utf-8');
|
||||
expect(updated).toContain('Change ID: $1');
|
||||
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(updated).toContain('argument-hint: change-id');
|
||||
expect(updated).toContain('$ARGUMENTS');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated).not.toContain('Old description');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
@@ -292,7 +295,7 @@ Old body
|
||||
await fs.mkdir(path.dirname(codexApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
codexApply,
|
||||
'Change ID: $1\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
'---\ndescription: Old\nargument-hint: old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
@@ -311,6 +314,135 @@ Old body
|
||||
await expect(FileSystemUtils.fileExists(codexArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing GitHub Copilot prompts', async () => {
|
||||
const ghPath = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-apply.prompt.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(ghPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
$ARGUMENTS
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(ghPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(ghPath, 'utf-8');
|
||||
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(updated).toContain('$ARGUMENTS');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .github/prompts/openspec-apply.prompt.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing GitHub Copilot prompts on update', async () => {
|
||||
const ghApply = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-apply.prompt.md'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(ghApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
ghApply,
|
||||
'---\ndescription: Old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const ghProposal = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-proposal.prompt.md'
|
||||
);
|
||||
const ghArchive = path.join(
|
||||
testDir,
|
||||
'.github/prompts/openspec-archive.prompt.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(ghProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing Amazon Q Developer prompts', async () => {
|
||||
const aqPath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(aqPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
|
||||
|
||||
<ChangeId>
|
||||
$ARGUMENTS
|
||||
</ChangeId>
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(aqPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updatedContent = await fs.readFile(aqPath, 'utf-8');
|
||||
expect(updatedContent).toContain('**Guardrails**');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).not.toContain('Old body');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('.amazonq/prompts/openspec-apply.md')
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing Amazon Q Developer prompts on update', async () => {
|
||||
const aqApply = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-apply.md'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(aqApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
aqApply,
|
||||
'---\ndescription: Old\n---\n\nThe user wants to apply the following change.\n\n<ChangeId>\n $ARGUMENTS\n</ChangeId>\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const aqProposal = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-proposal.md'
|
||||
);
|
||||
const aqArchive = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(aqProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(aqArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve Windsurf content outside markers during update', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
|
||||
@@ -160,4 +160,52 @@ describe('FileSystemUtils', () => {
|
||||
expect(hasPermission).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('joinPath', () => {
|
||||
it('should join POSIX-style paths', () => {
|
||||
const result = FileSystemUtils.joinPath(
|
||||
'/tmp/project',
|
||||
'.claude/commands/openspec/proposal.md'
|
||||
);
|
||||
expect(result).toBe('/tmp/project/.claude/commands/openspec/proposal.md');
|
||||
});
|
||||
|
||||
it('should join Linux home directory paths', () => {
|
||||
const result = FileSystemUtils.joinPath(
|
||||
'/home/dev/workspace/openspec',
|
||||
'.cursor/commands/install.md'
|
||||
);
|
||||
expect(result).toBe('/home/dev/workspace/openspec/.cursor/commands/install.md');
|
||||
});
|
||||
|
||||
it('should join Windows drive-letter paths with backslashes', () => {
|
||||
const result = FileSystemUtils.joinPath(
|
||||
'C:\\Users\\dev\\project',
|
||||
'.claude/commands/openspec/proposal.md'
|
||||
);
|
||||
expect(result).toBe(
|
||||
'C:\\Users\\dev\\project\\.claude\\commands\\openspec\\proposal.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('should join Windows paths that use forward slashes', () => {
|
||||
const result = FileSystemUtils.joinPath(
|
||||
'D:/workspace/app',
|
||||
'.cursor/commands/openspec-apply.md'
|
||||
);
|
||||
expect(result).toBe(
|
||||
'D:\\workspace\\app\\.cursor\\commands\\openspec-apply.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('should join UNC-style Windows paths', () => {
|
||||
const result = FileSystemUtils.joinPath(
|
||||
'\\server\\share\\repo',
|
||||
'.windsurf/workflows/openspec-archive.md'
|
||||
);
|
||||
expect(result).toBe(
|
||||
'\\server\\share\\repo\\.windsurf\\workflows\\openspec-archive.md'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user