mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f9d39c327 | ||
|
|
c9bc915f62 |
@@ -1,2 +0,0 @@
|
||||
---
|
||||
---
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": patch
|
||||
---
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **CLI path visibility**: OpenSpec now documents editor and agent PATH mismatches, warns during global installs when the detected CLI bin directory is not on PATH, and generates workflow skills with guidance for resolving `openspec` through `OPENSPEC_BIN` or an absolute path.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
### New Features
|
||||
|
||||
- **Kimi CLI support** — OpenSpec can now initialize Kimi CLI as a supported skills-only tool using `.kimi/skills/`
|
||||
|
||||
### Other
|
||||
|
||||
- Added Kimi-specific docs and init coverage aligned with skill-based `/skill:openspec-*` usage
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
### New Features
|
||||
|
||||
- Include the sync workflow in the default core profile so new installs generate `/opsx:sync` skills and commands by default.
|
||||
@@ -3,8 +3,6 @@ name: CI
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
merge_group:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
@@ -44,7 +42,7 @@ jobs:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -83,7 +81,7 @@ jobs:
|
||||
name: Test (${{ matrix.label }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -244,7 +242,7 @@ jobs:
|
||||
validate-changesets:
|
||||
name: Validate Changesets
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -277,7 +275,7 @@ jobs:
|
||||
name: All checks passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_pr, lint, nix-flake-validate]
|
||||
if: always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group')
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Verify all checks passed
|
||||
run: |
|
||||
@@ -303,7 +301,7 @@ jobs:
|
||||
name: All checks passed
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test_matrix, lint, nix-flake-validate]
|
||||
if: always() && github.event_name == 'push'
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Verify all checks passed
|
||||
run: |
|
||||
|
||||
@@ -153,9 +153,3 @@ result
|
||||
# OpenCode
|
||||
.opencode/
|
||||
opencode.json
|
||||
|
||||
# Codex
|
||||
.codex/
|
||||
|
||||
# Bob
|
||||
.bob/
|
||||
|
||||
@@ -1,60 +1,5 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 1.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#995](https://github.com/Fission-AI/OpenSpec/pull/995) [`d1f3861`](https://github.com/Fission-AI/OpenSpec/commit/d1f3861d9ec694cc924b042b5da01963dcf93137) Thanks [@TabishB](https://github.com/TabishB)! - ### Bug Fixes
|
||||
|
||||
- **Canonical artifact paths** — Workflow artifact paths are now resolved via the native `realpath`, so symlinks and case-insensitive filesystems no longer cause path mismatches during apply and archive.
|
||||
- **Glob apply instructions** — Apply instructions with glob artifact outputs now resolve correctly, and literal artifact outputs are enforced to be file paths.
|
||||
- **Hidden main spec requirements** — Requirements nested inside fenced code blocks or otherwise hidden in main specs are now detected during validation.
|
||||
- **Clean `--json` output** — Spinner progress text no longer leaks into stderr when `--json` is passed, so AI agents that combine stdout and stderr can parse the JSON reliably.
|
||||
- **Silent telemetry in firewalled environments** — PostHog network errors are now swallowed with a 1s timeout and retries/remote config disabled, so OpenSpec no longer surfaces `PostHogFetchNetworkError` in locked-down networks. Telemetry opt-out is documented earlier in the README, installation guide, and CLI reference.
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#952](https://github.com/Fission-AI/OpenSpec/pull/952) [`cce787e`](https://github.com/Fission-AI/OpenSpec/commit/cce787ec4083da2b27781f6786f5ce0002909a7b) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
|
||||
|
||||
- **Junie support** — Added tool and command generation for JetBrains Junie
|
||||
- **Lingma IDE support** — Added configuration support for Lingma IDE
|
||||
- **ForgeCode support** — Added tool support for ForgeCode
|
||||
- **IBM Bob support** — Added support for IBM Bob coding assistant
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Shell completions opt-in** — Completion install is now opt-in, fixing PowerShell encoding corruption
|
||||
- **Copilot auto-detection** — Prevented false GitHub Copilot detection from a bare `.github/` directory
|
||||
- **pi.dev command generation** — Fixed command reference transforms and template argument passing
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#760](https://github.com/Fission-AI/OpenSpec/pull/760) [`61eb999`](https://github.com/Fission-AI/OpenSpec/commit/61eb999f7c6c0fc98d2e7f3678756fce6a3f4378) Thanks [@fsilvaortiz](https://github.com/fsilvaortiz)! - fix: OpenCode adapter now uses `.opencode/commands/` (plural) to match OpenCode's official directory convention. Fixes #748.
|
||||
|
||||
- [#759](https://github.com/Fission-AI/OpenSpec/pull/759) [`afdca0d`](https://github.com/Fission-AI/OpenSpec/commit/afdca0d5dab1aa109cfd8848b2512333ccad60c3) Thanks [@fsilvaortiz](https://github.com/fsilvaortiz)! - fix: `openspec status` now exits gracefully when no changes exist instead of throwing a fatal error. Fixes #714.
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#747](https://github.com/Fission-AI/OpenSpec/pull/747) [`1e94443`](https://github.com/Fission-AI/OpenSpec/commit/1e94443a3551b228eecbc89e95d96d3b9600a192) Thanks [@TabishB](https://github.com/TabishB)! - ### New Features
|
||||
|
||||
- **Profile system** — Choose between `core` (4 essential workflows) and `custom` (pick any subset) profiles to control which skills get installed. Manage profiles with the new `openspec config profile` command
|
||||
- **Propose workflow** — New one-step workflow creates a complete change proposal with design, specs, and tasks from a single request — no need to run `new` then `ff` separately
|
||||
- **AI tool auto-detection** — `openspec init` now scans your project for existing tool directories (`.claude/`, `.cursor/`, etc.) and pre-selects detected tools
|
||||
- **Pi (pi.dev) support** — Pi coding agent is now a supported tool with prompt and skill generation
|
||||
- **Kiro support** — AWS Kiro IDE is now a supported tool with prompt and skill generation
|
||||
- **Sync prunes deselected workflows** — `openspec update` now removes command files and skill directories for workflows you've deselected, keeping your project clean
|
||||
- **Config drift warning** — `openspec config list` warns when global config is out of sync with the current project
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed onboard preflight giving a false "not initialized" error on freshly initialized projects
|
||||
- Fixed archive workflow stopping mid-way when syncing — it now properly resumes after sync completes
|
||||
- Added Windows PowerShell alternatives for onboard shell commands
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -36,20 +36,27 @@ Our philosophy:
|
||||
> [!TIP]
|
||||
> **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
|
||||
>
|
||||
> Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
|
||||
> Run `/opsx:onboard` to get started. → [Learn more here](docs/opsx.md)
|
||||
|
||||
<p align="center">
|
||||
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
|
||||
</p>
|
||||
|
||||
<!-- TODO: Add GIF demo of /opsx:propose → /opsx:archive workflow -->
|
||||
### Teams
|
||||
|
||||
Using OpenSpec in a team? [Email here](mailto:teams@openspec.dev) for access to our Slack channel.
|
||||
|
||||
<!-- TODO: Add GIF demo of /opsx:new → /opsx:archive workflow -->
|
||||
|
||||
## See it in action
|
||||
|
||||
```text
|
||||
You: /opsx:propose add-dark-mode
|
||||
You: /opsx:new add-dark-mode
|
||||
AI: Created openspec/changes/add-dark-mode/
|
||||
✓ proposal.md — why we're doing this, what's changing
|
||||
Ready to create: proposal
|
||||
|
||||
You: /opsx:ff # "fast-forward" - generate all planning docs
|
||||
AI: ✓ proposal.md — why we're doing this, what's changing
|
||||
✓ specs/ — requirements and scenarios
|
||||
✓ design.md — technical approach
|
||||
✓ tasks.md — implementation checklist
|
||||
@@ -94,12 +101,10 @@ cd your-project
|
||||
openspec init
|
||||
```
|
||||
|
||||
Now tell your AI: `/opsx:propose <what-you-want-to-build>`
|
||||
|
||||
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
|
||||
Now tell your AI: `/opsx:new <what-you-want-to-build>`
|
||||
|
||||
> [!NOTE]
|
||||
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
|
||||
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
|
||||
>
|
||||
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
|
||||
|
||||
@@ -115,13 +120,6 @@ If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/
|
||||
→ **[Customization](docs/customization.md)**: make it yours
|
||||
|
||||
|
||||
## Community schemas
|
||||
|
||||
Third-party schema bundles distributed via standalone repositories — these provide opinionated workflows that integrate OpenSpec with other tools, similar to how [github/spec-kit's community extension catalog](https://github.com/github/spec-kit/tree/main/extensions) handles tool integrations.
|
||||
|
||||
→ **[Browse the catalog](docs/customization.md#community-schemas)** in the customization docs.
|
||||
|
||||
|
||||
## Why OpenSpec?
|
||||
|
||||
AI coding assistants are powerful but unpredictable when requirements live only in chat history. OpenSpec adds a lightweight spec layer so you agree on what to build before any code is written.
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
# Workspace Reimplementation Direction
|
||||
|
||||
Date: 2026-04-30
|
||||
|
||||
Fresh-agent entry point: read `WORKSPACE_REIMPLEMENTATION_START_HERE.md` first, then return to this document for the full product direction.
|
||||
|
||||
This document captures the intended direction for reimplementing OpenSpec workspace support from scratch, based on what we learned from the workspace POC.
|
||||
|
||||
The reimplementation should be ordered around the path a real user takes through OpenSpec:
|
||||
|
||||
```text
|
||||
set up workspace
|
||||
-> link repos or folders
|
||||
-> open workspace
|
||||
-> explore across repos or folders
|
||||
-> create proposal
|
||||
-> apply one repo slice
|
||||
-> verify
|
||||
-> archive
|
||||
```
|
||||
|
||||
The goal is not to rebuild every POC mechanism. The goal is to get one user-facing capability working at a time, in the same order a user would naturally create, implement, verify, and archive a change.
|
||||
|
||||
## North Star
|
||||
|
||||
A user should think:
|
||||
|
||||
```text
|
||||
I have a multi-repo product goal.
|
||||
I set up an OpenSpec workspace.
|
||||
I open it with my agent.
|
||||
The agent can see the linked repos or folders.
|
||||
We explore until the scope is clear.
|
||||
Then we create a proposal.
|
||||
Then we implement one repo slice at a time.
|
||||
```
|
||||
|
||||
They should not think:
|
||||
|
||||
```text
|
||||
I need to create a change so repos become visible.
|
||||
I need to materialize repo-local artifacts.
|
||||
I need to understand implementation-specific workspace machinery.
|
||||
I need to manage target metadata separately from proposal files.
|
||||
```
|
||||
|
||||
The core product rule is:
|
||||
|
||||
```text
|
||||
Workspace visibility is not change commitment.
|
||||
```
|
||||
|
||||
Linked repos or folders are planning context. Creating a change is a planning commitment. Applying a change is an implementation workflow.
|
||||
|
||||
## Build Order
|
||||
|
||||
### 1. Workspace Setup And Links
|
||||
|
||||
First make workspace setup boring and solid.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Create a planning home and link the repos or folders OpenSpec should know about.
|
||||
```
|
||||
|
||||
Expected surface:
|
||||
|
||||
```bash
|
||||
openspec workspace setup
|
||||
openspec workspace setup --no-interactive --name platform --link /path/to/api --link web=/path/to/web
|
||||
openspec workspace list
|
||||
openspec workspace ls
|
||||
openspec workspace link /path/to/api
|
||||
openspec workspace link api-service /path/to/api
|
||||
openspec workspace relink api /new/path/to/api
|
||||
openspec workspace doctor
|
||||
```
|
||||
|
||||
Expected outcome:
|
||||
|
||||
```text
|
||||
workspace-folder/
|
||||
changes/
|
||||
.openspec-workspace/
|
||||
workspace.yaml
|
||||
local.yaml
|
||||
```
|
||||
|
||||
Product decisions:
|
||||
|
||||
- Use `.openspec-workspace/`, not `.openspec/`, for workspace metadata.
|
||||
- Keep `changes/` visible in the workspace folder.
|
||||
- Keep setup as the only public creation path for the first release; do not expose `workspace create`.
|
||||
- Use `workspace link` and `workspace relink`, not POC-era `add-repo` or `update-repo`.
|
||||
- Allow linked repos or folders without repo-local `openspec/` state.
|
||||
- Keep stable link names in shared workspace state and local paths in machine-local state.
|
||||
- Make `doctor` show link names, resolved paths, repo-local specs paths when present, and suggested fixes.
|
||||
|
||||
Defer:
|
||||
|
||||
- Agent launch and workspace open behavior.
|
||||
- Preferred-agent prompts.
|
||||
- Owner or handoff metadata.
|
||||
- Workspace change creation or target selection.
|
||||
- Branches.
|
||||
- Worktrees.
|
||||
- Apply.
|
||||
- Archive.
|
||||
- Complex target lifecycle.
|
||||
|
||||
Done when a user can set up a workspace, link repos or folders, list known workspaces, relink local paths, and run `doctor` to see exactly what OpenSpec can resolve.
|
||||
|
||||
### 2. Workspace Open
|
||||
|
||||
Next make the workspace openable in the way users expect.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Open this multi-repo planning context with my coding agent.
|
||||
```
|
||||
|
||||
Expected surface:
|
||||
|
||||
```bash
|
||||
openspec workspace open
|
||||
openspec workspace open --agent codex
|
||||
openspec workspace open --agent github-copilot
|
||||
```
|
||||
|
||||
Product behavior:
|
||||
|
||||
- `workspace open` opens the coordination workspace plus linked repos or folders.
|
||||
- Repo visibility is default.
|
||||
- Change selection is optional focus, not the mechanism for repo access.
|
||||
- `--agent` should be a one-session override by default. Persisting the preferred agent should require an explicit preference-setting action.
|
||||
|
||||
For GitHub Copilot, generate or open a `.code-workspace` file with:
|
||||
|
||||
```text
|
||||
workspace folder
|
||||
linked repo or folder A
|
||||
linked repo or folder B
|
||||
```
|
||||
|
||||
For Claude and Codex, attach the linked repo or folder directories through the agent's supported mechanism.
|
||||
|
||||
Defer:
|
||||
|
||||
- `workspace open --change`.
|
||||
- In-session upgrade flows.
|
||||
- Per-change attachment restrictions.
|
||||
|
||||
Done when opening a workspace gives the agent visibility into the coordination root and all linked repos or folders.
|
||||
|
||||
### 3. Agent Guidance And Explore
|
||||
|
||||
Then make exploration work.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Tell the agent a rough product goal and have it inspect the repos before creating a proposal.
|
||||
```
|
||||
|
||||
Expected user prompt:
|
||||
|
||||
```text
|
||||
Explore how we should make the OpenSpec docs available on the landing page.
|
||||
Look across the linked repos or folders, but do not implement yet.
|
||||
```
|
||||
|
||||
Agent behavior:
|
||||
|
||||
- Understand it is in workspace mode.
|
||||
- Inspect linked repos or folders.
|
||||
- Explain likely affected repos.
|
||||
- Ask for clarification only when needed.
|
||||
- Avoid implementation edits during explore.
|
||||
|
||||
Build:
|
||||
|
||||
- Workspace-level `AGENTS.md` guidance.
|
||||
- Normal OpenSpec skills and commands in workspace sessions.
|
||||
- Workspace-specific guidance layered on top of normal `/explore`, not replacing it.
|
||||
|
||||
Defer:
|
||||
|
||||
- Proposal artifact generation.
|
||||
- Target confirmation commands.
|
||||
- Apply context providers.
|
||||
|
||||
Done when a user can open a workspace and run a useful cross-repo exploration without creating a dummy change.
|
||||
|
||||
### 4. Proposal Creation
|
||||
|
||||
Only after explore works, build proposal creation.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Now that we understand the scope, capture the plan.
|
||||
```
|
||||
|
||||
Expected user prompt:
|
||||
|
||||
```text
|
||||
Create a proposal for this change.
|
||||
Target the repos that are actually affected.
|
||||
```
|
||||
|
||||
Preferred artifact shape:
|
||||
|
||||
```text
|
||||
changes/integrate-docs/
|
||||
proposal.md
|
||||
design.md
|
||||
tasks.md
|
||||
specs/
|
||||
openspec/
|
||||
docs-conventions/spec.md
|
||||
landing/
|
||||
docs-routing/spec.md
|
||||
```
|
||||
|
||||
Key workflow rule:
|
||||
|
||||
```text
|
||||
/explore may leave targets unknown.
|
||||
/propose may discover targets.
|
||||
/propose must confirm targets before saying ready for apply.
|
||||
```
|
||||
|
||||
Targets should be represented by the proposal artifacts themselves where possible. If there is `specs/landing/...`, then `landing` is in scope. Avoid a separate required `targets: [...]` metadata list as the active source of truth.
|
||||
|
||||
Defer:
|
||||
|
||||
- Repo-local materialization.
|
||||
- Worktree selection.
|
||||
- Multi-repo implementation.
|
||||
- Archive.
|
||||
|
||||
Done when a user can explore, then create a workspace proposal with repo-scoped specs and tasks.
|
||||
|
||||
### 5. Status
|
||||
|
||||
Before implementation, make status excellent.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Where are we, what repos are involved, and is this ready to implement?
|
||||
```
|
||||
|
||||
Expected surface:
|
||||
|
||||
```bash
|
||||
openspec status
|
||||
openspec status --change integrate-docs
|
||||
```
|
||||
|
||||
Human output should answer:
|
||||
|
||||
```text
|
||||
Change: integrate-docs
|
||||
Scope: openspec, landing
|
||||
Proposal: present
|
||||
Design: present
|
||||
Tasks: present
|
||||
Ready for apply: yes/no
|
||||
```
|
||||
|
||||
Status should also catch structural mistakes:
|
||||
|
||||
- Unknown repo folder under `specs/`.
|
||||
- Missing tasks.
|
||||
- No confirmed affected repo.
|
||||
- Linked repo or folder path missing.
|
||||
|
||||
Done when the agent and user can trust status before applying.
|
||||
|
||||
### 6. Apply One Repo Slice
|
||||
|
||||
Only now build `/apply`.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Implement the planned slice for one repo.
|
||||
```
|
||||
|
||||
Expected user prompt:
|
||||
|
||||
```text
|
||||
/apply integrate-docs for landing
|
||||
```
|
||||
|
||||
Product contract:
|
||||
|
||||
```text
|
||||
/apply means implement.
|
||||
```
|
||||
|
||||
It does not mean:
|
||||
|
||||
```text
|
||||
copy planning files
|
||||
materialize repo-local OpenSpec state
|
||||
create the proposal files for the first time
|
||||
```
|
||||
|
||||
Agent behavior:
|
||||
|
||||
1. Ask OpenSpec for apply context.
|
||||
2. Read proposal, design, tasks, and relevant specs.
|
||||
3. Confirm the target repo checkout.
|
||||
4. Edit only that repo.
|
||||
5. Update workspace tasks.
|
||||
6. Run relevant checks.
|
||||
|
||||
This likely wants a normalized context command internally, but that is supporting machinery:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "workspace",
|
||||
"change": "integrate-docs",
|
||||
"target": "landing",
|
||||
"implementationRoot": "/repos/openspec-landing",
|
||||
"contextFiles": [
|
||||
"changes/integrate-docs/proposal.md",
|
||||
"changes/integrate-docs/design.md",
|
||||
"changes/integrate-docs/tasks.md",
|
||||
"changes/integrate-docs/specs/landing/docs-routing/spec.md"
|
||||
],
|
||||
"allowedEditRoots": [
|
||||
"/repos/openspec-landing"
|
||||
],
|
||||
"tasksFile": "changes/integrate-docs/tasks.md"
|
||||
}
|
||||
```
|
||||
|
||||
Defer:
|
||||
|
||||
- Applying multiple repos at once.
|
||||
- Automatic branch creation.
|
||||
- Worktree management.
|
||||
- Repo-local OpenSpec mirroring.
|
||||
|
||||
Done when one repo slice can be implemented from the central workspace plan.
|
||||
|
||||
### 7. Verify
|
||||
|
||||
Then build verification.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
Check whether the implemented repo slice satisfies the plan.
|
||||
```
|
||||
|
||||
Expected prompt:
|
||||
|
||||
```text
|
||||
/verify integrate-docs for landing
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Read the same normalized context as `/apply`.
|
||||
- Inspect the implementation checkout.
|
||||
- Check tasks and specs for that repo.
|
||||
- Run repo validation.
|
||||
- Report gaps clearly.
|
||||
|
||||
Default behavior should verify one repo slice. Whole-workspace verification can come later.
|
||||
|
||||
Done when a user can verify one implemented repo slice against the central workspace plan.
|
||||
|
||||
### 8. Archive
|
||||
|
||||
Archive comes last in the first complete loop.
|
||||
|
||||
User goal:
|
||||
|
||||
```text
|
||||
The change is done. Move it out of active planning.
|
||||
```
|
||||
|
||||
Expected prompt:
|
||||
|
||||
```text
|
||||
/archive integrate-docs
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Require all targeted repo slices to be complete or explicitly accepted.
|
||||
- Archive the workspace change.
|
||||
- Do not require repo-local planning copies unless OpenSpec later decides that repo-local archival matters.
|
||||
|
||||
Done when a user can complete the full lifecycle:
|
||||
|
||||
```text
|
||||
workspace setup
|
||||
-> link repos or folders
|
||||
-> open
|
||||
-> explore
|
||||
-> propose
|
||||
-> apply repo A
|
||||
-> apply repo B
|
||||
-> verify
|
||||
-> archive
|
||||
```
|
||||
|
||||
## Implementation Discipline
|
||||
|
||||
Build only the next user-visible step.
|
||||
|
||||
The sequence should stay grounded in these questions:
|
||||
|
||||
```text
|
||||
1. Can I set up the workspace?
|
||||
2. Can I see my linked repos or folders?
|
||||
3. Can my agent explore them?
|
||||
4. Can we capture a proposal?
|
||||
5. Can status tell us if it is ready?
|
||||
6. Can the agent implement one repo slice?
|
||||
7. Can we verify it?
|
||||
8. Can we archive it?
|
||||
```
|
||||
|
||||
Avoid starting with internal abstractions unless they are required for the next user-visible capability.
|
||||
|
||||
Do not start with:
|
||||
|
||||
- Target metadata machinery.
|
||||
- Materialization.
|
||||
- Adapter abstractions.
|
||||
- Branch orchestration.
|
||||
- Worktree orchestration.
|
||||
- Multi-repo apply.
|
||||
|
||||
Those may matter later, but they should not define the first reimplementation path.
|
||||
|
||||
## Product Shape
|
||||
|
||||
The workspace should feel like OpenSpec's normal workflow stretched across multiple repos, not a second product with its own lifecycle.
|
||||
|
||||
The durable product model is:
|
||||
|
||||
```text
|
||||
workspace = durable planning home
|
||||
links = repos or folders visible for planning
|
||||
proposal = scoped planning commitment
|
||||
repo slice = one affected repo or folder in the plan
|
||||
branch/worktree = implementation checkout
|
||||
/apply = implement one selected repo slice
|
||||
```
|
||||
|
||||
Keep the user journey simple:
|
||||
|
||||
```text
|
||||
Open the workspace.
|
||||
Ask the agent to explore.
|
||||
Create the proposal when scope is clear.
|
||||
Implement one repo slice at a time.
|
||||
Verify.
|
||||
Archive.
|
||||
```
|
||||
@@ -1,67 +0,0 @@
|
||||
# Workspace Reimplementation Start Here
|
||||
|
||||
This is the grep-friendly entry point for agents working on the workspace reimplementation.
|
||||
|
||||
Useful search terms:
|
||||
|
||||
```text
|
||||
workspace reimplementation
|
||||
workspace poc
|
||||
workspace-poc
|
||||
workspace reference guide
|
||||
workspace roadmap
|
||||
fresh agent
|
||||
start here
|
||||
```
|
||||
|
||||
## Start Here
|
||||
|
||||
Read these files in order:
|
||||
|
||||
1. `WORKSPACE_REIMPLEMENTATION_DIRECTION.md`
|
||||
2. `openspec/changes/workspace-reimplementation-roadmap/README.md`
|
||||
3. `openspec/changes/workspace-reimplementation-roadmap/POC_REFERENCE_GUIDE.md`
|
||||
4. The proposal for the next implementation slice
|
||||
|
||||
The POC reference commit is:
|
||||
|
||||
```text
|
||||
workspace-poc @ 79a45ac043f414e63d13e08b9da83b135cb20a39
|
||||
```
|
||||
|
||||
Use the POC as research material. Do not merge it into an implementation branch. Do not preserve its architecture unless a slice proposal or design explicitly decides to do so.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Implement these flat OpenSpec changes in order:
|
||||
|
||||
1. `workspace-foundation`
|
||||
2. `workspace-create-and-register-repos`
|
||||
3. `workspace-open-agent-context`
|
||||
4. `workspace-change-planning`
|
||||
5. `workspace-apply-repo-slice`
|
||||
6. `workspace-verify-and-archive`
|
||||
|
||||
`workspace-reimplementation-roadmap` is the continuity and reference container for the plan.
|
||||
|
||||
## Before Editing
|
||||
|
||||
For the slice you are about to implement, inspect the pinned POC commit using `POC_REFERENCE_GUIDE.md`, then write down:
|
||||
|
||||
```text
|
||||
POC findings for <slice>:
|
||||
|
||||
User behavior to preserve:
|
||||
- ...
|
||||
|
||||
Tests or examples worth translating:
|
||||
- ...
|
||||
|
||||
Implementation shortcuts to avoid:
|
||||
- ...
|
||||
|
||||
Open design questions:
|
||||
- ...
|
||||
```
|
||||
|
||||
Capture durable findings in the relevant OpenSpec artifact so future sessions do not depend on chat history.
|
||||
+21
-134
@@ -1,13 +1,12 @@
|
||||
# CLI Reference
|
||||
|
||||
The OpenSpec CLI (`openspec`) provides terminal commands for project setup, validation, status inspection, and management. These commands complement the AI slash commands (like `/opsx:propose`) documented in [Commands](commands.md).
|
||||
The OpenSpec CLI (`openspec`) provides terminal commands for project setup, validation, status inspection, and management. These commands complement the AI slash commands (like `/opsx:new`) documented in [Commands](commands.md).
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Commands | Purpose |
|
||||
|----------|----------|---------|
|
||||
| **Setup** | `init`, `update` | Initialize and update OpenSpec in your project |
|
||||
| **Workspaces (beta)** | `workspace setup`, `workspace list`, `workspace ls`, `workspace link`, `workspace relink`, `workspace doctor` | Set up planning across linked repos or folders |
|
||||
| **Browsing** | `list`, `view`, `show` | Explore changes and specs |
|
||||
| **Validation** | `validate` | Check changes and specs for issues |
|
||||
| **Lifecycle** | `archive` | Finalize completed changes |
|
||||
@@ -47,11 +46,6 @@ These commands support `--json` output for programmatic use by AI agents and scr
|
||||
| `openspec instructions` | Get next steps | `--json` for agent instructions |
|
||||
| `openspec templates` | Find template paths | `--json` for path resolution |
|
||||
| `openspec schemas` | List available schemas | `--json` for schema discovery |
|
||||
| `openspec workspace setup --no-interactive` | Create a workspace with explicit inputs | `--json` for structured setup output |
|
||||
| `openspec workspace list` | Browse known workspaces | `--json` for typed workspace objects |
|
||||
| `openspec workspace link` | Link a repo or folder | `--json` for structured link output |
|
||||
| `openspec workspace relink` | Repair a linked path | `--json` for structured link output |
|
||||
| `openspec workspace doctor` | Check one workspace | `--json` for structured status output |
|
||||
|
||||
---
|
||||
|
||||
@@ -73,8 +67,6 @@ These options work with all commands:
|
||||
|
||||
Initialize OpenSpec in your project. Creates the folder structure and configures AI tool integrations.
|
||||
|
||||
Default behavior uses global config defaults: profile `core`, delivery `both`, workflows `propose, explore, apply, sync, archive`.
|
||||
|
||||
```
|
||||
openspec init [path] [options]
|
||||
```
|
||||
@@ -91,11 +83,8 @@ openspec init [path] [options]
|
||||
|--------|-------------|
|
||||
| `--tools <list>` | Configure AI tools non-interactively. Use `all`, `none`, or comma-separated list |
|
||||
| `--force` | Auto-cleanup legacy files without prompting |
|
||||
| `--profile <profile>` | Override global profile for this init run (`core` or `custom`) |
|
||||
|
||||
`--profile custom` uses whatever workflows are currently selected in global config (`openspec config profile`).
|
||||
|
||||
**Supported tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `opencode`, `pi`, `qoder`, `lingma`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
**Supported tools:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `opencode`, `qoder`, `qwen`, `roocode`, `windsurf`
|
||||
|
||||
**Examples:**
|
||||
|
||||
@@ -112,9 +101,6 @@ openspec init --tools claude,cursor
|
||||
# Configure for all supported tools
|
||||
openspec init --tools all
|
||||
|
||||
# Override profile for this run
|
||||
openspec init --profile core
|
||||
|
||||
# Skip prompts and auto-cleanup legacy files
|
||||
openspec init --force
|
||||
```
|
||||
@@ -127,9 +113,8 @@ openspec/
|
||||
├── changes/ # Proposed changes
|
||||
└── config.yaml # Project configuration
|
||||
|
||||
.claude/skills/ # Claude Code skills (if claude selected)
|
||||
.cursor/skills/ # Cursor skills (if cursor selected)
|
||||
.cursor/commands/ # Cursor OPSX commands (if delivery includes commands)
|
||||
.claude/skills/ # Claude Code skill files (if claude selected)
|
||||
.cursor/rules/ # Cursor rules (if cursor selected)
|
||||
... (other tool configs)
|
||||
```
|
||||
|
||||
@@ -137,7 +122,7 @@ openspec/
|
||||
|
||||
### `openspec update`
|
||||
|
||||
Update OpenSpec instruction files after upgrading the CLI. Re-generates AI tool configuration files using your current global profile, selected workflows, and delivery mode.
|
||||
Update OpenSpec instruction files after upgrading the CLI. Re-generates AI tool configuration files.
|
||||
|
||||
```
|
||||
openspec update [path] [options]
|
||||
@@ -165,103 +150,6 @@ openspec update
|
||||
|
||||
---
|
||||
|
||||
## Workspace Commands
|
||||
|
||||
Workspace commands are under active development and are not ready for use yet. Do not build external automation, integrations, or long-lived workflows on top of this command surface; command behavior, state files, and JSON output can change at any point.
|
||||
|
||||
Coordination workspaces are planning homes for work that spans multiple repos or folders. Workspace visibility is not change commitment: link the repos or folders OpenSpec should know about, then create changes when you are ready to plan specific work.
|
||||
|
||||
### `openspec workspace setup`
|
||||
|
||||
Create a workspace in the standard OpenSpec workspace location and link at least one existing repo or folder.
|
||||
|
||||
```bash
|
||||
openspec workspace setup [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--name <name>` | Workspace name. Names must be kebab-case |
|
||||
| `--link <path>` | Link an existing repo or folder and infer the link name from the folder name |
|
||||
| `--link <name>=<path>` | Link an existing repo or folder with an explicit link name |
|
||||
| `--no-interactive` | Disable prompts; requires `--name` and at least one `--link` |
|
||||
| `--json` | Output JSON; requires `--no-interactive` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
openspec workspace setup
|
||||
openspec workspace setup --no-interactive --name platform --link /repos/api --link web=/repos/web
|
||||
openspec workspace setup --no-interactive --json --name checkout --link /repos/platform/apps/checkout
|
||||
```
|
||||
|
||||
Setup prints the workspace location, planning path, linked repos or folders, and a workspace check. It does not ask for a preferred agent or open the workspace.
|
||||
|
||||
### `openspec workspace list`
|
||||
|
||||
List known OpenSpec workspaces from the local registry.
|
||||
|
||||
```bash
|
||||
openspec workspace list [--json]
|
||||
openspec workspace ls [--json]
|
||||
```
|
||||
|
||||
The list shows each workspace location and linked repos or folders. Stale registry records are reported but not changed.
|
||||
|
||||
### `openspec workspace link`
|
||||
|
||||
Record an existing repo or folder for one workspace.
|
||||
|
||||
```bash
|
||||
openspec workspace link [name] <path> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--workspace <name>` | Select a known workspace from the local registry |
|
||||
| `--json` | Output JSON |
|
||||
| `--no-interactive` | Disable workspace picker prompts |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
openspec workspace link /repos/api
|
||||
openspec workspace link api-service /repos/api
|
||||
openspec workspace link --workspace platform /repos/platform/apps/checkout
|
||||
```
|
||||
|
||||
The path must already exist. Relative paths are resolved against the command's current directory before OpenSpec stores the verified absolute path in machine-local workspace state. Linked paths can be full repos, packages, services, apps, or folders without repo-local `openspec/` state.
|
||||
|
||||
### `openspec workspace relink`
|
||||
|
||||
Repair or change the local path for an existing link.
|
||||
|
||||
```bash
|
||||
openspec workspace relink <name> <path> [options]
|
||||
```
|
||||
|
||||
The path must already exist. Relink updates only the machine-local path for the stable link name.
|
||||
|
||||
### `openspec workspace doctor`
|
||||
|
||||
Check what one workspace can resolve on the current machine.
|
||||
|
||||
```bash
|
||||
openspec workspace doctor [options]
|
||||
```
|
||||
|
||||
Doctor shows the workspace location, planning path, linked repos or folders, missing paths, repo-local specs paths when present, and suggested fixes. It reports issues only; it does not repair them automatically.
|
||||
|
||||
Commands that need one workspace use the current workspace when run from inside a workspace folder or subdirectory. From elsewhere, pass `--workspace <name>`, select from the picker in an interactive terminal, or rely on the only known workspace when exactly one exists. In `--json` or `--no-interactive` mode, ambiguous selection fails with a structured status error and suggests `--workspace <name>`.
|
||||
|
||||
JSON responses use typed objects plus `status` arrays. Primary data lives in `workspace`, `workspaces`, or `link`; warnings and errors live in `status`.
|
||||
|
||||
---
|
||||
|
||||
## Browsing Commands
|
||||
|
||||
### `openspec list`
|
||||
@@ -540,28 +428,29 @@ openspec status --change add-dark-mode --json
|
||||
```
|
||||
Change: add-dark-mode
|
||||
Schema: spec-driven
|
||||
Progress: 2/4 artifacts complete
|
||||
|
||||
[x] proposal
|
||||
[ ] design
|
||||
[x] specs
|
||||
[-] tasks (blocked by: design)
|
||||
Artifacts:
|
||||
✓ proposal proposal.md exists
|
||||
✓ specs specs/ exists
|
||||
◆ design ready (requires: specs)
|
||||
○ tasks blocked (requires: design)
|
||||
|
||||
Next: Create design using /opsx:continue
|
||||
```
|
||||
|
||||
**Output (JSON):**
|
||||
|
||||
```json
|
||||
{
|
||||
"changeName": "add-dark-mode",
|
||||
"schemaName": "spec-driven",
|
||||
"isComplete": false,
|
||||
"applyRequires": ["tasks"],
|
||||
"change": "add-dark-mode",
|
||||
"schema": "spec-driven",
|
||||
"artifacts": [
|
||||
{"id": "proposal", "outputPath": "proposal.md", "status": "done"},
|
||||
{"id": "design", "outputPath": "design.md", "status": "ready"},
|
||||
{"id": "specs", "outputPath": "specs/**/*.md", "status": "done"},
|
||||
{"id": "tasks", "outputPath": "tasks.md", "status": "blocked", "missingDeps": ["design"]}
|
||||
]
|
||||
{"id": "proposal", "status": "complete", "path": "proposal.md"},
|
||||
{"id": "specs", "status": "complete", "path": "specs/"},
|
||||
{"id": "design", "status": "ready", "requires": ["specs"]},
|
||||
{"id": "tasks", "status": "blocked", "requires": ["design"]}
|
||||
],
|
||||
"next": "design"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1023,8 +912,6 @@ openspec completion uninstall
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OPENSPEC_TELEMETRY` | Set to `0` to disable telemetry |
|
||||
| `DO_NOT_TRACK` | Set to `1` to disable telemetry (standard DNT signal) |
|
||||
| `OPENSPEC_CONCURRENCY` | Default concurrency for bulk validation (default: 6) |
|
||||
| `EDITOR` or `VISUAL` | Editor for `openspec config edit` |
|
||||
| `NO_COLOR` | Disable color output when set |
|
||||
@@ -1033,7 +920,7 @@ openspec completion uninstall
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Commands](commands.md) - AI slash commands (`/opsx:propose`, `/opsx:apply`, etc.)
|
||||
- [Commands](commands.md) - AI slash commands (`/opsx:new`, `/opsx:apply`, etc.)
|
||||
- [Workflows](workflows.md) - Common patterns and when to use each command
|
||||
- [Customization](customization.md) - Create custom schemas and templates
|
||||
- [Getting Started](getting-started.md) - First-time setup guide
|
||||
|
||||
+13
-63
@@ -6,70 +6,23 @@ For workflow patterns and when to use each command, see [Workflows](workflows.md
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Default Quick Path (`core` profile)
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
|
||||
| `/opsx:explore` | Think through ideas before committing to a change |
|
||||
| `/opsx:apply` | Implement tasks from the change |
|
||||
| `/opsx:sync` | Merge delta specs into main specs |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
|
||||
### Expanded Workflow Commands (custom workflow selection)
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/opsx:new` | Start a new change scaffold |
|
||||
| `/opsx:new` | Start a new change |
|
||||
| `/opsx:continue` | Create the next artifact based on dependencies |
|
||||
| `/opsx:ff` | Fast-forward: create all planning artifacts at once |
|
||||
| `/opsx:apply` | Implement tasks from the change |
|
||||
| `/opsx:verify` | Validate implementation matches artifacts |
|
||||
| `/opsx:sync` | Merge delta specs into main specs |
|
||||
| `/opsx:archive` | Archive a completed change |
|
||||
| `/opsx:bulk-archive` | Archive multiple changes at once |
|
||||
| `/opsx:onboard` | Guided tutorial through the complete workflow |
|
||||
|
||||
The default global profile is `core`. To enable expanded workflow commands, run `openspec config profile`, select workflows, then run `openspec update` in your project.
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
### `/opsx:propose`
|
||||
|
||||
Create a new change and generate planning artifacts in one step. This is the default start command in the `core` profile.
|
||||
|
||||
**Syntax:**
|
||||
```text
|
||||
/opsx:propose [change-name-or-description]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
| Argument | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `change-name-or-description` | No | Kebab-case name or plain-language change description |
|
||||
|
||||
**What it does:**
|
||||
- Creates `openspec/changes/<change-name>/`
|
||||
- Generates artifacts needed before implementation (for `spec-driven`: proposal, specs, design, tasks)
|
||||
- Stops when the change is ready for `/opsx:apply`
|
||||
|
||||
**Example:**
|
||||
```text
|
||||
You: /opsx:propose add-dark-mode
|
||||
|
||||
AI: Created openspec/changes/add-dark-mode/
|
||||
✓ proposal.md
|
||||
✓ specs/ui/spec.md
|
||||
✓ design.md
|
||||
✓ tasks.md
|
||||
Ready for implementation. Run /opsx:apply.
|
||||
```
|
||||
|
||||
**Tips:**
|
||||
- Use this for the fastest end-to-end path
|
||||
- If you want step-by-step artifact control, enable expanded workflows and use `/opsx:new` + `/opsx:continue`
|
||||
|
||||
---
|
||||
|
||||
### `/opsx:explore`
|
||||
|
||||
Think through ideas, investigate problems, and clarify requirements before committing to a change.
|
||||
@@ -89,7 +42,7 @@ Think through ideas, investigate problems, and clarify requirements before commi
|
||||
- Investigates the codebase to answer questions
|
||||
- Compares options and approaches
|
||||
- Creates visual diagrams to clarify thinking
|
||||
- Can transition to `/opsx:propose` (default) or `/opsx:new` (expanded workflow) when insights crystallize
|
||||
- Can transition to `/opsx:new` when insights crystallize
|
||||
|
||||
**Example:**
|
||||
```text
|
||||
@@ -113,7 +66,7 @@ AI: Let me investigate your current auth setup...
|
||||
|
||||
You: Let's go with JWT. Can we start a change for that?
|
||||
|
||||
AI: Ready when you are. Run /opsx:propose add-jwt-auth to begin.
|
||||
AI: Ready when you are. Run /opsx:new add-jwt-auth to begin.
|
||||
```
|
||||
|
||||
**Tips:**
|
||||
@@ -126,9 +79,7 @@ AI: Ready when you are. Run /opsx:propose add-jwt-auth to begin.
|
||||
|
||||
### `/opsx:new`
|
||||
|
||||
Start a new change scaffold. Creates the change folder and waits for you to generate artifacts with `/opsx:continue` or `/opsx:ff`.
|
||||
|
||||
This command is part of the expanded workflow set (not included in the default `core` profile).
|
||||
Start a new change. Creates the change folder structure and scaffolds it with the selected schema.
|
||||
|
||||
**Syntax:**
|
||||
```
|
||||
@@ -614,14 +565,13 @@ Different AI tools use slightly different command syntax. Use the format that ma
|
||||
|
||||
| Tool | Syntax Example |
|
||||
|------|----------------|
|
||||
| Claude Code | `/opsx:propose`, `/opsx:apply` |
|
||||
| Cursor | `/opsx-propose`, `/opsx-apply` |
|
||||
| Windsurf | `/opsx-propose`, `/opsx-apply` |
|
||||
| Copilot (IDE) | `/opsx-propose`, `/opsx-apply` |
|
||||
| Kimi CLI | Skill-based invocations such as `/skill:openspec-propose`, `/skill:openspec-apply-change` (no generated `opsx-*` command files) |
|
||||
| Trae | Skill-based invocations such as `/openspec-propose`, `/openspec-apply-change` (no generated `opsx-*` command files) |
|
||||
| Claude Code | `/opsx:new`, `/opsx:apply` |
|
||||
| Cursor | `/opsx-new`, `/opsx-apply` |
|
||||
| Windsurf | `/opsx-new`, `/opsx-apply` |
|
||||
| Copilot (IDE) | `/opsx-new`, `/opsx-apply` |
|
||||
| Trae | `/openspec-new-change`, `/openspec-apply-change` |
|
||||
|
||||
The intent is the same across tools, but how commands are surfaced can differ by integration.
|
||||
The functionality is identical regardless of syntax.
|
||||
|
||||
> **Note:** GitHub Copilot commands (`.github/prompts/*.prompt.md`) are only available in IDE extensions (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompt files — see [Supported Tools](supported-tools.md) for details and workarounds.
|
||||
|
||||
|
||||
+27
-144
@@ -7,10 +7,10 @@ This guide explains the core ideas behind OpenSpec and how they fit together. Fo
|
||||
OpenSpec is built around four principles:
|
||||
|
||||
```
|
||||
fluid not rigid — no phase gates, work on what makes sense
|
||||
fluid not rigid — no phase gates, work on what makes sense
|
||||
iterative not waterfall — learn as you build, refine as you go
|
||||
easy not complex — lightweight setup, minimal ceremony
|
||||
brownfield-first — works with existing codebases, not just greenfield
|
||||
easy not complex — lightweight setup, minimal ceremony
|
||||
brownfield-first — works with existing codebases, not just greenfield
|
||||
```
|
||||
|
||||
### Why These Principles Matter
|
||||
@@ -28,19 +28,19 @@ brownfield-first — works with existing codebases, not just greenfield
|
||||
OpenSpec organizes your work into two main areas:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ openspec/ │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌───────────────────────────────┐ │
|
||||
│ │ specs/ │ │ changes/ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Source of truth │◄─────│ Proposed modifications │ │
|
||||
│ │ How your system │ merge│ Each change = one folder │ │
|
||||
│ │ currently works │ │ Contains artifacts + deltas │ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────────────┘ └───────────────────────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ openspec/ │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌──────────────────────────────┐ │
|
||||
│ │ specs/ │ │ changes/ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Source of truth │◄─────│ Proposed modifications │ │
|
||||
│ │ How your system │ merge│ Each change = one folder │ │
|
||||
│ │ currently works │ │ Contains artifacts + deltas │ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────────────┘ └──────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Specs** are the source of truth — they describe how your system currently behaves.
|
||||
@@ -49,123 +49,6 @@ OpenSpec organizes your work into two main areas:
|
||||
|
||||
This separation is key. You can work on multiple changes in parallel without conflicts. You can review a change before it affects the main specs. And when you archive a change, its deltas merge cleanly into the source of truth.
|
||||
|
||||
## Coordination Workspaces
|
||||
|
||||
Workspace support is under active development and is not ready for use yet. Do not build external automation, integrations, or long-lived workflows on top of workspace behavior; the commands, state files, and JSON output can change at any point.
|
||||
|
||||
The commands below provide the first setup flow for planning across linked repos or folders.
|
||||
|
||||
Repo-local OpenSpec projects are the right default when one repo owns the planning, implementation, and archive flow. Some work spans several repos or folders. For that case, an OpenSpec coordination workspace is the durable planning home.
|
||||
|
||||
The workspace mental model is:
|
||||
|
||||
```text
|
||||
workspace = where related cross-repo changes live
|
||||
link = a stable name for a repo or folder the workspace can plan against
|
||||
change = one feature, fix, project, or other planned piece of work
|
||||
```
|
||||
|
||||
A workspace has a different shape from a repo-local project:
|
||||
|
||||
```text
|
||||
workspace-folder/
|
||||
├── changes/ # Workspace-level planning
|
||||
└── .openspec-workspace/
|
||||
├── workspace.yaml # Shared workspace identity and link names
|
||||
└── local.yaml # This machine's local paths
|
||||
```
|
||||
|
||||
Repo-local OpenSpec state keeps the existing shape:
|
||||
|
||||
```text
|
||||
repo-root/
|
||||
└── openspec/
|
||||
├── specs/
|
||||
└── changes/
|
||||
```
|
||||
|
||||
That distinction matters. The workspace folder is a coordination surface for planning across linked repos or folders. Each repo's `openspec/` directory remains the home for repo-owned specs, repo-local changes, and implementation planning. Users do not need to run repo-local `openspec init` inside a workspace folder.
|
||||
|
||||
Stable link names are how workspace planning refers to repos and folders. The shared workspace state keeps names such as `api`, `web`, or `checkout`; each machine maps those names to its own local paths in `.openspec-workspace/local.yaml`.
|
||||
|
||||
```yaml
|
||||
# .openspec-workspace/workspace.yaml
|
||||
version: 1
|
||||
name: platform
|
||||
links:
|
||||
api: {}
|
||||
web: {}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .openspec-workspace/local.yaml
|
||||
version: 1
|
||||
paths:
|
||||
api: /repos/api
|
||||
web: /repos/web
|
||||
```
|
||||
|
||||
OpenSpec-created workspaces exclude `.openspec-workspace/local.yaml` from portable collaboration state by default. `.openspec-workspace/workspace.yaml` remains portable because it stores the workspace name and stable link names, not one user's absolute checkout paths.
|
||||
|
||||
Linked paths can be full repos, folders inside a large monorepo, or other existing folders. They do not need repo-local `openspec/` state before they can participate in workspace planning. Later implementation, verify, or archive workflows may require more repo readiness, but planning visibility starts with the link.
|
||||
|
||||
```text
|
||||
multi-repo:
|
||||
api -> /repos/api
|
||||
web -> /repos/web
|
||||
|
||||
large monorepo:
|
||||
billing -> /repos/platform/services/billing
|
||||
checkout -> /repos/platform/apps/checkout
|
||||
```
|
||||
|
||||
Managed workspaces live under the standard OpenSpec data directory:
|
||||
|
||||
```text
|
||||
getGlobalDataDir()/workspaces
|
||||
```
|
||||
|
||||
That means `$XDG_DATA_HOME/openspec/workspaces` when `XDG_DATA_HOME` is set, `~/.local/share/openspec/workspaces` on Unix-style fallback, and `%LOCALAPPDATA%\openspec\workspaces` on native Windows fallback. Native Windows shells, PowerShell, and WSL2 each keep the path strings for the runtime running OpenSpec. This foundation does not translate between `D:\repo`, `/mnt/d/repo`, and UNC WSL paths.
|
||||
|
||||
OpenSpec also keeps a machine-local registry at:
|
||||
|
||||
```text
|
||||
getGlobalDataDir()/workspaces/registry.yaml
|
||||
```
|
||||
|
||||
The registry maps workspace names to workspace locations so later global commands can list or select known workspaces from anywhere. It is only an index. Each workspace folder remains authoritative for its own `.openspec-workspace/workspace.yaml` and `.openspec-workspace/local.yaml`, so stale registry records can be reported and repaired without redefining the workspace itself.
|
||||
|
||||
Workspace visibility is not change commitment. Set up a workspace when OpenSpec should know which repos or folders are relevant; create a change later when you are ready to plan a feature, fix, project, or other piece of work.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
# Guided setup
|
||||
openspec workspace setup
|
||||
|
||||
# Automation-friendly setup
|
||||
openspec workspace setup --no-interactive --name platform --link /repos/api --link web=/repos/web
|
||||
|
||||
# See known workspaces from the local registry
|
||||
openspec workspace list
|
||||
openspec workspace ls
|
||||
|
||||
# Add or repair links for the selected workspace
|
||||
openspec workspace link /repos/api
|
||||
openspec workspace link api-service /repos/api
|
||||
openspec workspace relink api-service /new/path/to/api
|
||||
|
||||
# Check what this machine can resolve
|
||||
openspec workspace doctor
|
||||
openspec workspace doctor --workspace platform
|
||||
```
|
||||
|
||||
`workspace setup` always creates the workspace in the standard workspace location, records it in the local registry, shows the workspace location, and requires at least one linked repo or folder. `workspace link` and `workspace relink` record existing folders only; they do not create, copy, move, initialize, or edit the linked repo or folder.
|
||||
|
||||
Workspace commands that need one workspace can run from anywhere with `--workspace <name>`. If you run them inside a workspace folder or subdirectory, OpenSpec uses that current workspace. If several known workspaces are available and you do not pass `--workspace <name>`, human commands show a picker; `--json` and `--no-interactive` fail with a structured status error instead of prompting.
|
||||
|
||||
Direct workspace commands support JSON output for scripts. JSON responses keep primary data in `workspace`, `workspaces`, or `link` objects and report warnings or errors in `status` arrays. Healthy objects use `status: []`.
|
||||
|
||||
## Specs
|
||||
|
||||
Specs describe your system's behavior using structured requirements and scenarios.
|
||||
@@ -387,7 +270,7 @@ Delta specs describe **what's changing** relative to the current specs. See [Del
|
||||
|
||||
The design captures **technical approach** and **architecture decisions**.
|
||||
|
||||
````markdown
|
||||
```markdown
|
||||
# Design: Add Dark Mode
|
||||
|
||||
## Technical Approach
|
||||
@@ -423,7 +306,7 @@ CSS Variables (applied to :root)
|
||||
- `src/contexts/ThemeContext.tsx` (new)
|
||||
- `src/components/ThemeToggle.tsx` (new)
|
||||
- `src/styles/globals.css` (modified)
|
||||
````
|
||||
```
|
||||
|
||||
**When to update the design:**
|
||||
- Implementation reveals the approach won't work
|
||||
@@ -675,17 +558,17 @@ openspec/
|
||||
## How It All Fits Together
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OPENSPEC FLOW │
|
||||
│ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ 1. START │ /opsx:propose (core) or /opsx:new (expanded) │
|
||||
│ │ 1. START │ /opsx:new creates a change folder │
|
||||
│ │ CHANGE │ │
|
||||
│ └───────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ 2. CREATE │ /opsx:ff or /opsx:continue (expanded workflow) │
|
||||
│ │ 2. CREATE │ /opsx:ff or /opsx:continue │
|
||||
│ │ ARTIFACTS │ Creates proposal → specs → design → tasks │
|
||||
│ │ │ (based on schema dependencies) │
|
||||
│ └───────┬────────┘ │
|
||||
@@ -704,13 +587,13 @@ openspec/
|
||||
│ └───────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────┐ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ 5. ARCHIVE │────►│ Delta specs merge into main specs │ │
|
||||
│ │ CHANGE │ │ Change folder moves to archive/ │ │
|
||||
│ └────────────────┘ │ Specs are now the updated source of truth │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ ┌────────────────┐ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ 5. ARCHIVE │────►│ Delta specs merge into main specs │ │
|
||||
│ │ CHANGE │ │ Change folder moves to archive/ │ │
|
||||
│ └────────────────┘ │ Specs are now the updated source of truth │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**The virtuous cycle:**
|
||||
|
||||
@@ -337,20 +337,6 @@ Then edit `schema.yaml` to add:
|
||||
|
||||
---
|
||||
|
||||
## Community Schemas
|
||||
|
||||
OpenSpec also supports community-maintained schemas distributed via standalone repositories. These provide opinionated workflows that integrate OpenSpec with other tools or systems, similar to how [github/spec-kit's community extension catalog](https://github.com/github/spec-kit/tree/main/extensions) works for spec-kit.
|
||||
|
||||
Community schemas are not vendored into OpenSpec core — they live in their own repositories with their own release cadence. To use one, copy the schema bundle into your project's `openspec/schemas/<schema-name>/` directory (each repo's README has install instructions).
|
||||
|
||||
| Schema | Maintainer | Repository | Description |
|
||||
|--------|-----------|-----------|-------------|
|
||||
| `superpowers-bridge` | @JiangWay | [JiangWay/openspec-schemas](https://github.com/JiangWay/openspec-schemas/tree/main/superpowers-bridge) | Integrates OpenSpec's artifact governance with [obra/superpowers](https://github.com/obra/superpowers) execution skills (brainstorming, writing-plans, TDD via subagents, code review, finishing). Adds an evidence-first `retrospective` artifact filling a gap Superpowers does not natively cover. |
|
||||
|
||||
> Want to contribute a community schema? Open an issue with a link to your repository, or submit a PR adding a row to this table.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [CLI Reference: Schema Commands](cli.md#schema-commands) - Full command documentation
|
||||
|
||||
+40
-20
@@ -4,22 +4,33 @@ This guide explains how OpenSpec works after you've installed and initialized it
|
||||
|
||||
## How It Works
|
||||
|
||||
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written.
|
||||
OpenSpec helps you and your AI coding assistant agree on what to build before any code is written. The workflow follows a simple pattern:
|
||||
|
||||
**Default quick path (core profile):**
|
||||
|
||||
```text
|
||||
/opsx:propose ──► /opsx:apply ──► /opsx:sync ──► /opsx:archive
|
||||
```
|
||||
|
||||
**Expanded path (custom workflow selection):**
|
||||
|
||||
```text
|
||||
/opsx:new ──► /opsx:ff or /opsx:continue ──► /opsx:apply ──► /opsx:verify ──► /opsx:archive
|
||||
┌────────────────────┐
|
||||
│ Start a Change │ /opsx:new
|
||||
└────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Create Artifacts │ /opsx:ff or /opsx:continue
|
||||
│ (proposal, specs, │
|
||||
│ design, tasks) │
|
||||
└────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Implement Tasks │ /opsx:apply
|
||||
│ (AI writes code) │
|
||||
└────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Archive & Merge │ /opsx:archive
|
||||
│ Specs │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
The default global profile is `core`, which includes `propose`, `explore`, `apply`, `sync`, and `archive`. You can enable the expanded workflow commands with `openspec config profile` and then `openspec update`.
|
||||
|
||||
## What OpenSpec Creates
|
||||
|
||||
After running `openspec init`, your project has this structure:
|
||||
@@ -120,12 +131,23 @@ The change folder moves to `openspec/changes/archive/` for audit history.
|
||||
|
||||
Let's walk through adding dark mode to an application.
|
||||
|
||||
### 1. Start the Change (Default)
|
||||
### 1. Start the Change
|
||||
|
||||
```text
|
||||
You: /opsx:propose add-dark-mode
|
||||
```
|
||||
You: /opsx:new add-dark-mode
|
||||
|
||||
AI: Created openspec/changes/add-dark-mode/
|
||||
Ready to create: proposal
|
||||
```
|
||||
|
||||
### 2. Create Artifacts
|
||||
|
||||
Use `/opsx:ff` (fast-forward) to create all planning artifacts at once:
|
||||
|
||||
```
|
||||
You: /opsx:ff
|
||||
|
||||
AI: Creating artifacts for add-dark-mode...
|
||||
✓ proposal.md — why we're doing this, what's changing
|
||||
✓ specs/ — requirements and scenarios
|
||||
✓ design.md — technical approach
|
||||
@@ -133,9 +155,7 @@ AI: Created openspec/changes/add-dark-mode/
|
||||
Ready for implementation!
|
||||
```
|
||||
|
||||
If you've enabled the expanded workflow profile, you can also do this as two steps: `/opsx:new` then `/opsx:ff` (or `/opsx:continue` incrementally).
|
||||
|
||||
### 2. What Gets Created
|
||||
### 3. What Gets Created
|
||||
|
||||
**proposal.md** - Captures the intent:
|
||||
|
||||
@@ -198,7 +218,7 @@ The system SHALL allow users to choose between light and dark themes.
|
||||
- [ ] 3.2 Update components to use CSS variables
|
||||
```
|
||||
|
||||
### 3. Implement
|
||||
### 4. Implement
|
||||
|
||||
```
|
||||
You: /opsx:apply
|
||||
@@ -214,7 +234,7 @@ AI: Working through tasks...
|
||||
|
||||
During implementation, if you discover the design needs adjustment, just update the artifact and continue.
|
||||
|
||||
### 4. Archive
|
||||
### 5. Archive
|
||||
|
||||
```
|
||||
You: /opsx:archive
|
||||
|
||||
@@ -26,9 +26,6 @@ yarn global add @fission-ai/openspec@latest
|
||||
|
||||
### bun
|
||||
|
||||
Bun can install OpenSpec globally, but OpenSpec currently runs on Node.js.
|
||||
You still need Node.js 20.19.0 or higher available on `PATH`.
|
||||
|
||||
```bash
|
||||
bun add -g @fission-ai/openspec@latest
|
||||
```
|
||||
@@ -70,44 +67,6 @@ Or add to your development environment in `flake.nix`:
|
||||
openspec --version
|
||||
```
|
||||
|
||||
## Troubleshooting PATH Visibility
|
||||
|
||||
If `openspec --version` works in one terminal but fails in an editor, AI agent,
|
||||
GUI app, or automation, OpenSpec is usually installed correctly but that process
|
||||
started with a different `PATH`.
|
||||
|
||||
Global package managers create an executable shim in a bin directory, then your
|
||||
shell or launcher must put that directory on `PATH`. Common ways to inspect the
|
||||
directory are:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
printf '%s/bin\n' "$(npm prefix -g)"
|
||||
|
||||
# pnpm
|
||||
pnpm bin -g
|
||||
|
||||
# bun
|
||||
bun pm bin -g
|
||||
|
||||
# current shell
|
||||
command -v openspec
|
||||
```
|
||||
|
||||
Make sure the environment that launches your editor, agent, GUI app, or
|
||||
automation includes the package-manager bin directory. For shell startup files,
|
||||
keep this to a minimal `PATH` export in a file that the target environment
|
||||
actually reads. Do not move interactive setup such as prompts, themes,
|
||||
completions, or commands that can block into always-loaded startup files.
|
||||
|
||||
To bypass global bin discovery while debugging, run OpenSpec through a package
|
||||
manager:
|
||||
|
||||
```bash
|
||||
npx -y @fission-ai/openspec@latest --version
|
||||
pnpm dlx @fission-ai/openspec@latest --version
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After installing, initialize OpenSpec in your project:
|
||||
|
||||
+15
-36
@@ -8,7 +8,7 @@ OPSX replaces the old phase-locked workflow with a fluid, action-based approach.
|
||||
|
||||
| Aspect | Legacy | OPSX |
|
||||
|--------|--------|------|
|
||||
| **Commands** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` | Default: `/opsx:propose`, `/opsx:apply`, `/opsx:sync`, `/opsx:archive` (expanded workflow commands optional) |
|
||||
| **Commands** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` | `/opsx:new`, `/opsx:continue`, `/opsx:apply`, and more |
|
||||
| **Workflow** | Create all artifacts at once | Create incrementally or all at once—your choice |
|
||||
| **Going back** | Awkward phase gates | Natural—update any artifact anytime |
|
||||
| **Customization** | Fixed structure | Schema-driven, fully hackable |
|
||||
@@ -84,9 +84,6 @@ Don't worry about getting it perfect. We're still learning what works best here,
|
||||
|
||||
Both `openspec init` and `openspec update` detect legacy files and guide you through the same cleanup process. Use whichever fits your situation:
|
||||
|
||||
- New installs default to profile `core` (`propose`, `explore`, `apply`, `sync`, `archive`).
|
||||
- Migrated installs preserve your previously installed workflows by writing a `custom` profile when needed.
|
||||
|
||||
### Using `openspec init`
|
||||
|
||||
Run this if you want to add new tools or reconfigure which tools are set up:
|
||||
@@ -144,7 +141,7 @@ Run this if you just want to migrate and refresh your existing tools to the late
|
||||
openspec update
|
||||
```
|
||||
|
||||
The update command also detects and cleans up legacy artifacts, then refreshes generated skills/commands to match your current profile and delivery settings.
|
||||
The update command also detects and cleans up legacy artifacts, then refreshes your skills to the latest version.
|
||||
|
||||
### Non-Interactive / CI Environments
|
||||
|
||||
@@ -278,43 +275,30 @@ The AI will help you identify what's essential vs. what can be trimmed.
|
||||
|
||||
## The New Commands
|
||||
|
||||
Command availability is profile-dependent:
|
||||
|
||||
**Default (`core` profile):**
|
||||
After migration, you have 9 OPSX commands instead of 3:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/opsx:propose` | Create a change and generate planning artifacts in one step |
|
||||
| `/opsx:explore` | Think through ideas with no structure |
|
||||
| `/opsx:apply` | Implement tasks from tasks.md |
|
||||
| `/opsx:archive` | Finalize and archive the change |
|
||||
|
||||
**Expanded workflow (custom selection):**
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/opsx:new` | Start a new change scaffold |
|
||||
| `/opsx:new` | Start a new change |
|
||||
| `/opsx:continue` | Create the next artifact (one at a time) |
|
||||
| `/opsx:ff` | Fast-forward—create planning artifacts at once |
|
||||
| `/opsx:ff` | Fast-forward—create all planning artifacts at once |
|
||||
| `/opsx:apply` | Implement tasks from tasks.md |
|
||||
| `/opsx:verify` | Validate implementation matches specs |
|
||||
| `/opsx:sync` | Preview/spec-merge without archiving |
|
||||
| `/opsx:sync` | Preview spec merge (optional—archive prompts if needed) |
|
||||
| `/opsx:archive` | Finalize and archive the change |
|
||||
| `/opsx:bulk-archive` | Archive multiple changes at once |
|
||||
| `/opsx:onboard` | Guided end-to-end onboarding workflow |
|
||||
|
||||
Enable expanded commands with `openspec config profile`, then run `openspec update`.
|
||||
|
||||
### Command Mapping from Legacy
|
||||
|
||||
| Legacy | OPSX Equivalent |
|
||||
|--------|-----------------|
|
||||
| `/openspec:proposal` | `/opsx:propose` (default) or `/opsx:new` then `/opsx:ff` (expanded) |
|
||||
| `/openspec:proposal` | `/opsx:new` then `/opsx:ff` |
|
||||
| `/openspec:apply` | `/opsx:apply` |
|
||||
| `/openspec:archive` | `/opsx:archive` |
|
||||
|
||||
### New Capabilities
|
||||
|
||||
These capabilities are part of the expanded workflow command set.
|
||||
|
||||
**Granular artifact creation:**
|
||||
```
|
||||
/opsx:continue
|
||||
@@ -558,11 +542,9 @@ project/
|
||||
│ └── config.yaml # NEW: Project configuration
|
||||
├── .claude/
|
||||
│ └── skills/ # NEW: OPSX skills
|
||||
│ ├── openspec-propose/ # default core profile
|
||||
│ ├── openspec-explore/
|
||||
│ ├── openspec-apply-change/
|
||||
│ ├── openspec-sync-specs/
|
||||
│ └── ... # expanded profile adds new/continue/ff/etc.
|
||||
│ ├── openspec-new-change/
|
||||
│ └── ...
|
||||
├── CLAUDE.md # OpenSpec markers removed, your content preserved
|
||||
└── AGENTS.md # OpenSpec markers removed, your content preserved
|
||||
```
|
||||
@@ -576,15 +558,12 @@ project/
|
||||
|
||||
### Command Cheatsheet
|
||||
|
||||
```text
|
||||
/opsx:propose Start quickly (default core profile)
|
||||
```
|
||||
/opsx:new Start a change
|
||||
/opsx:continue Create next artifact
|
||||
/opsx:ff Create all planning artifacts
|
||||
/opsx:apply Implement tasks
|
||||
/opsx:archive Finish and archive
|
||||
|
||||
# Expanded workflow (if enabled):
|
||||
/opsx:new Scaffold a change
|
||||
/opsx:continue Create next artifact
|
||||
/opsx:ff Create planning artifacts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+9
-24
@@ -65,8 +65,6 @@ openspec init
|
||||
|
||||
This creates skills in `.claude/skills/` (or equivalent) that AI coding assistants auto-detect.
|
||||
|
||||
By default, OpenSpec uses the `core` workflow profile (`propose`, `explore`, `apply`, `sync`, `archive`). If you want the expanded workflow commands (`new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`), configure them with `openspec config profile` and apply with `openspec update`.
|
||||
|
||||
During setup, you'll be prompted to create a **project config** (`openspec/config.yaml`). This is optional but recommended.
|
||||
|
||||
## Project Configuration
|
||||
@@ -157,17 +155,13 @@ rules:
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/opsx:propose` | Create a change and generate planning artifacts in one step (default quick path) |
|
||||
| `/opsx:explore` | Think through ideas, investigate problems, clarify requirements |
|
||||
| `/opsx:new` | Start a new change scaffold (expanded workflow) |
|
||||
| `/opsx:continue` | Create the next artifact (expanded workflow) |
|
||||
| `/opsx:ff` | Fast-forward planning artifacts (expanded workflow) |
|
||||
| `/opsx:new` | Start a new change |
|
||||
| `/opsx:continue` | Create the next artifact (based on what's ready) |
|
||||
| `/opsx:ff` | Fast-forward — create all planning artifacts at once |
|
||||
| `/opsx:apply` | Implement tasks, updating artifacts as needed |
|
||||
| `/opsx:verify` | Validate implementation against artifacts (expanded workflow) |
|
||||
| `/opsx:sync` | Sync delta specs to main (default workflow, optional) |
|
||||
| `/opsx:sync` | Sync delta specs to main (optional—archive prompts if needed) |
|
||||
| `/opsx:archive` | Archive when done |
|
||||
| `/opsx:bulk-archive` | Archive multiple completed changes (expanded workflow) |
|
||||
| `/opsx:onboard` | Guided walkthrough of an end-to-end change (expanded workflow) |
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -175,21 +169,13 @@ rules:
|
||||
```
|
||||
/opsx:explore
|
||||
```
|
||||
Think through ideas, investigate problems, compare options. No structure required - just a thinking partner. When insights crystallize, transition to `/opsx:propose` (default) or `/opsx:new`/`/opsx:ff` (expanded).
|
||||
Think through ideas, investigate problems, compare options. No structure required - just a thinking partner. When insights crystallize, transition to `/opsx:new` or `/opsx:ff`.
|
||||
|
||||
### Start a new change
|
||||
```
|
||||
/opsx:propose
|
||||
```
|
||||
Creates the change and generates planning artifacts needed before implementation.
|
||||
|
||||
If you've enabled expanded workflows, you can instead use:
|
||||
|
||||
```text
|
||||
/opsx:new # scaffold only
|
||||
/opsx:continue # create one artifact at a time
|
||||
/opsx:ff # create all planning artifacts at once
|
||||
/opsx:new
|
||||
```
|
||||
You'll be asked what you want to build and which workflow schema to use.
|
||||
|
||||
### Create artifacts
|
||||
```
|
||||
@@ -313,7 +299,6 @@ Think of it like git branches:
|
||||
## Architecture Deep Dive
|
||||
|
||||
This section explains how OPSX works under the hood and how it compares to the legacy workflow.
|
||||
Examples in this section use the expanded command set (`new`, `continue`, etc.); default `core` users can map the same flow to `propose → apply → sync → archive`.
|
||||
|
||||
### Philosophy: Phases vs Actions
|
||||
|
||||
@@ -371,7 +356,7 @@ Examples in this section use the expanded command set (`new`, `continue`, etc.);
|
||||
│ Hardcoded Templates (TypeScript strings) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Tool-specific configurators/adapters │
|
||||
│ Configurators (18+ classes, one per editor) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Generated Command Files (.claude/commands/openspec/*.md) │
|
||||
@@ -619,7 +604,7 @@ artifacts:
|
||||
| **State** | Phase-based mental model | Filesystem existence |
|
||||
| **Customization** | Edit source, rebuild | Create schema.yaml |
|
||||
| **Iteration** | Phase-locked | Fluid, edit anything |
|
||||
| **Editor Support** | Tool-specific configurator/adapters | Single skills directory |
|
||||
| **Editor Support** | 18+ configurator classes | Single skills directory |
|
||||
|
||||
## Schemas
|
||||
|
||||
|
||||
+52
-72
@@ -1,64 +1,50 @@
|
||||
# Supported Tools
|
||||
|
||||
OpenSpec works with many AI coding assistants. When you run `openspec init`, OpenSpec configures selected tools using your active profile/workflow selection and delivery mode.
|
||||
OpenSpec works with 20+ AI coding assistants. When you run `openspec init`, you'll be prompted to select which tools you use, and OpenSpec will configure the appropriate integrations.
|
||||
|
||||
## How It Works
|
||||
|
||||
For each selected tool, OpenSpec can install:
|
||||
For each tool you select, OpenSpec installs:
|
||||
|
||||
1. **Skills** (if delivery includes skills): `.../skills/openspec-*/SKILL.md`
|
||||
2. **Commands** (if delivery includes commands): tool-specific `opsx-*` command files
|
||||
|
||||
By default, OpenSpec uses the `core` profile, which includes:
|
||||
- `propose`
|
||||
- `explore`
|
||||
- `apply`
|
||||
- `sync`
|
||||
- `archive`
|
||||
|
||||
You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `bulk-archive`, `onboard`) via `openspec config profile`, then run `openspec update`.
|
||||
1. **Skills** — Reusable instruction files that power the `/opsx:*` workflow commands
|
||||
2. **Commands** — Tool-specific slash command bindings
|
||||
|
||||
## Tool Directory Reference
|
||||
|
||||
| Tool (ID) | Skills path pattern | Command path pattern |
|
||||
|-----------|---------------------|----------------------|
|
||||
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
|
||||
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
|
||||
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
|
||||
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
|
||||
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
|
||||
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
|
||||
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
|
||||
| Codex (`codex`) | `.codex/skills/openspec-*/SKILL.md` | `$CODEX_HOME/prompts/opsx-<id>.md`\* |
|
||||
| ForgeCode (`forgecode`) | `.forge/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
|
||||
| Continue (`continue`) | `.continue/skills/openspec-*/SKILL.md` | `.continue/prompts/opsx-<id>.prompt` |
|
||||
| CoStrict (`costrict`) | `.cospec/skills/openspec-*/SKILL.md` | `.cospec/openspec/commands/opsx-<id>.md` |
|
||||
| Crush (`crush`) | `.crush/skills/openspec-*/SKILL.md` | `.crush/commands/opsx/<id>.md` |
|
||||
| Cursor (`cursor`) | `.cursor/skills/openspec-*/SKILL.md` | `.cursor/commands/opsx-<id>.md` |
|
||||
| Factory Droid (`factory`) | `.factory/skills/openspec-*/SKILL.md` | `.factory/commands/opsx-<id>.md` |
|
||||
| Gemini CLI (`gemini`) | `.gemini/skills/openspec-*/SKILL.md` | `.gemini/commands/opsx/<id>.toml` |
|
||||
| GitHub Copilot (`github-copilot`) | `.github/skills/openspec-*/SKILL.md` | `.github/prompts/opsx-<id>.prompt.md`\*\* |
|
||||
| iFlow (`iflow`) | `.iflow/skills/openspec-*/SKILL.md` | `.iflow/commands/opsx-<id>.md` |
|
||||
| Junie (`junie`) | `.junie/skills/openspec-*/SKILL.md` | `.junie/commands/opsx-<id>.md` |
|
||||
| Kilo Code (`kilocode`) | `.kilocode/skills/openspec-*/SKILL.md` | `.kilocode/workflows/opsx-<id>.md` |
|
||||
| Kimi CLI (`kimi`) | `.kimi/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/skill:openspec-*` invocations) |
|
||||
| Kiro (`kiro`) | `.kiro/skills/openspec-*/SKILL.md` | `.kiro/prompts/opsx-<id>.prompt.md` |
|
||||
| Lingma (`lingma`) | `.lingma/skills/openspec-*/SKILL.md` | `.lingma/commands/opsx/<id>.md` |
|
||||
| OpenCode (`opencode`) | `.opencode/skills/openspec-*/SKILL.md` | `.opencode/commands/opsx-<id>.md` |
|
||||
| Pi (`pi`) | `.pi/skills/openspec-*/SKILL.md` | `.pi/prompts/opsx-<id>.md` |
|
||||
| Qoder (`qoder`) | `.qoder/skills/openspec-*/SKILL.md` | `.qoder/commands/opsx/<id>.md` |
|
||||
| Qwen Code (`qwen`) | `.qwen/skills/openspec-*/SKILL.md` | `.qwen/commands/opsx-<id>.toml` |
|
||||
| RooCode (`roocode`) | `.roo/skills/openspec-*/SKILL.md` | `.roo/commands/opsx-<id>.md` |
|
||||
| Trae (`trae`) | `.trae/skills/openspec-*/SKILL.md` | Not generated (no command adapter; use skill-based `/openspec-*` invocations) |
|
||||
| Windsurf (`windsurf`) | `.windsurf/skills/openspec-*/SKILL.md` | `.windsurf/workflows/opsx-<id>.md` |
|
||||
| Tool | Skills Location | Commands Location |
|
||||
|------|-----------------|-------------------|
|
||||
| Amazon Q Developer | `.amazonq/skills/` | `.amazonq/prompts/` |
|
||||
| Antigravity | `.agent/skills/` | `.agent/workflows/` |
|
||||
| Auggie (Augment CLI) | `.augment/skills/` | `.augment/commands/` |
|
||||
| Claude Code | `.claude/skills/` | `.claude/commands/opsx/` |
|
||||
| Cline | `.cline/skills/` | `.clinerules/workflows/` |
|
||||
| CodeBuddy | `.codebuddy/skills/` | `.codebuddy/commands/opsx/` |
|
||||
| Codex | `.codex/skills/` | `~/.codex/prompts/`\* |
|
||||
| Continue | `.continue/skills/` | `.continue/prompts/` |
|
||||
| CoStrict | `.cospec/skills/` | `.cospec/openspec/commands/` |
|
||||
| Crush | `.crush/skills/` | `.crush/commands/opsx/` |
|
||||
| Cursor | `.cursor/skills/` | `.cursor/commands/` |
|
||||
| Factory Droid | `.factory/skills/` | `.factory/commands/` |
|
||||
| Gemini CLI | `.gemini/skills/` | `.gemini/commands/opsx/` |
|
||||
| GitHub Copilot | `.github/skills/` | `.github/prompts/`\*\* |
|
||||
| iFlow | `.iflow/skills/` | `.iflow/commands/` |
|
||||
| Kilo Code | `.kilocode/skills/` | `.kilocode/workflows/` |
|
||||
| Kiro | `.kiro/skills/` | `.kiro/prompts/` |
|
||||
| OpenCode | `.opencode/skills/` | `.opencode/command/` |
|
||||
| Pi | `.pi/skills/` | `.pi/prompts/` |
|
||||
| Qoder | `.qoder/skills/` | `.qoder/commands/opsx/` |
|
||||
| Qwen Code | `.qwen/skills/` | `.qwen/commands/` |
|
||||
| RooCode | `.roo/skills/` | `.roo/commands/` |
|
||||
| Trae | `.trae/skills/` | `.trae/skills/` (via `/openspec-*`) |
|
||||
| Windsurf | `.windsurf/skills/` | `.windsurf/workflows/` |
|
||||
|
||||
\* Codex commands are installed in the global Codex home (`$CODEX_HOME/prompts/` if set, otherwise `~/.codex/prompts/`), not your project directory.
|
||||
\* Codex commands are installed to the global home directory (`~/.codex/prompts/` or `$CODEX_HOME/prompts/`), not the project directory.
|
||||
|
||||
\*\* GitHub Copilot prompt files are recognized as custom slash commands in IDE extensions (VS Code, JetBrains, Visual Studio). Copilot CLI does not currently consume `.github/prompts/*.prompt.md` directly.
|
||||
\*\* GitHub Copilot's `.github/prompts/*.prompt.md` files are recognized as custom slash commands in **IDE extensions only** (VS Code, JetBrains, Visual Studio). GitHub Copilot CLI does not currently support custom prompts from this directory — see [github/copilot-cli#618](https://github.com/github/copilot-cli/issues/618). If you use Copilot CLI, you may need to manually set up [custom agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents) in `.github/agents/` as a workaround.
|
||||
|
||||
## Non-Interactive Setup
|
||||
|
||||
For CI/CD or scripted setup, use `--tools` (and optionally `--profile`):
|
||||
For CI/CD or scripted setup, use the `--tools` flag:
|
||||
|
||||
```bash
|
||||
# Configure specific tools
|
||||
@@ -69,40 +55,34 @@ openspec init --tools all
|
||||
|
||||
# Skip tool configuration
|
||||
openspec init --tools none
|
||||
|
||||
# Override profile for this init run
|
||||
openspec init --profile core
|
||||
```
|
||||
|
||||
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `forgecode`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `junie`, `kilocode`, `kimi`, `kiro`, `opencode`, `pi`, `qoder`, `lingma`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
**Available tool IDs:** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codebuddy`, `codex`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
|
||||
|
||||
## Workflow-Dependent Installation
|
||||
## What Gets Installed
|
||||
|
||||
OpenSpec installs workflow artifacts based on selected workflows:
|
||||
For each tool, OpenSpec generates 10 skill files that power the OPSX workflow:
|
||||
|
||||
- **Core profile (default):** `propose`, `explore`, `apply`, `sync`, `archive`
|
||||
- **Custom selection:** any subset of all workflow IDs:
|
||||
`propose`, `explore`, `new`, `continue`, `apply`, `ff`, `sync`, `archive`, `bulk-archive`, `verify`, `onboard`
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `openspec-explore` | Thinking partner for exploring ideas |
|
||||
| `openspec-new-change` | Start a new change |
|
||||
| `openspec-continue-change` | Create the next artifact |
|
||||
| `openspec-ff-change` | Fast-forward through all planning artifacts |
|
||||
| `openspec-apply-change` | Implement tasks |
|
||||
| `openspec-verify-change` | Verify implementation completeness |
|
||||
| `openspec-sync-specs` | Sync delta specs to main (optional—archive prompts if needed) |
|
||||
| `openspec-archive-change` | Archive a completed change |
|
||||
| `openspec-bulk-archive-change` | Archive multiple changes at once |
|
||||
| `openspec-onboard` | Guided onboarding through a complete workflow cycle |
|
||||
|
||||
In other words, skill/command counts are profile-dependent and delivery-dependent, not fixed.
|
||||
These skills are invoked via slash commands like `/opsx:new`, `/opsx:apply`, etc. See [Commands](commands.md) for the full list.
|
||||
|
||||
## Generated Skill Names
|
||||
## Adding a New Tool
|
||||
|
||||
When selected by profile/workflow config, OpenSpec generates these skills:
|
||||
Want to add support for another AI coding assistant? Check out the [command adapter pattern](../CONTRIBUTING.md) or open an issue on GitHub.
|
||||
|
||||
- `openspec-propose`
|
||||
- `openspec-explore`
|
||||
- `openspec-new-change`
|
||||
- `openspec-continue-change`
|
||||
- `openspec-apply-change`
|
||||
- `openspec-ff-change`
|
||||
- `openspec-sync-specs`
|
||||
- `openspec-archive-change`
|
||||
- `openspec-bulk-archive-change`
|
||||
- `openspec-verify-change`
|
||||
- `openspec-onboard`
|
||||
|
||||
See [Commands](commands.md) for command behavior and [CLI](cli.md) for `init`/`update` options.
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
+7
-34
@@ -28,33 +28,7 @@ OPSX (fluid actions):
|
||||
|
||||
> **Customization:** OPSX workflows are driven by schemas that define artifact sequences. See [Customization](customization.md) for details on creating custom schemas.
|
||||
|
||||
## Two Modes
|
||||
|
||||
### Default Quick Path (`core` profile)
|
||||
|
||||
New installs default to `core`, which provides:
|
||||
- `/opsx:propose`
|
||||
- `/opsx:explore`
|
||||
- `/opsx:apply`
|
||||
- `/opsx:sync`
|
||||
- `/opsx:archive`
|
||||
|
||||
Typical flow:
|
||||
|
||||
```text
|
||||
/opsx:propose ──► /opsx:apply ──► /opsx:sync ──► /opsx:archive
|
||||
```
|
||||
|
||||
### Expanded/Full Workflow (custom selection)
|
||||
|
||||
If you want explicit scaffold-and-build commands (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), enable them with:
|
||||
|
||||
```bash
|
||||
openspec config profile
|
||||
openspec update
|
||||
```
|
||||
|
||||
## Workflow Patterns (Expanded Mode)
|
||||
## Workflow Patterns
|
||||
|
||||
### Quick Feature
|
||||
|
||||
@@ -434,16 +408,15 @@ For full command details and options, see [Commands](commands.md).
|
||||
|
||||
| Command | Purpose | When to Use |
|
||||
|---------|---------|-------------|
|
||||
| `/opsx:propose` | Create change + planning artifacts | Fast default path (`core` profile) |
|
||||
| `/opsx:explore` | Think through ideas | Unclear requirements, investigation |
|
||||
| `/opsx:new` | Start a change scaffold | Expanded mode, explicit artifact control |
|
||||
| `/opsx:continue` | Create next artifact | Expanded mode, step-by-step artifact creation |
|
||||
| `/opsx:ff` | Create all planning artifacts | Expanded mode, clear scope |
|
||||
| `/opsx:new` | Start a change | Beginning any new work |
|
||||
| `/opsx:continue` | Create next artifact | Step-by-step artifact creation |
|
||||
| `/opsx:ff` | Create all planning artifacts | Clear scope, ready to build |
|
||||
| `/opsx:apply` | Implement tasks | Ready to write code |
|
||||
| `/opsx:verify` | Validate implementation | Expanded mode, before archiving |
|
||||
| `/opsx:sync` | Merge delta specs | Expanded mode, optional |
|
||||
| `/opsx:verify` | Validate implementation | Before archiving, catch mismatches |
|
||||
| `/opsx:sync` | Merge delta specs | Optional—archive prompts if needed |
|
||||
| `/opsx:archive` | Complete the change | All work finished |
|
||||
| `/opsx:bulk-archive` | Archive multiple changes | Expanded mode, parallel work |
|
||||
| `/opsx:bulk-archive` | Archive multiple changes | Parallel work, batch completion |
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-23
|
||||
@@ -1,3 +0,0 @@
|
||||
# add-kimi-cli-skills-only-support
|
||||
|
||||
Add Kimi CLI as a supported skills-only tool without a command adapter
|
||||
@@ -1,85 +0,0 @@
|
||||
## Context
|
||||
|
||||
Kimi CLI is not another Claude/Codex-style adapter target. Its extension model is built around discovered skills, not external command files:
|
||||
|
||||
- skills are discovered from `.kimi/skills/`
|
||||
- skills are exposed as `/skill:<name>`
|
||||
- no stable `.kimi/commands/` or prompt-file loading mechanism was found in the Kimi CLI codebase
|
||||
|
||||
OpenSpec's existing architecture can already represent that shape:
|
||||
|
||||
- `AI_TOOLS` can advertise a `skillsDir`
|
||||
- `init` can install skills for any selected tool with `skillsDir`
|
||||
- when command generation is attempted for a tool without an adapter, OpenSpec already records `commandsSkipped`
|
||||
|
||||
## Goals
|
||||
|
||||
- Add Kimi CLI using the same narrow `skills-only` pattern already used by Trae
|
||||
- Keep the implementation small: metadata, docs, and a focused regression test
|
||||
- Make the spec text match the current code path for adapterless tools
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- designing a Kimi-specific command adapter without upstream support
|
||||
- changing tool capability modeling across the whole generation pipeline
|
||||
- reworking `delivery=commands` behavior for all adapterless tools
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Represent Kimi CLI as an adapterless tool with `.kimi`
|
||||
|
||||
Add a new `AI_TOOLS` entry:
|
||||
|
||||
```ts
|
||||
{ name: 'Kimi CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' }
|
||||
```
|
||||
|
||||
This matches Kimi CLI's project-local skills root and lets existing init/update detection paths treat it as a supported tool.
|
||||
|
||||
### 2. Do not add a Kimi command adapter
|
||||
|
||||
No `src/core/command-generation/adapters/kimi.ts` file will be added, and the command adapter registry will remain unchanged.
|
||||
|
||||
Rationale:
|
||||
|
||||
- Kimi CLI exposes skills dynamically as `/skill:<name>`
|
||||
- the previous upstream PR stalled specifically because no legitimate adapter target was available
|
||||
- adding a fake `.kimi/commands/...` path would create behavior OpenSpec cannot justify against upstream Kimi CLI behavior
|
||||
|
||||
### 3. Document Kimi by its real invocation surface
|
||||
|
||||
Kimi documentation in OpenSpec must use Kimi's actual skill invocation form:
|
||||
|
||||
- supported-tools: no generated command files, use `/skill:openspec-*`
|
||||
- commands doc: examples such as `/skill:openspec-propose`
|
||||
|
||||
The docs must not claim generated `opsx-*` files or `/openspec-*` direct invocations for Kimi.
|
||||
|
||||
### 4. Keep the change compatible with existing Trae-style behavior
|
||||
|
||||
This change intentionally follows the current adapterless-tool behavior already present in the codebase:
|
||||
|
||||
- skills are created whenever delivery includes skills
|
||||
- command generation is skipped when no adapter exists
|
||||
- init output reports `Commands skipped for: kimi (no adapter)`
|
||||
|
||||
This keeps the Kimi change small and avoids overlapping implementation work already captured in `add-tool-command-surface-capabilities`.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
Add one focused regression test in `test/core/init.test.ts`:
|
||||
|
||||
- configure `delivery=both`
|
||||
- run init with `--tools kimi`
|
||||
- verify Kimi skills are created under `.kimi/skills/...`
|
||||
- verify init reports the skipped command generation path for `kimi`
|
||||
|
||||
That test is enough for this narrow change because:
|
||||
|
||||
- adapterless update behavior already has generic coverage
|
||||
- CLI tool-id rendering is derived from `AI_TOOLS`
|
||||
- no command adapter or path formatting logic is being introduced
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
The main trade-off is scope: Kimi will inherit the current adapterless-tool behavior, including the broader limitation that `delivery=commands` is not yet capability-aware for skills-invocable tools. That is acceptable for this change because it matches the existing Trae/ForgeCode model and keeps the implementation aligned with verified Kimi CLI behavior.
|
||||
@@ -1,38 +0,0 @@
|
||||
## Why
|
||||
|
||||
OpenSpec already has user demand for Kimi CLI support, but the previous upstream attempt stalled because it assumed Kimi needed a command adapter. Local review of the Kimi CLI codebase shows a different integration surface: Kimi discovers `SKILL.md` files from `.kimi/skills/` and exposes them through `/skill:<name>`, but it does not provide a stable, file-based custom command directory like Claude Code or Codex.
|
||||
|
||||
OpenSpec already supports tools that install skills without a command adapter. Trae and ForgeCode are the existing examples. Kimi should follow the same pattern instead of introducing undocumented `.kimi/commands/...` behavior.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add Kimi CLI as a supported tool in `AI_TOOLS` with `skillsDir: '.kimi'`
|
||||
- Document Kimi CLI as a skills-only integration in supported tools and command usage docs
|
||||
- Align change specs so `cli-init` explicitly allows selected tools with `skillsDir` but no registered command adapter
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
_None._
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `ai-tool-paths`: define the `.kimi` skills root for Kimi CLI
|
||||
- `cli-init`: clarify that adapterless tools remain valid selections and skip command-file generation with an informational message
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/core/config.ts` - add Kimi CLI tool metadata
|
||||
- `docs/supported-tools.md` - add Kimi CLI row and tool id
|
||||
- `docs/commands.md` - document `/skill:openspec-*` usage for Kimi CLI
|
||||
- `docs/cli.md` - include `kimi` in the supported `--tools` list
|
||||
- `test/core/init.test.ts` - cover Kimi CLI as an adapterless tool during init
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Adding `src/core/command-generation/adapters/kimi.ts`
|
||||
- Defining a `.kimi/commands/...` output path
|
||||
- Changing the broader delivery model for adapterless tools under `delivery=commands`
|
||||
|
||||
That broader capability-aware delivery work is already being explored separately in `add-tool-command-surface-capabilities`. This change stays narrow and follows the existing Trae/ForgeCode pattern.
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
# ai-tool-paths Delta Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Path configuration for supported tools
|
||||
|
||||
The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent Skills specification.
|
||||
|
||||
#### Scenario: Kimi CLI paths defined
|
||||
|
||||
- **WHEN** looking up the `kimi` tool
|
||||
- **THEN** `skillsDir` SHALL be `.kimi`
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
# cli-init Delta Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Slash Command Generation
|
||||
|
||||
The command SHALL generate opsx slash commands only for selected tools that have a registered command adapter, while keeping adapterless tools valid for skill generation.
|
||||
|
||||
#### Scenario: Generating slash commands for a tool with a registered adapter
|
||||
|
||||
- **WHEN** a tool with a registered command adapter is selected during initialization
|
||||
- **THEN** create 9 slash command files using the tool's command adapter:
|
||||
- `/opsx:explore`
|
||||
- `/opsx:new`
|
||||
- `/opsx:continue`
|
||||
- `/opsx:apply`
|
||||
- `/opsx:ff`
|
||||
- `/opsx:verify`
|
||||
- `/opsx:sync`
|
||||
- `/opsx:archive`
|
||||
- `/opsx:bulk-archive`
|
||||
- **AND** use tool-specific path conventions (e.g., `.claude/commands/opsx/` for Claude)
|
||||
- **AND** include tool-specific frontmatter format
|
||||
|
||||
#### Scenario: Selected tool has no command adapter
|
||||
|
||||
- **GIVEN** a selected tool has `skillsDir` configured but no registered command adapter
|
||||
- **WHEN** initialization includes command generation
|
||||
- **THEN** skill generation for that tool SHALL still remain valid
|
||||
- **AND** command-file generation SHALL be skipped for that tool
|
||||
- **AND** the command output SHALL include `Commands skipped for: <tool-id> (no adapter)`
|
||||
|
||||
#### Scenario: Kimi CLI skips command-file generation
|
||||
|
||||
- **WHEN** the user selects Kimi CLI during initialization
|
||||
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi'`
|
||||
- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered
|
||||
@@ -1,22 +0,0 @@
|
||||
## 1. Change Artifacts
|
||||
|
||||
- [x] 1.1 Write proposal, design, and spec deltas for Kimi CLI skills-only support
|
||||
|
||||
## 2. Tool Metadata
|
||||
|
||||
- [x] 2.1 Add `Kimi CLI` to `src/core/config.ts` with `value: 'kimi'` and `skillsDir: '.kimi'`
|
||||
|
||||
## 3. Documentation
|
||||
|
||||
- [x] 3.1 Update `docs/supported-tools.md` with a Kimi CLI row that clearly states there is no command adapter
|
||||
- [x] 3.2 Update `docs/commands.md` to document Kimi CLI usage via `/skill:openspec-*`
|
||||
- [x] 3.3 Update `docs/cli.md` so the supported `--tools` list includes `kimi`
|
||||
|
||||
## 4. Tests
|
||||
|
||||
- [x] 4.1 Add a targeted init regression test for `--tools kimi` under adapterless command generation
|
||||
|
||||
## 5. Validation
|
||||
|
||||
- [x] 5.1 Validate the change artifacts with `openspec validate`
|
||||
- [x] 5.2 Run targeted tests and fix any regressions
|
||||
@@ -1,208 +0,0 @@
|
||||
## Product Model
|
||||
|
||||
An OpenSpec workspace is the durable planning home for work that spans multiple repos or folders.
|
||||
|
||||
It should feel like this:
|
||||
|
||||
```text
|
||||
workspace = where related changes live
|
||||
link = a named repo or folder the workspace can plan against
|
||||
change = one feature, fix, project, or other planned piece of work
|
||||
```
|
||||
|
||||
The foundation intentionally avoids the rest of the workflow. It only defines how OpenSpec recognizes a workspace, where managed workspaces live, how linked paths are represented, and how shared state differs from local state.
|
||||
|
||||
A workspace is not a feature. It can hold many changes over time. The linked repos or folders provide planning context, while the code stays where it is.
|
||||
|
||||
## Workspace Shape
|
||||
|
||||
OpenSpec workspaces use this shape:
|
||||
|
||||
```text
|
||||
workspace-root/
|
||||
changes/ # workspace-level proposals, tasks, specs
|
||||
.openspec-workspace/
|
||||
workspace.yaml # shared workspace information
|
||||
local.yaml # this machine's paths and preferences
|
||||
```
|
||||
|
||||
The user-facing planning surface is `changes/`. The identity file that makes the directory a workspace is `.openspec-workspace/workspace.yaml`.
|
||||
|
||||
Repo-local projects keep the existing shape:
|
||||
|
||||
```text
|
||||
repo-root/
|
||||
openspec/
|
||||
specs/
|
||||
changes/
|
||||
```
|
||||
|
||||
That distinction lets a user or agent tell which surface they are working in:
|
||||
|
||||
```text
|
||||
coordination workspace -> shared cross-repo planning
|
||||
repo-local project -> repo-owned specs and implementation planning
|
||||
```
|
||||
|
||||
Users should not run repo-local `openspec init` inside the workspace root. A workspace is already an OpenSpec coordination surface; it is not a product repo adopting repo-local OpenSpec.
|
||||
|
||||
## Workspace Names
|
||||
|
||||
A workspace name is a simple folder-style identifier, not a display name.
|
||||
|
||||
The name must be usable as a folder name in the current runtime. It must not be empty, must not be `.` or `..`, and must not contain path separators.
|
||||
|
||||
OpenSpec should not maintain a cross-platform reserved-name list in this slice. Setup/create flows should let filesystem creation surface OS-specific invalid folder names, then report that failure clearly.
|
||||
|
||||
The same workspace name is stored in `.openspec-workspace/workspace.yaml`, used as the default managed workspace folder name, and used as the local registry name.
|
||||
|
||||
## Shared And Local State
|
||||
|
||||
Workspace state follows a simple sharing rule:
|
||||
|
||||
```text
|
||||
share stable link names and planning
|
||||
keep local checkout paths local
|
||||
```
|
||||
|
||||
Expected shared state:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
name: platform
|
||||
links:
|
||||
api: {}
|
||||
web: {}
|
||||
```
|
||||
|
||||
Expected local state:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
paths:
|
||||
api: /repos/api
|
||||
web: /repos/web
|
||||
```
|
||||
|
||||
Later slices can expand these shapes, but the product rule should stay stable: a shared workspace should not commit one user's absolute checkout paths.
|
||||
|
||||
OpenSpec-created workspaces should include an ignore rule for `.openspec-workspace/local.yaml` so local checkout paths are not accidentally shared. `.openspec-workspace/workspace.yaml` remains the portable workspace identity and link-name state.
|
||||
|
||||
## Workspace Location
|
||||
|
||||
OpenSpec should create managed workspaces in one standard place:
|
||||
|
||||
```text
|
||||
getGlobalDataDir()/workspaces
|
||||
```
|
||||
|
||||
That reuses existing OpenSpec data-directory behavior:
|
||||
|
||||
- `$XDG_DATA_HOME/openspec/workspaces` when `XDG_DATA_HOME` is set
|
||||
- `~/.local/share/openspec/workspaces` on Unix/macOS fallback
|
||||
- `%LOCALAPPDATA%\openspec\workspaces` on native Windows fallback
|
||||
|
||||
This slice intentionally does not define a workspace-specific environment-variable, command, or configuration override for managed workspace storage. Tests should rely on existing global data-directory controls and test helpers instead of a separate workspace-home override.
|
||||
|
||||
This is deliberately quiet. The product should not ask most users where workspaces should live.
|
||||
|
||||
OpenSpec should show the resolved workspace path after setup. Quiet defaults should avoid a prompt, not hide where planning files were created.
|
||||
|
||||
## Local Workspace Registry
|
||||
|
||||
OpenSpec should keep a lightweight local registry of known workspaces:
|
||||
|
||||
```text
|
||||
getGlobalDataDir()/workspaces/registry.yaml
|
||||
```
|
||||
|
||||
Expected registry state:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
workspaces:
|
||||
platform: /Users/tabish/.local/share/openspec/workspaces/platform
|
||||
checkout: /Users/tabish/.local/share/openspec/workspaces/checkout
|
||||
```
|
||||
|
||||
The registry is a local index, not the source of truth. It exists so workspace commands can work from anywhere, show a picker when multiple workspaces exist, and list known workspaces without scanning arbitrary folders.
|
||||
|
||||
Each workspace folder remains authoritative for its own `.openspec-workspace/workspace.yaml` and `.openspec-workspace/local.yaml`. If a registry entry points at a missing or invalid workspace, later check/list flows can report that and suggest a repair.
|
||||
|
||||
## Windows And WSL2
|
||||
|
||||
Path behavior is runtime-local:
|
||||
|
||||
- PowerShell/native Windows uses Windows paths and Windows data-directory fallback.
|
||||
- WSL2 uses Linux paths and Linux/XDG fallback inside WSL.
|
||||
- Local repo paths are stored as the user supplied them for the current runtime.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
PowerShell:
|
||||
default base -> %LOCALAPPDATA%\openspec\workspaces
|
||||
|
||||
WSL2:
|
||||
default base -> ~/.local/share/openspec/workspaces
|
||||
```
|
||||
|
||||
This slice should not translate between `D:\repo`, `/mnt/d/repo`, and `\\wsl$` paths. Cross-runtime translation can be reconsidered later if an agent-launch workflow requires it.
|
||||
|
||||
## Link Names
|
||||
|
||||
A link name is the stable way to refer to a repo or folder inside workspace planning.
|
||||
|
||||
The local path can vary by machine:
|
||||
|
||||
```text
|
||||
shared link name: landing
|
||||
Tabish path: /Users/tabish/repos/landing
|
||||
Windows path: D:\repos\landing
|
||||
WSL2 path: /mnt/d/repos/landing
|
||||
```
|
||||
|
||||
Later workflows should refer to `landing` in workspace planning, status, and apply context. The local path is only how the current machine finds that repo or folder.
|
||||
|
||||
Link names are intentionally minimal: they must be non-empty, must not be `.` or `..`, must not contain path separators, and must be unique within the workspace.
|
||||
|
||||
The owning repo or folder remains the home of canonical specs and implementation work. The workspace makes the cross-boundary plan legible; it does not take ownership away from the linked repos or folders.
|
||||
|
||||
Link names are normally inferred from the folder basename in guided flows. Direct flows can allow an explicit name when the default would conflict or be unclear.
|
||||
|
||||
## Linked Repos And Folders
|
||||
|
||||
Workspace planning visibility should not require repo-local OpenSpec state.
|
||||
|
||||
That matters for two common cases:
|
||||
|
||||
- a repo has not adopted OpenSpec yet, but still needs to be considered in planning
|
||||
- a large monorepo has folders such as packages, services, or apps that should be planned like separate areas, without each folder having its own `openspec/`
|
||||
|
||||
Foundation should allow the link model to describe both:
|
||||
|
||||
```text
|
||||
multi-repo:
|
||||
api -> /repos/api
|
||||
web -> /repos/web
|
||||
|
||||
large monorepo:
|
||||
billing -> /repos/platform/services/billing
|
||||
checkout -> /repos/platform/apps/checkout
|
||||
```
|
||||
|
||||
Later apply/verify/archive workflows can decide what extra readiness is needed for implementation. Planning should be able to start before that.
|
||||
|
||||
Linking only records the relationship between a workspace link name and a local path. It must not create, copy, move, initialize, or edit files inside the linked repo or folder.
|
||||
|
||||
Repo-local spec availability is computed when needed. For example, `repo_specs_path` can be reported by a later doctor command when a linked path contains `openspec/specs`, but that path should not be treated as required workspace state.
|
||||
|
||||
## Later Slices
|
||||
|
||||
This foundation stops before user-facing workspace workflows:
|
||||
|
||||
- `workspace-create-and-register-repos` owns setup, link, relink, list, and doctor behavior.
|
||||
- `workspace-open-agent-context` owns agent launch context.
|
||||
- `workspace-change-planning` owns workspace proposals and repo scope.
|
||||
- `workspace-apply-repo-slice` owns implementation of one repo slice.
|
||||
- `workspace-verify-and-archive` owns completion and archive behavior.
|
||||
@@ -1,142 +0,0 @@
|
||||
## Why
|
||||
|
||||
Users need a workspace to feel like the obvious home for planning across multiple repos or folders.
|
||||
|
||||
They should be able to think:
|
||||
|
||||
```text
|
||||
I have repos or folders that are often planned together.
|
||||
I create an OpenSpec workspace.
|
||||
That workspace is where changes live.
|
||||
My code stays where it is.
|
||||
OpenSpec links the workspace to those local paths.
|
||||
```
|
||||
|
||||
A workspace is not a feature. It is the durable planning home. Individual features, fixes, and projects are changes inside the workspace.
|
||||
|
||||
Users should not have to choose a storage location, create a change early, or understand internal workspace state before OpenSpec can orient itself.
|
||||
|
||||
The POC proved that workspace state is useful. This reimplementation should turn that into a simple product model that users and agents can explain without special-case vocabulary.
|
||||
|
||||
## What Changes
|
||||
|
||||
This change defines the user-facing foundation for OpenSpec workspaces.
|
||||
|
||||
An OpenSpec workspace has a recognizable planning home:
|
||||
|
||||
```text
|
||||
workspace-root/
|
||||
changes/
|
||||
.openspec-workspace/
|
||||
```
|
||||
|
||||
`changes/` is where workspace-level planning lives. `.openspec-workspace/` identifies the directory as an OpenSpec workspace and stores workspace state.
|
||||
|
||||
OpenSpec-managed workspaces live in one standard location:
|
||||
|
||||
```text
|
||||
<global-data-dir>/workspaces/
|
||||
```
|
||||
|
||||
Users should not need to choose that location. OpenSpec still shows the workspace path after setup so users know where planning files live. This foundation slice does not provide a workspace-specific environment-variable or configuration override for managed workspace storage.
|
||||
|
||||
OpenSpec also keeps a lightweight local registry of known workspaces on the current machine. The registry powers global commands, pickers, and listing, but each workspace folder remains the source of truth.
|
||||
|
||||
Workspace state is split by user expectation:
|
||||
|
||||
- shared workspace information can move between machines
|
||||
- local checkout paths stay local to each machine
|
||||
- linked repos and folders are referred to by stable link names, not by absolute paths
|
||||
|
||||
A linked path can be a full repo, a folder inside a monorepo, or another existing folder the workspace should plan against. A linked path does not need repo-local `openspec/` state before it can be included in workspace planning. Repo-local OpenSpec state may still matter later for implementation, verification, or archive workflows, but it is not a prerequisite for planning visibility.
|
||||
|
||||
Native Windows/PowerShell and WSL2 are both supported. Each runtime uses its own path conventions. OpenSpec does not translate paths between Windows and WSL in this foundation slice.
|
||||
|
||||
## Outcome
|
||||
|
||||
After this change, later workspace features can rely on one clear product contract:
|
||||
|
||||
- OpenSpec can tell when the user is inside a workspace.
|
||||
- OpenSpec knows where to create managed workspaces by default.
|
||||
- OpenSpec can keep a local registry of known workspaces.
|
||||
- A workspace has one visible planning area: `changes/`.
|
||||
- Workspace state is distinguishable from repo-local `openspec/` state.
|
||||
- Shared workspace state does not force one user's local paths onto another user.
|
||||
- Workspace planning can reference existing repos or folders by stable link names.
|
||||
- Linked repos or folders do not need repo-local OpenSpec state for workspace planning.
|
||||
- Multi-repo and large-monorepo work can use the same workspace planning model.
|
||||
- Repo-owned specs and implementation remain owned by their repos or source areas.
|
||||
- Windows, PowerShell, and WSL2 path behavior is predictable.
|
||||
|
||||
This change does not deliver the full workspace workflow. It gives `workspace-create-and-register-repos` the foundation it needs to add the first user-facing commands.
|
||||
|
||||
## POC Findings
|
||||
|
||||
Behavior to preserve:
|
||||
|
||||
- A workspace is a durable coordination home for cross-repo planning.
|
||||
- The workspace has a visible `changes/` directory at its root.
|
||||
- Linked repos and folders provide the context the workspace can plan against.
|
||||
- Stable link names matter more than local checkout paths.
|
||||
- Local machine paths should not become shared workspace state.
|
||||
- Canonical specs and implementation still belong to the owning repos.
|
||||
|
||||
Lessons to carry forward:
|
||||
|
||||
- The POC's hidden `.openspec/` workspace metadata shape made workspace state too easy to confuse with repo-local OpenSpec state.
|
||||
- Users should not need to run repo-local `openspec init` inside the workspace root.
|
||||
- The POC's requirement that registered repos already have `openspec/` is too strict for planning. Repos and folders should be linkable before they adopt repo-local OpenSpec state.
|
||||
- Repo or folder visibility should not depend on creating a change.
|
||||
- Workspace setup should not imply repo-local implementation, branch, worktree, apply, verify, or archive behavior.
|
||||
- `add-repo` is too narrow for the user-facing model. Linking an existing repo or folder is clearer.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Workspace identity directory: `.openspec-workspace/`.
|
||||
- Workspace identity file: `.openspec-workspace/workspace.yaml`.
|
||||
- Workspace name: a valid folder name for the current OS, excluding empty names, `.`/`..`, and path separators.
|
||||
- Workspace name usage: stored in `workspace.yaml`, used as the default managed workspace folder name, and used as the local registry name.
|
||||
- Planning surface: top-level `changes/`.
|
||||
- Local machine state: `.openspec-workspace/local.yaml`.
|
||||
- Local machine state exclusion: OpenSpec-created workspaces exclude `.openspec-workspace/local.yaml` from portable collaboration state by default.
|
||||
- Local workspace registry: `<global-data-dir>/workspaces/registry.yaml`.
|
||||
- Default workspace base: `<global-data-dir>/workspaces/`.
|
||||
- Platform behavior: native Windows and WSL2 each use the path conventions of the runtime running OpenSpec.
|
||||
- Linked paths may be full repos, monorepo folders, or other existing folders.
|
||||
- Link names: non-empty stable names, unique within a workspace, excluding `.`/`..` and path separators.
|
||||
- Repo-local `openspec/` state is not required for workspace planning visibility.
|
||||
- Linking records the relationship only; it does not create, copy, move, initialize, or edit files in the linked repo or folder.
|
||||
|
||||
Planning dependency:
|
||||
|
||||
- None. This is the first implementation slice.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No complete `openspec workspace setup`, `openspec workspace link`, or `openspec workspace relink` flow yet.
|
||||
- No public `openspec workspace create` command in the first user-facing workspace flow.
|
||||
- No user-facing command, environment variable, or configuration setting for changing the standard workspace location.
|
||||
- No question that asks users where OpenSpec should store workspaces by default.
|
||||
- No automatic Windows-to-WSL or WSL-to-Windows path translation.
|
||||
- No workspace-open agent launch behavior.
|
||||
- No workspace-level proposal creation.
|
||||
- No repo-slice apply, verify, archive, branch, or worktree behavior.
|
||||
- No copying workspace planning files into linked repos or folders as a side effect of creating, detecting, or linking a workspace.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-foundation`: Defines the product foundation for OpenSpec workspaces.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `openspec-conventions`: Describes how coordination workspaces differ from repo-local OpenSpec projects.
|
||||
|
||||
## Impact
|
||||
|
||||
- Workspace recognition and path behavior.
|
||||
- Workspace state parsing.
|
||||
- Local workspace registry parsing.
|
||||
- Documentation and agent guidance for the workspace mental model.
|
||||
- Later workspace slices should build on this contract instead of redefining workspace storage, identity, registry, or path behavior.
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Workspace Product Language
|
||||
OpenSpec conventions SHALL describe coordination workspaces in user-facing product terms.
|
||||
|
||||
#### Scenario: Describing workspace structure
|
||||
- **WHEN** OpenSpec documentation describes workspace support
|
||||
- **THEN** it SHALL present a workspace as the planning home for work across linked repos or folders
|
||||
- **AND** it SHALL describe `changes/` as the workspace planning area
|
||||
|
||||
#### Scenario: Avoiding internal workspace vocabulary
|
||||
- **WHEN** OpenSpec documentation explains what a workspace includes
|
||||
- **THEN** it SHALL prefer plain product language such as "repos or folders"
|
||||
- **AND** it SHALL avoid user-facing reliance on terms such as "working set", "code area", "entry", "alias", or "local overlay"
|
||||
|
||||
#### Scenario: Distinguishing workspaces from changes
|
||||
- **WHEN** OpenSpec documentation explains workspace planning
|
||||
- **THEN** it SHALL describe a workspace as a durable planning home
|
||||
- **AND** it SHALL describe individual features, fixes, and projects as changes inside the workspace
|
||||
|
||||
#### Scenario: Distinguishing workspace and repo-local surfaces
|
||||
- **WHEN** OpenSpec documentation compares workspace and repo-local flows
|
||||
- **THEN** it SHALL explain that workspace planning lives in the workspace root
|
||||
- **AND** it SHALL explain that repo-local specs and changes continue to live under each repo's `openspec/` directory
|
||||
|
||||
#### Scenario: Sequencing the workspace roadmap
|
||||
- **WHEN** workspace reimplementation work is split across multiple active changes
|
||||
- **THEN** conventions SHALL allow those changes to remain flat siblings under `openspec/changes/`
|
||||
- **AND** dependency order MAY be documented in proposal prose until formal change stacking metadata is available
|
||||
-199
@@ -1,199 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Recognizable Workspace Home
|
||||
OpenSpec SHALL give users and agents a recognizable workspace home for cross-repo planning.
|
||||
|
||||
#### Scenario: Planning across linked repos or folders
|
||||
- **WHEN** a user creates an OpenSpec workspace for repos or folders they plan across
|
||||
- **THEN** the workspace SHALL provide a durable planning home
|
||||
- **AND** the workspace SHALL be able to hold multiple changes over time
|
||||
|
||||
#### Scenario: Working from inside a workspace
|
||||
- **GIVEN** a user runs OpenSpec from a workspace root or one of its subdirectories
|
||||
- **WHEN** OpenSpec resolves the current workspace
|
||||
- **THEN** it SHALL identify the workspace root
|
||||
- **AND** it SHALL use the workspace root's `changes/` directory as the workspace planning area
|
||||
|
||||
#### Scenario: Avoiding accidental workspace mode
|
||||
- **GIVEN** a directory has `changes/` but is not an OpenSpec workspace
|
||||
- **WHEN** OpenSpec resolves the current workspace
|
||||
- **THEN** it SHALL avoid treating that directory as a workspace
|
||||
- **AND** it SHALL enter workspace mode only when the workspace identity file is present
|
||||
|
||||
### Requirement: Stable Workspace Name
|
||||
OpenSpec SHALL use one folder-style workspace name across workspace identity, managed storage, and the local registry.
|
||||
|
||||
#### Scenario: Using one workspace name
|
||||
- **WHEN** OpenSpec creates or registers a managed workspace
|
||||
- **THEN** the workspace name SHALL be stored in `.openspec-workspace/workspace.yaml`
|
||||
- **AND** the same name SHALL be used as the default managed workspace folder name
|
||||
- **AND** the same name SHALL be used as the local registry name
|
||||
|
||||
#### Scenario: Rejecting invalid folder-style names
|
||||
- **WHEN** OpenSpec accepts a workspace name
|
||||
- **THEN** it SHALL reject empty names, `.` or `..`, and names containing path separators
|
||||
- **AND** setup or create flows SHALL report OS-level folder creation failures clearly
|
||||
|
||||
### Requirement: Dedicated Workspace Identity
|
||||
OpenSpec SHALL distinguish a coordination workspace from a repo-local OpenSpec project.
|
||||
|
||||
#### Scenario: Reading workspace identity
|
||||
- **WHEN** OpenSpec reads or writes workspace identity and workspace state
|
||||
- **THEN** it SHALL use `.openspec-workspace/`
|
||||
|
||||
#### Scenario: Preserving repo-local OpenSpec projects
|
||||
- **GIVEN** a repo-local OpenSpec project uses `openspec/`
|
||||
- **WHEN** that repo is linked to a workspace
|
||||
- **THEN** OpenSpec SHALL continue treating `openspec/` as that repo's local OpenSpec directory
|
||||
- **AND** workspace planning SHALL remain anchored in the workspace root
|
||||
|
||||
#### Scenario: Avoiding repo-local initialization in the workspace root
|
||||
- **WHEN** a user is working from an OpenSpec workspace root
|
||||
- **THEN** OpenSpec SHALL treat that root as a workspace coordination surface
|
||||
- **AND** users SHALL not need to initialize a repo-local `openspec/` project inside the workspace root
|
||||
|
||||
### Requirement: Safe Workspace Sharing
|
||||
OpenSpec SHALL keep shared workspace information separate from local machine paths.
|
||||
|
||||
#### Scenario: Sharing workspace planning
|
||||
- **WHEN** a workspace is shared with another user or machine
|
||||
- **THEN** shared workspace information SHALL include portable workspace identity and stable link names
|
||||
- **AND** it SHALL not require another user to reuse the original user's absolute checkout paths
|
||||
|
||||
#### Scenario: Keeping checkout paths local
|
||||
- **WHEN** OpenSpec stores local paths for a workspace
|
||||
- **THEN** those paths SHALL be treated as local to the current machine and runtime
|
||||
- **AND** another machine MAY map the same link names to different local paths
|
||||
|
||||
#### Scenario: Preserving runtime-local paths
|
||||
- **WHEN** OpenSpec reads or writes local workspace paths
|
||||
- **THEN** it SHALL preserve path strings valid for the current runtime
|
||||
- **AND** it SHALL support native Windows paths and WSL2/Linux paths as local state values
|
||||
|
||||
#### Scenario: Excluding local state from portable collaboration
|
||||
- **WHEN** OpenSpec creates a workspace
|
||||
- **THEN** it SHALL exclude `.openspec-workspace/local.yaml` from portable collaboration state by default
|
||||
- **AND** `.openspec-workspace/workspace.yaml` SHALL remain the portable workspace identity and link-name state
|
||||
|
||||
### Requirement: Standard Workspace Location
|
||||
OpenSpec SHALL use a standard location for OpenSpec-managed workspaces without asking most users to choose one.
|
||||
|
||||
#### Scenario: Using the standard workspace location
|
||||
- **WHEN** OpenSpec needs the location for OpenSpec-managed workspaces
|
||||
- **THEN** it SHALL use `<global-data-dir>/workspaces`
|
||||
- **AND** `<global-data-dir>` SHALL follow existing OpenSpec XDG and platform data directory behavior
|
||||
|
||||
#### Scenario: Avoiding workspace-specific storage overrides
|
||||
- **WHEN** OpenSpec resolves the location for OpenSpec-managed workspaces
|
||||
- **THEN** it SHALL not use a workspace-specific environment variable, command, or configuration setting in this slice
|
||||
- **AND** managed workspace storage SHALL remain under `<global-data-dir>/workspaces`
|
||||
|
||||
#### Scenario: Running from native Windows
|
||||
- **WHEN** OpenSpec runs from native Windows shells such as PowerShell
|
||||
- **AND** `XDG_DATA_HOME` is not set
|
||||
- **THEN** OpenSpec SHALL store managed workspaces under the Windows global data location
|
||||
- **AND** paths SHALL follow native Windows path behavior
|
||||
|
||||
#### Scenario: Running from WSL2
|
||||
- **WHEN** OpenSpec runs from WSL2
|
||||
- **THEN** OpenSpec SHALL store managed workspaces under the Linux/XDG data location inside WSL
|
||||
- **AND** paths SHALL follow Linux path behavior inside WSL
|
||||
|
||||
#### Scenario: Using the workspace location automatically
|
||||
- **WHEN** OpenSpec creates or resolves OpenSpec-managed workspaces in later workflows
|
||||
- **THEN** it SHALL use the resolved workspace location by default
|
||||
- **AND** users SHALL be able to follow the normal workspace flow without choosing a storage location
|
||||
|
||||
#### Scenario: Showing the workspace path
|
||||
- **WHEN** OpenSpec creates a workspace in the standard workspace location
|
||||
- **THEN** it SHALL report the workspace path to the user
|
||||
- **AND** it SHALL not hide where planning files were created
|
||||
|
||||
#### Scenario: Staying in the current runtime
|
||||
- **WHEN** OpenSpec resolves workspace paths or local repo paths
|
||||
- **THEN** it SHALL interpret paths for the runtime running OpenSpec
|
||||
- **AND** Windows, UNC WSL, and WSL mount paths SHALL remain explicit user-provided paths
|
||||
|
||||
### Requirement: Local Workspace Registry
|
||||
OpenSpec SHALL keep a lightweight local registry of known workspaces on the current machine.
|
||||
|
||||
#### Scenario: Recording known workspaces
|
||||
- **WHEN** OpenSpec creates or learns about a managed workspace
|
||||
- **THEN** it SHALL be able to record the workspace name and path in a local registry
|
||||
- **AND** the registry SHALL be machine-local state
|
||||
|
||||
#### Scenario: Keeping workspace folders authoritative
|
||||
- **WHEN** OpenSpec reads workspace details
|
||||
- **THEN** each workspace folder's `.openspec-workspace/workspace.yaml` SHALL remain the source of truth for that workspace
|
||||
- **AND** the local registry SHALL act only as an index of known workspace paths
|
||||
|
||||
#### Scenario: Finding workspaces from anywhere
|
||||
- **WHEN** a later workspace command runs outside a workspace directory
|
||||
- **THEN** OpenSpec MAY use the local registry to find known workspaces
|
||||
- **AND** commands that need one workspace MAY use the registry to support an interactive picker
|
||||
|
||||
### Requirement: Stable Link Names
|
||||
OpenSpec SHALL use stable link names to refer to repos and folders in workspace planning.
|
||||
|
||||
#### Scenario: Referring to a repo or folder in workspace planning
|
||||
- **WHEN** workspace state or later workspace planning artifacts refer to a linked repo or folder
|
||||
- **THEN** they SHALL use the stable link name
|
||||
- **AND** the same link name SHALL remain valid even when local checkout paths differ
|
||||
|
||||
#### Scenario: Reusing link names across machines
|
||||
- **WHEN** a workspace is used on another machine
|
||||
- **THEN** link names SHALL remain stable
|
||||
- **AND** local checkout paths MAY differ on that machine
|
||||
|
||||
#### Scenario: Rejecting invalid link names
|
||||
- **WHEN** OpenSpec accepts a workspace link name
|
||||
- **THEN** it SHALL reject empty names, `.` or `..`, and names containing path separators
|
||||
- **AND** link names SHALL be unique within the workspace
|
||||
|
||||
### Requirement: Linked Repos And Folders
|
||||
OpenSpec SHALL allow workspace planning to include linked repos and folders before they have repo-local OpenSpec state.
|
||||
|
||||
#### Scenario: Planning with a repo that has not adopted OpenSpec
|
||||
- **WHEN** a workspace links a repo path that does not yet contain repo-local `openspec/`
|
||||
- **THEN** the repo SHALL still be available for workspace-level planning
|
||||
- **AND** implementation readiness MAY be handled by a later workflow
|
||||
|
||||
#### Scenario: Planning across monorepo folders
|
||||
- **WHEN** planning spans multiple packages, services, apps, or directories inside one monorepo
|
||||
- **THEN** the workspace SHALL be able to link those folders separately
|
||||
- **AND** each folder SHALL not need its own repo-local `openspec/` directory to participate in workspace planning
|
||||
|
||||
#### Scenario: Treating repos and folders consistently
|
||||
- **WHEN** a workspace plan includes both separate repos and folders inside a monorepo
|
||||
- **THEN** OpenSpec SHALL use the same planning model for both
|
||||
- **AND** users SHALL not need to create different kinds of workspace plans for multi-repo and monorepo changes
|
||||
|
||||
#### Scenario: Recording links without changing targets
|
||||
- **WHEN** OpenSpec records a link between a workspace and a local repo or folder
|
||||
- **THEN** it SHALL store the link in workspace state
|
||||
- **AND** it SHALL not create, copy, move, initialize, or edit files inside the linked repo or folder
|
||||
|
||||
### Requirement: Planning Before Implementation
|
||||
OpenSpec SHALL treat workspace creation and detection as planning setup, not implementation.
|
||||
|
||||
#### Scenario: Creating or detecting a workspace
|
||||
- **WHEN** a workspace exists
|
||||
- **THEN** OpenSpec SHALL treat it as a place for workspace-level planning
|
||||
- **AND** repo implementation files SHALL remain unchanged until an explicit implementation workflow runs
|
||||
|
||||
#### Scenario: Deferring repo implementation
|
||||
- **WHEN** repo-local implementation, apply, verify, or archive behavior is needed
|
||||
- **THEN** that behavior SHALL require an explicit later workspace workflow
|
||||
|
||||
### Requirement: Repo Ownership Boundaries
|
||||
OpenSpec SHALL keep repo ownership legible when planning happens in a workspace.
|
||||
|
||||
#### Scenario: Planning across owned repos
|
||||
- **WHEN** a workspace plan refers to behavior owned by a repo or source area
|
||||
- **THEN** that owner SHALL remain the home for canonical specs and implementation work
|
||||
- **AND** the workspace SHALL make the cross-boundary plan visible without taking ownership away from that owner
|
||||
|
||||
#### Scenario: Drafting before ownership is clear
|
||||
- **WHEN** cross-repo behavior is still being explored and ownership is not clear
|
||||
- **THEN** the workspace MAY hold planning notes or draft behavior
|
||||
- **AND** those drafts SHALL remain distinguishable from canonical repo-owned specs
|
||||
@@ -1,56 +0,0 @@
|
||||
## 1. POC Findings And Model Decisions
|
||||
|
||||
- [x] 1.1 Capture the foundation POC findings in the proposal/design artifacts
|
||||
- [x] 1.2 Settle `.openspec-workspace/` as the workspace metadata directory
|
||||
- [x] 1.3 Define the minimal workspace root shape and root marker
|
||||
- [x] 1.4 Define committed workspace state versus machine-local workspace state
|
||||
- [x] 1.5 Capture that workspace setup is useful only after at least one repo or folder is linked
|
||||
- [x] 1.6 Capture that repo-owned specs and implementation remain owned by repos
|
||||
- [x] 1.7 Capture that planning can include repos or monorepo folders without repo-local OpenSpec state
|
||||
- [x] 1.8 Capture that workspaces hold many changes and are not feature containers
|
||||
- [x] 1.9 Capture `link`/`relink` as the user-facing model instead of `add-repo`/`update-repo`
|
||||
|
||||
## 2. Foundation Helpers
|
||||
|
||||
- [x] 2.1 Add workspace path constants and helpers for `.openspec-workspace/`, `workspace.yaml`, `local.yaml`, and root `changes/`
|
||||
- [x] 2.2 Add workspace root detection from an arbitrary starting directory
|
||||
- [x] 2.3 Add typed parsing and validation for minimal shared workspace state
|
||||
- [x] 2.4 Add typed parsing and validation for minimal machine-local workspace state
|
||||
- [x] 2.5 Ensure repo-local `openspec/` projects are not mistaken for coordination workspaces
|
||||
- [x] 2.6 Add a standard workspace location resolver using `getGlobalDataDir()/workspaces`
|
||||
- [x] 2.7 Ensure workspace path helpers use platform path APIs and avoid hardcoded POSIX separators
|
||||
- [x] 2.8 Add local workspace registry path constants and helpers
|
||||
|
||||
## 3. Metadata And Local State
|
||||
|
||||
- [x] 3.1 Define the versioned shared-state shape with workspace name and stable link map
|
||||
- [x] 3.2 Define the versioned local-state shape with stable link names mapped to local paths
|
||||
- [x] 3.3 Ensure local-state files are treated as machine-local and OpenSpec-created workspaces exclude `.openspec-workspace/local.yaml` from portable collaboration state
|
||||
- [x] 3.4 Add validation for invalid versions, invalid link names, malformed link maps, and malformed local path maps
|
||||
- [x] 3.5 Preserve native Windows and WSL2 path strings when reading and writing local path state
|
||||
- [x] 3.6 Define the versioned local registry shape with workspace names mapped to workspace roots
|
||||
- [x] 3.7 Ensure the local registry is treated as a convenience index, not the workspace source of truth
|
||||
|
||||
## 4. Documentation And Guidance
|
||||
|
||||
- [x] 4.1 Document the coordination workspace mental model
|
||||
- [x] 4.2 Document how `.openspec-workspace/` differs from repo-local `openspec/`
|
||||
- [x] 4.3 Document stable link names as the way to refer to linked repos and folders
|
||||
- [x] 4.4 Document which behavior is intentionally deferred to later workspace slices
|
||||
- [x] 4.5 Document native Windows/PowerShell and WSL2 path behavior for managed workspace storage
|
||||
- [x] 4.6 Document linked repos/folders without repo-local OpenSpec and large-monorepo planning behavior
|
||||
- [x] 4.7 Document the local workspace registry and global command model
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- [x] 5.1 Add unit tests for root detection and non-detection cases
|
||||
- [x] 5.2 Add unit tests for shared-state and local-state parsing
|
||||
- [x] 5.3 Add unit tests for standard workspace location resolution with XDG/Linux fallback and native Windows fallback
|
||||
- [x] 5.4 Add unit tests that local-state parsing preserves native Windows and WSL2-style paths
|
||||
- [x] 5.5 Add unit tests for repo-local compatibility boundaries
|
||||
- [x] 5.6 Add tests or docs coverage that linked repos/folders do not require repo-local `openspec/`
|
||||
- [x] 5.7 Add tests or docs coverage for monorepo folder links under the same workspace model
|
||||
- [x] 5.8 Add tests for local registry parsing and stale registry entries
|
||||
- [x] 5.9 Add tests or docs coverage for `.openspec-workspace/local.yaml` exclusion in OpenSpec-created workspaces
|
||||
- [x] 5.10 Run `openspec validate workspace-foundation --strict`
|
||||
- [x] 5.11 Run targeted test coverage for the new workspace foundation helpers
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-25
|
||||
@@ -1,48 +0,0 @@
|
||||
## Context
|
||||
|
||||
The OpenCode adapter in `src/core/command-generation/adapters/opencode.ts` currently generates command files at `.opencode/command/opsx-<id>.md` (singular `command`). OpenCode's official documentation uses `.opencode/commands/` (plural), and every other adapter in the codebase follows the plural convention for commands directories. The legacy cleanup module in `src/core/legacy-cleanup.ts` also references the singular form for detecting old artifacts.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Align the OpenCode adapter path with OpenCode's official `.opencode/commands/` convention
|
||||
- Add the old singular path `.opencode/command/` to legacy cleanup so existing installations are properly cleaned
|
||||
- Update documentation to reflect the corrected path
|
||||
- Update test assertions to match the new path
|
||||
|
||||
**Non-Goals:**
|
||||
- Changing the OpenCode skill path (`.opencode/skills/`) — already correct
|
||||
- Modifying any other adapter's directory structure
|
||||
- Adding migration prompts or interactive upgrade flows
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Direct path rename in adapter
|
||||
|
||||
**Decision:** Change `path.join('.opencode', 'command', ...)` to `path.join('.opencode', 'commands', ...)` in the adapter's `getFilePath` method.
|
||||
|
||||
**Rationale:** This is a single-line change that aligns with the established pattern across all other adapters. No abstraction or indirection needed.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Add a configuration option for the directory name — rejected as over-engineering for a bug fix
|
||||
- Keep singular and add plural as alias — rejected as it creates ambiguity about which is canonical
|
||||
|
||||
### 2. Legacy cleanup via existing constant map
|
||||
|
||||
**Decision:** Update the `LEGACY_SLASH_COMMAND_PATHS` entry for `'opencode'` from `'.opencode/command/openspec-*.md'` to `'.opencode/command/opsx-*.md'` (the old singular path becomes the legacy pattern) and ensure the new path is handled by the current command generation pipeline.
|
||||
|
||||
**Rationale:** The existing legacy cleanup infrastructure uses `LEGACY_SLASH_COMMAND_PATHS` as an explicit lookup. The old singular-path pattern already matches the legacy format (`openspec-*` prefix from the old SlashCommandRegistry era). The current command generation uses the `opsx-*` prefix, so we also need to add a legacy pattern for `opsx-*` files in the old singular directory.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Add a separate migration script — rejected; the existing legacy cleanup mechanism handles this scenario
|
||||
|
||||
### 3. Documentation update
|
||||
|
||||
**Decision:** Update the `docs/supported-tools.md` table entry for OpenCode from `.opencode/command/opsx-<id>.md` to `.opencode/commands/opsx-<id>.md`.
|
||||
|
||||
**Rationale:** Documentation must match the actual generated paths.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Existing installations have files at old path]** → Mitigated by legacy cleanup detecting `.opencode/command/` artifacts. On next `openspec init`, old files are cleaned up and new files written to `.opencode/commands/`.
|
||||
- **[Users referencing old path in custom scripts]** → Low risk. The old path was incorrect per OpenCode's specification, so custom references were already misaligned.
|
||||
@@ -1,26 +0,0 @@
|
||||
## Why
|
||||
|
||||
The OpenCode adapter uses `.opencode/command/` (singular) for its commands directory, but OpenCode's official documentation specifies `.opencode/commands/` (plural). Every other adapter in the codebase also uses plural directory names (`.claude/commands/`, `.cursor/commands/`, `.factory/commands/`, etc.). This inconsistency was introduced in Oct 2025 without documented rationale. Fixes [#748](https://github.com/Fission-AI/OpenSpec/issues/748).
|
||||
|
||||
## What Changes
|
||||
|
||||
- OpenCode adapter path changes from `.opencode/command/` to `.opencode/commands/`
|
||||
- Legacy cleanup adds `.opencode/command/` (old singular path) for backward compatibility
|
||||
- Documentation updated to reflect the new plural path
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
_None._
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `command-generation`: OpenCode adapter path changes from singular `command/` to plural `commands/` to match OpenCode's official directory convention
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/core/command-generation/adapters/opencode.ts` — adapter path
|
||||
- `src/core/legacy-cleanup.ts` — legacy cleanup pattern + add old singular path
|
||||
- `docs/supported-tools.md` — documentation table
|
||||
- `test/core/command-generation/adapters.test.ts` — test assertion
|
||||
@@ -1,63 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: ToolCommandAdapter interface
|
||||
|
||||
The system SHALL define a `ToolCommandAdapter` interface for per-tool formatting.
|
||||
|
||||
#### Scenario: Adapter interface structure
|
||||
|
||||
- **WHEN** implementing a tool adapter
|
||||
- **THEN** `ToolCommandAdapter` SHALL require:
|
||||
- `toolId`: string identifier matching `AIToolOption.value`
|
||||
- `getFilePath(commandId: string)`: returns file path for command (relative from project root, or absolute for global-scoped tools like Codex)
|
||||
- `formatFile(content: CommandContent)`: returns complete file content with frontmatter
|
||||
|
||||
#### Scenario: Claude adapter formatting
|
||||
|
||||
- **WHEN** formatting a command for Claude Code
|
||||
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
|
||||
- **AND** file path SHALL follow pattern `.claude/commands/opsx/<id>.md`
|
||||
|
||||
#### Scenario: Cursor adapter formatting
|
||||
|
||||
- **WHEN** formatting a command for Cursor
|
||||
- **THEN** the adapter SHALL output YAML frontmatter with `name` as `/opsx-<id>`, `id`, `category`, `description` fields
|
||||
- **AND** file path SHALL follow pattern `.cursor/commands/opsx-<id>.md`
|
||||
|
||||
#### Scenario: Windsurf adapter formatting
|
||||
|
||||
- **WHEN** formatting a command for Windsurf
|
||||
- **THEN** the adapter SHALL output YAML frontmatter with `name`, `description`, `category`, `tags` fields
|
||||
- **AND** file path SHALL follow pattern `.windsurf/workflows/opsx-<id>.md`
|
||||
|
||||
#### Scenario: OpenCode adapter formatting
|
||||
|
||||
- **WHEN** formatting a command for OpenCode
|
||||
- **THEN** the adapter SHALL output YAML frontmatter with `description` field
|
||||
- **AND** file path SHALL follow pattern `.opencode/commands/opsx-<id>.md` using `path.join('.opencode', 'commands', ...)` for cross-platform compatibility
|
||||
- **AND** the adapter SHALL transform colon-based command references (`/opsx:name`) to hyphen-based (`/opsx-name`) in the body
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Legacy cleanup for renamed OpenCode command directory
|
||||
|
||||
The legacy cleanup module SHALL detect and remove old OpenCode command files from the previous singular `.opencode/command/` directory path.
|
||||
|
||||
#### Scenario: Detect old singular-path OpenCode command files
|
||||
|
||||
- **WHEN** running legacy artifact detection on a project with files matching `.opencode/command/opsx-*.md` or `.opencode/command/openspec-*.md`
|
||||
- **THEN** the system SHALL include those files in the legacy slash command files list via `LEGACY_SLASH_COMMAND_PATHS`
|
||||
- **AND** `LegacySlashCommandPattern.pattern` SHALL accept `string | string[]` to support multiple glob patterns per tool
|
||||
|
||||
#### Scenario: Clean up old OpenCode command files on init
|
||||
|
||||
- **WHEN** a user runs `openspec init` in a project with old `.opencode/command/` artifacts
|
||||
- **THEN** the system SHALL remove the old files
|
||||
- **AND** generate new command files at `.opencode/commands/`
|
||||
|
||||
#### Scenario: Auto-cleanup legacy artifacts in non-interactive mode
|
||||
|
||||
- **WHEN** a user runs `openspec init` in non-interactive mode (e.g., CI) and legacy artifacts are detected
|
||||
- **THEN** the system SHALL auto-cleanup legacy artifacts without requiring `--force`
|
||||
- **AND** legacy slash command files (100% OpenSpec-managed) SHALL be removed
|
||||
- **AND** config file cleanup SHALL only remove OpenSpec markers (never delete user files)
|
||||
@@ -1,19 +0,0 @@
|
||||
## 1. Adapter Fix
|
||||
|
||||
- [x] 1.1 Update `src/core/command-generation/adapters/opencode.ts`: change `path.join('.opencode', 'command', ...)` to `path.join('.opencode', 'commands', ...)` and update the JSDoc comment
|
||||
|
||||
## 2. Legacy Cleanup
|
||||
|
||||
- [x] 2.1 Update `src/core/legacy-cleanup.ts`: update the `'opencode'` entry in `LEGACY_SLASH_COMMAND_PATHS` to detect both `opsx-*.md` and `openspec-*.md` patterns at `.opencode/command/` for backward compatibility
|
||||
|
||||
## 3. Documentation
|
||||
|
||||
- [x] 3.1 Update `docs/supported-tools.md`: change OpenCode command path from `.opencode/command/opsx-<id>.md` to `.opencode/commands/opsx-<id>.md`
|
||||
|
||||
## 4. Tests
|
||||
|
||||
- [x] 4.1 Update `test/core/command-generation/adapters.test.ts`: change the OpenCode file path assertion from `path.join('.opencode', 'command', 'opsx-explore.md')` to `path.join('.opencode', 'commands', 'opsx-explore.md')`
|
||||
|
||||
## 5. Changeset
|
||||
|
||||
- [x] 5.1 Create a changeset file (`.changeset/fix-opencode-commands-directory.md`) with a patch bump describing the path fix
|
||||
@@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-25
|
||||
@@ -1,38 +0,0 @@
|
||||
## Context
|
||||
|
||||
`statusCommand` in `src/commands/workflow/status.ts` calls `validateChangeExists()` from `shared.ts` as its first operation. When no `--change` option is provided and no change directories exist, `validateChangeExists` throws: `No changes found. Create one with: openspec new change <name>`. This error propagates up as a fatal CLI error (non-zero exit code).
|
||||
|
||||
This is correct behavior for commands like `apply` and `show` that require a change to operate on. However, `status` is an informational command — it should report the current state, even when that state is "no changes exist."
|
||||
|
||||
The error surfaces during onboarding (issue #714) when AI agents call `openspec status` before any change has been created.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Make `openspec status` exit with code 0 and a friendly message when no changes exist
|
||||
- Support both text and JSON output modes for the no-changes case
|
||||
- Keep all other commands' validation behavior unchanged
|
||||
|
||||
**Non-Goals:**
|
||||
- Changing the behavior of `validateChangeExists` (keep it strict for all consumers; only extract its internal helper)
|
||||
- Changing the onboard template or skill instructions
|
||||
- Handling the case where `--change` is provided but the specific change doesn't exist (this should remain an error)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Extract `getAvailableChanges` and check before validation
|
||||
|
||||
**Rationale**: Extract the private `getAvailableChanges` closure from `validateChangeExists` into a public exported function in `shared.ts`. Then, in `statusCommand`, call `getAvailableChanges` *before* `validateChangeExists` to detect the no-changes case early and handle it gracefully. This avoids using try/catch for control flow and eliminates any coupling to error message strings.
|
||||
|
||||
**Alternative considered**: Catching the error from `validateChangeExists` by matching `error.message.startsWith('No changes found')`. Rejected because string coupling is fragile — if the error message changes, the catch silently stops working.
|
||||
|
||||
**Alternative considered**: Adding a `throwOnEmpty` parameter to `validateChangeExists`. Rejected because it adds complexity to a shared function for a single consumer's needs and mixes UX concerns into a validation utility.
|
||||
|
||||
### Keep `validateChangeExists` strict
|
||||
|
||||
**Rationale**: `validateChangeExists` remains unchanged in behavior — it still throws for all error cases. The graceful handling lives entirely in `statusCommand`, which is the appropriate layer for UX decisions. Other commands (`apply`, `show`, `instructions`) are unaffected.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] Extra filesystem read when no `--change` is provided and changes *do* exist (`getAvailableChanges` is called first, then `validateChangeExists` performs its own read) → Mitigation: `statusCommand` returns early before reaching `validateChangeExists` when no changes exist, so the double-read only occurs when changes are present — minimal overhead.
|
||||
- [Risk] Other commands may also benefit from graceful no-changes handling in the future → Mitigation: `getAvailableChanges` is now public and reusable, making it easy to apply the same pattern elsewhere.
|
||||
@@ -1,25 +0,0 @@
|
||||
## Why
|
||||
|
||||
When `openspec status` is called without `--change` and no changes exist (e.g., during onboarding on a freshly initialized project), the CLI throws a fatal error: `No changes found. Create one with: openspec new change <name>`. This breaks the onboarding flow because AI agents may call `openspec status` before any change has been created, causing the agent to halt or report failure. Fixes [#714](https://github.com/Fission-AI/OpenSpec/issues/714).
|
||||
|
||||
## What Changes
|
||||
|
||||
- `openspec status` will exit gracefully (code 0) with a friendly message when no changes exist, instead of throwing a fatal error
|
||||
- `openspec status --json` will return a valid JSON object with an empty changes array when no changes exist
|
||||
- Other commands (`apply`, `show`, etc.) retain their current strict validation behavior
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `graceful-status-empty`: Graceful handling of `openspec status` when no changes exist, covering both text and JSON output modes
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_None — `validateChangeExists` was internally refactored to delegate to the newly exported `getAvailableChanges`, but its behavior and public contract are unchanged. Other consumers are unaffected._
|
||||
|
||||
## Impact
|
||||
|
||||
- `src/commands/workflow/shared.ts` — extract `getAvailableChanges` as a public function (validation behavior unchanged)
|
||||
- `src/commands/workflow/status.ts` — check for available changes before validation, handle empty case gracefully
|
||||
- Tests for the status command need to cover the new graceful behavior
|
||||
@@ -1,27 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Status command exits gracefully when no changes exist
|
||||
The `statusCommand` function SHALL check for available changes via `getAvailableChanges` before calling `validateChangeExists`. When no `--change` option is provided and no change directories exist, it SHALL print a friendly informational message and exit with code 0, instead of reaching `validateChangeExists` and propagating a fatal error.
|
||||
|
||||
#### Scenario: No changes exist, text mode
|
||||
- **WHEN** user runs `openspec status` without `--change` and no change directories exist under `openspec/changes/`
|
||||
- **THEN** the CLI prints `No active changes. Create one with: openspec new change <name>` to stdout and exits with code 0
|
||||
|
||||
#### Scenario: No changes exist, JSON mode
|
||||
- **WHEN** user runs `openspec status --json` without `--change` and no change directories exist
|
||||
- **THEN** the CLI outputs `{"changes":[],"message":"No active changes."}` as valid JSON to stdout and exits with code 0
|
||||
|
||||
### Requirement: Existing status validation behavior is preserved
|
||||
Other error paths in `validateChangeExists` that apply to the status command SHALL continue to throw errors as before. Commands other than `status` that use `validateChangeExists` SHALL NOT be affected.
|
||||
|
||||
#### Scenario: Changes exist but --change not specified
|
||||
- **WHEN** user runs `openspec status` without `--change` and one or more change directories exist
|
||||
- **THEN** the CLI throws an error listing available changes with the message `Missing required option --change. Available changes: ...`
|
||||
|
||||
#### Scenario: Specified change does not exist
|
||||
- **WHEN** user runs `openspec status --change non-existent`
|
||||
- **THEN** the CLI throws an error with message `Change 'non-existent' not found`
|
||||
|
||||
#### Scenario: Other commands unaffected
|
||||
- **WHEN** user runs `openspec show` or `openspec instructions` without `--change` and no changes exist
|
||||
- **THEN** the CLI throws the original `No changes found` error (no behavior change)
|
||||
@@ -1,16 +0,0 @@
|
||||
## 1. Implementation
|
||||
|
||||
- [x] 1.1 Extract `getAvailableChanges` in `shared.ts` and use it in `statusCommand` to check for changes before calling `validateChangeExists`
|
||||
- [x] 1.2 In text mode: print `No active changes. Create one with: openspec new change <name>` and return (exit 0)
|
||||
- [x] 1.3 In JSON mode: output `{"changes":[],"message":"No active changes."}` and return (exit 0)
|
||||
|
||||
## 2. Tests
|
||||
|
||||
- [x] 2.1 Add test: `openspec status` with no changes exits gracefully with friendly message (text mode)
|
||||
- [x] 2.2 Add test: `openspec status --json` with no changes returns valid JSON with empty changes array
|
||||
- [x] 2.3 Verify existing behavior: `openspec status` without `--change` when changes exist still throws missing option error
|
||||
- [x] 2.4 Verify cross-platform: tests use `path.join()` for any path assertions
|
||||
|
||||
## 3. Release
|
||||
|
||||
- [x] 3.1 Add changeset describing the fix
|
||||
@@ -160,14 +160,15 @@ The update command SHALL only run inside an initialized OpenSpec project.
|
||||
- **THEN** the system SHALL display: "No OpenSpec project found. Run 'openspec init' to set up."
|
||||
- **THEN** the system SHALL exit with code 1
|
||||
|
||||
### Requirement: Extra workflows synchronized to active profile
|
||||
The update command SHALL remove workflow files that are no longer selected in the current profile.
|
||||
### Requirement: Extra workflows preserved
|
||||
The update command SHALL NOT remove workflow files that aren't in the current profile.
|
||||
|
||||
#### Scenario: Deselected workflows from previous profile
|
||||
#### Scenario: Extra workflows from previous profile
|
||||
- **WHEN** user runs `openspec update`
|
||||
- **AND** project has workflows not in current profile (e.g., user switched from custom to core or deselected workflows via `openspec config profile`)
|
||||
- **THEN** the system SHALL delete skill and command workflow files for deselected workflows (respecting active delivery mode)
|
||||
- **THEN** the system SHALL keep only workflows currently selected in profile
|
||||
- **AND** project has workflows not in current profile (e.g., user switched from custom to core)
|
||||
- **THEN** the system SHALL NOT delete those extra workflow files
|
||||
- **THEN** the system SHALL only add/update workflows in the current profile
|
||||
- **THEN** the system SHALL display a note: "Note: <count> extra workflows not in profile (use `openspec config profile` to manage)"
|
||||
|
||||
#### Scenario: Delivery change with extra workflows
|
||||
- **WHEN** user runs `openspec update`
|
||||
|
||||
@@ -15,19 +15,21 @@ The design goal is to preserve current behavior while making extension points ex
|
||||
- Define one canonical source for workflow content and metadata
|
||||
- Make tool/agent-specific behavior explicit and centrally discoverable
|
||||
- Keep command adapters as the formatting boundary for tool syntax differences
|
||||
- Represent tool-specific command surfaces and terminology explicitly (not as scattered string rewrites)
|
||||
- Consolidate artifact generation/write orchestration into one reusable engine
|
||||
- Improve correctness with enforceable validation and parity tests
|
||||
|
||||
**Non-Goals:**
|
||||
- Redesigning command semantics or workflow instruction content
|
||||
- Changing user-facing CLI command names/flags in this proposal
|
||||
- Guaranteeing fully accurate literal slash-command strings for every supported tool on day one
|
||||
- Merging unrelated legacy cleanup behavior beyond artifact generation reuse
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Canonical `WorkflowManifest`
|
||||
|
||||
**Decision**: Represent each workflow once in a manifest entry containing canonical skill and command definitions plus metadata defaults.
|
||||
**Decision**: Represent each workflow once in a manifest entry containing canonical skill and command definitions plus metadata defaults. Canonical text uses semantic tokens for tool-specific references.
|
||||
|
||||
Suggested shape:
|
||||
|
||||
@@ -41,6 +43,12 @@ interface WorkflowManifestEntry {
|
||||
tags: string[];
|
||||
compatibility: string;
|
||||
}
|
||||
|
||||
// Examples in canonical workflow text:
|
||||
// - {{cmd.apply}}
|
||||
// - {{cmd.continue.withArg}}
|
||||
// - {{term.change}}
|
||||
// - {{term.workflow}}
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
@@ -50,7 +58,7 @@ interface WorkflowManifestEntry {
|
||||
|
||||
### 2. `ToolProfileRegistry` for capability wiring
|
||||
|
||||
**Decision**: Add a tool profile layer that maps tool IDs to generation capabilities and behavior.
|
||||
**Decision**: Add a tool profile layer that maps tool IDs to generation capabilities, command-surface rendering, and terminology.
|
||||
|
||||
Suggested shape:
|
||||
|
||||
@@ -59,6 +67,16 @@ interface ToolProfile {
|
||||
toolId: string;
|
||||
skillsDir?: string;
|
||||
commandAdapterId?: string;
|
||||
commandSurface: {
|
||||
pattern: 'opsx-colon' | 'opsx-hyphen' | 'opsx-slash' | 'openspec-hyphen' | 'custom';
|
||||
verified: boolean;
|
||||
aliases?: string[];
|
||||
};
|
||||
terminology: {
|
||||
change: string;
|
||||
workflow: string;
|
||||
command: string;
|
||||
};
|
||||
transforms: string[];
|
||||
}
|
||||
```
|
||||
@@ -67,10 +85,12 @@ interface ToolProfile {
|
||||
- Prevents capability drift between `AI_TOOLS`, adapter registry, and detection logic
|
||||
- Allows intentional "skills-only" tools without implicit special casing
|
||||
- Provides one place to answer "what does this tool support?"
|
||||
- Makes command rendering decisions explicit and testable
|
||||
- Supports future terminology tailoring without copy/paste template forks
|
||||
|
||||
### 3. First-class transform pipeline
|
||||
|
||||
**Decision**: Model transforms as ordered plugins with scope + phase + applicability.
|
||||
**Decision**: Model transforms as ordered plugins with scope + phase + applicability. Include token rendering in the transform pipeline instead of hardcoding literal command strings in templates.
|
||||
|
||||
Suggested shape:
|
||||
|
||||
@@ -87,17 +107,32 @@ interface ArtifactTransform {
|
||||
|
||||
Execution order:
|
||||
1. Render canonical content from manifest
|
||||
2. Apply matching `preAdapter` transforms
|
||||
3. For commands, run adapter formatting
|
||||
4. Apply matching `postAdapter` transforms
|
||||
5. Validate and write
|
||||
2. Apply token-render transform (`{{cmd.*}}`, `{{term.*}}`) using tool profile
|
||||
3. Apply matching `preAdapter` transforms
|
||||
4. For commands, run adapter formatting
|
||||
5. Apply matching `postAdapter` transforms
|
||||
6. Validate and write
|
||||
|
||||
**Rationale**:
|
||||
- Keeps adapters focused on tool formatting, not scattered behavioral rewrites
|
||||
- Makes agent-specific modifications explicit and testable
|
||||
- Replaces ad-hoc transform calls in `init`/`update`
|
||||
- Enables neutral fallback rendering when a tool profile is not verified for literal command syntax
|
||||
|
||||
### 4. Shared `ArtifactSyncEngine`
|
||||
### 4. Fallback policy for unverified command surfaces
|
||||
|
||||
**Decision**: When a tool profile has `commandSurface.verified === false`, command tokens SHALL render to neutral workflow guidance instead of literal slash-command strings.
|
||||
|
||||
Examples:
|
||||
- Literal (verified): `Run {{cmd.apply}}`
|
||||
- Neutral (unverified): `Run the Apply workflow` or `use the apply skill`
|
||||
|
||||
**Rationale**:
|
||||
- Prevents confidently wrong guidance in generated artifacts
|
||||
- Allows incremental tool-surface verification without blocking rollout
|
||||
- Keeps templates stable while rendering policy evolves
|
||||
|
||||
### 5. Shared `ArtifactSyncEngine`
|
||||
|
||||
**Decision**: Introduce a single orchestration engine used by all generation entry points.
|
||||
|
||||
@@ -112,13 +147,15 @@ Responsibilities:
|
||||
- Enables dry-run and future preview features without re-implementing logic
|
||||
- Improves reliability of updates and legacy migrations
|
||||
|
||||
### 5. Validation + parity guardrails
|
||||
### 6. Validation + parity guardrails
|
||||
|
||||
**Decision**: Add strict checks in tests (and optional runtime assertions in dev builds) for:
|
||||
|
||||
- Required skill metadata fields (`license`, `compatibility`, `metadata`) present for all manifest entries
|
||||
- Projection consistency (skills, commands, detection names derived from manifest)
|
||||
- Tool profile consistency (adapter existence, expected capabilities)
|
||||
- Token coverage checks (no unresolved `{{...}}` tokens in rendered outputs)
|
||||
- Tool command-surface verification matrix and fallback expectations
|
||||
- Golden/parity output for key workflows/tools
|
||||
|
||||
**Rationale**:
|
||||
@@ -143,7 +180,8 @@ Adding manifest/profile/transform registries increases conceptual surface area.
|
||||
## Implementation Approach
|
||||
|
||||
1. Build manifest + profile + transform types and registries behind current public API
|
||||
2. Rewire `getSkillTemplates`/`getCommandContents` to derive from manifest
|
||||
3. Introduce `ArtifactSyncEngine` and switch `init` to use it with parity checks
|
||||
4. Switch `update` and legacy upgrade flows to same engine
|
||||
5. Remove duplicate/hardcoded lists after parity is green
|
||||
2. Tokenize command/terminology references in workflow templates
|
||||
3. Rewire `getSkillTemplates`/`getCommandContents` to derive from manifest
|
||||
4. Introduce `ArtifactSyncEngine` and switch `init` to use it with parity checks
|
||||
5. Switch `update` and legacy upgrade flows to same engine
|
||||
6. Remove duplicate/hardcoded lists after parity is green
|
||||
|
||||
@@ -4,7 +4,8 @@ The recent split of `skill-templates.ts` into workflow modules improved readabil
|
||||
|
||||
- Workflow definitions are split from projection logic (`getSkillTemplates`, `getCommandTemplates`, `getCommandContents`)
|
||||
- Tool capability and compatibility are spread across `AI_TOOLS`, `CommandAdapterRegistry`, and hardcoded lists like `SKILL_NAMES`
|
||||
- Agent/tool-specific transformations (for example OpenCode command reference rewrites) are applied in different places (`init`, `update`, and adapter code)
|
||||
- Agent/tool-specific transformations are applied in different places (`init`, `update`, and adapter code)
|
||||
- Command and terminology references are currently hardcoded in workflow text, but tool invocation surfaces vary (`/opsx:apply`, `/opsx-apply`, `/opsx/apply`, and tool-specific naming)
|
||||
- Artifact writing logic is duplicated across `init`, `update`, and legacy-upgrade flow
|
||||
|
||||
This fragmentation creates drift risk (missing exports, missing metadata parity, mismatched counts/support) and makes future workflow/tool additions slower and less predictable.
|
||||
@@ -15,13 +16,16 @@ This fragmentation creates drift risk (missing exports, missing metadata parity,
|
||||
- Introduce a `ToolProfileRegistry` to centralize tool capabilities (skills path, command adapter, transforms)
|
||||
- Introduce a first-class transform pipeline with explicit phases (`preAdapter`, `postAdapter`) and scopes (`skill`, `command`, `both`)
|
||||
- Introduce a shared `ArtifactSyncEngine` used by `init`, `update`, and legacy upgrade paths
|
||||
- Add tokenized workflow text rendering so command references and tool terminology are resolved per tool profile at generation time
|
||||
- Add explicit command-surface profiles per tool (pattern, namespace/path style, alias support, verification status)
|
||||
- Add safe fallback behavior: when a tool command surface is not verified, render neutral workflow guidance (for example skill/workflow names) instead of potentially wrong literal command strings
|
||||
- Add strict validation and test guardrails to preserve fidelity during migration and future changes
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `template-artifact-pipeline`: Unified workflow manifest, tool profile registry, transform pipeline, and sync engine for skill/command generation
|
||||
- `template-artifact-pipeline`: Unified workflow manifest, tool profile registry, token-aware transform pipeline, and sync engine for skill/command generation
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
@@ -41,7 +45,9 @@ This fragmentation creates drift risk (missing exports, missing metadata parity,
|
||||
- **Testing additions**:
|
||||
- Manifest completeness tests (workflows, required metadata, projection parity)
|
||||
- Transform ordering and applicability tests
|
||||
- Tool command-surface/terminology profile validation tests
|
||||
- End-to-end parity tests for generated skill/command outputs across tools
|
||||
- **User-facing behavior**:
|
||||
- No new CLI surface area required
|
||||
- Existing generated artifacts remain behaviorally equivalent unless explicitly changed in future deltas
|
||||
- Generated text may become more tool-accurate for verified tool profiles
|
||||
- Generated text may intentionally use neutral workflow wording for unverified tools to avoid incorrect slash-command guidance
|
||||
|
||||
@@ -10,14 +10,18 @@
|
||||
- [ ] 2.1 Add `ToolProfile` types and `ToolProfileRegistry`
|
||||
- [ ] 2.2 Map all currently supported tools to explicit profile entries
|
||||
- [ ] 2.3 Wire profile lookups to command adapter resolution and skills path resolution
|
||||
- [ ] 2.4 Replace hardcoded detection arrays (for example `SKILL_NAMES`) with manifest-derived values
|
||||
- [ ] 2.4 Add per-tool `commandSurface` metadata (pattern, aliases, `verified` flag)
|
||||
- [ ] 2.5 Add per-tool terminology metadata (for example change/workflow/command labels)
|
||||
- [ ] 2.6 Replace hardcoded detection arrays (for example `SKILL_NAMES`) with manifest-derived values
|
||||
|
||||
## 3. Transform Pipeline
|
||||
|
||||
- [ ] 3.1 Introduce transform interfaces (`scope`, `phase`, `priority`, `applies`, `transform`)
|
||||
- [ ] 3.2 Implement transform runner with deterministic ordering
|
||||
- [ ] 3.3 Migrate OpenCode command reference rewrite to transform pipeline
|
||||
- [ ] 3.4 Remove ad-hoc transform invocation from `init` and `update`
|
||||
- [ ] 3.3 Add token renderer transform for command + terminology tokens (`{{cmd.*}}`, `{{term.*}}`)
|
||||
- [ ] 3.4 Implement neutral fallback rendering for tools with unverified command surfaces
|
||||
- [ ] 3.5 Migrate OpenCode command reference rewrite to transform pipeline
|
||||
- [ ] 3.6 Remove ad-hoc transform invocation from `init` and `update`
|
||||
|
||||
## 4. Artifact Sync Engine
|
||||
|
||||
@@ -29,10 +33,12 @@
|
||||
## 5. Validation and Tests
|
||||
|
||||
- [ ] 5.1 Add manifest completeness tests (metadata required fields, command IDs, dir names)
|
||||
- [ ] 5.2 Add tool-profile consistency tests (skillsDir support and adapter/profile alignment)
|
||||
- [ ] 5.3 Add transform applicability/order tests
|
||||
- [ ] 5.4 Expand parity tests for representative workflow/tool matrix
|
||||
- [ ] 5.5 Run full test suite and verify generated artifacts remain stable
|
||||
- [ ] 5.2 Add tool-profile consistency tests (skillsDir support, adapter/profile alignment, command-surface metadata)
|
||||
- [ ] 5.3 Add token rendering tests (all tokens resolved, per-tool rendering correctness)
|
||||
- [ ] 5.4 Add fallback tests for unverified tool command surfaces
|
||||
- [ ] 5.5 Add transform applicability/order tests
|
||||
- [ ] 5.6 Expand parity tests for representative workflow/tool matrix
|
||||
- [ ] 5.7 Run full test suite and verify generated artifacts remain stable
|
||||
|
||||
## 6. Cleanup and Documentation
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
## Why
|
||||
|
||||
After a workspace proposal exists, users need a practical way to implement one repo slice at a time.
|
||||
|
||||
In the proper workspace model, apply means implementation:
|
||||
|
||||
```text
|
||||
Take the selected workspace change.
|
||||
Take the selected repo slice.
|
||||
Open or use the right checkout.
|
||||
Implement that slice while preserving the workspace plan.
|
||||
```
|
||||
|
||||
It should not mean copying or materializing planning files into every repo as a user-facing workflow.
|
||||
|
||||
## What Changes
|
||||
|
||||
Add the repo-slice apply workflow for workspace changes:
|
||||
|
||||
- select a workspace change
|
||||
- select one target repo alias
|
||||
- resolve the local checkout for that alias
|
||||
- provide the agent with the workspace plan and repo-specific implementation context
|
||||
- track progress without making the workspace lose ownership of the plan
|
||||
|
||||
The workflow should support implementation across separate branches or sessions while keeping the workspace proposal as the continuity layer.
|
||||
|
||||
Planning dependency:
|
||||
|
||||
- Depends on `workspace-change-planning`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-repo-slice-apply`: Applies one repo slice of a workspace change as an implementation workflow.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `cli-artifact-workflow`: Defines workspace apply as implementation rather than materialization.
|
||||
- `context-injection`: Supplies repo-specific implementation context from a workspace change.
|
||||
|
||||
## Impact
|
||||
|
||||
- Workspace apply command behavior.
|
||||
- Agent handoff text for repo-slice implementation.
|
||||
- Local checkout resolution and branch/worktree assumptions.
|
||||
- Tests that apply operates on one target repo slice and does not require copying workspace planning artifacts as the primary user contract.
|
||||
@@ -1,47 +0,0 @@
|
||||
## Why
|
||||
|
||||
Once repos are visible and the agent has workspace context, the user should be able to plan a cross-repo change without immediately materializing repo-local artifacts.
|
||||
|
||||
The user goal is:
|
||||
|
||||
```text
|
||||
Explore the product goal across repos.
|
||||
Decide the scope.
|
||||
Create one workspace-level proposal that identifies the repo slices.
|
||||
```
|
||||
|
||||
Planning should be the commitment point. Repo visibility alone should remain lightweight.
|
||||
|
||||
## What Changes
|
||||
|
||||
Add workspace-level change planning:
|
||||
|
||||
- create a workspace change from the coordination root
|
||||
- capture the product goal once
|
||||
- identify target repos by registered alias
|
||||
- let the agent explore before committing to implementation slices
|
||||
- keep the workspace as the planning source of truth
|
||||
|
||||
This slice should avoid rebuilding the POC's materialization-first behavior. Repo-local artifacts should not be created merely because a workspace change exists.
|
||||
|
||||
Planning dependency:
|
||||
|
||||
- Depends on `workspace-open-agent-context`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-change-planning`: Creates and manages workspace-level proposals for cross-repo goals.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `change-creation`: Adds workspace-aware change creation semantics and target repo selection.
|
||||
- `openspec-conventions`: Defines the relationship between workspace-level planning and repo-local implementation work.
|
||||
|
||||
## Impact
|
||||
|
||||
- Workspace change creation.
|
||||
- Target repo metadata and validation.
|
||||
- Agent instructions for proposing cross-repo changes.
|
||||
- Tests that registered repos are visible before change creation and that creating a change does not imply repo-local materialization.
|
||||
@@ -1,356 +0,0 @@
|
||||
## Product Shape
|
||||
|
||||
This slice is the first user-facing step after `workspace-foundation`.
|
||||
|
||||
The user experience should be:
|
||||
|
||||
```text
|
||||
I set up a workspace.
|
||||
I link the repos or folders it should know about.
|
||||
I can list my workspaces later.
|
||||
I can ask OpenSpec what is broken and how to fix it.
|
||||
```
|
||||
|
||||
No change proposal is required yet.
|
||||
|
||||
## Links
|
||||
|
||||
A workspace link is a stable name plus a local path on the current machine.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
api -> /repos/api
|
||||
web -> /repos/web
|
||||
checkout -> /repos/platform/apps/checkout
|
||||
billing -> /repos/platform/services/billing
|
||||
```
|
||||
|
||||
The path may point at a full repo or a folder inside a large monorepo. It may point at a repo or folder that has not adopted repo-local OpenSpec yet.
|
||||
|
||||
The product language should say "repos or folders". It should avoid "working set", "code area", "entry", "alias", and "local overlay" in user-facing output.
|
||||
|
||||
Path handling should behave like a folder picker. The user may type a relative or absolute path, but OpenSpec should verify that it points to an existing folder, convert it to an absolute path relative to the command's current working directory when needed, and store that verified absolute path in local workspace state. OpenSpec should not store the raw string the user typed.
|
||||
|
||||
Path conversion stays in the current runtime. Native Windows paths, WSL2 paths, and Unix paths should not be translated across runtimes. Where duplicate-path detection needs canonical comparisons, OpenSpec may compare canonical existing paths internally, but it should store and display the verified absolute path for the current runtime.
|
||||
|
||||
## Names
|
||||
|
||||
Workspace names should be kebab-case:
|
||||
|
||||
```text
|
||||
platform
|
||||
checkout-web
|
||||
api2
|
||||
```
|
||||
|
||||
Invalid workspace names include uppercase letters, underscores, dots, spaces, leading hyphens, trailing hyphens, empty names, dot names, and path separators. Interactive setup should explain the expected form and let the user retry. Non-interactive setup should fail with the same expectation in the error message.
|
||||
|
||||
Link names should keep the folder-style validation from `workspace-foundation`: they must not be empty, must not be `.` or `..`, must not contain path separators, and must be unique inside the workspace. This lets inferred link names match existing folder basenames without forcing users to rename local folders for workspace planning.
|
||||
|
||||
Link names are normally inferred from the folder basename:
|
||||
|
||||
```text
|
||||
/repos/api -> api
|
||||
/repos/platform/apps/checkout -> checkout
|
||||
```
|
||||
|
||||
If the inferred name conflicts, interactive setup should show the conflicting name and the existing path it maps to, then ask for a different name. Non-interactive setup and direct `workspace link` should fail with a clear message instead of silently overwriting.
|
||||
|
||||
Duplicate-name errors should be specific:
|
||||
|
||||
```text
|
||||
Cannot use link name 'api' because another link already uses that name.
|
||||
Existing link:
|
||||
api -> /repos/api
|
||||
|
||||
Choose a different name:
|
||||
openspec workspace link archived-api /archive/api
|
||||
|
||||
If you meant to change the existing link path:
|
||||
openspec workspace relink api /archive/api
|
||||
```
|
||||
|
||||
This slice does not add a separate link-rename command. Renaming a link can be considered later if users need it, but v1 should keep the command model crisp: `link` adds a new link, and `relink` changes the local path for an existing link.
|
||||
|
||||
## Commands
|
||||
|
||||
### `workspace setup`
|
||||
|
||||
Guided onboarding:
|
||||
|
||||
- create a workspace in the standard workspace location
|
||||
- ask for a workspace name
|
||||
- require at least one existing repo or folder path
|
||||
- infer link names from folder names
|
||||
- let the user add more repos or folders with a simple repeated prompt
|
||||
- record the workspace in the local workspace registry
|
||||
- run `workspace doctor`
|
||||
- print the workspace location, planning path, linked repos or folders, and next useful commands
|
||||
|
||||
This slice should not ask for preferred agent or open the workspace with an agent. Those belong to `workspace-open-agent-context`.
|
||||
|
||||
Setup should support a non-interactive mode for automation:
|
||||
|
||||
```bash
|
||||
openspec workspace setup --no-interactive --name platform --link /path/to/api --link web=/path/to/web
|
||||
```
|
||||
|
||||
In non-interactive mode, setup should fail cleanly unless the user provides a valid workspace name and at least one valid link. `--link` should accept either a path, which infers the name from the folder basename, or `name=path`.
|
||||
|
||||
There is no public `workspace create` command in this slice. Setup is the creation flow.
|
||||
|
||||
### `workspace list`
|
||||
|
||||
Show known OpenSpec-managed workspaces from the local workspace registry.
|
||||
|
||||
`workspace ls` should behave the same way.
|
||||
|
||||
The output should answer what exists and what each workspace links to:
|
||||
|
||||
```yaml
|
||||
workspaces:
|
||||
- name: platform
|
||||
location: /.../openspec/workspaces/platform
|
||||
links:
|
||||
- name: api
|
||||
path: /repos/api
|
||||
- name: web
|
||||
path: /repos/web
|
||||
- name: checkout
|
||||
location: /.../openspec/workspaces/checkout
|
||||
links:
|
||||
- name: app
|
||||
path: /repos/platform/apps/checkout
|
||||
```
|
||||
|
||||
List should keep deep validation for `workspace doctor`. It can still report obviously stale workspace registry entries if a known workspace location no longer exists. Stale registry entries are report-only in this slice: `workspace list` should not delete, rewrite, or repair registry entries, and this slice should not add a `workspace forget` command.
|
||||
|
||||
For JSON output, list should use typed workspace objects with a structured `status` array for issues:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaces": [
|
||||
{
|
||||
"name": "platform",
|
||||
"root": "/.../openspec/workspaces/platform",
|
||||
"links": [
|
||||
{
|
||||
"name": "api",
|
||||
"path": "/repos/api",
|
||||
"status": []
|
||||
}
|
||||
],
|
||||
"status": []
|
||||
},
|
||||
{
|
||||
"name": "old-platform",
|
||||
"root": "/.../openspec/workspaces/old-platform",
|
||||
"links": [],
|
||||
"status": [
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "workspace_root_missing",
|
||||
"message": "Workspace location does not exist.",
|
||||
"fix": "Remove or repair the local registry entry."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": []
|
||||
}
|
||||
```
|
||||
|
||||
### `workspace link [name] <path>`
|
||||
|
||||
Record an existing repo or folder path for the selected workspace.
|
||||
|
||||
Supported forms:
|
||||
|
||||
```bash
|
||||
openspec workspace link /path/to/api
|
||||
openspec workspace link api-service /path/to/api
|
||||
```
|
||||
|
||||
The one-argument form infers the link name from the folder basename. The two-argument form lets the user choose the link name.
|
||||
|
||||
The path must exist. The command should accept:
|
||||
|
||||
- full repo roots
|
||||
- monorepo folders such as packages, services, and apps
|
||||
- repos or folders without repo-local `openspec/`
|
||||
|
||||
If the user passes a relative path, OpenSpec should resolve it against the command's current working directory before writing local state.
|
||||
|
||||
If the path has repo-local OpenSpec state, OpenSpec can report the repo specs path in doctor output. If it does not, OpenSpec should still allow workspace planning.
|
||||
|
||||
`workspace link` only records the link. It must not create, copy, move, initialize, or edit files in the linked repo or folder.
|
||||
|
||||
### `workspace relink <name> <path>`
|
||||
|
||||
Repair or change the local path for an existing link.
|
||||
|
||||
Relink should use the same path handling as link: require an existing folder, resolve relative inputs to absolute runtime-local paths, and store the verified path.
|
||||
|
||||
This slice should keep relink focused on path repair. It should not include owner or handoff metadata; that language was too process-heavy in the POC and can be revisited later if users need contact or notes fields.
|
||||
|
||||
### `workspace doctor`
|
||||
|
||||
Explain one selected workspace from the user's machine. If the command is run from a workspace folder or subdirectory and `--workspace <name>` is not provided, doctor should use that current workspace. Otherwise it should follow the normal workspace-selection rules.
|
||||
|
||||
Doctor should inspect:
|
||||
|
||||
- workspace location
|
||||
- workspace planning path
|
||||
- linked repos and folders
|
||||
- whether each local path exists
|
||||
- repo-local specs path when present
|
||||
- missing local paths
|
||||
- local names that are not in shared workspace state
|
||||
- shared link names that are missing local paths
|
||||
- suggested fixes for each issue
|
||||
|
||||
Doctor should not scan every known workspace in the local registry by default. Broad registry visibility belongs to `workspace list`. A future `workspace doctor --all` can be considered later if users need global workspace diagnostics.
|
||||
|
||||
Doctor should report issues and suggested fixes. It should not repair anything automatically.
|
||||
|
||||
Registry cleanup remains out of scope. If doctor cannot inspect the selected workspace because the registry points at a missing or invalid workspace location, it should report that selected-workspace issue through status entries and stop before inspecting links. Other stale registry entries should be surfaced by `workspace list`, not by selected-workspace doctor.
|
||||
|
||||
Human output should be readable by default: a short workspace summary, linked repo or folder rows, and a clear issues section when anything needs attention. It should not be raw JSON or a rigid YAML dump.
|
||||
|
||||
JSON output should follow the object/status pattern: primary data lives in typed objects, and diagnostics live in `status` arrays. A healthy object has `status: []`. Status entries should include `severity`, `code`, `message`, and optional `target` and `fix` fields.
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": {
|
||||
"name": "platform",
|
||||
"root": "/.../openspec/workspaces/platform",
|
||||
"planning_path": "/.../openspec/workspaces/platform/changes",
|
||||
"links": [
|
||||
{
|
||||
"name": "api",
|
||||
"path": "/repos/api",
|
||||
"repo_specs_path": "/repos/api/openspec/specs",
|
||||
"status": []
|
||||
},
|
||||
{
|
||||
"name": "web",
|
||||
"path": "/old/path/web",
|
||||
"repo_specs_path": null,
|
||||
"status": [
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "linked_path_missing",
|
||||
"message": "Linked path does not exist.",
|
||||
"target": "links.web.path",
|
||||
"fix": "openspec workspace relink web /path/to/web"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"status": []
|
||||
},
|
||||
"status": []
|
||||
}
|
||||
```
|
||||
|
||||
## Workspace Selection
|
||||
|
||||
Workspace commands should work from anywhere.
|
||||
|
||||
Commands that do not need one workspace:
|
||||
|
||||
- `workspace setup`
|
||||
- `workspace list`
|
||||
- `workspace ls`
|
||||
|
||||
Commands that need one workspace:
|
||||
|
||||
- `workspace link`
|
||||
- `workspace relink`
|
||||
- `workspace doctor`
|
||||
|
||||
If the current command needs one workspace and `--workspace <name>` is not provided:
|
||||
|
||||
- use the current workspace when running from inside a workspace
|
||||
- otherwise show an interactive picker when multiple known workspaces exist
|
||||
- otherwise select the only known workspace
|
||||
- otherwise explain that no workspaces exist and suggest `openspec workspace setup`
|
||||
|
||||
The current workspace wins even if it is not in the local workspace registry. This supports manually created or shared workspace folders. In that case commands should continue and include a non-fatal warning status:
|
||||
|
||||
```json
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "workspace_not_in_local_registry",
|
||||
"message": "This workspace is not recorded in the local workspace registry.",
|
||||
"target": "workspace.root",
|
||||
"fix": "Run a mutating workspace command from this workspace, such as workspace link or workspace relink, to record it locally."
|
||||
}
|
||||
```
|
||||
|
||||
For human output, this should be a short warning rather than a blocking error. Successful mutating commands that use an unregistered current workspace, such as `workspace link` or `workspace relink`, should record the workspace name and location in the local registry after the mutation succeeds. Non-mutating commands such as `workspace doctor` should not write registry state; they should only report the warning. This slice should not add a standalone `workspace register` or `workspace join` command.
|
||||
|
||||
In non-interactive mode, commands that need one workspace should fail when selection is ambiguous and suggest `--workspace <name>`.
|
||||
|
||||
`--json` should also suppress prompting for commands that need one workspace. If a command would otherwise show a picker, JSON mode should fail with a structured status error and suggest `--workspace <name>`.
|
||||
|
||||
## Machine-Local Files
|
||||
|
||||
Workspace creation should make machine-local state safe by default.
|
||||
|
||||
The workspace should ignore:
|
||||
|
||||
```text
|
||||
/.openspec-workspace/local.yaml
|
||||
```
|
||||
|
||||
The local workspace registry should also be machine-local:
|
||||
|
||||
```text
|
||||
<global-data-dir>/workspaces/registry.yaml
|
||||
```
|
||||
|
||||
Generated agent launch surfaces can be ignored by `workspace-open-agent-context` when that slice creates them.
|
||||
|
||||
## JSON Output
|
||||
|
||||
Interactive setup does not need JSON output as its primary contract. Non-interactive setup and direct commands should support JSON output for scripting:
|
||||
|
||||
- `workspace setup --no-interactive --json`
|
||||
- `workspace list --json`
|
||||
- `workspace link --json`
|
||||
- `workspace relink --json`
|
||||
- `workspace doctor --json`
|
||||
|
||||
`workspace setup --json` should require `--no-interactive`. If a user runs `workspace setup --json` without `--no-interactive`, setup should fail clearly because an interactive wizard cannot produce clean JSON. Direct commands such as `workspace list --json`, `workspace link --json`, `workspace relink --json`, and `workspace doctor --json` do not require `--no-interactive`, but JSON mode should disable prompts and fail on ambiguous workspace selection.
|
||||
|
||||
JSON output should use object/status structure across commands:
|
||||
|
||||
- primary entities such as `workspace`, `workspaces`, or `link` carry the durable data
|
||||
- `status` arrays carry warnings, errors, and suggested fixes
|
||||
- status entries use stable `code` values plus human-readable `message` text
|
||||
- command-level `status` describes the whole response
|
||||
- object-level `status` describes that specific workspace or link
|
||||
|
||||
## POC Adjustments
|
||||
|
||||
Keep:
|
||||
|
||||
- guided setup as the default first run
|
||||
- direct list/link/check commands
|
||||
- shared state separate from local paths
|
||||
- clean non-interactive failure when required setup inputs are missing
|
||||
- JSON output for non-interactive/direct commands
|
||||
|
||||
Change:
|
||||
|
||||
- do not expose public `workspace create` in the first release
|
||||
- do not require repo-local OpenSpec state to link a repo or folder
|
||||
- use `workspace link` instead of `workspace add-repo`
|
||||
- use `workspace relink` instead of `workspace update-repo`
|
||||
- do not save a preferred agent during setup
|
||||
- do not offer to open the workspace from setup
|
||||
- require setup to link at least one existing repo or folder
|
||||
- keep relink behavior focused on path repair rather than owner or handoff metadata
|
||||
- do not use "working set", "code area", "entry", "alias", or "local overlay" in human-facing output
|
||||
@@ -1,128 +0,0 @@
|
||||
## Why
|
||||
|
||||
Note: the change id keeps the older "register repos" wording for continuity. User-facing product language in this slice is `workspace setup`, `workspace link`, `workspace relink`, and "linked repos or folders."
|
||||
|
||||
Users start workspace work by creating a planning home and linking the repos or folders OpenSpec should know about.
|
||||
|
||||
They should not have to create a change before OpenSpec can see the relevant repos, monorepo folders, packages, services, or apps.
|
||||
|
||||
The product rule is:
|
||||
|
||||
```text
|
||||
Workspace visibility is not change commitment.
|
||||
```
|
||||
|
||||
A workspace is the durable planning home. A change is a feature, fix, project, or other planned piece of work inside that workspace.
|
||||
|
||||
## What Changes
|
||||
|
||||
Add the first user-facing workspace setup flow:
|
||||
|
||||
```text
|
||||
Set up a workspace.
|
||||
Link existing repos or folders.
|
||||
List known workspaces and what they link to.
|
||||
Check what OpenSpec can resolve and how to fix problems.
|
||||
```
|
||||
|
||||
Expected user surface:
|
||||
|
||||
```bash
|
||||
openspec workspace setup
|
||||
openspec workspace setup --no-interactive --name platform --link /path/to/api --link web=/path/to/web
|
||||
openspec workspace list
|
||||
openspec workspace ls
|
||||
openspec workspace link /path/to/api
|
||||
openspec workspace link api-service /path/to/api
|
||||
openspec workspace relink api /new/path/to/api
|
||||
openspec workspace doctor
|
||||
```
|
||||
|
||||
`workspace setup` is the creation path for users. It should ask for the workspace name first, create the workspace in the standard location, require at least one existing repo or folder path, infer link names from folder names, show the workspace location, and run a check at the end so the user knows what OpenSpec can see.
|
||||
|
||||
Workspace names should be kebab-case so they are clean managed-folder names and stable registry identifiers. Link names should keep the folder-style validation from `workspace-foundation` because they are often inferred directly from existing repo or folder basenames.
|
||||
|
||||
`workspace setup --no-interactive` is the automation path. It should require enough flags to create a useful workspace, including a workspace name and at least one link.
|
||||
|
||||
`workspace list` shows known OpenSpec-managed workspaces from the local workspace registry, including each workspace location and linked repos or folders.
|
||||
|
||||
`workspace link` records an existing local repo or folder path for the selected workspace. It should support a simple form that infers the link name from the folder name and an explicit-name form for conflicts or clarity. Linking does not create, copy, move, initialize, or edit files in the linked repo or folder.
|
||||
|
||||
Linking should behave like selecting a folder from a picker: OpenSpec verifies the folder exists, resolves relative inputs to an absolute path in the current runtime, and stores that verified path instead of the raw input string.
|
||||
|
||||
When a link name is already in use, OpenSpec should preserve the existing link and show the conflicting name with the existing path. The error should suggest choosing a different link name, or using `workspace relink <name> <path>` if the user intended to change the existing link's path.
|
||||
|
||||
`workspace relink` lets users repair or change the local path for an existing link without recreating the workspace. It should not introduce owner or handoff metadata in this slice.
|
||||
|
||||
`workspace doctor` explains what the current machine can resolve for one selected workspace: the workspace location, the workspace planning path, linked repos or folders, missing paths, repo-local specs paths when present, and suggested fixes. It should infer the current workspace when run from inside a workspace. It reports issues but does not repair them automatically.
|
||||
|
||||
Workspace commands should work globally. When a command needs one workspace and the user did not specify it, OpenSpec should use the local registry to show an interactive picker. In non-interactive mode, it should fail with a clear message and suggest `--workspace <name>`.
|
||||
|
||||
When a command runs from inside a valid workspace that is not in the local registry, OpenSpec should still use that current workspace. It should surface a non-fatal warning status that the workspace is not known locally, and successful mutating commands such as `workspace link` or `workspace relink` should record that workspace in the local registry after they update workspace state.
|
||||
|
||||
Machine-readable output should separate workspace or link objects from status entries. Status should be an array of structured issues instead of scattering fields such as `root_status`, `issue`, or `fix` through the primary object shape.
|
||||
|
||||
Interactive behavior should be disabled whenever output must be script-safe. `--no-interactive` means no prompts, and `--json` should fail instead of prompting when selection or setup inputs are ambiguous. `workspace setup --json` should require `--no-interactive` so JSON setup always uses the explicit automation path.
|
||||
|
||||
Planning dependency:
|
||||
|
||||
- Depends on `workspace-foundation`.
|
||||
|
||||
## POC Findings
|
||||
|
||||
Behavior to preserve:
|
||||
|
||||
- `workspace setup` was the friendly onboarding path.
|
||||
- `workspace list` made managed workspaces discoverable.
|
||||
- A direct automation path is still useful, but it should live under `workspace setup --no-interactive`.
|
||||
- Link repair is useful, but owner or handoff metadata should not carry forward in this slice.
|
||||
- `workspace doctor` was the right place to answer "what does OpenSpec know about this workspace?"
|
||||
- Shared workspace state and local paths were stored separately.
|
||||
- Setup failed cleanly when non-interactive inputs were incomplete.
|
||||
- Created workspaces excluded machine-local path state from portable workspace state.
|
||||
|
||||
Behavior to change:
|
||||
|
||||
- The POC required linked repo paths to already contain repo-local `openspec/`. This should become an implementation-readiness signal, not a planning prerequisite.
|
||||
- The POC used repo-only language. This slice should use "repos or folders" for user-facing text.
|
||||
- The public command should be `workspace link`, not `workspace add-repo`.
|
||||
- The repair command should be `workspace relink`, not `workspace update-repo`.
|
||||
- Public `workspace create` should be removed for the first release. Setup should be the creation flow.
|
||||
- The POC's `setup` flow stored preferred agent and open behavior. Agent launch preferences belong to `workspace-open-agent-context`, not this slice.
|
||||
- Human output should avoid implementation terms such as working set, code area, entry, alias, or local overlay.
|
||||
- `setup` should require at least one linked repo or folder so the created workspace is immediately useful.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No public `openspec workspace create` command in this first release.
|
||||
- No agent launch or workspace open behavior.
|
||||
- No preferred agent prompts or saved agent preference.
|
||||
- No owner or handoff metadata fields.
|
||||
- No workspace change creation or target selection.
|
||||
- No apply, verify, archive, branch, or worktree behavior.
|
||||
- No requirement that linked repos or folders have repo-local OpenSpec state.
|
||||
- No automatic repair behavior in `workspace doctor`.
|
||||
- No registry cleanup command such as `workspace forget`; stale registry entries are report-only in this slice.
|
||||
- No standalone `workspace register` or `workspace join` command; unregistered current workspaces are usable, and mutating workspace commands can record them locally.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-links`: Lets users set up a workspace, link repos or folders, list known workspaces, and check workspace resolution before change creation.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `cli-artifact-workflow`: Introduces workspace setup commands that happen before change creation.
|
||||
- `workspace-foundation`: Tightens workspace names to kebab-case while keeping folder-style link names.
|
||||
|
||||
## Impact
|
||||
|
||||
- `openspec workspace setup`
|
||||
- `openspec workspace list`
|
||||
- `openspec workspace ls`
|
||||
- `openspec workspace link`
|
||||
- `openspec workspace relink`
|
||||
- `openspec workspace doctor`
|
||||
- Local workspace registry usage from `workspace-foundation`.
|
||||
- Docs and generated guidance that explain linked repos or folders as planning context, not implementation commitment.
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Workspace Setup Commands
|
||||
The CLI artifact workflow SHALL expose workspace setup commands before change creation.
|
||||
|
||||
#### Scenario: Preparing workspace planning before a change
|
||||
- **WHEN** a user needs to prepare workspace planning across repos or folders
|
||||
- **THEN** the CLI SHALL provide commands to set up, list, link, relink, and doctor workspaces
|
||||
- **AND** those commands SHALL not require an active workspace change
|
||||
|
||||
#### Scenario: Listing workspaces with a short command
|
||||
- **WHEN** a user wants a concise workspace list command
|
||||
- **THEN** the CLI SHALL support `openspec workspace ls`
|
||||
- **AND** it SHALL behave the same as `openspec workspace list`
|
||||
|
||||
#### Scenario: Keeping setup separate from agent launch
|
||||
- **WHEN** a user completes workspace setup
|
||||
- **THEN** the setup workflow SHALL leave agent launch and workspace open behavior to a later workflow
|
||||
- **AND** setup SHALL not require a preferred agent choice
|
||||
|
||||
#### Scenario: Avoiding public direct creation
|
||||
- **WHEN** users create a workspace in the first workspace setup flow
|
||||
- **THEN** the CLI SHALL use `openspec workspace setup`
|
||||
- **AND** it SHALL not expose `openspec workspace create` as the public creation path
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Stable Workspace Name
|
||||
OpenSpec SHALL use one kebab-case workspace name across workspace identity, managed storage, and the local registry.
|
||||
|
||||
#### Scenario: Using one workspace name
|
||||
- **WHEN** OpenSpec creates or records a managed workspace
|
||||
- **THEN** the workspace name SHALL be stored in `.openspec-workspace/workspace.yaml`
|
||||
- **AND** the same name SHALL be used as the default managed workspace folder name
|
||||
- **AND** the same name SHALL be used as the local registry name
|
||||
|
||||
#### Scenario: Rejecting invalid workspace names
|
||||
- **WHEN** OpenSpec accepts a workspace name
|
||||
- **THEN** it SHALL require kebab-case names using lowercase letters, numbers, and single hyphen separators
|
||||
- **AND** it SHALL reject empty names, dot names, names with leading or trailing hyphens, names with repeated hyphens, uppercase letters, spaces, underscores, dots, and path separators
|
||||
- **AND** setup flows SHALL report OS-level folder creation failures clearly
|
||||
|
||||
### Requirement: Stable Link Names
|
||||
OpenSpec SHALL use stable folder-style link names to refer to repos and folders in workspace planning.
|
||||
|
||||
#### Scenario: Referring to a repo or folder in workspace planning
|
||||
- **WHEN** workspace state or later workspace planning artifacts refer to a linked repo or folder
|
||||
- **THEN** they SHALL use the stable link name
|
||||
- **AND** the same link name SHALL remain valid even when local checkout paths differ
|
||||
|
||||
#### Scenario: Reusing link names across machines
|
||||
- **WHEN** a workspace is used on another machine
|
||||
- **THEN** link names SHALL remain stable
|
||||
- **AND** local checkout paths MAY differ on that machine
|
||||
|
||||
#### Scenario: Rejecting invalid link names
|
||||
- **WHEN** OpenSpec accepts a workspace link name
|
||||
- **THEN** it SHALL reject empty names, `.` or `..`, and names containing path separators
|
||||
- **AND** link names SHALL be unique within the workspace
|
||||
- **AND** link names SHALL not be required to use workspace-name kebab-case
|
||||
@@ -1,356 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Guided Workspace Setup
|
||||
OpenSpec SHALL provide a guided setup flow for users starting workspace planning.
|
||||
|
||||
#### Scenario: Creating a workspace through setup
|
||||
- **WHEN** a user runs `openspec workspace setup`
|
||||
- **THEN** OpenSpec SHALL guide the user through creating an OpenSpec workspace
|
||||
- **AND** the workspace SHALL use the standard workspace location from the workspace foundation
|
||||
|
||||
#### Scenario: Asking for the workspace name first
|
||||
- **WHEN** interactive setup starts
|
||||
- **THEN** OpenSpec SHALL ask for the workspace name before asking for repos or folders
|
||||
- **AND** workspace names SHALL use kebab-case with lowercase letters, numbers, and hyphens
|
||||
|
||||
#### Scenario: Retrying an invalid workspace name during setup
|
||||
- **WHEN** an interactive user enters an invalid workspace name
|
||||
- **THEN** OpenSpec SHALL explain that workspace names must be kebab-case
|
||||
- **AND** it SHALL let the user enter another workspace name before continuing setup
|
||||
|
||||
#### Scenario: Linking a required first repo or folder
|
||||
- **WHEN** setup asks for repos or folders
|
||||
- **THEN** the user SHALL provide at least one existing repo or folder path
|
||||
- **AND** setup SHALL not finish successfully until at least one path is linked
|
||||
|
||||
#### Scenario: Inferring link names during setup
|
||||
- **WHEN** the user provides a repo or folder path during setup
|
||||
- **THEN** OpenSpec SHALL infer the link name from the folder basename
|
||||
- **AND** it SHALL ask for a different name only when the inferred name conflicts
|
||||
|
||||
#### Scenario: Handling inferred link name conflicts during setup
|
||||
- **GIVEN** setup infers a link name that already exists in the workspace
|
||||
- **WHEN** setup is interactive
|
||||
- **THEN** OpenSpec SHALL show the conflicting link name and the existing path for that link
|
||||
- **AND** it SHALL ask the user for a different link name before continuing
|
||||
|
||||
#### Scenario: Preserving folder-style link names
|
||||
- **WHEN** OpenSpec accepts a workspace link name
|
||||
- **THEN** it SHALL allow folder-style names that are valid under the workspace foundation link-name rules
|
||||
- **AND** it SHALL not require link names to use the stricter workspace-name kebab-case rule
|
||||
|
||||
#### Scenario: Adding multiple repos or folders during setup
|
||||
- **WHEN** setup links a repo or folder
|
||||
- **THEN** OpenSpec SHALL let the user add another repo or folder with a simple repeated prompt
|
||||
- **AND** each linked path SHALL be recorded without editing the target repo or folder
|
||||
|
||||
#### Scenario: Storing verified absolute paths during setup
|
||||
- **WHEN** setup links a repo or folder path
|
||||
- **THEN** OpenSpec SHALL verify that the path resolves to an existing folder
|
||||
- **AND** it SHALL store an absolute runtime-local path in machine-local state instead of the raw user input
|
||||
- **AND** relative inputs SHALL be resolved against the command's current working directory
|
||||
|
||||
#### Scenario: Preserving equals signs in setup link paths
|
||||
- **WHEN** non-interactive setup receives a `--link` value that resolves to an existing folder and contains `=`
|
||||
- **THEN** OpenSpec SHALL treat the full value as the path
|
||||
- **AND** it SHALL infer the link name from the folder basename
|
||||
- **AND** explicit `--link <name>=<path>` inputs SHALL preserve `=` characters inside `<path>`
|
||||
|
||||
#### Scenario: Running setup with non-interactive inputs
|
||||
- **WHEN** `openspec workspace setup --no-interactive` receives a workspace name and at least one valid link
|
||||
- **THEN** OpenSpec SHALL create the workspace without prompts
|
||||
- **AND** it SHALL support repeated `--link` values
|
||||
|
||||
#### Scenario: Non-interactive setup duplicate link names
|
||||
- **WHEN** `openspec workspace setup --no-interactive` receives two links with the same inferred or explicit name
|
||||
- **THEN** OpenSpec SHALL fail with a clear duplicate link-name error
|
||||
- **AND** the error SHALL show the conflicting link name and the first path using that name
|
||||
- **AND** it SHALL suggest using explicit `--link <name>=<path>` values with different names
|
||||
|
||||
#### Scenario: Missing non-interactive setup inputs
|
||||
- **WHEN** `openspec workspace setup --no-interactive` is missing a workspace name or link
|
||||
- **THEN** OpenSpec SHALL fail with a clear message
|
||||
- **AND** it SHALL explain which flags are required
|
||||
|
||||
#### Scenario: Finishing setup
|
||||
- **WHEN** setup finishes
|
||||
- **THEN** OpenSpec SHALL show the workspace location, planning path, and linked repos or folders
|
||||
- **AND** it SHALL check what the current machine can resolve
|
||||
|
||||
#### Scenario: Recording created workspaces locally
|
||||
- **WHEN** setup creates a workspace
|
||||
- **THEN** OpenSpec SHALL record it in the local workspace registry
|
||||
- **AND** the workspace folder SHALL remain the source of truth for workspace state
|
||||
|
||||
#### Scenario: Reusing an existing workspace name during setup
|
||||
- **GIVEN** a managed workspace already exists with the requested name
|
||||
- **WHEN** a user runs setup with that workspace name
|
||||
- **THEN** OpenSpec SHALL explain that the workspace already exists
|
||||
- **AND** it SHALL not overwrite the existing workspace
|
||||
|
||||
### Requirement: Workspace Discovery
|
||||
OpenSpec SHALL let users see the OpenSpec-managed workspaces available on the current machine.
|
||||
|
||||
#### Scenario: Listing workspaces
|
||||
- **WHEN** a user runs `openspec workspace list`
|
||||
- **THEN** OpenSpec SHALL list known managed workspaces
|
||||
- **AND** each workspace SHALL include the workspace name, workspace location, and linked repos or folders
|
||||
|
||||
#### Scenario: Using the short list command
|
||||
- **WHEN** a user runs `openspec workspace ls`
|
||||
- **THEN** OpenSpec SHALL behave the same as `openspec workspace list`
|
||||
|
||||
#### Scenario: Listing when no workspaces exist
|
||||
- **WHEN** a user runs `openspec workspace list`
|
||||
- **AND** no managed workspaces exist
|
||||
- **THEN** OpenSpec SHALL say that no workspaces were found
|
||||
- **AND** it SHALL show the user how to create one
|
||||
|
||||
#### Scenario: Listing stale registry entries
|
||||
- **WHEN** the local registry contains a workspace location that no longer exists
|
||||
- **THEN** `workspace list` SHALL report the stale workspace entry
|
||||
- **AND** it SHALL avoid silently deleting registry state
|
||||
- **AND** it SHALL avoid rewriting or repairing registry state automatically
|
||||
|
||||
#### Scenario: Avoiding registry cleanup commands
|
||||
- **WHEN** users inspect stale workspace registry entries in this slice
|
||||
- **THEN** OpenSpec SHALL treat stale entries as report-only diagnostics
|
||||
- **AND** it SHALL not expose a registry cleanup command such as `workspace forget`
|
||||
|
||||
### Requirement: Global Workspace Commands
|
||||
OpenSpec SHALL let workspace commands run from outside workspace directories.
|
||||
|
||||
#### Scenario: Selecting a workspace by flag
|
||||
- **WHEN** a command that needs one workspace receives `--workspace <name>`
|
||||
- **THEN** OpenSpec SHALL use that workspace from the local registry
|
||||
- **AND** it SHALL fail clearly if the workspace name is unknown
|
||||
|
||||
#### Scenario: Using the current workspace
|
||||
- **GIVEN** the command runs from a workspace folder or subdirectory
|
||||
- **WHEN** the command needs one workspace and no `--workspace` flag is provided
|
||||
- **THEN** OpenSpec SHALL use the current workspace
|
||||
|
||||
#### Scenario: Using an unregistered current workspace
|
||||
- **GIVEN** the command runs from a valid workspace folder or subdirectory
|
||||
- **AND** that workspace is not recorded in the local workspace registry
|
||||
- **WHEN** the command needs one workspace and no `--workspace <name>` flag is provided
|
||||
- **THEN** OpenSpec SHALL use the current workspace
|
||||
- **AND** it SHALL include a non-fatal warning status with code `workspace_not_in_local_registry`
|
||||
- **AND** the warning SHALL explain how the user can get the workspace recorded locally
|
||||
|
||||
#### Scenario: Recording an unregistered current workspace after mutation
|
||||
- **GIVEN** a mutating workspace command uses a valid current workspace that is not recorded in the local workspace registry
|
||||
- **WHEN** `workspace link` or `workspace relink` succeeds
|
||||
- **THEN** OpenSpec SHALL record the workspace name and location in the local workspace registry
|
||||
|
||||
#### Scenario: Doctor does not register current workspaces
|
||||
- **GIVEN** `workspace doctor` uses a valid current workspace that is not recorded in the local workspace registry
|
||||
- **WHEN** doctor finishes
|
||||
- **THEN** OpenSpec SHALL report the non-fatal registry warning
|
||||
- **AND** it SHALL not write registry state
|
||||
|
||||
#### Scenario: Picking from multiple workspaces
|
||||
- **GIVEN** multiple known workspaces exist
|
||||
- **WHEN** an interactive command needs one workspace and none is specified
|
||||
- **THEN** OpenSpec SHALL show a workspace picker
|
||||
- **AND** the picker SHALL include workspace names and paths
|
||||
|
||||
#### Scenario: Ambiguous non-interactive workspace selection
|
||||
- **GIVEN** multiple known workspaces exist
|
||||
- **WHEN** a non-interactive command needs one workspace and none is specified
|
||||
- **THEN** OpenSpec SHALL fail with a clear message
|
||||
- **AND** it SHALL suggest passing `--workspace <name>`
|
||||
|
||||
#### Scenario: Ambiguous JSON workspace selection
|
||||
- **GIVEN** multiple known workspaces exist
|
||||
- **WHEN** a command running with `--json` needs one workspace and none is specified
|
||||
- **THEN** OpenSpec SHALL fail without showing a picker
|
||||
- **AND** it SHALL emit a structured status error
|
||||
- **AND** it SHALL suggest passing `--workspace <name>`
|
||||
|
||||
#### Scenario: No known workspaces for a command that needs one
|
||||
- **GIVEN** no known workspaces exist in the local registry
|
||||
- **AND** the command is not running from a workspace folder or subdirectory
|
||||
- **WHEN** `workspace link`, `workspace relink`, `workspace doctor`, or another command that needs one workspace runs without `--workspace <name>`
|
||||
- **THEN** OpenSpec SHALL fail without showing a picker regardless of interactive mode
|
||||
- **AND** it SHALL print `No known OpenSpec workspaces. Run 'openspec workspace setup' first.`
|
||||
- **AND** it SHALL explain that `--workspace <name>` can be used after at least one workspace is known locally
|
||||
|
||||
### Requirement: Workspace Links
|
||||
OpenSpec SHALL let users link existing repos or folders to a workspace before creating a change.
|
||||
|
||||
#### Scenario: Linking with an inferred name
|
||||
- **WHEN** a user runs `openspec workspace link <path>`
|
||||
- **THEN** OpenSpec SHALL infer the link name from the folder basename
|
||||
- **AND** it SHALL store the verified absolute local path as machine-local state
|
||||
|
||||
#### Scenario: Linking with an explicit name
|
||||
- **WHEN** a user runs `openspec workspace link <name> <path>`
|
||||
- **THEN** OpenSpec SHALL use the explicit link name for planning
|
||||
- **AND** it SHALL store the verified absolute local path as machine-local state
|
||||
|
||||
#### Scenario: Requiring an existing path
|
||||
- **WHEN** a user links a repo or folder path
|
||||
- **THEN** the path SHALL exist on the current machine
|
||||
- **AND** OpenSpec SHALL reject missing paths with a clear message
|
||||
|
||||
#### Scenario: Resolving linked paths before storage
|
||||
- **WHEN** a user links a repo or folder path
|
||||
- **THEN** OpenSpec SHALL store the verified absolute path for the current runtime
|
||||
- **AND** relative inputs SHALL be resolved against the command's current working directory
|
||||
- **AND** OpenSpec SHALL not translate paths between native Windows, WSL2, and Unix runtimes
|
||||
|
||||
#### Scenario: Linking a monorepo folder
|
||||
- **WHEN** a user links a package, service, app, or directory inside a monorepo
|
||||
- **THEN** OpenSpec SHALL store it as a workspace link
|
||||
- **AND** it SHALL not require that folder to have its own repo-local `openspec/` directory
|
||||
|
||||
#### Scenario: Linking without repo-local OpenSpec
|
||||
- **WHEN** a user links a path that does not contain repo-local OpenSpec state
|
||||
- **THEN** OpenSpec SHALL keep that repo or folder available for workspace planning
|
||||
- **AND** it SHALL not treat missing repo-local OpenSpec state as a link failure
|
||||
|
||||
#### Scenario: Link records only
|
||||
- **WHEN** a user links a repo or folder
|
||||
- **THEN** OpenSpec SHALL record workspace state and local path state
|
||||
- **AND** it SHALL not create, copy, move, initialize, or edit files in the linked repo or folder
|
||||
|
||||
#### Scenario: Blocking link when local state is invalid
|
||||
- **GIVEN** the workspace machine-local state file exists but cannot be parsed or validated
|
||||
- **WHEN** a user runs `openspec workspace link`
|
||||
- **THEN** OpenSpec SHALL fail with status code `workspace_local_state_invalid`
|
||||
- **AND** it SHALL not rewrite shared workspace state or machine-local path state
|
||||
|
||||
#### Scenario: Reusing a link name
|
||||
- **GIVEN** a workspace already has a link with a given name
|
||||
- **WHEN** a user tries to link another path with the same name
|
||||
- **THEN** OpenSpec SHALL explain that the link name is already in use by another link
|
||||
- **AND** it SHALL show the existing link name and existing path
|
||||
- **AND** it SHALL suggest choosing a different link name
|
||||
- **AND** it SHALL suggest `workspace relink <name> <path>` when the user intended to change the existing link path
|
||||
- **AND** it SHALL preserve the existing link unless the user explicitly relinks it
|
||||
|
||||
### Requirement: Workspace Relinks
|
||||
OpenSpec SHALL let users update existing link paths without recreating the workspace.
|
||||
|
||||
#### Scenario: Updating a local path
|
||||
- **GIVEN** a workspace has a link
|
||||
- **WHEN** a user runs `openspec workspace relink <name> <path>`
|
||||
- **THEN** OpenSpec SHALL keep the stable link name
|
||||
- **AND** it SHALL update the machine-local path for the current machine to the verified absolute path
|
||||
|
||||
#### Scenario: Requiring an existing relink path
|
||||
- **WHEN** a user relinks to a new path
|
||||
- **THEN** the new path SHALL exist on the current machine
|
||||
- **AND** OpenSpec SHALL reject missing paths with a clear message
|
||||
|
||||
#### Scenario: Resolving relink paths before storage
|
||||
- **WHEN** a user relinks to a new path
|
||||
- **THEN** OpenSpec SHALL store the verified absolute path for the current runtime
|
||||
- **AND** relative inputs SHALL be resolved against the command's current working directory
|
||||
|
||||
#### Scenario: Blocking relink when local state is invalid
|
||||
- **GIVEN** the workspace machine-local state file exists but cannot be parsed or validated
|
||||
- **WHEN** a user runs `openspec workspace relink`
|
||||
- **THEN** OpenSpec SHALL fail with status code `workspace_local_state_invalid`
|
||||
- **AND** it SHALL not rewrite machine-local path state
|
||||
|
||||
#### Scenario: Updating an unknown link
|
||||
- **WHEN** a user tries to relink a link that does not exist
|
||||
- **THEN** OpenSpec SHALL explain that the link name is unknown
|
||||
- **AND** it SHALL preserve existing workspace state
|
||||
|
||||
#### Scenario: Avoiding owner and handoff fields
|
||||
- **WHEN** users link or relink repos or folders in this slice
|
||||
- **THEN** OpenSpec SHALL not ask for owner or handoff metadata
|
||||
- **AND** link maintenance SHALL focus on names and local paths
|
||||
|
||||
### Requirement: Workspace Health Check
|
||||
OpenSpec SHALL explain what the current machine can resolve for a workspace.
|
||||
|
||||
#### Scenario: Doctor checks one selected workspace
|
||||
- **WHEN** a user runs `openspec workspace doctor`
|
||||
- **THEN** OpenSpec SHALL inspect one selected workspace
|
||||
- **AND** it SHALL not scan every known workspace in the local registry by default
|
||||
|
||||
#### Scenario: Doctor infers the current workspace
|
||||
- **GIVEN** the command runs from a workspace folder or subdirectory
|
||||
- **WHEN** the user runs `openspec workspace doctor` without `--workspace <name>`
|
||||
- **THEN** OpenSpec SHALL inspect the current workspace
|
||||
|
||||
#### Scenario: Checking a healthy workspace
|
||||
- **WHEN** a user runs `openspec workspace doctor`
|
||||
- **THEN** OpenSpec SHALL show the workspace location and workspace planning path
|
||||
- **AND** it SHALL show linked repos or folders and which paths resolve on the current machine
|
||||
|
||||
#### Scenario: Selected workspace location is missing
|
||||
- **GIVEN** the selected workspace comes from the local registry
|
||||
- **AND** the registered workspace location is missing or invalid
|
||||
- **WHEN** a user runs `openspec workspace doctor`
|
||||
- **THEN** OpenSpec SHALL report a selected-workspace status error
|
||||
- **AND** it SHALL not attempt to inspect links for that workspace
|
||||
|
||||
#### Scenario: Reporting repo-local specs paths
|
||||
- **WHEN** a linked repo or folder resolves
|
||||
- **THEN** doctor SHALL report `repo_specs_path` when repo-local `openspec/specs` exists
|
||||
- **AND** it SHALL report `repo_specs_path: null` when repo-local specs are not present
|
||||
|
||||
#### Scenario: Checking missing paths
|
||||
- **WHEN** a link points to a path that is missing on the current machine
|
||||
- **THEN** doctor SHALL identify the affected link name
|
||||
- **AND** it SHALL include a suggested `workspace relink` fix
|
||||
|
||||
#### Scenario: Checking shared and local state drift
|
||||
- **WHEN** shared workspace state and machine-local path state do not agree
|
||||
- **THEN** doctor SHALL explain which link names are affected
|
||||
- **AND** it SHALL distinguish shared workspace links from local-only paths
|
||||
|
||||
#### Scenario: Reporting invalid local state
|
||||
- **WHEN** list or doctor reads a workspace whose machine-local state file cannot be parsed or validated
|
||||
- **THEN** OpenSpec SHALL report status code `workspace_local_state_invalid`
|
||||
- **AND** it SHALL avoid treating the invalid local state as an empty path map for mutation or repair suggestions
|
||||
- **AND** it SHALL not rewrite workspace registry state or machine-local path state
|
||||
|
||||
#### Scenario: Reporting without auto-repair
|
||||
- **WHEN** doctor finds issues
|
||||
- **THEN** it SHALL report all issues it can find
|
||||
- **AND** it SHALL not automatically repair workspace state
|
||||
|
||||
#### Scenario: Using readable human output
|
||||
- **WHEN** doctor prints human output
|
||||
- **THEN** it SHALL show a readable workspace summary, linked repos or folders, and issues when present
|
||||
- **AND** it SHALL avoid printing raw JSON or relying on a rigid YAML dump as the default human experience
|
||||
|
||||
### Requirement: Scriptable Workspace Setup Commands
|
||||
OpenSpec SHALL provide JSON output for direct workspace setup commands.
|
||||
|
||||
#### Scenario: Requesting JSON output
|
||||
- **WHEN** a user passes `--json` to direct workspace setup commands
|
||||
- **THEN** OpenSpec SHALL print machine-readable output
|
||||
- **AND** the output SHALL avoid extra human-readable text
|
||||
- **AND** the output SHALL separate primary objects from structured `status` entries
|
||||
|
||||
#### Scenario: Setup JSON requires non-interactive setup
|
||||
- **WHEN** a user runs `openspec workspace setup --json` without `--no-interactive`
|
||||
- **THEN** OpenSpec SHALL fail clearly
|
||||
- **AND** it SHALL explain that `workspace setup --json` requires `--no-interactive`
|
||||
|
||||
#### Scenario: JSON output disables prompts
|
||||
- **WHEN** a direct workspace setup command runs with `--json`
|
||||
- **THEN** OpenSpec SHALL avoid interactive prompts
|
||||
- **AND** it SHALL fail with structured status output when required choices are ambiguous
|
||||
|
||||
#### Scenario: JSON status entry shape
|
||||
- **WHEN** a direct workspace setup command reports warnings, errors, or suggested fixes in JSON output
|
||||
- **THEN** each status entry SHALL include a stable `code`, a `severity`, and a human-readable `message`
|
||||
- **AND** status entries MAY include `target` and `fix` fields when a specific object field or suggested command is useful
|
||||
|
||||
#### Scenario: JSON object status shape
|
||||
- **WHEN** a direct workspace setup command emits JSON for workspace, link, or list objects
|
||||
- **THEN** each object MAY include a `status` array for object-specific warnings or errors
|
||||
- **AND** the top-level response SHALL include a `status` array for command-level warnings or errors
|
||||
- **AND** healthy objects and healthy responses SHALL use an empty `status` array
|
||||
|
||||
#### Scenario: Commands with JSON output
|
||||
- **WHEN** users run `workspace setup --no-interactive`, `workspace list`, `workspace link`, `workspace relink`, or `workspace doctor`
|
||||
- **THEN** each command SHALL support JSON output
|
||||
@@ -1,121 +0,0 @@
|
||||
## 1. POC Findings And Scope
|
||||
|
||||
- [x] 1.1 Confirm `setup`, `list`, and `doctor` belong to this slice
|
||||
- [x] 1.2 Capture that setup should not own preferred agent or workspace open behavior
|
||||
- [x] 1.3 Capture that linked repos or folders and monorepo paths are allowed without repo-local OpenSpec state
|
||||
- [x] 1.4 Capture decisions for JSON output, `ls`, `.gitignore`, non-interactive setup, required first link, and relink behavior
|
||||
- [x] 1.5 Capture that public `workspace create` is out of scope for the first release
|
||||
- [x] 1.6 Capture `link`/`relink` as the user-facing commands
|
||||
|
||||
## 2. Workspace Setup
|
||||
|
||||
- [x] 2.1 Implement `openspec workspace setup` as the only public creation path
|
||||
- [x] 2.2 Prompt for workspace name first in interactive setup
|
||||
- [x] 2.3 Validate workspace names as kebab-case and let interactive users retry invalid names
|
||||
- [x] 2.4 Require at least one existing repo or folder path during setup
|
||||
- [x] 2.5 Infer link names from folder basenames during setup
|
||||
- [x] 2.6 Let users add more repos or folders with a simple repeated prompt
|
||||
- [x] 2.7 Run `workspace doctor` after setup and show a readable summary
|
||||
- [x] 2.8 Print the workspace location, planning path, linked repos or folders, and next useful commands
|
||||
- [x] 2.9 Keep preferred agent prompts and workspace opening out of this slice
|
||||
- [x] 2.10 Add `.gitignore` handling for machine-local workspace state
|
||||
- [x] 2.11 Record created workspaces in the local workspace registry
|
||||
- [x] 2.12 Add tests for native Windows/PowerShell and WSL2-compatible path construction where practical
|
||||
|
||||
## 3. Non-Interactive Setup
|
||||
|
||||
- [x] 3.1 Add `workspace setup --no-interactive --name <name> --link <path>` support
|
||||
- [x] 3.2 Support repeated `--link` values
|
||||
- [x] 3.3 Support `--link <path>` with inferred names
|
||||
- [x] 3.4 Support `--link <name>=<path>` with explicit names
|
||||
- [x] 3.5 Fail cleanly when non-interactive setup is missing a name or at least one link
|
||||
- [x] 3.6 Resolve relative link paths to verified absolute runtime-local paths before storing local state
|
||||
- [x] 3.7 Require `--no-interactive` when `workspace setup --json` is used
|
||||
- [x] 3.8 Add `--json` output for non-interactive setup
|
||||
- [x] 3.9 Preserve the interactive setup UX when `--no-interactive` is not passed
|
||||
|
||||
## 4. Workspace Listing
|
||||
|
||||
- [x] 4.1 Implement `openspec workspace list`
|
||||
- [x] 4.2 Add `workspace ls` as an alias for `workspace list`
|
||||
- [x] 4.3 List known OpenSpec-managed workspaces from the local workspace registry
|
||||
- [x] 4.4 Handle the no-workspaces case with a clear next step
|
||||
- [x] 4.5 Show each workspace location and linked repos or folders
|
||||
- [x] 4.6 Report stale registry entries with status entries without deleting, rewriting, or repairing registry state
|
||||
- [x] 4.7 Add JSON output with typed workspace objects and structured status arrays
|
||||
|
||||
## 5. Workspace Selection
|
||||
|
||||
- [x] 5.1 Make workspace commands work from outside workspace directories
|
||||
- [x] 5.2 Add `--workspace <name>` to commands that need one workspace
|
||||
- [x] 5.3 Use the current workspace when running from inside a workspace
|
||||
- [x] 5.4 Use unregistered current workspaces with a non-fatal warning status
|
||||
- [x] 5.5 Record unregistered current workspaces in the local registry after successful `workspace link` or `workspace relink`
|
||||
- [x] 5.6 Keep `workspace doctor` diagnostic-only when the current workspace is unregistered
|
||||
- [x] 5.7 Show an interactive picker when multiple known workspaces exist and no workspace is specified
|
||||
- [x] 5.8 Select the only known workspace automatically when there is exactly one
|
||||
- [x] 5.9 Fail clearly in non-interactive mode when workspace selection is ambiguous
|
||||
- [x] 5.10 Fail with structured status output instead of prompting when `--json` workspace selection is ambiguous
|
||||
- [x] 5.11 Use the local workspace registry for workspace lookup
|
||||
|
||||
## 6. Workspace Links
|
||||
|
||||
- [x] 6.1 Implement `openspec workspace link <path>` with inferred link names
|
||||
- [x] 6.2 Implement `openspec workspace link <name> <path>` with explicit link names
|
||||
- [x] 6.3 Accept full repo roots and monorepo package/service/app folder paths
|
||||
- [x] 6.4 Require linked paths to exist
|
||||
- [x] 6.5 Allow links without repo-local `openspec/`
|
||||
- [x] 6.6 Store stable link names in shared state and local paths in machine-local state
|
||||
- [x] 6.7 Keep link names folder-style, and detect duplicate link names with a specific error that shows the existing link path and suggests a different name or `workspace relink`
|
||||
- [x] 6.8 Resolve relative linked paths to verified absolute runtime-local paths before storing local state
|
||||
- [x] 6.9 Preserve native Windows and WSL2-style paths as local path values without cross-runtime translation
|
||||
- [x] 6.10 Ensure link only records state and does not edit the linked repo/folder
|
||||
- [x] 6.11 Add `--json` output for `workspace link`
|
||||
|
||||
## 7. Workspace Relinks
|
||||
|
||||
- [x] 7.1 Implement `openspec workspace relink <name> <path>`
|
||||
- [x] 7.2 Let users repair or change the local path for an existing link
|
||||
- [x] 7.3 Require relink paths to exist
|
||||
- [x] 7.4 Resolve relative relink paths to verified absolute runtime-local paths before storing local state
|
||||
- [x] 7.5 Keep owner or handoff metadata out of this slice
|
||||
- [x] 7.6 Add `--json` output for `workspace relink`
|
||||
- [x] 7.7 Return a clear error for unknown link names
|
||||
|
||||
## 8. Workspace Doctor
|
||||
|
||||
- [x] 8.1 Implement `openspec workspace doctor` for one selected workspace only
|
||||
- [x] 8.2 Show the workspace location and workspace planning path
|
||||
- [x] 8.3 Show linked repos or folders in readable human output with a clear issues section
|
||||
- [x] 8.4 Report missing local paths, missing filesystem paths, local-only names, and selected-workspace location problems
|
||||
- [x] 8.5 Report `repo_specs_path` when repo-local `openspec/specs` exists and `null` otherwise
|
||||
- [x] 8.6 Include suggested fixes for each issue
|
||||
- [x] 8.7 Avoid automatic repair behavior
|
||||
- [x] 8.8 Add JSON output with typed workspace/link objects and structured status arrays
|
||||
- [x] 8.9 Keep stale registry cleanup commands such as `workspace forget` out of this slice
|
||||
|
||||
## 9. Documentation And Guidance
|
||||
|
||||
- [x] 9.1 Document setup/list/link/relink/doctor in user-facing product language
|
||||
- [x] 9.2 Document linked repos or folders and large-monorepo folder links
|
||||
- [x] 9.3 Document that workspace visibility is not change commitment
|
||||
- [x] 9.4 Avoid "working set", "code area", "entry", "alias", and "local overlay" in human-facing docs
|
||||
- [x] 9.5 Document JSON output support and the object/status response pattern for non-interactive/direct commands
|
||||
- [x] 9.6 Document global command behavior, workspace picker behavior, and `--workspace <name>`
|
||||
- [x] 9.7 Document that setup controls workspace storage and always shows the workspace location
|
||||
|
||||
## 10. Verification
|
||||
|
||||
- [x] 10.1 Run `openspec validate workspace-create-and-register-repos --strict`
|
||||
- [x] 10.2 Run targeted command tests for workspace setup/list/link/relink/doctor, including doctor inferring the current workspace
|
||||
- [x] 10.3 Run targeted tests for links without repo-local OpenSpec and monorepo folder links
|
||||
- [x] 10.4 Run targeted tests for JSON output, `ls`, `.gitignore`, non-interactive setup, required first link, verified absolute path storage, and JSON/no-interactive prompt suppression
|
||||
- [x] 10.5 Run targeted tests for global command selection, unregistered current workspace handling, and local workspace registry behavior
|
||||
|
||||
## 11. Review Fixes
|
||||
|
||||
- [x] 11.1 Preserve `=` characters in inferred setup link paths while keeping explicit `--link <name>=<path>` support
|
||||
- [x] 11.2 Add reusable core helpers for optional local state reads and setup link input parsing
|
||||
- [x] 11.3 Fail `workspace link` and `workspace relink` before mutation when local state is invalid
|
||||
- [x] 11.4 Report invalid local state distinctly in `workspace list` and `workspace doctor`
|
||||
- [x] 11.5 Add regression tests for equals-sign setup paths and malformed local state behavior
|
||||
@@ -1,44 +0,0 @@
|
||||
## Why
|
||||
|
||||
After a user creates a workspace and links repos or folders, they need to open that workspace with an agent and have the agent understand the working set immediately.
|
||||
|
||||
The user should not need to explain where every repo lives, which aliases matter, or whether they are currently planning versus implementing. The workspace should provide that context.
|
||||
|
||||
## What Changes
|
||||
|
||||
Add the workspace-open experience:
|
||||
|
||||
```text
|
||||
Open this workspace with my agent.
|
||||
The agent sees the workspace location, linked repos or folders, current changes, and relevant instructions.
|
||||
```
|
||||
|
||||
Links are the planning context. The local registry is only a workspace-discovery index for finding known workspaces on the current machine.
|
||||
|
||||
The launch context should separate stable guidance from dynamic runtime scope:
|
||||
|
||||
- stable behavior belongs in workspace-level agent guidance where possible
|
||||
- dynamic scope belongs in the launch prompt or equivalent runtime context
|
||||
- linked repos or folders should be visible even when no change is active
|
||||
- change-scoped sessions should include the selected change and target repo context
|
||||
|
||||
Planning dependency:
|
||||
|
||||
- Depends on `workspace-create-and-register-repos`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-agent-context`: Opens a workspace session with enough dynamic context for an agent to reason across linked repos or folders.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `context-injection`: Extends context construction to include workspace location, workspace links, active workspace changes, and selected change scope.
|
||||
|
||||
## Impact
|
||||
|
||||
- `openspec workspace open`
|
||||
- Workspace prompt and agent-launch context.
|
||||
- Generated or committed agent guidance for workspace mode.
|
||||
- Tests for opening outside a workspace, opening a workspace by name, and opening change-scoped workspace sessions.
|
||||
@@ -1,259 +0,0 @@
|
||||
# Workspace POC Reference Guide
|
||||
|
||||
This guide is for a fresh agent starting a new session with no prior context about the workspace POC.
|
||||
|
||||
Root entry point: `WORKSPACE_REIMPLEMENTATION_START_HERE.md`.
|
||||
|
||||
The goal is not to continue the POC. The goal is to use it as research material before reimplementing workspace support cleanly from the current base.
|
||||
|
||||
## Reference Point
|
||||
|
||||
Use this exact commit as the stable reference:
|
||||
|
||||
```text
|
||||
workspace-poc @ 79a45ac043f414e63d13e08b9da83b135cb20a39
|
||||
```
|
||||
|
||||
Do not rely only on the moving branch name. Do not merge this commit into the implementation branch. Do not cherry-pick from it unless a later proposal explicitly decides that a small piece should be preserved.
|
||||
|
||||
## What The POC Was Trying To Prove
|
||||
|
||||
Start from the user journey:
|
||||
|
||||
```text
|
||||
create workspace
|
||||
-> add repos
|
||||
-> open workspace with an agent
|
||||
-> explore across repos
|
||||
-> create a proposal
|
||||
-> apply one repo slice
|
||||
-> verify
|
||||
-> archive
|
||||
```
|
||||
|
||||
The POC is useful if it helps answer:
|
||||
|
||||
- What did the user experience feel like when workspace mode worked?
|
||||
- Which CLI surfaces made the workflow easier to understand?
|
||||
- Which tests captured real product expectations?
|
||||
- Which implementation choices were shortcuts that should not survive?
|
||||
- Which terminology became misleading once the desired product shape was clearer?
|
||||
|
||||
## First Files To Read
|
||||
|
||||
Read these from the POC commit before implementation:
|
||||
|
||||
```text
|
||||
WORKSPACE_REIMPLEMENTATION_DIRECTION.md
|
||||
WORKSPACE_POC_FOLLOWUP_NOTES.md
|
||||
docs/workspace.md
|
||||
docs/workspace-demo.md
|
||||
docs/cli.md
|
||||
src/commands/workspace.ts
|
||||
src/core/workspace/open.ts
|
||||
test/commands/workspace/open.test.ts
|
||||
test/core/workspace/open.test.ts
|
||||
test/cli-e2e/workspace/workspace-open-cli.test.ts
|
||||
```
|
||||
|
||||
Optional deeper context:
|
||||
|
||||
```text
|
||||
workspace-poc-explorer.html
|
||||
workspace-poc-phase-playground.html
|
||||
copilot-session-d4e9c61e-readable.md
|
||||
copilot-session-d4e9c61e-timeline.md
|
||||
```
|
||||
|
||||
The optional files are historical research aids. Use them to understand how the POC evolved, not as implementation requirements.
|
||||
|
||||
## How To Inspect The POC Safely
|
||||
|
||||
Preferred approach: use a separate worktree or read files directly from the pinned commit.
|
||||
|
||||
Example direct reads:
|
||||
|
||||
```bash
|
||||
git show 79a45ac043f414e63d13e08b9da83b135cb20a39:WORKSPACE_REIMPLEMENTATION_DIRECTION.md
|
||||
git show 79a45ac043f414e63d13e08b9da83b135cb20a39:src/commands/workspace.ts
|
||||
git diff origin/main...79a45ac043f414e63d13e08b9da83b135cb20a39 --stat
|
||||
```
|
||||
|
||||
Example separate worktree:
|
||||
|
||||
```bash
|
||||
git worktree add ../openspec-workspace-poc 79a45ac043f414e63d13e08b9da83b135cb20a39
|
||||
```
|
||||
|
||||
Keep the implementation branch based on the current target branch. The POC worktree is for reading and running tests only.
|
||||
|
||||
## What To Bring Back
|
||||
|
||||
Before implementing a slice, come back with a short POC findings note:
|
||||
|
||||
```text
|
||||
POC findings for <slice>:
|
||||
|
||||
User behavior to preserve:
|
||||
- ...
|
||||
|
||||
Tests or examples worth translating:
|
||||
- ...
|
||||
|
||||
Implementation shortcuts to avoid:
|
||||
- ...
|
||||
|
||||
Open design questions:
|
||||
- ...
|
||||
```
|
||||
|
||||
Put durable findings in the relevant OpenSpec proposal or design artifact. Do not leave important decisions only in chat.
|
||||
|
||||
## Slice-Specific Reading
|
||||
|
||||
### `workspace-foundation`
|
||||
|
||||
Focus on:
|
||||
|
||||
- workspace folder shape
|
||||
- metadata directory naming
|
||||
- local versus committed state
|
||||
- stable workspace name semantics
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
WORKSPACE_REIMPLEMENTATION_DIRECTION.md
|
||||
WORKSPACE_POC_FOLLOWUP_NOTES.md
|
||||
docs/workspace.md
|
||||
src/commands/workspace.ts
|
||||
```
|
||||
|
||||
Bring back:
|
||||
|
||||
- the storage model worth keeping
|
||||
- the metadata naming decision
|
||||
- any compatibility risks with repo-local `openspec/`
|
||||
|
||||
### `workspace-create-and-register-repos`
|
||||
|
||||
Focus on:
|
||||
|
||||
- how a user creates a workspace
|
||||
- how repos or folders are linked
|
||||
- what `doctor` or equivalent status output should explain
|
||||
- how POC `create`/`add-repo` behavior maps to the target `setup`/`link`/`relink`/`doctor` flow before change creation
|
||||
- how planning-only repos and monorepo modules differ from implementation-ready repo-local OpenSpec projects
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
docs/workspace.md
|
||||
docs/workspace-demo.md
|
||||
src/commands/workspace.ts
|
||||
test/commands/workspace/setup.test.ts
|
||||
```
|
||||
|
||||
Bring back:
|
||||
|
||||
- expected commands
|
||||
- expected files
|
||||
- validation behavior for bad paths, duplicate workspace names, missing paths, planning-only links, and duplicate link names
|
||||
|
||||
### `workspace-open-agent-context`
|
||||
|
||||
Focus on:
|
||||
|
||||
- what context the agent receives
|
||||
- how linked repos or folders become visible
|
||||
- how one-session agent selection should work
|
||||
- what should be stable guidance versus dynamic launch context
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
WORKSPACE_POC_FOLLOWUP_NOTES.md
|
||||
src/commands/workspace.ts
|
||||
src/core/workspace/open.ts
|
||||
test/commands/workspace/open.test.ts
|
||||
test/core/workspace/open.test.ts
|
||||
test/cli-e2e/workspace/workspace-open-cli.test.ts
|
||||
```
|
||||
|
||||
Bring back:
|
||||
|
||||
- launch-context requirements
|
||||
- agent-specific behavior to preserve
|
||||
- prompt or guidance text that should become stable instructions
|
||||
|
||||
### `workspace-change-planning`
|
||||
|
||||
Focus on:
|
||||
|
||||
- when repo scope becomes a planning commitment
|
||||
- whether targets should be inferred from artifacts
|
||||
- how proposal, design, tasks, and specs should be arranged
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
WORKSPACE_REIMPLEMENTATION_DIRECTION.md
|
||||
docs/workspace.md
|
||||
docs/workspace-demo.md
|
||||
```
|
||||
|
||||
Bring back:
|
||||
|
||||
- the artifact shape to use
|
||||
- how targets should be confirmed
|
||||
- which POC target metadata ideas should be avoided or deferred
|
||||
|
||||
### `workspace-apply-repo-slice`
|
||||
|
||||
Focus on:
|
||||
|
||||
- the terminology decision that apply means implementation
|
||||
- what context the agent needs to implement one repo slice
|
||||
- why materialization should not be the user-facing contract
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
WORKSPACE_REIMPLEMENTATION_DIRECTION.md
|
||||
WORKSPACE_POC_FOLLOWUP_NOTES.md
|
||||
```
|
||||
|
||||
Bring back:
|
||||
|
||||
- the normalized apply context shape
|
||||
- the user-facing apply contract
|
||||
- any POC materialization behavior that should be explicitly rejected
|
||||
|
||||
### `workspace-verify-and-archive`
|
||||
|
||||
Focus on:
|
||||
|
||||
- partial repo completion versus full workspace completion
|
||||
- how verification should report gaps
|
||||
- how archive should avoid forcing repo-local planning copies
|
||||
|
||||
Read:
|
||||
|
||||
```text
|
||||
WORKSPACE_REIMPLEMENTATION_DIRECTION.md
|
||||
docs/workspace-demo.md
|
||||
```
|
||||
|
||||
Bring back:
|
||||
|
||||
- the minimum useful verify behavior
|
||||
- the archive preconditions
|
||||
- the distinction between repo-slice completion and workspace hard-done state
|
||||
|
||||
## Ground Rules
|
||||
|
||||
- Treat the POC as evidence, not inheritance.
|
||||
- Preserve user-visible lessons before preserving code.
|
||||
- Prefer current repo patterns over POC-only abstractions.
|
||||
- Implement one user-visible step at a time.
|
||||
- Update this roadmap when a POC lesson changes a later slice.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Workspace Reimplementation Roadmap
|
||||
|
||||
This change is the continuity layer for reimplementing workspace support across multiple sessions and branches.
|
||||
|
||||
Root entry point for fresh agents: `WORKSPACE_REIMPLEMENTATION_START_HERE.md`.
|
||||
|
||||
The user journey we are implementing is:
|
||||
|
||||
```text
|
||||
create workspace
|
||||
-> add repos
|
||||
-> open workspace with agent context
|
||||
-> plan a cross-repo change
|
||||
-> implement one repo slice
|
||||
-> verify and archive
|
||||
```
|
||||
|
||||
The POC branch is reference material only:
|
||||
|
||||
```text
|
||||
workspace-poc @ 79a45ac043f414e63d13e08b9da83b135cb20a39
|
||||
```
|
||||
|
||||
Use it to understand behavior, tests, and lessons learned. Do not merge it or preserve its architecture by default. The full source direction document from that branch is copied at the repository root as `WORKSPACE_REIMPLEMENTATION_DIRECTION.md`.
|
||||
|
||||
Fresh agents should read `POC_REFERENCE_GUIDE.md` before implementing any slice. That guide explains how to inspect the pinned POC commit, which files to read for each slice, and what findings to bring back into the OpenSpec artifacts.
|
||||
|
||||
## Change Order
|
||||
|
||||
Implement the flat sibling changes in this order:
|
||||
|
||||
1. `workspace-foundation`
|
||||
2. `workspace-create-and-register-repos`
|
||||
3. `workspace-open-agent-context`
|
||||
4. `workspace-change-planning`
|
||||
5. `workspace-apply-repo-slice`
|
||||
6. `workspace-verify-and-archive`
|
||||
|
||||
OpenSpec currently discovers active changes as immediate directories under `openspec/changes/`, and change names are kebab-case identifiers. Keep these changes as flat siblings until formal change-stacking metadata is available.
|
||||
|
||||
## Dependency Notes
|
||||
|
||||
`workspace-foundation` establishes the storage, root detection, and naming model. Every later slice should build on that model instead of redefining workspace metadata.
|
||||
|
||||
`workspace-create-and-register-repos` creates the workspace and makes linked repos or folders visible before a change exists. Linked items may be full repos, monorepo modules, or planning-only folders. This preserves the product rule that workspace visibility is not change commitment.
|
||||
|
||||
`workspace-open-agent-context` gives the agent the workspace location, linked repos or folders, active changes, and selected change scope.
|
||||
|
||||
`workspace-change-planning` creates the workspace-level planning commitment and identifies target repo slices.
|
||||
|
||||
`workspace-apply-repo-slice` treats apply as implementation of one selected repo slice, not materialization of workspace planning files.
|
||||
|
||||
`workspace-verify-and-archive` makes cross-repo progress visible and separates partial repo completion from final workspace completion.
|
||||
|
||||
## Session Handoff Prompt
|
||||
|
||||
Use this prompt at the start of future implementation sessions:
|
||||
|
||||
```text
|
||||
Continue the workspace reimplementation roadmap. Read
|
||||
openspec/changes/workspace-reimplementation-roadmap/README.md and
|
||||
openspec/changes/workspace-reimplementation-roadmap/POC_REFERENCE_GUIDE.md
|
||||
first, then pick up the next unfinished flat sibling change in order. Use
|
||||
workspace-poc at 79a45ac043f414e63d13e08b9da83b135cb20a39 as reference
|
||||
material only. Preserve intended behavior, but reimplement cleanly from the
|
||||
current base. Before editing, summarize the POC findings for the slice.
|
||||
```
|
||||
|
||||
## Branching Guidance
|
||||
|
||||
Each sibling change may be implemented on its own branch or PR. Keep decisions that affect later slices in this README or in the relevant proposal so future sessions do not depend on chat history.
|
||||
@@ -1,53 +0,0 @@
|
||||
## Why
|
||||
|
||||
Workspace support needs to be reimplemented as a user-facing workflow, not carried forward as a direct port of the proof of concept.
|
||||
|
||||
A user should be able to say they have a multi-repo product goal, create a workspace, add the relevant repos, open that workspace with an agent, plan the change, implement one repo slice at a time, verify it, and archive it. The POC branch captured useful behavior and discovery, but its implementation should remain reference material rather than the base architecture.
|
||||
|
||||
This roadmap also needs to survive multiple sessions and branches. Current OpenSpec change discovery treats active changes as flat immediate directories under `openspec/changes/`, and change names are kebab-case identifiers rather than nested paths. This change is therefore a flat planning container with sibling proposal changes instead of nested child changes.
|
||||
|
||||
Reference material:
|
||||
|
||||
- `workspace-poc` at `79a45ac043f414e63d13e08b9da83b135cb20a39`
|
||||
- `WORKSPACE_REIMPLEMENTATION_DIRECTION.md` on that branch
|
||||
- `WORKSPACE_POC_FOLLOWUP_NOTES.md` on that branch
|
||||
|
||||
## What Changes
|
||||
|
||||
Add a lightweight roadmap for reimplementing workspace support as a stack of flat sibling OpenSpec changes:
|
||||
|
||||
- `workspace-foundation`
|
||||
- `workspace-create-and-register-repos`
|
||||
- `workspace-open-agent-context`
|
||||
- `workspace-change-planning`
|
||||
- `workspace-apply-repo-slice`
|
||||
- `workspace-verify-and-archive`
|
||||
|
||||
Each sibling change owns one step in the lived user journey. Dependencies are documented in proposal prose for now. When change stacking metadata lands, this roadmap can be migrated to explicit `parent` and `dependsOn` metadata.
|
||||
|
||||
The intended order is:
|
||||
|
||||
```text
|
||||
workspace-foundation
|
||||
-> workspace-create-and-register-repos
|
||||
-> workspace-open-agent-context
|
||||
-> workspace-change-planning
|
||||
-> workspace-apply-repo-slice
|
||||
-> workspace-verify-and-archive
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-reimplementation-roadmap`: Coordinates the workspace reimplementation plan across multiple flat OpenSpec changes.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `openspec-conventions`: Clarifies that this workspace effort uses flat sibling changes until nested or stacked change metadata is supported.
|
||||
|
||||
## Impact
|
||||
|
||||
- Planning only in this PR.
|
||||
- Future changes will affect workspace metadata, workspace CLI flows, agent context construction, workspace change planning, repo-slice application, verification, and archive behavior.
|
||||
- No runtime behavior changes are introduced by this roadmap proposal.
|
||||
@@ -1,47 +0,0 @@
|
||||
## Why
|
||||
|
||||
Users need to know whether a cross-repo workspace change is complete without flattening all repo progress into one ambiguous done state.
|
||||
|
||||
The desired lifecycle is:
|
||||
|
||||
```text
|
||||
Verify each repo slice.
|
||||
See which slices are complete or still open.
|
||||
Archive repo-local results when appropriate.
|
||||
Archive the workspace change when the cross-repo goal is done.
|
||||
```
|
||||
|
||||
Verification and archive should make the user's cross-repo status clearer, not force them to reason about internal artifact placement.
|
||||
|
||||
## What Changes
|
||||
|
||||
Add workspace-aware verify and archive behavior:
|
||||
|
||||
- verify workspace-level change structure and target repo status
|
||||
- show per-repo slice progress
|
||||
- support repo-local archive work where needed
|
||||
- support explicit workspace-level archive when the coordinated goal is complete
|
||||
- avoid treating partial repo completion as full workspace completion
|
||||
|
||||
Planning dependency:
|
||||
|
||||
- Depends on `workspace-apply-repo-slice`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `workspace-verify-archive`: Verifies and archives workspace changes with per-repo progress visibility.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `cli-archive`: Adds workspace-aware archive semantics.
|
||||
- `opsx-verify-skill`: Adds workspace verification guidance.
|
||||
- `opsx-archive-skill`: Adds workspace archive guidance.
|
||||
|
||||
## Impact
|
||||
|
||||
- Workspace status, verify, and archive behavior.
|
||||
- Per-repo slice completion reporting.
|
||||
- Workspace-level hard-done marker or equivalent archive state.
|
||||
- Tests for partial completion, final workspace archive, and compatibility with standalone repo-local archive flows.
|
||||
@@ -5,12 +5,6 @@ context: |
|
||||
Package manager: pnpm
|
||||
CLI framework: Commander.js
|
||||
|
||||
Product language:
|
||||
- Write OpenSpec proposals and specs in user-facing product behavior language
|
||||
- Requirements should describe the experience, observable behavior, and product contract
|
||||
- Avoid implementation-negative SHALL statements when a positive user outcome can express the same rule
|
||||
- Put internal mechanisms in design.md or tasks.md unless the mechanism is itself part of the user-facing contract
|
||||
|
||||
Cross-platform requirements:
|
||||
- This tool runs on macOS, Linux, AND Windows
|
||||
- Always use path.join() or path.resolve() for file paths - never hardcode slashes
|
||||
@@ -22,8 +16,7 @@ rules:
|
||||
specs:
|
||||
- Include scenarios for Windows path handling when dealing with file paths
|
||||
- Requirements involving paths must specify cross-platform behavior
|
||||
- Prefer user-facing product behavior and observable outcomes over internal implementation mechanics
|
||||
- Include HOW details only when the mechanism is part of the product contract
|
||||
- Be explicit about mechanisms, not just outcomes (say HOW, not just WHAT)
|
||||
- If we generate artifacts, specify deletion/modification by explicit list lookup, not pattern matching
|
||||
tasks:
|
||||
- Add Windows CI verification as a task when changes involve file paths
|
||||
|
||||
@@ -6,8 +6,6 @@ The explore workflow is part of the core loop (`propose`, `explore`, `apply`, `a
|
||||
|
||||
Currently, explore references `/opsx:new` and `/opsx:ff` which are being replaced with `/opsx:propose`. But beyond just updating references, there are deeper UX questions about how explore should work.
|
||||
|
||||
This exploration is also affected by the emerging workspace direction: for larger cross-team or cross-repo work, OpenSpec may need to treat the **initiative** as the first-class planning object and repo-local changes as execution artifacts. That means explore may sometimes be seeding an initiative, not just a single change.
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Exploration Artifacts
|
||||
@@ -19,7 +17,6 @@ This exploration is also affected by the emerging workspace direction: for large
|
||||
2. **Where should exploration files live?**
|
||||
- `openspec/explorations/<name>.md`?
|
||||
- `openspec/changes/<change>/explorations/`?
|
||||
- `.openspec-workspace/initiatives/<initiative>/explorations/` for coordinated work?
|
||||
- Somewhere else?
|
||||
|
||||
3. **What should the format be?**
|
||||
@@ -33,17 +30,15 @@ This exploration is also affected by the emerging workspace direction: for large
|
||||
- e.g., exploring auth approaches separately from UI approaches
|
||||
- How would these relate to each other?
|
||||
|
||||
5. **How do explorations relate to changes or initiatives?**
|
||||
5. **How do explorations relate to changes?**
|
||||
- Before change exists: standalone exploration
|
||||
- After repo-local change exists: exploration linked to change?
|
||||
- For coordinated work: exploration linked to initiative first, then optionally referenced by repo-local changes?
|
||||
- After change exists: exploration linked to change?
|
||||
|
||||
### Lifecycle & Transitions
|
||||
|
||||
6. **What happens before a change proposal exists?**
|
||||
- Exploration is standalone
|
||||
- When ready, user runs `/opsx:propose`
|
||||
- For coordinated work, should exploration context seed an initiative first?
|
||||
- Should exploration context automatically seed the proposal?
|
||||
|
||||
7. **What happens after a change proposal exists?**
|
||||
@@ -95,19 +90,11 @@ This exploration is also affected by the emerging workspace direction: for large
|
||||
- **Pro:** Clear relationship to changes
|
||||
- **Con:** Where do pre-change explorations go?
|
||||
|
||||
### Approach E: Initiative-First Explorations for Coordinated Work
|
||||
- Local work can stay standalone or change-linked
|
||||
- Coordinated work saves exploration notes under an initiative in the coordination workspace
|
||||
- Repo-local changes can reference the shared exploration when execution starts
|
||||
- **Pro:** Matches the emerging split between shared planning and repo-local execution
|
||||
- **Con:** Adds another context where exploration artifacts may live
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ ] User research: How do people actually use explore today?
|
||||
- [ ] Prototype: Try saving explorations and see if propose benefits
|
||||
- [ ] Decide: Pick an approach based on findings
|
||||
- [ ] Reconcile explore UX with initiative-first coordinated planning
|
||||
- [ ] Implement: Update explore workflow accordingly
|
||||
|
||||
## Related
|
||||
|
||||
@@ -77,7 +77,7 @@ We researched how similar tools handle config layering:
|
||||
| **ESLint (flat)** | Single root config | *Deliberately killed cascading* - "complexity exploded exponentially" |
|
||||
| **Turborepo** | Root + package extends | Per-package `turbo.json` with `extends: ["//"]` for overrides |
|
||||
| **Nx** | Integrated vs Package-based | Two modes - shared root OR per-package. Hard to migrate from integrated. |
|
||||
| **pnpm** | Workspace file defines package scope | `pnpm-workspace.yaml` at the package-set root. Dependencies can be shared or per-package |
|
||||
| **pnpm** | Workspace root defines scope | `pnpm-workspace.yaml` at root. Dependencies can be shared or per-package |
|
||||
| **Claude Code** | Global + Project | `~/.claude/` for global, `.claude/` per-project. No workspace tracking. |
|
||||
| **Kiro** | Distributed per-root | Each folder has `.kiro/`. Aggregated display, no inheritance. |
|
||||
|
||||
@@ -645,199 +645,23 @@ To avoid losing this in exploration notes, codify it in:
|
||||
|
||||
---
|
||||
|
||||
## Part 10: Design Decisions (April 2026)
|
||||
|
||||
After evaluating the models above against real multi-repo use cases (see [#725](https://github.com/Fission-AI/OpenSpec/issues/725)), we converged on the following design direction.
|
||||
|
||||
### Core Insight
|
||||
|
||||
The workspace itself is not the durable thing. For large teams, the durable planning object is the **initiative** or **plan**, while repo-local specs and changes remain the execution artifacts owned by each repo. The set of repos involved in a feature is typically feature-scoped and changes over time, so a static workspace manifest that must be configured before work begins creates ceremony that doesn't match how teams actually work.
|
||||
|
||||
### Decision: Model D with Lazy Workspace
|
||||
|
||||
Choose Model D (Hybrid) from Part 4, but make the workspace manifest **optional and lazy, not prerequisite**.
|
||||
|
||||
- **Each repo keeps its own canonical `openspec/`** — no change to the fundamental storage model.
|
||||
- **Cross-root work can be coordinated through an initiative in a coordination workspace** — this is where shared planning lives when the work stops being cleanly repo-scoped.
|
||||
- **"Workspace" is a derived or explicit coordination view** over linked repos and linked changes, not something users must register up front.
|
||||
- **Persist a workspace manifest only when someone explicitly wants a reusable cross-repo bundle** — this is an opt-in convenience, not a requirement.
|
||||
|
||||
### Decision: Initiative-First Planning with Linked Repo-Local Changes
|
||||
|
||||
For larger multi-team work, repo-centric planning is the wrong primary abstraction. Teams and repos are many-to-many facets over the same work. OpenSpec should treat the **initiative / plan** as the first-class planning object, then link repo-local changes to it.
|
||||
|
||||
This is especially important because a change today bundles:
|
||||
|
||||
- `proposal.md`
|
||||
- `design.md`
|
||||
- `tasks.md`
|
||||
- delta specs
|
||||
- `.openspec.yaml`
|
||||
|
||||
That bundled shape works well for repo-local work, but becomes awkward when one piece of work spans multiple repos or teams. In those cases, a single repo-local change is trying to act as both:
|
||||
|
||||
- the shared planning object
|
||||
- the repo-specific execution artifact
|
||||
|
||||
Those should be split.
|
||||
|
||||
The preferred model is:
|
||||
|
||||
```text
|
||||
coordination workspace /
|
||||
.openspec-workspace/
|
||||
workspace.yaml
|
||||
initiatives/
|
||||
add-3ds/
|
||||
initiative.yaml
|
||||
proposal.md
|
||||
design.md
|
||||
links.yaml
|
||||
|
||||
repo-A/
|
||||
openspec/
|
||||
changes/
|
||||
add-3ds-api/
|
||||
.openspec.yaml
|
||||
tasks.md
|
||||
specs/
|
||||
|
||||
repo-B/
|
||||
openspec/
|
||||
changes/
|
||||
add-3ds-web/
|
||||
.openspec.yaml
|
||||
tasks.md
|
||||
specs/
|
||||
```
|
||||
|
||||
The initiative holds the shared planning layer:
|
||||
|
||||
- proposal / intent
|
||||
- shared design and tradeoffs
|
||||
- participating teams
|
||||
- impacted repos
|
||||
- milestones, risks, and dependencies
|
||||
- links to repo-local changes
|
||||
|
||||
Each repo-local change holds the execution layer for that repo:
|
||||
|
||||
- repo-specific tasks
|
||||
- delta specs
|
||||
- local implementation status
|
||||
- optional local notes that should archive with that repo's work
|
||||
|
||||
Cross-repo linking still matters, but it should hang off the initiative and the repo-local changes:
|
||||
|
||||
```yaml
|
||||
# billing-service/openspec/changes/add-3ds/.openspec.yaml
|
||||
schema: spec-driven
|
||||
created: 2026-04-12
|
||||
initiative: add-3ds
|
||||
links:
|
||||
- project: github.com/fission/web-client
|
||||
change: add-3ds-checkout
|
||||
- project: github.com/fission/ios-client
|
||||
change: add-3ds-checkout
|
||||
```
|
||||
|
||||
Each repo still holds its own change with its own deltas. A cross-repo effort is represented as one initiative plus N linked single-repo changes. This is preferable to a single mega-change because:
|
||||
- Shared planning has one truthful home
|
||||
- Each repo's change goes through its own archive cycle
|
||||
- No need to resolve cross-repo file paths in delta specs
|
||||
- Teams can move at different speeds (web ships before iOS)
|
||||
|
||||
For small single-repo work, a repo-local change may still be "good enough" as both plan and execution bundle. The initiative-first split matters once work becomes cross-team, cross-module, cross-repo, or otherwise coordination-heavy.
|
||||
|
||||
### Decision: Stable Project Identifiers, Not Paths
|
||||
|
||||
Cross-repo links must use **stable project identifiers**, not filesystem paths.
|
||||
|
||||
- **Canonical form:** A normalized `host/org/repo` tuple (e.g., `github.com/fission/web-client`).
|
||||
- **Authoring shorthand:** The CLI accepts `org/repo` (e.g., `fission/web-client`) and infers the host from the current repo's remote.
|
||||
- **Relative paths are never the durable identifier.** They may exist only as cached local resolution results.
|
||||
|
||||
### Decision: Offline-First Resolution
|
||||
|
||||
The CLI resolves project identifiers to local paths using an offline-first chain:
|
||||
|
||||
1. **Explicit paths** passed for the current run (e.g., CLI flags, ad-hoc multi-root).
|
||||
2. **Local OpenSpec repo registry** — a persistent mapping in `~/.config/openspec/` or `~/.local/share/openspec/` (see `src/core/global-config.ts`).
|
||||
3. **Parent directory scanning** — scan known parent directories for git checkouts whose remotes match the target identifier.
|
||||
4. **Unresolved** — if no local path is found, leave the target unresolved and continue with a partial workspace. The CLI must not fail.
|
||||
|
||||
The registry is populated progressively: when the CLI discovers a clone (via scanning or user prompt), it persists the mapping for future resolution. The registry also stores "known scan roots" (e.g., `~/work/`) so scanning improves over time without upfront configuration.
|
||||
|
||||
### Decision: Informational References Only (v1)
|
||||
|
||||
Spec-level cross-repo references are **documentation-only pointers**:
|
||||
|
||||
```yaml
|
||||
# web-client/openspec/specs/checkout/spec.md frontmatter
|
||||
references:
|
||||
- project: github.com/fission/contracts-service
|
||||
spec: checkout-contract
|
||||
```
|
||||
|
||||
- The CLI does **not** fail validation because a referenced cross-repo spec is missing or unresolved.
|
||||
- The CLI **does** surface references to humans and agents when planning, viewing, or applying changes.
|
||||
- Stronger guarantees (e.g., staleness warnings, cross-repo validation) are an opt-in layer added later — via `lint`, `doctor`, or a feature flag — not baseline behavior.
|
||||
|
||||
This avoids accidentally committing OpenSpec to a full dependency graph system before the use cases justify it.
|
||||
|
||||
### Decision: Explicit Owner Repo for Shared Contracts
|
||||
|
||||
When a spec cannot be mapped to a single implementation repo (e.g., a shared API contract):
|
||||
|
||||
- **One repo must be the explicit owner.** This can be a dedicated "contracts" repo, or whichever repo is the natural source of truth.
|
||||
- **Other repos reference the owning repo's spec** via informational references (see above).
|
||||
- **There is no default "pure spec repo" pattern.** Separating spec ownership from code ownership too aggressively makes agent execution awkward and diffuses responsibility.
|
||||
|
||||
### Monorepo vs. Multi-Repo Summary
|
||||
|
||||
| Concern | Monorepo | Multi-Repo |
|
||||
|---------|----------|------------|
|
||||
| **Spec organization** | Nested specs inside one `openspec/` (Model B) | Each repo has its own `openspec/` |
|
||||
| **Cross-cutting specs** | Nested under a `contracts/` or `shared/` directory | Dedicated owner repo, others reference it |
|
||||
| **Planning object** | Initiative optional for simple work, useful for large cross-team efforts | Initiative is the primary coordination object |
|
||||
| **Changes** | One or more repo-local changes can implement one initiative | Linked per-repo changes implement one initiative |
|
||||
| **Relationships** | References (no inheritance in v1) | Project identifier links, informational only |
|
||||
| **Workspace** | Usually not needed, but can host initiative planning for complex work | Coordination workspace hosts initiative planning; optional manifest for reuse |
|
||||
|
||||
### Implementation Path
|
||||
|
||||
1. **Define initiative artifacts** — add an initiative format for shared planning in coordination workspaces.
|
||||
2. **Extend change metadata** — let repo-local changes point at an initiative and linked sibling changes.
|
||||
3. **Extend spec metadata** — add `references` field for cross-repo spec pointers.
|
||||
4. **Build project resolution** — implement the offline-first resolution chain and local registry.
|
||||
5. **Build initiative and link views** — commands that resolve and display the initiative graph plus linked repo-local changes.
|
||||
6. **Support ad-hoc multi-root** — "add these dirs for this run" or "derive roots from this initiative's links."
|
||||
7. **Optional workspace manifest** — add saved workspaces only if teams demonstrate reuse patterns.
|
||||
|
||||
Nested specs (Model B inside a single repo) are a prerequisite for clean monorepo support and should be tackled first, as outlined in #662.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Question | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| Profile UX | Decided | `openspec config profile` with presets |
|
||||
| Config layering | Decided | Two layers: global + project (no workspace layer) |
|
||||
| Spec organization | **Direction set** | Nested specs per repo, explicit owner repos for shared contracts, references for cross-repo context |
|
||||
| Spec organization | Open | Four models under consideration (including hybrid Model D) |
|
||||
| Spec philosophy | Direction set | Behavior-first contracts, progressive rigor, and agent-aligned authoring |
|
||||
| Spec inheritance | **Decided** | References only, no inheritance in v1 |
|
||||
| Initiative / planning model | **Direction set** | Initiative-first planning for larger work, with repo-local changes as execution artifacts |
|
||||
| Multi-repo support | **Direction set** | Linked per-repo changes under shared initiatives; workspace is coordination, not canonical execution storage |
|
||||
| Dependency tracking | **Decided** | Out of scope for v1; references are informational only |
|
||||
| Cross-repo resolution | **Decided** | Offline-first resolution chain with local registry |
|
||||
| Shared contracts | **Decided** | Explicit owner repo required; no default pure-spec-repo pattern |
|
||||
| Spec inheritance | Open | Inheritance vs references vs none |
|
||||
| Multi-repo support | Open | Workspace concept TBD |
|
||||
| Dependency tracking | Open | Probably out of scope initially |
|
||||
|
||||
### Key Insight
|
||||
|
||||
The "workspace" question is really two separate questions:
|
||||
1. **Config/profile scope** → Solved with global + project (no workspace needed)
|
||||
2. **Plan vs. execution organization** → Direction set: initiatives coordinate, repo-local changes implement, workspace remains a coordination layer
|
||||
2. **Spec/change organization** → Unsolved, needs deeper design work
|
||||
|
||||
These should be separate changes with separate explorations.
|
||||
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
# Workspace Roadmap
|
||||
|
||||
## Purpose
|
||||
|
||||
This document proposes a lightweight roadmap for workspace, monorepo, and multi-repo support in OpenSpec.
|
||||
|
||||
It assumes:
|
||||
|
||||
- single-repo is the current default experience
|
||||
- monorepo pain is already real
|
||||
- multi-repo coordination is not hypothetical
|
||||
- large engineering organizations already need this
|
||||
|
||||
This roadmap is intentionally staged.
|
||||
|
||||
The goal is not to build the full conceptual system at once.
|
||||
|
||||
The goal is to ship the smallest credible version of cross-boundary support while preserving a path to a stronger long-term model.
|
||||
|
||||
---
|
||||
|
||||
## Product Principle
|
||||
|
||||
> Prefer the smallest feature set that solves real cross-boundary work without blocking the likely long-term direction.
|
||||
|
||||
This means:
|
||||
|
||||
- do not overbuild governance before usage proves it
|
||||
- do not underbuild coordination if real teams already need it
|
||||
- do not add complexity to the single-repo path unless it clearly pays for itself
|
||||
|
||||
---
|
||||
|
||||
## What We Believe Now
|
||||
|
||||
Based on the current exploration work, several things look increasingly clear.
|
||||
|
||||
### 1. Nested spec organization is needed
|
||||
|
||||
OpenSpec needs a better way to organize:
|
||||
|
||||
- shared contracts
|
||||
- local implementation specs
|
||||
- multi-area behavior inside one root
|
||||
|
||||
### 2. Informational references are low-risk and useful
|
||||
|
||||
References help agents and humans navigate related specs without requiring OpenSpec to build a dependency graph system on day one.
|
||||
|
||||
### 3. Initiatives plus linked per-repo changes are the right primitive
|
||||
|
||||
For true multi-repo work, the likely durable primitive is:
|
||||
|
||||
- one initiative as the shared planning object
|
||||
- one linked change per owning repo as the execution artifact
|
||||
- stable identifiers connecting them
|
||||
|
||||
### 4. Cross-repo work needs a neutral planning location
|
||||
|
||||
For multi-repo work, a single repo is not an honest home for the whole planning artifact.
|
||||
|
||||
Some form of coordination workspace or coordination repo is needed for the initiative-level plan.
|
||||
|
||||
### 5. Team-shared coordination is a real requirement
|
||||
|
||||
This is not just a solo-user thought experiment.
|
||||
|
||||
Real teams and large engineering orgs already need a way to coordinate multi-repo work.
|
||||
|
||||
### 6. The risk is shipping too much at once
|
||||
|
||||
Even though the need is real, the full model has many moving parts:
|
||||
|
||||
- nested spec paths
|
||||
- shared contracts
|
||||
- linked changes
|
||||
- cross-root planning
|
||||
- partial repo resolution
|
||||
- agent capability differences
|
||||
- team-shared coordination state
|
||||
|
||||
The roadmap should sequence these carefully.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Better Structure Inside One Root
|
||||
|
||||
### Goal
|
||||
|
||||
Reduce pain in single-repo and monorepo setups without introducing coordination machinery yet.
|
||||
|
||||
### Ship
|
||||
|
||||
1. Nested spec paths within one `openspec/` root
|
||||
2. Informational `references` in specs
|
||||
3. Better filtering of relevant spec paths during planning
|
||||
4. Better handling of multi-area changes inside one root
|
||||
|
||||
### User value
|
||||
|
||||
- monorepo users can organize shared and local specs more naturally
|
||||
- large roots become less noisy
|
||||
- shared contracts inside one root become easier to model
|
||||
|
||||
### Do not ship yet
|
||||
|
||||
- coordination workspaces
|
||||
- linked multi-repo changes
|
||||
- team-shared coordination repos
|
||||
- sponsor/owner workflow machinery
|
||||
|
||||
### Success criteria
|
||||
|
||||
- users can model large monorepos without flattening everything at the top level
|
||||
- users can represent shared contracts inside one root
|
||||
- planning context gets smaller and more relevant
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Thin Cross-Repo Coordination
|
||||
|
||||
### Goal
|
||||
|
||||
Support real multi-repo planning demand with the thinnest credible coordination layer.
|
||||
|
||||
### Ship
|
||||
|
||||
1. Initiative artifacts for shared planning in a neutral coordination workspace or coordination repo
|
||||
2. Linked per-repo changes using stable project identifiers
|
||||
3. Explicit repo linking via project IDs
|
||||
4. Resolution through:
|
||||
- explicit input
|
||||
- git remote matching
|
||||
5. Partial-resolution support
|
||||
6. Basic agent handoff instructions for coordinated planning
|
||||
|
||||
### User value
|
||||
|
||||
- users have an honest place to stand for multi-repo work
|
||||
- cross-repo plans are no longer buried in one repo
|
||||
- shared planning and repo-local execution are clearly separated
|
||||
- ownership stays with the real repos
|
||||
- agents can be told what roots matter
|
||||
|
||||
### Key constraints
|
||||
|
||||
This phase should remain thin.
|
||||
|
||||
Avoid:
|
||||
|
||||
- dependency validation across repos
|
||||
- rich governance flows
|
||||
- too many new abstractions in the CLI
|
||||
- heavyweight local/shared state semantics
|
||||
|
||||
### v1 shape
|
||||
|
||||
This phase should feel like:
|
||||
|
||||
- local planning by default
|
||||
- upgrade to a coordinated initiative when needed
|
||||
- initiative-level planning in the coordination workspace
|
||||
- linked repo-local changes underneath
|
||||
|
||||
Not like:
|
||||
|
||||
- a whole second product mode with many admin concepts
|
||||
|
||||
### Success criteria
|
||||
|
||||
- teams can coordinate multi-repo changes without inventing ad hoc spreadsheets or naming conventions
|
||||
- users understand where planning lives and where implementation lives
|
||||
- agents can plan across roots in a way that is operationally usable
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Team-Shared Coordination Hardening
|
||||
|
||||
### Goal
|
||||
|
||||
Make coordinated planning work cleanly across teammates and teams.
|
||||
|
||||
### Ship
|
||||
|
||||
1. Shared coordination repo/workspace support as a first-class pattern
|
||||
2. Clear split between:
|
||||
- committed shared initiative state
|
||||
- local machine-specific path resolution
|
||||
3. Lightweight relinking / repair flows
|
||||
4. Better onboarding for teammates joining an initiative
|
||||
5. Better agent instruction generation for shared workspaces
|
||||
|
||||
### User value
|
||||
|
||||
- teams can share a stable cross-repo initiative
|
||||
- each teammate can map project IDs to their own local clones
|
||||
- new participants can join without reverse-engineering how the initiative is set up
|
||||
|
||||
### Important constraint
|
||||
|
||||
The local side of this model should stay as thin as possible.
|
||||
|
||||
The ideal local layer is:
|
||||
|
||||
- regenerable
|
||||
- non-authoritative
|
||||
- not semantically important beyond path resolution
|
||||
|
||||
### Success criteria
|
||||
|
||||
- team-shared coordination works without local path leakage into committed state
|
||||
- joining an initiative feels lightweight
|
||||
- maintenance cost stays acceptable
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Shared Contract and Governance Maturity
|
||||
|
||||
### Goal
|
||||
|
||||
Support organizations that need stronger contract ownership and more formal cross-boundary planning.
|
||||
|
||||
### Ship only if demand justifies it
|
||||
|
||||
1. Guided shared contract ownership flows
|
||||
2. Promotion of initiative-only draft behavior into canonical shared contracts
|
||||
3. Stronger role visibility:
|
||||
- canonical shared contract owner
|
||||
- initiative sponsor/driver
|
||||
4. Optional linting or policy checks
|
||||
5. Optional validation around missing owners or unresolved references
|
||||
|
||||
### User value
|
||||
|
||||
- larger orgs can create durable shared contracts cleanly
|
||||
- governance becomes explicit where needed
|
||||
- cross-team ownership becomes easier to understand
|
||||
|
||||
### Important constraint
|
||||
|
||||
This should not become mandatory for normal users.
|
||||
|
||||
These features should remain:
|
||||
|
||||
- opt-in
|
||||
- advanced
|
||||
- proportional to org complexity
|
||||
|
||||
### Success criteria
|
||||
|
||||
- shared-contract workflows solve real org-scale problems without making normal planning feel bureaucratic
|
||||
|
||||
---
|
||||
|
||||
## What Should Not Be Delayed
|
||||
|
||||
Because demand is already real, some things should not be treated as purely future work.
|
||||
|
||||
### Should happen soon
|
||||
|
||||
- nested spec paths
|
||||
- references
|
||||
- initiative artifact + linked change primitive
|
||||
- stable project identifiers
|
||||
- thin coordination layer for multi-repo planning
|
||||
|
||||
### Can wait
|
||||
|
||||
- rich ownership workflows
|
||||
- strong dependency semantics
|
||||
- broad policy and governance features
|
||||
- too much agent-specific machinery
|
||||
|
||||
---
|
||||
|
||||
## UX Guardrails Across All Phases
|
||||
|
||||
No matter the phase, the UX should follow these rules.
|
||||
|
||||
### 1. Default local
|
||||
|
||||
Users should start where they already are.
|
||||
|
||||
### 2. Escalate only when necessary
|
||||
|
||||
Coordinated planning should appear as an upgrade path, not the default mode.
|
||||
|
||||
### 3. Keep advanced concepts mostly implicit
|
||||
|
||||
Only expose concepts like shared owners, sponsor roles, overlays, and manifests when the user truly needs to decide something.
|
||||
|
||||
### 4. Canonical storage follows ownership
|
||||
|
||||
Specs and repo-local changes stay with the owning root.
|
||||
|
||||
### 5. Shared coordination is not canonical spec storage
|
||||
|
||||
Coordination data helps planning, but does not replace the source of truth.
|
||||
|
||||
### 6. Hidden local state must stay thin
|
||||
|
||||
If OpenSpec uses local path caches or machine-specific mappings, they should be:
|
||||
|
||||
- repairable
|
||||
- replaceable
|
||||
- non-authoritative
|
||||
|
||||
---
|
||||
|
||||
## Likely Deliverable Sequence
|
||||
|
||||
If this roadmap were translated into actual change proposals, the sequence would likely be:
|
||||
|
||||
1. nested spec paths + references
|
||||
2. monorepo scope filtering and multi-area planning improvements
|
||||
3. initiative artifact for shared planning
|
||||
4. linked change metadata across repos
|
||||
5. thin coordination workspace / repo for multi-repo planning
|
||||
6. team-shared coordination hardening
|
||||
7. optional shared contract maturity features
|
||||
|
||||
---
|
||||
|
||||
## Open Risks
|
||||
|
||||
### 1. Coordination may still be too heavy in v1
|
||||
|
||||
Even a thin coordination layer may feel like too much if the handoff is clumsy.
|
||||
|
||||
### 2. Hidden local state may become more important than intended
|
||||
|
||||
If path resolution or local repo linking becomes semantically important, the system will become harder to trust and debug.
|
||||
|
||||
### 3. Monorepo and multi-repo may diverge unintentionally
|
||||
|
||||
The product should resist evolving two completely separate mental models.
|
||||
|
||||
### 4. Agent capability differences may distort the design
|
||||
|
||||
The UX should not assume every coding agent handles multi-root planning equally well.
|
||||
|
||||
### 5. Team-scale needs may pressure early governance
|
||||
|
||||
Large orgs may quickly ask for ownership, permissions, and review structures. That should not force all users into heavyweight flows.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The roadmap should not be:
|
||||
|
||||
- "wait on multi-repo until later"
|
||||
|
||||
Because the demand is already real.
|
||||
|
||||
It also should not be:
|
||||
|
||||
- "build the full workspace model now"
|
||||
|
||||
Because the complexity surface is too large.
|
||||
|
||||
The right roadmap is:
|
||||
|
||||
1. improve structure inside one root
|
||||
2. ship a thin but real coordination layer for multi-repo work
|
||||
3. harden team-shared coordination
|
||||
4. add more formal shared contract and governance support only as justified
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,491 +0,0 @@
|
||||
# Workspace UX Simplification
|
||||
|
||||
## Purpose
|
||||
|
||||
This document focuses on one UX goal:
|
||||
|
||||
> OpenSpec should have one default path, one escalation path, and fewer explicit concepts shown to the user unless the system actually needs a decision from them.
|
||||
|
||||
This is a follow-up to `workspace-user-journeys.md`. That document is useful for completeness, but it exposes too much of the conceptual model too early.
|
||||
|
||||
This document is about how the product should **feel**.
|
||||
|
||||
---
|
||||
|
||||
## The UX Problem
|
||||
|
||||
The current user-journey exploration is coherent, but it is too heavy at first contact.
|
||||
|
||||
The main issues are:
|
||||
|
||||
1. Too many concepts appear before the user has done anything:
|
||||
- scope
|
||||
- project
|
||||
- owning root
|
||||
- shared contract owner
|
||||
- coordination workspace
|
||||
- initiative sponsor
|
||||
- shared manifest vs local overlay
|
||||
|
||||
2. Cross-root work feels like a workflow restart:
|
||||
- user starts in one repo
|
||||
- OpenSpec says this is multi-repo
|
||||
- user creates a workspace
|
||||
- user reopens the agent there
|
||||
- user effectively starts again
|
||||
|
||||
3. Shared contract decisions are asked too explicitly and too early.
|
||||
|
||||
4. Team-scale coordination is conceptually right, but reads more like infra setup than a lightweight workflow.
|
||||
|
||||
The system is internally clean, but the product experience should be more progressive.
|
||||
|
||||
---
|
||||
|
||||
## Design Goal
|
||||
|
||||
The user should feel:
|
||||
|
||||
- "I just start where I am"
|
||||
- "OpenSpec figures out whether this stays local or needs to expand"
|
||||
- "If it expands, it carries me forward instead of making me restart"
|
||||
- "I only see advanced concepts when OpenSpec needs a real decision from me"
|
||||
|
||||
---
|
||||
|
||||
## The Core UX Shape
|
||||
|
||||
### One default path
|
||||
|
||||
The default path should always be:
|
||||
|
||||
1. Enter a repo or monorepo root
|
||||
2. Run `/opsx:explore` or `/opsx:propose`
|
||||
3. OpenSpec plans locally unless it has a strong reason not to
|
||||
|
||||
This should work for:
|
||||
|
||||
- single repo
|
||||
- normal monorepo work
|
||||
- many users in many situations
|
||||
|
||||
The default assumption should be:
|
||||
|
||||
> This is a local change until proven otherwise.
|
||||
|
||||
### One escalation path
|
||||
|
||||
The only escalation path should be:
|
||||
|
||||
> This work spans multiple owned areas strongly enough that OpenSpec needs to upgrade it into a coordinated initiative.
|
||||
|
||||
That escalation may happen for:
|
||||
|
||||
- large monorepo cross-team work
|
||||
- true multi-repo work
|
||||
- creation of a shared cross-boundary contract
|
||||
|
||||
The important UX point is that these should all feel like the same escalation:
|
||||
|
||||
- "OpenSpec is upgrading this into a coordinated initiative"
|
||||
|
||||
Not:
|
||||
|
||||
- one flow for multi-repo
|
||||
- another flow for large monorepos
|
||||
- another flow for shared contracts
|
||||
|
||||
---
|
||||
|
||||
## Progressive Disclosure
|
||||
|
||||
Users should not have to understand the full data model up front.
|
||||
|
||||
### Concepts users should see by default
|
||||
|
||||
At the start, users should mostly see:
|
||||
|
||||
- change
|
||||
- affected area
|
||||
- maybe repo if relevant
|
||||
|
||||
That is enough for the first planning step.
|
||||
|
||||
### Concepts OpenSpec should keep implicit until needed
|
||||
|
||||
These should usually stay hidden until escalation:
|
||||
|
||||
- scope
|
||||
- coordination workspace
|
||||
- initiative
|
||||
- shared contract owner
|
||||
- sponsor/driver
|
||||
- manifest vs local overlay
|
||||
|
||||
### Concepts OpenSpec should only show when a real decision is needed
|
||||
|
||||
Show these only at the point of action:
|
||||
|
||||
- "This spans multiple repos. Create a coordinated initiative?"
|
||||
- "This looks like shared behavior. Where should the canonical contract live?"
|
||||
- "This initiative is team-shared. Do you want to commit it in a shared coordination repo?"
|
||||
|
||||
The system should not front-load these concepts as theory.
|
||||
|
||||
---
|
||||
|
||||
## The Simplest User Story
|
||||
|
||||
This is the baseline story the UX should optimize for.
|
||||
|
||||
### Story
|
||||
|
||||
The user is in a repo and types:
|
||||
|
||||
```text
|
||||
/opsx:propose add-3ds
|
||||
```
|
||||
|
||||
OpenSpec should:
|
||||
|
||||
1. inspect local context
|
||||
2. infer likely affected areas
|
||||
3. ask for confirmation only if needed
|
||||
4. continue immediately
|
||||
|
||||
The user should feel like they are doing one thing:
|
||||
|
||||
```text
|
||||
I am proposing a change.
|
||||
```
|
||||
|
||||
Not:
|
||||
|
||||
```text
|
||||
I am selecting between multiple planning abstractions.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Escalation Story
|
||||
|
||||
If OpenSpec realizes the work is no longer local, it should escalate in one motion.
|
||||
|
||||
### Desired feel
|
||||
|
||||
```text
|
||||
This change affects multiple owned areas.
|
||||
I can upgrade it into a coordinated initiative and carry your current planning context forward.
|
||||
```
|
||||
|
||||
That wording matters.
|
||||
|
||||
It should feel like:
|
||||
|
||||
- an upgrade
|
||||
- a continuation
|
||||
- a convenience
|
||||
|
||||
It should not feel like:
|
||||
|
||||
- an error
|
||||
- a hard stop
|
||||
- a separate setup workflow
|
||||
|
||||
### What should happen during escalation
|
||||
|
||||
If escalation is needed, OpenSpec should do as much as possible automatically:
|
||||
|
||||
1. carry forward the current change name / description
|
||||
2. preserve the already inferred affected areas
|
||||
3. create the coordination artifact
|
||||
4. resolve any local roots it can
|
||||
5. generate agent instructions
|
||||
6. then tell the user the next step
|
||||
|
||||
### Example escalation UX
|
||||
|
||||
```text
|
||||
This work spans multiple owned areas:
|
||||
- contracts
|
||||
- billing-service
|
||||
- web-client
|
||||
- ios-client
|
||||
|
||||
OpenSpec can upgrade this into a coordinated initiative.
|
||||
|
||||
Suggested next step:
|
||||
- create a coordination workspace at ~/work/openspec-workspaces/add-3ds
|
||||
|
||||
I’ll carry forward:
|
||||
- your current change description
|
||||
- affected repos
|
||||
- any planning notes already gathered
|
||||
```
|
||||
|
||||
This is much better than making the user feel they must restart.
|
||||
|
||||
---
|
||||
|
||||
## The Minimum Decision Set
|
||||
|
||||
When OpenSpec has to ask questions, it should ask the smallest useful set.
|
||||
|
||||
### Decision 1: Is this local or coordinated?
|
||||
|
||||
Most important product question.
|
||||
|
||||
User-facing form:
|
||||
|
||||
```text
|
||||
This appears to span multiple owned areas.
|
||||
|
||||
How should I proceed?
|
||||
- Keep this as one local change
|
||||
- Upgrade to a coordinated initiative
|
||||
```
|
||||
|
||||
This should be used sparingly and only when ambiguity matters.
|
||||
|
||||
### Decision 2: What areas are affected?
|
||||
|
||||
User-facing form:
|
||||
|
||||
```text
|
||||
Which areas are affected?
|
||||
```
|
||||
|
||||
This is much more intuitive than asking users about "scopes" first.
|
||||
|
||||
Internally this is scope selection, but the user does not need that term unless advanced users want it.
|
||||
|
||||
### Decision 3: Is this shared behavior?
|
||||
|
||||
Only ask if OpenSpec has strong evidence of a cross-boundary contract.
|
||||
|
||||
User-facing form:
|
||||
|
||||
```text
|
||||
This looks like behavior that multiple areas need to follow.
|
||||
|
||||
Should I treat this as:
|
||||
- local changes only
|
||||
- a shared contract
|
||||
- draft coordination notes for now
|
||||
```
|
||||
|
||||
### Decision 4: Where should shared ownership live?
|
||||
|
||||
Only ask if the user confirms shared contract behavior and no obvious existing owner exists.
|
||||
|
||||
User-facing form:
|
||||
|
||||
```text
|
||||
Where should the canonical shared contract live?
|
||||
```
|
||||
|
||||
This should appear late, not early.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Terminology
|
||||
|
||||
The internal model may use many precise terms. The UI should use simpler terms.
|
||||
|
||||
### Prefer in user-facing UX
|
||||
|
||||
- "area" instead of "scope" by default
|
||||
- "coordinated initiative" instead of "workspace model"
|
||||
- "shared contract" instead of "cross-boundary canonical spec"
|
||||
- "owner" instead of "owning root"
|
||||
- "team-shared initiative" instead of "shared coordination manifest"
|
||||
|
||||
### Reserve for advanced UX or docs
|
||||
|
||||
- scope
|
||||
- project root
|
||||
- local overlay
|
||||
- sponsor/driver
|
||||
- coordination workspace
|
||||
|
||||
These terms are useful, but not ideal as the first thing users must absorb.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Default Behavior
|
||||
|
||||
To keep the UX intuitive, OpenSpec should aggressively choose defaults.
|
||||
|
||||
### Default 1: Stay local
|
||||
|
||||
Unless there is strong evidence otherwise, planning stays in the current root.
|
||||
|
||||
### Default 2: Infer affected areas
|
||||
|
||||
OpenSpec should infer affected areas from:
|
||||
|
||||
- request wording
|
||||
- current repo
|
||||
- known spec layout
|
||||
- recent initiative context
|
||||
|
||||
Ask the user only when there is meaningful ambiguity.
|
||||
|
||||
### Default 3: Reuse existing shared owners
|
||||
|
||||
If an existing shared contract owner already exists, OpenSpec should suggest it instead of asking an abstract ownership question.
|
||||
|
||||
### Default 4: Treat unresolved roots as partial, not fatal
|
||||
|
||||
For coordinated initiatives, unresolved repos should not block planning unless the user explicitly needs implementation there now.
|
||||
|
||||
### Default 5: Team-shared only when collaboration is real
|
||||
|
||||
Do not force team/shared setup for solo or exploratory work.
|
||||
|
||||
OpenSpec can start with a local coordination workspace and later offer:
|
||||
|
||||
```text
|
||||
This now looks collaborative. Do you want to move it into a shared coordination repo?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How To Make Team UX Feel Light
|
||||
|
||||
The team story should not feel like an admin ceremony.
|
||||
|
||||
### Desired team experience
|
||||
|
||||
1. One person starts planning normally
|
||||
2. OpenSpec upgrades to a coordinated initiative if needed
|
||||
3. When the work becomes collaborative, OpenSpec offers to make it team-shared
|
||||
4. Teammates clone the initiative repo and run one linking command
|
||||
5. Everyone starts from the same shared initiative context
|
||||
|
||||
### Team onboarding should feel like this
|
||||
|
||||
```text
|
||||
Clone the initiative repo.
|
||||
Run `openspec workspace doctor`.
|
||||
Open your agent here.
|
||||
```
|
||||
|
||||
Not like this:
|
||||
|
||||
```text
|
||||
Learn a new planning model, understand manifests, configure overlays, and attach roots manually.
|
||||
```
|
||||
|
||||
The implementation may require those concepts, but the UX should compress them into a few actions.
|
||||
|
||||
---
|
||||
|
||||
## UX Heuristics For Prompting
|
||||
|
||||
OpenSpec should avoid asking users to classify work in abstract ways if it can infer a reasonable default.
|
||||
|
||||
### Good prompt
|
||||
|
||||
```text
|
||||
This affects:
|
||||
- web checkout
|
||||
- billing API
|
||||
- shared checkout behavior
|
||||
|
||||
I think this should become a coordinated initiative.
|
||||
Proceed?
|
||||
```
|
||||
|
||||
Why this is good:
|
||||
|
||||
- concrete
|
||||
- recommendation included
|
||||
- low cognitive load
|
||||
|
||||
### Weaker prompt
|
||||
|
||||
```text
|
||||
Would you like to create a coordination workspace with linked changes and shared ownership metadata?
|
||||
```
|
||||
|
||||
Why this is weaker:
|
||||
|
||||
- too much internal machinery exposed
|
||||
- user has to parse product architecture before saying yes
|
||||
|
||||
### Good ownership prompt
|
||||
|
||||
```text
|
||||
I found an existing shared contracts area: `contracts/checkout`.
|
||||
Use that as the canonical owner?
|
||||
```
|
||||
|
||||
### Weaker ownership prompt
|
||||
|
||||
```text
|
||||
Choose a canonical shared contract owner for this cross-boundary behavior.
|
||||
```
|
||||
|
||||
The latter is precise, but too abstract unless the user is already deep in the workflow.
|
||||
|
||||
---
|
||||
|
||||
## The Experience We Should Aim For
|
||||
|
||||
By default, OpenSpec should feel like:
|
||||
|
||||
- "Start here"
|
||||
- "Describe the work"
|
||||
- "I’ll handle the shape unless I need your judgment"
|
||||
|
||||
When the system escalates, it should feel like:
|
||||
|
||||
- "This got bigger than one local change"
|
||||
- "I’ve prepared the coordinated setup for you"
|
||||
- "Here is the next obvious step"
|
||||
|
||||
When collaboration expands, it should feel like:
|
||||
|
||||
- "This is now team-shared"
|
||||
- "Commit the stable plan"
|
||||
- "Everyone links their own local clones"
|
||||
|
||||
The user should not feel like they are constantly switching conceptual frameworks.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Follow-Up Changes To The Journeys
|
||||
|
||||
To make `workspace-user-journeys.md` simpler and more intuitive, the next revision should:
|
||||
|
||||
1. Move the simplest single-repo and monorepo journey to the top.
|
||||
2. Move most terminology and internal model sections later or into an appendix.
|
||||
3. Reframe "coordination workspace" as an escalation artifact, not a starting abstraction.
|
||||
4. Replace many uses of "scope" with "area" in user-facing examples.
|
||||
5. Convert abstract ownership questions into recommendation-first prompts.
|
||||
6. Compress the team-scale setup into one simple story:
|
||||
- shared initiative repo
|
||||
- local link command
|
||||
- open agent here
|
||||
7. Make the escalation flow explicitly preserve user context so it reads as continuation, not restart.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The current workspace thinking is directionally right, but the UX should become much more opinionated and much less explanatory up front.
|
||||
|
||||
The simplest product shape is:
|
||||
|
||||
- one default path: local planning from where the user already is
|
||||
- one escalation path: upgrade into a coordinated initiative when needed
|
||||
- progressive disclosure: only show advanced concepts when OpenSpec needs a real decision
|
||||
|
||||
If OpenSpec does this well, the same system can feel intuitive for:
|
||||
|
||||
- solo users
|
||||
- small teams
|
||||
- large monorepos
|
||||
- multi-repo teams
|
||||
- cross-team initiatives
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
Define AI tool path metadata used to generate OpenSpec skills and commands in tool-specific directories.
|
||||
|
||||
## Requirements
|
||||
### Requirement: AIToolOption skillsDir field
|
||||
|
||||
@@ -37,11 +38,6 @@ The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent
|
||||
- **WHEN** looking up the `windsurf` tool
|
||||
- **THEN** `skillsDir` SHALL be `.windsurf`
|
||||
|
||||
#### Scenario: Kimi CLI paths defined
|
||||
|
||||
- **WHEN** looking up the `kimi` tool
|
||||
- **THEN** `skillsDir` SHALL be `.kimi`
|
||||
|
||||
#### Scenario: Tools without skillsDir
|
||||
|
||||
- **WHEN** a tool has no `skillsDir` defined
|
||||
@@ -61,3 +57,4 @@ The system SHALL handle paths correctly across operating systems.
|
||||
|
||||
- **WHEN** constructing skill paths on macOS or Linux
|
||||
- **THEN** the system SHALL use `path.join()` for consistency
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ The system SHALL generate schema-aware apply instructions via `openspec instruct
|
||||
- **WHEN** user runs `openspec instructions apply --change <id>`
|
||||
- **AND** all required artifacts (per schema's `apply.requires`) exist
|
||||
- **THEN** the system outputs:
|
||||
- `contextFiles` mapping artifact IDs to arrays of concrete paths for all existing artifacts
|
||||
- Context files from all existing artifacts
|
||||
- Schema-specific instruction text
|
||||
- Progress tracking file path (if `apply.tracks` is set)
|
||||
|
||||
@@ -218,7 +218,7 @@ The system SHALL generate schema-aware apply instructions via `openspec instruct
|
||||
|
||||
- **WHEN** user runs `openspec instructions apply --change <id> --json`
|
||||
- **THEN** the system outputs JSON with:
|
||||
- `contextFiles`: object mapping artifact IDs to arrays of concrete paths for existing artifacts
|
||||
- `contextFiles`: array of paths to existing artifacts
|
||||
- `instruction`: the apply instruction text
|
||||
- `tracks`: path to progress file or null
|
||||
- `applyRequires`: list of required artifact IDs
|
||||
|
||||
@@ -200,11 +200,11 @@ The command SHALL generate Agent Skills for selected AI tools.
|
||||
|
||||
### Requirement: Slash Command Generation
|
||||
|
||||
The command SHALL generate opsx slash commands only for selected tools that have a registered command adapter, while keeping adapterless tools valid for skill generation.
|
||||
The command SHALL generate opsx slash commands for selected AI tools.
|
||||
|
||||
#### Scenario: Generating slash commands for a tool with a registered adapter
|
||||
#### Scenario: Generating slash commands for a tool
|
||||
|
||||
- **WHEN** a tool with a registered command adapter is selected during initialization
|
||||
- **WHEN** a tool is selected during initialization
|
||||
- **THEN** create 9 slash command files using the tool's command adapter:
|
||||
- `/opsx:explore`
|
||||
- `/opsx:new`
|
||||
@@ -218,20 +218,6 @@ The command SHALL generate opsx slash commands only for selected tools that have
|
||||
- **AND** use tool-specific path conventions (e.g., `.claude/commands/opsx/` for Claude)
|
||||
- **AND** include tool-specific frontmatter format
|
||||
|
||||
#### Scenario: Selected tool has no command adapter
|
||||
|
||||
- **GIVEN** a selected tool has `skillsDir` configured but no registered command adapter
|
||||
- **WHEN** initialization includes command generation
|
||||
- **THEN** skill generation for that tool SHALL still remain valid
|
||||
- **AND** command-file generation SHALL be skipped for that tool
|
||||
- **AND** the command output SHALL include `Commands skipped for: <tool-id> (no adapter)`
|
||||
|
||||
#### Scenario: Kimi CLI skips command-file generation
|
||||
|
||||
- **WHEN** the user selects Kimi CLI during initialization
|
||||
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi'`
|
||||
- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered
|
||||
|
||||
### Requirement: Config File Generation
|
||||
|
||||
The command SHALL create an OpenSpec config file with schema settings.
|
||||
|
||||
@@ -168,7 +168,7 @@ The archive slash command template SHALL support optional change ID arguments fo
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Error Handling
|
||||
### Requirement: Error Handling
|
||||
|
||||
The command SHALL handle edge cases gracefully.
|
||||
|
||||
|
||||
@@ -245,34 +245,6 @@ OpenSpec CLI design SHALL use verbs as top-level commands with nouns provided as
|
||||
- **THEN** `openspec show` and `openspec validate` SHALL accept `--type spec|change`
|
||||
- **AND** the help text SHALL document this clearly
|
||||
|
||||
### Requirement: Workspace Product Language
|
||||
OpenSpec conventions SHALL describe coordination workspaces in user-facing product terms.
|
||||
|
||||
#### Scenario: Describing workspace structure
|
||||
- **WHEN** OpenSpec documentation describes workspace support
|
||||
- **THEN** it SHALL present a workspace as the planning home for work across linked repos or folders
|
||||
- **AND** it SHALL describe `changes/` as the workspace planning area
|
||||
|
||||
#### Scenario: Avoiding internal workspace vocabulary
|
||||
- **WHEN** OpenSpec documentation explains what a workspace includes
|
||||
- **THEN** it SHALL prefer plain product language such as "repos or folders"
|
||||
- **AND** it SHALL avoid user-facing reliance on terms such as "working set", "code area", "entry", "alias", or "local overlay"
|
||||
|
||||
#### Scenario: Distinguishing workspaces from changes
|
||||
- **WHEN** OpenSpec documentation explains workspace planning
|
||||
- **THEN** it SHALL describe a workspace as a durable planning home
|
||||
- **AND** it SHALL describe individual features, fixes, and projects as changes inside the workspace
|
||||
|
||||
#### Scenario: Distinguishing workspace and repo-local surfaces
|
||||
- **WHEN** OpenSpec documentation compares workspace and repo-local flows
|
||||
- **THEN** it SHALL explain that workspace planning lives in the workspace folder
|
||||
- **AND** it SHALL explain that repo-local specs and changes continue to live under each repo's `openspec/` directory
|
||||
|
||||
#### Scenario: Sequencing the workspace roadmap
|
||||
- **WHEN** workspace reimplementation work is split across multiple active changes
|
||||
- **THEN** conventions SHALL allow those changes to remain flat siblings under `openspec/changes/`
|
||||
- **AND** dependency order MAY be documented in proposal prose until formal change stacking metadata is available
|
||||
|
||||
## Core Principles
|
||||
|
||||
The system SHALL follow these principles:
|
||||
@@ -283,7 +255,7 @@ The system SHALL follow these principles:
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Project Structure
|
||||
### Requirement: Project Structure
|
||||
|
||||
An OpenSpec project SHALL maintain a consistent directory structure for specifications and changes.
|
||||
|
||||
@@ -313,7 +285,7 @@ openspec/
|
||||
|
||||
## Specification Format
|
||||
|
||||
### Behavioral Spec Format
|
||||
### Requirement: Structured Format for Behavioral Specs
|
||||
|
||||
Behavioral specifications SHALL use a structured format with consistent section headers and keywords to ensure visual consistency and parseability.
|
||||
|
||||
@@ -344,7 +316,7 @@ Behavioral specifications SHALL use a structured format with consistent section
|
||||
|
||||
## Change Storage Convention
|
||||
|
||||
### Header-Based Requirement Identification
|
||||
### Requirement: Header-Based Requirement Identification
|
||||
|
||||
Requirement headers SHALL serve as unique identifiers for programmatic matching between current specs and proposed changes.
|
||||
|
||||
@@ -373,7 +345,7 @@ Requirement headers SHALL serve as unique identifiers for programmatic matching
|
||||
- **THEN** ensure no duplicate headers exist within a spec
|
||||
- **AND** validation tools SHALL flag duplicate headers as errors
|
||||
|
||||
### Change Storage Convention
|
||||
### Requirement: Change Storage Convention
|
||||
|
||||
Change proposals SHALL store only the additions, modifications, and removals to specifications, not complete future states.
|
||||
|
||||
@@ -416,7 +388,7 @@ The `changes/[name]/specs/` directory SHALL contain:
|
||||
- `-` for REMOVED (red)
|
||||
- `→` for RENAMED (cyan)
|
||||
|
||||
### Archive Process Enhancement
|
||||
### Requirement: Archive Process Enhancement
|
||||
|
||||
The archive process SHALL programmatically apply delta changes to current specifications using header-based matching.
|
||||
|
||||
@@ -439,7 +411,7 @@ The archive process SHALL programmatically apply delta changes to current specif
|
||||
- **AND** require manual resolution before proceeding
|
||||
- **AND** provide clear guidance on resolving conflicts
|
||||
|
||||
### Proposal Format
|
||||
### Requirement: Proposal Format
|
||||
|
||||
Proposals SHALL explicitly document all changes with clear from/to comparisons.
|
||||
|
||||
@@ -472,7 +444,7 @@ The change process SHALL follow these states:
|
||||
|
||||
## Viewing Changes
|
||||
|
||||
### Change Review
|
||||
### Requirement: Change Review
|
||||
|
||||
The system SHALL support multiple methods for reviewing proposed changes.
|
||||
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
# workspace-foundation Specification
|
||||
|
||||
## Purpose
|
||||
Define the product and storage foundation for OpenSpec coordination workspaces,
|
||||
including workspace identity, shared versus local state, managed storage,
|
||||
registry behavior, stable link names, and repo ownership boundaries.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Recognizable Workspace Home
|
||||
OpenSpec SHALL give users and agents a recognizable workspace home for cross-repo planning.
|
||||
|
||||
#### Scenario: Planning across linked repos or folders
|
||||
- **WHEN** a user creates an OpenSpec workspace for repos or folders they plan across
|
||||
- **THEN** the workspace SHALL provide a durable planning home
|
||||
- **AND** the workspace SHALL be able to hold multiple changes over time
|
||||
|
||||
#### Scenario: Working from inside a workspace
|
||||
- **GIVEN** a user runs OpenSpec from a workspace folder or one of its subdirectories
|
||||
- **WHEN** OpenSpec resolves the current workspace
|
||||
- **THEN** it SHALL identify the workspace location
|
||||
- **AND** it SHALL use the workspace location's `changes/` directory as the workspace planning area
|
||||
|
||||
#### Scenario: Avoiding accidental workspace mode
|
||||
- **GIVEN** a directory has `changes/` but is not an OpenSpec workspace
|
||||
- **WHEN** OpenSpec resolves the current workspace
|
||||
- **THEN** it SHALL avoid treating that directory as a workspace
|
||||
- **AND** it SHALL enter workspace mode only when the workspace identity file is present
|
||||
|
||||
### Requirement: Stable Workspace Name
|
||||
OpenSpec SHALL use one folder-style workspace name across workspace identity, managed storage, and the local registry.
|
||||
|
||||
#### Scenario: Using one workspace name
|
||||
- **WHEN** OpenSpec creates or registers a managed workspace
|
||||
- **THEN** the workspace name SHALL be stored in `.openspec-workspace/workspace.yaml`
|
||||
- **AND** the same name SHALL be used as the default managed workspace folder name
|
||||
- **AND** the same name SHALL be used as the local registry name
|
||||
|
||||
#### Scenario: Rejecting invalid folder-style names
|
||||
- **WHEN** OpenSpec accepts a workspace name
|
||||
- **THEN** it SHALL reject empty names, `.` or `..`, and names containing path separators
|
||||
- **AND** setup or create flows SHALL report OS-level folder creation failures clearly
|
||||
|
||||
### Requirement: Dedicated Workspace Identity
|
||||
OpenSpec SHALL distinguish a coordination workspace from a repo-local OpenSpec project.
|
||||
|
||||
#### Scenario: Reading workspace identity
|
||||
- **WHEN** OpenSpec reads or writes workspace identity and workspace state
|
||||
- **THEN** it SHALL use `.openspec-workspace/`
|
||||
|
||||
#### Scenario: Preserving repo-local OpenSpec projects
|
||||
- **GIVEN** a repo-local OpenSpec project uses `openspec/`
|
||||
- **WHEN** that repo is linked to a workspace
|
||||
- **THEN** OpenSpec SHALL continue treating `openspec/` as that repo's local OpenSpec directory
|
||||
- **AND** workspace planning SHALL remain anchored in the workspace folder
|
||||
|
||||
#### Scenario: Avoiding repo-local initialization in the workspace folder
|
||||
- **WHEN** a user is working from an OpenSpec workspace folder
|
||||
- **THEN** OpenSpec SHALL treat that folder as a workspace coordination surface
|
||||
- **AND** users SHALL not need to initialize a repo-local `openspec/` project inside the workspace folder
|
||||
|
||||
### Requirement: Safe Workspace Sharing
|
||||
OpenSpec SHALL keep shared workspace information separate from local machine paths.
|
||||
|
||||
#### Scenario: Sharing workspace planning
|
||||
- **WHEN** a workspace is shared with another user or machine
|
||||
- **THEN** shared workspace information SHALL include portable workspace identity and stable link names
|
||||
- **AND** it SHALL not require another user to reuse the original user's absolute checkout paths
|
||||
|
||||
#### Scenario: Keeping checkout paths local
|
||||
- **WHEN** OpenSpec stores local paths for a workspace
|
||||
- **THEN** those paths SHALL be treated as local to the current machine and runtime
|
||||
- **AND** another machine MAY map the same link names to different local paths
|
||||
|
||||
#### Scenario: Preserving runtime-local paths
|
||||
- **WHEN** OpenSpec reads or writes machine-local path state
|
||||
- **THEN** it SHALL preserve path strings valid for the current runtime
|
||||
- **AND** it SHALL support native Windows paths and WSL2/Linux paths as local state values
|
||||
|
||||
#### Scenario: Excluding local state from portable collaboration
|
||||
- **WHEN** OpenSpec creates a workspace
|
||||
- **THEN** it SHALL exclude `.openspec-workspace/local.yaml` from portable collaboration state by default
|
||||
- **AND** `.openspec-workspace/workspace.yaml` SHALL remain the portable workspace identity and link-name state
|
||||
|
||||
### Requirement: Standard Workspace Location
|
||||
OpenSpec SHALL use a standard location for OpenSpec-managed workspaces without asking most users to choose one.
|
||||
|
||||
#### Scenario: Using the standard workspace location
|
||||
- **WHEN** OpenSpec needs the location for OpenSpec-managed workspaces
|
||||
- **THEN** it SHALL use `<global-data-dir>/workspaces`
|
||||
- **AND** `<global-data-dir>` SHALL follow existing OpenSpec XDG and platform data directory behavior
|
||||
|
||||
#### Scenario: Avoiding workspace-specific storage overrides
|
||||
- **WHEN** OpenSpec resolves the location for OpenSpec-managed workspaces
|
||||
- **THEN** it SHALL not use a workspace-specific environment variable, command, or configuration setting in this slice
|
||||
- **AND** managed workspace storage SHALL remain under `<global-data-dir>/workspaces`
|
||||
|
||||
#### Scenario: Running from native Windows
|
||||
- **WHEN** OpenSpec runs from native Windows shells such as PowerShell
|
||||
- **AND** `XDG_DATA_HOME` is not set
|
||||
- **THEN** OpenSpec SHALL store managed workspaces under the Windows global data location
|
||||
- **AND** paths SHALL follow native Windows path behavior
|
||||
|
||||
#### Scenario: Running from WSL2
|
||||
- **WHEN** OpenSpec runs from WSL2
|
||||
- **THEN** OpenSpec SHALL store managed workspaces under the Linux/XDG data location inside WSL
|
||||
- **AND** paths SHALL follow Linux path behavior inside WSL
|
||||
|
||||
#### Scenario: Using the workspace location automatically
|
||||
- **WHEN** OpenSpec creates or resolves OpenSpec-managed workspaces in later workflows
|
||||
- **THEN** it SHALL use the resolved workspace location by default
|
||||
- **AND** users SHALL be able to follow the normal workspace flow without choosing a storage location
|
||||
|
||||
#### Scenario: Showing the workspace location
|
||||
- **WHEN** OpenSpec creates a workspace in the standard workspace location
|
||||
- **THEN** it SHALL report the workspace location to the user
|
||||
- **AND** it SHALL not hide where planning files were created
|
||||
|
||||
#### Scenario: Staying in the current runtime
|
||||
- **WHEN** OpenSpec resolves workspace locations or local repo paths
|
||||
- **THEN** it SHALL interpret paths for the runtime running OpenSpec
|
||||
- **AND** Windows, UNC WSL, and WSL mount paths SHALL remain explicit user-provided paths
|
||||
|
||||
### Requirement: Local Workspace Registry
|
||||
OpenSpec SHALL keep a lightweight local registry of known workspaces on the current machine.
|
||||
|
||||
#### Scenario: Recording known workspaces
|
||||
- **WHEN** OpenSpec creates or learns about a managed workspace
|
||||
- **THEN** it SHALL be able to record the workspace name and location in a local registry
|
||||
- **AND** the registry SHALL be machine-local state
|
||||
|
||||
#### Scenario: Keeping workspace folders authoritative
|
||||
- **WHEN** OpenSpec reads workspace details
|
||||
- **THEN** each workspace folder's `.openspec-workspace/workspace.yaml` SHALL remain the source of truth for that workspace
|
||||
- **AND** the local registry SHALL act only as an index of known workspace locations
|
||||
|
||||
#### Scenario: Finding workspaces from anywhere
|
||||
- **WHEN** a later workspace command runs outside a workspace directory
|
||||
- **THEN** OpenSpec MAY use the local registry to find known workspaces
|
||||
- **AND** commands that need one workspace MAY use the registry to support an interactive picker
|
||||
|
||||
### Requirement: Stable Link Names
|
||||
OpenSpec SHALL use stable link names to refer to repos and folders in workspace planning.
|
||||
|
||||
#### Scenario: Referring to a repo or folder in workspace planning
|
||||
- **WHEN** workspace state or later workspace planning artifacts refer to a linked repo or folder
|
||||
- **THEN** they SHALL use the stable link name
|
||||
- **AND** the same link name SHALL remain valid even when local checkout paths differ
|
||||
|
||||
#### Scenario: Reusing link names across machines
|
||||
- **WHEN** a workspace is used on another machine
|
||||
- **THEN** link names SHALL remain stable
|
||||
- **AND** local checkout paths MAY differ on that machine
|
||||
|
||||
#### Scenario: Rejecting invalid link names
|
||||
- **WHEN** OpenSpec accepts a workspace link name
|
||||
- **THEN** it SHALL reject empty names, `.` or `..`, and names containing path separators
|
||||
- **AND** link names SHALL be unique within the workspace
|
||||
|
||||
### Requirement: Linked Repos And Folders
|
||||
OpenSpec SHALL allow workspace planning to include linked repos and folders before they have repo-local OpenSpec state.
|
||||
|
||||
#### Scenario: Planning with a repo that has not adopted OpenSpec
|
||||
- **WHEN** a workspace links a repo path that does not yet contain repo-local `openspec/`
|
||||
- **THEN** the repo SHALL still be available for workspace-level planning
|
||||
- **AND** implementation readiness MAY be handled by a later workflow
|
||||
|
||||
#### Scenario: Planning across monorepo folders
|
||||
- **WHEN** planning spans multiple packages, services, apps, or directories inside one monorepo
|
||||
- **THEN** the workspace SHALL be able to link those folders separately
|
||||
- **AND** each folder SHALL not need its own repo-local `openspec/` directory to participate in workspace planning
|
||||
|
||||
#### Scenario: Treating repos and folders consistently
|
||||
- **WHEN** a workspace plan includes both separate repos and folders inside a monorepo
|
||||
- **THEN** OpenSpec SHALL use the same planning model for both
|
||||
- **AND** users SHALL not need to create different kinds of workspace plans for multi-repo and monorepo changes
|
||||
|
||||
#### Scenario: Recording links without changing targets
|
||||
- **WHEN** OpenSpec records a link between a workspace and a local repo or folder
|
||||
- **THEN** it SHALL store the link in workspace state
|
||||
- **AND** it SHALL not create, copy, move, initialize, or edit files inside the linked repo or folder
|
||||
|
||||
### Requirement: Planning Before Implementation
|
||||
OpenSpec SHALL treat workspace creation and detection as planning setup, not implementation.
|
||||
|
||||
#### Scenario: Creating or detecting a workspace
|
||||
- **WHEN** a workspace exists
|
||||
- **THEN** OpenSpec SHALL treat it as a place for workspace-level planning
|
||||
- **AND** repo implementation files SHALL remain unchanged until an explicit implementation workflow runs
|
||||
|
||||
#### Scenario: Deferring repo implementation
|
||||
- **WHEN** repo-local implementation, apply, verify, or archive behavior is needed
|
||||
- **THEN** that behavior SHALL require an explicit later workspace workflow
|
||||
|
||||
### Requirement: Repo Ownership Boundaries
|
||||
OpenSpec SHALL keep repo ownership legible when planning happens in a workspace.
|
||||
|
||||
#### Scenario: Planning across owned repos
|
||||
- **WHEN** a workspace plan refers to behavior owned by a repo or source area
|
||||
- **THEN** that owner SHALL remain the home for canonical specs and implementation work
|
||||
- **AND** the workspace SHALL make the cross-boundary plan visible without taking ownership away from that owner
|
||||
|
||||
#### Scenario: Drafting before ownership is clear
|
||||
- **WHEN** cross-repo behavior is still being explored and ownership is not clear
|
||||
- **THEN** the workspace MAY hold planning notes or draft behavior
|
||||
- **AND** those drafts SHALL remain distinguishable from canonical repo-owned specs
|
||||
Generated
+2
-13
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1803,7 +1803,6 @@
|
||||
"integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -1853,7 +1852,6 @@
|
||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
@@ -2184,7 +2182,6 @@
|
||||
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -2222,7 +2219,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2689,7 +2685,6 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4453,7 +4448,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4552,7 +4546,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4618,7 +4611,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4735,7 +4727,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4749,7 +4740,6 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -4929,7 +4919,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.3.1",
|
||||
"version": "1.1.1",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
+72
-120
@@ -1,13 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Postinstall script that hints about shell completions and CLI path visibility
|
||||
* Postinstall script for auto-installing shell completions
|
||||
*
|
||||
* Completion installation is opt-in: the user must run
|
||||
* `openspec completion install` explicitly. This script only
|
||||
* prints lightweight tips after npm install.
|
||||
*
|
||||
* The tips are suppressed when:
|
||||
* This script runs automatically after npm install unless:
|
||||
* - CI=true environment variable is set
|
||||
* - OPENSPEC_NO_COMPLETIONS=1 environment variable is set
|
||||
* - dist/ directory doesn't exist (dev setup scenario)
|
||||
@@ -15,121 +11,12 @@
|
||||
* The script never fails npm install - all errors are caught and handled gracefully.
|
||||
*/
|
||||
|
||||
import { constants as fsConstants, promises as fs } from 'fs';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const EXECUTABLE_NAMES = process.platform === 'win32'
|
||||
? ['openspec.cmd', 'openspec.ps1', 'openspec']
|
||||
: ['openspec'];
|
||||
|
||||
function getEnv(name) {
|
||||
return process.env[name] || process.env[name.toUpperCase()];
|
||||
}
|
||||
|
||||
function isTruthy(value) {
|
||||
return value ? ['1', 'true', 'yes'].includes(value.toLowerCase()) : false;
|
||||
}
|
||||
|
||||
function isLikelyGlobalInstall() {
|
||||
return isTruthy(getEnv('npm_config_global')) || getEnv('npm_config_location') === 'global';
|
||||
}
|
||||
|
||||
function normalizeForComparison(value) {
|
||||
const resolved = path.resolve(value);
|
||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
||||
}
|
||||
|
||||
function pathEntries() {
|
||||
return (process.env.PATH || '')
|
||||
.split(path.delimiter)
|
||||
.filter(Boolean)
|
||||
.map(normalizeForComparison);
|
||||
}
|
||||
|
||||
function isOnPath(dir) {
|
||||
const normalizedDir = normalizeForComparison(dir);
|
||||
return pathEntries().includes(normalizedDir);
|
||||
}
|
||||
|
||||
function addCandidateDir(dirs, dir) {
|
||||
if (!dir) return;
|
||||
dirs.set(normalizeForComparison(dir), dir);
|
||||
}
|
||||
|
||||
function getPrefixBinDir(prefix) {
|
||||
return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
|
||||
}
|
||||
|
||||
function getCandidateCliBinDirs() {
|
||||
const dirs = new Map();
|
||||
|
||||
addCandidateDir(dirs, getEnv('npm_config_global_bin_dir'));
|
||||
addCandidateDir(dirs, getEnv('npm_config_bin'));
|
||||
addCandidateDir(dirs, getEnv('PNPM_HOME'));
|
||||
|
||||
const npmPrefix = getEnv('npm_config_prefix');
|
||||
if (npmPrefix) {
|
||||
addCandidateDir(dirs, getPrefixBinDir(npmPrefix));
|
||||
}
|
||||
|
||||
const bunInstall = getEnv('BUN_INSTALL');
|
||||
if (bunInstall) {
|
||||
addCandidateDir(dirs, path.join(bunInstall, 'bin'));
|
||||
}
|
||||
|
||||
return [...dirs.values()];
|
||||
}
|
||||
|
||||
async function directoryHasOpenSpecBin(dir) {
|
||||
for (const executableName of EXECUTABLE_NAMES) {
|
||||
try {
|
||||
await fs.access(path.join(dir, executableName), fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
// Continue checking other executable names.
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getCliBinDirsMissingFromPath() {
|
||||
if (!isLikelyGlobalInstall()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const missingDirs = [];
|
||||
for (const dir of getCandidateCliBinDirs()) {
|
||||
if (isOnPath(dir)) continue;
|
||||
if (await directoryHasOpenSpecBin(dir)) {
|
||||
missingDirs.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
return missingDirs;
|
||||
}
|
||||
|
||||
function printPathVisibilityHint(missingDirs) {
|
||||
if (missingDirs.length === 0) return;
|
||||
|
||||
console.log('');
|
||||
console.log(
|
||||
'OpenSpec was installed, but this shell may not find the CLI because these bin directories are not on PATH:'
|
||||
);
|
||||
for (const dir of missingDirs) {
|
||||
console.log(` ${dir}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(
|
||||
'If `openspec --version` fails in an editor, agent, GUI app, or automation, add the relevant package-manager bin directory to the PATH used by that environment.'
|
||||
);
|
||||
console.log(
|
||||
'See: https://github.com/Fission-AI/OpenSpec/blob/main/docs/installation.md#troubleshooting-path-visibility'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should skip installation
|
||||
@@ -161,6 +48,65 @@ async function distExists() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the user's shell
|
||||
*/
|
||||
async function detectShell() {
|
||||
try {
|
||||
const { detectShell } = await import('../dist/utils/shell-detection.js');
|
||||
const result = detectShell();
|
||||
return result.shell;
|
||||
} catch (error) {
|
||||
// Fail silently if detection module doesn't exist
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install completions for the detected shell
|
||||
*/
|
||||
async function installCompletions(shell) {
|
||||
try {
|
||||
const { CompletionFactory } = await import('../dist/core/completions/factory.js');
|
||||
const { COMMAND_REGISTRY } = await import('../dist/core/completions/command-registry.js');
|
||||
|
||||
// Check if shell is supported
|
||||
if (!CompletionFactory.isSupported(shell)) {
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate completion script
|
||||
const generator = CompletionFactory.createGenerator(shell);
|
||||
const script = generator.generate(COMMAND_REGISTRY);
|
||||
|
||||
// Install completion script
|
||||
const installer = CompletionFactory.createInstaller(shell);
|
||||
const result = await installer.install(script);
|
||||
|
||||
if (result.success) {
|
||||
// Show success message based on installation type
|
||||
if (result.isOhMyZsh) {
|
||||
console.log(`✓ Shell completions installed`);
|
||||
console.log(` Restart shell: exec zsh`);
|
||||
} else if (result.zshrcConfigured) {
|
||||
console.log(`✓ Shell completions installed and configured`);
|
||||
console.log(` Restart shell: exec zsh`);
|
||||
} else {
|
||||
console.log(`✓ Shell completions installed to ~/.zsh/completions/`);
|
||||
console.log(` Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)`);
|
||||
console.log(` Then: exec zsh`);
|
||||
}
|
||||
} else {
|
||||
// Installation failed, show tip for manual install
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Fail gracefully - show tip for manual install
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
@@ -178,13 +124,19 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Completions are opt-in — just print a hint
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
// Detect shell
|
||||
const shell = await detectShell();
|
||||
if (!shell) {
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
return;
|
||||
}
|
||||
|
||||
const missingDirs = await getCliBinDirsMissingFromPath();
|
||||
printPathVisibilityHint(missingDirs);
|
||||
// Install completions
|
||||
await installCompletions(shell);
|
||||
} catch (error) {
|
||||
// Fail gracefully - never break npm install
|
||||
// Show tip for manual install
|
||||
console.log(`\nTip: Run 'openspec completion install' for shell completions`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for postinstall.js
|
||||
# Tests different scenarios: normal install, CI, opt-out, PATH hints
|
||||
# Tests different scenarios: normal install, CI, opt-out
|
||||
|
||||
set -e
|
||||
|
||||
@@ -13,49 +13,25 @@ echo ""
|
||||
# Save original environment
|
||||
ORIGINAL_CI="${CI:-}"
|
||||
ORIGINAL_OPENSPEC_NO_COMPLETIONS="${OPENSPEC_NO_COMPLETIONS:-}"
|
||||
ORIGINAL_NPM_CONFIG_GLOBAL="${npm_config_global:-}"
|
||||
ORIGINAL_NPM_CONFIG_PREFIX="${npm_config_prefix:-}"
|
||||
ORIGINAL_PATH="$PATH"
|
||||
NODE_BIN="$(command -v node)"
|
||||
|
||||
# Test 1: Normal install
|
||||
echo "Test 1: Normal install (should print tip about completions)"
|
||||
echo "Test 1: Normal install (should attempt to install completions)"
|
||||
echo "--------------------------------------"
|
||||
unset CI
|
||||
unset OPENSPEC_NO_COMPLETIONS
|
||||
node scripts/postinstall.js
|
||||
echo ""
|
||||
|
||||
# Test 2: Global install with CLI bin missing from PATH (should print PATH hint)
|
||||
echo "Test 2: Global install with CLI bin missing from PATH (should print PATH hint)"
|
||||
echo "--------------------------------------"
|
||||
TMP_PREFIX="$(mktemp -d)"
|
||||
TMP_HOME="$(mktemp -d)"
|
||||
mkdir -p "$TMP_PREFIX/bin"
|
||||
printf '#!/bin/sh\nexit 0\n' > "$TMP_PREFIX/bin/openspec"
|
||||
chmod +x "$TMP_PREFIX/bin/openspec"
|
||||
unset CI
|
||||
unset OPENSPEC_NO_COMPLETIONS
|
||||
export npm_config_global=true
|
||||
export npm_config_prefix="$TMP_PREFIX"
|
||||
HOME="$TMP_HOME" PNPM_HOME="$TMP_HOME/no-pnpm" PATH="/usr/bin:/bin" "$NODE_BIN" scripts/postinstall.js
|
||||
rm -rf "$TMP_PREFIX"
|
||||
rm -rf "$TMP_HOME"
|
||||
unset npm_config_global
|
||||
unset npm_config_prefix
|
||||
export PATH="$ORIGINAL_PATH"
|
||||
echo ""
|
||||
|
||||
# Test 3: CI environment (should skip silently)
|
||||
echo "Test 3: CI=true (should skip silently)"
|
||||
# Test 2: CI environment (should skip silently)
|
||||
echo "Test 2: CI=true (should skip silently)"
|
||||
echo "--------------------------------------"
|
||||
export CI=true
|
||||
node scripts/postinstall.js
|
||||
echo "[No output expected - skipped due to CI]"
|
||||
echo ""
|
||||
|
||||
# Test 4: Opt-out flag (should skip silently)
|
||||
echo "Test 4: OPENSPEC_NO_COMPLETIONS=1 (should skip silently)"
|
||||
# Test 3: Opt-out flag (should skip silently)
|
||||
echo "Test 3: OPENSPEC_NO_COMPLETIONS=1 (should skip silently)"
|
||||
echo "--------------------------------------"
|
||||
unset CI
|
||||
export OPENSPEC_NO_COMPLETIONS=1
|
||||
@@ -76,20 +52,6 @@ else
|
||||
unset OPENSPEC_NO_COMPLETIONS
|
||||
fi
|
||||
|
||||
if [ -n "$ORIGINAL_NPM_CONFIG_GLOBAL" ]; then
|
||||
export npm_config_global="$ORIGINAL_NPM_CONFIG_GLOBAL"
|
||||
else
|
||||
unset npm_config_global
|
||||
fi
|
||||
|
||||
if [ -n "$ORIGINAL_NPM_CONFIG_PREFIX" ]; then
|
||||
export npm_config_prefix="$ORIGINAL_NPM_CONFIG_PREFIX"
|
||||
else
|
||||
unset npm_config_prefix
|
||||
fi
|
||||
|
||||
export PATH="$ORIGINAL_PATH"
|
||||
|
||||
echo "======================================"
|
||||
echo "All tests completed successfully!"
|
||||
echo "======================================"
|
||||
|
||||
@@ -16,7 +16,6 @@ import { CompletionCommand } from '../commands/completion.js';
|
||||
import { FeedbackCommand } from '../commands/feedback.js';
|
||||
import { registerConfigCommand } from '../commands/config.js';
|
||||
import { registerSchemaCommand } from '../commands/schema.js';
|
||||
import { registerWorkspaceCommand } from '../commands/workspace.js';
|
||||
import {
|
||||
statusCommand,
|
||||
instructionsCommand,
|
||||
@@ -286,7 +285,6 @@ program
|
||||
registerSpecCommand(program);
|
||||
registerConfigCommand(program);
|
||||
registerSchemaCommand(program);
|
||||
registerWorkspaceCommand(program);
|
||||
|
||||
// Top-level validate command
|
||||
program
|
||||
|
||||
@@ -279,13 +279,6 @@ export class CompletionCommand {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'schemas': {
|
||||
const schemaNames = await this.completionProvider.getSchemaNames();
|
||||
for (const name of schemaNames) {
|
||||
console.log(`${name}\tschema`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'archived-changes': {
|
||||
const archivedIds = await getArchivedChangeIds();
|
||||
for (const id of archivedIds) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
loadChangeContext,
|
||||
generateInstructions,
|
||||
resolveSchema,
|
||||
resolveArtifactOutputs,
|
||||
type ArtifactInstructions,
|
||||
} from '../../core/artifact-graph/index.js';
|
||||
import {
|
||||
@@ -46,7 +45,7 @@ export async function instructionsCommand(
|
||||
artifactId: string | undefined,
|
||||
options: InstructionsOptions
|
||||
): Promise<void> {
|
||||
const spinner = options.json ? undefined : ora('Generating instructions...').start();
|
||||
const spinner = ora('Generating instructions...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
@@ -61,7 +60,7 @@ export async function instructionsCommand(
|
||||
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
||||
|
||||
if (!artifactId) {
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
||||
throw new Error(
|
||||
`Missing required argument <artifact>. Valid artifacts:\n ${validIds.join('\n ')}`
|
||||
@@ -71,7 +70,7 @@ export async function instructionsCommand(
|
||||
const artifact = context.graph.getArtifact(artifactId);
|
||||
|
||||
if (!artifact) {
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
const validIds = context.graph.getAllArtifacts().map((a) => a.id);
|
||||
throw new Error(
|
||||
`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`
|
||||
@@ -81,7 +80,7 @@ export async function instructionsCommand(
|
||||
const instructions = generateInstructions(context, artifactId, projectRoot);
|
||||
const isBlocked = instructions.dependencies.some((d) => !d.done);
|
||||
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(instructions, null, 2));
|
||||
@@ -90,7 +89,7 @@ export async function instructionsCommand(
|
||||
|
||||
printInstructionsText(instructions, isBlocked);
|
||||
} catch (error) {
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -238,6 +237,68 @@ function parseTasksFile(content: string): TaskItem[] {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an artifact output exists in the change directory.
|
||||
* Supports glob patterns (e.g., "specs/*.md") by verifying at least one matching file exists.
|
||||
*/
|
||||
function artifactOutputExists(changeDir: string, generates: string): boolean {
|
||||
// Normalize the generates path to use platform-specific separators
|
||||
const normalizedGenerates = generates.split('/').join(path.sep);
|
||||
const fullPath = path.join(changeDir, normalizedGenerates);
|
||||
|
||||
// If it's a glob pattern (contains ** or *), check for matching files
|
||||
if (generates.includes('*')) {
|
||||
// Extract the directory part before the glob pattern
|
||||
const parts = normalizedGenerates.split(path.sep);
|
||||
const dirParts: string[] = [];
|
||||
let patternPart = '';
|
||||
for (const part of parts) {
|
||||
if (part.includes('*')) {
|
||||
patternPart = part;
|
||||
break;
|
||||
}
|
||||
dirParts.push(part);
|
||||
}
|
||||
const dirPath = path.join(changeDir, ...dirParts);
|
||||
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract expected extension from pattern (e.g., "*.md" -> ".md")
|
||||
const extMatch = patternPart.match(/\*(\.[a-zA-Z0-9]+)$/);
|
||||
const expectedExt = extMatch ? extMatch[1] : null;
|
||||
|
||||
// Recursively check for matching files
|
||||
const hasMatchingFiles = (dir: string): boolean => {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
// For ** patterns, recurse into subdirectories
|
||||
if (generates.includes('**') && hasMatchingFiles(path.join(dir, entry.name))) {
|
||||
return true;
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
// Check if file matches expected extension (or any file if no extension specified)
|
||||
if (!expectedExt || entry.name.endsWith(expectedExt)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return hasMatchingFiles(dirPath);
|
||||
}
|
||||
|
||||
return fs.existsSync(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates apply instructions for implementing tasks from a change.
|
||||
* Schema-aware: reads apply phase configuration from schema to determine
|
||||
@@ -250,7 +311,7 @@ export async function generateApplyInstructions(
|
||||
): Promise<ApplyInstructions> {
|
||||
// loadChangeContext will auto-detect schema from metadata if not provided
|
||||
const context = loadChangeContext(projectRoot, changeName, schemaName);
|
||||
const changeDir = context.changeDir;
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
|
||||
// Get the full schema to access the apply phase configuration
|
||||
const schema = resolveSchema(context.schemaName, projectRoot);
|
||||
@@ -266,17 +327,16 @@ export async function generateApplyInstructions(
|
||||
const missingArtifacts: string[] = [];
|
||||
for (const artifactId of requiredArtifactIds) {
|
||||
const artifact = schema.artifacts.find((a) => a.id === artifactId);
|
||||
if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
|
||||
if (artifact && !artifactOutputExists(changeDir, artifact.generates)) {
|
||||
missingArtifacts.push(artifactId);
|
||||
}
|
||||
}
|
||||
|
||||
// Build context files from all existing artifacts in schema
|
||||
const contextFiles: Record<string, string[]> = {};
|
||||
const contextFiles: Record<string, string> = {};
|
||||
for (const artifact of schema.artifacts) {
|
||||
const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
|
||||
if (outputs.length > 0) {
|
||||
contextFiles[artifact.id] = outputs;
|
||||
if (artifactOutputExists(changeDir, artifact.generates)) {
|
||||
contextFiles[artifact.id] = path.join(changeDir, artifact.generates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +400,7 @@ export async function generateApplyInstructions(
|
||||
}
|
||||
|
||||
export async function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void> {
|
||||
const spinner = options.json ? undefined : ora('Generating apply instructions...').start();
|
||||
const spinner = ora('Generating apply instructions...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
@@ -354,7 +414,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions
|
||||
// generateApplyInstructions uses loadChangeContext which auto-detects schema
|
||||
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema);
|
||||
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(instructions, null, 2));
|
||||
@@ -363,7 +423,7 @@ export async function applyInstructionsCommand(options: ApplyInstructionsOptions
|
||||
|
||||
printApplyInstructionsText(instructions);
|
||||
} catch (error) {
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -388,10 +448,8 @@ export function printApplyInstructionsText(instructions: ApplyInstructions): voi
|
||||
const contextFileEntries = Object.entries(contextFiles);
|
||||
if (contextFileEntries.length > 0) {
|
||||
console.log('### Context Files');
|
||||
for (const [artifactId, filePaths] of contextFileEntries) {
|
||||
for (const filePath of filePaths) {
|
||||
console.log(`- ${artifactId}: ${filePath}`);
|
||||
}
|
||||
for (const [artifactId, filePath] of contextFileEntries) {
|
||||
console.log(`- ${artifactId}: ${filePath}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface ApplyInstructions {
|
||||
changeName: string;
|
||||
changeDir: string;
|
||||
schemaName: string;
|
||||
contextFiles: Record<string, string[]>;
|
||||
contextFiles: Record<string, string>;
|
||||
progress: {
|
||||
total: number;
|
||||
complete: number;
|
||||
@@ -86,23 +86,6 @@ export function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of available change directory names under openspec/changes/.
|
||||
* Excludes the archive directory and hidden directories.
|
||||
*/
|
||||
export async function getAvailableChanges(projectRoot: string): Promise<string[]> {
|
||||
const changesPath = path.join(projectRoot, 'openspec', 'changes');
|
||||
try {
|
||||
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
|
||||
.map((e) => e.name);
|
||||
} catch (error: unknown) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a change exists and returns available changes if not.
|
||||
* Checks directory existence directly to support scaffolded changes (without proposal.md).
|
||||
@@ -111,8 +94,22 @@ export async function validateChangeExists(
|
||||
changeName: string | undefined,
|
||||
projectRoot: string
|
||||
): Promise<string> {
|
||||
const changesPath = path.join(projectRoot, 'openspec', 'changes');
|
||||
|
||||
// Get all change directories (not just those with proposal.md)
|
||||
const getAvailableChanges = async (): Promise<string[]> => {
|
||||
try {
|
||||
const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
|
||||
.map((e) => e.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
if (!changeName) {
|
||||
const available = await getAvailableChanges(projectRoot);
|
||||
const available = await getAvailableChanges();
|
||||
if (available.length === 0) {
|
||||
throw new Error('No changes found. Create one with: openspec new change <name>');
|
||||
}
|
||||
@@ -128,11 +125,11 @@ export async function validateChangeExists(
|
||||
}
|
||||
|
||||
// Check directory existence directly
|
||||
const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
const changePath = path.join(changesPath, changeName);
|
||||
const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory();
|
||||
|
||||
if (!exists) {
|
||||
const available = await getAvailableChanges(projectRoot);
|
||||
const available = await getAvailableChanges();
|
||||
if (available.length === 0) {
|
||||
throw new Error(
|
||||
`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import {
|
||||
validateChangeExists,
|
||||
validateSchemaExists,
|
||||
getAvailableChanges,
|
||||
getStatusIndicator,
|
||||
getStatusColor,
|
||||
} from './shared.js';
|
||||
@@ -34,31 +33,10 @@ export interface StatusOptions {
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function statusCommand(options: StatusOptions): Promise<void> {
|
||||
const spinner = options.json ? undefined : ora('Loading change status...').start();
|
||||
const spinner = ora('Loading change status...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
// Handle no-changes case gracefully — status is informational,
|
||||
// so "no changes" is a valid state, not an error.
|
||||
if (!options.change) {
|
||||
const available = await getAvailableChanges(projectRoot);
|
||||
if (available.length === 0) {
|
||||
spinner?.stop();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log('No active changes. Create one with: openspec new change <name>');
|
||||
return;
|
||||
}
|
||||
// Changes exist but --change not provided
|
||||
spinner?.stop();
|
||||
throw new Error(
|
||||
`Missing required option --change. Available changes:\n ${available.join('\n ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const changeName = await validateChangeExists(options.change, projectRoot);
|
||||
|
||||
// Validate schema if explicitly provided
|
||||
@@ -70,7 +48,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
|
||||
const context = loadChangeContext(projectRoot, changeName, options.schema);
|
||||
const status = formatChangeStatus(context);
|
||||
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(status, null, 2));
|
||||
@@ -79,7 +57,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
|
||||
|
||||
printStatusText(status);
|
||||
} catch (error) {
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
getSchemaDir,
|
||||
ArtifactGraph,
|
||||
} from '../../core/artifact-graph/index.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import { validateSchemaExists, DEFAULT_SCHEMA } from './shared.js';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -34,7 +33,7 @@ export interface TemplateInfo {
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function templatesCommand(options: TemplatesOptions): Promise<void> {
|
||||
const spinner = options.json ? undefined : ora('Loading templates...').start();
|
||||
const spinner = ora('Loading templates...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
@@ -69,13 +68,11 @@ export async function templatesCommand(options: TemplatesOptions): Promise<void>
|
||||
|
||||
const templates: TemplateInfo[] = graph.getAllArtifacts().map((artifact) => ({
|
||||
artifactId: artifact.id,
|
||||
templatePath: FileSystemUtils.canonicalizeExistingPath(
|
||||
path.join(schemaDir, 'templates', artifact.template)
|
||||
),
|
||||
templatePath: path.join(schemaDir, 'templates', artifact.template),
|
||||
source,
|
||||
}));
|
||||
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
const output: Record<string, { path: string; source: string }> = {};
|
||||
@@ -95,7 +92,7 @@ export async function templatesCommand(options: TemplatesOptions): Promise<void>
|
||||
console.log(` ${t.templatePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
spinner?.stop();
|
||||
spinner.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,622 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import * as nodeFs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { listWorkspaceRegistryEntries } from '../core/workspace/index.js';
|
||||
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
||||
import {
|
||||
addWorkspaceLink,
|
||||
createManagedWorkspace,
|
||||
inferLinkName,
|
||||
loadWorkspaceForDoctor,
|
||||
loadWorkspaceForList,
|
||||
parseSetupLinks,
|
||||
readRegistry,
|
||||
resolveExistingDirectory,
|
||||
updateWorkspaceLink,
|
||||
validateLinkNameForCommand,
|
||||
validateWorkspaceNameForSetup,
|
||||
} from './workspace/operations.js';
|
||||
import { selectWorkspaceForCommand } from './workspace/selection.js';
|
||||
import {
|
||||
WorkspaceCliError,
|
||||
WorkspaceLinkMutationPayload,
|
||||
WorkspaceListOutput,
|
||||
WorkspaceLinkOptions,
|
||||
WorkspaceListOptions,
|
||||
WorkspaceOutput,
|
||||
WorkspaceSetupOptions,
|
||||
WorkspaceStatus,
|
||||
appendStatus,
|
||||
asErrorMessage,
|
||||
asStatus,
|
||||
} from './workspace/types.js';
|
||||
|
||||
function printJson(payload: unknown): void {
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
}
|
||||
|
||||
const workspacePromptTheme = {
|
||||
prefix: '',
|
||||
style: {
|
||||
answer: (text: string) => chalk.cyan(text),
|
||||
defaultAnswer: (text: string) => chalk.dim(text),
|
||||
error: (text: string) => chalk.red(text),
|
||||
help: (text: string) => chalk.dim(text),
|
||||
highlight: (text: string) => chalk.cyan(text),
|
||||
key: (text: string) => chalk.cyan(text),
|
||||
message: (text: string) => chalk.bold(text),
|
||||
},
|
||||
};
|
||||
|
||||
const workspaceSelectTheme = {
|
||||
...workspacePromptTheme,
|
||||
icon: {
|
||||
cursor: chalk.cyan('>'),
|
||||
},
|
||||
style: {
|
||||
...workspacePromptTheme.style,
|
||||
keysHelpTip: (keys: [key: string, action: string][]) =>
|
||||
chalk.dim(keys.map(([key, action]) => `${key}: ${action}`).join(' | ')),
|
||||
},
|
||||
};
|
||||
|
||||
function printWorkspaceSetupIntro(): void {
|
||||
console.log(chalk.bold('Workspace setup'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function isPromptCancellationError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
(error.name === 'ExitPromptError' || error.message.includes('force closed the prompt with SIGINT'))
|
||||
);
|
||||
}
|
||||
|
||||
async function promptWorkspaceName(initialName?: string): Promise<string> {
|
||||
if (initialName) {
|
||||
return validateWorkspaceNameForSetup(initialName);
|
||||
}
|
||||
|
||||
const { input } = await import('@inquirer/prompts');
|
||||
|
||||
console.log(chalk.bold('[1/3] Name the workspace'));
|
||||
console.log(chalk.dim('Use a stable name for the repo group, e.g. platform.'));
|
||||
console.log('');
|
||||
|
||||
return input({
|
||||
message: 'Workspace name:',
|
||||
required: true,
|
||||
theme: workspacePromptTheme,
|
||||
validate(value: string) {
|
||||
try {
|
||||
validateWorkspaceNameForSetup(value);
|
||||
return true;
|
||||
} catch {
|
||||
return 'Workspace names must be kebab-case with lowercase letters, numbers, and single hyphen separators.';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function promptExistingPath(message: string, defaultPath?: string): Promise<string> {
|
||||
const { input } = await import('@inquirer/prompts');
|
||||
|
||||
const pathInput = await input({
|
||||
message,
|
||||
default: defaultPath,
|
||||
prefill: defaultPath ? 'editable' : undefined,
|
||||
required: true,
|
||||
theme: workspacePromptTheme,
|
||||
validate(value: string) {
|
||||
const resolvedPath = path.isAbsolute(value)
|
||||
? path.resolve(value)
|
||||
: path.resolve(process.cwd(), value);
|
||||
return nodeFs.existsSync(resolvedPath) && nodeFs.statSync(resolvedPath).isDirectory()
|
||||
? true
|
||||
: 'Enter an existing repo or folder path.';
|
||||
},
|
||||
});
|
||||
|
||||
return resolveExistingDirectory(pathInput);
|
||||
}
|
||||
|
||||
async function promptLinkName(existingLinks: Record<string, string>): Promise<string> {
|
||||
const { input } = await import('@inquirer/prompts');
|
||||
|
||||
return input({
|
||||
message: 'Link name:',
|
||||
required: true,
|
||||
theme: workspacePromptTheme,
|
||||
validate(value: string) {
|
||||
try {
|
||||
validateLinkNameForCommand(value);
|
||||
} catch (error) {
|
||||
return asErrorMessage(error);
|
||||
}
|
||||
|
||||
if (existingLinks[value]) {
|
||||
return `Link name '${value}' is already linked to ${existingLinks[value]}.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function promptSetupLinks(): Promise<Record<string, string>> {
|
||||
const { select } = await import('@inquirer/prompts');
|
||||
const links: Record<string, string> = {};
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.bold('[2/3] Link repos or folders'));
|
||||
console.log(chalk.dim('Start with the current directory, or enter another repo path.'));
|
||||
console.log('');
|
||||
|
||||
while (true) {
|
||||
const linkCount = Object.keys(links).length;
|
||||
const resolvedPath = await promptExistingPath(
|
||||
linkCount === 0 ? 'Repo or folder path:' : 'Another repo or folder path:',
|
||||
linkCount === 0 ? '.' : undefined
|
||||
);
|
||||
let linkName = inferLinkName(resolvedPath);
|
||||
|
||||
try {
|
||||
validateLinkNameForCommand(linkName);
|
||||
} catch {
|
||||
linkName = await promptLinkName(links);
|
||||
}
|
||||
|
||||
if (links[linkName]) {
|
||||
console.log(`Link name '${linkName}' is already linked to ${links[linkName]}.`);
|
||||
linkName = await promptLinkName(links);
|
||||
}
|
||||
|
||||
links[linkName] = resolvedPath;
|
||||
console.log(chalk.green(`Added link '${linkName}'`));
|
||||
console.log(chalk.dim(` ${resolvedPath}`));
|
||||
|
||||
const nextAction = await select({
|
||||
message: 'Continue',
|
||||
default: 'finish',
|
||||
choices: [
|
||||
{
|
||||
name: 'Create workspace files',
|
||||
short: 'Create workspace files',
|
||||
value: 'finish',
|
||||
description: 'Run a workspace check after setup',
|
||||
},
|
||||
{
|
||||
name: 'Add another repo or folder',
|
||||
short: 'Add another',
|
||||
value: 'add',
|
||||
description: 'Include another local directory in this workspace',
|
||||
},
|
||||
],
|
||||
theme: workspaceSelectTheme,
|
||||
});
|
||||
|
||||
if (nextAction === 'finish') {
|
||||
return links;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printStatusLines(statuses: WorkspaceStatus[]): void {
|
||||
for (const status of statuses) {
|
||||
const label = status.severity === 'warning' ? 'Warning' : 'Issue';
|
||||
console.log(`${label}: ${status.message}`);
|
||||
if (status.fix) {
|
||||
console.log(`Fix: ${status.fix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printLinksHuman(links: WorkspaceOutput['links']): void {
|
||||
if (links.length === 0) {
|
||||
console.log(' (no linked repos or folders)');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
const suffix = link.status.some((status) => status.severity === 'error') ? ' [issue]' : '';
|
||||
console.log(` ${link.name} -> ${link.path ?? '(no local path recorded)'}${suffix}`);
|
||||
if (link.repo_specs_path) {
|
||||
console.log(` repo specs: ${link.repo_specs_path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectWorkspaceIssues(workspace: WorkspaceListOutput): WorkspaceStatus[] {
|
||||
return [
|
||||
...workspace.status,
|
||||
...workspace.links.flatMap((link) => link.status),
|
||||
];
|
||||
}
|
||||
|
||||
function printDoctorHuman(result: { workspace: WorkspaceOutput; status: WorkspaceStatus[] }): void {
|
||||
console.log(`Workspace: ${result.workspace.name}`);
|
||||
console.log(`Location: ${result.workspace.root}`);
|
||||
console.log(`Planning path: ${result.workspace.planning_path}`);
|
||||
console.log('');
|
||||
printStatusLines(result.status);
|
||||
if (result.status.length > 0) {
|
||||
console.log('');
|
||||
}
|
||||
console.log('Linked repos or folders:');
|
||||
printLinksHuman(result.workspace.links);
|
||||
|
||||
const issues = collectWorkspaceIssues(result.workspace);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log('');
|
||||
console.log('No workspace issues found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('Issues:');
|
||||
for (const issue of issues) {
|
||||
console.log(` - ${issue.message}`);
|
||||
if (issue.target) {
|
||||
console.log(` Target: ${issue.target}`);
|
||||
}
|
||||
if (issue.fix) {
|
||||
console.log(` Fix: ${issue.fix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printWorkspaceListHuman(workspaces: WorkspaceListOutput[]): void {
|
||||
console.log(chalk.bold(`OpenSpec workspaces (${workspaces.length})`));
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
console.log('');
|
||||
console.log(chalk.bold(workspace.name));
|
||||
console.log(` Location: ${workspace.root}`);
|
||||
|
||||
if (workspace.status.length > 0) {
|
||||
console.log(' Status:');
|
||||
for (const status of workspace.status) {
|
||||
const statusLabel = status.severity === 'warning' ? chalk.yellow('Warning') : chalk.red('Issue');
|
||||
console.log(` ${statusLabel}: ${status.message}`);
|
||||
if (status.fix) {
|
||||
console.log(` Fix: ${status.fix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` Linked repos or folders (${workspace.links.length}):`);
|
||||
if (workspace.links.length === 0) {
|
||||
console.log(chalk.dim(' (none)'));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const link of workspace.links) {
|
||||
const suffix = link.status.some((status) => status.severity === 'error') ? chalk.red(' [issue]') : '';
|
||||
console.log(` ${link.name} -> ${link.path ?? '(no local path recorded)'}${suffix}`);
|
||||
if (link.repo_specs_path) {
|
||||
console.log(chalk.dim(` repo specs: ${link.repo_specs_path}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printWorkspaceCheckSummaryHuman(result: { workspace: WorkspaceOutput; status: WorkspaceStatus[] }): void {
|
||||
printStatusLines(result.status);
|
||||
const issues = collectWorkspaceIssues(result.workspace);
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log(' No workspace issues found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' Issues:');
|
||||
for (const issue of issues) {
|
||||
console.log(` - ${issue.message}`);
|
||||
if (issue.target) {
|
||||
console.log(` Target: ${issue.target}`);
|
||||
}
|
||||
if (issue.fix) {
|
||||
console.log(` Fix: ${issue.fix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printLinkMutationHuman(
|
||||
heading: string,
|
||||
payload: WorkspaceLinkMutationPayload
|
||||
): void {
|
||||
printStatusLines(payload.status);
|
||||
console.log(heading);
|
||||
console.log(` ${payload.link.name} -> ${payload.link.path}`);
|
||||
console.log(`Workspace: ${payload.workspace.name}`);
|
||||
}
|
||||
|
||||
class WorkspaceCommand {
|
||||
async setup(options: WorkspaceSetupOptions = {}): Promise<void> {
|
||||
try {
|
||||
const noInteractive = resolveNoInteractive(options);
|
||||
|
||||
if (options.json && !noInteractive) {
|
||||
throw new WorkspaceCliError(
|
||||
'workspace setup --json requires --no-interactive.',
|
||||
'setup_json_requires_no_interactive',
|
||||
{
|
||||
fix: 'openspec workspace setup --no-interactive --json --name <name> --link <path>',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const interactive = !noInteractive && isInteractive(options);
|
||||
if (interactive) {
|
||||
printWorkspaceSetupIntro();
|
||||
}
|
||||
|
||||
if (!interactive && (!options.name || (options.link ?? []).length === 0)) {
|
||||
throw new WorkspaceCliError(
|
||||
'workspace setup --no-interactive requires --name <name> and at least one --link <path>.',
|
||||
'missing_setup_inputs',
|
||||
{
|
||||
fix: 'openspec workspace setup --no-interactive --name platform --link /path/to/repo',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceName = interactive
|
||||
? await promptWorkspaceName(options.name)
|
||||
: validateWorkspaceNameForSetup(options.name ?? '');
|
||||
const links = interactive ? await promptSetupLinks() : await parseSetupLinks(options.link);
|
||||
|
||||
if (Object.keys(links).length === 0) {
|
||||
throw new WorkspaceCliError(
|
||||
'workspace setup --no-interactive requires --name <name> and at least one --link <path>.',
|
||||
'missing_setup_inputs',
|
||||
{
|
||||
fix: 'openspec workspace setup --no-interactive --name platform --link /path/to/repo',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
console.log('');
|
||||
console.log(chalk.bold('[3/3] Create workspace files'));
|
||||
}
|
||||
|
||||
const workspace = await createManagedWorkspace(workspaceName, links);
|
||||
const doctorResult = await loadWorkspaceForDoctor({
|
||||
name: workspace.name,
|
||||
root: workspace.root,
|
||||
status: [],
|
||||
unregisteredCurrentWorkspace: false,
|
||||
});
|
||||
|
||||
if (options.json) {
|
||||
printJson({
|
||||
workspace: doctorResult.workspace,
|
||||
status: doctorResult.status,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.green('Workspace setup complete'));
|
||||
console.log('');
|
||||
printWorkspaceListHuman([doctorResult.workspace]);
|
||||
console.log('');
|
||||
console.log(`Planning path: ${doctorResult.workspace.planning_path}`);
|
||||
console.log('');
|
||||
console.log('Workspace check:');
|
||||
printWorkspaceCheckSummaryHuman(doctorResult);
|
||||
console.log('');
|
||||
console.log('Next useful commands:');
|
||||
console.log(` openspec workspace doctor --workspace ${workspace.name}`);
|
||||
console.log(' openspec workspace list');
|
||||
} catch (error) {
|
||||
this.handleFailure(options.json, { workspace: null, status: [] }, error);
|
||||
}
|
||||
}
|
||||
|
||||
async list(options: WorkspaceListOptions = {}): Promise<void> {
|
||||
try {
|
||||
const registry = await readRegistry();
|
||||
const entries = listWorkspaceRegistryEntries(registry);
|
||||
const workspaces = await Promise.all(entries.map((entry) => loadWorkspaceForList(entry)));
|
||||
const payload = { workspaces, status: [] as WorkspaceStatus[] };
|
||||
|
||||
if (options.json) {
|
||||
printJson(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
console.log("No OpenSpec workspaces found. Run 'openspec workspace setup' first.");
|
||||
return;
|
||||
}
|
||||
|
||||
printWorkspaceListHuman(workspaces);
|
||||
} catch (error) {
|
||||
this.handleFailure(options.json, { workspaces: [], status: [] }, error);
|
||||
}
|
||||
}
|
||||
|
||||
async link(
|
||||
nameOrPath: string | undefined,
|
||||
linkPath: string | undefined,
|
||||
options: WorkspaceLinkOptions = {}
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!nameOrPath) {
|
||||
throw new WorkspaceCliError(
|
||||
'workspace link requires a repo or folder path.',
|
||||
'missing_link_path',
|
||||
{
|
||||
fix: 'openspec workspace link /path/to/repo',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const selected = await selectWorkspaceForCommand(options, 'link');
|
||||
const payload = await addWorkspaceLink(selected, nameOrPath, linkPath);
|
||||
|
||||
if (options.json) {
|
||||
printJson(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
printLinkMutationHuman('Linked repo or folder:', payload);
|
||||
} catch (error) {
|
||||
this.handleFailure(options.json, { workspace: null, link: null, status: [] }, error);
|
||||
}
|
||||
}
|
||||
|
||||
async relink(
|
||||
linkNameInput: string | undefined,
|
||||
linkPath: string | undefined,
|
||||
options: WorkspaceLinkOptions = {}
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!linkNameInput || !linkPath) {
|
||||
throw new WorkspaceCliError(
|
||||
'workspace relink requires a link name and repo or folder path.',
|
||||
'missing_relink_arguments',
|
||||
{
|
||||
fix: 'openspec workspace relink <name> /path/to/repo',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const selected = await selectWorkspaceForCommand(options, 'relink');
|
||||
const payload = await updateWorkspaceLink(selected, linkNameInput, linkPath);
|
||||
|
||||
if (options.json) {
|
||||
printJson(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
printLinkMutationHuman('Relinked repo or folder:', payload);
|
||||
} catch (error) {
|
||||
this.handleFailure(options.json, { workspace: null, link: null, status: [] }, error);
|
||||
}
|
||||
}
|
||||
|
||||
async doctor(options: WorkspaceLinkOptions = {}): Promise<void> {
|
||||
try {
|
||||
const selected = await selectWorkspaceForCommand(options, 'doctor');
|
||||
const result = await loadWorkspaceForDoctor(selected);
|
||||
|
||||
if (options.json) {
|
||||
printJson(result);
|
||||
return;
|
||||
}
|
||||
|
||||
printDoctorHuman(result);
|
||||
} catch (error) {
|
||||
this.handleFailure(options.json, { workspace: null, status: [] }, error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleFailure<T extends { status: WorkspaceStatus[] }>(
|
||||
json: boolean | undefined,
|
||||
payload: T,
|
||||
error: unknown
|
||||
): void {
|
||||
if (!json && isPromptCancellationError(error)) {
|
||||
console.error('Cancelled.');
|
||||
process.exitCode = 130;
|
||||
return;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
printJson(appendStatus(payload, asStatus(error)));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const status = asStatus(error);
|
||||
console.error(`Error: ${status.message}`);
|
||||
if (status.fix) {
|
||||
console.error(`Fix: ${status.fix}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function collectOption(value: string, previous: string[]): string[] {
|
||||
return [...previous, value];
|
||||
}
|
||||
|
||||
function addWorkspaceSelectionOptions(command: Command): Command {
|
||||
return command
|
||||
.option('--workspace <name>', 'Workspace name from the local workspace registry')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--no-interactive', 'Disable prompts');
|
||||
}
|
||||
|
||||
export function registerWorkspaceCommand(program: Command): void {
|
||||
const workspaceCommand = new WorkspaceCommand();
|
||||
const workspace = program
|
||||
.command('workspace')
|
||||
.description('Set up and inspect coordination workspaces');
|
||||
|
||||
workspace
|
||||
.command('setup')
|
||||
.description('Set up a workspace and link existing repos or folders')
|
||||
.option('--name <name>', 'Workspace name')
|
||||
.option('--link <link>', 'Repo or folder link. Use <path> or <name>=<path>.', collectOption, [])
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--no-interactive', 'Disable prompts')
|
||||
.action(async (options: WorkspaceSetupOptions) => {
|
||||
await workspaceCommand.setup(options);
|
||||
});
|
||||
|
||||
workspace
|
||||
.command('list')
|
||||
.description('List known OpenSpec workspaces')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options: WorkspaceListOptions) => {
|
||||
await workspaceCommand.list(options);
|
||||
});
|
||||
|
||||
workspace
|
||||
.command('ls')
|
||||
.description('List known OpenSpec workspaces')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options: WorkspaceListOptions) => {
|
||||
await workspaceCommand.list(options);
|
||||
});
|
||||
|
||||
addWorkspaceSelectionOptions(
|
||||
workspace
|
||||
.command('link [nameOrPath] [path]')
|
||||
.description('Link an existing repo or folder to a workspace')
|
||||
).action(async (
|
||||
nameOrPath: string | undefined,
|
||||
linkPath: string | undefined,
|
||||
options: WorkspaceLinkOptions
|
||||
) => {
|
||||
await workspaceCommand.link(nameOrPath, linkPath, options);
|
||||
});
|
||||
|
||||
addWorkspaceSelectionOptions(
|
||||
workspace
|
||||
.command('relink <name> <path>')
|
||||
.description('Update the local path for an existing workspace link')
|
||||
).action(async (
|
||||
linkName: string | undefined,
|
||||
linkPath: string | undefined,
|
||||
options: WorkspaceLinkOptions
|
||||
) => {
|
||||
await workspaceCommand.relink(linkName, linkPath, options);
|
||||
});
|
||||
|
||||
addWorkspaceSelectionOptions(
|
||||
workspace
|
||||
.command('doctor')
|
||||
.description('Check what a workspace can resolve on this machine')
|
||||
).action(async (options: WorkspaceLinkOptions) => {
|
||||
await workspaceCommand.doctor(options);
|
||||
});
|
||||
|
||||
// Intentionally no public `workspace create` command in this slice.
|
||||
}
|
||||
@@ -1,703 +0,0 @@
|
||||
import * as nodeFs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import {
|
||||
WorkspaceLocalState,
|
||||
WorkspaceRegistryEntry,
|
||||
WorkspaceRegistryState,
|
||||
WorkspaceSharedState,
|
||||
getManagedWorkspaceRoot,
|
||||
getWorkspaceChangesDir,
|
||||
getWorkspacePortableIgnorePatterns,
|
||||
isWorkspaceRoot,
|
||||
parseWorkspaceSetupLinkInput,
|
||||
readOptionalWorkspaceLocalState,
|
||||
readWorkspaceRegistryState,
|
||||
readWorkspaceSharedState,
|
||||
validateWorkspaceLinkName,
|
||||
validateWorkspaceName,
|
||||
writeWorkspaceLocalState,
|
||||
writeWorkspaceRegistryState,
|
||||
writeWorkspaceSharedState,
|
||||
} from '../../core/workspace/index.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import {
|
||||
SelectedWorkspace,
|
||||
WorkspaceCliError,
|
||||
WorkspaceLinkMutationPayload,
|
||||
WorkspaceLinkOutput,
|
||||
WorkspaceListOutput,
|
||||
WorkspaceOutput,
|
||||
WorkspaceStatus,
|
||||
asErrorMessage,
|
||||
makeStatus,
|
||||
} from './types.js';
|
||||
|
||||
const fs = nodeFs.promises;
|
||||
|
||||
function emptyRegistry(): WorkspaceRegistryState {
|
||||
return { version: 1, workspaces: {} };
|
||||
}
|
||||
|
||||
function emptyLocalState(): WorkspaceLocalState {
|
||||
return { version: 1, paths: {} };
|
||||
}
|
||||
|
||||
export async function readRegistry(): Promise<WorkspaceRegistryState> {
|
||||
return (await readWorkspaceRegistryState()) ?? emptyRegistry();
|
||||
}
|
||||
|
||||
async function recordWorkspaceInRegistry(name: string, workspaceRoot: string): Promise<void> {
|
||||
const registry = await readRegistry();
|
||||
const recordedWorkspaceRoot = normalizeExistingPathForStorage(workspaceRoot);
|
||||
|
||||
await writeWorkspaceRegistryState({
|
||||
version: 1,
|
||||
workspaces: {
|
||||
...registry.workspaces,
|
||||
[name]: recordedWorkspaceRoot,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function directoryExists(dirPath: string): Promise<boolean> {
|
||||
try {
|
||||
return (await fs.stat(dirPath)).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
return (await fs.stat(filePath)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExistingPathForStorage(existingPath: string): string {
|
||||
return process.platform === 'win32'
|
||||
? FileSystemUtils.canonicalizeExistingPath(existingPath)
|
||||
: existingPath;
|
||||
}
|
||||
|
||||
export async function resolveExistingDirectory(
|
||||
inputPath: string,
|
||||
cwd = process.cwd()
|
||||
): Promise<string> {
|
||||
if (inputPath.length === 0) {
|
||||
throw new WorkspaceCliError('Repo or folder path must not be empty.', 'linked_path_empty', {
|
||||
target: 'link.path',
|
||||
fix: 'Choose an existing repo or folder path.',
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedPath = path.isAbsolute(inputPath)
|
||||
? path.resolve(inputPath)
|
||||
: path.resolve(cwd, inputPath);
|
||||
|
||||
if (!(await directoryExists(resolvedPath))) {
|
||||
throw new WorkspaceCliError(
|
||||
`Path '${inputPath}' is not an existing folder.`,
|
||||
'linked_path_missing',
|
||||
{
|
||||
target: 'link.path',
|
||||
fix: 'Choose an existing repo or folder path.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return normalizeExistingPathForStorage(resolvedPath);
|
||||
}
|
||||
|
||||
export function inferLinkName(absolutePath: string): string {
|
||||
return path.basename(absolutePath);
|
||||
}
|
||||
|
||||
function normalizeLinksForOutput(
|
||||
sharedState: WorkspaceSharedState,
|
||||
localState: WorkspaceLocalState | null
|
||||
): WorkspaceLinkOutput[] {
|
||||
return Object.keys(sharedState.links)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((name) => ({
|
||||
name,
|
||||
path: localState?.paths[name] ?? null,
|
||||
status: [],
|
||||
}));
|
||||
}
|
||||
|
||||
function formatDuplicateLinkMessage(
|
||||
linkName: string,
|
||||
existingPath: string | null,
|
||||
replacementPath: string
|
||||
): string {
|
||||
return [
|
||||
`Cannot use link name '${linkName}' because another link already uses that name.`,
|
||||
'Existing link:',
|
||||
` ${linkName} -> ${existingPath ?? '(no local path recorded)'}`,
|
||||
'',
|
||||
'Choose a different link name:',
|
||||
` openspec workspace link archived-${linkName} ${replacementPath}`,
|
||||
'',
|
||||
'If you meant to change the existing link path:',
|
||||
` openspec workspace relink ${linkName} ${replacementPath}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function duplicateLinkError(
|
||||
linkName: string,
|
||||
existingPath: string | null,
|
||||
replacementPath: string
|
||||
): WorkspaceCliError {
|
||||
return new WorkspaceCliError(
|
||||
formatDuplicateLinkMessage(linkName, existingPath, replacementPath),
|
||||
'duplicate_link_name',
|
||||
{
|
||||
target: `links.${linkName}`,
|
||||
fix: `Choose a different link name or run 'openspec workspace relink ${linkName} ${replacementPath}'.`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function duplicateSetupLinkError(
|
||||
linkName: string,
|
||||
existingPath: string,
|
||||
replacementPath: string
|
||||
): WorkspaceCliError {
|
||||
return new WorkspaceCliError(
|
||||
[
|
||||
`Cannot use link name '${linkName}' because another setup link already uses that name.`,
|
||||
'Existing link:',
|
||||
` ${linkName} -> ${existingPath}`,
|
||||
'',
|
||||
'Use explicit --link <name>=<path> values with different names.',
|
||||
].join('\n'),
|
||||
'duplicate_link_name',
|
||||
{
|
||||
target: `links.${linkName}`,
|
||||
fix: `Use explicit --link ${linkName}-alt=${replacementPath} with a different link name.`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function validateWorkspaceNameForSetup(name: string): string {
|
||||
try {
|
||||
return validateWorkspaceName(name);
|
||||
} catch {
|
||||
throw new WorkspaceCliError(
|
||||
'Workspace name must be kebab-case with lowercase letters, numbers, and single hyphen separators.',
|
||||
'invalid_workspace_name',
|
||||
{
|
||||
target: 'workspace.name',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateLinkNameForCommand(name: string): string {
|
||||
try {
|
||||
return validateWorkspaceLinkName(name);
|
||||
} catch (error) {
|
||||
throw new WorkspaceCliError(asErrorMessage(error), 'invalid_link_name', {
|
||||
target: 'link.name',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function localStateInvalidStatus(error: unknown): WorkspaceStatus {
|
||||
return makeStatus(
|
||||
'error',
|
||||
'workspace_local_state_invalid',
|
||||
`Machine-local paths could not be read: ${asErrorMessage(error)}`,
|
||||
{
|
||||
target: 'workspace.local_state',
|
||||
fix: 'Repair or remove .openspec-workspace/local.yaml, then run openspec workspace relink <name> <path> for affected links.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function readLocalStateForMutation(workspaceRoot: string): Promise<WorkspaceLocalState> {
|
||||
try {
|
||||
return (await readOptionalWorkspaceLocalState(workspaceRoot)) ?? emptyLocalState();
|
||||
} catch (error) {
|
||||
const status = localStateInvalidStatus(error);
|
||||
throw new WorkspaceCliError(status.message, status.code, {
|
||||
target: status.target,
|
||||
fix: status.fix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWorkspaceGitignore(workspaceRoot: string): Promise<void> {
|
||||
const gitignorePath = path.join(workspaceRoot, '.gitignore');
|
||||
const patterns = getWorkspacePortableIgnorePatterns();
|
||||
const existingContent = (await fileExists(gitignorePath))
|
||||
? await fs.readFile(gitignorePath, 'utf-8')
|
||||
: '';
|
||||
const existingLines = new Set(
|
||||
existingContent
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
);
|
||||
const missingPatterns = patterns.filter((pattern) => !existingLines.has(pattern));
|
||||
|
||||
if (missingPatterns.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = existingContent.length > 0 && !existingContent.endsWith('\n') ? '\n' : '';
|
||||
const content = `${existingContent}${prefix}${missingPatterns.join('\n')}\n`;
|
||||
await fs.writeFile(gitignorePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
export async function createManagedWorkspace(
|
||||
name: string,
|
||||
links: Record<string, string>
|
||||
): Promise<WorkspaceOutput> {
|
||||
const workspaceName = validateWorkspaceNameForSetup(name);
|
||||
const workspaceRoot = getManagedWorkspaceRoot(workspaceName);
|
||||
const registry = await readRegistry();
|
||||
|
||||
if (registry.workspaces[workspaceName]) {
|
||||
throw new WorkspaceCliError(
|
||||
`Workspace '${workspaceName}' is already recorded in the local workspace registry at ${registry.workspaces[workspaceName]}.`,
|
||||
'workspace_already_exists',
|
||||
{
|
||||
target: 'workspace.name',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (await directoryExists(workspaceRoot)) {
|
||||
throw new WorkspaceCliError(
|
||||
`Workspace '${workspaceName}' already exists at ${workspaceRoot}.`,
|
||||
'workspace_already_exists',
|
||||
{
|
||||
target: 'workspace.root',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let createdWorkspaceRoot = false;
|
||||
|
||||
try {
|
||||
await FileSystemUtils.createDirectory(path.dirname(workspaceRoot));
|
||||
await fs.mkdir(workspaceRoot);
|
||||
createdWorkspaceRoot = true;
|
||||
await FileSystemUtils.createDirectory(getWorkspaceChangesDir(workspaceRoot));
|
||||
await writeWorkspaceSharedState(workspaceRoot, {
|
||||
version: 1,
|
||||
name: workspaceName,
|
||||
links: Object.fromEntries(Object.keys(links).map((linkName) => [linkName, {}])),
|
||||
});
|
||||
await writeWorkspaceLocalState(workspaceRoot, {
|
||||
version: 1,
|
||||
paths: links,
|
||||
});
|
||||
await ensureWorkspaceGitignore(workspaceRoot);
|
||||
await recordWorkspaceInRegistry(workspaceName, workspaceRoot);
|
||||
} catch (error) {
|
||||
if (createdWorkspaceRoot) {
|
||||
try {
|
||||
await fs.rm(workspaceRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Preserve the original creation failure; callers can retry or inspect the path.
|
||||
}
|
||||
}
|
||||
|
||||
throw new WorkspaceCliError(
|
||||
`Could not create workspace '${workspaceName}': ${asErrorMessage(error)}`,
|
||||
'workspace_create_failed',
|
||||
{
|
||||
target: 'workspace.root',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: workspaceName,
|
||||
root: workspaceRoot,
|
||||
planning_path: getWorkspaceChangesDir(workspaceRoot),
|
||||
links: Object.entries(links)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([linkName, linkPath]) => ({
|
||||
name: linkName,
|
||||
path: linkPath,
|
||||
status: [],
|
||||
})),
|
||||
status: [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseSetupLinks(
|
||||
linkInputs: string[] | undefined
|
||||
): Promise<Record<string, string>> {
|
||||
const links: Record<string, string> = {};
|
||||
|
||||
for (const rawLink of linkInputs ?? []) {
|
||||
const parsed = await parseWorkspaceSetupLinkInput(rawLink);
|
||||
const resolvedPath = await resolveExistingDirectory(parsed.pathInput);
|
||||
const linkName = validateLinkNameForCommand(parsed.name ?? inferLinkName(resolvedPath));
|
||||
|
||||
if (links[linkName]) {
|
||||
throw duplicateSetupLinkError(linkName, links[linkName], resolvedPath);
|
||||
}
|
||||
|
||||
links[linkName] = resolvedPath;
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
export async function loadWorkspaceForList(
|
||||
entry: WorkspaceRegistryEntry
|
||||
): Promise<WorkspaceListOutput> {
|
||||
const workspaceStatus: WorkspaceStatus[] = [];
|
||||
|
||||
if (!(await directoryExists(entry.workspaceRoot)) || !(await isWorkspaceRoot(entry.workspaceRoot))) {
|
||||
return {
|
||||
name: entry.name,
|
||||
root: entry.workspaceRoot,
|
||||
links: [],
|
||||
status: [
|
||||
makeStatus('error', 'workspace_root_missing', 'Workspace location does not exist.', {
|
||||
target: 'workspace.root',
|
||||
fix: 'Remove or repair the local registry record.',
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let sharedState: WorkspaceSharedState;
|
||||
let localState: WorkspaceLocalState | null = null;
|
||||
|
||||
try {
|
||||
sharedState = await readWorkspaceSharedState(entry.workspaceRoot);
|
||||
} catch (error) {
|
||||
return {
|
||||
name: entry.name,
|
||||
root: entry.workspaceRoot,
|
||||
links: [],
|
||||
status: [
|
||||
makeStatus(
|
||||
'error',
|
||||
'workspace_state_invalid',
|
||||
`Workspace state could not be read: ${asErrorMessage(error)}`,
|
||||
{
|
||||
target: 'workspace.root',
|
||||
fix: 'Repair the workspace state files before using this workspace.',
|
||||
}
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
localState = await readOptionalWorkspaceLocalState(entry.workspaceRoot);
|
||||
} catch (error) {
|
||||
workspaceStatus.push(localStateInvalidStatus(error));
|
||||
}
|
||||
|
||||
return {
|
||||
name: sharedState.name,
|
||||
root: entry.workspaceRoot,
|
||||
links: normalizeLinksForOutput(sharedState, localState),
|
||||
status: workspaceStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadWorkspaceForDoctor(
|
||||
selected: SelectedWorkspace
|
||||
): Promise<{ workspace: WorkspaceOutput; status: WorkspaceStatus[] }> {
|
||||
const commandStatus = [...selected.status];
|
||||
const workspaceStatus: WorkspaceStatus[] = [];
|
||||
const planningPath = getWorkspaceChangesDir(selected.root);
|
||||
|
||||
if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
|
||||
return {
|
||||
workspace: {
|
||||
name: selected.name,
|
||||
root: selected.root,
|
||||
planning_path: planningPath,
|
||||
links: [],
|
||||
status: [
|
||||
makeStatus(
|
||||
'error',
|
||||
'selected_workspace_root_missing',
|
||||
'Selected workspace location does not exist or is not a valid workspace.',
|
||||
{
|
||||
target: 'workspace.root',
|
||||
fix: 'Repair the local workspace registry record or choose another workspace.',
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
status: commandStatus,
|
||||
};
|
||||
}
|
||||
|
||||
let sharedState: WorkspaceSharedState;
|
||||
let localState: WorkspaceLocalState;
|
||||
let localStateInvalid = false;
|
||||
|
||||
try {
|
||||
sharedState = await readWorkspaceSharedState(selected.root);
|
||||
} catch (error) {
|
||||
return {
|
||||
workspace: {
|
||||
name: selected.name,
|
||||
root: selected.root,
|
||||
planning_path: planningPath,
|
||||
links: [],
|
||||
status: [
|
||||
makeStatus(
|
||||
'error',
|
||||
'workspace_state_invalid',
|
||||
`Workspace state could not be read: ${asErrorMessage(error)}`,
|
||||
{
|
||||
target: 'workspace.root',
|
||||
fix: 'Repair .openspec-workspace/workspace.yaml before using this workspace.',
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
status: commandStatus,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const optionalLocalState = await readOptionalWorkspaceLocalState(selected.root);
|
||||
localState = optionalLocalState ?? emptyLocalState();
|
||||
|
||||
if (!optionalLocalState) {
|
||||
workspaceStatus.push(
|
||||
makeStatus(
|
||||
'warning',
|
||||
'workspace_local_state_missing',
|
||||
'Machine-local paths are not recorded yet.',
|
||||
{
|
||||
target: 'workspace.local_state',
|
||||
fix: 'Run openspec workspace relink <name> <path> for each linked repo or folder on this machine.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
localState = emptyLocalState();
|
||||
localStateInvalid = true;
|
||||
workspaceStatus.push(localStateInvalidStatus(error));
|
||||
}
|
||||
|
||||
if (!(await directoryExists(planningPath))) {
|
||||
workspaceStatus.push(
|
||||
makeStatus(
|
||||
'error',
|
||||
'workspace_planning_path_missing',
|
||||
'Workspace planning path does not exist.',
|
||||
{
|
||||
target: 'workspace.planning_path',
|
||||
fix: `Create ${planningPath} or recreate the workspace with openspec workspace setup.`,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sharedNames = new Set(Object.keys(sharedState.links));
|
||||
const localNames = new Set(Object.keys(localState.paths));
|
||||
const linkNames = [...new Set([...sharedNames, ...localNames])].sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
);
|
||||
const links: WorkspaceLinkOutput[] = [];
|
||||
|
||||
for (const linkName of linkNames) {
|
||||
const linkStatus: WorkspaceStatus[] = [];
|
||||
const localPath = localState.paths[linkName] ?? null;
|
||||
let repoSpecsPath: string | null = null;
|
||||
|
||||
if (!sharedNames.has(linkName)) {
|
||||
linkStatus.push(
|
||||
makeStatus(
|
||||
'warning',
|
||||
'local_path_without_shared_link',
|
||||
'Local path is recorded without a shared workspace link.',
|
||||
{
|
||||
target: `links.${linkName}`,
|
||||
fix: `Add a shared link with openspec workspace link ${linkName} ${localPath ?? '/path/to/folder'} or remove the local-only path from .openspec-workspace/local.yaml.`,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (sharedNames.has(linkName) && !localPath && !localStateInvalid) {
|
||||
linkStatus.push(
|
||||
makeStatus(
|
||||
'error',
|
||||
'linked_path_missing_from_local_state',
|
||||
'Shared link does not have a local path on this machine.',
|
||||
{
|
||||
target: `links.${linkName}.path`,
|
||||
fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (localPath) {
|
||||
if (await directoryExists(localPath)) {
|
||||
const candidateSpecsPath = path.join(localPath, 'openspec', 'specs');
|
||||
repoSpecsPath = (await directoryExists(candidateSpecsPath)) ? candidateSpecsPath : null;
|
||||
} else {
|
||||
linkStatus.push(
|
||||
makeStatus('error', 'linked_path_missing', 'Linked path does not exist.', {
|
||||
target: `links.${linkName}.path`,
|
||||
fix: `openspec workspace relink ${linkName} /path/to/${linkName}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
links.push({
|
||||
name: linkName,
|
||||
path: localPath,
|
||||
repo_specs_path: repoSpecsPath,
|
||||
status: linkStatus,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
workspace: {
|
||||
name: sharedState.name,
|
||||
root: selected.root,
|
||||
planning_path: planningPath,
|
||||
links,
|
||||
status: workspaceStatus,
|
||||
},
|
||||
status: commandStatus,
|
||||
};
|
||||
}
|
||||
|
||||
async function readWorkspaceForMutation(
|
||||
selected: SelectedWorkspace
|
||||
): Promise<{ sharedState: WorkspaceSharedState; localState: WorkspaceLocalState }> {
|
||||
if (!(await directoryExists(selected.root)) || !(await isWorkspaceRoot(selected.root))) {
|
||||
throw new WorkspaceCliError(
|
||||
`Workspace location does not exist for '${selected.name}': ${selected.root}`,
|
||||
'selected_workspace_root_missing',
|
||||
{
|
||||
target: 'workspace.root',
|
||||
fix: 'Run openspec workspace list to inspect known workspaces.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sharedState: await readWorkspaceSharedState(selected.root),
|
||||
localState: await readLocalStateForMutation(selected.root),
|
||||
};
|
||||
}
|
||||
|
||||
async function recordSelectedWorkspaceAfterMutation(selected: SelectedWorkspace): Promise<void> {
|
||||
if (selected.unregisteredCurrentWorkspace) {
|
||||
await recordWorkspaceInRegistry(selected.name, selected.root);
|
||||
}
|
||||
}
|
||||
|
||||
function buildLinkMutationPayload(
|
||||
selected: SelectedWorkspace,
|
||||
sharedState: WorkspaceSharedState,
|
||||
localState: WorkspaceLocalState,
|
||||
linkName: string,
|
||||
linkPath: string
|
||||
): WorkspaceLinkMutationPayload {
|
||||
return {
|
||||
workspace: {
|
||||
name: sharedState.name,
|
||||
root: selected.root,
|
||||
planning_path: getWorkspaceChangesDir(selected.root),
|
||||
links: normalizeLinksForOutput(sharedState, localState),
|
||||
status: [],
|
||||
},
|
||||
link: {
|
||||
name: linkName,
|
||||
path: linkPath,
|
||||
status: [],
|
||||
},
|
||||
status: selected.status,
|
||||
};
|
||||
}
|
||||
|
||||
export async function addWorkspaceLink(
|
||||
selected: SelectedWorkspace,
|
||||
nameOrPath: string,
|
||||
linkPath?: string
|
||||
): Promise<WorkspaceLinkMutationPayload> {
|
||||
const explicitName = linkPath ? nameOrPath : undefined;
|
||||
const pathInput = linkPath ?? nameOrPath;
|
||||
const resolvedPath = await resolveExistingDirectory(pathInput);
|
||||
const linkName = validateLinkNameForCommand(explicitName ?? inferLinkName(resolvedPath));
|
||||
const { sharedState, localState } = await readWorkspaceForMutation(selected);
|
||||
|
||||
if (sharedState.links[linkName]) {
|
||||
throw duplicateLinkError(linkName, localState.paths[linkName] ?? null, resolvedPath);
|
||||
}
|
||||
|
||||
const updatedSharedState: WorkspaceSharedState = {
|
||||
...sharedState,
|
||||
links: {
|
||||
...sharedState.links,
|
||||
[linkName]: {},
|
||||
},
|
||||
};
|
||||
const updatedLocalState: WorkspaceLocalState = {
|
||||
version: 1,
|
||||
paths: {
|
||||
...localState.paths,
|
||||
[linkName]: resolvedPath,
|
||||
},
|
||||
};
|
||||
|
||||
await writeWorkspaceSharedState(selected.root, updatedSharedState);
|
||||
await writeWorkspaceLocalState(selected.root, updatedLocalState);
|
||||
await recordSelectedWorkspaceAfterMutation(selected);
|
||||
|
||||
return buildLinkMutationPayload(
|
||||
selected,
|
||||
updatedSharedState,
|
||||
updatedLocalState,
|
||||
linkName,
|
||||
resolvedPath
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateWorkspaceLink(
|
||||
selected: SelectedWorkspace,
|
||||
linkNameInput: string,
|
||||
linkPath: string
|
||||
): Promise<WorkspaceLinkMutationPayload> {
|
||||
const linkName = validateLinkNameForCommand(linkNameInput);
|
||||
const resolvedPath = await resolveExistingDirectory(linkPath);
|
||||
const { sharedState, localState } = await readWorkspaceForMutation(selected);
|
||||
|
||||
if (!sharedState.links[linkName]) {
|
||||
throw new WorkspaceCliError(`Unknown workspace link '${linkName}'.`, 'unknown_link_name', {
|
||||
target: `links.${linkName}`,
|
||||
fix: 'Run openspec workspace doctor to see linked repos or folders.',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedLocalState: WorkspaceLocalState = {
|
||||
version: 1,
|
||||
paths: {
|
||||
...localState.paths,
|
||||
[linkName]: resolvedPath,
|
||||
},
|
||||
};
|
||||
|
||||
await writeWorkspaceLocalState(selected.root, updatedLocalState);
|
||||
await recordSelectedWorkspaceAfterMutation(selected);
|
||||
|
||||
return buildLinkMutationPayload(selected, sharedState, updatedLocalState, linkName, resolvedPath);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import {
|
||||
findWorkspaceRoot,
|
||||
listWorkspaceRegistryEntries,
|
||||
readWorkspaceSharedState,
|
||||
} from '../../core/workspace/index.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import { isInteractive, resolveNoInteractive } from '../../utils/interactive.js';
|
||||
import { readRegistry, validateWorkspaceNameForSetup } from './operations.js';
|
||||
import {
|
||||
SelectedWorkspace,
|
||||
WorkspaceCliError,
|
||||
WorkspaceSelectionOptions,
|
||||
makeStatus,
|
||||
} from './types.js';
|
||||
|
||||
function normalizeRegistryRootForComparison(workspaceRoot: string): string {
|
||||
return process.platform === 'win32'
|
||||
? FileSystemUtils.canonicalizeExistingPath(workspaceRoot)
|
||||
: workspaceRoot;
|
||||
}
|
||||
|
||||
export async function selectWorkspaceForCommand(
|
||||
options: WorkspaceSelectionOptions,
|
||||
commandName: string
|
||||
): Promise<SelectedWorkspace> {
|
||||
const registry = await readRegistry();
|
||||
|
||||
if (options.workspace) {
|
||||
const workspaceName = validateWorkspaceNameForSetup(options.workspace);
|
||||
const registryRoot = registry.workspaces[workspaceName];
|
||||
|
||||
if (!registryRoot) {
|
||||
throw new WorkspaceCliError(
|
||||
`Unknown OpenSpec workspace '${workspaceName}'.`,
|
||||
'workspace_not_found',
|
||||
{
|
||||
target: 'workspace.name',
|
||||
fix: 'Run openspec workspace list to see known workspaces.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: workspaceName,
|
||||
root: registryRoot,
|
||||
status: [],
|
||||
unregisteredCurrentWorkspace: false,
|
||||
};
|
||||
}
|
||||
|
||||
const currentWorkspaceRoot = await findWorkspaceRoot(process.cwd());
|
||||
|
||||
if (currentWorkspaceRoot) {
|
||||
const sharedState = await readWorkspaceSharedState(currentWorkspaceRoot);
|
||||
const registeredRoot = registry.workspaces[sharedState.name];
|
||||
const isRegistered =
|
||||
registeredRoot !== undefined &&
|
||||
normalizeRegistryRootForComparison(registeredRoot) === currentWorkspaceRoot;
|
||||
const warning = makeStatus(
|
||||
'warning',
|
||||
'workspace_not_in_local_registry',
|
||||
'This workspace is not recorded in the local workspace registry.',
|
||||
{
|
||||
target: 'workspace.root',
|
||||
fix: 'Run a mutating workspace command from this workspace, such as workspace link or workspace relink, to record it locally.',
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
name: sharedState.name,
|
||||
root: currentWorkspaceRoot,
|
||||
status: isRegistered ? [] : [warning],
|
||||
unregisteredCurrentWorkspace: !isRegistered,
|
||||
};
|
||||
}
|
||||
|
||||
const entries = listWorkspaceRegistryEntries(registry);
|
||||
|
||||
if (entries.length === 0) {
|
||||
throw new WorkspaceCliError(
|
||||
"No known OpenSpec workspaces. Run 'openspec workspace setup' first.\nAfter at least one workspace is known locally, you can also pass --workspace <name>.",
|
||||
'no_known_workspaces',
|
||||
{
|
||||
target: 'workspace.name',
|
||||
fix: 'openspec workspace setup',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (entries.length === 1) {
|
||||
const [entry] = entries;
|
||||
|
||||
return {
|
||||
name: entry.name,
|
||||
root: entry.workspaceRoot,
|
||||
status: [],
|
||||
unregisteredCurrentWorkspace: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (options.json || resolveNoInteractive(options) || !isInteractive(options)) {
|
||||
throw new WorkspaceCliError(
|
||||
'Multiple OpenSpec workspaces are known. Pass --workspace <name>.',
|
||||
'workspace_selection_ambiguous',
|
||||
{
|
||||
target: 'workspace.name',
|
||||
fix: `openspec workspace ${commandName} --workspace <name>`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { select } = await import('@inquirer/prompts');
|
||||
const selectedName = await select({
|
||||
message: 'Select workspace:',
|
||||
choices: entries.map((entry) => ({
|
||||
name: `${entry.name} (${entry.workspaceRoot})`,
|
||||
value: entry.name,
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
name: selectedName,
|
||||
root: registry.workspaces[selectedName],
|
||||
status: [],
|
||||
unregisteredCurrentWorkspace: false,
|
||||
};
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
export type StatusSeverity = 'error' | 'warning';
|
||||
|
||||
export interface WorkspaceStatus {
|
||||
severity: StatusSeverity;
|
||||
code: string;
|
||||
message: string;
|
||||
target?: string;
|
||||
fix?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceLinkOutput {
|
||||
name: string;
|
||||
path: string | null;
|
||||
repo_specs_path?: string | null;
|
||||
status: WorkspaceStatus[];
|
||||
}
|
||||
|
||||
export interface WorkspaceOutput {
|
||||
name: string;
|
||||
root: string;
|
||||
planning_path: string;
|
||||
links: WorkspaceLinkOutput[];
|
||||
status: WorkspaceStatus[];
|
||||
}
|
||||
|
||||
export interface WorkspaceListOutput {
|
||||
name: string;
|
||||
root: string;
|
||||
links: WorkspaceLinkOutput[];
|
||||
status: WorkspaceStatus[];
|
||||
}
|
||||
|
||||
export interface WorkspaceSetupOptions {
|
||||
name?: string;
|
||||
link?: string[];
|
||||
json?: boolean;
|
||||
noInteractive?: boolean;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceSelectionOptions {
|
||||
workspace?: string;
|
||||
json?: boolean;
|
||||
noInteractive?: boolean;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export type WorkspaceLinkOptions = WorkspaceSelectionOptions;
|
||||
|
||||
export interface WorkspaceListOptions {
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectedWorkspace {
|
||||
name: string;
|
||||
root: string;
|
||||
status: WorkspaceStatus[];
|
||||
unregisteredCurrentWorkspace: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceLinkMutationPayload {
|
||||
workspace: WorkspaceOutput;
|
||||
link: {
|
||||
name: string;
|
||||
path: string;
|
||||
status: WorkspaceStatus[];
|
||||
};
|
||||
status: WorkspaceStatus[];
|
||||
}
|
||||
|
||||
export class WorkspaceCliError extends Error {
|
||||
readonly status: WorkspaceStatus;
|
||||
|
||||
constructor(message: string, code: string, options: { target?: string; fix?: string } = {}) {
|
||||
super(message);
|
||||
this.status = {
|
||||
severity: 'error',
|
||||
code,
|
||||
message,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function makeStatus(
|
||||
severity: StatusSeverity,
|
||||
code: string,
|
||||
message: string,
|
||||
options: { target?: string; fix?: string } = {}
|
||||
): WorkspaceStatus {
|
||||
return {
|
||||
severity,
|
||||
code,
|
||||
message,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function asErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export function asStatus(error: unknown): WorkspaceStatus {
|
||||
if (error instanceof WorkspaceCliError) {
|
||||
return error.status;
|
||||
}
|
||||
|
||||
return makeStatus('error', 'workspace_error', asErrorMessage(error));
|
||||
}
|
||||
|
||||
export function appendStatus<T extends { status: WorkspaceStatus[] }>(
|
||||
payload: T,
|
||||
status: WorkspaceStatus
|
||||
): T {
|
||||
return {
|
||||
...payload,
|
||||
status: [...payload.status, status],
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export { ArtifactGraph } from './graph.js';
|
||||
|
||||
// State detection
|
||||
export { detectCompleted } from './state.js';
|
||||
export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
|
||||
|
||||
// Schema resolution
|
||||
export {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getSchemaDir, resolveSchema } from './resolver.js';
|
||||
import { ArtifactGraph } from './graph.js';
|
||||
import { detectCompleted } from './state.js';
|
||||
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import { readProjectConfig, validateConfigRules } from '../project-config.js';
|
||||
import type { Artifact, CompletedSet } from './types.js';
|
||||
|
||||
@@ -138,17 +137,15 @@ export function loadTemplate(
|
||||
);
|
||||
}
|
||||
|
||||
const templatePathOnDisk = path.join(schemaDir, 'templates', templatePath);
|
||||
const fullPath = path.join(schemaDir, 'templates', templatePath);
|
||||
|
||||
if (!fs.existsSync(templatePathOnDisk)) {
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new TemplateLoadError(
|
||||
`Template not found: ${templatePathOnDisk}`,
|
||||
templatePathOnDisk
|
||||
`Template not found: ${fullPath}`,
|
||||
fullPath
|
||||
);
|
||||
}
|
||||
|
||||
const fullPath = FileSystemUtils.canonicalizeExistingPath(templatePathOnDisk);
|
||||
|
||||
try {
|
||||
return fs.readFileSync(fullPath, 'utf-8');
|
||||
} catch (err) {
|
||||
@@ -178,9 +175,7 @@ export function loadChangeContext(
|
||||
changeName: string,
|
||||
schemaName?: string
|
||||
): ChangeContext {
|
||||
const changeDir = FileSystemUtils.canonicalizeExistingPath(
|
||||
path.join(projectRoot, 'openspec', 'changes', changeName)
|
||||
);
|
||||
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
|
||||
|
||||
// Resolve schema: explicit > metadata > default
|
||||
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import fg from 'fast-glob';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
|
||||
/**
|
||||
* Checks if a path contains glob pattern characters.
|
||||
*/
|
||||
export function isGlobPattern(pattern: string): boolean {
|
||||
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an artifact's output path(s) to concrete files that currently exist.
|
||||
* Returns absolute file paths. Glob matches are sorted for deterministic output.
|
||||
*/
|
||||
export function resolveArtifactOutputs(changeDir: string, generates: string): string[] {
|
||||
if (!isGlobPattern(generates)) {
|
||||
const fullPath = path.join(changeDir, generates);
|
||||
try {
|
||||
return fs.statSync(fullPath).isFile()
|
||||
? [FileSystemUtils.canonicalizeExistingPath(fullPath)]
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedPattern = FileSystemUtils.toPosixPath(generates);
|
||||
const matches = fg
|
||||
.sync(normalizedPattern, { cwd: changeDir, onlyFiles: true, absolute: true })
|
||||
.map((match) => FileSystemUtils.canonicalizeExistingPath(path.normalize(match)));
|
||||
|
||||
return Array.from(new Set(matches)).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an artifact has at least one resolved output file.
|
||||
*/
|
||||
export function artifactOutputExists(changeDir: string, generates: string): boolean {
|
||||
return resolveArtifactOutputs(changeDir, generates).length > 0;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import fg from 'fast-glob';
|
||||
import type { CompletedSet } from './types.js';
|
||||
import type { ArtifactGraph } from './graph.js';
|
||||
import { artifactOutputExists } from './outputs.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
|
||||
/**
|
||||
* Detects which artifacts are completed by checking file existence in the change directory.
|
||||
@@ -33,5 +35,30 @@ export function detectCompleted(graph: ArtifactGraph, changeDir: string): Comple
|
||||
* Supports both simple paths and glob patterns.
|
||||
*/
|
||||
function isArtifactComplete(generates: string, changeDir: string): boolean {
|
||||
return artifactOutputExists(changeDir, generates);
|
||||
const fullPattern = path.join(changeDir, generates);
|
||||
|
||||
// Check if it's a glob pattern
|
||||
if (isGlobPattern(generates)) {
|
||||
return hasGlobMatches(fullPattern);
|
||||
}
|
||||
|
||||
// Simple file path - check if file exists
|
||||
return fs.existsSync(fullPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path contains glob pattern characters.
|
||||
*/
|
||||
function isGlobPattern(pattern: string): boolean {
|
||||
return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a glob pattern has any matches.
|
||||
* Normalizes Windows backslashes to forward slashes for cross-platform glob compatibility.
|
||||
*/
|
||||
function hasGlobMatches(pattern: string): boolean {
|
||||
const normalizedPattern = FileSystemUtils.toPosixPath(pattern);
|
||||
const matches = fg.sync(normalizedPattern, { onlyFiles: true });
|
||||
return matches.length > 0;
|
||||
}
|
||||
|
||||
@@ -13,26 +13,12 @@ import { AI_TOOLS, type AIToolOption } from './config.js';
|
||||
* Scans the project path for AI tool configuration directories and returns
|
||||
* the tools that are present.
|
||||
*
|
||||
* For tools with `detectionPaths`, checks those specific paths (files or
|
||||
* directories). Otherwise checks for the tool's `skillsDir` directory at
|
||||
* the project root. Only tools with a `skillsDir` property are considered.
|
||||
* Checks for each tool's `skillsDir` (e.g., `.claude/`, `.cursor/`) at the
|
||||
* project root. Only tools with a `skillsDir` property are considered.
|
||||
*/
|
||||
export function getAvailableTools(projectPath: string): AIToolOption[] {
|
||||
return AI_TOOLS.filter((tool) => {
|
||||
if (!tool.skillsDir) return false;
|
||||
|
||||
if (tool.detectionPaths && tool.detectionPaths.length > 0) {
|
||||
// statSync without .isDirectory() — detection paths can be files or directories
|
||||
return tool.detectionPaths.some((p) => {
|
||||
try {
|
||||
fs.statSync(path.join(projectPath, p));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const dirPath = path.join(projectPath, tool.skillsDir);
|
||||
try {
|
||||
return fs.statSync(dirPath).isDirectory();
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Bob Shell Command Adapter
|
||||
*
|
||||
* Formats commands for Bob Shell following its markdown specification.
|
||||
* Commands are stored in .bob/commands/ directory.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
||||
|
||||
/**
|
||||
* Escapes a string value for safe YAML output.
|
||||
* Quotes the string if it contains special YAML characters.
|
||||
*/
|
||||
function escapeYamlValue(value: string): string {
|
||||
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
|
||||
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
|
||||
if (needsQuoting) {
|
||||
// Use double quotes and escape internal double quotes and backslashes
|
||||
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob Shell adapter for command generation.
|
||||
* File path: .bob/commands/opsx-<id>.md
|
||||
* Frontmatter: description, argument-hint
|
||||
*/
|
||||
export const bobAdapter: ToolCommandAdapter = {
|
||||
toolId: 'bob',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.bob', 'commands', `opsx-${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
// Transform command references from colon to hyphen format for Bob
|
||||
const transformedBody = transformToHyphenCommands(content.body);
|
||||
|
||||
return `---
|
||||
description: ${escapeYamlValue(content.description)}
|
||||
argument-hint: command arguments
|
||||
---
|
||||
|
||||
${transformedBody}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -7,7 +7,6 @@
|
||||
export { amazonQAdapter } from './amazon-q.js';
|
||||
export { antigravityAdapter } from './antigravity.js';
|
||||
export { auggieAdapter } from './auggie.js';
|
||||
export { bobAdapter } from './bob.js';
|
||||
export { claudeAdapter } from './claude.js';
|
||||
export { clineAdapter } from './cline.js';
|
||||
export { codexAdapter } from './codex.js';
|
||||
@@ -20,13 +19,11 @@ export { factoryAdapter } from './factory.js';
|
||||
export { geminiAdapter } from './gemini.js';
|
||||
export { githubCopilotAdapter } from './github-copilot.js';
|
||||
export { iflowAdapter } from './iflow.js';
|
||||
export { junieAdapter } from './junie.js';
|
||||
export { kilocodeAdapter } from './kilocode.js';
|
||||
export { kiroAdapter } from './kiro.js';
|
||||
export { opencodeAdapter } from './opencode.js';
|
||||
export { piAdapter } from './pi.js';
|
||||
export { qoderAdapter } from './qoder.js';
|
||||
export { lingmaAdapter } from './lingma.js';
|
||||
export { qwenAdapter } from './qwen.js';
|
||||
export { roocodeAdapter } from './roocode.js';
|
||||
export { windsurfAdapter } from './windsurf.js';
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Junie Command Adapter
|
||||
*
|
||||
* Formats commands for Junie following its frontmatter specification.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
|
||||
/**
|
||||
* Junie adapter for command generation.
|
||||
* File path: .junie/commands/opsx-<id>.md
|
||||
* Frontmatter: description
|
||||
*/
|
||||
export const junieAdapter: ToolCommandAdapter = {
|
||||
toolId: 'junie',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.junie', 'commands', `opsx-${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
return `---
|
||||
description: ${content.description}
|
||||
---
|
||||
|
||||
${content.body}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Lingma Command Adapter
|
||||
*
|
||||
* Formats commands for Lingma following its frontmatter specification.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
|
||||
/**
|
||||
* Lingma adapter for command generation.
|
||||
* File path: .lingma/commands/opsx/<id>.md
|
||||
* Frontmatter: name, description, category, tags
|
||||
*/
|
||||
export const lingmaAdapter: ToolCommandAdapter = {
|
||||
toolId: 'lingma',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.lingma', 'commands', 'opsx', `${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
const tagsStr = content.tags.join(', ');
|
||||
return `---
|
||||
name: ${content.name}
|
||||
description: ${content.description}
|
||||
category: ${content.category}
|
||||
tags: [${tagsStr}]
|
||||
---
|
||||
|
||||
${content.body}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -10,14 +10,14 @@ import { transformToHyphenCommands } from '../../../utils/command-references.js'
|
||||
|
||||
/**
|
||||
* OpenCode adapter for command generation.
|
||||
* File path: .opencode/commands/opsx-<id>.md
|
||||
* File path: .opencode/command/opsx-<id>.md
|
||||
* Frontmatter: description
|
||||
*/
|
||||
export const opencodeAdapter: ToolCommandAdapter = {
|
||||
toolId: 'opencode',
|
||||
|
||||
getFilePath(commandId: string): string {
|
||||
return path.join('.opencode', 'commands', `opsx-${commandId}.md`);
|
||||
return path.join('.opencode', 'command', `opsx-${commandId}.md`);
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
|
||||
@@ -7,20 +7,6 @@
|
||||
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.js';
|
||||
import { transformToHyphenCommands } from '../../../utils/command-references.js';
|
||||
|
||||
const PI_INPUT_HEADING = /^\*\*Input\*\*:[^\n]*$/m;
|
||||
|
||||
function injectPiArgs(body: string): string {
|
||||
if (body.includes('$@') || body.includes('$ARGUMENTS')) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return body.replace(
|
||||
PI_INPUT_HEADING,
|
||||
(heading) => `${heading}\n**Provided arguments**: $@`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string value for safe YAML output.
|
||||
@@ -41,10 +27,6 @@ function escapeYamlValue(value: string): string {
|
||||
* Pi adapter for prompt template generation.
|
||||
* File path: .pi/prompts/opsx-<id>.md
|
||||
* Frontmatter: description
|
||||
*
|
||||
* Pi uses the filename (minus .md) as the slash command name, so
|
||||
* opsx-propose.md → /opsx-propose. Command references in the body
|
||||
* are transformed from /opsx: to /opsx- for consistency.
|
||||
*/
|
||||
export const piAdapter: ToolCommandAdapter = {
|
||||
toolId: 'pi',
|
||||
@@ -54,14 +36,11 @@ export const piAdapter: ToolCommandAdapter = {
|
||||
},
|
||||
|
||||
formatFile(content: CommandContent): string {
|
||||
// Transform /opsx: references to /opsx- and inject $@ for template args
|
||||
const transformedBody = transformToHyphenCommands(content.body);
|
||||
|
||||
return `---
|
||||
description: ${escapeYamlValue(content.description)}
|
||||
---
|
||||
|
||||
${injectPiArgs(transformedBody)}
|
||||
${content.body}
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { ToolCommandAdapter } from './types.js';
|
||||
import { amazonQAdapter } from './adapters/amazon-q.js';
|
||||
import { antigravityAdapter } from './adapters/antigravity.js';
|
||||
import { auggieAdapter } from './adapters/auggie.js';
|
||||
import { bobAdapter } from './adapters/bob.js';
|
||||
import { claudeAdapter } from './adapters/claude.js';
|
||||
import { clineAdapter } from './adapters/cline.js';
|
||||
import { codexAdapter } from './adapters/codex.js';
|
||||
@@ -22,13 +21,11 @@ import { factoryAdapter } from './adapters/factory.js';
|
||||
import { geminiAdapter } from './adapters/gemini.js';
|
||||
import { githubCopilotAdapter } from './adapters/github-copilot.js';
|
||||
import { iflowAdapter } from './adapters/iflow.js';
|
||||
import { junieAdapter } from './adapters/junie.js';
|
||||
import { kilocodeAdapter } from './adapters/kilocode.js';
|
||||
import { kiroAdapter } from './adapters/kiro.js';
|
||||
import { opencodeAdapter } from './adapters/opencode.js';
|
||||
import { piAdapter } from './adapters/pi.js';
|
||||
import { qoderAdapter } from './adapters/qoder.js';
|
||||
import { lingmaAdapter } from './adapters/lingma.js';
|
||||
import { qwenAdapter } from './adapters/qwen.js';
|
||||
import { roocodeAdapter } from './adapters/roocode.js';
|
||||
import { windsurfAdapter } from './adapters/windsurf.js';
|
||||
@@ -44,7 +41,6 @@ export class CommandAdapterRegistry {
|
||||
CommandAdapterRegistry.register(amazonQAdapter);
|
||||
CommandAdapterRegistry.register(antigravityAdapter);
|
||||
CommandAdapterRegistry.register(auggieAdapter);
|
||||
CommandAdapterRegistry.register(bobAdapter);
|
||||
CommandAdapterRegistry.register(claudeAdapter);
|
||||
CommandAdapterRegistry.register(clineAdapter);
|
||||
CommandAdapterRegistry.register(codexAdapter);
|
||||
@@ -57,13 +53,11 @@ export class CommandAdapterRegistry {
|
||||
CommandAdapterRegistry.register(geminiAdapter);
|
||||
CommandAdapterRegistry.register(githubCopilotAdapter);
|
||||
CommandAdapterRegistry.register(iflowAdapter);
|
||||
CommandAdapterRegistry.register(junieAdapter);
|
||||
CommandAdapterRegistry.register(kilocodeAdapter);
|
||||
CommandAdapterRegistry.register(kiroAdapter);
|
||||
CommandAdapterRegistry.register(opencodeAdapter);
|
||||
CommandAdapterRegistry.register(piAdapter);
|
||||
CommandAdapterRegistry.register(qoderAdapter);
|
||||
CommandAdapterRegistry.register(lingmaAdapter);
|
||||
CommandAdapterRegistry.register(qwenAdapter);
|
||||
CommandAdapterRegistry.register(roocodeAdapter);
|
||||
CommandAdapterRegistry.register(windsurfAdapter);
|
||||
|
||||
@@ -155,106 +155,6 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'workspace',
|
||||
description: 'Set up and inspect coordination workspaces',
|
||||
flags: [],
|
||||
subcommands: [
|
||||
{
|
||||
name: 'setup',
|
||||
description: 'Set up a workspace and link existing repos or folders',
|
||||
flags: [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Workspace name',
|
||||
takesValue: true,
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
description: 'Repo or folder link. Use <path> or <name>=<path>',
|
||||
takesValue: true,
|
||||
},
|
||||
COMMON_FLAGS.json,
|
||||
COMMON_FLAGS.noInteractive,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
description: 'List known OpenSpec workspaces',
|
||||
flags: [
|
||||
COMMON_FLAGS.json,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ls',
|
||||
description: 'List known OpenSpec workspaces',
|
||||
flags: [
|
||||
COMMON_FLAGS.json,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
description: 'Link an existing repo or folder to a workspace',
|
||||
acceptsPositional: true,
|
||||
positionals: [
|
||||
{
|
||||
name: 'name-or-path',
|
||||
type: 'path',
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
type: 'path',
|
||||
},
|
||||
],
|
||||
flags: [
|
||||
{
|
||||
name: 'workspace',
|
||||
description: 'Workspace name from the local workspace registry',
|
||||
takesValue: true,
|
||||
},
|
||||
COMMON_FLAGS.json,
|
||||
COMMON_FLAGS.noInteractive,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'relink',
|
||||
description: 'Update the local path for an existing workspace link',
|
||||
acceptsPositional: true,
|
||||
positionals: [
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
type: 'path',
|
||||
},
|
||||
],
|
||||
flags: [
|
||||
{
|
||||
name: 'workspace',
|
||||
description: 'Workspace name from the local workspace registry',
|
||||
takesValue: true,
|
||||
},
|
||||
COMMON_FLAGS.json,
|
||||
COMMON_FLAGS.noInteractive,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'doctor',
|
||||
description: 'Check what a workspace can resolve on this machine',
|
||||
flags: [
|
||||
{
|
||||
name: 'workspace',
|
||||
description: 'Workspace name from the local workspace registry',
|
||||
takesValue: true,
|
||||
},
|
||||
COMMON_FLAGS.json,
|
||||
COMMON_FLAGS.noInteractive,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'feedback',
|
||||
description: 'Submit feedback about OpenSpec',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getActiveChangeIds, getSpecIds } from '../../utils/item-discovery.js';
|
||||
import { listSchemas } from '../artifact-graph/index.js';
|
||||
|
||||
/**
|
||||
* Cache entry for completion data
|
||||
@@ -18,7 +17,6 @@ export class CompletionProvider {
|
||||
private readonly cacheTTL: number;
|
||||
private changeCache: CacheEntry<string[]> | null = null;
|
||||
private specCache: CacheEntry<string[]> | null = null;
|
||||
private schemaCache: CacheEntry<string[]> | null = null;
|
||||
|
||||
/**
|
||||
* Creates a new completion provider
|
||||
@@ -83,31 +81,6 @@ export class CompletionProvider {
|
||||
return specIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all schema names for completion
|
||||
*
|
||||
* @returns Array of schema names
|
||||
*/
|
||||
async getSchemaNames(): Promise<string[]> {
|
||||
const now = Date.now();
|
||||
|
||||
// Check if cache is valid
|
||||
if (this.schemaCache && now - this.schemaCache.timestamp < this.cacheTTL) {
|
||||
return this.schemaCache.data;
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
const schemaNames = listSchemas(this.projectRoot);
|
||||
|
||||
// Update cache
|
||||
this.schemaCache = {
|
||||
data: schemaNames,
|
||||
timestamp: now,
|
||||
};
|
||||
|
||||
return schemaNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get both change and spec IDs for completion
|
||||
*
|
||||
@@ -128,7 +101,6 @@ export class CompletionProvider {
|
||||
clearCache(): void {
|
||||
this.changeCache = null;
|
||||
this.specCache = null;
|
||||
this.schemaCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +111,6 @@ export class CompletionProvider {
|
||||
getCacheStats(): {
|
||||
changeCache: { valid: boolean; age?: number };
|
||||
specCache: { valid: boolean; age?: number };
|
||||
schemaCache: { valid: boolean; age?: number };
|
||||
} {
|
||||
const now = Date.now();
|
||||
|
||||
@@ -152,10 +123,6 @@ export class CompletionProvider {
|
||||
valid: this.specCache !== null && now - this.specCache.timestamp < this.cacheTTL,
|
||||
age: this.specCache ? now - this.specCache.timestamp : undefined,
|
||||
},
|
||||
schemaCache: {
|
||||
valid: this.schemaCache !== null && now - this.schemaCache.timestamp < this.cacheTTL,
|
||||
age: this.schemaCache ? now - this.schemaCache.timestamp : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
CompletionGenerator,
|
||||
CommandDefinition,
|
||||
FlagDefinition,
|
||||
PositionalDefinition,
|
||||
} from '../types.js';
|
||||
import { CompletionGenerator, CommandDefinition, FlagDefinition } from '../types.js';
|
||||
import { BASH_DYNAMIC_HELPERS } from '../templates/bash-templates.js';
|
||||
|
||||
/**
|
||||
@@ -114,14 +109,14 @@ complete -F _openspec_completion openspec
|
||||
|
||||
for (const subcmd of cmd.subcommands) {
|
||||
lines.push(`${indent} ${subcmd.name})`);
|
||||
lines.push(...this.generateArgumentCompletion(subcmd, indent + ' ', 3));
|
||||
lines.push(...this.generateArgumentCompletion(subcmd, indent + ' '));
|
||||
lines.push(`${indent} ;;`);
|
||||
}
|
||||
|
||||
lines.push(`${indent}esac`);
|
||||
} else {
|
||||
// No subcommands, just complete arguments
|
||||
lines.push(...this.generateArgumentCompletion(cmd, indent, 2));
|
||||
lines.push(...this.generateArgumentCompletion(cmd, indent));
|
||||
}
|
||||
|
||||
return lines;
|
||||
@@ -130,11 +125,7 @@ complete -F _openspec_completion openspec
|
||||
/**
|
||||
* Generate argument completion (flags and positional arguments)
|
||||
*/
|
||||
private generateArgumentCompletion(
|
||||
cmd: CommandDefinition,
|
||||
indent: string,
|
||||
firstPositionalWordIndex: number
|
||||
): string[] {
|
||||
private generateArgumentCompletion(cmd: CommandDefinition, indent: string): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
// Check for flag completion
|
||||
@@ -154,14 +145,7 @@ complete -F _openspec_completion openspec
|
||||
}
|
||||
|
||||
// Handle positional completions
|
||||
if (cmd.positionals && cmd.positionals.length > 0) {
|
||||
lines.push(...this.generateIndexedPositionalCompletion(
|
||||
cmd.positionals,
|
||||
cmd.flags,
|
||||
firstPositionalWordIndex,
|
||||
indent
|
||||
));
|
||||
} else if (cmd.acceptsPositional) {
|
||||
if (cmd.acceptsPositional) {
|
||||
lines.push(...this.generatePositionalCompletion(cmd.positionalType, indent));
|
||||
}
|
||||
|
||||
@@ -184,9 +168,6 @@ complete -F _openspec_completion openspec
|
||||
case 'change-or-spec-id':
|
||||
lines.push(`${indent}_openspec_complete_items`);
|
||||
break;
|
||||
case 'schema-name':
|
||||
lines.push(`${indent}_openspec_complete_schemas`);
|
||||
break;
|
||||
case 'shell':
|
||||
lines.push(`${indent}local shells="zsh bash fish powershell"`);
|
||||
lines.push(`${indent}COMPREPLY=($(compgen -W "$shells" -- "$cur"))`);
|
||||
@@ -199,73 +180,6 @@ complete -F _openspec_completion openspec
|
||||
return lines;
|
||||
}
|
||||
|
||||
private generateIndexedPositionalCompletion(
|
||||
positionals: PositionalDefinition[],
|
||||
flags: FlagDefinition[],
|
||||
firstPositionalWordIndex: number,
|
||||
indent: string
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
const valueFlagCases = this.generateValueFlagCases(flags);
|
||||
|
||||
if (valueFlagCases.length > 0) {
|
||||
lines.push(`${indent}case "$prev" in`);
|
||||
lines.push(`${indent} ${valueFlagCases.join('|')}) return 0 ;;`);
|
||||
lines.push(`${indent}esac`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push(`${indent}local positional_index=0`);
|
||||
lines.push(`${indent}local skip_next=0`);
|
||||
lines.push(`${indent}local i`);
|
||||
lines.push(`${indent}for ((i = ${firstPositionalWordIndex}; i < cword; i++)); do`);
|
||||
lines.push(`${indent} if [[ $skip_next -eq 1 ]]; then`);
|
||||
lines.push(`${indent} skip_next=0`);
|
||||
lines.push(`${indent} continue`);
|
||||
lines.push(`${indent} fi`);
|
||||
lines.push(`${indent} case "\${words[i]}" in`);
|
||||
|
||||
if (valueFlagCases.length > 0) {
|
||||
lines.push(`${indent} ${valueFlagCases.join('|')}) skip_next=1 ;;`);
|
||||
lines.push(`${indent} ${valueFlagCases.map((flag) => `${flag}=*`).join('|')}) ;;`);
|
||||
}
|
||||
|
||||
lines.push(`${indent} -*) ;;`);
|
||||
lines.push(`${indent} *) ((positional_index++)) ;;`);
|
||||
lines.push(`${indent} esac`);
|
||||
lines.push(`${indent}done`);
|
||||
lines.push('');
|
||||
lines.push(`${indent}case "$positional_index" in`);
|
||||
|
||||
for (const [index, positional] of positionals.entries()) {
|
||||
const completion = this.generateIndexedPositionalCase(positional, indent + ' ');
|
||||
if (completion.length === 0) continue;
|
||||
lines.push(`${indent} ${index})`);
|
||||
lines.push(...completion);
|
||||
lines.push(`${indent} ;;`);
|
||||
}
|
||||
|
||||
lines.push(`${indent}esac`);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
private generateValueFlagCases(flags: FlagDefinition[]): string[] {
|
||||
return flags
|
||||
.filter((flag) => flag.takesValue)
|
||||
.flatMap((flag) => [
|
||||
`--${flag.name}`,
|
||||
...(flag.short ? [`-${flag.short}`] : []),
|
||||
]);
|
||||
}
|
||||
|
||||
private generateIndexedPositionalCase(
|
||||
positional: PositionalDefinition,
|
||||
indent: string
|
||||
): string[] {
|
||||
return this.generatePositionalCompletion(positional.type, indent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escape command/subcommand names for safe use in Bash scripts
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user