mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 14:38:54 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e0d21d1ed | ||
|
|
31d85d0e8b | ||
|
|
bc3666d702 | ||
|
|
970b9f6e2d | ||
|
|
5cb84a775e | ||
|
|
adc63069a9 | ||
|
|
f8eca37796 |
@@ -1,19 +1,8 @@
|
||||
# Repository Guidelines
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
## Project Structure & Module Organization
|
||||
OpenSpec ships as a TypeScript-first CLI. Source code lives in `src`, with feature logic in `core`, interactive flows in `cli`, reusable helpers in `utils`, and command wiring in `commands`. After `pnpm run build`, deliverables land in `dist` and feed the published entry point `bin/openspec.js`. Specs and change proposals reside in `openspec/specs` and `openspec/changes`; update them whenever behavior shifts so automation stays aligned. Shared assets live in `assets`, and Vitest suites in `test` mirror the source layout for easy cross-reference.
|
||||
This project uses OpenSpec to manage AI assistant workflows.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Run `pnpm install` to sync dependencies. `pnpm run build` compiles TypeScript to `dist` and must stay green before release. Use `pnpm run dev` for a `tsc --watch` loop and `pnpm run dev:cli` to rebuild then execute the local CLI. `pnpm test` runs the Vitest suite once, `pnpm run test:watch` keeps it hot while iterating, and `pnpm run test:coverage` verifies instrumentation thresholds. Use `pnpm run changeset` when preparing a release entry.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
We follow idiomatic TypeScript with ES modules, 2-space indentation, and semicolons. Prefer named exports from index barrels and keep filenames kebab-cased (e.g., `list-command.ts`). Classes use `PascalCase`, functions and variables use `camelCase`, and constants representing flags may use `SCREAMING_SNAKE_CASE`. Keep modules small, colocate helpers under `src/utils`, and avoid new dependencies without spec-backed justification.
|
||||
|
||||
## Testing Guidelines
|
||||
Every behavior change needs Vitest coverage under `test`, co-located by feature (e.g., `test/core/update.test.ts`). Name suites after the module under test and lean on `vitest.setup.ts` for shared configuration. Run `pnpm test` before pushing and add regression cases for each bug fix or spec requirement.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Commits follow Conventional Commits (`type(scope): subject`) and stay single-line. Reference the touched module in the scope when practical. Each PR should summarize the spec or issue it fulfills, list manual verification steps, and note updates to any `openspec/` assets. Include CLI output snippets or screenshots when the UX changes, and ensure CI and coverage checks pass before requesting review.
|
||||
|
||||
## OpenSpec Workflow Tips
|
||||
Treat specs as the contract: update `openspec/project.md` or the relevant `openspec/specs/*.md` before coding, then run `pnpm run dev:cli` to validate the CLI against the revised artifacts. `openspec list --specs` confirms the catalog, and `openspec change` drafts proposals—commit these alongside code so reviewers can trace rationale to implementation.
|
||||
- Full guidance lives in '@/openspec/AGENTS.md'.
|
||||
- Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
<!-- OPENSPEC:END -->
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add native Kilo Code workflow integration so `openspec init` and `openspec update` manage `.kilocode/workflows/openspec-*.md` files.
|
||||
- Always scaffold the managed root `AGENTS.md` hand-off stub and regroup the AI tool prompts during init/update to keep instructions consistent.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Slim the generated root agent instructions down to a managed hand-off stub and update the init/update flows to refresh it safely.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -84,6 +84,9 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
|
||||
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
|
||||
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
|
||||
|
||||
#### AGENTS.md Compatible
|
||||
These tools automatically read workflow instructions from `openspec/AGENTS.md`. Ask them to follow the OpenSpec workflow if they need a reminder. Learn more about the [AGENTS.md convention](https://agents.md/).
|
||||
@@ -121,8 +124,8 @@ openspec init
|
||||
```
|
||||
|
||||
**What happens during initialization:**
|
||||
- You'll be prompted to select your AI tool (Claude Code, Cursor, etc.)
|
||||
- OpenSpec automatically configures slash commands or `AGENTS.md` based on your selection
|
||||
- You'll be prompted to pick any natively supported AI tools (Claude Code, Cursor, OpenCode, etc.); other assistants always rely on the shared `AGENTS.md` stub
|
||||
- OpenSpec automatically configures slash commands for the tools you choose and always writes a managed `AGENTS.md` hand-off at the project root
|
||||
- A new `openspec/` directory structure is created in your project
|
||||
|
||||
**After setup:**
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
## Why
|
||||
A recent feature request highlighted that users struggle to scope proposals when all clarifications have to be typed manually. They want a guided interview that surfaces missing assumptions, recommends best-practice defaults, and produces a sharper brief before handing off to `/openspec/proposal`. Delivering an interactive Q&A flow keeps OpenSpec competitive with other planning assistants and shortens the loop between idea and actionable change specification.
|
||||
|
||||
## What Changes
|
||||
- Add a dedicated `/openspec/proposal-qa` slash command that runs a structured discovery interview before drafting a change proposal.
|
||||
- Teach the agent to analyse the initial request, label what is already explicit versus ambiguous, and derive a small set of high-impact clarifying questions.
|
||||
- Require every question to include rationale, 2–4 recommended options with a default, and clear guidance on when to pick each option.
|
||||
- Capture the conversation outcome in a structured summary (problem, clarified decisions, open risks) that the user can accept or tweak before invoking `/openspec/proposal`.
|
||||
- Update onboarding (`init`/`update`) and slash command templates so the new command ships everywhere OpenSpec currently provisions proposal/apply/archive helpers.
|
||||
|
||||
## Impact
|
||||
- Affected specs: assistant-proposal-qa (new), cli-init, cli-update, slash-commands-template (if modelled separately)
|
||||
- Affected code: `src/core/templates/slash-command-templates.ts`, `src/core/configurators/slash/*`, command scaffolding that writes `.claude/.cursor/.opencode` files, and associated tests.
|
||||
@@ -1,63 +0,0 @@
|
||||
# assistant-proposal-qa Specification
|
||||
|
||||
## ADDED Requirements
|
||||
### Requirement: Interactive Proposal Q&A Command
|
||||
|
||||
The system SHALL provide a `/openspec/proposal-qa` slash command that prepares users for `/openspec/proposal` by running a structured discovery interview.
|
||||
|
||||
#### Scenario: Launching the proposal interview
|
||||
- **WHEN** the user runs `/openspec/proposal-qa Build a notifications digest`
|
||||
- **THEN** acknowledge the request and restate the draft problem statement
|
||||
- **AND** highlight what parts of the request are already concrete versus ambiguous (e.g., "Strong signals" and "Needs clarity")
|
||||
- **AND** outline the upcoming steps: targeted questions followed by a summary hand-off.
|
||||
|
||||
#### Scenario: Honour non-interactive environments
|
||||
- **GIVEN** slash commands are invoked in a non-interactive environment (e.g., automation requesting defaults)
|
||||
- **WHEN** `/openspec/proposal-qa` is triggered with `--no-interactive`
|
||||
- **THEN** skip the question loop
|
||||
- **AND** produce a summary that records the request, recommended defaults, and instructions for editing manually before running `/openspec/proposal`.
|
||||
|
||||
### Requirement: Targeted Clarifying Questions
|
||||
|
||||
The interview SHALL adaptively surface 3–6 high-leverage questions that eliminate ambiguity in the proposal brief.
|
||||
|
||||
#### Scenario: Ask one question at a time with rationale
|
||||
- **WHEN** the interview begins gathering answers
|
||||
- **THEN** select the next most risky/vague aspect of the feature
|
||||
- **AND** present a single question that includes:
|
||||
- A short rationale explaining why the question matters for the proposal
|
||||
- 2–4 recommended options formatted as a bulleted list with `**Default**` clearly marked
|
||||
- Guidance for when to choose each option (one sentence per option)
|
||||
- **AND** wait for the user response (or `default`/`skip`) before showing another question.
|
||||
|
||||
#### Scenario: Provide fallbacks when users defer
|
||||
- **WHEN** the user replies with `idk`, `default`, or leaves the answer empty
|
||||
- **THEN** accept the default option for that question
|
||||
- **AND** note in the transcript that the default was applied.
|
||||
|
||||
#### Scenario: Capture bespoke answers
|
||||
- **WHEN** the user supplies an answer that does not match any recommended option
|
||||
- **THEN** accept the custom answer
|
||||
- **AND** record a short interpretation describing how it will shape the proposal.
|
||||
|
||||
### Requirement: Synthesis and Handoff
|
||||
|
||||
The interview SHALL produce an actionable summary that readies the agent to draft the formal change proposal.
|
||||
|
||||
#### Scenario: Summarise discoveries before exit
|
||||
- **WHEN** the question loop completes (or is skipped)
|
||||
- **THEN** output a structured summary containing:
|
||||
- Problem statement and scope recap
|
||||
- Table or bullet list of decisions (question → final answer → reasoning/default flag)
|
||||
- Noted risks, open questions, and assumptions to confirm in the proposal
|
||||
- **AND** recommend next actions: either ask for revisions, run `/openspec/proposal` with this summary, or request further research.
|
||||
|
||||
#### Scenario: Provide reusable prompt snippet
|
||||
- **WHEN** the summary is generated
|
||||
- **THEN** include a copyable prompt block that the user can paste into `/openspec/proposal`
|
||||
- **AND** ensure the prompt references the summary decisions and flags any open items for follow-up.
|
||||
|
||||
#### Scenario: Allow re-entry for more questions
|
||||
- **WHEN** the user indicates they want to refine further (e.g., "ask more" or "another pass")
|
||||
- **THEN** identify remaining ambiguous areas not yet questioned
|
||||
- **AND** continue with additional questions (up to the 6-question cap) before regenerating the summary.
|
||||
@@ -1,21 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates, including the interactive proposal interview instructions.
|
||||
|
||||
#### 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/proposal-qa.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** include guidance that the `proposal-qa` command runs the interactive discovery interview before `/openspec/proposal`.
|
||||
|
||||
#### Scenario: Generating slash commands for Cursor
|
||||
- **WHEN** the user selects Cursor during initialization
|
||||
- **THEN** create `.cursor/commands/openspec-proposal.md`, `.cursor/commands/openspec-proposal-qa.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** include guidance that the `proposal-qa` command runs the interactive discovery interview before `/openspec/proposal`.
|
||||
|
||||
#### Scenario: Generating slash commands for OpenCode
|
||||
- **WHEN** the user selects OpenCode during initialization
|
||||
- **THEN** create `.opencode/commands/openspec-proposal.md`, `.opencode/commands/openspec-proposal-qa.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** include guidance that the `proposal-qa` command runs the interactive discovery interview before `/openspec/proposal`.
|
||||
@@ -1,18 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools, including the interactive proposal interview template, without creating new ones.
|
||||
|
||||
#### Scenario: Updating slash commands for Claude Code
|
||||
- **WHEN** `.claude/commands/openspec/` contains `proposal.md`, `proposal-qa.md`, `apply.md`, and `archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure the `proposal-qa` template contains the interactive discovery instructions aligned with the latest assistant guidance.
|
||||
|
||||
#### Scenario: Updating slash commands for Cursor
|
||||
- **WHEN** `.cursor/commands/` contains `openspec-proposal.md`, `openspec-proposal-qa.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure the `proposal-qa` template contains the interactive discovery instructions aligned with the latest assistant guidance.
|
||||
|
||||
#### Scenario: Updating slash commands for OpenCode
|
||||
- **WHEN** `.opencode/commands/` contains `openspec-proposal.md`, `openspec-proposal-qa.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure the `proposal-qa` template contains the interactive discovery instructions aligned with the latest assistant guidance.
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1. Discovery & Design
|
||||
- [ ] 1.1 Audit existing slash command files (`src/core/templates/slash-command-templates.ts`, configurators, tests) to mirror tone/format.
|
||||
- [ ] 1.2 Draft conversational flow map covering request analysis, question generation heuristics, question formatting, and post-qa summary/next-steps.
|
||||
- [ ] 1.3 Validate the flow against the example repo in issue #85 to ensure parity with proven UX patterns (e.g., defaults, recommended answers).
|
||||
|
||||
## 2. Specification Updates
|
||||
- [ ] 2.1 Add `assistant-proposal-qa` capability spec capturing requirements for analysis, questioning, summary, and hand-off UX.
|
||||
- [ ] 2.2 Update `cli-init` and `cli-update` specs so generated slash command files include the new `proposal-qa` command for all supported assistants.
|
||||
|
||||
## 3. Implementation
|
||||
- [ ] 3.1 Extend `SlashCommandId` union, templates, and file writers to emit `/openspec/proposal-qa` with the new instructions body.
|
||||
- [ ] 3.2 Implement helper(s) that build recommended answer options from agent analysis (list of option label, description, default marker).
|
||||
- [ ] 3.3 Ensure question loop enforces 3–6 prompts, each with rationale and recommended default, and gracefully handles user-supplied alternatives.
|
||||
- [ ] 3.4 Update onboarding/update flows to write `.claude/.cursor/.opencode` command markdown for `proposal-qa` alongside existing commands.
|
||||
|
||||
## 4. Validation & QA
|
||||
- [ ] 4.1 Add unit tests covering slash template rendering for the new command and regression tests for init/update scaffolding.
|
||||
- [ ] 4.2 Update documentation and examples (README, CHANGELOG as needed) showcasing how to use `/openspec/proposal-qa` and the resulting summary output.
|
||||
- [ ] 4.3 Run `openspec validate add-interactive-proposal-qa --strict` and full test suite (`pnpm test`) to confirm specs and tooling stay green.
|
||||
@@ -0,0 +1,17 @@
|
||||
## Why
|
||||
- Kilo Code executes \"slash commands\" by loading markdown workflows from `.kilocode/workflows/` (or the global `~/.kilocode/workflows/`) and running them when a user types `/workflow-name.md`, making project-local workflow files the analogue to the slash-command files we already ship for other tools.\\
|
||||
([Workflows | Kilo Code Docs](https://kilocode.ai/docs/features/slash-commands/workflows))
|
||||
- Those workflows are plain markdown with step-by-step instructions that can call built-in tools and MCP integrations, so reusing OpenSpec's shared proposal/apply/archive bodies keeps behaviour aligned across assistants without inventing new content.
|
||||
- OpenSpec already detects configured tools and refreshes marker-wrapped files during `init`/`update`; extending the same mechanism to `.kilocode/workflows/openspec-*.md` ensures Kilo Code stays in sync with one source of truth.
|
||||
|
||||
## What Changes
|
||||
- Add Kilo Code to the `openspec init` tool picker with \"already configured\" detection, including wiring for extend mode so teams can refresh Kilo Code assets.
|
||||
- Implement a `KiloCodeSlashCommandConfigurator` that creates `.kilocode/workflows/openspec-{proposal,apply,archive}.md`, ensuring the workflow directory exists and wrapping shared content in OpenSpec markers (no front matter required).
|
||||
- Teach `openspec update` to refresh existing Kilo Code workflows (and only those that already exist) using the shared slash-command templates.
|
||||
- Update documentation, release notes, and integration tests so the new workflow support is covered alongside Claude, Cursor, OpenCode, and Windsurf.
|
||||
|
||||
## Impact
|
||||
- Specs: `cli-init`, `cli-update`
|
||||
- Code: `src/core/config.ts`, `src/core/configurators/(registry|slash/*)`, `src/core/templates/slash-command-templates.ts`, CLI wiring for tool summaries
|
||||
- Tests: init/update workflow coverage, regression for marker preservation in `.kilocode/workflows/`
|
||||
- Docs: README / CHANGELOG updates advertising Kilo Code workflow support
|
||||
@@ -0,0 +1,24 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: AI Tool Configuration
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system.
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
- **WHEN** run interactively
|
||||
- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu
|
||||
- **AND** list every available tool with a checkbox:
|
||||
- Claude Code (creates or refreshes CLAUDE.md and slash commands)
|
||||
- Cursor (creates or refreshes `.cursor/commands/*` slash commands)
|
||||
- OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands)
|
||||
- Windsurf (creates or refreshes `.windsurf/workflows/openspec-*.md` workflows)
|
||||
- Kilo Code (creates or refreshes `.kilocode/workflows/openspec-*.md` workflows)
|
||||
- AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers)
|
||||
- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content
|
||||
- **AND** treat disabled tools as "coming soon" and keep them unselectable
|
||||
- **AND** allow confirming with Enter after selecting one or more tools
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
#### Scenario: Generating slash commands for Kilo Code
|
||||
- **WHEN** the user selects Kilo Code during initialization
|
||||
- **THEN** create `.kilocode/workflows/openspec-proposal.md`, `.kilocode/workflows/openspec-apply.md`, and `.kilocode/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
@@ -0,0 +1,8 @@
|
||||
## 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)
|
||||
@@ -0,0 +1,15 @@
|
||||
## 1. CLI wiring
|
||||
- [x] 1.1 Add Kilo Code to the selectable AI tools in `openspec init`, including "already configured" detection and success summaries.
|
||||
- [x] 1.2 Register a `KiloCodeSlashCommandConfigurator` alongside other slash-command tools.
|
||||
|
||||
## 2. Workflow generation
|
||||
- [x] 2.1 Implement the configurator so it creates `.kilocode/workflows/` (if needed) and writes `openspec-{proposal,apply,archive}.md` with OpenSpec markers.
|
||||
- [x] 2.2 Reuse the shared slash-command bodies without front matter; verify resulting files stay Markdown-only with no extra metadata.
|
||||
|
||||
## 3. Update support
|
||||
- [x] 3.1 Ensure `openspec update` refreshes existing Kilo Code workflows while skipping ones that are absent.
|
||||
- [x] 3.2 Add regression coverage confirming marker content is replaced (not duplicated) during updates.
|
||||
|
||||
## 4. Documentation
|
||||
- [x] 4.1 Update README / docs to note Kilo Code workflow support and path (`.kilocode/workflows/`).
|
||||
- [x] 4.2 Mention the integration in CHANGELOG or release notes if applicable.
|
||||
@@ -0,0 +1,17 @@
|
||||
## Why
|
||||
- Windsurf exposes "Workflows" as the vehicle for slash-like automation: saved Markdown files under `.windsurf/workflows/` that Cascade discovers across the workspace (including subdirectories and up to the git root), then executes when a user types `/workflow-name`. These files can be team-authored, must stay under 12k characters, and can call other workflows, making them the natural place to publish OpenSpec guidance for Windsurf users.\
|
||||
([Windsurf Workflows documentation](https://docs.windsurf.com/windsurf/cascade/workflows))
|
||||
- The Wave 12 changelog reiterates that workflows are invoked via slash commands and that Windsurf stores them in `.windsurf/workflows`, so the OpenSpec CLI just needs to generate Markdown there to participate in Windsurf's command palette.\
|
||||
("Custom Workflows" section, [Windsurf changelog](https://windsurf.com/changelog))
|
||||
- OpenSpec already ships shared command bodies for proposal/apply/archive and uses markers so commands stay up to date. Extending the same templates to Windsurf keeps behaviour consistent with Claude, Cursor, and OpenCode without inventing new content flows.
|
||||
|
||||
## What Changes
|
||||
- Add Windsurf to the CLI tool picker (`openspec init`) and the slash-command registry so selecting it scaffolds `.windsurf/workflows/openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` with marker-managed bodies.
|
||||
- Shape each Windsurf workflow with a short heading/description plus the existing OpenSpec guardrails/steps wrapped in markers, ensuring the total payload remains well below the 12,000 character limit.
|
||||
- Ensure `openspec update` refreshes existing Windsurf workflows (and only those that already exist) in-place, mirroring current behaviour for other editors.
|
||||
- Extend unit tests for init/update to cover Windsurf generation and updates, and update the README/tooling docs to advertise Windsurf support.
|
||||
|
||||
## Impact
|
||||
- Specs: `cli-init`, `cli-update`
|
||||
- Code: `src/core/configurators/slash/*`, `src/core/templates/slash-command-templates.ts`, CLI prompts, README
|
||||
- Tests: init/update integration coverage for Windsurf workflows
|
||||
@@ -0,0 +1,23 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: AI Tool Configuration
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions using a marker system.
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
- **WHEN** run interactively
|
||||
- **THEN** prompt the user with "Which AI tools do you use?" using a multi-select menu
|
||||
- **AND** list every available tool with a checkbox:
|
||||
- Claude Code (creates or refreshes CLAUDE.md and slash commands)
|
||||
- Cursor (creates or refreshes `.cursor/commands/*` slash commands)
|
||||
- OpenCode (creates or refreshes `.opencode/command/openspec-*.md` slash commands)
|
||||
- Windsurf (creates or refreshes `.windsurf/workflows/openspec-*.md` workflows)
|
||||
- AGENTS.md standard (creates or refreshes AGENTS.md with OpenSpec markers)
|
||||
- **AND** show "(already configured)" beside tools whose managed files exist so users understand selections will refresh content
|
||||
- **AND** treat disabled tools as "coming soon" and keep them unselectable
|
||||
- **AND** allow confirming with Enter after selecting one or more tools
|
||||
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
#### Scenario: Generating slash commands for Windsurf
|
||||
- **WHEN** the user selects Windsurf during initialization
|
||||
- **THEN** create `.windsurf/workflows/openspec-proposal.md`, `.windsurf/workflows/openspec-apply.md`, and `.windsurf/workflows/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates (wrapped in OpenSpec markers) so workflow text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
@@ -0,0 +1,8 @@
|
||||
## 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)
|
||||
@@ -0,0 +1,17 @@
|
||||
## 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.
|
||||
@@ -0,0 +1,13 @@
|
||||
## Why
|
||||
The project root currently receives a full copy of the OpenSpec agent instructions, duplicating the content that also lives in `openspec/AGENTS.md`. When teams edit one copy but not the other, the files drift and onboarding assistants see conflicting guidance.
|
||||
|
||||
## What Changes
|
||||
- Keep generating the complete template in `openspec/AGENTS.md` during `openspec init` and follow-up updates.
|
||||
- Replace the root-level file (`AGENTS.md` or `CLAUDE.md`, depending on tool selection) with a short hand-off that explains the project uses OpenSpec and points directly to `openspec/AGENTS.md`.
|
||||
- Add a dedicated stub template so both the init and update flows reuse the same minimal copy instructions.
|
||||
- Update CLI tests and documentation to reflect the new root-level messaging and ensure the OpenSpec marker block still protects future updates.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `cli-init`, `cli-update`
|
||||
- Affected code: `src/core/init.ts`, `src/core/update.ts`, `src/core/templates/agents-template.ts`
|
||||
- Update assets/readmes that mention the root `AGENTS.md` contents to reference the new stub message.
|
||||
@@ -0,0 +1,15 @@
|
||||
## 1. Templates
|
||||
- [x] 1.1 Add a shared stub template that renders the root agent instructions hand-off message.
|
||||
- [x] 1.2 Ensure the stub covers both `AGENTS.md` and `CLAUDE.md` variants.
|
||||
|
||||
## 2. Init Flow
|
||||
- [x] 2.1 Update `createInitArtifacts` to write the stub to the project root instead of the full instructions.
|
||||
- [x] 2.2 Preserve the managed block markers so future updates can overwrite the stub safely.
|
||||
|
||||
## 3. Update Flow
|
||||
- [x] 3.1 Make the update command refresh the root stub rather than the full instructions.
|
||||
- [x] 3.2 Confirm the update log output still reflects the files that changed.
|
||||
|
||||
## 4. Tests & Docs
|
||||
- [x] 4.1 Adjust CLI/init tests to match the new root content.
|
||||
- [x] 4.2 Document the stub message in `openspec/specs/cli-init` and `openspec/specs/cli-update` (and any relevant README snippets).
|
||||
@@ -0,0 +1,15 @@
|
||||
## Why
|
||||
OpenSpec currently creates the root-level `AGENTS.md` stub only when teams explicitly select the "AGENTS.md standard" tool during `openspec init`. Projects that skip that checkbox never get a managed stub, so non-native assistants (Copilot, Codeium, etc.) have no entry point and later `openspec update` runs silently create the file without any context. We need to bake the stub into initialization, clarify the tool selection experience, and keep the update workflow aligned so every teammate lands on the right instructions from day one.
|
||||
|
||||
## What Changes
|
||||
- Update `openspec init` so the root `AGENTS.md` stub is always generated (first run and extend mode) and refreshed from a shared utility instead of being tied to a tool selection.
|
||||
- Redesign the AI tool selection wizard to split options into "Natively supported" (Claude, Cursor, OpenCode, …) and an informational "Other tools" section that explains the always-on `AGENTS.md` hand-off.
|
||||
- Adjust CLI specs, prompts, and success messaging to reflect the new categories while keeping extend-mode behaviour consistent.
|
||||
- Update automated tests and fixtures to cover the unconditional stub creation and the reworked prompt flow.
|
||||
- Refresh documentation and onboarding snippets so they no longer describe the stub as opt-in and instead call out the new grouping.
|
||||
- Ensure `openspec update` continues to reconcile both `openspec/AGENTS.md` and the root stub, documenting the expected behaviour so mismatched setups self-heal.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `cli-init`, `cli-update`
|
||||
- Affected code: `src/core/init.ts`, `src/core/config.ts`, `src/core/configurators/agents.ts`, `src/core/templates/agents-root-stub.ts`, `src/core/update.ts`, related tests under `test/core/`
|
||||
- Docs & assets: README, CHANGELOG, any setup guides that reference choosing the "AGENTS.md standard" option
|
||||
@@ -0,0 +1,32 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: AI Tool Configuration
|
||||
The command SHALL configure AI coding assistants with OpenSpec instructions using a grouped selection experience so teams can enable native integrations while always provisioning guidance for other assistants.
|
||||
|
||||
#### Scenario: Prompting for AI tool selection
|
||||
- **WHEN** run interactively
|
||||
- **THEN** present a multi-select wizard that separates options into two headings:
|
||||
- **Natively supported providers** shows each available first-party integration (Claude Code, Cursor, OpenCode, …) with checkboxes
|
||||
- **Other tools** explains that the root-level `AGENTS.md` stub is always generated for AGENTS-compatible assistants and cannot be deselected
|
||||
- **AND** mark already configured native tools with "(already configured)" to signal that choosing them will refresh managed content
|
||||
- **AND** keep disabled or unavailable providers labelled as "coming soon" so users know they cannot opt in yet
|
||||
- **AND** allow confirming the selection even when no native provider is chosen because the root stub remains enabled by default
|
||||
- **AND** change the base prompt copy in extend mode to "Which natively supported AI tools would you like to add or refresh?"
|
||||
|
||||
### Requirement: Exit Code Adjustments
|
||||
`openspec init` SHALL treat extend mode without new native tool selections as a successful refresh.
|
||||
|
||||
#### Scenario: Allowing empty extend runs
|
||||
- **WHEN** OpenSpec is already initialized and the user selects no additional natively supported tools
|
||||
- **THEN** complete successfully while refreshing the root `AGENTS.md` stub
|
||||
- **AND** exit with code 0
|
||||
|
||||
## ADDED Requirements
|
||||
### Requirement: Root instruction stub
|
||||
`openspec init` SHALL always scaffold the root-level `AGENTS.md` hand-off so every teammate finds the primary OpenSpec instructions.
|
||||
|
||||
#### Scenario: Creating root `AGENTS.md`
|
||||
- **GIVEN** the project may or may not already contain an `AGENTS.md` file
|
||||
- **WHEN** initialization completes in fresh or extend mode
|
||||
- **THEN** create or refresh `AGENTS.md` at the repository root using the managed marker block from `TemplateManager.getAgentsStandardTemplate()`
|
||||
- **AND** preserve any existing content outside the managed markers while replacing the stub text inside them
|
||||
- **AND** create the stub regardless of which native AI tools are selected
|
||||
@@ -0,0 +1,10 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Tool-Agnostic Updates
|
||||
The update command SHALL refresh OpenSpec-managed files in a predictable manner while respecting each team's chosen tooling.
|
||||
|
||||
#### Scenario: Updating files
|
||||
- **WHEN** updating files
|
||||
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** create or refresh the root-level `AGENTS.md` stub using the managed marker block, even if the file was previously absent
|
||||
- **AND** update only the OpenSpec-managed sections inside existing AI tool files, leaving user-authored content untouched
|
||||
- **AND** avoid creating new native-tool configuration files (slash commands, CLAUDE.md, etc.) unless they already exist
|
||||
@@ -0,0 +1,11 @@
|
||||
## 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.
|
||||
@@ -38,7 +38,7 @@ The command SHALL generate required template files with appropriate content for
|
||||
|
||||
#### Scenario: Generating template files
|
||||
- **WHEN** initializing OpenSpec
|
||||
- **THEN** generate `AGENTS.md` containing complete OpenSpec instructions for AI assistants
|
||||
- **THEN** generate `openspec/AGENTS.md` containing complete OpenSpec instructions for AI assistants
|
||||
- **AND** generate `project.md` with project context template
|
||||
|
||||
### Requirement: AI Tool Configuration
|
||||
@@ -52,7 +52,7 @@ The command SHALL configure AI coding assistants with OpenSpec instructions base
|
||||
- **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 with OpenSpec markers)
|
||||
- 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
|
||||
@@ -65,16 +65,22 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
|
||||
|
||||
- **WHEN** Claude Code is selected
|
||||
- **THEN** create or update `CLAUDE.md` in the project root directory (not inside openspec/)
|
||||
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
|
||||
|
||||
#### Scenario: Creating new CLAUDE.md
|
||||
|
||||
- **WHEN** CLAUDE.md does not exist
|
||||
- **THEN** create new file with OpenSpec content wrapped in markers:
|
||||
- **THEN** create new file with stub instructions wrapped in markers so the full workflow stays in `openspec/AGENTS.md`:
|
||||
```markdown
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
This project uses OpenSpec to manage AI assistant workflows.
|
||||
|
||||
- Full guidance lives in '@/openspec/AGENTS.md'.
|
||||
- Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
<!-- OPENSPEC:END -->
|
||||
```
|
||||
|
||||
### Requirement: Interactive Mode
|
||||
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
|
||||
@@ -168,4 +174,4 @@ Manual creation of OpenSpec structure is error-prone and creates adoption fricti
|
||||
- Consistent structure across all projects
|
||||
- Proper AI instruction files are always included
|
||||
- Quick onboarding for new projects
|
||||
- Clear conventions from the start
|
||||
- Clear conventions from the start
|
||||
|
||||
@@ -10,6 +10,7 @@ The update command SHALL update OpenSpec instruction files to the latest templat
|
||||
#### Scenario: Running update command
|
||||
- **WHEN** a user runs `openspec update`
|
||||
- **THEN** replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** if a root-level stub (`AGENTS.md`/`CLAUDE.md`) exists, refresh it so it points to `@/openspec/AGENTS.md`
|
||||
|
||||
### Requirement: Prerequisites
|
||||
|
||||
@@ -28,6 +29,7 @@ The update command SHALL handle file updates in a predictable and safe manner.
|
||||
#### Scenario: Updating files
|
||||
- **WHEN** updating files
|
||||
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
|
||||
- **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.
|
||||
@@ -36,11 +38,12 @@ The update command SHALL handle file updates in a predictable and safe manner wh
|
||||
|
||||
- **WHEN** updating files
|
||||
- **THEN** completely replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** create or update the root-level `AGENTS.md` using the OpenSpec markers
|
||||
- **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
|
||||
|
||||
### Requirement: Core Files Always Updated
|
||||
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
|
||||
@@ -48,6 +51,7 @@ The update command SHALL always update the core OpenSpec files and display an AS
|
||||
#### Scenario: Successful update
|
||||
- **WHEN** the update completes successfully
|
||||
- **THEN** replace `openspec/AGENTS.md` with the latest template
|
||||
- **AND** if a root-level stub exists, refresh it so it still directs contributors to `@/openspec/AGENTS.md`
|
||||
|
||||
### Requirement: Slash Command Updates
|
||||
The update command SHALL refresh existing slash command files for configured tools without creating new ones.
|
||||
@@ -63,7 +67,7 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for OpenCode
|
||||
- **WHEN** `.opencode/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **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
|
||||
|
||||
@@ -104,4 +108,4 @@ Users SHALL be able to:
|
||||
The update process SHALL be:
|
||||
- Simple and fast (no version checking)
|
||||
- Predictable (same result every time)
|
||||
- Self-contained (no network required)
|
||||
- Self-contained (no network required)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
+5
-4
@@ -17,8 +17,9 @@ export interface AIToolOption {
|
||||
}
|
||||
|
||||
export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Claude Code (✅ OpenSpec custom slash commands available)', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cursor (✅ OpenSpec custom slash commands available)', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'OpenCode (✅ OpenSpec custom slash commands available)', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'AGENTS.md (works with Codex, Amp, Copilot, …)', value: 'agents', available: true, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
||||
{ name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SlashCommandConfigurator } from "./base.js";
|
||||
import { SlashCommandId } from "../../templates/index.js";
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: ".kilocode/workflows/openspec-proposal.md",
|
||||
apply: ".kilocode/workflows/openspec-apply.md",
|
||||
archive: ".kilocode/workflows/openspec-archive.md"
|
||||
};
|
||||
|
||||
export class KiloCodeSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = "kilocode";
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(_id: SlashCommandId): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { ClaudeSlashCommandConfigurator } from './claude.js';
|
||||
import { CursorSlashCommandConfigurator } from './cursor.js';
|
||||
import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
||||
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
@@ -9,10 +10,12 @@ export class SlashCommandRegistry {
|
||||
static {
|
||||
const claude = new ClaudeSlashCommandConfigurator();
|
||||
const cursor = new CursorSlashCommandConfigurator();
|
||||
const kilocode = new KiloCodeSlashCommandConfigurator();
|
||||
const opencode = new OpenCodeSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
this.configurators.set(kilocode.toolId, kilocode);
|
||||
this.configurators.set(opencode.toolId, opencode);
|
||||
}
|
||||
|
||||
|
||||
+238
-75
@@ -22,19 +22,13 @@ import {
|
||||
OPENSPEC_DIR_NAME,
|
||||
AIToolOption,
|
||||
} from './config.js';
|
||||
import { PALETTE } from './styles/palette.js';
|
||||
|
||||
const PROGRESS_SPINNER = {
|
||||
interval: 80,
|
||||
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
||||
};
|
||||
|
||||
const PALETTE = {
|
||||
white: chalk.hex('#f4f4f4'),
|
||||
lightGray: chalk.hex('#c8c8c8'),
|
||||
midGray: chalk.hex('#8a8a8a'),
|
||||
darkGray: chalk.hex('#4a4a4a'),
|
||||
};
|
||||
|
||||
const LETTER_MAP: Record<string, string[]> = {
|
||||
O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
|
||||
P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
|
||||
@@ -65,11 +59,24 @@ const parseToolLabel = (raw: string): ToolLabel => {
|
||||
};
|
||||
};
|
||||
|
||||
type ToolWizardChoice = {
|
||||
value: string;
|
||||
label: ToolLabel;
|
||||
configured: boolean;
|
||||
};
|
||||
const isSelectableChoice = (
|
||||
choice: ToolWizardChoice
|
||||
): choice is Extract<ToolWizardChoice, { selectable: true }> => choice.selectable;
|
||||
|
||||
type ToolWizardChoice =
|
||||
| {
|
||||
kind: 'heading' | 'info';
|
||||
value: string;
|
||||
label: ToolLabel;
|
||||
selectable: false;
|
||||
}
|
||||
| {
|
||||
kind: 'option';
|
||||
value: string;
|
||||
label: ToolLabel;
|
||||
configured: boolean;
|
||||
selectable: true;
|
||||
};
|
||||
|
||||
type ToolWizardConfig = {
|
||||
extendMode: boolean;
|
||||
@@ -82,21 +89,41 @@ type WizardStep = 'intro' | 'select' | 'review';
|
||||
|
||||
type ToolSelectionPrompt = (config: ToolWizardConfig) => Promise<string[]>;
|
||||
|
||||
type RootStubStatus = 'created' | 'updated' | 'skipped';
|
||||
|
||||
const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
|
||||
|
||||
const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
|
||||
const LIST_SPACER_VALUE = '__list-spacer__';
|
||||
|
||||
const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
(config, done) => {
|
||||
const totalSteps = 3;
|
||||
const [step, setStep] = useState<WizardStep>('intro');
|
||||
const [cursor, setCursor] = useState<number>(0);
|
||||
const [selected, setSelected] = useState<string[]>(
|
||||
() => config.initialSelected ?? []
|
||||
const selectableChoices = config.choices.filter(isSelectableChoice);
|
||||
const initialCursorIndex = config.choices.findIndex((choice) =>
|
||||
choice.selectable
|
||||
);
|
||||
const [cursor, setCursor] = useState<number>(
|
||||
initialCursorIndex === -1 ? 0 : initialCursorIndex
|
||||
);
|
||||
const [selected, setSelected] = useState<string[]>(() => {
|
||||
const initial = new Set(
|
||||
(config.initialSelected ?? []).filter((value) =>
|
||||
selectableChoices.some((choice) => choice.value === value)
|
||||
)
|
||||
);
|
||||
return selectableChoices
|
||||
.map((choice) => choice.value)
|
||||
.filter((value) => initial.has(value));
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const selectedSet = new Set(selected);
|
||||
const pageSize = Math.max(Math.min(config.choices.length, 7), 1);
|
||||
const pageSize = Math.max(config.choices.length, 1);
|
||||
|
||||
const updateSelected = (next: Set<string>) => {
|
||||
const ordered = config.choices
|
||||
const ordered = selectableChoices
|
||||
.map((choice) => choice.value)
|
||||
.filter((value) => next.has(value));
|
||||
setSelected(ordered);
|
||||
@@ -106,8 +133,17 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
items: config.choices,
|
||||
active: cursor,
|
||||
pageSize,
|
||||
loop: config.choices.length > 1,
|
||||
loop: false,
|
||||
renderItem: ({ item, isActive }) => {
|
||||
if (!item.selectable) {
|
||||
const prefix = item.kind === 'info' ? ' ' : '';
|
||||
const textColor =
|
||||
item.kind === 'heading' ? PALETTE.lightGray : PALETTE.midGray;
|
||||
return `${PALETTE.midGray(' ')} ${PALETTE.midGray(' ')} ${textColor(
|
||||
`${prefix}${item.label.primary}`
|
||||
)}`;
|
||||
}
|
||||
|
||||
const isSelected = selectedSet.has(item.value);
|
||||
const cursorSymbol = isActive
|
||||
? PALETTE.white('›')
|
||||
@@ -116,13 +152,36 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
? PALETTE.white('◉')
|
||||
: PALETTE.midGray('○');
|
||||
const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
|
||||
const label = `${nameColor(item.label.primary)}${
|
||||
item.configured ? PALETTE.midGray(' (already configured)') : ''
|
||||
}`;
|
||||
const annotation = item.label.annotation
|
||||
? PALETTE.midGray(` (${item.label.annotation})`)
|
||||
: '';
|
||||
const configuredNote = item.configured
|
||||
? PALETTE.midGray(' (already configured)')
|
||||
: '';
|
||||
const label = `${nameColor(item.label.primary)}${annotation}${configuredNote}`;
|
||||
return `${cursorSymbol} ${indicator} ${label}`;
|
||||
},
|
||||
});
|
||||
|
||||
const moveCursor = (direction: 1 | -1) => {
|
||||
if (selectableChoices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextIndex = cursor;
|
||||
while (true) {
|
||||
nextIndex = nextIndex + direction;
|
||||
if (nextIndex < 0 || nextIndex >= config.choices.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.choices[nextIndex]?.selectable) {
|
||||
setCursor(nextIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useKeypress((key) => {
|
||||
if (step === 'intro') {
|
||||
if (isEnterKey(key)) {
|
||||
@@ -133,24 +192,20 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
|
||||
if (step === 'select') {
|
||||
if (isUpKey(key)) {
|
||||
const previousIndex =
|
||||
cursor <= 0 ? config.choices.length - 1 : cursor - 1;
|
||||
setCursor(previousIndex);
|
||||
moveCursor(-1);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownKey(key)) {
|
||||
const nextIndex =
|
||||
cursor >= config.choices.length - 1 ? 0 : cursor + 1;
|
||||
setCursor(nextIndex);
|
||||
moveCursor(1);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSpaceKey(key)) {
|
||||
const current = config.choices[cursor];
|
||||
if (!current) return;
|
||||
if (!current || !current.selectable) return;
|
||||
|
||||
const next = new Set(selected);
|
||||
if (next.has(current.value)) {
|
||||
@@ -165,17 +220,14 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
}
|
||||
|
||||
if (isEnterKey(key)) {
|
||||
if (selected.length === 0) {
|
||||
setError('Select at least one AI tool to continue.');
|
||||
return;
|
||||
}
|
||||
setStep('review');
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.name === 'escape') {
|
||||
setSelected([]);
|
||||
const next = new Set<string>();
|
||||
updateSelected(next);
|
||||
setError(null);
|
||||
}
|
||||
return;
|
||||
@@ -185,7 +237,10 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
if (isEnterKey(key)) {
|
||||
const finalSelection = config.choices
|
||||
.map((choice) => choice.value)
|
||||
.filter((value) => selectedSet.has(value));
|
||||
.filter(
|
||||
(value) =>
|
||||
selectedSet.has(value) && value !== ROOT_STUB_CHOICE_VALUE
|
||||
);
|
||||
done(finalSelection);
|
||||
return;
|
||||
}
|
||||
@@ -197,9 +252,30 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
}
|
||||
});
|
||||
|
||||
const selectedNames = config.choices
|
||||
.filter((choice) => selectedSet.has(choice.value))
|
||||
.map((choice) => choice.label.primary);
|
||||
const rootStubChoice = selectableChoices.find(
|
||||
(choice) => choice.value === ROOT_STUB_CHOICE_VALUE
|
||||
);
|
||||
const rootStubSelected = rootStubChoice
|
||||
? selectedSet.has(ROOT_STUB_CHOICE_VALUE)
|
||||
: false;
|
||||
const nativeChoices = selectableChoices.filter(
|
||||
(choice) => choice.value !== ROOT_STUB_CHOICE_VALUE
|
||||
);
|
||||
const selectedNativeChoices = nativeChoices.filter((choice) =>
|
||||
selectedSet.has(choice.value)
|
||||
);
|
||||
|
||||
const formatSummaryLabel = (
|
||||
choice: Extract<ToolWizardChoice, { selectable: true }>
|
||||
) => {
|
||||
const annotation = choice.label.annotation
|
||||
? PALETTE.midGray(` (${choice.label.annotation})`)
|
||||
: '';
|
||||
const configuredNote = choice.configured
|
||||
? PALETTE.midGray(' (already configured)')
|
||||
: '';
|
||||
return `${PALETTE.white(choice.label.primary)}${annotation}${configuredNote}`;
|
||||
};
|
||||
|
||||
const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
|
||||
const lines: string[] = [];
|
||||
@@ -228,16 +304,21 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
lines.push('');
|
||||
lines.push(page);
|
||||
lines.push('');
|
||||
if (selectedNames.length === 0) {
|
||||
lines.push(PALETTE.midGray('Selected configuration:'));
|
||||
if (rootStubSelected && rootStubChoice) {
|
||||
lines.push(
|
||||
`${PALETTE.midGray('Selected')}: ${PALETTE.midGray(
|
||||
'None selected yet'
|
||||
)}`
|
||||
` ${PALETTE.white('-')} ${formatSummaryLabel(rootStubChoice)}`
|
||||
);
|
||||
}
|
||||
if (selectedNativeChoices.length === 0) {
|
||||
lines.push(
|
||||
` ${PALETTE.midGray('- No natively supported providers selected')}`
|
||||
);
|
||||
} else {
|
||||
lines.push(PALETTE.midGray('Selected:'));
|
||||
selectedNames.forEach((name) => {
|
||||
lines.push(` ${PALETTE.white('-')} ${PALETTE.white(name)}`);
|
||||
selectedNativeChoices.forEach((choice) => {
|
||||
lines.push(
|
||||
` ${PALETTE.white('-')} ${formatSummaryLabel(choice)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -247,13 +328,23 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
|
||||
);
|
||||
lines.push('');
|
||||
|
||||
if (selectedNames.length === 0) {
|
||||
if (rootStubSelected && rootStubChoice) {
|
||||
lines.push(
|
||||
PALETTE.midGray('No tools selected. Press Backspace to return.')
|
||||
`${PALETTE.white('▌')} ${formatSummaryLabel(rootStubChoice)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedNativeChoices.length === 0) {
|
||||
lines.push(
|
||||
PALETTE.midGray(
|
||||
'No natively supported providers selected. Universal instructions will still be applied.'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
selectedNames.forEach((name) => {
|
||||
lines.push(`${PALETTE.white('▌')} ${PALETTE.white(name)}`);
|
||||
selectedNativeChoices.forEach((choice) => {
|
||||
lines.push(
|
||||
`${PALETTE.white('▌')} ${formatSummaryLabel(choice)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -291,17 +382,6 @@ export class InitCommand {
|
||||
// Get configuration (after validation to avoid prompts if validation fails)
|
||||
const config = await this.getConfiguration(existingToolStates, extendMode);
|
||||
|
||||
if (config.aiTools.length === 0) {
|
||||
if (extendMode) {
|
||||
throw new Error(
|
||||
`OpenSpec seems to already be initialized at ${openspecPath}.\n` +
|
||||
`Use 'openspec update' to update the structure.`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('You must select at least one AI tool to configure.');
|
||||
}
|
||||
|
||||
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
||||
const selectedIds = new Set(config.aiTools);
|
||||
const selectedTools = availableTools.filter((tool) =>
|
||||
@@ -341,7 +421,11 @@ export class InitCommand {
|
||||
|
||||
// Step 2: Configure AI tools
|
||||
const toolSpinner = this.startSpinner('Configuring AI tools...');
|
||||
await this.configureAITools(projectPath, openspecDir, config.aiTools);
|
||||
const rootStubStatus = await this.configureAITools(
|
||||
projectPath,
|
||||
openspecDir,
|
||||
config.aiTools
|
||||
);
|
||||
toolSpinner.stopAndPersist({
|
||||
symbol: PALETTE.white('▌'),
|
||||
text: PALETTE.white('AI tools configured'),
|
||||
@@ -354,7 +438,8 @@ export class InitCommand {
|
||||
refreshed,
|
||||
skippedExisting,
|
||||
skipped,
|
||||
extendMode
|
||||
extendMode,
|
||||
rootStubStatus
|
||||
);
|
||||
}
|
||||
|
||||
@@ -388,27 +473,69 @@ export class InitCommand {
|
||||
): Promise<string[]> {
|
||||
const availableTools = AI_TOOLS.filter((tool) => tool.available);
|
||||
|
||||
if (availableTools.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const baseMessage = extendMode
|
||||
? 'Which AI tools would you like to add or refresh?'
|
||||
: 'Which AI tools do you use?';
|
||||
const initialSelected = extendMode
|
||||
? 'Which natively supported AI tools would you like to add or refresh?'
|
||||
: 'Which natively supported AI tools do you use?';
|
||||
const initialNativeSelection = extendMode
|
||||
? availableTools
|
||||
.filter((tool) => existingTools[tool.value])
|
||||
.map((tool) => tool.value)
|
||||
: [];
|
||||
|
||||
return this.prompt({
|
||||
extendMode,
|
||||
baseMessage,
|
||||
choices: availableTools.map((tool) => ({
|
||||
const initialSelected = Array.from(new Set(initialNativeSelection));
|
||||
|
||||
const choices: ToolWizardChoice[] = [
|
||||
{
|
||||
kind: 'heading',
|
||||
value: '__heading-native__',
|
||||
label: {
|
||||
primary:
|
||||
'Natively supported providers (✔ OpenSpec custom slash commands available)',
|
||||
},
|
||||
selectable: false,
|
||||
},
|
||||
...availableTools.map<ToolWizardChoice>((tool) => ({
|
||||
kind: 'option',
|
||||
value: tool.value,
|
||||
label: parseToolLabel(tool.name),
|
||||
configured: Boolean(existingTools[tool.value]),
|
||||
selectable: true,
|
||||
})),
|
||||
...(availableTools.length
|
||||
? ([
|
||||
{
|
||||
kind: 'info' as const,
|
||||
value: LIST_SPACER_VALUE,
|
||||
label: { primary: '' },
|
||||
selectable: false,
|
||||
},
|
||||
] as ToolWizardChoice[])
|
||||
: []),
|
||||
{
|
||||
kind: 'heading',
|
||||
value: OTHER_TOOLS_HEADING_VALUE,
|
||||
label: {
|
||||
primary:
|
||||
'Other tools (use Universal AGENTS.md for Codex, Amp, VS Code, GitHub Copilot, …)',
|
||||
},
|
||||
selectable: false,
|
||||
},
|
||||
{
|
||||
kind: 'option',
|
||||
value: ROOT_STUB_CHOICE_VALUE,
|
||||
label: {
|
||||
primary: 'Universal AGENTS.md',
|
||||
annotation: 'always available',
|
||||
},
|
||||
configured: extendMode,
|
||||
selectable: true,
|
||||
},
|
||||
];
|
||||
|
||||
return this.prompt({
|
||||
extendMode,
|
||||
baseMessage,
|
||||
choices,
|
||||
initialSelected,
|
||||
});
|
||||
}
|
||||
@@ -481,7 +608,12 @@ export class InitCommand {
|
||||
projectPath: string,
|
||||
openspecDir: string,
|
||||
toolIds: string[]
|
||||
): Promise<void> {
|
||||
): Promise<RootStubStatus> {
|
||||
const rootStubStatus = await this.configureRootAgentsStub(
|
||||
projectPath,
|
||||
openspecDir
|
||||
);
|
||||
|
||||
for (const toolId of toolIds) {
|
||||
const configurator = ToolRegistry.get(toolId);
|
||||
if (configurator && configurator.isAvailable) {
|
||||
@@ -493,6 +625,25 @@ export class InitCommand {
|
||||
await slashConfigurator.generateAll(projectPath, openspecDir);
|
||||
}
|
||||
}
|
||||
|
||||
return rootStubStatus;
|
||||
}
|
||||
|
||||
private async configureRootAgentsStub(
|
||||
projectPath: string,
|
||||
openspecDir: string
|
||||
): Promise<RootStubStatus> {
|
||||
const configurator = ToolRegistry.get('agents');
|
||||
if (!configurator || !configurator.isAvailable) {
|
||||
return 'skipped';
|
||||
}
|
||||
|
||||
const stubPath = path.join(projectPath, configurator.configFileName);
|
||||
const existed = await FileSystemUtils.fileExists(stubPath);
|
||||
|
||||
await configurator.configure(projectPath, openspecDir);
|
||||
|
||||
return existed ? 'updated' : 'created';
|
||||
}
|
||||
|
||||
private displaySuccessMessage(
|
||||
@@ -501,7 +652,8 @@ export class InitCommand {
|
||||
refreshed: AIToolOption[],
|
||||
skippedExisting: AIToolOption[],
|
||||
skipped: AIToolOption[],
|
||||
extendMode: boolean
|
||||
extendMode: boolean,
|
||||
rootStubStatus: RootStubStatus
|
||||
): void {
|
||||
console.log(); // Empty line for spacing
|
||||
const successHeadline = extendMode
|
||||
@@ -512,6 +664,16 @@ export class InitCommand {
|
||||
console.log();
|
||||
console.log(PALETTE.lightGray('Tool summary:'));
|
||||
const summaryLines = [
|
||||
rootStubStatus === 'created'
|
||||
? `${PALETTE.white('▌')} ${PALETTE.white(
|
||||
'Root AGENTS.md stub created for other assistants'
|
||||
)}`
|
||||
: null,
|
||||
rootStubStatus === 'updated'
|
||||
? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray(
|
||||
'Root AGENTS.md stub refreshed for other assistants'
|
||||
)}`
|
||||
: null,
|
||||
created.length
|
||||
? `${PALETTE.white('▌')} ${PALETTE.white(
|
||||
'Created:'
|
||||
@@ -593,7 +755,8 @@ export class InitCommand {
|
||||
.map((tool) => tool.successLabel ?? tool.name)
|
||||
.filter((name): name is string => Boolean(name));
|
||||
|
||||
if (names.length === 0) return PALETTE.lightGray('your AI assistant');
|
||||
if (names.length === 0)
|
||||
return PALETTE.lightGray('your AGENTS.md-compatible assistant');
|
||||
if (names.length === 1) return PALETTE.white(names[0]);
|
||||
|
||||
const base = names.slice(0, -1).map((name) => PALETTE.white(name));
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export const PALETTE = {
|
||||
white: chalk.hex('#f4f4f4'),
|
||||
lightGray: chalk.hex('#c8c8c8'),
|
||||
midGray: chalk.hex('#8a8a8a'),
|
||||
darkGray: chalk.hex('#4a4a4a')
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export const agentsRootStubTemplate = `# OpenSpec Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Always open \`@/openspec/AGENTS.md\` when the request:
|
||||
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||
- Sounds ambiguous and you need the authoritative spec before coding
|
||||
|
||||
Use \`@/openspec/AGENTS.md\` to learn:
|
||||
- How to create and apply change proposals
|
||||
- Spec format and conventions
|
||||
- Project structure and guidelines
|
||||
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
`;
|
||||
@@ -1 +1 @@
|
||||
export { agentsTemplate as claudeTemplate } from './agents-template.js';
|
||||
export { agentsRootStubTemplate as claudeTemplate } from './agents-root-stub.js';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { agentsTemplate } from './agents-template.js';
|
||||
import { projectTemplate, ProjectContext } from './project-template.js';
|
||||
import { claudeTemplate } from './claude-template.js';
|
||||
import { agentsRootStubTemplate } from './agents-root-stub.js';
|
||||
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
|
||||
|
||||
export interface Template {
|
||||
@@ -27,7 +28,7 @@ export class TemplateManager {
|
||||
}
|
||||
|
||||
static getAgentsStandardTemplate(): string {
|
||||
return agentsTemplate;
|
||||
return agentsRootStubTemplate;
|
||||
}
|
||||
|
||||
static getSlashCommandBody(id: SlashCommandId): string {
|
||||
|
||||
@@ -3,7 +3,7 @@ export type SlashCommandId = 'proposal' | 'apply' | 'archive';
|
||||
const baseGuardrails = `**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to \`openspec/AGENTS.md\` if you need additional OpenSpec conventions or clarifications.`;
|
||||
- Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`;
|
||||
|
||||
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
|
||||
|
||||
@@ -12,7 +12,7 @@ const proposalSteps = `**Steps**
|
||||
2. Choose a unique verb-led \`change-id\` and scaffold \`proposal.md\`, \`tasks.md\`, and \`design.md\` (when needed) under \`openspec/changes/<id>/\`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in \`design.md\` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in \`changes/<id>/specs/\` using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
|
||||
5. Draft spec deltas in \`changes/<id>/specs/<capability>/spec.md\` (one folder per capability) using \`## ADDED|MODIFIED|REMOVED Requirements\` with at least one \`#### Scenario:\` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft \`tasks.md\` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with \`openspec validate <id> --strict\` and resolve every issue before sharing the proposal.`;
|
||||
|
||||
|
||||
+76
-49
@@ -1,10 +1,9 @@
|
||||
import path from 'path';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { OPENSPEC_DIR_NAME, OPENSPEC_MARKERS } from './config.js';
|
||||
import { agentsTemplate } from './templates/agents-template.js';
|
||||
import { TemplateManager } from './templates/index.js';
|
||||
import { OPENSPEC_DIR_NAME } from './config.js';
|
||||
import { ToolRegistry } from './configurators/registry.js';
|
||||
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
||||
import { agentsTemplate } from './templates/agents-template.js';
|
||||
|
||||
export class UpdateCommand {
|
||||
async execute(projectPath: string): Promise<void> {
|
||||
@@ -19,41 +18,51 @@ export class UpdateCommand {
|
||||
|
||||
// 2. Update AGENTS.md (full replacement)
|
||||
const agentsPath = path.join(openspecPath, 'AGENTS.md');
|
||||
const rootAgentsPath = path.join(resolvedProjectPath, 'AGENTS.md');
|
||||
const rootAgentsExisted = await FileSystemUtils.fileExists(rootAgentsPath);
|
||||
|
||||
await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
|
||||
const agentsStandardContent = TemplateManager.getAgentsStandardTemplate();
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
rootAgentsPath,
|
||||
agentsStandardContent,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
|
||||
// 3. Update existing AI tool configuration files only
|
||||
const configurators = ToolRegistry.getAll();
|
||||
const slashConfigurators = SlashCommandRegistry.getAll();
|
||||
let updatedFiles: string[] = [];
|
||||
let failedFiles: string[] = [];
|
||||
let updatedSlashFiles: string[] = [];
|
||||
let failedSlashTools: string[] = [];
|
||||
|
||||
const updatedFiles: string[] = [];
|
||||
const createdFiles: string[] = [];
|
||||
const failedFiles: string[] = [];
|
||||
const updatedSlashFiles: string[] = [];
|
||||
const failedSlashTools: string[] = [];
|
||||
|
||||
for (const configurator of configurators) {
|
||||
const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
|
||||
|
||||
// Only update if the file already exists
|
||||
if (await FileSystemUtils.fileExists(configFilePath)) {
|
||||
try {
|
||||
if (!await FileSystemUtils.canWriteFile(configFilePath)) {
|
||||
throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
|
||||
}
|
||||
await configurator.configure(resolvedProjectPath, openspecPath);
|
||||
updatedFiles.push(configurator.configFileName);
|
||||
} catch (error) {
|
||||
failedFiles.push(configurator.configFileName);
|
||||
console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
const configFilePath = path.join(
|
||||
resolvedProjectPath,
|
||||
configurator.configFileName
|
||||
);
|
||||
const fileExists = await FileSystemUtils.fileExists(configFilePath);
|
||||
const shouldConfigure =
|
||||
fileExists || configurator.configFileName === 'AGENTS.md';
|
||||
|
||||
if (!shouldConfigure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (fileExists && !await FileSystemUtils.canWriteFile(configFilePath)) {
|
||||
throw new Error(
|
||||
`Insufficient permissions to modify ${configurator.configFileName}`
|
||||
);
|
||||
}
|
||||
|
||||
await configurator.configure(resolvedProjectPath, openspecPath);
|
||||
updatedFiles.push(configurator.configFileName);
|
||||
|
||||
if (!fileExists) {
|
||||
createdFiles.push(configurator.configFileName);
|
||||
}
|
||||
} catch (error) {
|
||||
failedFiles.push(configurator.configFileName);
|
||||
console.error(
|
||||
`Failed to update ${configurator.configFileName}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,38 +72,56 @@ export class UpdateCommand {
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
|
||||
updatedSlashFiles = updatedSlashFiles.concat(updated);
|
||||
const updated = await slashConfigurator.updateExisting(
|
||||
resolvedProjectPath,
|
||||
openspecPath
|
||||
);
|
||||
updatedSlashFiles.push(...updated);
|
||||
} catch (error) {
|
||||
failedSlashTools.push(slashConfigurator.toolId);
|
||||
console.error(
|
||||
`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`
|
||||
`Failed to update slash commands for ${slashConfigurator.toolId}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Success message (ASCII-safe)
|
||||
const instructionUpdates = ['openspec/AGENTS.md'];
|
||||
instructionUpdates.push(`AGENTS.md${rootAgentsExisted ? '' : ' (created)'}`);
|
||||
const summaryParts: string[] = [];
|
||||
const instructionFiles: string[] = ['openspec/AGENTS.md'];
|
||||
|
||||
const messages: string[] = [`Updated OpenSpec instructions (${instructionUpdates.join(', ')})`];
|
||||
|
||||
if (updatedFiles.length > 0) {
|
||||
messages.push(`Updated AI tool files: ${updatedFiles.join(', ')}`);
|
||||
if (updatedFiles.includes('AGENTS.md')) {
|
||||
instructionFiles.push(
|
||||
createdFiles.includes('AGENTS.md') ? 'AGENTS.md (created)' : 'AGENTS.md'
|
||||
);
|
||||
}
|
||||
|
||||
summaryParts.push(
|
||||
`Updated OpenSpec instructions (${instructionFiles.join(', ')})`
|
||||
);
|
||||
|
||||
const aiToolFiles = updatedFiles.filter((file) => file !== 'AGENTS.md');
|
||||
if (aiToolFiles.length > 0) {
|
||||
summaryParts.push(`Updated AI tool files: ${aiToolFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (updatedSlashFiles.length > 0) {
|
||||
messages.push(`Updated slash commands: ${updatedSlashFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
if (failedFiles.length > 0) {
|
||||
messages.push(`Failed to update: ${failedFiles.join(', ')}`);
|
||||
summaryParts.push(
|
||||
`Updated slash commands: ${updatedSlashFiles.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (failedSlashTools.length > 0) {
|
||||
messages.push(`Failed slash command updates: ${failedSlashTools.join(', ')}`);
|
||||
const failedItems = [
|
||||
...failedFiles,
|
||||
...failedSlashTools.map(
|
||||
(toolId) => `slash command refresh (${toolId})`
|
||||
),
|
||||
];
|
||||
|
||||
if (failedItems.length > 0) {
|
||||
summaryParts.push(`Failed to update: ${failedItems.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(messages.join('\n'));
|
||||
|
||||
console.log(summaryParts.join(' | '));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ export const VALIDATION_MESSAGES = {
|
||||
|
||||
// Guidance snippets (appended to primary messages for remediation)
|
||||
GUIDE_NO_DELTAS:
|
||||
'No deltas found. Ensure your change has a specs/ directory with .md files using delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
||||
'No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
||||
GUIDE_MISSING_SPEC_SECTIONS:
|
||||
'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
|
||||
GUIDE_MISSING_CHANGE_SECTIONS:
|
||||
'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
|
||||
GUIDE_SCENARIO_FORMAT:
|
||||
'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
|
||||
} as const;
|
||||
} as const;
|
||||
|
||||
@@ -1,6 +1,46 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function isMarkerOnOwnLine(content: string, markerIndex: number, markerLength: number): boolean {
|
||||
let leftIndex = markerIndex - 1;
|
||||
while (leftIndex >= 0 && content[leftIndex] !== '\n') {
|
||||
const char = content[leftIndex];
|
||||
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
||||
return false;
|
||||
}
|
||||
leftIndex--;
|
||||
}
|
||||
|
||||
let rightIndex = markerIndex + markerLength;
|
||||
while (rightIndex < content.length && content[rightIndex] !== '\n') {
|
||||
const char = content[rightIndex];
|
||||
if (char !== ' ' && char !== '\t' && char !== '\r') {
|
||||
return false;
|
||||
}
|
||||
rightIndex++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function findMarkerIndex(
|
||||
content: string,
|
||||
marker: string,
|
||||
fromIndex = 0
|
||||
): number {
|
||||
let currentIndex = content.indexOf(marker, fromIndex);
|
||||
|
||||
while (currentIndex !== -1) {
|
||||
if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
currentIndex = content.indexOf(marker, currentIndex + marker.length);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export class FileSystemUtils {
|
||||
static async createDirectory(dirPath: string): Promise<void> {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
@@ -70,10 +110,18 @@ export class FileSystemUtils {
|
||||
if (await this.fileExists(filePath)) {
|
||||
existingContent = await this.readFile(filePath);
|
||||
|
||||
const startIndex = existingContent.indexOf(startMarker);
|
||||
const endIndex = existingContent.indexOf(endMarker);
|
||||
|
||||
const startIndex = findMarkerIndex(existingContent, startMarker);
|
||||
const endIndex = startIndex !== -1
|
||||
? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
|
||||
: findMarkerIndex(existingContent, endMarker);
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1) {
|
||||
if (endIndex < startIndex) {
|
||||
throw new Error(
|
||||
`Invalid marker state in ${filePath}. End marker appears before start marker.`
|
||||
);
|
||||
}
|
||||
|
||||
const before = existingContent.substring(0, startIndex);
|
||||
const after = existingContent.substring(endIndex + endMarker.length);
|
||||
existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
|
||||
@@ -109,4 +157,4 @@ export class FileSystemUtils {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-11
@@ -106,7 +106,8 @@ describe('InitCommand', () => {
|
||||
|
||||
const content = await fs.readFile(claudePath, 'utf-8');
|
||||
expect(content).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(content).toContain('OpenSpec Instructions');
|
||||
expect(content).toContain("@/openspec/AGENTS.md");
|
||||
expect(content).toContain('openspec update');
|
||||
expect(content).toContain('<!-- OPENSPEC:END -->');
|
||||
});
|
||||
|
||||
@@ -122,13 +123,14 @@ describe('InitCommand', () => {
|
||||
|
||||
const updatedContent = await fs.readFile(claudePath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('OpenSpec Instructions');
|
||||
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
||||
expect(updatedContent).toContain('openspec update');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain('Custom instructions here');
|
||||
});
|
||||
|
||||
it('should create AGENTS.md in project root when AGENTS standard is selected', async () => {
|
||||
queueSelections('agents', DONE);
|
||||
it('should always create AGENTS.md in project root', async () => {
|
||||
queueSelections(DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
@@ -137,7 +139,8 @@ describe('InitCommand', () => {
|
||||
|
||||
const content = await fs.readFile(rootAgentsPath, 'utf-8');
|
||||
expect(content).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(content).toContain('OpenSpec Instructions');
|
||||
expect(content).toContain("@/openspec/AGENTS.md");
|
||||
expect(content).toContain('openspec update');
|
||||
expect(content).toContain('<!-- OPENSPEC:END -->');
|
||||
|
||||
const claudeExists = await fileExists(path.join(testDir, 'CLAUDE.md'));
|
||||
@@ -262,6 +265,42 @@ describe('InitCommand', () => {
|
||||
expect(archiveContent).toContain('openspec list --specs');
|
||||
});
|
||||
|
||||
it('should create Kilo Code workflows with templates', async () => {
|
||||
queueSelections('kilocode', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-proposal.md'
|
||||
);
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-apply.md'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(proposalPath)).toBe(true);
|
||||
expect(await fileExists(applyPath)).toBe(true);
|
||||
expect(await fileExists(archivePath)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
expect(proposalContent).not.toContain('---\n');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
expect(applyContent).not.toContain('---\n');
|
||||
|
||||
const archiveContent = await fs.readFile(archivePath, 'utf-8');
|
||||
expect(archiveContent).toContain('openspec list --specs');
|
||||
expect(archiveContent).not.toContain('---\n');
|
||||
});
|
||||
|
||||
it('should add new tool when OpenSpec already exists', async () => {
|
||||
queueSelections('claude', DONE, 'cursor', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
@@ -274,12 +313,10 @@ describe('InitCommand', () => {
|
||||
expect(await fileExists(cursorProposal)).toBe(true);
|
||||
});
|
||||
|
||||
it('should error when extend mode selects no tools', async () => {
|
||||
it('should allow extend mode with no additional native tools', async () => {
|
||||
queueSelections('claude', DONE, DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await expect(initCommand.execute(testDir)).rejects.toThrow(
|
||||
/OpenSpec seems to already be initialized/
|
||||
);
|
||||
await expect(initCommand.execute(testDir)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle non-existent target directory', async () => {
|
||||
@@ -303,7 +340,7 @@ describe('InitCommand', () => {
|
||||
});
|
||||
|
||||
it('should reference AGENTS compatible assistants in success message', async () => {
|
||||
queueSelections('agents', DONE);
|
||||
queueSelections(DONE);
|
||||
const logSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
@@ -323,7 +360,9 @@ describe('InitCommand', () => {
|
||||
|
||||
expect(mockPrompt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseMessage: expect.stringContaining('Which AI tools do you use?'),
|
||||
baseMessage: expect.stringContaining(
|
||||
'Which natively supported AI tools do you use?'
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -350,6 +389,16 @@ describe('InitCommand', () => {
|
||||
);
|
||||
expect(claudeChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should preselect Kilo Code when workflows already exist', async () => {
|
||||
queueSelections('kilocode', DONE, 'kilocode', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const preselected = secondRunArgs.initialSelected ?? [];
|
||||
expect(preselected).toContain('kilocode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
||||
@@ -51,7 +51,8 @@ More content after.`;
|
||||
const updatedContent = await fs.readFile(claudePath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).toContain('OpenSpec Instructions');
|
||||
expect(updatedContent).toContain("@/openspec/AGENTS.md");
|
||||
expect(updatedContent).toContain('openspec update');
|
||||
expect(updatedContent).toContain('Some existing content here');
|
||||
expect(updatedContent).toContain('More content after');
|
||||
|
||||
@@ -191,6 +192,34 @@ Old body
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Kilo Code workflows', async () => {
|
||||
const kilocodePath = path.join(
|
||||
testDir,
|
||||
'.kilocode/workflows/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(kilocodePath), { recursive: true });
|
||||
const initialContent = `<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(kilocodePath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(kilocodePath, 'utf-8');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated.startsWith('<!-- OPENSPEC:START -->')).toBe(true);
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .kilocode/workflows/openspec-apply.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle no AI tool files present', async () => {
|
||||
// Execute update command with no AI tool files
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
@@ -303,7 +332,8 @@ Old content
|
||||
|
||||
const content = await fs.readFile(rootAgentsPath, 'utf-8');
|
||||
expect(content).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(content).toContain('OpenSpec Instructions');
|
||||
expect(content).toContain("@/openspec/AGENTS.md");
|
||||
expect(content).toContain('openspec update');
|
||||
expect(content).toContain('<!-- OPENSPEC:END -->');
|
||||
});
|
||||
|
||||
@@ -319,7 +349,8 @@ Old content
|
||||
const updated = await fs.readFile(rootAgentsPath, 'utf-8');
|
||||
expect(updated).toContain('# Custom intro');
|
||||
expect(updated).toContain('# Footnotes');
|
||||
expect(updated).toContain('OpenSpec Instructions');
|
||||
expect(updated).toContain("@/openspec/AGENTS.md");
|
||||
expect(updated).toContain('openspec update');
|
||||
expect(updated).not.toContain('Old content');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
|
||||
@@ -248,5 +248,40 @@ Line 5 with gap`;
|
||||
const result = await fs.readFile(filePath, 'utf-8');
|
||||
expect(result).toContain(content);
|
||||
});
|
||||
|
||||
it('should ignore inline mentions of markers when updating content', async () => {
|
||||
const filePath = path.join(testDir, 'inline-mentions.md');
|
||||
const existingFile = `Intro referencing markers like ${START_MARKER} and ${END_MARKER} inside text.
|
||||
|
||||
${START_MARKER}
|
||||
Original content
|
||||
${END_MARKER}
|
||||
`;
|
||||
|
||||
await fs.writeFile(filePath, existingFile);
|
||||
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
filePath,
|
||||
'Updated content',
|
||||
START_MARKER,
|
||||
END_MARKER
|
||||
);
|
||||
|
||||
const firstResult = await fs.readFile(filePath, 'utf-8');
|
||||
expect(firstResult).toContain('Intro referencing markers like');
|
||||
expect(firstResult).toContain('Updated content');
|
||||
expect(firstResult.match(new RegExp(START_MARKER, 'g'))?.length).toBe(2);
|
||||
expect(firstResult.match(new RegExp(END_MARKER, 'g'))?.length).toBe(2);
|
||||
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
filePath,
|
||||
'Updated content',
|
||||
START_MARKER,
|
||||
END_MARKER
|
||||
);
|
||||
|
||||
const secondResult = await fs.readFile(filePath, 'utf-8');
|
||||
expect(secondResult).toBe(firstResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user