mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-11 04:49:52 +08:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8faf504363 | ||
|
|
17d1e5db3f | ||
|
|
3f5a66d3e4 | ||
|
|
c08fbc1ba0 | ||
|
|
938d03be9a | ||
|
|
19ccaabfc7 | ||
|
|
2e382b9898 | ||
|
|
b5a7d096f0 | ||
|
|
c54079a0cd | ||
|
|
1050e57ae4 |
@@ -1,5 +1,24 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c08fbc1: Add new AI tool integrations and enhancements:
|
||||
|
||||
- **feat(iflow-cli)**: Add iFlow-cli integration with slash command support and documentation
|
||||
- **feat(init)**: Add IDE restart instruction after init to inform users about slash command availability
|
||||
**feat(antigravity)**: Add Antigravity slash command support
|
||||
- **fix**: Generate TOML commands for Qwen Code (fixes #293)
|
||||
- Clarify scaffold proposal documentation and enhance proposal guidelines
|
||||
- Update proposal guidelines to emphasize design-first approach before implementation
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add Antigravity slash command support so `openspec init` can generate `.agent/workflows/openspec-*.md` files with description-only frontmatter and `openspec update` refreshes existing workflows alongside Windsurf.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -102,12 +102,14 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
|
||||
| **Qoder (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.qoder/commands/openspec/`) — see [docs](https://qoder.com/cli) |
|
||||
| **Antigravity** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.agent/workflows/`) |
|
||||
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
|
||||
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
|
||||
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
|
||||
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
|
||||
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
|
||||
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
|
||||
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/commands/`) |
|
||||
|
||||
|
||||
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`.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
## Why
|
||||
Google is rolling out Antigravity, a Windsurf-derived IDE that discovers workflows from `.agent/workflows/*.md`. Today OpenSpec can only scaffold slash commands for Windsurf directories, so Antigravity users cannot run the proposal/apply/archive flows from the IDE.
|
||||
|
||||
## What Changes
|
||||
- Add Antigravity as a selectable native tool in `openspec init` so it creates `.agent/workflows/openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` with YAML frontmatter containing only a `description` field plus the standard OpenSpec-managed body.
|
||||
- Ensure `openspec update` refreshes the body of any existing Antigravity workflows inside `.agent/workflows/` without creating missing files, mirroring the Windsurf behavior.
|
||||
- Share e2e/template coverage confirming the generator writes the proper directory, filename casing, and frontmatter format so Antigravity picks up the workflows.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/cli-init`, `specs/cli-update`
|
||||
- Expected code: CLI init/update tool registries, slash-command templates, associated tests
|
||||
@@ -0,0 +1,9 @@
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Slash Command Configuration
|
||||
The init command SHALL generate slash command files for supported editors using shared templates.
|
||||
|
||||
#### Scenario: Generating slash commands for Antigravity
|
||||
- **WHEN** the user selects Antigravity during initialization
|
||||
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
|
||||
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
|
||||
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
|
||||
@@ -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, and ensure the OpenCode archive command accepts change ID arguments.
|
||||
|
||||
#### Scenario: Updating slash commands for Antigravity
|
||||
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
|
||||
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
|
||||
@@ -0,0 +1,12 @@
|
||||
## 1. CLI init support
|
||||
- [x] 1.1 Surface Antigravity in the native-tool picker (interactive + `--tools`) so it toggles alongside other IDEs.
|
||||
- [x] 1.2 Generate `.agent/workflows/openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` with YAML frontmatter restricted to a single `description` field for each stage and wrap the body in OpenSpec markers.
|
||||
- [x] 1.3 Confirm workspace scaffolding covers missing directory creation and re-run scenarios so repeated init refreshes the managed block.
|
||||
|
||||
## 2. CLI update support
|
||||
- [x] 2.1 Detect existing Antigravity workflow files during `openspec update` and refresh only the managed body, skipping creation when files are missing.
|
||||
- [x] 2.2 Ensure update logic preserves the `description` frontmatter block exactly as written by init, including case and spacing, and refreshes body templates alongside other tools.
|
||||
|
||||
## 3. Templates and tests
|
||||
- [x] 3.1 Add shared template entries for Antigravity that reuse the Windsurf copy but target `.agent/workflows` plus the description-only frontmatter requirement.
|
||||
- [x] 3.2 Expand automated coverage (unit or integration) verifying init and update produce the expected file paths and frontmatter + body markers for Antigravity.
|
||||
@@ -2,9 +2,9 @@
|
||||
Manual setup for new changes leads to formatting mistakes in spec deltas and slows agents who must recreate the same file skeletons for every proposal. A built-in scaffold command will generate compliant templates so assistants can focus on the change content instead of structure.
|
||||
|
||||
## What Changes
|
||||
- Add an `openspec scaffold <change-id>` CLI command that creates a change directory with validated `proposal.md`, `tasks.md`, and spec delta templates.
|
||||
- Update CLI documentation and quick-reference guidance so agents discover the scaffold workflow before drafting files manually.
|
||||
- Add automated coverage (unit/integ tests) to ensure the command respects existing naming rules and generated Markdown passes validation.
|
||||
- Add an `openspec scaffold <change-id>` CLI command that creates a change directory (if it does not already exist) with validated `proposal.md`, `tasks.md`, and spec delta templates.
|
||||
- Update CLI documentation and quick-reference guidance so agents discover the scaffold workflow before drafting files manually, including reminders on when to create spec deltas.
|
||||
- Add automated coverage (unit/integ tests) to ensure the command respects naming rules, copies templates correctly, fails for existing directories, and produces output that passes `openspec validate --strict` untouched.
|
||||
|
||||
## Impact
|
||||
- Affected specs: `specs/cli-scaffold`
|
||||
|
||||
@@ -9,13 +9,13 @@ The CLI SHALL expose an `openspec scaffold <change-id>` command that validates t
|
||||
- **AND** exit with code 0 after successful scaffolding
|
||||
|
||||
### Requirement: Change Directory Structure
|
||||
The scaffold command SHALL create the standard change workspace with proposal, tasks, optional design, and delta directories laid out according to OpenSpec conventions.
|
||||
The scaffold command SHALL create the standard change workspace (if it does not already exist) with proposal, tasks, optional design, and `specs/` directories laid out according to OpenSpec conventions.
|
||||
|
||||
#### Scenario: Generating change workspace
|
||||
- **WHEN** scaffolding a new change with id `add-user-notifications`
|
||||
- **THEN** create `openspec/changes/add-user-notifications/`
|
||||
- **AND** generate `proposal.md`, `tasks.md`, and `design.md` (commented placeholder content) in that directory when missing
|
||||
- **AND** create `openspec/changes/add-user-notifications/specs/` ready for capability-specific deltas
|
||||
- **THEN** create `openspec/changes/add-user-notifications/` if it does not exist
|
||||
- **AND** copy the default template bundle (proposal, tasks, design placeholders) into that directory in a single operation
|
||||
- **AND** create an empty `openspec/changes/add-user-notifications/specs/` directory ready for capability-specific deltas that will be authored later
|
||||
|
||||
### Requirement: Template Content Guidance
|
||||
The scaffold command SHALL populate generated Markdown files with OpenSpec-compliant templates so authors can copy, edit, and pass validation without reformatting.
|
||||
@@ -25,15 +25,7 @@ The scaffold command SHALL populate generated Markdown files with OpenSpec-compl
|
||||
- **THEN** include the `## Why`, `## What Changes`, and `## Impact` headings with placeholder guidance text
|
||||
- **AND** ensure `tasks.md` starts with `## 1. Implementation` and numbered checklist items using `- [ ]` syntax
|
||||
- **AND** annotate optional sections (like `design.md`) with inline TODO comments so users understand when to keep or delete them
|
||||
|
||||
### Requirement: Delta Spec Creation
|
||||
The scaffold command SHALL create at least one capability delta file with correctly formatted requirement and scenario placeholders that guide authors to enter the actual behavior.
|
||||
|
||||
#### Scenario: Creating spec delta skeleton
|
||||
- **WHEN** scaffolding a change and the capability `cli-scaffold` is provided interactively or via flags
|
||||
- **THEN** generate `openspec/changes/add-user-notifications/specs/cli-scaffold/spec.md`
|
||||
- **AND** include `## ADDED Requirements` with at least one `### Requirement:` block and matching `#### Scenario:` entries that remind the author to replace placeholder text
|
||||
- **AND** ensure the generated delta passes `openspec validate add-user-notifications --strict` until the author edits it
|
||||
- **AND** include a short reminder inside `specs/README.md` (or similar) instructing authors to add deltas once they know the affected capability
|
||||
|
||||
### Requirement: Idempotent Execution
|
||||
The scaffold command SHALL be safe to rerun, preserving user edits while filling in any missing managed sections.
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
## 1. CLI scaffolding command
|
||||
- [ ] 1.1 Register an `openspec scaffold` command in the CLI entrypoint with `change-id` argument validation.
|
||||
- [ ] 1.2 Implement generator logic that creates the change directory structure plus default `proposal.md`, `tasks.md`, and delta spec skeletons without overwriting existing populated files.
|
||||
- [ ] 1.2 Implement generator logic that copies the default change template bundle (`proposal.md`, `tasks.md`, optional `design.md`, `specs/README.md`) into `openspec/changes/<id>/`, creating the directory tree in a single pass.
|
||||
- [ ] 1.3 Detect when `openspec/changes/<id>/` already exists and exit with a clear error instead of overwriting user files.
|
||||
|
||||
## 2. Templates and documentation
|
||||
- [ ] 2.1 Surface copy/paste templates and scaffold usage in the top-level quick reference for `openspec/AGENTS.md`.
|
||||
- [ ] 2.2 Refresh other CLI docs (`docs/`, README) to mention the scaffold workflow and link to instructions.
|
||||
- [ ] 2.1 Update `openspec/AGENTS.md` quick reference so agents see `openspec scaffold` before drafting files manually.
|
||||
- [ ] 2.2 Refresh CLI docs/README/help text to mention the scaffold workflow, template bundle contents, and when to add spec deltas manually.
|
||||
|
||||
## 3. Test coverage
|
||||
- [ ] 3.1 Add unit tests covering name validation, file generation, and idempotent reruns.
|
||||
- [ ] 3.2 Add integration coverage ensuring generated files pass `openspec validate --strict` without manual edits.
|
||||
- [ ] 3.1 Add unit tests covering name validation, template copying, and existing-directory failures.
|
||||
- [ ] 3.2 Add integration coverage ensuring a freshly scaffolded change (without deltas) passes `openspec validate --strict` until the author customizes it.
|
||||
|
||||
@@ -76,6 +76,12 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
|
||||
- **THEN** create or update `CLINE.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: Configuring iFlow CLI
|
||||
|
||||
- **WHEN** iFlow CLI is selected
|
||||
- **THEN** create or update `IFLOW.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
|
||||
@@ -118,6 +124,12 @@ The command SHALL provide clear, actionable next steps upon successful initializ
|
||||
- **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"
|
||||
|
||||
#### Scenario: Displaying restart instruction
|
||||
- **WHEN** initialization completes successfully and tools were created or refreshed
|
||||
- **THEN** display a prominent restart instruction before the "Next steps" section
|
||||
- **AND** inform users that slash commands are loaded at startup
|
||||
- **AND** instruct users to restart their coding assistant to ensure /openspec commands appear
|
||||
|
||||
### Requirement: Exit Codes
|
||||
|
||||
The command SHALL use consistent exit codes to indicate different failure modes.
|
||||
@@ -238,6 +250,14 @@ The init command SHALL generate slash command files for supported editors using
|
||||
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
|
||||
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
|
||||
|
||||
#### Scenario: Generating slash commands for iFlow CLI
|
||||
- **WHEN** the user selects iFlow CLI during initialization
|
||||
- **THEN** create `.iflow/commands/openspec-proposal.md`, `.iflow/commands/openspec-apply.md`, and `.iflow/commands/openspec-archive.md`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** include YAML frontmatter with `name`, `id`, `category`, and `description` fields for each command
|
||||
- **AND** wrap the generated content in OpenSpec managed markers so `openspec update` can safely refresh the commands
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for RooCode
|
||||
- **WHEN** the user selects RooCode during initialization
|
||||
- **THEN** create `.roo/commands/openspec-proposal.md`, `.roo/commands/openspec-apply.md`, and `.roo/commands/openspec-archive.md`
|
||||
|
||||
@@ -123,6 +123,13 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
|
||||
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
|
||||
|
||||
#### Scenario: Updating slash commands for iFlow CLI
|
||||
- **WHEN** `.iflow/commands/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** preserve the YAML frontmatter with `name`, `id`, `category`, and `description` fields
|
||||
- **AND** update only the OpenSpec-managed block between markers
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Missing slash command file
|
||||
- **WHEN** a tool lacks a slash command file
|
||||
- **THEN** do not create a new file during update
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
# RFC 0001 Appendix: Research, Decisions & Background
|
||||
|
||||
> This document accompanies [RFC 0001: OpenSpec Workspaces](./0001-openspec-workspaces.md).
|
||||
> It contains the research, Q&A, decision rationale, and design exploration that informed the main RFC.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#executive-summary)
|
||||
2. [Background & Motivation](#background--motivation)
|
||||
3. [Key Decisions (V1)](#key-decisions-v1)
|
||||
4. [Research: Cross-Platform Storage](#research-cross-platform-storage)
|
||||
5. [Q&A: Design Clarifications](#qa-design-clarifications)
|
||||
6. [Risk Analysis](#risk-analysis)
|
||||
7. [Alternatives Considered](#alternatives-considered)
|
||||
8. [Future Considerations](#future-considerations)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
OpenSpec is being split into two concerns:
|
||||
|
||||
1. **Specs** (source of truth) — remain in-repo at `/openspec/specs/`
|
||||
2. **Change proposals** (planning context) — move to a centralized user-scoped directory at `~/.config/openspec/`
|
||||
|
||||
This enables:
|
||||
- Multi-repo planning without duplicating proposals
|
||||
- Simplified change lifecycle (no archival step)
|
||||
- Path to customizable planning workflows
|
||||
- Better agent context management
|
||||
|
||||
---
|
||||
|
||||
## Background & Motivation
|
||||
|
||||
### Problems Being Solved
|
||||
|
||||
1. **Multi-repo coordination**: Teams working across multiple repos currently must duplicate change proposals in each repo, leading to drift and coordination overhead.
|
||||
|
||||
2. **Convoluted archival process**: The current workflow requires agents to make exact copies of specs, then archive changes after applying. This is error-prone and adds friction.
|
||||
|
||||
3. **Limited customization**: The tightly-coupled structure makes it hard to customize the planning process or add features.
|
||||
|
||||
### Benefits of the New Approach
|
||||
|
||||
| Benefit | Single-Repo Users | Multi-Repo Teams |
|
||||
|---------|-------------------|------------------|
|
||||
| Simpler workflow | No archival step | Single change spans repos |
|
||||
| Better PRs | Spec + code diff together | Coordinated changes |
|
||||
| Agent context | Clear active change | Cross-repo awareness |
|
||||
| Customization | Planning templates | Shared workflows |
|
||||
|
||||
### Why Split Specs and Changes?
|
||||
|
||||
- **Specs are code** — they belong with the codebase, versioned in git, reviewed in PRs
|
||||
- **Changes are ephemeral** — they're planning artifacts that don't need to live in the repo permanently
|
||||
- **Cross-repo changes need a home** — can't pick one repo arbitrarily; centralized location is neutral ground
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions (V1)
|
||||
|
||||
### 1. Workspace/Index Location
|
||||
|
||||
**Decision**: `~/.config/openspec/index.json` (XDG-style)
|
||||
|
||||
**Rationale**: XDG spec is the modern standard for CLI tools, has clear cross-platform mappings, and libraries exist in every language.
|
||||
|
||||
**RepoId format**: `{primary-remote-url}@{default-branch}`
|
||||
- Supports multiple local clone paths for the same repo
|
||||
- SSH/HTTPS variants normalize to same ID
|
||||
- Repos without remotes fall back to local path hash
|
||||
|
||||
### 2. Active Context Projection
|
||||
|
||||
**Decision**: Write a git-ignored shadow manifest `./.openspec-context.json` in each attached repo.
|
||||
|
||||
**V1 Schema**:
|
||||
```
|
||||
schemaVersion, generatedAt, repoId, workspaceId, branch,
|
||||
activeChangeId|null, changeTitle, status,
|
||||
specRoots, tasks array, warnings,
|
||||
sourceHash/sourceVersion
|
||||
```
|
||||
|
||||
**Rationale**: Agents can discover context without CLI calls; file is lightweight and regenerated on demand.
|
||||
|
||||
### 3. Refresh Policy
|
||||
|
||||
**Decision**: Regenerate the manifest on any context-touching command (`openspec status/context/set-active/attach`) if missing or branch mismatch detected. Force refresh via `openspec context --refresh`.
|
||||
|
||||
**V1 Simplification**: No age checks; git-less repos skip branch checks.
|
||||
|
||||
### 4. Drift Detection
|
||||
|
||||
**Decision**: `openspec verify` auto-runs on `status/context`, surfaces drift as **warnings only** (non-blocking).
|
||||
|
||||
**Rationale**: Start with visibility, not enforcement. Let users understand drift before making it blocking.
|
||||
|
||||
### 5. Task Storage
|
||||
|
||||
**Decision**: Stay as Markdown checkboxes in V1.
|
||||
|
||||
**Future option**: Normalize to structured format (JSONL/SQLite) for better agent integration, but not required for V1.
|
||||
|
||||
### 6. Cross-Repo Specs
|
||||
|
||||
**Decision**: Defer to future version with dedicated "specs repo" as canonical home for cross-repo capabilities.
|
||||
|
||||
**Rationale**: This is complex and not needed for V1. Single-repo and simple multi-repo cases work without it.
|
||||
|
||||
### 7. Active Change Detection
|
||||
|
||||
**Decision**: Use git branch to determine which change is active.
|
||||
|
||||
**How it works**:
|
||||
- When user creates a change, they optionally create a feature branch
|
||||
- OpenSpec detects current branch and maps it to corresponding change
|
||||
- If on main/master, user can manually select which change to work on
|
||||
|
||||
**Open questions** (deferred):
|
||||
- Where is branch→change mapping stored?
|
||||
- Handling multiple changes for same branch?
|
||||
- Branch renames?
|
||||
|
||||
### 8. Agent Integration Strategy
|
||||
|
||||
**Decision**: Support multiple modes (hybrid approach).
|
||||
|
||||
| Mode | Target | How it works |
|
||||
|------|--------|--------------|
|
||||
| SDK mode | Claude Code, etc. | OpenSpec invokes agent, injects context, verifies completion |
|
||||
| CLI mode | Cursor, etc. | Agent calls CLI commands, best-effort tracking |
|
||||
| Manual mode | Any | User manually syncs, OpenSpec provides verification |
|
||||
|
||||
**Rationale**: Provide best experience where possible while still working with any agent.
|
||||
|
||||
### 9. Spec Sync Strategy
|
||||
|
||||
**Decision**: Push-based updates as ideal path, `openspec verify` for manual sync/drift detection.
|
||||
|
||||
**Push-based** (ideal): All changes go through OpenSpec commands which update both the proposal and trigger code changes.
|
||||
|
||||
**Pull-based** (fallback): `openspec verify` command compares spec-deltas to actual spec files and reports what's out of sync.
|
||||
|
||||
**Automatic sync** (future): File watchers or git hooks, but start with manual for V1.
|
||||
|
||||
---
|
||||
|
||||
## Research: Cross-Platform Storage
|
||||
|
||||
### Location Options Compared
|
||||
|
||||
| Approach | macOS | Linux | Windows | Used by |
|
||||
|----------|-------|-------|---------|---------|
|
||||
| XDG spec | `~/.config/openspec` | `~/.config/openspec` | `%APPDATA%\openspec` | Many CLI tools |
|
||||
| Home dotfile | `~/.openspec` | `~/.openspec` | `%USERPROFILE%\.openspec` | npm, cargo, rustup |
|
||||
| App Support | `~/Library/Application Support/openspec` | `~/.local/share/openspec` | `%LOCALAPPDATA%\openspec` | VS Code, Electron apps |
|
||||
|
||||
### Recommendation
|
||||
|
||||
Use XDG-style (`~/.config/openspec`) because:
|
||||
- Modern standard for CLI tools
|
||||
- Clear cross-platform mappings
|
||||
- Libraries exist in every language (`dirs` in Rust, `appdirs` in Python, `env-paths` in Node)
|
||||
- Keeps home directory cleaner than dotfiles
|
||||
|
||||
**Note**: `~/.openspec` is hidden by default (dot prefix) which conflicts with "open in editor" goal. Consider `~/openspec` if visibility matters, but XDG is more conventional.
|
||||
|
||||
---
|
||||
|
||||
## Q&A: Design Clarifications
|
||||
|
||||
### Location and Structure
|
||||
|
||||
**Q1: Where exactly does `.openspec` live?**
|
||||
|
||||
A: `~/.config/openspec` (XDG-style). For V1, assume change proposals only exist locally. Cloud/git-based sync for team sharing is deferred.
|
||||
|
||||
**Q2: What's the structure inside `.openspec`?**
|
||||
|
||||
A: File-based structure so teams can open the folder in their code editor:
|
||||
```
|
||||
~/.config/openspec/
|
||||
├─ index.json # repo/workspace index
|
||||
└─ workspaces/<workspace-id>/
|
||||
├─ workspace.json # attached repos, defaults
|
||||
└─ changes/<change-id>/
|
||||
├─ proposal.md # overview, design choices
|
||||
├─ tasks.md # task breakdown with checkboxes
|
||||
└─ specs/ # spec deltas
|
||||
```
|
||||
|
||||
**Q3: How are repos "attached" to a workspace?**
|
||||
|
||||
A: Configuration reference (not symlinks). User selects a folder/repository locally. Does not need to be git-enabled, but git provides branch detection benefits.
|
||||
|
||||
### Multi-Repo Mechanics
|
||||
|
||||
**Q4: How does a single change span multiple repos?**
|
||||
|
||||
A: Expand existing spec-deltas concept across repos. Task phases can be split between repos (e.g., "Phase 1: API repo", "Phase 2: Frontend repo").
|
||||
|
||||
**Q5: How do PRs work across repos?**
|
||||
|
||||
A: Left to the developer. OpenSpec helps create changes in each repo; PR management is manual. This is intentional — automated cross-repo transactions are complex and error-prone.
|
||||
|
||||
**Q6: How do you handle version coordination?**
|
||||
|
||||
A: Planning is based on local version of repos (or main/master). Explicit version dependencies are out of scope for V1.
|
||||
|
||||
**Q7: Where do cross-repo specs live?**
|
||||
|
||||
A: Deferred. Initial thinking: a dedicated "specs repo" that OpenSpec manages. For V1, keep specs in individual repos.
|
||||
|
||||
### Agent Integration
|
||||
|
||||
**Q8: How do agents discover the relevant context?**
|
||||
|
||||
A: Agent calls OpenSpec CLI from the repository. CLI identifies workspace and returns relevant context. Alternatively, read `.openspec-context.json` manifest directly.
|
||||
|
||||
**Q9: Do agents need CLI commands to access context?**
|
||||
|
||||
A: CLI is the primary method. The manifest (`.openspec-context.json`) provides a cached view to reduce CLI calls. Trade-off: manifest can be stale, but refresh is cheap.
|
||||
|
||||
**Q10: What if an agent modifies specs directly?**
|
||||
|
||||
A: Changes live in `~/.config/openspec`, specs live in repos. Agent can modify specs directly (they're just files). `openspec verify` detects drift between expected and actual state.
|
||||
|
||||
### Workflow and State
|
||||
|
||||
**Q11: What's the lifecycle of a change?**
|
||||
|
||||
A: `Draft → In Progress → Completed`
|
||||
|
||||
There may be sub-states in drafting (research, planning, breakdown). This should be configurable for users.
|
||||
|
||||
**Q12: What does the "work log" contain?**
|
||||
|
||||
A: Agent-generated record of work done. Less important for V1; deferred.
|
||||
|
||||
**Q13: How does "active change" context work?**
|
||||
|
||||
A: Branch-based detection is the primary method. Open question: how to handle non-git repos or when working on main. Stored state vs. git reconstruction needs more thought.
|
||||
|
||||
**Q14: How do you resume work on a change?**
|
||||
|
||||
A: Agent reads the change, looks at which tasks are ticked off, reviews any work log, and picks up from there.
|
||||
|
||||
### Task Management
|
||||
|
||||
**Q15: Why is task management "unknown" if it's core to planning?**
|
||||
|
||||
A: Goal is minimal V1, not complete solution. Markdown checkboxes work today. Agent forgetfulness is a known issue but not blocking for V1.
|
||||
|
||||
**Q16: What's wrong with current task tracking?**
|
||||
|
||||
A: Agents forget to tick checkboxes after doing work. This is an agent behavior problem, not purely a data model problem. Solutions involve better prompting, verification, or agent-side tooling.
|
||||
|
||||
**Q17: Should task state live in `.openspec` or in-repo?**
|
||||
|
||||
A: Central (`~/.config/openspec`) for simplicity.
|
||||
|
||||
### Migration and Compatibility
|
||||
|
||||
**Q18: How do existing users migrate?**
|
||||
|
||||
A: Automatic migration on CLI update. Detect existing `/openspec/changes`, copy to workspace store, write manifests.
|
||||
|
||||
**Q19: Are you removing the in-repo changes workflow?**
|
||||
|
||||
A: Yes, eventually. One flow to maintain. May keep legacy layout as read-only during transition.
|
||||
|
||||
---
|
||||
|
||||
## Risk Analysis
|
||||
|
||||
### High Risk
|
||||
|
||||
#### Agent Context Accessibility (Partially Mitigated)
|
||||
|
||||
Moving change context outside the repo is the biggest risk. Currently, agents work because all context is co-located in `/openspec/`.
|
||||
|
||||
**Concerns**:
|
||||
- Agents lose direct filesystem access to change proposals
|
||||
- CLI fetch adds friction and indirection
|
||||
- Not all agent systems can invoke CLI commands mid-task
|
||||
- Mental model breaks: "where I'm coding" vs. "where my plan lives"
|
||||
|
||||
**Mitigation**: Hybrid agent integration (SDK/CLI/Manual modes), shadow manifest for quick reads.
|
||||
|
||||
### Medium-High Risk
|
||||
|
||||
#### Multi-Repo Coordination Complexity
|
||||
|
||||
Multi-repo is the main selling point but introduces complexity:
|
||||
- No atomic cross-repo git transactions
|
||||
- PR coordination is manual and error-prone
|
||||
- Version dependencies need explicit modeling
|
||||
- CI/CD becomes more complex
|
||||
|
||||
**Mitigation**: V1 keeps it simple — no automated PR orchestration, no version enforcement. Let users handle coordination manually.
|
||||
|
||||
#### Cross-Repo Spec Ownership
|
||||
|
||||
Where do specs live that span multiple repos?
|
||||
|
||||
**Options considered**:
|
||||
| Option | Problem |
|
||||
|--------|---------|
|
||||
| Duplicated in each repo | Sync problems, unclear authority |
|
||||
| In one "primary" repo | Arbitrary, hard to discover |
|
||||
| In centralized `.openspec` | Split location for specs |
|
||||
| In separate "specs repo" | Another repo to maintain |
|
||||
|
||||
**Decision**: Defer to future "specs repo" concept. V1 keeps specs in individual repos.
|
||||
|
||||
### Medium Risk
|
||||
|
||||
#### Task Management Undefined
|
||||
|
||||
Task management is central but left undefined. This affects:
|
||||
- Data model for tasks
|
||||
- Agent integration
|
||||
- Work log concept
|
||||
|
||||
**Mitigation**: Start with Markdown checkboxes. Iterate based on real usage.
|
||||
|
||||
#### Location and Portability (Partially Mitigated)
|
||||
|
||||
User-level directory creates challenges:
|
||||
- CI/CD access
|
||||
- Team collaboration
|
||||
- Machine portability
|
||||
- Discoverability
|
||||
|
||||
**Mitigation**: Cross-platform location research done. V1 is local-only; team sync deferred.
|
||||
|
||||
#### State Synchronization (Partially Mitigated)
|
||||
|
||||
Specs in-repo, changes out-of-repo can drift:
|
||||
- Direct spec edits bypass change tracking
|
||||
- Stale `.openspec` state
|
||||
- Orphaned changes when repos deleted
|
||||
|
||||
**Mitigation**: `openspec verify` for drift detection. Automatic sync is future work.
|
||||
|
||||
### Low-Medium Risk
|
||||
|
||||
#### Backwards Compatibility
|
||||
|
||||
Existing users have workflows around `/openspec/changes/`.
|
||||
|
||||
**Mitigation**: Clear migration path, automatic migration on update, optional read-only legacy support during transition.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Keep Everything In-Repo
|
||||
|
||||
**Pros**: Simple, portable, git-versioned, agent-friendly
|
||||
**Cons**: Can't support multi-repo, archival is messy
|
||||
|
||||
**Why rejected**: Multi-repo is a key goal; this doesn't solve it.
|
||||
|
||||
### Monorepo-Only Support
|
||||
|
||||
**Pros**: Simpler than true multi-repo, single git history
|
||||
**Cons**: Many teams don't use monorepos, doesn't help existing multi-repo setups
|
||||
|
||||
**Why rejected**: Too limiting; multi-repo is common in practice.
|
||||
|
||||
### Git Submodules for Changes
|
||||
|
||||
**Pros**: Git-native, versioned, portable
|
||||
**Cons**: Submodules are notoriously painful, adds complexity
|
||||
|
||||
**Why rejected**: Complexity outweighs benefits.
|
||||
|
||||
### Cloud-First Storage
|
||||
|
||||
**Pros**: Team sync built-in, no local state issues
|
||||
**Cons**: Requires account/auth, internet dependency, privacy concerns
|
||||
|
||||
**Why rejected**: Local-first is simpler for V1; cloud can be added later.
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Post-V1 Features
|
||||
|
||||
1. **Team sync**: Git-based or cloud-based workspace sharing
|
||||
2. **CI/CD integration**: Environment overrides, workspace exports
|
||||
3. **Structured tasks**: JSONL or SQLite for better agent integration
|
||||
4. **Cross-repo specs**: Dedicated specs repo as canonical home
|
||||
5. **Work logs**: Agent-generated session records
|
||||
6. **Branch→change mapping**: More robust active change detection
|
||||
7. **API/SDK**: Programmatic access beyond CLI
|
||||
|
||||
### Open Design Questions
|
||||
|
||||
- How granular is drift detection?
|
||||
- Should verification block PR creation or just warn?
|
||||
- How to handle branch renames?
|
||||
- What if multiple changes exist for the same branch?
|
||||
- How to represent cross-repo spec ownership without a dedicated specs repo?
|
||||
|
||||
### Agent Integration Evolution
|
||||
|
||||
Current thinking on improving agent task completion:
|
||||
1. Better prompting (explicit reminders to tick checkboxes)
|
||||
2. Verification commands (agent calls `openspec verify` before finishing)
|
||||
3. SDK mode (OpenSpec invokes agent, validates completion)
|
||||
4. Work log (track what agent actually did vs. what was planned)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 0001: OpenSpec Workspaces](./0001-openspec-workspaces.md) — the main RFC
|
||||
- XDG Base Directory Specification
|
||||
- Similar tools: Nx, Turborepo, Lerna (monorepo coordination)
|
||||
@@ -0,0 +1,328 @@
|
||||
# RFC 0001: OpenSpec Workspaces and Off-Repo Change Storage
|
||||
|
||||
**Status:** Draft
|
||||
**Authors/Reviewers:** TBC
|
||||
**Last Updated:** 2024-XX-XX
|
||||
|
||||
> **See also:** [Appendix: Research & Decisions](./0001-openspec-workspaces-appendix.md) for background research, Q&A, and detailed decision rationale.
|
||||
|
||||
## Purpose
|
||||
|
||||
**Problem:** Users working on features that span multiple repos must currently duplicate change proposals in each repo, leading to drift and coordination overhead. Single-repo users also suffer from a convoluted archive step when applying changes.
|
||||
|
||||
**Solution:**
|
||||
- Enable multi-repo planning by moving change proposals out of individual repos while keeping specs in-repo.
|
||||
- Preserve agent usability with a lightweight, discoverable context projection per repo.
|
||||
- Simplify the change lifecycle (apply changes directly, drop archiving) and keep the spec and code diff side-by-side.
|
||||
|
||||
## Goals (V1)
|
||||
|
||||
- Single source of truth for changes lives in a user-scoped workspace store (`~/.config/openspec`).
|
||||
- Specs stay in the repo; only change proposals and workspace metadata move out-of-repo.
|
||||
- Agents and humans can see "what change is active here?" via a small manifest in the repo root.
|
||||
- Lay the groundwork for customizable workflows (hooks, reusable instructions) scoped to a workspace.
|
||||
- Keep task tracking simple (Markdown checkboxes) while allowing future structured storage.
|
||||
- Provide a clear migration path from in-repo `/openspec/changes`.
|
||||
|
||||
## Non-Goals (V1)
|
||||
|
||||
- Team/shared cloud sync (local-only for now).
|
||||
- Automatic cross-repo PR orchestration or transactions.
|
||||
- Rich task model or work log; basic checkboxes only.
|
||||
- Replacing existing specs layout in-repo.
|
||||
- CI/CD integration (workspace store lives in user home; CI pipelines that need change context should use environment overrides or check in workspace exports—deferred to future work).
|
||||
|
||||
## Before and After (Conceptual)
|
||||
|
||||
```
|
||||
Before (today)
|
||||
repo/
|
||||
└─ openspec/
|
||||
├─ specs/
|
||||
└─ changes/<change-id>/
|
||||
├─ proposal.md
|
||||
├─ tasks.md
|
||||
└─ specs/... (deltas)
|
||||
|
||||
After (V1)
|
||||
repo/
|
||||
├─ openspec/specs/ # unchanged spec source of truth
|
||||
├─ .openspec-context.json # git-ignored manifest (shadow)
|
||||
└─ ...code...
|
||||
|
||||
~/.config/openspec/ # user-scoped workspace store
|
||||
├─ index.json # repo/workspace index
|
||||
└─ workspaces/<workspace-id>/
|
||||
├─ workspace.json # attached repos, defaults
|
||||
└─ changes/<change-id>/ # proposals, tasks, deltas
|
||||
├─ proposal.md
|
||||
├─ tasks.md
|
||||
└─ specs/... (deltas)
|
||||
```
|
||||
|
||||
## Proposed Model (High-Level)
|
||||
|
||||
- **Workspace store**: XDG path `~/.config/openspec` holds `index.json` plus per-workspace folders.
|
||||
- **Identifiers**:
|
||||
- `workspaceId`: user-provided slug or auto-generated UUID (e.g., `default`, `my-project`).
|
||||
- `changeId`: user-provided slug derived from change title or auto-generated (e.g., `add-user-auth`).
|
||||
- `repoId`: `{primary-remote-url}@{default-branch}` (normalized: SSH/HTTPS variants resolve to same ID). For repos without remotes, fallback to local path hash.
|
||||
- **Context projection**: generate `./.openspec-context.json` in each attached repo (git-ignored) with:
|
||||
- `schemaVersion`, `generatedAt`, `repoId`, `workspaceId`, `branch`
|
||||
- `activeChangeId|null`, `changeTitle`, `status`
|
||||
- `specRoots`, `tasks` (checkbox state), `warnings`
|
||||
- `sourceHash`/`sourceVersion` for drift detection
|
||||
- **Refresh policy**: regenerate manifest on `openspec status/context/set-active/attach`, or `openspec context --refresh`.
|
||||
- **Drift detection**: `openspec verify` runs on `status/context`, warns (non-blocking) when:
|
||||
- Manifest `sourceHash` (hash of change proposal + tasks) differs from workspace store.
|
||||
- In-repo specs differ from expected state based on applied changes.
|
||||
- Branch in manifest doesn't match current git branch.
|
||||
- **Tasks**: stay as Markdown checkboxes in change folders; structured format is future work.
|
||||
- **Cross-repo specs**: defer to future dedicated "specs repo"; out-of-scope for V1.
|
||||
|
||||
## Workspace Customization & Hooks
|
||||
|
||||
Workspaces double as the unit of customization. Moving changes into `~/.config/openspec` gives us a neutral location to store **per-workspace workflow configuration** that can be projected into every attached repo alongside the manifest. Early capabilities include:
|
||||
|
||||
- **Lifecycle hooks**: Users can define shell commands or scripts (e.g., `./scripts/post-create.sh`) that OpenSpec runs after key events such as `change create`, `change apply`, `task toggle`, or `context --refresh`. Hooks let teams codify "what happens next?" right after a proposal is created—notify Slack, scaffold implementation branches, sync Linear tickets, etc.
|
||||
- **Workflow presets**: Workspace metadata (`workspace.json`) can include reusable instructions/templates that describe the expected flow (e.g., "After proposal, run design review checklist", "Before apply, execute `openspec verify --strict`"). Agents surface these reminders in their startup instructions, and humans can read them from the manifest.
|
||||
- **Composable steps**: Hooks are optional and orderable, so teams can compose bespoke workflows (e.g., `change.create` → run linting hooks, attach repo-specific instructions; `change.apply` → trigger spec sync + notify QA). V1 focuses on definition + discovery—the appendix tracks richer automation (custom task types, SDK integrations) for future versions.
|
||||
|
||||
This section keeps the RFC honest about a core benefit of workspaces: breaking the monolithic flow into smaller, customizable checkpoints without hard-coding them in every repo. Detailed research and long-term options live in the appendix’s "Future Considerations" and "Risk Analysis" tables.
|
||||
|
||||
## ASCII Flow (Agent View)
|
||||
|
||||
```
|
||||
+---------------------------+
|
||||
| ~/.config/openspec |
|
||||
| - index.json |
|
||||
| - workspaces/<ws>/... |
|
||||
+-------------+-------------+
|
||||
|
|
||||
| openspec status/context --refresh
|
||||
v
|
||||
+---------------------------+
|
||||
| repo/ |
|
||||
| - .openspec-context.json | <- lightweight shadow manifest
|
||||
| - openspec/specs/... | <- truth for specs
|
||||
| - code |
|
||||
+---------------------------+
|
||||
```
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
How agents discover and use change context:
|
||||
|
||||
1. **Discover active change**: Agent reads `.openspec-context.json` in repo root (or calls `openspec status --json`).
|
||||
- Manifest provides: `activeChangeId`, `changeTitle`, `status`, `tasks` summary, `warnings`.
|
||||
- If manifest missing or stale, agent calls `openspec context --refresh`.
|
||||
|
||||
2. **Read full change details**: Agent calls `openspec change show <changeId> --json` to get:
|
||||
- Full proposal content (`proposal.md`)
|
||||
- Complete task list with checkbox states (`tasks.md`)
|
||||
- Spec deltas (what specs will change)
|
||||
- Paths to all change artifacts in workspace store
|
||||
|
||||
3. **Update task progress**: Agent calls `openspec task toggle <changeId> <taskIndex>` or edits `tasks.md` directly via workspace path (provided in step 2).
|
||||
|
||||
4. **Resume work**: On session start, agent reads manifest → identifies incomplete tasks → continues from last known state.
|
||||
|
||||
**Fallback for non-CLI agents**: If agent cannot invoke CLI, it reads the manifest for summary context and relies on user to sync task state manually.
|
||||
|
||||
## Workflows (Before → After)
|
||||
|
||||
- **Create change**
|
||||
- *Before*: `openspec change create` writes under `repo/openspec/changes`.
|
||||
- *After*: same command writes under `~/.config/openspec/workspaces/<ws>/changes/<id>`, then projects manifest into repo.
|
||||
|
||||
- **Resume work**
|
||||
- *Before*: read `/openspec/changes/<id>` directly in repo.
|
||||
- *After*: `openspec status` (or manifest) tells active change; agent reads tasks/proposal via CLI or manifest pointers.
|
||||
|
||||
- **Apply spec update**
|
||||
- *Before*: copy deltas into `/openspec/specs` and archive change.
|
||||
- *After*: applying a change updates in-repo specs directly; no archive step. Change record stays in workspace store.
|
||||
|
||||
## CLI Impact (V1)
|
||||
|
||||
- **New/updated commands** (shape, not syntax-final):
|
||||
- `openspec context [--refresh]` → writes `.openspec-context.json`.
|
||||
- `openspec attach <repo-path>` → register repo to workspace, emit manifest.
|
||||
- `openspec status [--json]` → shows active workspace/change, drift warnings.
|
||||
- `openspec verify` → compare change store vs. repo specs/tasks, warn on drift.
|
||||
- `openspec change show <changeId> [--json]` → output full change details (proposal, tasks, spec deltas, artifact paths).
|
||||
- `openspec change apply <changeId>` → applies spec deltas to in-repo specs, updates change status to `completed`, refreshes manifest.
|
||||
- `openspec task toggle <changeId> <taskIndex>` → toggle checkbox state in `tasks.md`, refresh manifest.
|
||||
- Existing `change create/validate` operate on workspace store paths.
|
||||
|
||||
## Data Examples
|
||||
|
||||
- **Manifest sketch (`./.openspec-context.json`):**
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "1",
|
||||
"generatedAt": "2024-XX-XXT12:00:00Z",
|
||||
"repoId": "git@github.com:org/api.git@main",
|
||||
"workspaceId": "my-project",
|
||||
"branch": "feature/add-auth",
|
||||
"activeChangeId": "add-user-auth",
|
||||
"changeTitle": "Add user authentication",
|
||||
"status": "in-progress",
|
||||
"specRoots": ["openspec/specs"],
|
||||
"tasks": [
|
||||
{"title": "Define auth spec", "done": true},
|
||||
{"title": "Implement JWT middleware", "done": false}
|
||||
],
|
||||
"warnings": [],
|
||||
"sourceHash": "abc123",
|
||||
"relatedRepos": [
|
||||
{"repoId": "git@github.com:org/frontend.git@main", "role": "consumer"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
*Note: `relatedRepos` shows other repos in the same workspace affected by this change. Each repo gets its own manifest with the same `activeChangeId`.*
|
||||
|
||||
- **Workspace index (`~/.config/openspec/index.json`):**
|
||||
```json
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"id": "my-project",
|
||||
"repos": [
|
||||
{
|
||||
"repoId": "git@github.com:org/api.git@main",
|
||||
"paths": ["/Users/me/dev/api"]
|
||||
},
|
||||
{
|
||||
"repoId": "git@github.com:org/frontend.git@main",
|
||||
"paths": ["/Users/me/dev/frontend"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Migration (Happy Path)
|
||||
|
||||
- Detect existing `/openspec/changes` and prompt to migrate into the workspace store.
|
||||
- Copy change folders to `~/.config/openspec/workspaces/<ws>/changes/`.
|
||||
- Write manifests into each repo (`.openspec-context.json`) pointing to migrated changes.
|
||||
- Keep in-repo specs untouched; future changes update specs directly.
|
||||
- Optionally keep legacy layout temporarily as read-only until confident.
|
||||
|
||||
## Risks / Tradeoffs
|
||||
|
||||
- **Loss of co-location**: agents need CLI/manifest indirection; mitigated by shadow manifest and simple commands (see Agent Workflow).
|
||||
- **Portability**: local-only workspace; migration between machines requires exporting/importing the workspace folder.
|
||||
- **CI/CD access**: out-of-scope for V1 (see Non-Goals); workaround is environment override or workspace export.
|
||||
- **Drift**: specs live in-repo, changes live out-of-repo; mitigated by `openspec verify` and manifest hashes.
|
||||
- **Back-compat**: legacy `/openspec/changes` requires migration; optionally support read-only compatibility during transition.
|
||||
|
||||
## Open Questions (Post-V1)
|
||||
|
||||
- Where to store branch→change mapping (manifest vs. workspace config) and how to handle branch renames?
|
||||
- Minimal API/SDK needed for agent integrations vs. relying solely on CLI.
|
||||
- Packaging/sync of workspace store for teams (git repo? cloud bucket?).
|
||||
- How to represent cross-repo spec ownership without a dedicated specs repo?
|
||||
|
||||
## Acceptance Checklist (for reviewers)
|
||||
|
||||
- High-level model understandable via bullets + diagrams.
|
||||
- Data shapes for manifest and index are clear enough for implementation.
|
||||
- Migration story covers legacy users.
|
||||
- Risks called out with basic mitigations.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: User Flows
|
||||
|
||||
### A. Single-Repo Setup
|
||||
|
||||
```bash
|
||||
cd ~/dev/my-project
|
||||
openspec init # creates workspace, attaches current repo
|
||||
# → ~/.config/openspec/index.json updated
|
||||
# → .openspec-context.json written (git-ignored)
|
||||
# → openspec/specs/ created if missing
|
||||
```
|
||||
|
||||
That's it. User is ready to create changes.
|
||||
|
||||
---
|
||||
|
||||
### B. Multi-Repo Setup
|
||||
|
||||
```bash
|
||||
# Create workspace and attach first repo
|
||||
cd ~/dev/api
|
||||
openspec init --workspace my-platform
|
||||
|
||||
# Attach additional repos to same workspace
|
||||
cd ~/dev/frontend
|
||||
openspec attach --workspace my-platform
|
||||
|
||||
cd ~/dev/shared-types
|
||||
openspec attach --workspace my-platform
|
||||
```
|
||||
|
||||
All three repos now share `my-platform` workspace. Changes created in any repo can reference specs across all attached repos.
|
||||
|
||||
---
|
||||
|
||||
### C. Creating a Change Proposal
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CLI as openspec CLI
|
||||
participant WS as ~/.config/openspec
|
||||
participant Repo as repo/
|
||||
|
||||
User->>CLI: openspec change create "Add auth"
|
||||
CLI->>WS: Create workspaces/<ws>/changes/add-auth/
|
||||
CLI->>WS: Write proposal.md, tasks.md
|
||||
CLI->>Repo: Write .openspec-context.json
|
||||
CLI->>User: Change "add-auth" created ✓
|
||||
|
||||
Note over User,Repo: User/agent can now edit proposal.md and tasks.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### D. Applying a Change Proposal
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CLI as openspec CLI
|
||||
participant WS as ~/.config/openspec
|
||||
participant Repo as repo/
|
||||
|
||||
User->>CLI: openspec change apply add-auth
|
||||
CLI->>WS: Read changes/add-auth/specs/*.md (deltas)
|
||||
CLI->>Repo: Merge deltas into openspec/specs/
|
||||
CLI->>WS: Update change status → "completed"
|
||||
CLI->>Repo: Refresh .openspec-context.json
|
||||
CLI->>User: Specs updated, change complete ✓
|
||||
|
||||
Note over Repo: User commits spec changes + code together
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### E. After the Work is Done
|
||||
|
||||
Once a change is applied:
|
||||
|
||||
| Location | State |
|
||||
|----------|-------|
|
||||
| `repo/openspec/specs/` | Updated with new/modified specs |
|
||||
| `repo/.openspec-context.json` | `activeChangeId: null` or next change |
|
||||
| `~/.config/openspec/.../changes/add-auth/` | Retained as historical record (status: `completed`) |
|
||||
|
||||
**Next steps for user:**
|
||||
1. `git add . && git commit` — spec updates and code ship together
|
||||
2. Create PR, merge
|
||||
3. Start next change or detach repo from workspace when done
|
||||
+9
-7
@@ -17,23 +17,25 @@ export interface AIToolOption {
|
||||
}
|
||||
|
||||
export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
|
||||
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity' },
|
||||
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie' },
|
||||
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
|
||||
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
|
||||
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
|
||||
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
|
||||
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
|
||||
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
|
||||
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
|
||||
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
|
||||
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
||||
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
|
||||
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
|
||||
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
|
||||
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
|
||||
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
|
||||
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' },
|
||||
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
|
||||
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
|
||||
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
|
||||
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' },
|
||||
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode' },
|
||||
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
|
||||
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import path from "path";
|
||||
import { ToolConfigurator } from "./base.js";
|
||||
import { FileSystemUtils } from "../../utils/file-system.js";
|
||||
import { TemplateManager } from "../templates/index.js";
|
||||
import { OPENSPEC_MARKERS } from "../config.js";
|
||||
|
||||
export class IflowConfigurator implements ToolConfigurator {
|
||||
name = "iFlow";
|
||||
configFileName = "IFLOW.md";
|
||||
isAvailable = true;
|
||||
|
||||
async configure(projectPath: string, openspecDir: string): Promise<void> {
|
||||
const filePath = path.join(projectPath, this.configFileName);
|
||||
const content = TemplateManager.getClaudeTemplate();
|
||||
|
||||
await FileSystemUtils.updateFileWithMarkers(
|
||||
filePath,
|
||||
content,
|
||||
OPENSPEC_MARKERS.start,
|
||||
OPENSPEC_MARKERS.end
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ClineConfigurator } from './cline.js';
|
||||
import { CodeBuddyConfigurator } from './codebuddy.js';
|
||||
import { CostrictConfigurator } from './costrict.js';
|
||||
import { QoderConfigurator } from './qoder.js';
|
||||
import { IflowConfigurator } from './iflow.js';
|
||||
import { AgentsStandardConfigurator } from './agents.js';
|
||||
import { QwenConfigurator } from './qwen.js';
|
||||
|
||||
@@ -16,6 +17,7 @@ export class ToolRegistry {
|
||||
const codeBuddyConfigurator = new CodeBuddyConfigurator();
|
||||
const costrictConfigurator = new CostrictConfigurator();
|
||||
const qoderConfigurator = new QoderConfigurator();
|
||||
const iflowConfigurator = new IflowConfigurator();
|
||||
const agentsConfigurator = new AgentsStandardConfigurator();
|
||||
const qwenConfigurator = new QwenConfigurator();
|
||||
// Register with the ID that matches the checkbox value
|
||||
@@ -24,6 +26,7 @@ export class ToolRegistry {
|
||||
this.tools.set('codebuddy', codeBuddyConfigurator);
|
||||
this.tools.set('costrict', costrictConfigurator);
|
||||
this.tools.set('qoder', qoderConfigurator);
|
||||
this.tools.set('iflow', iflowConfigurator);
|
||||
this.tools.set('agents', agentsConfigurator);
|
||||
this.tools.set('qwen', qwenConfigurator);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.agent/workflows/openspec-proposal.md',
|
||||
apply: '.agent/workflows/openspec-apply.md',
|
||||
archive: '.agent/workflows/openspec-archive.md'
|
||||
};
|
||||
|
||||
const DESCRIPTIONS: Record<SlashCommandId, string> = {
|
||||
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
|
||||
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
|
||||
export class AntigravitySlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'antigravity';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string | undefined {
|
||||
const description = DESCRIPTIONS[id];
|
||||
return `---\ndescription: ${description}\n---`;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { FileSystemUtils } from '../../../utils/file-system.js';
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId, TemplateManager } from '../../templates/index.js';
|
||||
import { OPENSPEC_MARKERS } from '../../config.js';
|
||||
import { TomlSlashCommandConfigurator } from './toml-base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.gemini/commands/openspec/proposal.toml',
|
||||
@@ -15,7 +13,7 @@ const DESCRIPTIONS: Record<SlashCommandId, string> = {
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
|
||||
export class GeminiSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
export class GeminiSlashCommandConfigurator extends TomlSlashCommandConfigurator {
|
||||
readonly toolId = 'gemini';
|
||||
readonly isAvailable = true;
|
||||
|
||||
@@ -23,61 +21,7 @@ export class GeminiSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(_id: SlashCommandId): string | undefined {
|
||||
// TOML doesn't use separate frontmatter - it's all in one structure
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Override to generate TOML format with markers inside the prompt field
|
||||
async generateAll(projectPath: string, _openspecDir: string): Promise<string[]> {
|
||||
const createdOrUpdated: string[] = [];
|
||||
|
||||
for (const target of this.getTargets()) {
|
||||
const body = this.getBody(target.id);
|
||||
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
|
||||
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
await this.updateBody(filePath, body);
|
||||
} else {
|
||||
const tomlContent = this.generateTOML(target.id, body);
|
||||
await FileSystemUtils.writeFile(filePath, tomlContent);
|
||||
}
|
||||
|
||||
createdOrUpdated.push(target.path);
|
||||
}
|
||||
|
||||
return createdOrUpdated;
|
||||
}
|
||||
|
||||
private generateTOML(id: SlashCommandId, body: string): string {
|
||||
const description = DESCRIPTIONS[id];
|
||||
|
||||
// TOML format with triple-quoted string for multi-line prompt
|
||||
// Markers are inside the prompt value
|
||||
return `description = "${description}"
|
||||
|
||||
prompt = """
|
||||
${OPENSPEC_MARKERS.start}
|
||||
${body}
|
||||
${OPENSPEC_MARKERS.end}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
// Override updateBody to handle TOML format
|
||||
protected async updateBody(filePath: string, body: string): Promise<void> {
|
||||
const content = await FileSystemUtils.readFile(filePath);
|
||||
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
|
||||
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error(`Missing OpenSpec markers in ${filePath}`);
|
||||
}
|
||||
|
||||
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
|
||||
const after = content.slice(endIndex);
|
||||
const updatedContent = `${before}\n${body}\n${after}`;
|
||||
|
||||
await FileSystemUtils.writeFile(filePath, updatedContent);
|
||||
protected getDescription(id: SlashCommandId): string {
|
||||
return DESCRIPTIONS[id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.iflow/commands/openspec-proposal.md',
|
||||
apply: '.iflow/commands/openspec-apply.md',
|
||||
archive: '.iflow/commands/openspec-archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
name: /openspec-proposal
|
||||
id: openspec-proposal
|
||||
category: OpenSpec
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---`,
|
||||
apply: `---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---`,
|
||||
archive: `---
|
||||
name: /openspec-archive
|
||||
id: openspec-archive
|
||||
category: OpenSpec
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---`
|
||||
};
|
||||
|
||||
export class IflowSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'iflow';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
agent: build
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---
|
||||
The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
|
||||
@@ -20,7 +19,6 @@ The user has requested the following change proposal. Use the openspec instructi
|
||||
</UserRequest>
|
||||
`,
|
||||
apply: `---
|
||||
agent: build
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
The user has requested to implement the following change proposal. Find the change proposal and follow the instructions below. If you're not sure or if ambiguous, ask for clarification from the user.
|
||||
@@ -29,7 +27,6 @@ The user has requested to implement the following change proposal. Find the chan
|
||||
</UserRequest>
|
||||
`,
|
||||
archive: `---
|
||||
agent: build
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---
|
||||
<ChangeId>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* @implements {SlashCommandConfigurator}
|
||||
*/
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { TomlSlashCommandConfigurator } from './toml-base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
/**
|
||||
@@ -13,35 +13,15 @@ import { SlashCommandId } from '../../templates/index.js';
|
||||
* @type {Record<SlashCommandId, string>}
|
||||
*/
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.qwen/commands/openspec-proposal.md',
|
||||
apply: '.qwen/commands/openspec-apply.md',
|
||||
archive: '.qwen/commands/openspec-archive.md'
|
||||
proposal: '.qwen/commands/openspec-proposal.toml',
|
||||
apply: '.qwen/commands/openspec-apply.toml',
|
||||
archive: '.qwen/commands/openspec-archive.toml'
|
||||
};
|
||||
|
||||
/**
|
||||
* YAML frontmatter definitions for Qwen command files.
|
||||
* These provide metadata for each slash command to ensure proper recognition by Qwen Code.
|
||||
* @type {Record<SlashCommandId, string>}
|
||||
*/
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
name: /openspec-proposal
|
||||
id: openspec-proposal
|
||||
category: OpenSpec
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---`,
|
||||
apply: `---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---`,
|
||||
archive: `---
|
||||
name: /openspec-archive
|
||||
id: openspec-archive
|
||||
category: OpenSpec
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---`
|
||||
const DESCRIPTIONS: Record<SlashCommandId, string> = {
|
||||
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
|
||||
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
|
||||
archive: 'Archive a deployed OpenSpec change and update specs.'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -53,10 +33,10 @@ description: Archive a deployed OpenSpec change and update specs.
|
||||
* - /openspec-apply: Apply an approved OpenSpec change
|
||||
* - /openspec-archive: Archive a deployed OpenSpec change
|
||||
*/
|
||||
export class QwenSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
export class QwenSlashCommandConfigurator extends TomlSlashCommandConfigurator {
|
||||
/** Unique identifier for the Qwen tool */
|
||||
readonly toolId = 'qwen';
|
||||
|
||||
|
||||
/** Availability status for the Qwen tool */
|
||||
readonly isAvailable = true;
|
||||
|
||||
@@ -69,12 +49,7 @@ export class QwenSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the YAML frontmatter for a given slash command ID.
|
||||
* @param {SlashCommandId} id - The slash command identifier
|
||||
* @returns {string} The YAML frontmatter string
|
||||
*/
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
protected getDescription(id: SlashCommandId): string {
|
||||
return DESCRIPTIONS[id];
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import { CrushSlashCommandConfigurator } from './crush.js';
|
||||
import { CostrictSlashCommandConfigurator } from './costrict.js';
|
||||
import { QwenSlashCommandConfigurator } from './qwen.js';
|
||||
import { RooCodeSlashCommandConfigurator } from './roocode.js';
|
||||
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
|
||||
import { IflowSlashCommandConfigurator } from './iflow.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
@@ -40,6 +42,8 @@ export class SlashCommandRegistry {
|
||||
const costrict = new CostrictSlashCommandConfigurator();
|
||||
const qwen = new QwenSlashCommandConfigurator();
|
||||
const roocode = new RooCodeSlashCommandConfigurator();
|
||||
const antigravity = new AntigravitySlashCommandConfigurator();
|
||||
const iflow = new IflowSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(codeBuddy.toolId, codeBuddy);
|
||||
@@ -59,6 +63,8 @@ export class SlashCommandRegistry {
|
||||
this.configurators.set(costrict.toolId, costrict);
|
||||
this.configurators.set(qwen.toolId, qwen);
|
||||
this.configurators.set(roocode.toolId, roocode);
|
||||
this.configurators.set(antigravity.toolId, antigravity);
|
||||
this.configurators.set(iflow.toolId, iflow);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { FileSystemUtils } from '../../../utils/file-system.js';
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
import { OPENSPEC_MARKERS } from '../../config.js';
|
||||
|
||||
export abstract class TomlSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
protected getFrontmatter(_id: SlashCommandId): string | undefined {
|
||||
// TOML doesn't use separate frontmatter - it's all in one structure
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected abstract getDescription(id: SlashCommandId): string;
|
||||
|
||||
// Override to generate TOML format with markers inside the prompt field
|
||||
async generateAll(projectPath: string, _openspecDir: string): Promise<string[]> {
|
||||
const createdOrUpdated: string[] = [];
|
||||
|
||||
for (const target of this.getTargets()) {
|
||||
const body = this.getBody(target.id);
|
||||
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
|
||||
|
||||
if (await FileSystemUtils.fileExists(filePath)) {
|
||||
await this.updateBody(filePath, body);
|
||||
} else {
|
||||
const tomlContent = this.generateTOML(target.id, body);
|
||||
await FileSystemUtils.writeFile(filePath, tomlContent);
|
||||
}
|
||||
|
||||
createdOrUpdated.push(target.path);
|
||||
}
|
||||
|
||||
return createdOrUpdated;
|
||||
}
|
||||
|
||||
private generateTOML(id: SlashCommandId, body: string): string {
|
||||
const description = this.getDescription(id);
|
||||
|
||||
// TOML format with triple-quoted string for multi-line prompt
|
||||
// Markers are inside the prompt value
|
||||
return `description = "${description}"
|
||||
|
||||
prompt = """
|
||||
${OPENSPEC_MARKERS.start}
|
||||
${body}
|
||||
${OPENSPEC_MARKERS.end}
|
||||
"""
|
||||
`;
|
||||
}
|
||||
|
||||
// Override updateBody to handle TOML format
|
||||
protected async updateBody(filePath: string, body: string): Promise<void> {
|
||||
const content = await FileSystemUtils.readFile(filePath);
|
||||
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
|
||||
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
||||
throw new Error(`Missing OpenSpec markers in ${filePath}`);
|
||||
}
|
||||
|
||||
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
|
||||
const after = content.slice(endIndex);
|
||||
const updatedContent = `${before}\n${body}\n${after}`;
|
||||
|
||||
await FileSystemUtils.writeFile(filePath, updatedContent);
|
||||
}
|
||||
}
|
||||
@@ -862,6 +862,22 @@ export class InitCommand {
|
||||
)
|
||||
);
|
||||
|
||||
// Show restart instruction if any tools were configured
|
||||
if (created.length > 0 || refreshed.length > 0) {
|
||||
console.log();
|
||||
console.log(PALETTE.white('Important: Restart your IDE'));
|
||||
console.log(
|
||||
PALETTE.midGray(
|
||||
'Slash commands are loaded at startup. Please restart your coding assistant'
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
PALETTE.midGray(
|
||||
'to ensure the new /openspec commands appear in your command palette.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Get the selected tool name(s) for display
|
||||
const toolName = this.formatToolNames(selectedTools);
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const baseGuardrails = `**Guardrails**
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to \`openspec/AGENTS.md\` (located inside the \`openspec/\` directory—run \`ls openspec\` or \`openspec update\` if you don't see it) if you need additional OpenSpec conventions or clarifications.`;
|
||||
|
||||
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.`;
|
||||
const proposalGuardrails = `${baseGuardrails}\n- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.`;
|
||||
|
||||
const proposalSteps = `**Steps**
|
||||
1. Review \`openspec/project.md\`, run \`openspec list\` and \`openspec list --specs\`, and inspect related code or docs (e.g., via \`rg\`/\`ls\`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||
@@ -16,6 +17,7 @@ const proposalSteps = `**Steps**
|
||||
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.`;
|
||||
|
||||
|
||||
const proposalReferences = `**Reference**
|
||||
- Use \`openspec show <id> --json --deltas-only\` or \`openspec show <spec> --type spec\` to inspect details when validation fails.
|
||||
- Search existing requirements with \`rg -n "Requirement:|Scenario:" openspec/specs\` before writing new ones.
|
||||
|
||||
+120
-16
@@ -50,7 +50,7 @@ describe('InitCommand', () => {
|
||||
process.env.CODEX_HOME = path.join(testDir, '.codex');
|
||||
|
||||
// Mock console.log to suppress output during tests
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'log').mockImplementation(() => { });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -213,6 +213,50 @@ describe('InitCommand', () => {
|
||||
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
|
||||
});
|
||||
|
||||
it('should create Antigravity workflows when Antigravity is selected', async () => {
|
||||
queueSelections('antigravity', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const agProposal = path.join(
|
||||
testDir,
|
||||
'.agent/workflows/openspec-proposal.md'
|
||||
);
|
||||
const agApply = path.join(
|
||||
testDir,
|
||||
'.agent/workflows/openspec-apply.md'
|
||||
);
|
||||
const agArchive = path.join(
|
||||
testDir,
|
||||
'.agent/workflows/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(agProposal)).toBe(true);
|
||||
expect(await fileExists(agApply)).toBe(true);
|
||||
expect(await fileExists(agArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(agProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
expect(proposalContent).not.toContain('auto_execution_mode');
|
||||
|
||||
const applyContent = await fs.readFile(agApply, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
expect(applyContent).not.toContain('auto_execution_mode');
|
||||
|
||||
const archiveContent = await fs.readFile(agArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
|
||||
expect(archiveContent).not.toContain('auto_execution_mode');
|
||||
});
|
||||
|
||||
it('should always create AGENTS.md in project root', async () => {
|
||||
queueSelections(DONE);
|
||||
|
||||
@@ -372,6 +416,59 @@ describe('InitCommand', () => {
|
||||
expect(updatedContent).not.toContain('Custom instruction added by user');
|
||||
});
|
||||
|
||||
it('should create IFlow CLI slash command files with templates', async () => {
|
||||
queueSelections('iflow', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const iflowProposal = path.join(
|
||||
testDir,
|
||||
'.iflow/commands/openspec-proposal.md'
|
||||
);
|
||||
const iflowApply = path.join(
|
||||
testDir,
|
||||
'.iflow/commands/openspec-apply.md'
|
||||
);
|
||||
const iflowArchive = path.join(
|
||||
testDir,
|
||||
'.iflow/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(iflowProposal)).toBe(true);
|
||||
expect(await fileExists(iflowApply)).toBe(true);
|
||||
expect(await fileExists(iflowArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(iflowProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
|
||||
|
||||
const applyContent = await fs.readFile(iflowApply, 'utf-8');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(iflowArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('openspec archive <id>');
|
||||
});
|
||||
|
||||
it('should update existing IFLOW.md with markers', async () => {
|
||||
queueSelections('iflow', DONE);
|
||||
|
||||
const iflowPath = path.join(testDir, 'IFLOW.md');
|
||||
const existingContent = '# My IFLOW Instructions\nCustom instructions here';
|
||||
await fs.writeFile(iflowPath, existingContent);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const updatedContent = await fs.readFile(iflowPath, 'utf-8');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
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 OpenCode slash command files with templates', async () => {
|
||||
queueSelections('opencode', DONE);
|
||||
|
||||
@@ -395,21 +492,21 @@ describe('InitCommand', () => {
|
||||
expect(await fileExists(openCodeArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(openCodeProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('agent: build');
|
||||
expect(proposalContent).not.toContain('agent:');
|
||||
expect(proposalContent).toContain(
|
||||
'description: Scaffold a new OpenSpec change and validate strictly.'
|
||||
);
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
|
||||
const applyContent = await fs.readFile(openCodeApply, 'utf-8');
|
||||
expect(applyContent).toContain('agent: build');
|
||||
expect(applyContent).not.toContain('agent:');
|
||||
expect(applyContent).toContain(
|
||||
'description: Implement an approved OpenSpec change and keep tasks in sync.'
|
||||
);
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(openCodeArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('agent: build');
|
||||
expect(archiveContent).not.toContain('agent:');
|
||||
expect(archiveContent).toContain(
|
||||
'description: Archive a deployed OpenSpec change and update specs.'
|
||||
);
|
||||
@@ -424,15 +521,15 @@ describe('InitCommand', () => {
|
||||
const qwenConfigPath = path.join(testDir, 'QWEN.md');
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-proposal.md'
|
||||
'.qwen/commands/openspec-proposal.toml'
|
||||
);
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-apply.md'
|
||||
'.qwen/commands/openspec-apply.toml'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-archive.md'
|
||||
'.qwen/commands/openspec-archive.toml'
|
||||
);
|
||||
|
||||
expect(await fileExists(qwenConfigPath)).toBe(true);
|
||||
@@ -446,21 +543,16 @@ describe('InitCommand', () => {
|
||||
expect(qwenConfigContent).toContain('<!-- OPENSPEC:END -->');
|
||||
|
||||
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(proposalContent).toContain('name: /openspec-proposal');
|
||||
expect(proposalContent).toContain('category: OpenSpec');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('description = "Scaffold a new OpenSpec change and validate strictly."');
|
||||
expect(proposalContent).toContain('prompt = """');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('name: /openspec-apply');
|
||||
expect(applyContent).toContain('category: OpenSpec');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('description = "Implement an approved OpenSpec change and keep tasks in sync."');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(archivePath, 'utf-8');
|
||||
expect(archiveContent).toContain('name: /openspec-archive');
|
||||
expect(archiveContent).toContain('category: OpenSpec');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('description = "Archive a deployed OpenSpec change and update specs."');
|
||||
expect(archiveContent).toContain('openspec archive <id>');
|
||||
});
|
||||
|
||||
@@ -854,6 +946,18 @@ describe('InitCommand', () => {
|
||||
expect(wsChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark Antigravity as already configured during extend mode', async () => {
|
||||
queueSelections('antigravity', DONE, 'antigravity', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const antigravityChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'antigravity'
|
||||
);
|
||||
expect(antigravityChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark Codex as already configured during extend mode', async () => {
|
||||
queueSelections('codex', DONE, 'codex', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
+96
-21
@@ -152,19 +152,17 @@ Old slash content
|
||||
it('should refresh existing Qwen slash command files', async () => {
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-apply.md'
|
||||
'.qwen/commands/openspec-apply.toml'
|
||||
);
|
||||
await fs.mkdir(path.dirname(applyPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Old description
|
||||
---
|
||||
const initialContent = `description = "Implement an approved OpenSpec change and keep tasks in sync."
|
||||
|
||||
prompt = """
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
<!-- OPENSPEC:END -->
|
||||
"""
|
||||
`;
|
||||
await fs.writeFile(applyPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
@@ -172,8 +170,8 @@ Old body
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(updated).toContain('name: /openspec-apply');
|
||||
expect(updated).toContain('category: OpenSpec');
|
||||
expect(updated).toContain('description = "Implement an approved OpenSpec change and keep tasks in sync."');
|
||||
expect(updated).toContain('prompt = """');
|
||||
expect(updated).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
@@ -184,7 +182,7 @@ Old body
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .qwen/commands/openspec-apply.md'
|
||||
'Updated slash commands: .qwen/commands/openspec-apply.toml'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
@@ -193,22 +191,20 @@ Old body
|
||||
it('should not create missing Qwen slash command files on update', async () => {
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-apply.md'
|
||||
'.qwen/commands/openspec-apply.toml'
|
||||
);
|
||||
|
||||
await fs.mkdir(path.dirname(applyPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
applyPath,
|
||||
`---
|
||||
name: /openspec-apply
|
||||
id: openspec-apply
|
||||
category: OpenSpec
|
||||
description: Old description
|
||||
---
|
||||
`description = "Old description"
|
||||
|
||||
prompt = """
|
||||
<!-- OPENSPEC:START -->
|
||||
Old content
|
||||
<!-- OPENSPEC:END -->`
|
||||
<!-- OPENSPEC:END -->
|
||||
"""
|
||||
`
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
@@ -219,11 +215,11 @@ Old content
|
||||
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-proposal.md'
|
||||
'.qwen/commands/openspec-proposal.toml'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.qwen/commands/openspec-archive.md'
|
||||
'.qwen/commands/openspec-archive.toml'
|
||||
);
|
||||
|
||||
await expect(FileSystemUtils.fileExists(proposalPath)).resolves.toBe(false);
|
||||
@@ -467,6 +463,38 @@ Old body
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Antigravity workflows', async () => {
|
||||
const agPath = path.join(
|
||||
testDir,
|
||||
'.agent/workflows/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(agPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(agPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(agPath, 'utf-8');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(updated).not.toContain('auto_execution_mode: 3');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .agent/workflows/openspec-apply.md'
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Codex prompts', async () => {
|
||||
const codexPath = path.join(
|
||||
testDir,
|
||||
@@ -635,6 +663,53 @@ Old Gemini body
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing IFLOW slash commands', async () => {
|
||||
const iflowProposal = path.join(
|
||||
testDir,
|
||||
'.iflow/commands/openspec-proposal.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(iflowProposal), { recursive: true });
|
||||
const initialContent = `description: Scaffold a new OpenSpec change and validate strictly."
|
||||
|
||||
prompt = """
|
||||
<!-- OPENSPEC:START -->
|
||||
Old IFlow body
|
||||
<!-- OPENSPEC:END -->
|
||||
"""
|
||||
`;
|
||||
await fs.writeFile(iflowProposal, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(iflowProposal, 'utf-8');
|
||||
expect(updated).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(updated).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updated).toContain('**Guardrails**');
|
||||
expect(updated).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updated).not.toContain('Old IFlow body');
|
||||
|
||||
const iflowApply = path.join(
|
||||
testDir,
|
||||
'.iflow/commands/openspec-apply.md'
|
||||
);
|
||||
const iflowArchive = path.join(
|
||||
testDir,
|
||||
'.iflow/commands/openspec-archive.md'
|
||||
);
|
||||
|
||||
await expect(FileSystemUtils.fileExists(iflowApply)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(iflowArchive)).resolves.toBe(false);
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .iflow/commands/openspec-proposal.md'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Factory slash commands', async () => {
|
||||
const factoryPath = path.join(
|
||||
|
||||
Reference in New Issue
Block a user