Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale d9d3709fad docs(changes): plan interactive proposal qa flow 2025-09-30 11:47:37 +10:00
Tabish Bidiwale a908dc5a05 chore(release): version packages (#93)
Bump version to 0.5.0 with new features and improvements:
- E2E testing with cross-platform CI matrix
- Improved apply instructions
- Documentation improvements and cleanup
2025-09-29 23:47:22 +10:00
Tabish Bidiwale b46f99b9bc Make apply instructions more specific (#92) 2025-09-29 23:41:24 +10:00
Tabish Bidiwale 6f7cc2abd2 archive completed changes (#91) 2025-09-29 23:03:04 +10:00
Tabish Bidiwale 4867bfade5 feat: implement Phase 1 E2E testing with cross-platform CI matrix (#80)
* feat: implement Phase 1 E2E testing with cross-platform CI matrix

- Add shared runCLI helper in test/helpers/run-cli.ts for spawn testing
- Create test/cli-e2e/basic.test.ts covering help, version, validate flows
- Migrate existing CLI exec tests to use runCLI helper
- Extend CI matrix to bash (Linux/macOS) and pwsh (Windows)
- Update Phase 1 tasks and proposal with implementation status

* fix: correct YAML syntax in CI workflow diagnostics command

* fix: use multiline YAML for diagnostics command

* fix ci

* fix: ci

* fix: update core validation and json converter

* chore(ci): split pr and main workflows

* refactor: simplify CI workflow with unified matrix strategy

- Consolidate test_pr and test_matrix into single test job
- Add proper shell configuration with defaults
- Add timeout protection (15 minutes)
- Simplify required-checks to single job
- Maintain cross-platform testing (bash on Linux/macOS, pwsh on Windows)

* fix: restore lean PR workflow with async main branch matrix

- PRs run only essential tests on ubuntu-latest (fast feedback)
- Main branch runs full cross-platform matrix asynchronously
- Separate required-checks for each workflow type
- Different timeouts: 10min for PR, 15min for matrix
2025-09-29 22:20:30 +10:00
63 changed files with 720 additions and 311 deletions
+89 -8
View File
@@ -15,10 +15,12 @@ concurrency:
cancel-in-progress: true
jobs:
test:
test_pr:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -48,7 +50,68 @@ jobs:
- name: Upload test coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
name: coverage-report-pr
path: coverage/
retention-days: 7
test_matrix:
name: Test (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: github.event_name != 'pull_request'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
shell: bash
label: linux-bash
- os: macos-latest
shell: bash
label: macos-bash
- os: windows-latest
shell: pwsh
label: windows-pwsh
defaults:
run:
shell: ${{ matrix.shell }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Print environment diagnostics
run: |
node -p "JSON.stringify({ platform: process.platform, arch: process.arch, shell: process.env.SHELL || process.env.ComSpec || '' })"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm run build
- name: Run tests
run: pnpm test
- name: Upload test coverage
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: coverage-report-main
path: coverage/
retention-days: 7
@@ -122,15 +185,15 @@ jobs:
echo "Changesets not configured, skipping validation"
fi
required-checks:
required-checks-pr:
name: All checks passed
runs-on: ubuntu-latest
needs: [test, lint]
if: always()
needs: [test_pr, lint]
if: always() && github.event_name == 'pull_request'
steps:
- name: Verify all checks passed
run: |
if [[ "${{ needs.test.result }}" != "success" ]]; then
if [[ "${{ needs.test_pr.result }}" != "success" ]]; then
echo "Test job failed"
exit 1
fi
@@ -138,4 +201,22 @@ jobs:
echo "Lint job failed"
exit 1
fi
echo "All required checks passed!"
echo "All required checks passed!"
required-checks-main:
name: All checks passed
runs-on: ubuntu-latest
needs: [test_matrix, lint]
if: always() && github.event_name != 'pull_request'
steps:
- name: Verify all checks passed
run: |
if [[ "${{ needs.test_matrix.result }}" != "success" ]]; then
echo "Matrix test job failed"
exit 1
fi
if [[ "${{ needs.lint.result }}" != "success" ]]; then
echo "Lint job failed"
exit 1
fi
echo "All required checks passed!"
+24
View File
@@ -1,5 +1,29 @@
# @fission-ai/openspec
## 0.5.0
### Minor Changes
- feat: implement Phase 1 E2E testing with cross-platform CI matrix
- Add shared runCLI helper in test/helpers/run-cli.ts for spawn testing
- Create test/cli-e2e/basic.test.ts covering help, version, validate flows
- Migrate existing CLI exec tests to use runCLI helper
- Extend CI matrix to bash (Linux/macOS) and pwsh (Windows)
- Split PR and main workflows for optimized feedback
### Patch Changes
- Make apply instructions more specific
Improve agent templates and slash command templates with more specific and actionable apply instructions.
- docs: improve documentation and cleanup
- Document non-interactive flag for archive command
- Replace discord badge in README
- Archive completed changes for better organization
## 0.4.0
### Minor Changes
+12 -4
View File
@@ -1,7 +1,15 @@
#!/usr/bin/env node
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import { existsSync, rmSync } from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const runTsc = (args = []) => {
const tscPath = require.resolve('typescript/bin/tsc');
execFileSync(process.execPath, [tscPath, ...args], { stdio: 'inherit' });
};
console.log('🔨 Building OpenSpec...\n');
@@ -14,10 +22,10 @@ if (existsSync('dist')) {
// Run TypeScript compiler (use local version explicitly)
console.log('Compiling TypeScript...');
try {
execSync('./node_modules/.bin/tsc -v', { stdio: 'inherit' });
execSync('./node_modules/.bin/tsc', { stdio: 'inherit' });
runTsc(['--version']);
runTsc();
console.log('\n✅ Build completed successfully!');
} catch (error) {
console.error('\n❌ Build failed!');
process.exit(1);
}
}
+4 -2
View File
@@ -47,12 +47,14 @@ Skip proposal for:
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
### Stage 2: Implementing Changes
Track these steps as TODOs and complete them one by one.
1. **Read proposal.md** - Understand what's being built
2. **Read design.md** (if exists) - Review technical decisions
3. **Read tasks.md** - Get implementation checklist
4. **Implement tasks sequentially** - Complete in order
5. **Mark complete immediately** - Update `- [x]` after each task
6. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
### Stage 3: Archiving Changes
After deployment, create separate PR to:
@@ -0,0 +1,13 @@
## 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.
@@ -0,0 +1,63 @@
# 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.
@@ -0,0 +1,21 @@
## 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`.
@@ -0,0 +1,18 @@
## 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.
@@ -0,0 +1,19 @@
## 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,19 @@
## Why
Recent cross-shell regressions for `openspec` commands revealed that our existing unit/integration tests do not exercise the packaged CLI or shell-specific behavior. The prior attempt at Vitest spawn tests stalled because it coupled e2e coverage with `pnpm pack` installs, which fail in network-restricted environments. With those findings incorporated, we now need an approved plan to realign the work.
## What Changes
- Adopt a phased strategy that first stabilizes direct spawn testing of the built CLI (`node dist/cli/index.js`) using lightweight fixtures and a shared `runCLI` helper.
- Expand coverage once the spawn harness is stable, keeping the initial matrix focused on bash jobs for Linux/macOS and `pwsh` on Windows while exercising both the direct `node dist/cli/index.js` invocation and the bin shim with non-TTY defaults and captured diagnostics.
- Treat packaging/install validation as an optional CI safeguard: when a runner has registry access, run a simple pnpm-based pack→install→smoke-test flow; otherwise document it as out of scope while closing remaining hardening items.
- Close out the remaining cross-shell hardening items: ensure `.gitattributes` covers packaged assets, enforce executable bits for CLI shims during CI, and finish the pending SIGINT handling improvements.
## Impact
- Tests: add `test/cli-e2e` spawn suite, create the shared `runCLI` helper, and adjust `vitest.setup.ts` as needed.
- Tooling: update GitHub Actions workflows with the lightweight matrix above and (optionally) a packaging install check where network is available.
- Docs: note phase progress and any limitations inline in this proposal (or the relevant spec) so future phases have clear context.
### Phase 1 Status
- Shared `test/helpers/run-cli.ts` guarantees the CLI bundle exists before spawning and enforces non-TTY defaults for every invocation.
- New `test/cli-e2e/basic.test.ts` covers `--help`, `--version`, a successful `validate --all --json`, and an unknown-item error path against the `tmp-init` fixture copy.
- Legacy top-level `validate` exec tests now rely on `runCLI`, avoiding manual `execSync` usage while keeping their fixture authoring intact.
- CI matrix groundwork is in place (bash on Linux/macOS, pwsh on Windows) so the spawn suite runs the same way the helper does across supported shells.
@@ -0,0 +1,9 @@
## 1. Phase 1 – Stabilize Local Spawn Coverage
- [x] 1.1 Add `test/helpers/run-cli.ts` that ensures the build runs once and executes `node dist/cli/index.js` with non-TTY defaults; update `vitest.setup.ts` to reuse the shared build step.
- [x] 1.2 Seed `test/cli-e2e` using the minimal fixture set (`tmp-init` or copy) to cover help/version, a happy-path `validate`, and a representative error flow via the new helper.
- [x] 1.3 Migrate the highest-value existing CLI exec tests (e.g., validate) onto `runCLI` and summarize Phase 1 coverage in this proposal for the next phase.
## 2. Phase 2 – Expand Cross-Shell Validation
- [x] 2.1 Exercise both entry points (`node dist/cli/index.js`, `bin/openspec.js`) in the spawn suite and add diagnostics for shell/OS context.
- [x] 2.2 Extend GitHub Actions to run the spawn suite on bash jobs for Linux/macOS and a `pwsh` job on Windows; capture shell/OS diagnostics and note follow-ups for additional shells.
@@ -21,5 +21,5 @@
## 5. Optional (Not Needed Now)
- [x] 5.1 Add optional root param to discovery helpers (default process.cwd())
- [ ] 5.2 Consider threading root through command constructors if ever required
@@ -0,0 +1,12 @@
## 1. Planning & Spec Updates
- [x] 1.1 Confirm overlap with `add-multi-agent-init` and coordinate extend-mode flow
- [x] 1.2 Update `openspec/specs/cli-init/spec.md` to capture multi-select onboarding requirements
## 2. Implementation
- [x] 2.1 Add multi-select support to the `openspec init` prompt, including indicators for existing tool configs
- [x] 2.2 Enhance success messaging to summarize created/refreshed assets per tool
- [x] 2.3 Ensure shared instruction template is applied consistently (CLAUDE.md, AGENTS.md, slash commands)
## 3. Quality
- [x] 3.1 Expand unit tests for init/update flows covering multi-select and summaries
- [x] 3.2 Perform `openspec init` smoke test in a temp directory (document output)
@@ -26,10 +26,6 @@
- Archive documentation
- Change proposals
## 6. Add Deprecation Notice (Optional Phase)
- [ ] Consider adding a deprecation warning before full removal
- [ ] Provide helpful message directing users to `openspec show` command
## 7. Testing
- [x] Ensure all tests pass after removal
- [x] Verify CLI help text no longer shows diff command
@@ -25,12 +25,16 @@ The command SHALL generate required template files with appropriate content for
### Requirement: AI Tool Configuration Details
The command SHALL properly configure selected AI tools with OpenSpec-specific instructions using a marker system.
#### Scenario: Creating new CLAUDE.md
- **WHEN** CLAUDE.md does not exist
- **THEN** create new file with OpenSpec content wrapped in markers including reference to `@openspec/AGENTS.md`
### Requirement: Success Output
The command SHALL provide clear, actionable next steps upon successful initialization.
#### Scenario: Displaying success message
- **WHEN** initialization completes successfully
- **THEN** include prompt: "Please explain the OpenSpec workflow from openspec/AGENTS.md and how I should work with you on this project"
@@ -1,12 +0,0 @@
## Why
Recent cross-shell regressions for `openspec` commands revealed that our existing unit/integration tests do not exercise the packaged CLI or shell-specific behavior. The prior attempt at Vitest spawn tests stalled because it coupled e2e coverage with `pnpm pack` installs, which fail in network-restricted environments. With those findings incorporated, we now need an approved plan to realign the work.
## What Changes
- Adopt a phased strategy that first stabilizes direct spawn testing of the built CLI (`node dist/cli/index.js`) using lightweight fixtures and shared helpers.
- Expand coverage to cross-shell/OS matrices once the spawn harness is stable, ensuring both the direct `node dist/cli/index.js` invocation and the bin shim are exercised with non-TTY defaults and captured diagnostics.
- Treat packaging/install validation as an optional CI safeguard: when a runner has registry access, run a simple pnpm-based pack→install→smoke-test flow; otherwise document it as out of scope while closing remaining hardening items.
## Impact
- Tests: add `test/cli-e2e` spawn suite, helpers, and fixture usage updates; adjust `vitest.setup.ts` as needed.
- Tooling: update GitHub Actions workflows to add shell/OS matrices and (optionally) a packaging install check where network is available.
- Docs: keep `CROSS-SHELL-PLAN.md` aligned with the phased rollout and record any limitations called out during execution.
@@ -1,13 +0,0 @@
## 1. Phase 1 – Stabilize Local Spawn Coverage
- [ ] 1.1 Update `vitest.setup.ts` and helpers so the CLI build runs once and `runCLI` executes `node dist/cli.js` with non-TTY defaults.
- [ ] 1.2 Reuse the minimal fixture set (`tmp-init` or copy) to seed initial spawn tests for help/version, a happy-path `validate`, and a representative error flow.
- [ ] 1.3 Document the Phase 1 coverage details in `CROSS-SHELL-PLAN.md`, noting any outstanding gaps.
## 2. Phase 2 – Expand Cross-Shell Validation
- [ ] 2.1 Exercise both entry points (`node dist/cli.js`, `bin/openspec.js`) in the spawn suite and add diagnostics for shell/OS context.
- [ ] 2.2 Extend GitHub Actions to run the spawn suite across a matrix of shells (bash, zsh, fish, pwsh, cmd) on macOS, Linux, and Windows runners.
## 3. Phase 3 – Package Validation (Optional)
- [ ] 3.1 Add a simple CI job on runners with registry access that runs `pnpm pack`, installs the tarball into a temp workspace (e.g., `pnpm add --no-save`), and executes `pnpm exec openspec --version`.
- [ ] 3.2 If network-restricted environments can’t exercise installs, document the limitation in `CROSS-SHELL-PLAN.md` and skip the job there.
- [ ] 3.3 Close out remaining hardening items from the original cross-shell plan (e.g., `.gitattributes`, chmod enforcement, SIGINT follow-ups) and update the plan accordingly.
@@ -1,12 +0,0 @@
## 1. Planning & Spec Updates
- [ ] 1.1 Confirm overlap with `add-multi-agent-init` and coordinate extend-mode flow
- [ ] 1.2 Update `openspec/specs/cli-init/spec.md` to capture multi-select onboarding requirements
## 2. Implementation
- [ ] 2.1 Add multi-select support to the `openspec init` prompt, including indicators for existing tool configs
- [ ] 2.2 Enhance success messaging to summarize created/refreshed assets per tool
- [ ] 2.3 Ensure shared instruction template is applied consistently (CLAUDE.md, AGENTS.md, slash commands)
## 3. Quality
- [ ] 3.1 Expand unit tests for init/update flows covering multi-select and summaries
- [ ] 3.2 Perform `openspec init` smoke test in a temp directory (document output)
+66 -91
View File
@@ -3,9 +3,7 @@
## Purpose
The `openspec init` command SHALL create a complete OpenSpec directory structure in any project, enabling immediate adoption of OpenSpec conventions with support for multiple AI coding assistants.
## Requirements
### Requirement: Progress Indicators
The command SHALL display progress indicators during initialization to provide clear feedback about each step.
@@ -21,11 +19,9 @@ The command SHALL display progress indicators during initialization to provide c
- Then success: "✔ AI tools configured"
### Requirement: Directory Creation
The command SHALL create the complete OpenSpec directory structure with all required directories and files.
#### Scenario: Creating OpenSpec structure
- **WHEN** `openspec init` is executed
- **THEN** create the following directory structure:
```
@@ -38,11 +34,9 @@ openspec/
```
### Requirement: File Generation
The command SHALL generate required template files with appropriate content for immediate use.
#### Scenario: Generating template files
- **WHEN** initializing OpenSpec
- **THEN** generate `AGENTS.md` containing complete OpenSpec instructions for AI assistants
- **AND** generate `project.md` with project context template
@@ -54,10 +48,14 @@ The command SHALL configure AI coding assistants with OpenSpec instructions base
#### Scenario: Prompting for AI tool selection
- **WHEN** run interactively
- **THEN** prompt user to select AI tools to configure:
- Claude Code (updates/creates CLAUDE.md with OpenSpec markers)
- Cursor (future)
- Aider (future)
- **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 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: AI Tool Configuration Details
@@ -74,105 +72,36 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
- **THEN** create new file with OpenSpec content wrapped in markers:
```markdown
<!-- OPENSPEC:START -->
# OpenSpec Project
# OpenSpec Instructions
This document provides instructions for AI coding assistants on how to use OpenSpec conventions for spec-driven development. Follow these rules precisely when working on OpenSpec-enabled projects.
This project uses OpenSpec for spec-driven development. Specifications are the source of truth.
See @openspec/AGENTS.md for detailed conventions and guidelines.
<!-- OPENSPEC:END -->
```
#### Scenario: Updating existing CLAUDE.md
- **WHEN** CLAUDE.md already exists
- **THEN** preserve all existing content
- **AND** insert OpenSpec content at the beginning of the file using markers
- **AND** ensure markers don't duplicate if they already exist
#### Scenario: Managing content with markers
- **WHEN** using the marker system
- **THEN** use `<!-- OPENSPEC:START -->` to mark the beginning of managed content
- **AND** use `<!-- OPENSPEC:END -->` to mark the end of managed content
- **AND** allow OpenSpec to update its content without affecting user customizations
- **AND** preserve all content outside the markers intact
WHY use markers:
- Users may have existing CLAUDE.md instructions they want to keep
- OpenSpec can update its instructions in future versions
- Clear boundary between OpenSpec-managed and user-managed content
Instructions for AI coding assistants using OpenSpec for spec-driven development.
### Requirement: Interactive Mode
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
#### Scenario: Displaying interactive menu
- **WHEN** run
- **THEN** prompt user with: "Which AI tool do you use?"
- **AND** show single-select menu with available tools:
- Claude Code
- **AND** show disabled options as "coming soon" (not selectable):
- Cursor (coming soon)
- Aider (coming soon)
- Continue (coming soon)
#### Scenario: Navigating the menu
- **WHEN** user is in the menu
- **THEN** allow arrow keys to move between options
- **AND** allow Enter key to select the highlighted option
- **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
- **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
### Requirement: Safety Checks
The command SHALL perform safety checks to prevent overwriting existing structures and ensure proper permissions.
#### Scenario: Detecting existing initialization
- **WHEN** `openspec/` directory already exists
- **THEN** display error with ora fail indicator:
- "✖ Error: OpenSpec seems to already be initialized. Use 'openspec update' to update the structure."
#### Scenario: Checking write permissions
- **WHEN** checking initialization feasibility
- **THEN** verify write permissions in the target directory silently
- **AND** only display error if permissions are insufficient
- **WHEN** the `openspec/` directory already exists
- **THEN** inform the user that OpenSpec is already initialized, skip recreating the base structure, and enter an extend mode
- **AND** continue to the AI tool selection step so additional tools can be configured
- **AND** display the existing-initialization error message only when the user declines to add any AI tools
### Requirement: Success Output
The command SHALL provide clear, actionable next steps upon successful initialization.
#### Scenario: Displaying success message
- **WHEN** initialization completes successfully
- **THEN** display actionable prompts for AI-driven workflow:
```
✔ OpenSpec initialized successfully!
Next steps - Copy these prompts to Claude:
────────────────────────────────────────────────────────────
1. Populate your project context:
"Please read openspec/project.md and help me fill it out
with details about my project, tech stack, and conventions"
2. Create your first change proposal:
"I want to add [YOUR FEATURE HERE]. Please create an
OpenSpec change proposal for this feature"
3. Learn the OpenSpec workflow:
"Please explain the OpenSpec workflow from openspec/AGENTS.md
and how I should work with you on this project"
────────────────────────────────────────────────────────────
```
The prompts SHALL:
- Be copy-pasteable for immediate use with AI tools
- Guide users through the AI-driven workflow
- Replace placeholder text ([YOUR FEATURE HERE]) with actual features
- **THEN** include prompt: "Please explain the OpenSpec workflow from openspec/AGENTS.md and how I should work with you on this project"
### Requirement: Exit Codes
@@ -187,6 +116,52 @@ The command SHALL use consistent exit codes to indicate different failure modes.
- 2: Insufficient permissions (reserved for future use)
- 3: User cancelled operation (reserved for future use)
### Requirement: Additional AI Tool Initialization
`openspec init` SHALL allow users to add configuration files for new AI coding assistants after the initial setup.
#### Scenario: Configuring an extra tool after initial setup
- **GIVEN** an `openspec/` directory already exists and at least one AI tool file is present
- **WHEN** the user runs `openspec init` and selects a different supported AI tool
- **THEN** generate that tool's configuration files with OpenSpec markers the same way as during first-time initialization
- **AND** leave existing tool configuration files unchanged except for managed sections that need refreshing
- **AND** exit with code 0 and display a success summary highlighting the newly added tool files
### Requirement: Success Output Enhancements
`openspec init` SHALL summarize tool actions when initialization or extend mode completes.
#### Scenario: Showing tool summary
- **WHEN** the command completes successfully
- **THEN** display a categorized summary of tools that were created, refreshed, or skipped (including already-configured skips)
- **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.
#### 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
### 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
## Why
Manual creation of OpenSpec structure is error-prone and creates adoption friction. A standardized init command ensures:
+32 -29
View File
@@ -5,22 +5,11 @@
As a developer using OpenSpec, I want to update the OpenSpec instructions in my project when new versions are released, so that I can benefit from improvements to AI agent instructions.
## Requirements
### Requirement: Update Behavior
The update command SHALL update OpenSpec instruction files to the latest templates in a team-friendly manner.
#### Scenario: Running update command
- **WHEN** a user runs `openspec update`
- **THEN** the command SHALL:
- Check if the `openspec` directory exists
- Replace `openspec/AGENTS.md` with the latest template (complete replacement)
- Update **only existing** AI tool configuration files (e.g., CLAUDE.md)
- Check each registered AI tool configurator
- For each configurator, check if its file exists
- Update only files that already exist using their markers
- Preserve user content outside markers
- **Never create new AI tool configuration files**
- Display success message listing updated files
- **THEN** replace `openspec/AGENTS.md` with the latest template
### Requirement: Prerequisites
@@ -34,39 +23,53 @@ The command SHALL require an existing OpenSpec structure before allowing updates
- **AND** exit with code 1
### Requirement: File Handling
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
### Requirement: Tool-Agnostic Updates
The update command SHALL handle file updates in a predictable and safe manner while respecting team tool choices.
#### Scenario: Updating files
- **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 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 unwanted files
### Requirement: Tool-Agnostic Updates
The update command SHALL update only existing AI tool configuration files and SHALL NOT create new ones.
#### Scenario: Updating existing tool files
- **WHEN** a user runs `openspec update`
- **THEN** update each AI tool configuration file that exists (e.g., CLAUDE.md, COPILOT.md)
- **AND** do not create missing tool configuration files
- **AND** preserve user content outside OpenSpec markers
- **AND** respect team members' AI tool choices by not creating additional tool files beyond the root `AGENTS.md`
### Requirement: Core Files Always Updated
The update command SHALL always update the core OpenSpec files and display an ASCII-safe success message.
#### Scenario: Successful update
- **WHEN** the update completes successfully
- **THEN** replace `openspec/AGENTS.md` with the latest template
- **AND** update existing AI tool configuration files within markers
- **AND** display the message: "Updated OpenSpec instructions"
### 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/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: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
## Edge Cases
+9
View File
@@ -199,3 +199,12 @@ The validate command SHALL handle ambiguous names and explicit type overrides to
- **THEN** the CLI SHALL not display interactive prompts
- **AND** SHALL print non-interactive hints or chosen outputs as appropriate
### Requirement: Parser SHALL handle cross-platform line endings
The markdown parser SHALL correctly identify sections regardless of line ending format (LF, CRLF, CR).
#### Scenario: Required sections parsed with CRLF line endings
- **GIVEN** a change proposal markdown saved with CRLF line endings
- **AND** the document contains `## Why` and `## What Changes`
- **WHEN** running `openspec validate <change-id>`
- **THEN** validation SHALL recognize the sections and NOT raise parsing errors
-14
View File
@@ -36,28 +36,14 @@ The dashboard SHALL display a summary section with key project metrics.
- **THEN** summary shows zero counts for all metrics
### Requirement: Active Changes Display
The dashboard SHALL show active changes with visual progress indicators.
#### Scenario: Active changes with progress bars
- **WHEN** there are in-progress changes with tasks
- **THEN** system displays each change with change name left-aligned
- **AND** visual progress bar using Unicode characters
- **AND** percentage completion on the right
#### Scenario: Active changes ordered by completion percentage
- **WHEN** multiple active changes are displayed with progress information
- **THEN** list them sorted by completion percentage ascending so 0% items appear first
- **AND** treat missing progress values as 0% for ordering
- **AND** break ties by change identifier in ascending alphabetical order to keep output deterministic
#### Scenario: No active changes
- **WHEN** all changes are completed or no changes exist
- **THEN** active changes section is omitted from display
### Requirement: Completed Changes Display
The dashboard SHALL list completed changes in a separate section.
@@ -14,11 +14,9 @@ OpenSpec conventions SHALL mandate a structured spec format with clear requireme
- **THEN** authors SHALL use `### Requirement: ...` followed by at least one `#### Scenario: ...` section
### Requirement: Project Structure
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
#### Scenario: Initializing project structure
- **WHEN** an OpenSpec project is initialized
- **THEN** it SHALL have this structure:
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.4.0",
"version": "0.5.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+6 -4
View File
@@ -43,7 +43,8 @@ export class JsonConverter {
}
private extractNameFromPath(filePath: string): string {
const parts = filePath.split('/');
const normalizedPath = filePath.replaceAll('\\', '/');
const parts = normalizedPath.split('/');
for (let i = parts.length - 1; i >= 0; i--) {
if (parts[i] === 'specs' || parts[i] === 'changes') {
@@ -53,7 +54,8 @@ export class JsonConverter {
}
}
const fileName = parts[parts.length - 1];
return fileName.replace('.md', '');
const fileName = parts[parts.length - 1] ?? '';
const dotIndex = fileName.lastIndexOf('.');
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
}
}
}
+4 -2
View File
@@ -47,12 +47,14 @@ Skip proposal for:
4. Run \`openspec validate <id> --strict\` and resolve any issues before sharing the proposal.
### Stage 2: Implementing Changes
Track these steps as TODOs and complete them one by one.
1. **Read proposal.md** - Understand what's being built
2. **Read design.md** (if exists) - Review technical decisions
3. **Read tasks.md** - Get implementation checklist
4. **Implement tasks sequentially** - Complete in order
5. **Mark complete immediately** - Update \`- [x]\` after each task
6. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
5. **Confirm completion** - Ensure every item in \`tasks.md\` is finished before updating statuses
6. **Update checklist** - After all work is done, set every task to \`- [x]\` so the list reflects reality
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
### Stage 3: Archiving Changes
After deployment, create separate PR to:
@@ -22,10 +22,12 @@ const proposalReferences = `**Reference**
- Explore the codebase with \`rg <keyword>\`, \`ls\`, or direct file reads so proposals align with current implementation realities.`;
const applySteps = `**Steps**
Track these steps as TODOs and complete them one by one.
1. Read \`changes/<id>/proposal.md\`, \`design.md\` (if present), and \`tasks.md\` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Mark each task \`- [x]\` immediately after completing it to keep the checklist in sync.
4. Reference \`openspec list\` or \`openspec show <item>\` when additional context is required.`;
3. Confirm completion before updating statuses—make sure every item in \`tasks.md\` is finished.
4. Update the checklist after all work is done so each task is marked \`- [x]\` and reflects reality.
5. Reference \`openspec list\` or \`openspec show <item>\` when additional context is required.`;
const applyReferences = `**Reference**
- Use \`openspec show <id> --json --deltas-only\` if you need additional context from the proposal while implementing.`;
+6 -4
View File
@@ -331,7 +331,8 @@ export class Validator {
}
private extractNameFromPath(filePath: string): string {
const parts = filePath.split('/');
const normalizedPath = filePath.replaceAll('\\', '/');
const parts = normalizedPath.split('/');
// Look for the directory name after 'specs' or 'changes'
for (let i = parts.length - 1; i >= 0; i--) {
@@ -343,8 +344,9 @@ export class Validator {
}
// Fallback to filename without extension if not in expected structure
const fileName = parts[parts.length - 1];
return fileName.replace('.md', '');
const fileName = parts[parts.length - 1] ?? '';
const dotIndex = fileName.lastIndexOf('.');
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
}
private createReport(issues: ValidationIssue[]): ValidationReport {
@@ -393,4 +395,4 @@ export class Validator {
const matches = blockRaw.match(/^####\s+/gm);
return matches ? matches.length : 0;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import { afterAll, describe, it, expect } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { tmpdir } from 'os';
import { runCLI, cliProjectRoot } from '../helpers/run-cli.js';
const tempRoots: string[] = [];
async function prepareFixture(fixtureName: string): Promise<string> {
const base = await fs.mkdtemp(path.join(tmpdir(), 'openspec-cli-e2e-'));
tempRoots.push(base);
const projectDir = path.join(base, 'project');
await fs.mkdir(projectDir, { recursive: true });
const fixtureDir = path.join(cliProjectRoot, 'test', 'fixtures', fixtureName);
await fs.cp(fixtureDir, projectDir, { recursive: true });
return projectDir;
}
afterAll(async () => {
await Promise.all(tempRoots.map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
describe('openspec CLI e2e basics', () => {
it('shows help output', async () => {
const result = await runCLI(['--help']);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Usage: openspec');
expect(result.stderr).toBe('');
});
it('reports the package version', async () => {
const pkgRaw = await fs.readFile(path.join(cliProjectRoot, 'package.json'), 'utf-8');
const pkg = JSON.parse(pkgRaw);
const result = await runCLI(['--version']);
expect(result.exitCode).toBe(0);
expect(result.stdout.trim()).toBe(pkg.version);
});
it('validates the tmp-init fixture with --all --json', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['validate', '--all', '--json'], { cwd: projectDir });
expect(result.exitCode).toBe(0);
const output = result.stdout.trim();
expect(output).not.toBe('');
const json = JSON.parse(output);
expect(json.summary?.totals?.failed).toBe(0);
expect(json.items.some((item: any) => item.id === 'c1' && item.type === 'change')).toBe(true);
});
it('returns an error for unknown items in the fixture', async () => {
const projectDir = await prepareFixture('tmp-init');
const result = await runCLI(['validate', 'does-not-exist'], { cwd: projectDir });
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("Unknown item 'does-not-exist'");
});
});
+58 -87
View File
@@ -1,31 +1,33 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { runCLI } from '../helpers/run-cli.js';
describe('top-level validate command', () => {
const projectRoot = process.cwd();
const testDir = path.join(projectRoot, 'test-validate-command-tmp');
const changesDir = path.join(testDir, 'openspec', 'changes');
const specsDir = path.join(testDir, 'openspec', 'specs');
const bin = path.join(projectRoot, 'bin', 'openspec.js');
beforeEach(async () => {
await fs.mkdir(changesDir, { recursive: true });
await fs.mkdir(specsDir, { recursive: true });
// Create a valid spec
const specContent = `## Purpose
Valid spec for testing.
## Requirements
### Requirement: Foo
Text
#### Scenario: Bar
Given A\nWhen B\nThen C`;
const specContent = [
'## Purpose',
'This spec ensures the validation harness exercises a deterministic alpha module for automated tests.',
'',
'## Requirements',
'',
'### Requirement: Alpha module SHALL produce deterministic output',
'The alpha module SHALL produce a deterministic response for validation.',
'',
'#### Scenario: Deterministic alpha run',
'- **GIVEN** a configured alpha module',
'- **WHEN** the module runs the default flow',
'- **THEN** the output matches the expected fixture result',
].join('\n');
await fs.mkdir(path.join(specsDir, 'alpha'), { recursive: true });
await fs.writeFile(path.join(specsDir, 'alpha', 'spec.md'), specContent, 'utf-8');
@@ -33,10 +35,26 @@ Given A\nWhen B\nThen C`;
const changeContent = `# Test Change\n\n## Why\nBecause reasons that are sufficiently long for validation.\n\n## What Changes\n- **alpha:** Add something`;
await fs.mkdir(path.join(changesDir, 'c1'), { recursive: true });
await fs.writeFile(path.join(changesDir, 'c1', 'proposal.md'), changeContent, 'utf-8');
const deltaContent = [
'## ADDED Requirements',
'### Requirement: Validator SHALL support alpha change deltas',
'The validator SHALL accept deltas provided by the test harness.',
'',
'#### Scenario: Apply alpha delta',
'- **GIVEN** the test change delta',
'- **WHEN** openspec validate runs',
'- **THEN** the validator reports the change as valid',
].join('\n');
const c1DeltaDir = path.join(changesDir, 'c1', 'specs', 'alpha');
await fs.mkdir(c1DeltaDir, { recursive: true });
await fs.writeFile(path.join(c1DeltaDir, 'spec.md'), deltaContent, 'utf-8');
// Duplicate name for ambiguity test
await fs.mkdir(path.join(changesDir, 'dup'), { recursive: true });
await fs.writeFile(path.join(changesDir, 'dup', 'proposal.md'), changeContent, 'utf-8');
const dupDeltaDir = path.join(changesDir, 'dup', 'specs', 'dup');
await fs.mkdir(dupDeltaDir, { recursive: true });
await fs.writeFile(path.join(dupDeltaDir, 'spec.md'), deltaContent, 'utf-8');
await fs.mkdir(path.join(specsDir, 'dup'), { recursive: true });
await fs.writeFile(path.join(specsDir, 'dup', 'spec.md'), specContent, 'utf-8');
});
@@ -45,77 +63,36 @@ Given A\nWhen B\nThen C`;
await fs.rm(testDir, { recursive: true, force: true });
});
it('prints a helpful hint when no args in non-interactive mode', () => {
const originalCwd = process.cwd();
const originalEnv = { ...process.env };
try {
process.chdir(testDir);
process.env.OPEN_SPEC_INTERACTIVE = '0';
let err: any;
try {
execSync(`node ${bin} validate`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.status).not.toBe(0);
expect(err.stderr.toString()).toContain('Nothing to validate. Try one of:');
} finally {
process.chdir(originalCwd);
process.env = originalEnv;
}
it('prints a helpful hint when no args in non-interactive mode', async () => {
const result = await runCLI(['validate'], { cwd: testDir });
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('Nothing to validate. Try one of:');
});
it('validates all with --all and outputs JSON summary', () => {
const originalCwd = process.cwd();
try {
process.chdir(testDir);
let outStr = '';
try {
outStr = execSync(`node ${bin} validate --all --json`, { encoding: 'utf-8' });
} catch (e: any) {
// If exit code is non-zero (e.g., on failures), still parse stdout JSON
outStr = e.stdout?.toString?.() ?? '';
}
const json = JSON.parse(outStr);
expect(Array.isArray(json.items)).toBe(true);
expect(json.summary?.totals?.items).toBeDefined();
expect(json.version).toBe('1.0');
} finally {
process.chdir(originalCwd);
}
it('validates all with --all and outputs JSON summary', async () => {
const result = await runCLI(['validate', '--all', '--json'], { cwd: testDir });
expect(result.exitCode).toBe(0);
const output = result.stdout.trim();
expect(output).not.toBe('');
const json = JSON.parse(output);
expect(Array.isArray(json.items)).toBe(true);
expect(json.summary?.totals?.items).toBeDefined();
expect(json.version).toBe('1.0');
});
it('validates only specs with --specs and respects --concurrency', () => {
const originalCwd = process.cwd();
try {
process.chdir(testDir);
let outStr = '';
try {
outStr = execSync(`node ${bin} validate --specs --json --concurrency 1`, { encoding: 'utf-8' });
} catch (e: any) {
outStr = e.stdout?.toString?.() ?? '';
}
const json = JSON.parse(outStr);
// All items should be specs
expect(json.items.every((i: any) => i.type === 'spec')).toBe(true);
} finally {
process.chdir(originalCwd);
}
it('validates only specs with --specs and respects --concurrency', async () => {
const result = await runCLI(['validate', '--specs', '--json', '--concurrency', '1'], { cwd: testDir });
expect(result.exitCode).toBe(0);
const output = result.stdout.trim();
expect(output).not.toBe('');
const json = JSON.parse(output);
expect(json.items.every((i: any) => i.type === 'spec')).toBe(true);
});
it('errors on ambiguous item names and suggests type override', () => {
const originalCwd = process.cwd();
try {
process.chdir(testDir);
let err: any;
try {
execSync(`node ${bin} validate dup`, { encoding: 'utf-8' });
} catch (e) { err = e; }
expect(err).toBeDefined();
expect(err.stderr.toString()).toContain('Ambiguous item');
expect(err.status).not.toBe(0);
} finally {
process.chdir(originalCwd);
}
it('errors on ambiguous item names and suggests type override', async () => {
const result = await runCLI(['validate', 'dup'], { cwd: testDir });
expect(result.exitCode).toBe(1);
expect(result.stderr).toContain('Ambiguous item');
});
it('accepts change proposals saved with CRLF line endings', async () => {
@@ -141,6 +118,7 @@ Given A\nWhen B\nThen C`;
'The parser SHALL accept CRLF change proposals without manual edits.',
'',
'#### Scenario: Validate CRLF change',
'- **GIVEN** a change proposal saved with CRLF line endings',
'- **WHEN** a developer runs openspec validate on the proposal',
'- **THEN** validation succeeds without section errors',
]);
@@ -149,14 +127,7 @@ Given A\nWhen B\nThen C`;
await fs.mkdir(deltaDir, { recursive: true });
await fs.writeFile(path.join(deltaDir, 'spec.md'), deltaContent, 'utf-8');
const originalCwd = process.cwd();
try {
process.chdir(testDir);
expect(() => execSync(`node ${bin} validate ${changeId}`, { encoding: 'utf-8' })).not.toThrow();
} finally {
process.chdir(originalCwd);
}
const result = await runCLI(['validate', changeId], { cwd: testDir });
expect(result.exitCode).toBe(0);
});
});
@@ -0,0 +1,7 @@
# Test Change
## Why
Because reasons that are sufficiently long for validation.
## What Changes
- **alpha:** Add something
@@ -0,0 +1,8 @@
## ADDED Requirements
### Requirement: Parser SHALL accept CRLF change proposals
The parser SHALL accept CRLF change proposals without manual edits.
#### Scenario: Validate CRLF change
- **GIVEN** a change proposal saved with CRLF line endings
- **WHEN** a developer runs openspec validate on the proposal
- **THEN** validation succeeds without section errors
+12
View File
@@ -0,0 +1,12 @@
## Purpose
This spec ensures the validation harness exercises a deterministic alpha module for automated tests.
## Requirements
### Requirement: Alpha module SHALL produce deterministic output
The alpha module SHALL produce a deterministic response for validation.
#### Scenario: Deterministic alpha run
- **GIVEN** a configured alpha module
- **WHEN** the module runs the default flow
- **THEN** the output matches the expected fixture result
+139
View File
@@ -0,0 +1,139 @@
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..', '..');
const cliEntry = path.join(projectRoot, 'dist', 'cli', 'index.js');
let buildPromise: Promise<void> | undefined;
interface RunCommandOptions {
cwd?: string;
env?: NodeJS.ProcessEnv;
}
interface RunCLIOptions {
cwd?: string;
env?: NodeJS.ProcessEnv;
input?: string;
timeoutMs?: number;
}
export interface RunCLIResult {
exitCode: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
timedOut: boolean;
command: string;
}
function runCommand(command: string, args: string[], options: RunCommandOptions = {}) {
return new Promise<void>((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd ?? projectRoot,
env: { ...process.env, ...options.env },
stdio: 'inherit',
shell: process.platform === 'win32',
});
child.on('error', (error) => reject(error));
child.on('close', (code, signal) => {
if (code === 0) {
resolve();
} else {
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
reject(new Error(`Command failed (${reason}): ${command} ${args.join(' ')}`));
}
});
});
}
export async function ensureCliBuilt() {
if (existsSync(cliEntry)) {
return;
}
if (!buildPromise) {
buildPromise = runCommand('pnpm', ['run', 'build']).catch((error) => {
buildPromise = undefined;
throw error;
});
}
await buildPromise;
if (!existsSync(cliEntry)) {
throw new Error('CLI entry point missing after build. Expected dist/cli/index.js');
}
}
export async function runCLI(args: string[] = [], options: RunCLIOptions = {}): Promise<RunCLIResult> {
await ensureCliBuilt();
const finalArgs = Array.isArray(args) ? args : [args];
const invocation = [cliEntry, ...finalArgs].join(' ');
return new Promise<RunCLIResult>((resolve, reject) => {
const child = spawn(process.execPath, [cliEntry, ...finalArgs], {
cwd: options.cwd ?? projectRoot,
env: {
...process.env,
OPEN_SPEC_INTERACTIVE: '0',
...options.env,
},
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
});
let stdout = '';
let stderr = '';
let timedOut = false;
const timeout = options.timeoutMs
? setTimeout(() => {
timedOut = true;
child.kill('SIGKILL');
}, options.timeoutMs)
: undefined;
child.stdout?.setEncoding('utf-8');
child.stdout?.on('data', (chunk) => {
stdout += chunk;
});
child.stderr?.setEncoding('utf-8');
child.stderr?.on('data', (chunk) => {
stderr += chunk;
});
child.on('error', (error) => {
if (timeout) clearTimeout(timeout);
reject(error);
});
child.on('close', (code, signal) => {
if (timeout) clearTimeout(timeout);
resolve({
exitCode: code,
signal,
stdout,
stderr,
timedOut,
command: `node ${invocation}`,
});
});
if (options.input && child.stdin) {
child.stdin.end(options.input);
} else if (child.stdin) {
child.stdin.end();
}
});
}
export const cliProjectRoot = projectRoot;
+4 -19
View File
@@ -1,21 +1,6 @@
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import path from 'path';
import { ensureCliBuilt } from './test/helpers/run-cli.js';
// Run once before all tests
// Ensure the CLI bundle exists before tests execute
export async function setup() {
const distPath = path.join(process.cwd(), 'dist', 'cli', 'index.js');
if (!existsSync(distPath)) {
console.log('Building project before tests...');
try {
execSync('pnpm run build', {
stdio: 'inherit',
cwd: process.cwd()
});
} catch (error) {
console.error('Failed to build project:', error);
process.exit(1);
}
}
}
await ensureCliBuilt();
}