Compare commits

..
Author SHA1 Message Date
Clay Good cd5083dcb8 docs(root): propose custom OpenSpec directory 2026-09-28 15:55:25 -05:00
151 changed files with 1114 additions and 6305 deletions
-3
View File
@@ -1,5 +1,2 @@
# Default code ownership
* @Fission-AI/openspec-maintainers
# Route docs-lab changes to the docs owner for review
/docs-lab/ @TabishB
-68
View File
@@ -1,68 +0,0 @@
name: Bug report
description: Something in OpenSpec does not work the way it should.
labels: ['bug', 'needs-triage']
body:
- type: markdown
attributes:
value: |
Thanks for reporting this.
You can also run `openspec feedback "your report"` to submit an issue
immediately with your version and platform, or get a submission link
if GitHub CLI is unavailable or unauthenticated.
- type: textarea
id: what_happened
attributes:
label: What happened
description: The actual behavior. Paste the command you ran and its output if you have it.
placeholder: |
I ran `openspec archive add-login` and it exited 0 without writing the main spec.
validations:
required: true
- type: textarea
id: expected
attributes:
label: What you expected instead
placeholder: The main spec at openspec/specs/auth/spec.md should have been updated.
validations:
required: true
- type: textarea
id: repro
attributes:
label: Minimal steps to reproduce
description: The shortest path from a fresh project to the problem. This is the single most useful thing you can give us.
placeholder: |
1. `openspec init` in an empty directory
2. ...
3. ...
validations:
required: true
- type: input
id: version
attributes:
label: OpenSpec version
description: Output of `openspec --version`, or "unknown" if installation failed or the command cannot run.
placeholder: '1.11.0'
validations:
required: true
- type: input
id: agent
attributes:
label: Coding agent and model
description: Which agent and model were driving OpenSpec, if any. Behavior often differs between them.
placeholder: Claude Code, Opus 4.6
validations:
required: false
- type: input
id: environment
attributes:
label: OS and Node version
placeholder: macOS 15.6, Node 22.11.0
validations:
required: false
-12
View File
@@ -1,12 +0,0 @@
# Keep the prefilled blank-issue URL from `openspec feedback` working.
blank_issues_enabled: true
contact_links:
- name: Core design change
url: https://github.com/Fission-AI/OpenSpec/discussions/categories/ideas
about: Anything that changes how OpenSpec works at its core starts as a discussion, per CONTRIBUTING step 1.
- name: Question or help with your setup
url: https://github.com/Fission-AI/OpenSpec/discussions/categories/q-a
about: Not sure whether it is a bug? Ask here and we will help you narrow it down.
- name: Discord
url: https://discord.gg/YctCnvvshC
about: Chat with the community and the maintainers.
@@ -1,44 +0,0 @@
name: Feature request
description: Something OpenSpec should do that it does not do yet.
labels: ['enhancement', 'needs-triage']
body:
- type: markdown
attributes:
value: |
If this would change OpenSpec's core design, open a
[discussion](https://github.com/Fission-AI/OpenSpec/discussions) instead — see
[CONTRIBUTING.md](https://github.com/Fission-AI/OpenSpec/blob/main/CONTRIBUTING.md).
- type: textarea
id: problem
attributes:
label: The problem, in one or two sentences
description: What you were trying to do, and where OpenSpec got in the way. Describe the problem, not the solution.
placeholder: There is no way to tell which change a spec came from after it is archived.
validations:
required: true
- type: textarea
id: who
attributes:
label: Who this affects
description: Just you, everyone on a particular agent, everyone using a particular workflow, or everyone. OpenSpec serves many agents and models, so this shapes whether a change fits.
placeholder: Anyone archiving more than a handful of changes.
validations:
required: true
- type: textarea
id: tried
attributes:
label: What you tried
description: Existing commands, flags, or workarounds you reached for, and why they fell short.
validations:
required: false
- type: textarea
id: proposal
attributes:
label: What you have in mind
description: Optional. A sketch is fine — we will agree on the approach before anyone builds it.
validations:
required: false
-30
View File
@@ -1,30 +0,0 @@
Closes #
<!--
No issue yet? Every change starts with one (CONTRIBUTING step 1).
Run `openspec feedback "your report"` to submit immediately, or open one here:
https://github.com/Fission-AI/OpenSpec/issues/new/choose
Already discussed instead of filed? Replace the line above with a link to the discussion.
-->
## What this changes
<!-- What was wrong or missing, and what the new behavior is. Plain language. -->
## How you verified it
<!--
The failing-then-passing test, repro steps, or before/after output.
Run the core checks for code changes:
pnpm build && pnpm test && pnpm exec tsc --noEmit && pnpm lint
-->
## Notes
<!-- Optional: scope limits, follow-ups, anything non-blocking. -->
---
- [ ] Ran `pnpm changeset` if this affects users, and committed the file
- [ ] If a coding agent wrote this, named the agent and model in the Notes section, and verified the result myself
-41
View File
@@ -184,11 +184,6 @@ jobs:
- name: Setup Nix cache
uses: DeterminateSystems/magic-nix-cache-action@84c0677f58dcedf3b91f8223ce36a9ea5b3c84b7 # v15
with:
# Dependabot runs cannot access the FlakeHub credentials available to
# regular CI, so keep those runs on the GitHub Actions cache.
use-flakehub: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && 'disabled' || 'no-preference' }}
use-gha-cache: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && 'enabled' || 'no-preference' }}
# Run the update script before `nix build`, not after. The script recomputes
# the pnpmDeps hash from pnpm-lock.yaml and rewrites flake.nix in place, so a
@@ -215,42 +210,6 @@ jobs:
if: always()
run: git checkout -- flake.nix || true
- name: Test downstream overlay composition
run: |
# Interpolation belongs to Nix, not the shell.
# shellcheck disable=SC2016
nix eval --impure --expr '
let
flake = builtins.getFlake (toString ./.);
system = builtins.currentSystem;
pkgs = import flake.inputs.nixpkgs {
inherit system;
overlays = [ flake.overlays.default ];
};
composed = import flake.inputs.nixpkgs {
inherit system;
overlays = [
flake.overlays.default
(_final: prev: {
nodejs_22 = prev.nodejs_22.overrideAttrs (_: {
pname = "openspec-test-nodejs";
});
})
];
};
overridden = pkgs.openspec.overrideAttrs (_: { version = "0.0.0-test"; });
in
assert pkgs.openspec.drvPath == flake.packages.${system}.default.drvPath;
assert pkgs.openspec.drvPath == flake.packages.${system}.openspec.drvPath;
# stdenv selects the dev output of multi-output native build inputs.
assert builtins.any (input: input.drvPath == composed.nodejs_22.drvPath)
composed.openspec.nativeBuildInputs;
assert composed.openspec.drvPath != pkgs.openspec.drvPath;
assert overridden.version == "0.0.0-test";
assert overridden.pnpmDeps.version == "0.0.0-test";
true
'
- name: Build with Nix
run: nix build
-88
View File
@@ -1,93 +1,5 @@
# @fission-ai/openspec
## 1.14.0
### Minor Changes
- [#883](https://github.com/Fission-AI/OpenSpec/pull/883) [`c879d13`](https://github.com/Fission-AI/OpenSpec/commit/c879d13d5f5d045c532a08523316d2d74f2db99a) Thanks [@Code-Studio-Team](https://github.com/Code-Studio-Team)! - Add Code Studio as an `init` and `update` target, with project skills and `.prompt.md` commands under `.codestudio/`.
- [#1672](https://github.com/Fission-AI/OpenSpec/pull/1672) [`297092c`](https://github.com/Fission-AI/OpenSpec/commit/297092cb25d9831a408d2ce9bfd55daec1431b74) Thanks [@DarkskyX15](https://github.com/DarkskyX15)! - - **DeepSeek Harness** — `openspec init --tools dsh` (command-line id `dsh`) installs the OpenSpec workflow skills into `.dsh/skills/` for DeepSeek Harness. It is skills-only (no command adapter or command files): dsh discovers the generated `SKILL.md` files as its highest-priority project root and surfaces them through its skill catalog, `skill` tool, and `/openspec-*` user invocations.
- [#1961](https://github.com/Fission-AI/OpenSpec/pull/1961) [`3c3e6e3`](https://github.com/Fission-AI/OpenSpec/commit/3c3e6e3d423625ffe554ff050c09bb530f17dc5e) Thanks [@fresh-fx59](https://github.com/fresh-fx59)! - Add GigaCode as a supported `--tools` target, with skills in `.gigacode/skills/openspec-*/SKILL.md` and Markdown commands in `.gigacode/commands/opsx-<id>.md`.
- [#1211](https://github.com/Fission-AI/OpenSpec/pull/1211) [`3de7c72`](https://github.com/Fission-AI/OpenSpec/commit/3de7c72c267c40ff89809d2ae08b7bf6ac6faf8c) Thanks [@hu-qi](https://github.com/hu-qi)! - Add AtomCode support through `openspec init --tools atomcode`, with project skills in `.atomcode/skills/` and `/opsx-<id>` commands in `.atomcode/commands/`. Generated commands declare `args: optional` and receive `$ARGUMENTS` when the workflow reads invocation input, and `args: none` when it does not, so AtomCode runs them straight from the slash menu. Follows the selected workflow profile and delivery mode.
- [#1082](https://github.com/Fission-AI/OpenSpec/pull/1082) [`a7f08b8`](https://github.com/Fission-AI/OpenSpec/commit/a7f08b8a462db5eeddae4e0b03f427987e3806a2) Thanks [@Storm-Chaser](https://github.com/Storm-Chaser)! - ### New Features
- **GSD support**: Install OpenSpec workflows as project skills with `openspec init --tools gsd`.
- [#420](https://github.com/Fission-AI/OpenSpec/pull/420) [`070de01`](https://github.com/Fission-AI/OpenSpec/commit/070de01dfac4ea343747fbd37b9bb9e77acdf7d0) Thanks [@jeanduplessis](https://github.com/jeanduplessis)! - ### New Features
- **Amp support**: select `amp` during init to install OpenSpec workflows as project skills under `.agents/skills/`.
- [#2001](https://github.com/Fission-AI/OpenSpec/pull/2001) [`56528ea`](https://github.com/Fission-AI/OpenSpec/commit/56528ea454a926159d05eb7c9ec1b687d7544b56) Thanks [@clay-good](https://github.com/clay-good)! - ### New Features
- **Version reports** — Run `openspec version` to inspect the installed version and install type, or add `--check` and `--json` for structured update information that tools can consume.
- [#1352](https://github.com/Fission-AI/OpenSpec/pull/1352) [`d1642cb`](https://github.com/Fission-AI/OpenSpec/commit/d1642cb58cb4ce2cda0a140cf2322aded53f4e46) Thanks [@redknox](https://github.com/redknox)! - Add EasyCode support to init and update, with project-local skills and TOML commands invoked as `/opsx:<id>`.
- [#399](https://github.com/Fission-AI/OpenSpec/pull/399) [`ded99e2`](https://github.com/Fission-AI/OpenSpec/commit/ded99e27de71c32647ae7fc2f51112219420b5d5) Thanks [@ZEDce](https://github.com/ZEDce)! - Add `openspec list --archived` and `--all` to browse archived changes, including JSON output and sorting. Show archived changes separately in the `openspec view` dashboard.
- [#848](https://github.com/Fission-AI/OpenSpec/pull/848) [`5a360c2`](https://github.com/Fission-AI/OpenSpec/commit/5a360c2088ad3094a092c34993eab97b43c25124) Thanks [@Columpio](https://github.com/Columpio)! - ### New Features
- **Veai support**: select `veai` during init to install OpenSpec workflows as project skills under `.veai/skills/`.
- [#1439](https://github.com/Fission-AI/OpenSpec/pull/1439) [`f197804`](https://github.com/Fission-AI/OpenSpec/commit/f197804a38057eee2272952b74d89884a9d3b7a4) Thanks [@jmuchovej](https://github.com/jmuchovej)! - Expose OpenSpec as a reusable Nix overlay through `overlays.default`.
- [#1349](https://github.com/Fission-AI/OpenSpec/pull/1349) [`e232080`](https://github.com/Fission-AI/OpenSpec/commit/e232080d0943bd535388bdc2304b3301f1496226) Thanks [@0x6d6e647a](https://github.com/0x6d6e647a)! - Add Grok Build as a skills-only tool. Run `openspec init --tools grok` to install skills in `.grok/skills`, then invoke them with `/openspec-propose` and other skill names. Existing Grok installations are refreshed by `openspec update`.
- [#1738](https://github.com/Fission-AI/OpenSpec/pull/1738) [`781c7f9`](https://github.com/Fission-AI/OpenSpec/commit/781c7f9447b4eeb6fdc69fa745ff46f6168f3edf) Thanks [@clay-good](https://github.com/clay-good)! - Add Warp support through project-local skills. Select `warp` during init to install OpenSpec workflows in `.warp/skills`, invoke them with `/openspec-*`, and refresh them with `openspec update`. Skills remain available in every delivery mode.
- [#807](https://github.com/Fission-AI/OpenSpec/pull/807) [`c21d897`](https://github.com/Fission-AI/OpenSpec/commit/c21d897261b5daf0c61c49ccd0862288d4664db4) Thanks [@Million-mo](https://github.com/Million-mo)! - ### New Features
- **Dashboard workflow status**: `openspec view` now shows each active change's schema and which artifacts are done, ready, blocked, or skipped. Task progress remains visible if a workflow cannot be loaded. Thanks to @Million-mo for the original contribution in [#807](https://github.com/Fission-AI/OpenSpec/issues/807).
### Patch Changes
- [#1977](https://github.com/Fission-AI/OpenSpec/pull/1977) [`7728194`](https://github.com/Fission-AI/OpenSpec/commit/772819417a2aa8a90cd50743139f402261628d21) Thanks [@clay-good](https://github.com/clay-good)! - The `openspec-archive-change` skill no longer tells the agent to run the `openspec-sync-specs` skill when that skill is not installed. It merges the delta specs into the main specs itself instead, as the `/opsx:archive` command already did ([#1975](https://github.com/Fission-AI/OpenSpec/issues/1975)).
- [#1722](https://github.com/Fission-AI/OpenSpec/pull/1722) [`817cdb6`](https://github.com/Fission-AI/OpenSpec/commit/817cdb64be744d4ee65d1a9922b23edc0c4699b8) Thanks [@caseyg](https://github.com/caseyg)! - ### Bug Fixes
- IBM Bob now appears by its full product name in the tool picker and success messages. Existing `bob` selections, configuration, skills, and slash-command paths continue to work unchanged.
- [#1999](https://github.com/Fission-AI/OpenSpec/pull/1999) [`bda8556`](https://github.com/Fission-AI/OpenSpec/commit/bda85565ef974d07c1c202c0ac4b2613241dd184) Thanks [@clay-good](https://github.com/clay-good)! - Guide users through an AI-assisted migration from legacy `project.md` to `config.yaml`.
- [#1997](https://github.com/Fission-AI/OpenSpec/pull/1997) [`e70dcc7`](https://github.com/Fission-AI/OpenSpec/commit/e70dcc7c82a3b100145795d32ea9930dca7b4073) Thanks [@clay-good](https://github.com/clay-good)! - Guide proposal authors toward durable, behavior-based capability names.
- [#2018](https://github.com/Fission-AI/OpenSpec/pull/2018) [`81c2f9f`](https://github.com/Fission-AI/OpenSpec/commit/81c2f9fce30d103bd22377042af1d428453b0bbc) Thanks [@clay-good](https://github.com/clay-good)! - ### Bug Fixes
- **Apply edits the right task** — `openspec instructions apply --json` now gives each task its `sourcePath` and `line`. The apply workflow checks the checkbox at that location before marking the task done and rechecks progress afterward, so agents update the exact task, even when tasks span several files.
- **Archive stops on a failed spec sync** — When the spec sync inside `/opsx:archive` reports a blocking condition, such as a capability retirement it could not complete, the archive now stops and leaves the change in place instead of archiving it with the main specs unchanged. The same applies to bulk archive.
- **Aligned `openspec view` progress bars** — Active change names up to 48 characters now line up their progress bars instead of pushing each bar out of line.
- [#1969](https://github.com/Fission-AI/OpenSpec/pull/1969) [`9557b43`](https://github.com/Fission-AI/OpenSpec/commit/9557b43aaff05af8bd9862c4755f7ac7b7f43cf1) Thanks [@flcrom](https://github.com/flcrom)! - ### Bug Fixes
- Let store-only repositories run `openspec init` at the repo root to install integrations without changing the external-store config or creating local planning directories.
- [#2004](https://github.com/Fission-AI/OpenSpec/pull/2004) [`d4e1c77`](https://github.com/Fission-AI/OpenSpec/commit/d4e1c77ebae0bd96a7c649fa35edef997989450a) Thanks [@clay-good](https://github.com/clay-good)! - Warn that files listed for legacy cleanup are deleted entirely and ask users to back up custom content first. The `openspec/AGENTS.md` check detects the file by existence alone.
- [#1995](https://github.com/Fission-AI/OpenSpec/pull/1995) [`baad449`](https://github.com/Fission-AI/OpenSpec/commit/baad4494b48f1497ec2f73a457210c5567176942) Thanks [@clay-good](https://github.com/clay-good)! - Guide agents to keep project documentation and codebase facts out of `config.yaml` context.
- [#1978](https://github.com/Fission-AI/OpenSpec/pull/1978) [`1872982`](https://github.com/Fission-AI/OpenSpec/commit/187298289dc5a7a63df87425151981586cbe5d7e) Thanks [@clay-good](https://github.com/clay-good)! - The specs instruction now tells agents the 500-character requirement length that `openspec validate` flags as an informational hint, and how to stay under it when writing new requirements without splitting existing ones. The validator's too-long message now explains how to split a requirement too.
- [#1972](https://github.com/Fission-AI/OpenSpec/pull/1972) [`d28fb49`](https://github.com/Fission-AI/OpenSpec/commit/d28fb49c1ca901fe19fa443a56ede37ff8b8c61a) Thanks [@ryandemelo](https://github.com/ryandemelo)! - `show --json` now includes each requirement's and scenario's `name`, matching the header names archive uses, so JSON readers can cite a requirement without parsing the markdown again ([#1971](https://github.com/Fission-AI/OpenSpec/issues/1971)).
- [#2014](https://github.com/Fission-AI/OpenSpec/pull/2014) [`cf2859a`](https://github.com/Fission-AI/OpenSpec/commit/cf2859a52089dd6dd37f9b3388db90c2ca3f06e7) Thanks [@clay-good](https://github.com/clay-good)! - Fix `status --json` for store-backed changes: `actionContext.allowedEditRoots` now lists the project on the current path that declares the store alongside the store, so apply no longer stops on a store-only edit scope. When no project on the current path declares the store, the constraint tells the agent to ask which repository to edit instead of naming the store.
- [#1984](https://github.com/Fission-AI/OpenSpec/pull/1984) [`42671df`](https://github.com/Fission-AI/OpenSpec/commit/42671df890fab730058fee108a2090e7c1e491b9) Thanks [@Yi-111-a](https://github.com/Yi-111-a)! - Name the offending index when a `rules:` list is not an array of strings
A rule item containing an unquoted `": "` is valid-looking YAML but parses as a
mapping, so the artifact's whole rule set is dropped with only a stderr warning
naming the artifact. The warning now also names the index and the shape YAML
produced there, plus the quoting fix, so the bad item can be found without
bisecting the list by hand.
- [#1925](https://github.com/Fission-AI/OpenSpec/pull/1925) [`88692b3`](https://github.com/Fission-AI/OpenSpec/commit/88692b3bb42262d30172819936847ed10f42a98f) Thanks [@kevin9327](https://github.com/kevin9327)! - ### Bug Fixes
- **Change metadata** — Warn when `.openspec.yaml` contains unrecognized keys such as `skip_design`. Those keys were stripped with no signal, so `status` still demanded the design artifact and `validate --strict` exited 0. `status`, `validate`, and `archive` now name the ignored keys; `validate --strict` fails.
- [#2016](https://github.com/Fission-AI/OpenSpec/pull/2016) [`cd4f9e4`](https://github.com/Fission-AI/OpenSpec/commit/cd4f9e4a5f99e7b48f2c452ef0fa3769db4dde4a) Thanks [@huiq777](https://github.com/huiq777)! - Make `openspec completion uninstall zsh` hand `.zshrc` back exactly as `completion install zsh` found it. Uninstall stripped every blank line at the top of the file, so a `.zshrc` that started with blank lines lost them after an install/uninstall round trip, even when the OpenSpec block had been moved further down. Uninstall now drops only the separator line install added, and only when the block sits at the top of the file, matching the bash installer.
## 1.13.2
### Patch Changes
+8 -7
View File
@@ -35,8 +35,8 @@ For example, with a `context` field and the rule from the top of this page, here
<!-- From your config.yaml: context -->
<project_context>
Designs and tasks must cover Windows, macOS, and Linux
Write all artifacts in Spanish
Tech stack: TypeScript, Node.js
Domain: e-commerce platform
</project_context>
<!-- From your config.yaml: rules for tasks -->
@@ -74,17 +74,18 @@ The last column is exact, so a field reaches only the steps listed there. In par
### context
`context` is background the agent receives when it creates an artifact, applies tasks, or archives a change.
`context` is what the agent should know up front when planning a change, whether it's creating an artifact, applying tasks, or archiving:
```yaml
context: |
We ship cross-platform. Designs and tasks must cover Windows, macOS, and Linux
Write all artifacts in Spanish
We ship cross-platform; designs and tasks must cover Windows, macOS, and Linux
Tech stack: TypeScript, Node.js, Commander.js
We use conventional commits
```
Use `context` for facts and constraints that should shape workflow output. Keep durable project documentation in the project's documentation files, and leave out anything the agent can learn by reading the code.
This is planning context, not project documentation. Add a fact when it should shape every plan, like the cross-platform line above. Leave out anything the agent can learn by reading the code.
**Another language**: because context reaches every artifact, you can add `Write all artifacts in Spanish.` to instruct the agent to write proposal, spec, design, and tasks artifacts in Spanish.
**Another language**: because context reaches every artifact, it's also how you change the output language. One line, like `Write all artifacts in Spanish.`, switches every proposal, spec, and tasks file the workflows write.
### rules
-6
View File
@@ -162,9 +162,3 @@ Sharing a schema means copying its folder.
- **From the community**: the [community catalog](https://github.com/Fission-AI/OpenSpec/blob/main/docs/customization.md#community-schemas) lists shared schemas. Copy one into `openspec/schemas/<name>` and it works like your own.
We're working on a schema registry, public and private, so schemas can be installed by name instead of copied by hand.
### Use OpenSpec with Superpowers
The community-maintained [`superpowers-bridge`](https://github.com/JiangWay/openspec-schemas/tree/main/superpowers-bridge) schema connects OpenSpec artifacts to [Superpowers](https://github.com/obra/superpowers) execution skills. Follow the bridge's installation and compatibility notes before copying it into your project.
The bridge is released outside OpenSpec. OpenSpec does not test or version its Superpowers integration.
+1 -22
View File
@@ -2,7 +2,7 @@
> The two-minute pass that catches wrong turns before they're code.
<!-- Partial draft: the plan-review sections are still headings only. -->
<!-- Skeleton: headings only. -->
## The two-minute pass
@@ -13,24 +13,3 @@
## Pushing back
## Advanced: verify after apply
Before archiving, check that the code does what the scenarios describe. The optional
[verify skill](../reference/skills.md#openspec-verify-change) can help find gaps.
### Keep a record when checks are hard to track
Use the change's `tasks.md` or an existing test report. For each check, record:
- **Scenario**: the requirement and scenario it checks, with a link to that version of the spec.
- **Check**: the test or manual check and what should happen.
- **Result**: pass, fail, not run, or unknown. Link to the original run or dated observation.
- **Tested version**: the code revision or build tested, and where it ran.
### Review the results
- **Open the source.** Confirm the result in the linked run or report. A checked task or an agent's summary alone does not prove the test passed.
- **Look for gaps.** Check that every scenario has a result. A passing test on one device or environment does not cover another. Keep missing and failed checks visible.
- **Check for changes.** Rerun checks affected by changes to the requirements, code, or environment. Unrelated documentation edits may leave earlier results valid.
**Archiving does not enforce these checks.** If passing results are required for
release, enforce that in your CI or release process.
-8
View File
@@ -15,12 +15,4 @@ once the prose lands. -->
## Migrating a project
### Back up custom content before cleanup
Files listed under **Files to remove** are deleted entirely. Back up any custom content before accepting cleanup.
- **`openspec/AGENTS.md`**: detected by existence alone; cleanup does not inspect its contents.
- **Root-level `AGENTS.md`, `CLAUDE.md`, and other config files**: cleanup removes OpenSpec marker blocks and preserves content outside those blocks.
- **Legacy command directories**: cleanup preserves files it does not recognize as generated commands.
## Behavior differences
-17
View File
@@ -223,23 +223,6 @@ No active changes. Create one with: openspec new change <name> --store team-plan
- **Commit it**: teammates who clone your project get the line too. They still need the store registered on their machine ([step 3 of Set up a store](#set-up-a-store)), or OpenSpec errors and tells them to register it.
- **Next to real folders**: if your project also has `specs/` or `changes/` folders, OpenSpec uses those and ignores the line, with a warning.
### Install integrations in a store-only repo
Run init from the code repo's root to install AI tool integration files without moving planning back into that repo:
```bash
# inside web-app, at the repository root
openspec init --tools claude
```
- **Integration files**: written in the code repo.
- **`openspec/config.yaml`**: preserved byte-for-byte, including the `store:` line.
- **`openspec/specs/` and `openspec/changes/`**: not created in the code repo.
OpenSpec refuses this command from a subdirectory of the code repo. Run it from the repository root.
OpenSpec also refuses `--language` here because the language belongs in the external store's config. Run init in the store root or edit that config directly.
### `defaultStore` on your machine
Set it once if every project you work in uses the same store. OpenSpec falls back to it when it finds no flag, no local `openspec/` folder, and no `store:` line:
+8 -157
View File
@@ -50,7 +50,6 @@ Your agent runs most of these during the workflow.
| Command | What it does |
|---|---|
| [`openspec version`](#openspec-version) | Report the installed version and optionally check for an update. |
| [`openspec feedback`](#openspec-feedback) | Submit feedback about OpenSpec. |
| [`openspec completion`](#openspec-completion) | Install or generate shell completions. |
@@ -78,21 +77,6 @@ openspec init --tools none # openspec/ structure only, no tool files
With no `--tools`, init prompts you to pick tools in an interactive terminal. Outside one, it sets up the tools it detects in the project. With none detected it exits 1 and lists the valid ids.
**Store-only repositories**
When `openspec/config.yaml` contains a `store:` line and the repo has no local specs or changes, run init from the repository root:
```bash
# install Claude Code integration files in the code repo
openspec init --tools claude
```
- **Integration files**: written in the code repo.
- **`openspec/config.yaml`**: preserved byte-for-byte.
- **`openspec/specs/` and `openspec/changes/`**: not created in the code repo.
Running init from a subdirectory exits 1 and tells you to run it from the repository root. `--language` also exits 1 because the language belongs in the external store's config. Run init in the store root or edit that config directly.
**Arguments**
| Argument | What it is |
@@ -104,7 +88,6 @@ Running init from a subdirectory exits 1 and tells you to run it from the reposi
| Flag | Effect |
|---|---|
| `--tools <tools>` | Comma-separated tool ids, `all`, or `none`. Skips the picker. Ids are listed in [Supported tools](supported-tools.md). |
| `--language <language>` | Add a language instruction to a new project config. Rejected when the repo's `store:` line points to an external store. |
| `--force` | Remove files from older OpenSpec layouts without asking. Interactive runs otherwise confirm the cleanup first. |
| `--profile <profile>` | Override the global config profile for this run: `core` (the standard workflow set) or `custom` (the workflows saved in global config). |
| `--no-animation` | Show a static welcome screen instead of the animated one. |
@@ -134,7 +117,7 @@ Restart your IDE for the new commands to take effect.
**Exit codes**
- `0`: setup completed.
- `1`: invalid `--tools` or `--profile` value, a non-interactive run with no tools detected and no `--tools`, or an invalid store-only invocation.
- `1`: invalid `--tools` or `--profile` value, or a non-interactive run with no tools detected and no `--tools`.
## openspec update
@@ -426,14 +409,12 @@ Config updated. Run `openspec update` in your projects to apply.
Lists changes, or specs with `--specs`.
```bash
openspec list # active changes, most recently modified first
openspec list --archived # archived changes
openspec list --all # active and archived changes
openspec list --specs # specs with requirement counts
openspec list --json # machine-readable, includes the resolved root
openspec list # changes, most recently modified first
openspec list --specs # specs with requirement counts
openspec list --json # machine-readable, includes the resolved root
```
Rows come from `openspec/changes/` and `openspec/specs/` under the resolved root. The default change listing skips `openspec/changes/archive/`.
Rows come from `openspec/changes/` and `openspec/specs/` under the resolved root. The `archive/` folder is skipped.
**Options**
@@ -441,8 +422,6 @@ Rows come from `openspec/changes/` and `openspec/specs/` under the resolved root
|---|---|
| `--specs` | List specs instead of changes. |
| `--changes` | List changes. This is the default. |
| `--archived` | List only archived changes. Can't be combined with `--specs`. |
| `--all` | List active and archived changes. Can't be combined with `--specs`. Takes precedence over `--archived`. |
| `--sort <order>` | `recent` (last modified first) or `name`. Default: `recent`. Specs always sort by name. |
| `--json` | Print JSON instead of the table. |
| `--store <id>` | Use a registered store as the OpenSpec root instead of the current project. |
@@ -456,16 +435,6 @@ Changes:
add-rate-limit No tasks just now
```
`--all` groups active and archived changes under separate headings. Each group uses the selected sort order:
```
Changes:
add-rate-limit No tasks just now
Archived Changes:
2026-08-10-add-login ✓ Complete 2d ago
```
```
Specs:
api requirements 1
@@ -491,9 +460,7 @@ Specs:
}
```
With `--archived` or `--all`, every change object includes an `archived` boolean. The combined array uses the selected sort order. An archived change can still have an `in-progress` status when its tracked task file has unchecked tasks. Without either flag, the JSON shape stays unchanged.
An empty listing prints `No active changes found.`, `No archived changes found.`, `No changes found.`, or `No specs found.` and still exits 0.
An empty listing prints `No active changes found.` or `No specs found.` and still exits 0.
A change is a directory directly under `openspec/changes/`. Unlike specs, changes cannot be nested in a namespace folder. A folder like `changes/mobile/` that only wraps a change (`changes/mobile/refresh-token/`) is listed with the status `not a change`, followed by a warning that names the nested directories. `--json` marks that entry with a `nested` array and adds a top-level `warnings` array. `show`, `status`, `validate` and `archive` refuse the folder with the same message. To fix it, move the change up and fold the namespace into its name:
@@ -574,11 +541,9 @@ A change with `--json` is delta-shaped:
"operation": "ADDED",
"description": "Add requirement: The API SHALL limit each client to 100 requests per minute.",
"requirement": {
"name": "Rate limit",
"text": "The API SHALL limit each client to 100 requests per minute.",
"scenarios": [
{
"name": "Client exceeds the limit",
"rawText": "- **WHEN** a client sends its 101st request within a minute\n- **THEN** the API responds 429"
}
]
@@ -593,11 +558,9 @@ A change with `--json` is delta-shaped:
}
```
Each requirement carries its `name`, the header text after `Requirement:`. This is the name archive matches MODIFIED, REMOVED and RENAMED entries against. Each scenario carries its `name`, the header text after `Scenario:`. A closing `#` run on either header is not part of the name.
`--json --diff` keeps this top-level shape. A MODIFIED delta gains a `diff` string, a `warning` string, or both. Other operations are unchanged. An empty `diff` string means the main and delta blocks are textually identical.
A spec with `--json` lists its requirements with scenarios. Requirements and scenarios carry the same `name` fields as change JSON:
A spec with `--json` lists its requirements with scenarios:
```json
{
@@ -607,11 +570,9 @@ A spec with `--json` lists its requirements with scenarios. Requirements and sce
"requirementCount": 1,
"requirements": [
{
"name": "Health endpoint",
"text": "The API SHALL expose a health endpoint.",
"scenarios": [
{
"name": "Health check succeeds",
"rawText": "- **WHEN** a client requests GET /health\n- **THEN** the API responds 200"
}
]
@@ -643,9 +604,7 @@ Prints a one-screen dashboard of specs and changes.
openspec view # project summary in one screen
```
view prints the dashboard once and exits. It reads no keystrokes. Changes group by task progress: Draft (no tasks yet), Active (tasks underway, with a progress bar and percent), Completed (every task checked), and Archived. Specs list with requirement counts, largest first.
Archived changes appear by directory name in alphabetical order. They do not contribute to the Draft, Active, Completed, or Task Progress totals.
view prints the dashboard once and exits. It reads no keystrokes. Changes group by task progress: Draft (no tasks yet), Active (tasks underway, with a progress bar and percent), Completed (every task checked). Specs list with requirement counts, largest first.
**Options**
@@ -664,16 +623,11 @@ Summary:
● Draft Changes: 1
● Active Changes: 0 in progress
● Completed Changes: 0
● Archived Changes: 1
Draft Changes
────────────────────────────────────────────────────────────
○ add-rate-limit
Archived Changes
────────────────────────────────────────────────────────────
◦ 2026-08-10-add-login
Specifications
────────────────────────────────────────────────────────────
▪ api 1 requirement
@@ -685,21 +639,6 @@ Use openspec list --changes or openspec list --specs for detailed views
A `Task Progress` summary line appears when any change has tasks underway.
Each active change also shows its schema and artifact states below its task progress bar:
```text
└─ [spec-driven] proposal✓ specs→ design→ tasks✓
```
| Marker | Artifact state |
|---|---|
| `✓` | Its output exists. An existing tasks artifact is done even when its checklist is unfinished. |
| `→` | It is ready to create. |
| No marker | It is blocked by a missing dependency. |
| `(skipped)` | The change skips it. |
If a workflow cannot be loaded, view prints a warning and keeps that change's task progress visible. Run `openspec status --change <name>` to inspect the workflow separately. After `openspec view --store <id>`, pass the same `--store <id>` to status.
**Exit codes**
- `0`: dashboard printed.
@@ -1351,8 +1290,6 @@ With `--json`, each form returns one object. The artifact form starts:
and continues with `outputPath`, `existingOutputPaths`, the full `instruction` and `template` strings, `dependencies`, `unlocks`, and `root`. The `apply` form carries `contextFiles`, `progress`, `tasks`, `taskTrackingConfigured`, `state` (`blocked`, `ready`, `all_done`), and `instruction`.
Each `tasks` entry carries `id`, `description`, `done`, `sourcePath`, and `line`. `sourcePath` is the absolute path to the tracked file that supplied the task. `line` is the task checkbox's one-based line number in that file.
`taskTrackingConfigured` is always a boolean: `true` when the schema sets a non-null [`apply.tracks`](schemas/schema-yaml.md#tracks), even if no file matches, and `false` otherwise. If a matched tracking file cannot be read, `unavailableTrackingFiles` contains its absolute `path` and error `reason`. This field is omitted when every matched file is readable. Readable files still contribute to `tasks` and `progress`, but `state` cannot be `all_done` until every matched file is read.
**Exit codes**
@@ -2200,92 +2137,6 @@ In an interactive terminal, remove shows the workset and asks you to confirm. Wi
Removed workset 'checkout'. Member folders were not touched.
```
## openspec version
Reports the running OpenSpec version and how this copy was installed.
```bash
openspec version # local version and install details
openspec version --json # structured local report
openspec version --check # also check the registry for an update
openspec version --check --json # structured local and update report
```
Without `--check`, this command is local and does not contact a registry. It works outside an OpenSpec project. The existing `openspec --version` flag remains the shortest form and prints only the bare version number.
**Options**
| Flag | Effect |
|---|---|
| `--json` | Print one versioned JSON document instead of text. |
| `--check` | Check the configured registry for a newer release. |
**Output**
For a global npm install:
```text
OpenSpec 1.13.2 (npm, global)
```
The install scope is `global`, `project`, `temporary` for an ephemeral runner such as npx, or `source` for a checkout. OpenSpec omits details it cannot identify instead of guessing.
`--json` keeps unknown details as explicit `null` values:
```json
{
"schemaVersion": 1,
"version": "1.13.2",
"install": {
"location": "/opt/homebrew/lib/node_modules/@fission-ai/openspec",
"packageManager": "npm",
"scope": "global"
}
}
```
With `--check`, an available update adds the latest version and a command when OpenSpec can identify a safe command for that install:
```text
OpenSpec 1.13.2 (npm, global)
Update available: 1.14.0
npm install -g @fission-ai/openspec@latest
```
```json
{
"schemaVersion": 1,
"version": "1.13.2",
"install": {
"location": "/opt/homebrew/lib/node_modules/@fission-ai/openspec",
"packageManager": "npm",
"scope": "global"
},
"update": {
"status": "available",
"latest": "1.14.0",
"command": "npm install -g @fission-ai/openspec@latest",
"canSelfUpgrade": true
}
}
```
Update status values:
| Status | Meaning |
|---|---|
| `available` | The registry returned a safe version newer than the running version. |
| `current` | The check completed and found no newer version. |
| `disabled` | An existing privacy or update-check setting blocked registry access. `latest` is `null`. |
| `offline` | The registry was unavailable or returned an unusable response. `latest` is `null`. |
`DO_NOT_TRACK`, telemetry opt-outs, `OPENSPEC_NO_UPDATE_CHECK`, CI detection, and rejected non-HTTPS registry overrides disable the check. Disabled and offline checks still exit 0 because update availability is advisory. This command never upgrades OpenSpec; `canSelfUpgrade` only reports whether the existing `openspec update` path could safely upgrade this copy.
**Exit codes**
- `0`: the local report printed, including disabled or offline update checks.
- `1`: command syntax was invalid, such as the unsupported `--upgrade` option.
## openspec feedback
Submits feedback about OpenSpec.
@@ -59,7 +59,4 @@ affected_areas:
The file is validated whenever a command writes or reads it. A write that fails validation throws and writes nothing. Reading an existing file fails on invalid YAML, a field that breaks its contract, or a schema name that is not available. A missing file is not an error, and the change is treated as having no metadata.
Unlike [config.yaml](config-yaml.md), bad values are never dropped with a warning. A metadata error stops the command.
- **Unknown top-level keys**: OpenSpec ignores them. `status`, `instructions`, `validate`, and `archive` report that they have no effect. JSON output carries the warning in its structured result.
- **Strict validation**: `openspec validate --strict` treats an unknown-key warning as a failure.
Unlike [config.yaml](config-yaml.md), bad values are never dropped with a warning. A metadata error stops the command. The one exception is unknown top-level keys, which are ignored rather than rejected.
@@ -11,7 +11,7 @@ Each OpenSpec project keeps its config file at `openspec/config.yaml`, in the pr
| Key | Type | Required | Effect |
| --- | --- | --- | --- |
| `schema` | string | Yes | The workflow schema this project's changes follow |
| `context` | string | No | Injected into every artifact, apply, and archive |
| `context` | string | No | Injected into every artifact's instructions |
| `rules` | map: artifact ID → list of strings | No | Extra rules added to one artifact's built-in guidance |
| `operations` | map: operation → guidance list | No | Advisory guidance for apply and archive work |
| `store` | string | No | Fallback OpenSpec root when this openspec/ is config-only |
@@ -27,7 +27,7 @@ The workflow schema every change in this project follows. Valid values are `spec
### context
Free text injected into every artifact's instructions and supplied to apply and archive. The limit is 50KB, and a larger value is ignored with a warning.
Free text injected into every artifact's instructions. The limit is 50KB, and a larger value is ignored with a warning.
### rules
@@ -77,13 +77,14 @@ A filled-in config.yaml:
schema: spec-driven
context: |
Designs and tasks must cover Windows, macOS, and Linux
Write all artifacts in Spanish
Tech stack: TypeScript, React, Node.js
We use conventional commits
Domain: e-commerce platform
rules:
proposal:
- Keep proposals under 500 words
- Always state what is out of scope
- Always include a "Non-goals" section
tasks:
- Break tasks into chunks of max 2 hours
@@ -67,12 +67,9 @@ The template the agent receives as the output format ([templates/proposal.md](ht
## Capabilities
### New Capabilities
<!-- Capabilities being introduced. Name each capability for a cohesive system
behavior that can own related requirements as the system evolves. Do not name
implementation tasks or proposal sections. Avoid broad catch-all names. Use
kebab-case for path segments you introduce (e.g., user-auth or identity/user-auth)
that follow the project's existing spec organization. Each creates
specs/<capability-path>/spec.md. -->
<!-- Capabilities being introduced. Use kebab-case for path segments you introduce
(e.g., user-auth or identity/user-auth) that follow the project's existing
spec organization. Each creates specs/<capability-path>/spec.md. -->
- `<capability-path>`: <brief description of what this capability covers>
### Modified Capabilities
@@ -101,7 +98,7 @@ Sections:
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
- **Capabilities**: Identify which specs will be created or modified:
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Name each capability for a durable system behavior (for example, `user-auth`), not the work in this change (for example, `add-login-endpoint`). Choose a cohesive boundary that can own related requirements as the system evolves; avoid broad catch-all capabilities. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
- **Impact**: Affected code, APIs, dependencies, or systems.
@@ -208,7 +205,6 @@ Format requirements:
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
- Every requirement MUST have at least one scenario.
- Keep each requirement's description (the text between `### Requirement:` and its first scenario) to 500 characters or fewer. `openspec validate` flags longer descriptions once they reach the main spec. This is an informational hint, not an error. When writing a new requirement, state one behavior per requirement: move examples and edge cases into scenarios, and split a requirement that covers several behaviors into separate `### Requirement:` blocks, each with its own scenarios. Under MODIFIED, keep the existing requirement block whole; never split, trim or rewrite existing text just to meet the length.
New capabilities only: the delta spec's first section is `## Purpose` -
one or two sentences (50+ characters, or `openspec validate --strict`
+2 -2
View File
@@ -90,7 +90,7 @@ Implement a change proposal's tasks, working through the list until done or bloc
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name (`add-auth`), optional. If the target is ambiguous it lists the active change proposals and asks you to pick. |
| **Creates** | Code: the minimal changes each task calls for, in your project files. In the change proposal it checks off each finished task (`- [ ]` to `- [x]`) in the tracked file identified by `sourcePath` and one-based `line`. A schema may track tasks across multiple files. |
| **Creates** | Code: the minimal changes each task calls for, in your project files. In the change proposal it touches only the tasks file, checking off each finished task (`- [ ]` to `- [x]`). |
| **Response** | Progress per task, then an overall count (N/M tasks complete). All done: suggests `openspec-archive-change`. Blocked by missing artifacts: points to `openspec-continue-change`, or to `openspec status` and `openspec instructions` when that skill is not installed (the core profile leaves it out). Unclear tasks or errors: pauses and asks. |
## openspec-update-change
@@ -121,7 +121,7 @@ Move a finished change proposal to the archive.
| Contract | Description |
|---|---|
| **Arguments** | A change proposal name, optional. |
| **Creates** | Moves the change proposal folder to `openspec/changes/archive/YYYY-MM-DD-<name>/` (no date added if the name already starts with one). With your approval it first syncs outstanding delta specs. When `openspec-sync-specs` is installed, it runs that workflow. Otherwise, it merges the delta specs into the main specs itself. Never code. |
| **Creates** | Moves the change proposal folder to `openspec/changes/archive/YYYY-MM-DD-<name>/` (no date added if the name already starts with one). With your approval it first syncs outstanding delta specs via `openspec-sync-specs`. Never code. |
| **Response** | Warns and asks before archiving with incomplete artifacts or tasks, and asks whether to sync when delta specs exist. Ends with a summary: name, schema, archive location, spec sync status, and any warnings. |
## openspec-new-change
+11 -57
View File
@@ -15,31 +15,23 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
| Tool | `--tools` id | Skills | Skill invocation | Commands | Command invocation |
|---|---|---|---|---|---|
| Amazon Q Developer | `amazon-q` | `.amazonq/skills/` | `/openspec-apply-change` | `.amazonq/prompts/` | `@opsx-apply` |
| Amp | `amp` | `.agents/skills/` | `/openspec-apply-change` | none | none |
| Antigravity | `antigravity` | `.agents/skills/` | `/openspec-apply-change` | `.agents/workflows/` | `/opsx-apply` |
| AtomCode | `atomcode` | `.atomcode/skills/` | `/openspec-apply-change` | `.atomcode/commands/` | `/opsx-apply` |
| Auggie (Augment CLI) | `auggie` | `.augment/skills/` | `/openspec-apply-change` | `.augment/commands/` | `/opsx-apply` |
| IBM Bob | `bob` | `.bob/skills/` | `/openspec-apply-change` | `.bob/commands/` | `/opsx-apply` |
| Bob Shell | `bob` | `.bob/skills/` | `/openspec-apply-change` | `.bob/commands/` | `/opsx-apply` |
| Claude Code | `claude` | `.claude/skills/` | `/openspec-apply-change` | `.claude/commands/opsx/` | `/opsx:apply` |
| Cline | `cline` | `.cline/skills/` | `/openspec-apply-change` | `.clinerules/workflows/` | `/opsx-apply` |
| CodeArts | `codeartsagent` | `.codeartsdoer/skills/` | `/openspec-apply-change` | none | none |
| CodeBuddy Code (CLI) | `codebuddy` | `.codebuddy/skills/` | `/openspec-apply-change` | `.codebuddy/commands/opsx/` | `/opsx:apply` |
| Code Studio | `codestudio` | `.codestudio/skills/` | `/openspec-apply-change` | `.codestudio/prompts/` | `/opsx-apply` |
| Codex | `codex` | `.agents/skills/` | `$openspec-apply-change` | none | none |
| Continue | `continue` | `.continue/skills/` | `/openspec-apply-change` | `.continue/prompts/` | `/opsx-apply` |
| CoStrict | `costrict` | `.cospec/skills/` | `/openspec-apply-change` | `.cospec/openspec/commands/` | `/opsx-apply` |
| Crush | `crush` | `.crush/skills/` | `/openspec-apply-change` | `.crush/commands/opsx/` | `/opsx:apply` |
| Cursor | `cursor` | `.cursor/skills/` | `/openspec-apply-change` | `.cursor/commands/` | `/opsx-apply` |
| DeepSeek Harness | `dsh` | `.dsh/skills/` | `/openspec-apply-change` | none | none |
| Devin Desktop (formerly Windsurf) | `devin` | `.devin/skills/` | `/openspec-apply-change` | `.devin/workflows/` | `/opsx-apply` |
| EasyCode | `easycode` | `.easycode/skills/` | `/openspec-apply-change` | `.easycode/commands/opsx/` | `/opsx:apply` |
| Factory Droid | `factory` | `.factory/skills/` | `/openspec-apply-change` | `.factory/commands/` | `/opsx-apply` |
| ForgeCode | `forgecode` | `.forge/skills/` | `/openspec-apply-change` | none | none |
| Gemini CLI | `gemini` | `.gemini/skills/` | `/openspec-apply-change` | `.gemini/commands/opsx/` | `/opsx:apply` |
| GigaCode | `gigacode` | `.gigacode/skills/` | `/openspec-apply-change` | `.gigacode/commands/` | `/opsx-apply` |
| GitHub Copilot | `github-copilot` | `.github/skills/` | `/openspec-apply-change` | `.github/prompts/` | `/opsx-apply` |
| Grok Build | `grok` | `.grok/skills/` | `/openspec-apply-change` | none | none |
| GSD | `gsd` | `.agents/skills/` | ask for `openspec-apply-change` | none | none |
| Hermes Agent | `hermes` | `.hermes/skills/` | `/openspec-apply-change` | none | none |
| iFlow | `iflow` | `.iflow/skills/` | `/openspec-apply-change` | `.iflow/commands/` | `/opsx-apply` |
| Junie | `junie` | `.junie/skills/` | `/openspec-apply-change` | `.junie/commands/` | `/opsx-apply` |
@@ -55,8 +47,6 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
| Qoder | `qoder` | `.qoder/skills/` | `/openspec-apply-change` | `.qoder/commands/opsx/` | `/opsx:apply` |
| Qwen Code | `qwen` | `.qwen/skills/` | `/openspec-apply-change` | `.qwen/commands/` | `/opsx-apply` |
| Trae | `trae` | `.trae/skills/` | `/openspec-apply-change` | `.trae/commands/` | `/opsx-apply` |
| [Veai](https://veai.ru/docs/veai/download) | `veai` | `.veai/skills/` | `/openspec-apply-change` | none | none |
| Warp | `warp` | `.warp/skills/` | `/openspec-apply-change` | none | none |
| ZCode | `zcode` | `.zcode/skills/` | `/openspec-apply-change` | `.zcode/commands/opsx/` | `/opsx:apply` |
| Zoo Code | `roocode` | `.roo/skills/` | `/openspec-apply-change` | `.roo/commands/` | `/opsx-apply` |
| Other / Universal | `agents` | `.agents/skills/` | `/openspec-apply-change` | none | none |
@@ -64,21 +54,14 @@ The id goes to `openspec init --tools <id>` to skip the picker ([CLI](cli.md)).
- **Skill invocation**: whether a tool registers skills as typed entries is the tool's
own behavior. The column shows the spelling OpenSpec uses in generated files and in
the hint init prints. Check your tool's docs if typing it does nothing.
- **Command file formats**: most tools take `.md` command files. EasyCode and Gemini
CLI take `.toml`, Continue `.prompt`, and Code Studio, Kiro, and GitHub Copilot
`.prompt.md`. The spelling you type is the same either way.
- **Command file formats**: most tools take `.md` command files. Gemini CLI takes
`.toml`, Continue `.prompt`, Kiro and GitHub Copilot `.prompt.md`. The spelling you
type is the same either way.
## Per-tool notes
A tool not listed here behaves exactly as its row reads.
### Amp
- **Project skills**: Amp reads OpenSpec skills from `.agents/skills/`.
- **No command files**: Amp runs skills directly, so init skips command generation.
- **Shared folder**: Amp shares `.agents/skills/` with Antigravity, Codex, Zed Agent,
and the `agents` target. OpenSpec writes the skill tree once.
### Antigravity
- **Current folder**: Antigravity v1.20.5 and later read workspace skills and
@@ -86,8 +69,8 @@ A tool not listed here behaves exactly as its row reads.
- **Legacy folder**: after OpenSpec writes replacements, it removes equivalent
generated files from `.agent/`. Custom files and changed generated files stay in
`.agent/` for you to review.
- **Shared skills**: Antigravity shares `.agents/skills/` with Amp, Codex, Zed Agent,
and the `agents` target. OpenSpec writes that skill tree once while still writing
- **Shared skills**: Antigravity shares `.agents/skills/` with Codex, Zed Agent, and
the `agents` target. OpenSpec writes that skill tree once while still writing
Antigravity commands to `.agents/workflows/`.
### Cline
@@ -105,26 +88,13 @@ Skills stay in `.cline/skills/`.
describes both interfaces.
- **No command files**: Codex runs skills directly, so init skips commands even when
delivery includes them and prints `Commands skipped for: codex (uses skills)`.
- **Shared folder**: Codex skills land in `.agents/skills/`, the same tree Amp,
Antigravity, Zed Agent, and the `agents` target use. Selecting more than one keeps a
single compatible tree, and its handoffs spell both `$openspec-*` and `/openspec-*`
when Codex owns it.
- **Shared folder**: Codex skills land in `.agents/skills/`, the same tree Antigravity,
Zed Agent, and the `agents` target use. Selecting more than one keeps a single
compatible tree, and its handoffs spell both `$openspec-*` and `/openspec-*` when
Codex owns it.
- **Legacy path**: skills installed under `.codex/skills/` by older versions are
migrated on the next `openspec update`.
### DeepSeek Harness
- **Project root**: DSH uses the nearest `.git` ancestor, or the current directory
outside Git. Run `openspec init --tools dsh` there. For a nested OpenSpec project,
add the absolute path to its `.dsh/skills/` directory to DSH's `customSkillDirs`.
Git-root skills still win if names overlap
([upstream discovery rules](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/skill/skill-filesystem)).
- **Priority**: `.dsh/skills/` takes precedence over same-named skills in
`.agents/skills/`.
- **Delivery**: use `skills` or `both`. With `commands`, no DSH workflows are
installed. Change delivery with `openspec config profile`, then rerun
`openspec init --tools dsh`.
### Devin Desktop (formerly Windsurf)
- **Two agents**: command files in `.devin/workflows/` work only in Devin Desktop.
@@ -144,15 +114,6 @@ Skills stay in `.cline/skills/`.
`/openspec-<skill>`. If a skill disappears while its file still exists, run
`/skills reload`, then `/skills info openspec-propose` to confirm discovery.
### GSD
- **Project skills**: GSD reads OpenSpec workflows from
[`.agents/skills/`](https://github.com/open-gsd/gsd-pi/blob/main/docs/user-docs/skills.md).
- **Invocation**: ask GSD to use the `openspec-<workflow>` skill. GSD can also select
a matching skill through its skill discovery setting.
- **No subagent files**: [`.gsd/agents/`](https://github.com/open-gsd/gsd-pi/blob/main/docs/user-docs/subagents.md)
contains GSD subagent definitions. OpenSpec does not write workflow skills there.
### Hermes Agent
Hermes loads skills only from `~/.hermes/skills/` by default. Add the project's
@@ -166,13 +127,6 @@ init prints this reminder after install.
- **Safe across projects**: a commands-only delivery leaves the global skills in
place, so one project's setting cannot remove skills another project uses.
### Warp
- **Skills always**: skills go to `.warp/skills/` even when delivery is `commands`,
because Warp has no command files and invokes skills directly.
- **What OpenSpec claims**: only `.warp/skills/`. Warp settings and `WARP.md` are
not created or edited.
### Other / Universal (shared `.agents` skills)
- **When it fits**: any tool that reads the shared `.agents/skills/` folder,
@@ -180,7 +134,7 @@ init prints this reminder after install.
assistant is not listed. The init picker's search box finds it by `universal`,
`other`, `generic`, `custom`, `proprietary`, `unlisted`, `unsupported`,
`vendor-neutral`, or `agents.md`.
- **Alongside other targets**: Amp, Antigravity, Codex, Zed Agent, and this target share
- **Alongside other targets**: Antigravity, Codex, Zed Agent, and this target share
one physical skill tree. OpenSpec records one writer in `.openspec-target` and
writes the tree once per run. Each tool's separate command files are still
generated.
-13
View File
@@ -158,19 +158,6 @@ Step through what archiving does:
Git is a separate concern. Commit the change folder with the code, and nothing else about your workflow changes.
### Keep or prune archived changes
`openspec/changes/archive/` keeps the proposal, design, tasks, and delta for each finished change. In the archive flow above, the delta has already updated `openspec/specs/`.
- **Keep the whole change folder** when you want a self-contained record in the current checkout.
- **Remove an archived change's `specs/` folder** when Git is your spec history. Commit the archive first, then delete `openspec/changes/archive/<change>/specs/`. The proposal, design, and tasks remain in the checkout. `openspec validate --archived` still works because it checks task completion, not applied deltas.
- **Remove the whole change folder** only when you no longer need its proposal, design, or task history in the checkout. The current specs do not change, but `openspec validate --archived` and searches of the checkout no longer include that change.
> [!WARNING]
> Keep the archive commit in your repository history if you want Git to retain the deleted files. Squashing the archive and cleanup commits together removes that intermediate snapshot.
OpenSpec does not prune archived changes automatically or provide a retention setting.
## Going further
- [Delta specs](../reference/schemas/spec-driven/index.md#delta-specs-specmd): how to write the behavior changes in a delta spec.
-16
View File
@@ -34,22 +34,6 @@ Re-running init is safe:
- Running init again with a new tool selected adds that tool.
- The `--tools` flag skips the picker ([CLI reference](../reference/cli.md)).
### Migrate an existing `project.md`
Init does not copy legacy `openspec/project.md` into `config.yaml`. It keeps the file and prints an AI-assisted migration request.
In your AI chat:
```
Review openspec/project.md and migrate its useful content to openspec/config.yaml.
Keep context concise: include only project-wide facts needed during artifact creation, apply, and archive.
Move artifact-specific guidance into rules for the matching artifacts.
Move guidance for apply or archive into the matching operations entry.
Leave out generic, outdated, or verbose material. Do not delete project.md.
```
Review `config.yaml`, then delete `project.md` when ready.
## What init installs
Running init creates two things in your project:
+2 -2
View File
@@ -52,13 +52,13 @@ deliberately remains the compatibility bare array documented in §4.13:
`warnings` (omitted when empty) reports directories under `changes/` that are not changes. Today the only code is `nested_change_directory`: a namespace folder wrapping change directories, which OpenSpec cannot address because a change is always a directory directly under `changes/`. The same entry carries `nested` on the listed change, whose `status` is then meaningless. Do not treat such an entry as a change; report the message and leave the directories alone.
### 4.2 `show <item> --json`
Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }`. A requirement, in a spec or in a change delta's `requirement`/`requirements`, is `{ "name", "text", "scenarios": [ { "name", "rawText" } ] }`. A requirement `name` is its header without `Requirement:` and without a closing `#` run, the exact name archive matches MODIFIED/REMOVED/RENAMED entries against. A scenario `name` is its level-4 header without `Scenario:` and without a closing `#` run, the name the MODIFIED scenario-loss check compares.
Change: `{ "id", "title", "deltaCount", "deltas": [...], "root" }`. Spec: `{ "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }`.
### 4.3 `validate --json`
`{ "items": [ { "id", "type": "change"|"spec", "valid", "issues": [ { "level", "path", "message", "line"?, "column"? } ], "durationMs" } ], "summary": { "totals": {items,passed,failed}, "byType": {...} }, "version": "1.0", "root" }`. Exit 1 when any item fails.
### 4.4 `status --json`
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isPlanningComplete", "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }`. For a store-selected root, `allowedEditRoots` is `[<declaring project>, <store>]` when the nearest project on the current path is a config-only root whose `store:` pointer names that store, and `[<store>]` otherwise (including a global `defaultStore`), with a constraint telling the agent to ask which repository to edit. OpenSpec does not route a store's tasks to repos, so the declaring project is the current one, not every repo the change touches. `isPlanningComplete` means every non-skipped planning artifact exists; skipped artifacts count as satisfied without being created. It does not mean implementation tasks are complete. `isComplete` is retained as a compatibility alias with the same value. Each artifact's `requires` is its direct dependency ids (present for every status, so the transitive required set is computable even when the artifact is `done`); `missingDeps` appears only when `blocked`. The `artifacts` array is in dependency order, with the schema's `artifacts:` declaration order breaking ties between artifacts that become ready at the same time (never alphabetical), so the first `ready` entry is the artifact to write next; `missingDeps` uses that same order. `"skipped"` marks an artifact whose `generates` path is under `specs/` in a change whose `.openspec.yaml` declares `skip_specs: true`; it satisfies dependencies but must not be created. No active changes: `{ "changes": [], "message", "root" }`, exit 0.
`{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isPlanningComplete", "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }`. `isPlanningComplete` means every non-skipped planning artifact exists; skipped artifacts count as satisfied without being created. It does not mean implementation tasks are complete. `isComplete` is retained as a compatibility alias with the same value. Each artifact's `requires` is its direct dependency ids (present for every status, so the transitive required set is computable even when the artifact is `done`); `missingDeps` appears only when `blocked`. The `artifacts` array is in dependency order, with the schema's `artifacts:` declaration order breaking ties between artifacts that become ready at the same time (never alphabetical), so the first `ready` entry is the artifact to write next; `missingDeps` uses that same order. `"skipped"` marks an artifact whose `generates` path is under `specs/` in a change whose `.openspec.yaml` declares `skip_specs: true`; it satisfies dependencies but must not be created. No active changes: `{ "changes": [], "message", "root" }`, exit 0.
`--all` (batch, mutually exclusive with `--change` — combining them is an error with the `{ "changes": [], "root": null, "status": [d] }` null-shape): `{ "changes": [ <per-change status object, no per-change root>, ... ], "root" }`, sorted by change name. A change that fails to load contributes `{ "changeName", "status": [d] }` in place; the sweep continues, preserves the complete envelope, and exits 1 in both text and JSON modes. An invalid `--schema` fails the whole invocation with the null-shape, even when no changes exist.
-1
View File
@@ -7,7 +7,6 @@ Listed projects are maintained independently. Inclusion does not imply official
## Projects and resources
- **[OpenSpec Workbench](https://github.com/VeryComplexAndLongName/OpenSpec-UI)**: Running and supervising agents on OpenSpec changes.
- **[MySpec](https://myspec.dev)**: Cloud beta (account required) that conducts guided spec interviews and exports four-file bundles or OpenSpec-compatible changes. The service sends submitted content to third-party AI providers and is not designed for sensitive data.
- **[openspec-guard](https://github.com/guillaume-flambard/spec-guard)**: CLI and GitHub Action that reports which OpenSpec scenarios are covered by a Vitest or Jest test, without running the tests.
## Add your project
+74 -85
View File
@@ -15,98 +15,87 @@
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems f;
pkgsFor =
system:
import nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
in
{
overlays.default = final: _prev: {
openspec = final.stdenv.mkDerivation (finalAttrs: {
pname = "openspec";
version = (builtins.fromJSON (builtins.readFile ./package.json)).version;
src = final.lib.fileset.toSource {
root = ./.;
fileset = final.lib.fileset.unions [
./src
./bin
./schemas
./scripts
./test
./package.json
./pnpm-lock.yaml
./pnpm-workspace.yaml
./tsconfig.json
./build.js
./vitest.config.ts
./vitest.setup.ts
./eslint.config.js
];
};
pnpmDeps = final.fetchPnpmDeps {
inherit (finalAttrs) pname version src;
pnpm = final.pnpm_10;
fetcherVersion = 3;
hash = "sha256-gPGdwmj4oLb/j3D/BGNCaI0hFfrHKCjH4I71UYzmhfk=";
};
nativeBuildInputs = with final; [
installShellFiles
nodejs_22
npmHooks.npmInstallHook
pnpmConfigHook
pnpm_10
];
buildPhase = ''
runHook preBuild
pnpm run build
runHook postBuild
'';
dontNpmPrune = true;
# `openspec completion generate` renders a static command registry, so it
# needs no project and no network. Opting out of telemetry also disables
# the update check, keeping the build offline.
postInstall = final.lib.optionalString (final.stdenv.buildPlatform.canExecute final.stdenv.hostPlatform) ''
export OPENSPEC_TELEMETRY=0
completions=$(mktemp -d)
for shell in bash fish zsh; do
$out/bin/openspec completion generate "$shell" > "$completions/openspec.$shell"
done
installShellCompletion --cmd openspec \
--bash "$completions/openspec.bash" \
--fish "$completions/openspec.fish" \
--zsh "$completions/openspec.zsh"
'';
meta = with final.lib; {
description = "AI-native system for spec-driven development";
homepage = "https://github.com/Fission-AI/OpenSpec";
license = licenses.mit;
maintainers = [ ];
mainProgram = "openspec";
};
});
};
packages = forAllSystems (
system:
let
pkgs = pkgsFor system;
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib;
in
{
default = pkgs.openspec;
inherit (pkgs) openspec;
default = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "openspec";
version = (builtins.fromJSON (builtins.readFile ./package.json)).version;
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./src
./bin
./schemas
./scripts
./test
./package.json
./pnpm-lock.yaml
./pnpm-workspace.yaml
./tsconfig.json
./build.js
./vitest.config.ts
./vitest.setup.ts
./eslint.config.js
];
};
pnpmDeps = pkgs.fetchPnpmDeps {
inherit (finalAttrs) pname version src;
pnpm = pkgs.pnpm_10;
fetcherVersion = 3;
hash = "sha256-ifgjl6/g7wpvcF4Ly/p+rxUbNEKiXF2u69CmpUM0olg=";
};
nativeBuildInputs = with pkgs; [
installShellFiles
nodejs_22
npmHooks.npmInstallHook
pnpmConfigHook
pnpm_10
];
buildPhase = ''
runHook preBuild
pnpm run build
runHook postBuild
'';
dontNpmPrune = true;
# `openspec completion generate` renders a static command registry, so it
# needs no project and no network. Opting out of telemetry also disables
# the update check, keeping the build offline.
postInstall = lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) ''
export OPENSPEC_TELEMETRY=0
completions=$(mktemp -d)
for shell in bash fish zsh; do
$out/bin/openspec completion generate "$shell" > "$completions/openspec.$shell"
done
installShellCompletion --cmd openspec \
--bash "$completions/openspec.bash" \
--fish "$completions/openspec.fish" \
--zsh "$completions/openspec.zsh"
'';
meta = with pkgs.lib; {
description = "AI-native system for spec-driven development";
homepage = "https://github.com/Fission-AI/OpenSpec";
license = licenses.mit;
maintainers = [ ];
mainProgram = "openspec";
};
});
}
);
@@ -1,2 +0,0 @@
schema: spec-driven
created: 2025-12-29
@@ -1,31 +0,0 @@
## Why
Amp reads project skills from `.agents/skills/`, but OpenSpec does not list Amp in its tool picker or accept `amp` through `--tools`. Amp users can select the universal `.agents` target, but only if they already know how Amp discovers skills.
## What Changes
- Add Amp as a supported skills-only tool with `amp` as its tool id.
- Generate Amp's OpenSpec skills through the existing shared `.agents/skills/` pipeline.
- Detect Amp projects from `.amp/` and recognize Amp-owned OpenSpec skill trees during update.
- Document Amp's paths and invocation syntax in the docs-lab supported-tools reference.
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
- `ai-tool-paths`: define Amp's shared Agent Skills path and skills-only behavior.
## Impact
- `src/core/config.ts`: add the Amp tool metadata.
- `test/core/init.test.ts`, `test/core/update.test.ts`, and `test/core/available-tools.test.ts`: cover generation, refresh, and detection.
- `docs-lab/reference/supported-tools.md`: add Amp to the support matrix and shared-folder notes.
## Non-Goals
- Adding an Amp command adapter. Amp's supported project extension surface is Agent Skills.
- Adding a second Amp-specific skill generator or template set.
@@ -1,28 +0,0 @@
## ADDED Requirements
### Requirement: Amp skills integration
OpenSpec SHALL expose Amp as a supported skills-only tool that uses Amp's project Agent Skills directory.
#### Scenario: Selecting Amp
- **WHEN** the user selects Amp in `openspec init` or passes `--tools amp`
- **THEN** OpenSpec SHALL generate the active profile's skills under `.agents/skills/`
- **AND** the generated skills SHALL use `/openspec-<skill>` references
- **AND** OpenSpec SHALL NOT generate command files for Amp
#### Scenario: Detecting an Amp project
- **WHEN** a project contains an `.amp/` directory
- **THEN** OpenSpec SHALL detect Amp as an available tool
#### Scenario: Updating an Amp-owned skill tree
- **GIVEN** `.agents/skills/.openspec-target` names `amp`
- **WHEN** `openspec update` runs
- **THEN** OpenSpec SHALL refresh the Amp skill tree through the shared skill generator
#### Scenario: Sharing the Agent Skills directory
- **WHEN** Amp is selected with another tool that writes `.agents/skills/`
- **THEN** OpenSpec SHALL write one compatible skill tree rather than letting the tools overwrite each other
-20
View File
@@ -1,20 +0,0 @@
## 1. Tool support
- [x] 1.1 Add Amp to `AI_TOOLS` as a skills-only `.agents` target.
- [x] 1.2 Detect Amp projects from `.amp/` and preserve shared-root ownership.
## 2. Documentation
- [x] 2.1 Add Amp to the docs-lab supported-tools matrix.
- [x] 2.2 Document Amp's skills-only and shared-folder behavior.
## 3. Tests
- [x] 3.1 Cover Amp detection from `.amp/` and `.openspec-target`.
- [x] 3.2 Cover init generation, Agent Skills frontmatter, invocation syntax, and command skipping.
- [x] 3.3 Cover update of an Amp-owned shared skill tree.
## 4. Verification
- [x] 4.1 Validate `add-amp-support` in strict mode.
- [x] 4.2 Run targeted tests, lint, build, and the full test suite.
@@ -1,131 +0,0 @@
# Design: a read-only `openspec version` command
## Context
`openspec --version` is Commander's built-in version flag and intentionally prints only the package version. `src/core/version-check.ts` already knows how to locate the running package, classify several install layouts, select package-manager-specific update advice, enforce update-check privacy controls, and query a registry defensively. Those helpers currently serve `openspec update`, where a nullable return value is enough: either announce a newer release or continue silently.
The new command has a different contract. It must explain why no update was reported, and external tools need a stable JSON shape rather than terminal prose. That requires an additive command and a structured result from the existing version-check core; it does not require a second detection or networking implementation.
## Goals / Non-Goals
**Goals**
- Give people and tools one supported way to inspect the running version and install context.
- Keep the default command local and instant; network access requires `--check`.
- Return one stable JSON document suitable for editor extensions, GUIs, and scripts.
- Reuse the existing install-detection and registry-safety rules.
- Preserve `openspec --version` byte-for-byte for existing scripts.
**Non-Goals**
- Installing or upgrading OpenSpec; that work is tracked separately in #1989.
- Release notes, channels, prerelease selection, or background checks.
- Perfectly identifying every custom package-manager layout. Unknown values remain honest `null`s.
- Changing `openspec update` or its interactive upgrade offer.
## Decisions
### 1. Add a command; do not extend `--version`
`openspec --version` is a widely scripted, root-level flag whose bare output is useful precisely because it has no other fields. Commander also treats root flags differently from subcommands. A separate `openspec version` command creates room for options and structured output without changing the old contract.
### 2. Separate local inspection from the network check
`openspec version` and `openspec version --json` inspect only local process and package paths. `--check` is the sole trigger for registry access. This makes the default deterministic, fast, and safe in offline or air-gapped environments.
The command remains successful when checking is disabled or unavailable. Update availability is advisory, so disabled privacy settings and network failure are data states rather than command failures.
### 3. Use one versioned JSON envelope
The JSON response always starts with the same base fields:
```json
{
"schemaVersion": 1,
"version": "1.13.2",
"install": {
"location": "/path/to/@fission-ai/openspec",
"packageManager": "npm",
"scope": "global"
}
}
```
With `--check`, the response adds:
```json
{
"update": {
"status": "available",
"latest": "1.14.0",
"command": "npm install -g @fission-ai/openspec@latest",
"canSelfUpgrade": true
}
}
```
`schemaVersion` versions the document independently of the OpenSpec package. The `update` object is absent unless requested, so local callers do not need to distinguish "not checked" from a check outcome. Nullable fields are explicit when detection has no defensible answer.
Status values are deliberately small:
- `available`: a safe newer registry version was found.
- `current`: the check completed and found no newer version.
- `disabled`: policy prevented a request, including privacy opt-outs or a rejected registry.
- `offline`: a permitted request did not produce a usable answer, including timeouts and invalid responses.
### 4. Classify ownership before naming a package manager
Install scope is determined before package-manager ownership:
1. A source checkout reports `scope: "source"` and `packageManager: null`.
2. An ephemeral runner/cache reports `scope: "temporary"`.
3. A dependency owned by the current project reports `scope: "project"`.
4. A recognized global layout reports `scope: "global"`.
5. If no classification is defensible, the field is `null` rather than guessing.
The implementation should reuse `getInstallDir()`, `isSourceCheckout()`, `isEphemeralRunnerInstall()`, `isProjectLocalInstall()`, and `detectPackageManager()`, while adding one pure function that assembles the public install record. Detection stays separately unit-testable with POSIX and Windows paths.
### 5. Return a structured check result from the existing core
`getAvailableCliUpdate()` currently collapses four conditions into `null`: current, disabled, unreachable, and invalid response. Keep it as a compatibility wrapper for `openspec update`, but implement it over a new structured check function whose result maps directly to the four public statuses.
The structured function must share the current request implementation. It must not duplicate registry selection, TLS-only configured-registry behavior, redirect limits, timeouts, response-size limits, or safe-version validation.
### 6. Derive update guidance from existing decisions
The reported command and `canSelfUpgrade` value come from the same install classification used by `openspec update`. Refactor terminal-line builders only as needed to expose a pure structured recommendation; do not parse human-readable strings back into JSON.
`canSelfUpgrade` describes whether the existing safe self-upgrade mechanism could operate on this install. The version command never invokes that mechanism.
### 7. Keep incidental output away from JSON
The CLI already defers telemetry and completion notices for JSON runs. The command follows existing JSON error/output conventions and writes exactly one JSON document to stdout. Human-readable output may use multiple lines but remains uncolored when global color is disabled.
## Security and Privacy
- No network access occurs without `--check`.
- Existing privacy opt-outs continue to block the request.
- A rejected configured registry does not cause a fallback request to public npm.
- Registry-provided versions pass the current strict validator before display.
- Existing redirect, timeout, and response-size limits remain in force.
- Install paths are printed only in direct response to the user's command and are never sent as telemetry by this change.
- The command never executes the reported update command.
## Documentation
The implementation updates `docs-lab/reference/cli.md`, using the current docs-lab page structure and examples. It does not update the legacy `docs/cli.md` page.
## Risks / Trade-offs
- **Public schema commitment:** integrations may depend on field names and status values. `schemaVersion` and regression fixtures make future incompatible changes explicit.
- **Install detection is heuristic:** custom layouts may remain unknown. Returning `null` is less convenient but safer than incorrect update guidance.
- **Absolute path disclosure:** `install.location` can contain a user name. It appears only on explicit local invocation; callers that persist or transmit it are responsible for handling it as local environment data.
- **Status vocabulary:** `offline` also covers unusable registry responses, not only literal network loss. It is intentionally user-facing shorthand for "no usable remote answer" while logs/tests retain the detailed cause internally if needed.
## Migration Plan
This change is additive. Existing flags and commands retain their behavior. No stored data, configuration, or generated files require migration.
## Open Questions
None required for implementation. Review may rename a JSON field or status before approval; after release, incompatible changes require a new `schemaVersion`.
@@ -1,35 +0,0 @@
# Proposal
## Why
Editor extensions, GUIs, and scripts can read OpenSpec's bare version number, but they cannot ask how this copy was installed or whether an update is available. They must duplicate OpenSpec's install detection and update-check behavior, which produces inconsistent advice and makes integrations depend on human-oriented terminal output.
## What Changes
- Add `openspec version` as a read-only command that reports the installed version and install context without requiring an OpenSpec project.
- Add `openspec version --json` with a versioned, machine-readable response for integrations.
- Add an opt-in `--check` flag that queries the configured registry and reports whether an update is available, disabled, current, or temporarily unavailable.
- Reuse the existing privacy opt-outs, registry safeguards, package-manager detection, and update-command selection.
- Keep the existing `openspec --version` output and behavior unchanged for backward compatibility.
- Document the command in `docs-lab/reference/cli.md`; the legacy `docs/` tree is not updated.
## Capabilities
### New Capabilities
- `cli-version`: Report the installed OpenSpec version and install context, with an optional privacy-aware update check and stable JSON output.
### Modified Capabilities
None.
## Impact
- Public CLI: one additive `version` command with `--json` and `--check` options.
- Public machine interface: a new JSON document identified by `schemaVersion: 1`.
- Version-check core: separate update-check outcomes from the current nullable result so callers can distinguish disabled, current, and unavailable states.
- Tests: unit coverage for install classification and update outcomes, plus CLI end-to-end coverage for text/JSON output and backward compatibility.
- Documentation: `docs-lab/reference/cli.md` only, following the current docs-lab format.
- No new dependency, background network request, telemetry field, or automatic upgrade behavior.
Tracks [#1988](https://github.com/Fission-AI/OpenSpec/issues/1988); the issue remains open until implementation lands.
@@ -1,120 +0,0 @@
## ADDED Requirements
### Requirement: Report the installed version
The system SHALL provide an `openspec version` command that reports the running OpenSpec version and install context without requiring an OpenSpec project or contacting the network.
#### Scenario: Human-readable version report
- **WHEN** a user runs `openspec version`
- **THEN** the command reports the running OpenSpec version
- **AND** identifies the install's package manager and scope when they can be determined
- **AND** exits successfully without contacting a registry
#### Scenario: Machine-readable version report
- **WHEN** a user runs `openspec version --json`
- **THEN** stdout contains one valid JSON document
- **AND** the document contains `schemaVersion: 1`, `version`, and `install`
- **AND** `install` contains `location`, `packageManager`, and `scope`
- **AND** `scope` is one of `global`, `project`, `temporary`, or `source`
- **AND** values that cannot be determined are represented as `null`
#### Scenario: Source checkout
- **WHEN** the running CLI is a source checkout
- **THEN** the command reports `scope` as `source`
- **AND** it does not claim that a package manager owns the checkout
#### Scenario: Existing version flag remains compatible
- **WHEN** a user runs `openspec --version`
- **THEN** the command prints the same bare version string as before this capability was added
- **AND** no JSON or install metadata is added to that output
### Requirement: Check for an available update on request
The system SHALL contact the configured package registry only when `openspec version` receives `--check`, and SHALL report the outcome without making command success depend on registry availability.
#### Scenario: Update is available
- **WHEN** a user runs `openspec version --check`
- **AND** the registry reports a safe newer version
- **THEN** the command reports the latest version
- **AND** reports the appropriate update command only when one exists for this install
- **AND** human-readable output omits package-manager guidance when no such command exists
- **AND** reports whether the existing self-upgrade path can safely update this copy
- **AND** exits successfully
#### Scenario: Installed version is current
- **WHEN** a user runs `openspec version --check`
- **AND** the registry reports no version newer than the running version
- **THEN** the command reports the update status as `current`
- **AND** exits successfully
#### Scenario: Update check is disabled
- **WHEN** a user runs `openspec version --check`
- **AND** an existing OpenSpec privacy or update-check opt-out disables registry access
- **THEN** the command does not contact the registry
- **AND** reports the update status as `disabled`
- **AND** reports no latest version
- **AND** exits successfully
#### Scenario: Registry is unavailable
- **WHEN** a user runs `openspec version --check`
- **AND** the registry cannot be reached or returns an unusable response
- **THEN** the command reports the update status as `offline`
- **AND** reports no latest version
- **AND** exits successfully
#### Scenario: Machine-readable update result
- **WHEN** a user runs `openspec version --check --json`
- **THEN** the base version and install fields remain present
- **AND** the document contains an `update` object with `status`, `latest`, `command`, and `canSelfUpgrade`
- **AND** `status` is one of `available`, `current`, `disabled`, or `offline`
- **AND** unavailable values are represented as `null`
- **AND** stdout contains no text outside the JSON document
### Requirement: Preserve update-check safeguards
The version command SHALL use the same privacy, registry, timeout, response-size, redirect, and version-validation safeguards as OpenSpec's existing update check.
#### Scenario: Explicit privacy opt-out
- **WHEN** telemetry is disabled or `DO_NOT_TRACK`, `OPENSPEC_TELEMETRY`, or `OPENSPEC_NO_UPDATE_CHECK` disables outbound checks
- **AND** a user runs `openspec version --check`
- **THEN** the command performs no update-check request
- **AND** reports the update status as `disabled`
#### Scenario: Unsafe configured registry
- **WHEN** the configured registry is rejected by the existing registry safeguards
- **AND** a user runs `openspec version --check`
- **THEN** the command does not fall back to the public registry
- **AND** reports the update status as `disabled`
#### Scenario: Untrusted version response
- **WHEN** the registry response does not contain a version accepted by OpenSpec's existing version validator
- **THEN** the command does not print the untrusted value
- **AND** reports the update status as `offline`
### Requirement: Version reporting is read-only
The version command SHALL NOT install, upgrade, or modify OpenSpec, project files, or user configuration.
#### Scenario: Update is available
- **WHEN** `openspec version --check` reports an available update
- **THEN** it reports guidance only
- **AND** does not run a package manager or alter the installed copy
#### Scenario: Upgrade option is rejected
- **WHEN** a user runs `openspec version --upgrade`
- **THEN** the command reports that `--upgrade` is not supported
- **AND** does not attempt an upgrade
@@ -1,35 +0,0 @@
# Tasks
## 1. Model version and install information
- [x] 1.1 Add typed, pure install classification that reports location, package manager, and scope without guessing ownership for source or unknown layouts.
- [x] 1.2 Add POSIX and Windows unit cases for global, project, temporary, source, and unknown installs.
## 2. Expose structured update-check outcomes
- [x] 2.1 Refactor the existing update check to return `available`, `current`, `disabled`, or `offline` with structured latest-version and update-guidance fields.
- [x] 2.2 Keep `getAvailableCliUpdate()` as a compatibility wrapper so `openspec update` behavior does not change.
- [x] 2.3 Cover privacy opt-outs, rejected registries, timeouts, invalid responses, current versions, and available updates without weakening existing network safeguards.
## 3. Add the version command
- [x] 3.1 Add `openspec version` with `--json` and `--check` options and no project-root prerequisite.
- [x] 3.2 Emit one schema-versioned JSON document with explicit nulls for unknown values and no incidental stdout.
- [x] 3.3 Add human-readable output for local information and each update-check status.
- [x] 3.4 Reject `--upgrade` and other unsupported options without running an installer.
## 4. Verify compatibility and behavior
- [x] 4.1 Add CLI end-to-end coverage for text output, JSON output, `--check`, and execution outside an OpenSpec project.
- [x] 4.2 Prove `openspec --version` still emits only the bare version string.
- [x] 4.3 Prove JSON runs emit no telemetry notice, completion tip, color sequence, or extra stdout text.
## 5. Document and release
- [x] 5.1 Document `openspec version`, `--json`, and `--check` in `docs-lab/reference/cli.md` using the current docs-lab format; do not update the legacy `docs/` tree.
- [x] 5.2 Add a minor changeset for `@fission-ai/openspec`.
## 6. Final verification
- [x] 6.1 Run `pnpm build`, the focused version-check and CLI tests, `pnpm test`, `pnpm exec tsc --noEmit`, and `pnpm lint`.
- [x] 6.2 Run `openspec validate add-version-command --strict` and confirm every planning artifact is complete.
@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-07-11
@@ -1,109 +0,0 @@
## Context
Grok Build (CLI binary `grok`) is not a Claude/Codex-style command-file adapter target. Its documented extension model is skill-centric:
- project skills from `./.grok/skills/` (walked up to the repo root)
- user skills from `~/.grok/skills/`
- plugin skills and optional `[skills] paths` in config
- user-invocable skills appear as slash commands: `/<skill-name>`
- core TUI commands (`/plan`, `/model`, `/skills`, …) are built-in, not project-generated files
- no documented project-local `.grok/commands/` layout for custom OpenSpec command generation
Grok also has Claude/Cursor compatibility scanners that can free-ride existing `.claude/skills` or `.cursor/skills`. That is a personal workaround, not the product integration: OpenSpec should own a native `.grok` skills install so pure-Grok users and multi-tool projects get first-class init/update behavior.
OpenSpec already represents this shape:
- `AI_TOOLS` can advertise a `skillsDir`
- `init`/`update` install skills for any selected tool with `skillsDir`
- when command generation is attempted for a tool without an adapter, OpenSpec records `commandsSkipped`
## Goals / Non-Goals
**Goals:**
- Add Grok Build using the same narrow skills-only pattern as Kimi CLI / ForgeCode / Mistral Vibe
- Keep the implementation small: metadata, docs, focused regression test, changeset
- Align specs with the existing adapterless code path
**Non-Goals:**
- designing a Grok-specific command adapter without a documented project command-file surface
- relying on Claude/Cursor free-ride as the supported integration
- changing tool capability modeling or `delivery=commands` behavior for all adapterless tools (tracked in `add-tool-command-surface-capabilities`)
## Decisions
### 1. Represent Grok Build as an adapterless tool with `.grok`
Add a new `AI_TOOLS` entry:
```ts
{ name: 'Grok Build', value: 'grok', available: true, successLabel: 'Grok Build', skillsDir: '.grok' }
```
Rationale for IDs:
- `value: 'grok'` matches the CLI binary and the project directory `.grok` (same pattern as `claude` → `.claude`, `kimi` → `.kimi`)
- display name `Grok Build` matches xAI product naming
- alternatives considered: `grok-build` (product-accurate but inconsistent with other short tool IDs)
### 2. Do not add a Grok command adapter
No `src/core/command-generation/adapters/grok.ts`, and no registry change.
Rationale:
- skills are the documented custom extension surface and already become slash commands
- inventing `.grok/commands/...` would create OpenSpec behavior that cannot be justified against xAI docs
- existing adapterless path already skips command generation with an informational message
### 3. Document Grok by its real invocation surface
Grok docs in OpenSpec must use skill-name slash form:
- supported-tools: no generated command files; use skill-based `/openspec-*` invocations
- commands / how-commands-work: examples such as `/openspec-propose`, `/openspec-apply-change`
Do not claim generated `opsx-*` files or Claude-style `/opsx:propose` as Grok's primary surface.
### 4. Treat Claude free-ride as out-of-scope workaround, not design
Grok can discover Claude skills when compat scanners are enabled. Native `.grok` support remains required because:
- pure Grok users may never select Claude
- free-ride couples Grok to Claude layout and can be disabled via Grok config/env
- OpenSpec update tracks configured tools by skillsDir presence; free-ride never registers Grok
If both Claude and Grok are configured, duplicate skill discovery is acceptable; `.grok` remains the canonical OpenSpec target for Grok Build.
### 5. Keep behavior aligned with current adapterless tools
- skills are created whenever delivery includes skills
- command generation is skipped when no adapter exists
- init output reports `Commands skipped for: grok (no adapter)`
- update refreshes Grok when `.grok/skills/openspec-*` exists
## Test Strategy
Add one focused regression test in `test/core/init.test.ts`:
- configure `delivery=both`
- run init with `--tools grok`
- verify skills under `.grok/skills/...` (use `path.join` for expectations)
- verify no `.grok/commands` directory is created
- verify init log includes skipped command generation for `grok` with `(no adapter)` (use relaxed `.some()` matching, as in the Kimi follow-up commit)
That is enough 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 introduced
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| Users confuse Claude free-ride with native support | Document native `.grok` path; optional brief note that Claude compat is separate |
| `delivery=commands` still not capability-aware for skills-only tools | Accept same limitation as Kimi/ForgeCode/Vibe; capability work is separate |
| Duplicate skills when both Claude and Grok selected | Acceptable; document that Grok may see both trees |
| xAI later documents project command files | Skills-only remains correct today; adapter can be added later without breaking skills |
@@ -1,40 +0,0 @@
## Why
xAI Grok Build is a coding agent with a documented project skills root at `.grok/skills/`, and user-invocable skills surface as slash commands (`/<skill-name>`). OpenSpec does not yet list Grok Build as a supported tool, so users must free-ride on Claude/Cursor compat scanners or configure extra skill paths manually.
OpenSpec already supports adapterless skills-only tools (Kimi CLI, ForgeCode, Mistral Vibe). Grok Build should follow that pattern: install skills under `.grok/skills/` without inventing a command adapter for a project command-file surface that xAI docs do not define.
## What Changes
- Add Grok Build as a supported tool in `AI_TOOLS` with `value: 'grok'` and `skillsDir: '.grok'`
- Document Grok Build as a skills-only integration (no generated `opsx-*` command files; invoke via `/openspec-*` skill names)
- Align specs so `ai-tool-paths` and `cli-init` cover the Grok Build path and adapterless init behavior
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
- `ai-tool-paths`: define the `.grok` skills root for Grok Build
- `cli-init`: treat Grok Build as a supported adapterless selection that still generates skills and skips command-file generation
## Impact
- `src/core/config.ts` - add Grok Build tool metadata
- `docs/supported-tools.md` - add Grok Build row and tool id
- `docs/commands.md` - document `/openspec-*` skill invocations for Grok Build
- `docs/how-commands-work.md` - include Grok Build in slash-syntax table
- `docs/cli.md` - include `grok` in the supported `--tools` list
- `docs/troubleshooting.md` - list Grok Build among skills-only tools
- `test/core/init.test.ts` - cover Grok Build as an adapterless tool during init
- `.changeset/` - minor release note for the new tool
## Non-Goals
- Adding `src/core/command-generation/adapters/grok.ts`
- Defining a `.grok/commands/...` output path
- Relying on Claude/Cursor free-ride as the product integration
- Changing the broader delivery model for adapterless tools under `delivery=commands` (tracked separately in `add-tool-command-surface-capabilities`)
@@ -1,37 +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: Claude Code paths defined
- **WHEN** looking up the `claude` tool
- **THEN** `skillsDir` SHALL be `.claude`
#### Scenario: Cursor paths defined
- **WHEN** looking up the `cursor` tool
- **THEN** `skillsDir` SHALL be `.cursor`
#### Scenario: Windsurf paths defined
- **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: Grok Build paths defined
- **WHEN** looking up the `grok` tool
- **THEN** `skillsDir` SHALL be `.grok`
#### Scenario: Tools without skillsDir
- **WHEN** a tool has no `skillsDir` defined
- **THEN** skill generation SHALL error with message indicating the tool is not supported
@@ -1,43 +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
#### Scenario: Grok Build skips command-file generation
- **WHEN** the user selects Grok Build during initialization
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.grok'`
- **AND** command-file generation SHALL be skipped because no Grok adapter is registered
@@ -1,24 +0,0 @@
## 1. Tool Metadata
- [x] 1.1 Add `Grok Build` to `src/core/config.ts` with `value: 'grok'`, `successLabel: 'Grok Build'`, and `skillsDir: '.grok'` (alphabetically near related tools)
## 2. Documentation
- [x] 2.1 Update `docs/supported-tools.md` with a Grok Build row (`skillsDir` `.grok`, no command adapter; skill-based `/openspec-*` invocations) and add `grok` to the `--tools` list
- [x] 2.2 Update `docs/commands.md` to document Grok Build skill invocations such as `/openspec-propose`, `/openspec-apply-change`
- [x] 2.3 Update `docs/how-commands-work.md` slash-syntax table to include Grok Build (`/openspec-*` skill form)
- [x] 2.4 Update `docs/cli.md` so the supported `--tools` list includes `grok`
- [x] 2.5 Update `docs/troubleshooting.md` skills-only tool list to include Grok Build
## 3. Tests
- [x] 3.1 Add a targeted init regression test for `--tools grok` with `delivery=both`: skills under `.grok/skills/...`, no `.grok/commands`, and commands-skipped log for `grok` `(no adapter)` using relaxed log matching and `path.join` expectations
## 4. Release Notes
- [x] 4.1 Add a changeset noting Grok Build as a supported skills-only tool via `.grok/skills/`
## 5. Validation
- [x] 5.1 Validate the change artifacts with `openspec validate add-grok-build-skills-only-support --strict` (or project-equivalent)
- [x] 5.2 Run targeted tests (`test/core/init.test.ts` Grok case) and fix any regressions
@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-08-15
@@ -1,86 +0,0 @@
## Context
See [proposal.md](proposal.md#why).
OpenSpec already routes every skill-capable tool through one pipeline: `AI_TOOLS` metadata in `src/core/config.ts` drives tool detection (`available-tools.ts`), selection and validation (`init.ts`), skill path resolution (`shared/skill-paths.ts`), generation, version drift, and update. Tools that expose no custom command files simply have no `ToolCommandAdapter`, which `command-surface.ts` classifies as capability `none`.
DeepSeek Harness parses skills from fixed local roots (see the [upstream filesystem provider](https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/skill/skill-filesystem)): `<project>/.dsh/skills` (rank 100), `<project>/.agents/skills` (rank 200), and user-level `~/.dsh/skills` (rank 400). It discovers only one level (`<root>/<name>/SKILL.md` or `<root>/<name>.md`), requires `name` (kebab-case) and non-empty `description` frontmatter, tolerates extra fields, and exposes skills to the model through `<available_skills>` plus a `skill` tool; users can also trigger them with the `/name` gesture. OpenSpec's generated `SKILL.md` files already satisfy every dsh constraint, so no template or frontmatter changes are needed.
## Goals / Non-Goals
**Goals:**
- Add one `dsh` entry to `AI_TOOLS` that opts into the existing project-local skills pipeline.
- Make first-time setup, auto-detection, refresh, and profile/delivery drift work through existing generic code.
- Lock the dsh path and invocation behavior with focused tests.
**Non-Goals:**
- A dsh command adapter or any `.dsh/commands/` output — dsh has no file-based command surface.
- A global `~/.dsh/skills` install target — dsh has a higher-priority project root and OpenSpec manages per-project artifacts.
- Reclassifying dsh as `skills-invocable` in `command-surface.ts`; that belongs to the in-flight `add-tool-command-surface-capabilities` work. Until then dsh shares the current adapterless behavior of Rovo Dev CLI and Kimi Code.
- Changing generated skill templates or frontmatter.
## Decisions
### 1. Represent dsh as an adapterless, project-local tool entry
Add to `src/core/config.ts`:
```ts
{
name: 'DeepSeek Harness',
value: 'dsh',
available: true,
successLabel: 'DeepSeek Harness',
skillsDir: '.dsh',
},
```
`resolveToolSkillsDir()` then resolves to `<projectRoot>/.dsh/skills`, which is dsh's rank-100 project root. Nothing else in init/update/selection needs a code change because those paths derive from `AI_TOOLS`.
Alternative considered: write to `~/.dsh/skills` via `globalSkillsDir`. Rejected because the project root outranks the user root, keeps artifacts repo-local and reviewable, and matches OpenSpec's project-scoped update/removal semantics (MiniMax Code's global-only design exists to work around a tool that only reads the user root, which is not dsh's case).
### 2. Detect dsh from its `.dsh` directory
Use the existing `skillsDir` detection, which requires a directory. This recognizes both a bare `.dsh` project root and a populated `.dsh/skills` tree, but rejects a regular file named `.dsh`.
Explicit `detectionPaths` are unnecessary: `.dsh/skills` already implies a `.dsh` directory, and overrides accept file signals for tools that need them. Auto-detection identifies a tool root; it does not guarantee every child path is writable. A regular file at `.dsh/skills` remains a filesystem conflict reported during generation, as for other directory-based tools.
### 3. No command adapter; inherit capability `none`
`resolveCommandSurfaceCapability('dsh')` returns `none` because no adapter is registered. Consequences, all existing generic behavior:
- `delivery=both` / `skills`: skills generated; init reports `Commands skipped for: dsh (no adapter)`.
- `delivery=commands`: no dsh artifacts and the existing zero-artifact correction is printed.
Alternative considered: special-case dsh as `skills-invocable` like Codex so commands-only delivery keeps skills. Semantically dsh's skill tool + `/name` gesture are invocable, but the current shipped model only special-cases Codex; widening it here would duplicate the open `add-tool-command-surface-capabilities` change and expand this change's test matrix. Deferred deliberately.
### 4. Use the default `/openspec-*` skill reference spelling
dsh's user-facing `/name` gesture makes `/openspec-propose` a real, typeable invocation, so the default transformer (`getSkillReferenceTransformer` fallback) is correct. The model side can call the `skill` tool by name regardless.
Alternative considered: add `dsh` to `NATURAL_LANGUAGE_SKILL_TOOLS` (like Rovo). Rejected because Rovo has no slash-like gesture at all, while dsh documents `/name`.
### 5. No shared-root ownership work
`.dsh/skills` is used by no other `AI_TOOLS` entry, so `shared-skill-target.ts` marker/reconciliation logic does not apply. If the same repo also generates the `.agents` target, dsh will prefer its rank-100 `.dsh/skills` tree and there is no single-writer conflict to resolve.
### 6. No frontmatter or template changes
OpenSpec writes `---` first line, kebab-case `name`, non-empty `description`, one-level `<name>/SKILL.md`, and extra fields such as `license`, `compatibility`, and `metadata`. The upstream parser accepts these extra fields. Tests parse every generated skill's YAML frontmatter and check required names and descriptions.
## Risks / Trade-offs
- [Commands-only delivery leaves dsh with zero artifacts] → Mitigation: init/update already print the existing `delivery` correction for capability-`none` tools; docs list dsh as skills-only, and the deferred capability work is the real fix.
- [`.dsh` detection can fire on a stale empty directory after commands-only removal] → Mitigation: interactive init shows detected-but-unconfigured tools as unselected in extend mode; behavior matches Rovo and is a cosmetic pre-selection, never a forced write.
- [dsh fail-closed parsing could silently drop skills] → Mitigation: generated files already comply; the init regression test checks frontmatter shape, and manual smoke testing against a real dsh session is in tasks.
- [Same-name skills under `.dsh/skills` and `.agents/skills`] → Mitigation: dsh's rank ordering (100 < 200) deterministically prefers `.dsh/skills`; this is upstream behavior, documented in supported-tools.
## Migration Plan
No data migration is required. Reverting the entry stops future dsh detection and generation but leaves existing `.dsh/skills` files in user projects. Remove the generated `openspec-*` folders separately if rollback is needed; preserve user-authored skills. Projects using the shared `.agents` target today keep working; selecting `dsh` on a later `openspec init` writes the dedicated higher-priority root without touching `.agents`.
## Open Questions
_None._
@@ -1,31 +0,0 @@
## Why
DeepSeek Harness discovers skills from fixed local roots, with `<project>/.dsh/skills` as its highest-priority project root. OpenSpec supports many assistants but has no dedicated target for it today, so dsh users can only use the vendor-neutral shared `.agents` target or hand-place skills — losing the dedicated `.dsh` integration.
## What Changes
- Add DeepSeek Harness as a supported tool with id `dsh`, `skillsDir: '.dsh'`, and directory-based auto-detection from `.dsh`.
- Generate the OpenSpec workflow skills into `.dsh/skills/openspec-*/SKILL.md` for dsh via `openspec init --tools dsh` and `openspec update`.
- Keep dsh skills-only: no command adapter and no `.dsh/commands/` files, because dsh has no file-based custom command surface.
- Spell dsh skill references as `/openspec-*` (dsh supports the user `/name` gesture), matching the existing skills-only tool pattern.
- Document dsh in the supported tools and command syntax docs.
- Add regression tests for detection, path resolution, init, update, and invocation spelling.
## Capabilities
### New Capabilities
_None._
### Modified Capabilities
- `ai-tool-paths`: define the `.dsh` skills root and directory-based detection for DeepSeek Harness.
## Impact
- `src/core/config.ts` — add the `dsh` entry to `AI_TOOLS`
- `docs/supported-tools.md` — tool row, invocation table, and `--tools` id list
- `docs/cli.md` — supported `--tools` id list
- `docs/commands.md`, `docs/how-commands-work.md`, `docs/troubleshooting.md` — skills-only invocation tables and notes
- `test/core/available-tools.test.ts`, `test/core/shared/skill-paths.test.ts`, `test/core/shared/tool-detection.test.ts`, `test/core/init.test.ts`, `test/core/update.test.ts`, `test/utils/command-references.test.ts`, `test/core/command-generation/registry.test.ts` — targeted dsh coverage
- `.changeset/add-dsh-support.md` — release note
@@ -1,47 +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: Claude Code paths defined
- **WHEN** looking up the `claude` tool
- **THEN** `skillsDir` SHALL be `.claude`
#### Scenario: Cursor paths defined
- **WHEN** looking up the `cursor` tool
- **THEN** `skillsDir` SHALL be `.cursor`
#### Scenario: Windsurf paths defined
- **WHEN** looking up the `windsurf` tool
- **THEN** `skillsDir` SHALL be `.windsurf`
#### Scenario: Kimi Code paths defined
- **WHEN** looking up the `kimi` tool
- **THEN** `skillsDir` SHALL be `.kimi-code`
- **AND** OpenSpec-managed skills remaining under the legacy `.kimi/skills` directory SHALL be migrated to `.kimi-code/skills` during init and update, preserving user files
#### Scenario: Hermes Agent paths defined
- **WHEN** looking up the `hermes` tool
- **THEN** `skillsDir` SHALL be `.hermes`
- **AND** `setupNote` SHALL explain that project `.hermes/skills` must be added to `skills.external_dirs` in `~/.hermes/config.yaml`
- **AND** `openspec init` and `openspec update` SHALL display the note whenever `hermes` is configured
#### Scenario: DeepSeek Harness paths defined
- **WHEN** looking up the `dsh` tool
- **THEN** `skillsDir` SHALL be `.dsh`
- **AND** auto-detection SHALL require `.dsh` to be a directory
- **AND** OpenSpec SHALL write dsh skills under `<projectRoot>/.dsh/skills/` using platform-native path joining
#### Scenario: Tools without skillsDir
- **WHEN** a tool has no `skillsDir` defined
- **THEN** skill generation SHALL error with message indicating the tool is not supported
@@ -1,30 +0,0 @@
## 1. Tool Metadata
- [x] 1.1 Add the `DeepSeek Harness` entry to `AI_TOOLS` in `src/core/config.ts` with `value: 'dsh'` and `skillsDir: '.dsh'`, using the existing directory-based detection
- [x] 1.2 Verify no other production code changes are required: init selection, `--tools` help, command surface capability, update drift, and shared-root handling must all derive from the new metadata
## 2. Detection and Path Tests
- [x] 2.1 Add `test/core/available-tools.test.ts` cases: detect `dsh` from `.dsh/skills` and from a bare `.dsh` directory; do not detect when neither exists or `.dsh` is a regular file
- [x] 2.2 Add a `test/core/shared/skill-paths.test.ts` case resolving `dsh` to `path.join(root, '.dsh', 'skills')`
- [x] 2.3 Add `test/core/shared/tool-detection.test.ts` cases: `getToolsWithSkillsDir()` includes `dsh`; skill status and configured-tool detection work for `.dsh/skills/openspec-*/SKILL.md`
## 3. Generation and Update Tests
- [x] 3.1 Add an `InitCommand` regression in `test/core/init.test.ts`: `--tools dsh` writes `.dsh/skills/openspec-explore/SKILL.md`, creates no `.dsh/commands`, logs the no-adapter skip, uses `/openspec-*` references in skill bodies and the getting-started hint, and the generated frontmatter satisfies dsh parsing (leading `---`, kebab-case name, non-empty description)
- [x] 3.2 Add an `UpdateCommand` regression in `test/core/update.test.ts`: refresh a stale dsh skill and verify a second update is idempotent
- [x] 3.3 Add `test/utils/command-references.test.ts` coverage that dsh uses the default `/openspec-*` form, and `test/core/command-generation/registry.test.ts` coverage that dsh has no command adapter
## 4. Documentation
- [x] 4.1 Update `docs/supported-tools.md`: add the dsh tool row, add dsh to the skills-only invocation row and the `--tools` id list, and explain that dsh reads `.dsh/skills` at higher priority than `.agents/skills`
- [x] 4.2 Update the supported `--tools` id list in `docs/cli.md`
- [x] 4.3 Update the skills-only syntax tables in `docs/commands.md` and `docs/how-commands-work.md`, and the skills-only tool list in `docs/troubleshooting.md`
## 5. Release and Validation
- [x] 5.1 Add `.changeset/add-dsh-support.md` with a minor bump describing `openspec init --tools dsh`
- [x] 5.2 Run `pnpm run lint`, `pnpm run build`, and the targeted vitest files for detection, paths, init, update, and command references
- [x] 5.3 Run the full test suite (`pnpm test`) and confirm cross-platform path assertions pass on Windows (no hardcoded separators in new tests)
- [x] 5.4 Run `openspec validate` for this change and fix any spec or change validation issues
- [x] 5.5 Manual smoke test in a temporary git project: `openspec init --tools dsh`, confirm `.dsh/skills/openspec-*/SKILL.md` files, start a dsh session and confirm the skills appear in the catalog and load via the skill tool or `/openspec-propose`
@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-08-25
@@ -1,25 +0,0 @@
## Why
OpenSpec labels the `bob` integration as "Bob Shell," but the same `.bob` configuration root serves the IBM Bob product. The narrower name makes the tool picker and status output look limited to the CLI.
## What Changes
- Rename the `bob` tool entry and success label to "IBM Bob."
- Update the supported-tools reference to use the product name.
- Preserve `.bob/commands/` generation for Bob Shell, which still supports custom slash commands.
## Capabilities
### New Capabilities
- None.
### Modified Capabilities
- `ai-tool-paths`: Use "IBM Bob" as the user-facing name for the `bob` integration.
## Impact
- Affected code: `src/core/config.ts` and its tool-detection test.
- Affected docs: `docs-lab/reference/supported-tools.md`.
- Command and skill paths do not change.
@@ -1,12 +0,0 @@
## ADDED Requirements
### Requirement: IBM Bob tool identity
The `AI_TOOLS` entry for `bob` SHALL use the IBM Bob product name without changing its skill or command paths.
#### Scenario: IBM Bob paths and display name
- **WHEN** looking up the `bob` tool
- **THEN** `name` and `successLabel` SHALL be `IBM Bob`
- **AND** `skillsDir` SHALL be `.bob`
- **AND** generated commands SHALL remain under `.bob/commands/`
@@ -1,6 +0,0 @@
## 1. Implementation
- [x] 1.1 Rename the `bob` tool entry and success label to "IBM Bob."
- [x] 1.2 Keep the Bob command adapter and existing command paths unchanged.
- [x] 1.3 Update the docs-lab supported-tools reference.
- [x] 1.4 Cover the user-facing name in a tool-detection test.
@@ -0,0 +1,90 @@
## Context
See `proposal.md` for motivation and `specs/custom-openspec-directory/spec.md` for the user contract.
The current root model has one overloaded path. `ResolvedOpenSpecRoot.path` names a repository or store root, while most consumers reconstruct its data directory with `path.join(root.path, 'openspec', ...)`. `init` and `update` use that same path for both planning files and project-local agent integrations. Stores work because their planning files retain the conventional `<store root>/openspec` layout; relocating only a normal project's planning directory exposes the missing distinction.
The implementation also contains command-local path construction and generated instructions that say `<planningHome.root>/openspec/...`. Adding an environment check at one entry point would therefore produce split-brain behavior: some commands would use the custom location while others and the coding agent would continue reading or writing the default one.
## Goals / Non-Goals
**Goals:**
- Represent the workspace and complete OpenSpec directory separately in one canonical root result.
- Give every shipped command and generated workflow the same custom-directory behavior.
- Preserve store validation, identity, precedence, diagnostics, and default project behavior.
- Make selection visible and fail closed before mutations.
**Non-Goals:**
- Configure `specs`, `changes`, `archive`, or `schemas` independently.
- Add a committed root pointer, search descendants for candidate directories, or infer a directory from agent instructions.
- Turn an in-repository custom directory into a registered store or change store metadata.
- Make `OPENSPEC_DIR` a persistent team setting. Teams remain responsible for setting the environment consistently in shells, task runners, and CI.
## Decisions
### 1. Select the complete directory with `OPENSPEC_DIR`
`OPENSPEC_DIR` names the directory that directly contains `config.yaml`, `specs/`, `changes/`, and optional `schemas/`. For example, `OPENSPEC_DIR=ai/openspec` selects `<workspace>/ai/openspec`.
The variable is the bootstrap mechanism because config inside the directory cannot locate itself. A new root-level YAML file was rejected because it replaces one unwanted root entry with another. Recursive discovery was rejected because it becomes ambiguous in monorepos and makes command behavior depend on unrelated descendants. Reusing store registration was rejected because a directory inside one code workspace is not a standalone planning repository and should not acquire store identity or machine-global registration.
### 2. Resolve a workspace-relative path by walking ancestors
`OPENSPEC_DIR` is a relative path within a workspace. Normal commands walk from the start directory toward the filesystem root and select the nearest ancestor where the relative candidate is an existing, qualifying OpenSpec directory. The matching ancestor is therefore the unambiguous workspace root, while the candidate is the OpenSpec directory. A closer ancestor with its own qualifying candidate is a nested workspace and wins for invocations inside it, matching existing nearest-root semantics. This mirrors nearest-root discovery without scanning descendants: `OPENSPEC_DIR=ai/openspec` works from both a workspace root and its subdirectories.
`init [path]` is the creation exception. It resolves the value against the explicit target workspace because the directory does not exist yet. `update [path]` resolves against the explicit target first and requires the result to exist. Unset, empty, and whitespace-only values mean no override so common shell and CI defaults preserve today's behavior. Non-empty absolute values, paths that escape the workspace lexically or after symlink canonicalization, filesystem roots, files, and directories without an OpenSpec config or planning shape fail with typed diagnostics. Existing path identity is canonicalized using the same utilities as store resolution, including symlink and Windows alias handling, before enforcing workspace containment.
Absolute and outside-workspace values are deliberately rejected: they select planning data without identifying the workspace that owns project-local agent files. A standalone planning repository already has an explicit, identity-preserving mechanism in stores. Keeping `OPENSPEC_DIR` workspace-relative makes `root.path` stable and avoids adding a second workspace variable or guessing from Git metadata.
### 3. Make selection explicit and non-ambiguous
An explicit `--store` and a non-empty `OPENSPEC_DIR` are mutually exclusive; supplying both fails before registry or filesystem mutation. Otherwise, a non-empty `OPENSPEC_DIR` is resolved before nearest-root and default-store fallback because setting it is an explicit process-level choice. An invalid non-empty value fails closed instead of silently falling back to another root.
The selected custom directory prints a root banner in human mode. JSON root output adds `openspec_dir` and the source value `environment`; `openspec_dir` is emitted for every resolved source so agents never reconstruct it. Existing `root.path` remains the workspace/store root for compatibility. Generated workflow instruction payloads likewise add `planningHome.openspecDir` while retaining existing fields.
The always-visible provenance addresses the main environment-variable risk: a long-lived shell can otherwise direct writes somewhere unexpected. This follows stores' established banner and machine-readable-root pattern rather than creating an invisible override.
### 4. Separate workspace paths from planning paths
The canonical result will carry at least:
- `path`: the existing workspace or store root contract.
- `openspecDir`: the authoritative complete OpenSpec directory.
- `changesDir`, `specsDir`, and `archiveDir`: derived once from `openspecDir`.
- `source` and optional `storeId`: selection provenance.
For a default project, `openspecDir` is `<workspace>/openspec`. For a store, it is `<store root>/openspec`. For an environment-selected project, `path` is the ancestor that resolved the relative value (or the explicit init/update workspace), while `openspecDir` is the selected directory. Because the environment value must be relative and remain inside that workspace, both paths are defined for every successful resolution.
`init` and `update` continue using the workspace path for `.claude/`, `.codex/`, other project-local tool files, root stubs, detection, and cleanup. Config, OpenSpec-owned instructions, schemas, specs, changes, and archives use `openspecDir`. This distinction is passed explicitly; no consumer derives one path from the other.
### 5. Migrate consumers onto directories, not another helper that hides joins
The root resolver and planning-home adapter derive all OpenSpec subdirectories. Root-scoped command APIs receive the resolved root or the exact directory they need. Project-config and schema APIs gain directory-aware entry points while compatibility wrappers retain default behavior for external callers.
Every shipped command path is included, including `init`, `update`, `config`, `schema`, `templates`, `view`, `doctor`, and deprecated noun-form commands that remain executable. Leaving a cwd-based path in a deprecated command would still let the same invocation read one root and write another.
Generated workflow text must read `root.openspec_dir` or `planningHome.openspecDir`. Literal `openspec/` examples may remain only when they describe the default layout, never when they direct a file operation. Generated snapshots and parity hashes are refreshed from the shared templates.
### 6. Keep store structure and registration unchanged
Registered stores continue requiring their conventional `openspec/` child and identity metadata. `OPENSPEC_DIR` does not customize a store's internal layout, participate in the registry, or weaken health checks. Stores benefit only from the shared explicit `openspecDir` field and from consumers no longer reconstructing their paths.
This keeps #697 independent from project-scoped store discovery and configurable individual artifact paths. It also preserves the stores simplification principle: one root-selection path and one observable root contract.
## Risks / Trade-offs
- **A partially migrated consumer could read or write the default directory.** → Inventory every hardcoded runtime and generated `openspec/` path, add command-parity tests, and make directory derivation a shared dependency-direction invariant.
- **A stale environment variable could redirect writes.** → Emit provenance for every custom-root command, reject invalid values and `--store` conflicts, and document consistent environment use across a workflow.
- **Relative resolution could differ by invocation directory.** → Use nearest-ancestor candidate resolution and cover workspace-root, nested-directory, spaces, Windows separators, and symlink aliases.
- **Separating workspace and planning paths expands the init/update surface.** → Test that planning files move while every project-local tool file remains byte-for-byte at the workspace root.
- **Adding JSON fields can affect strict consumers.** → Keep all existing keys and meanings, document the additive fields, and add agent-contract fixtures. This is a minor feature change, not a key rename.
## Migration Plan
1. Introduce the directory-aware root types and default/store adapters with compatibility tests while behavior is unchanged.
2. Add environment resolution, diagnostics, provenance, and root-selection tests.
3. Move runtime consumers, init/update, and generated workflows onto `openspecDir`, with parity coverage after each group.
4. Document `OPENSPEC_DIR`, add a minor changeset, and run the complete macOS/Linux suite plus Windows CI.
5. Rollback removes environment selection and the additive fields; default projects and stores retain their existing on-disk layouts throughout.
@@ -0,0 +1,34 @@
## Why
OpenSpec can select a standalone store anywhere on disk, but a normal project still has to keep its complete `openspec/` directory at the workspace root. This blocks teams that must place tool-owned content under an existing hierarchy such as `ai/openspec/`, even though the canonical root-selection layer already proves that commands can operate on planning data outside the current directory.
## What Changes
- Add `OPENSPEC_DIR` as an explicit, process-scoped selection of the complete OpenSpec directory.
- Make the canonical root result distinguish the workspace root, where project-local agent integrations live, from the OpenSpec directory, where config, schemas, specs, and changes live.
- Route every shipped CLI command and generated workflow through that canonical directory instead of reconstructing `<root>/openspec` locally.
- Preserve stores as standalone registered planning roots: a store still resolves to `<store root>/openspec`, but it uses the same directory-aware result as a normal or customized project.
- Keep existing root selection, filesystem behavior, and human output unchanged when `OPENSPEC_DIR` is unset or empty; retain every existing JSON field and meaning while adding the authoritative directory field.
- Reject invalid or conflicting custom-directory selection before reading or writing files, with the selected directory visible in human and JSON provenance.
## Capabilities
### New Capabilities
- `custom-openspec-directory`: Environment-based selection, precedence, diagnostics, provenance, and command parity for a relocated complete OpenSpec directory.
### Modified Capabilities
- `cli-init`: Initialize planning files in the selected OpenSpec directory while keeping project-local agent integrations at the requested workspace root.
- `cli-update`: Refresh planning instructions in the selected OpenSpec directory and agent integrations at the workspace root.
- `config-loading`: Load project configuration from the selected OpenSpec directory.
- `schema-resolution`: Resolve project-local schemas from the selected OpenSpec directory.
- `ai-tool-paths`: Keep project-local tool paths anchored to the workspace when planning files are relocated.
## Impact
- Root contract: `ResolvedOpenSpecRoot`, `PlanningHome`, human root banners, and JSON root output gain an authoritative OpenSpec-directory path and environment provenance.
- CLI: `init`, `update`, normal root-scoped commands, config/schema commands, view, doctor, and deprecated-but-shipped command paths must share the same directory resolution.
- Generated content: workflow templates and GitHub Copilot agent files must consume the resolved directory rather than assume `openspec/` beneath the process working directory.
- Tests and docs: cross-platform path, symlink identity, precedence, store parity, JSON, init/update, and generated-content parity coverage; CLI and agent-contract documentation; a minor changeset.
- No new project file, registry format, dependency, directory scan, or `specsPath`-style partial layout is introduced.
@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Planning relocation does not relocate project-local AI tools
Project-local AI tool skills, commands, prompts, workflows, and managed root stubs SHALL remain anchored to the workspace root when `OPENSPEC_DIR` relocates planning data.
#### Scenario: Tool paths remain at workspace root
- **GIVEN** `OPENSPEC_DIR` selects `ai/openspec`
- **WHEN** init or update generates project-local files for a selected AI tool
- **THEN** those files SHALL remain under the tool's documented workspace-relative path
- **AND** they SHALL NOT be generated beneath `ai/`
#### Scenario: Generated instructions use authoritative planning paths
- **GIVEN** planning data is outside the default root-level `openspec/` directory
- **WHEN** an installed OpenSpec workflow directs an agent to read or write a planning artifact
- **THEN** it SHALL obtain and use the authoritative OpenSpec directory reported by the CLI
- **AND** it SHALL NOT infer the planning path from the workspace or process working directory
@@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: Init separates workspace and planning destinations
`openspec init [path]` SHALL treat its path argument as the workspace root and, when `OPENSPEC_DIR` is set, create the complete OpenSpec structure at the selected directory while keeping workspace-scoped integrations at the workspace root.
#### Scenario: Initialize a custom relative directory
- **GIVEN** the user runs `openspec init .` with `OPENSPEC_DIR=ai/openspec`
- **WHEN** initialization completes
- **THEN** config, specs, changes, archive, and OpenSpec-owned instructions SHALL be created under `ai/openspec`
- **AND** selected project-local AI tool files SHALL be created under the workspace root
- **AND** no default root-level `openspec/` directory SHALL be created
#### Scenario: Custom directory already exists
- **GIVEN** `OPENSPEC_DIR` selects an existing valid OpenSpec directory
- **WHEN** the user runs `openspec init` to add or refresh tools
- **THEN** init SHALL enter its existing extend behavior for that directory
- **AND** it SHALL keep project-local tool discovery and generation anchored to the workspace root
#### Scenario: Invalid custom destination
- **GIVEN** `OPENSPEC_DIR` resolves to a file or selects an unsafe filesystem root
- **WHEN** the user runs `openspec init`
- **THEN** init SHALL fail before creating planning or tool files
@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Update separates workspace and planning destinations
`openspec update [path]` SHALL refresh OpenSpec-owned planning instructions in the environment-selected OpenSpec directory while detecting and refreshing project-local AI tool integrations at the workspace root.
#### Scenario: Update a relocated project
- **GIVEN** the workspace uses `OPENSPEC_DIR=ai/openspec`
- **WHEN** the user runs `openspec update .`
- **THEN** OpenSpec-owned files under `ai/openspec` SHALL be refreshed
- **AND** existing project-local tool files under the workspace root SHALL be refreshed
- **AND** update SHALL NOT create or refresh a default root-level `openspec/` directory
#### Scenario: Selected directory is missing
- **GIVEN** `OPENSPEC_DIR` does not resolve to an existing valid OpenSpec directory
- **WHEN** the user runs `openspec update`
- **THEN** update SHALL fail with a diagnostic naming the selected directory
- **AND** it SHALL NOT fall back to the default directory
@@ -0,0 +1,19 @@
## ADDED Requirements
### Requirement: Load project config from the authoritative OpenSpec directory
Project configuration SHALL be loaded from `config.yaml` or `config.yml` inside the authoritative OpenSpec directory returned by root selection.
#### Scenario: Environment-selected config
- **GIVEN** `OPENSPEC_DIR` selects `ai/openspec`
- **AND** `ai/openspec/config.yaml` contains project context, rules, and a schema
- **WHEN** a root-scoped command loads project configuration
- **THEN** it SHALL use `ai/openspec/config.yaml`
- **AND** it SHALL NOT read a config from the default root-level directory
#### Scenario: Store config remains conventional
- **GIVEN** a registered store is selected
- **WHEN** a command loads project configuration
- **THEN** it SHALL continue reading config from the store's authoritative `openspec` directory
@@ -0,0 +1,144 @@
## Purpose
Allow a project to relocate its complete OpenSpec directory without changing the workspace location of coding-agent integrations or adopting store machinery.
## ADDED Requirements
### Requirement: Environment selects the complete OpenSpec directory
OpenSpec SHALL accept `OPENSPEC_DIR` as a workspace-relative path to the complete directory for project config, specs, changes, and optional custom schemas, and SHALL use that directory consistently across every shipped command that reads or writes OpenSpec project data.
For discovery, a custom directory SHALL use the same qualification predicate as nearest-root selection: it qualifies when `specs/` or `changes/` is a planning directory that is not itself a store root, or when `config.yaml` or `config.yml` exists. A config-only directory therefore qualifies for selection, while config parsing and command-specific validation remain responsible for reporting malformed or incomplete content.
#### Scenario: Relative directory from workspace root
- **GIVEN** `OPENSPEC_DIR` is `ai/openspec`
- **AND** `ai/openspec` qualifies under the established nearest-root predicate
- **WHEN** the user runs a root-scoped command from the workspace root
- **THEN** the command SHALL read and write planning data under `ai/openspec`
#### Scenario: Relative directory from a workspace subdirectory
- **GIVEN** `OPENSPEC_DIR` is `ai/openspec`
- **AND** the user is inside a subdirectory of the same workspace
- **AND** no closer ancestor has its own qualifying `ai/openspec` directory
- **WHEN** the user runs a root-scoped command
- **THEN** OpenSpec SHALL resolve the nearest ancestor whose `ai/openspec` qualifies under that predicate
- **AND** the command SHALL use the same planning data as an invocation from the workspace root
#### Scenario: Nested workspace has its own custom directory
- **GIVEN** `OPENSPEC_DIR` is `ai/openspec`
- **AND** both an outer workspace and a nested workspace contain a qualifying `ai/openspec` directory
- **WHEN** the user runs a root-scoped command inside the nested workspace
- **THEN** OpenSpec SHALL treat the nested workspace as a separate workspace
- **AND** it SHALL select the nested workspace's `ai/openspec` directory
#### Scenario: Absolute directory is rejected
- **GIVEN** `OPENSPEC_DIR` is an absolute platform-native path to a valid OpenSpec directory
- **WHEN** the user runs a root-scoped command
- **THEN** the command SHALL fail with guidance to use a workspace-relative path
- **AND** it SHALL explain that standalone planning repositories use stores
#### Scenario: Relative directory escapes the workspace
- **GIVEN** `OPENSPEC_DIR` contains parent traversal that resolves outside the workspace candidate
- **WHEN** the user runs an OpenSpec command
- **THEN** the command SHALL fail before reading or writing project data
#### Scenario: Symlink escapes the workspace
- **GIVEN** `OPENSPEC_DIR` is a relative path whose existing candidate is a symlink
- **AND** the candidate's canonical path is outside the workspace ancestor that resolved it
- **WHEN** the user runs an OpenSpec command
- **THEN** the command SHALL fail before reading or writing project data
#### Scenario: Environment is unset or empty
- **WHEN** `OPENSPEC_DIR` is unset, empty, or whitespace-only
- **THEN** existing project and store root selection SHALL behave as before
#### Scenario: Config-only directory qualifies
- **GIVEN** `OPENSPEC_DIR` resolves to a directory containing `config.yaml` but no `specs/` or `changes/` directory
- **WHEN** OpenSpec performs root selection
- **THEN** it SHALL select that custom directory
- **AND** any config-content error SHALL be reported by the existing config validation path
### Requirement: Custom-directory selection is explicit and fail-closed
OpenSpec SHALL validate an environment-selected directory before a command mutates project data and SHALL NOT silently fall back to another project or store root when explicit custom-directory selection is invalid or conflicts with explicit store selection.
#### Scenario: Directory is missing
- **GIVEN** `OPENSPEC_DIR` names a directory that cannot be resolved
- **WHEN** the user runs a command other than `init`
- **THEN** the command SHALL fail with an actionable custom-directory diagnostic
- **AND** it SHALL NOT read from or write to the default `openspec/` directory
#### Scenario: Path is not a directory
- **GIVEN** `OPENSPEC_DIR` resolves to a file or another non-directory object
- **WHEN** the user runs an OpenSpec command
- **THEN** the command SHALL fail before reading or writing project data
#### Scenario: Explicit store conflicts with environment selection
- **GIVEN** `OPENSPEC_DIR` is set to a non-empty value
- **WHEN** the user also passes `--store <id>`
- **THEN** the command SHALL fail with guidance to choose one root source
- **AND** it SHALL NOT mutate either location
### Requirement: Root provenance exposes the authoritative directory
OpenSpec SHALL make the authoritative OpenSpec directory available to humans and agents so neither has to reconstruct it from the workspace or store root.
#### Scenario: Human command uses custom directory
- **WHEN** a human-mode command resolves `OPENSPEC_DIR`
- **THEN** stderr SHALL identify the selected custom OpenSpec directory before command-specific output
#### Scenario: JSON command reports custom directory
- **WHEN** a JSON command resolves `OPENSPEC_DIR`
- **THEN** its root object SHALL report `source: "environment"`
- **AND** it SHALL report the canonical absolute directory in `openspec_dir`
#### Scenario: Default project or store reports directory
- **WHEN** a JSON command resolves a default project root or registered store
- **THEN** its root object SHALL report the canonical absolute OpenSpec directory in `openspec_dir`
- **AND** existing root fields SHALL retain their meanings
### Requirement: Custom directories work across supported platforms
OpenSpec SHALL resolve custom-directory identity using platform-native path semantics and canonical existing-path identity on macOS, Linux, and Windows.
#### Scenario: Path contains spaces
- **GIVEN** `OPENSPEC_DIR` resolves to a valid directory whose path contains spaces
- **WHEN** the user completes a normal change lifecycle
- **THEN** creation, status, instructions, validation, and archive SHALL all use that directory
#### Scenario: Windows path and alias
- **GIVEN** a Windows path uses supported native separators or an existing filesystem alias
- **WHEN** OpenSpec resolves the custom directory
- **THEN** all commands SHALL agree on one canonical directory identity
- **AND** no command SHALL construct a mixed-separator filesystem path
### Requirement: Store behavior remains isolated
Custom project-directory selection SHALL reuse the canonical root contract without changing registered-store layout, identity, health, or registry behavior.
#### Scenario: Registered store is selected
- **WHEN** the user selects a registered store with `--store <id>` and `OPENSPEC_DIR` is unset or empty
- **THEN** the store SHALL continue using `<store root>/openspec`
- **AND** store identity and health validation SHALL remain unchanged
#### Scenario: Custom project needs no store metadata
- **WHEN** a project selects an in-workspace directory with `OPENSPEC_DIR`
- **THEN** OpenSpec SHALL NOT require store registration or store identity metadata
@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Project-local schemas follow the authoritative OpenSpec directory
Schema discovery and loading SHALL resolve project-local schemas beneath the authoritative OpenSpec directory rather than reconstructing an `openspec/schemas` path from the process working directory or workspace root.
#### Scenario: Environment-selected project schema
- **GIVEN** `OPENSPEC_DIR` selects `ai/openspec`
- **AND** a project-local schema exists under `ai/openspec/schemas/<name>`
- **WHEN** the user lists schemas or creates a change using that schema
- **THEN** schema discovery and schema loading SHALL both use the selected project-local schema
#### Scenario: Default and store schemas retain precedence
- **WHEN** `OPENSPEC_DIR` is unset or empty
- **THEN** project-local, user override, and package schema precedence SHALL remain unchanged for default projects and stores
@@ -0,0 +1,33 @@
## 1. Establish the directory-aware root contract
- [ ] 1.1 Add focused root-selection tests for default projects, stores, unset/empty compatibility, ancestor-resolved and nested-workspace relative `OPENSPEC_DIR` values, rejected absolute, lexical-escape, and symlink-escape values, other invalid values, `--store` conflicts, spaces, and canonical in-workspace alias identity; verify the new cases fail for the missing directory contract.
- [ ] 1.2 Extend the canonical resolved-root and planning-home types with authoritative OpenSpec-directory fields, derive all planning subdirectories once, and verify existing default/store tests remain green.
- [ ] 1.3 Implement environment selection, typed diagnostics, fail-closed precedence, human provenance, and additive JSON root output; verify focused human and JSON tests pass without changing existing field meanings.
- [ ] 1.4 Update `docs/agent-contract.md` and root-selection CLI documentation in the same change, then verify their documented payloads and precedence against the focused tests.
## 2. Move runtime consumers onto the authoritative directory
- [ ] 2.1 Inventory every runtime construction of config, schemas, specs, changes, and archive paths; migrate normal and deprecated-but-shipped commands to the resolved directories and verify command-parity tests cover list, show, validate, status, instructions, new change, archive, doctor, context, schemas, templates, config, view, and noun-form compatibility paths.
- [ ] 2.2 Add directory-aware project-config and schema-resolution APIs with compatibility wrappers for existing callers; verify config loading, rules/context injection, schema listing, schema loading, and custom-schema change creation against a custom directory.
- [ ] 2.3 Update reference, health, discovery, archive, and spec-application consumers to accept authoritative directories; verify a complete create-to-archive lifecycle in a path containing spaces.
- [ ] 2.4 Add a static regression guard for forbidden command-local `<root>/openspec` reconstruction and verify it permits only constants, default-layout descriptions, and store bootstrap code explicitly reviewed by name.
## 3. Separate init/update workspace and planning paths
- [ ] 3.1 Add failing init tests proving `OPENSPEC_DIR` creates the complete planning structure at the selected destination while project-local tool files, detection, cleanup, and managed root stubs stay at the explicit workspace path.
- [ ] 3.2 Implement separate workspace and OpenSpec-directory inputs through init, including extend mode, pointer guards, config creation, anchors, Copilot cloud state, and rollback; verify focused init and cleanup suites pass.
- [ ] 3.3 Add failing update tests for relocated planning instructions, workspace-local tool refresh, missing/invalid custom directories, and absence of default-directory writes.
- [ ] 3.4 Implement the same separation through update and verify focused update, migration, shared-skill, and Copilot cloud suites pass.
- [ ] 3.5 Update `cli-init`, `cli-update`, customization, and troubleshooting documentation with cross-platform examples and consistent-environment guidance; run every documented command in temporary fixtures.
## 4. Make generated workflows directory-aware
- [ ] 4.1 Add generated-content tests proving workflow instructions consume `root.openspec_dir` or `planningHome.openspecDir` for config, specs, changes, schemas, and archive operations under a custom directory.
- [ ] 4.2 Replace operational hardcoded `openspec/` paths in shared workflow templates and GitHub Copilot agent content with authoritative-directory guidance while preserving default-layout examples; regenerate committed skills and parity hashes and verify generated-content equivalence.
- [ ] 4.3 Extend cold-start and capstone agent journeys with a relocated directory and verify the agent creates, reads, validates, syncs, and archives only in the selected location.
## 5. Complete compatibility and release verification
- [ ] 5.1 Add a minor changeset and a migration note; verify unset-environment human output, filesystem effects, and existing JSON keys remain compatible for default projects and stores.
- [ ] 5.2 Run `pnpm run build`, `pnpm exec tsc --noEmit`, `pnpm run lint`, `pnpm test`, strict OpenSpec validation, generated-content parity checks, and `git diff --check`.
- [ ] 5.3 Verify Windows CI covers native separators, rejected drive-qualified absolute paths, spaces, and alias canonicalization before marking the implementation ready to merge.
-25
View File
@@ -56,31 +56,6 @@ The `AI_TOOLS` array SHALL include `skillsDir` for tools that support the Agent
- **AND** `setupNote` SHALL explain that project `.hermes/skills` must be added to `skills.external_dirs` in `~/.hermes/config.yaml`
- **AND** `openspec init` and `openspec update` SHALL display the note whenever `hermes` is configured
#### Scenario: DeepSeek Harness paths defined
- **WHEN** looking up the `dsh` tool
- **THEN** `skillsDir` SHALL be `.dsh`
- **AND** auto-detection SHALL require `.dsh` to be a directory
- **AND** OpenSpec SHALL write dsh skills under `<projectRoot>/.dsh/skills/` using platform-native path joining
#### Scenario: Grok Build paths defined
- **WHEN** looking up the `grok` tool
- **THEN** `skillsDir` SHALL be `.grok`
#### Scenario: Warp paths and detection defined
- **WHEN** looking up the `warp` tool
- **THEN** `skillsDir` SHALL be `.warp`
- **AND** `detectionPaths` SHALL include `.warp` and `WARP.md`
#### Scenario: Warp invokes skills without command files
- **WHEN** generating workflows for the `warp` tool with delivery set to `commands`
- **THEN** skills SHALL remain installed in `.warp/skills/`
- **AND** no command adapter or command files SHALL be required
- **AND** each skill SHALL be directly invocable by its `/openspec-*` name
#### Scenario: Tools without skillsDir
- **WHEN** a tool has no `skillsDir` defined
@@ -183,6 +183,17 @@ The system SHALL provide consistent output formatting.
- **WHEN** loading change state takes time
- **THEN** the system displays a spinner during loading
### Requirement: Experimental Isolation
The system SHALL implement artifact workflow commands in isolation for easy removal.
#### Scenario: Single file implementation
- **WHEN** artifact workflow feature is implemented
- **THEN** all commands are in `src/commands/artifact-workflow.ts`
#### Scenario: Help text marking
- **WHEN** user runs `--help` on any artifact workflow command
- **THEN** help text indicates the command is experimental
### Requirement: Schema Apply Block
The system SHALL support an `apply` block in schema definitions that controls when and how implementation begins.
+3 -3
View File
@@ -182,9 +182,9 @@ The `openspec config profile` command SHALL provide an action-first interactive
- **WHEN** user runs `openspec config profile` interactively
- **THEN** the first prompt SHALL offer:
- `Delivery and workflows`
- `Delivery only`
- `Workflows only`
- `Change delivery + workflows`
- `Change delivery only`
- `Change workflows only`
- `Keep current settings (exit)`
#### Scenario: Delivery prompt marks current selection
-6
View File
@@ -231,12 +231,6 @@ The command SHALL generate opsx slash commands only for selected tools that have
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.kimi-code'`
- **AND** command-file generation SHALL be skipped because no Kimi adapter is registered
#### Scenario: Grok Build skips command-file generation
- **WHEN** the user selects Grok Build during initialization
- **THEN** OpenSpec SHALL treat it as a supported tool with `skillsDir: '.grok'`
- **AND** command-file generation SHALL be skipped because no Grok adapter is registered
### Requirement: Config File Generation
The command SHALL create an OpenSpec config file with schema settings.
+1 -29
View File
@@ -45,35 +45,6 @@ The dashboard SHALL show active changes with visual progress indicators.
- **AND** treat missing progress values as 0% for ordering
- **AND** break ties by change identifier in ascending alphabetical order to keep output deterministic
### Requirement: Active Change Workflow Status
The dashboard SHALL show each active change's schema and artifact states beneath its task progress, using the same workflow resolution as `openspec status`. Workflow status SHALL NOT change task progress, change categories, or sorting.
#### Scenario: Workflow states
- **WHEN** an active change's workflow can be loaded
- **THEN** show the schema name and artifacts in dependency order
- **AND** mark existing artifact outputs with `✓`, ready artifacts with `→`, blocked artifacts with no symbol, and skipped artifacts with `(skipped)`
- **AND** treat an existing tasks artifact as done even when its implementation checklist is unfinished
#### Scenario: Store-local workflow
- **WHEN** the dashboard targets a store through `--store` or a project store pointer
- **THEN** resolve workflow schemas and artifact files from that store
- **AND** use the store's default schema for changes without a schema in their metadata
#### Scenario: Invalid workflow
- **WHEN** an active change's metadata or schema cannot be loaded
- **THEN** print a warning identifying the change and the error
- **AND** omit only that change's workflow status while retaining its task progress and rendering other changes
#### Scenario: Terminal controls in workflow text
- **WHEN** schema names, artifact identifiers, or workflow errors contain terminal control characters
- **THEN** replace those characters with inert text in the dashboard output
- **AND** preserve the underlying identifiers and workflow states
### Requirement: Completed Changes Display
The dashboard SHALL list completed changes in a separate section, only showing changes with ALL tasks completed.
@@ -155,3 +126,4 @@ The dashboard SHALL display changes without tasks in a separate "Draft" section.
- **WHEN** multiple draft changes exist
- **THEN** system sorts them alphabetically by name
+2 -2
View File
@@ -99,8 +99,8 @@ Requirement headers SHALL serve as unique identifiers for programmatic matching
- **WHEN** processing delta changes
- **THEN** use the `### Requirement: [Name]` header as the unique identifier
- **AND** strip a closing run of `#` characters when a space or tab precedes it and only spaces or tabs follow it, then trim surrounding whitespace
- **AND** compare normalized requirement names with case-sensitive equality
- **AND** match using normalized headers: `normalize(header) = trim(header)`
- **AND** compare headers with case-sensitive equality after normalization
#### Scenario: Handling requirement renames
+1 -2
View File
@@ -88,8 +88,7 @@ The skill SHALL prompt to sync delta specs before archiving if specs exist.
- **AND** if user cancels, stop without archiving
- **AND** if user confirms, execute `/opsx:sync` logic inline and wait for it to complete
- **AND** verify every capability that has a delta spec, not only those the sync reports it touched: ADDED requirements present, MODIFIED requirements carrying the changes named in the delta, REMOVED requirements absent, RENAMED requirements present under the new name and absent under the old one
- **AND** treat a capability whose last requirement the sync removed as verified when its main spec was deleted rather than left empty
- **AND** treat any stop or blocking condition the sync reports as a failed sync, including a main spec it left unmodified because a retirement was blocked
- **AND** treat a capability whose last requirement the sync removed as verified when its main spec was deleted rather than left empty, and a spec the sync deliberately kept and reported as verified too
- **AND** stop without archiving if the sync fails or any capability does not verify
- **AND** archive only after verification passes, or when the user explicitly chose to archive without syncing or to archive already-synced specs
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "1.14.0",
"version": "1.13.2",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+188 -188
View File
@@ -61,7 +61,7 @@ importers:
version: 4.1.11(vitest@4.1.11)
eslint:
specifier: ^10.5.0
version: 10.11.0
version: 10.10.0
smol-toml:
specifier: ^1.7.1
version: 1.8.0
@@ -70,7 +70,7 @@ importers:
version: 6.0.3
typescript-eslint:
specifier: ^8.65.0
version: 8.70.1(eslint@10.11.0)(typescript@6.0.3)
version: 8.70.0(eslint@10.10.0)(typescript@6.0.3)
vitest:
specifier: ^4.1.11
version: 4.1.11(@types/node@20.19.43)(@vitest/ui@4.1.11)(vite@7.3.6(@types/node@20.19.43)(yaml@2.9.1))
@@ -576,141 +576,141 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@rollup/rollup-android-arm-eabi@4.63.5':
resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==}
'@rollup/rollup-android-arm-eabi@4.63.4':
resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.63.5':
resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==}
'@rollup/rollup-android-arm64@4.63.4':
resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.63.5':
resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==}
'@rollup/rollup-darwin-arm64@4.63.4':
resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.63.5':
resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==}
'@rollup/rollup-darwin-x64@4.63.4':
resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.63.5':
resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==}
'@rollup/rollup-freebsd-arm64@4.63.4':
resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.63.5':
resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==}
'@rollup/rollup-freebsd-x64@4.63.4':
resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.63.5':
resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==}
'@rollup/rollup-linux-arm-gnueabihf@4.63.4':
resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.63.5':
resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==}
'@rollup/rollup-linux-arm-musleabihf@4.63.4':
resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.63.5':
resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==}
'@rollup/rollup-linux-arm64-gnu@4.63.4':
resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.63.5':
resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==}
'@rollup/rollup-linux-arm64-musl@4.63.4':
resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.63.5':
resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==}
'@rollup/rollup-linux-loong64-gnu@4.63.4':
resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.63.5':
resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==}
'@rollup/rollup-linux-loong64-musl@4.63.4':
resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.63.5':
resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==}
'@rollup/rollup-linux-ppc64-gnu@4.63.4':
resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.63.5':
resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==}
'@rollup/rollup-linux-ppc64-musl@4.63.4':
resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.63.5':
resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==}
'@rollup/rollup-linux-riscv64-gnu@4.63.4':
resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.63.5':
resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==}
'@rollup/rollup-linux-riscv64-musl@4.63.4':
resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.63.5':
resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==}
'@rollup/rollup-linux-s390x-gnu@4.63.4':
resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.63.5':
resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==}
'@rollup/rollup-linux-x64-gnu@4.63.4':
resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.63.5':
resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==}
'@rollup/rollup-linux-x64-musl@4.63.4':
resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.63.5':
resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==}
'@rollup/rollup-openbsd-x64@4.63.4':
resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.63.5':
resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==}
'@rollup/rollup-openharmony-arm64@4.63.4':
resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.63.5':
resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==}
'@rollup/rollup-win32-arm64-msvc@4.63.4':
resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.63.5':
resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==}
'@rollup/rollup-win32-ia32-msvc@4.63.4':
resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.63.5':
resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==}
'@rollup/rollup-win32-x64-gnu@4.63.4':
resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.63.5':
resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==}
'@rollup/rollup-win32-x64-msvc@4.63.4':
resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==}
cpu: [x64]
os: [win32]
@@ -735,63 +735,63 @@ packages:
'@types/node@20.19.43':
resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
'@typescript-eslint/eslint-plugin@8.70.1':
resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
'@typescript-eslint/eslint-plugin@8.70.0':
resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.70.1
'@typescript-eslint/parser': ^8.70.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.70.1':
resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==}
'@typescript-eslint/parser@8.70.0':
resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.70.1':
resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==}
'@typescript-eslint/project-service@8.70.0':
resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.70.1':
resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==}
'@typescript-eslint/scope-manager@8.70.0':
resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.70.1':
resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==}
'@typescript-eslint/tsconfig-utils@8.70.0':
resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.70.1':
resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==}
'@typescript-eslint/type-utils@8.70.0':
resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.70.1':
resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==}
'@typescript-eslint/types@8.70.0':
resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.70.1':
resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==}
'@typescript-eslint/typescript-estree@8.70.0':
resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.70.1':
resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==}
'@typescript-eslint/utils@8.70.0':
resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.70.1':
resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==}
'@typescript-eslint/visitor-keys@8.70.0':
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/expect@4.1.11':
@@ -849,8 +849,8 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@5.0.12:
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
braces@3.0.3:
@@ -941,8 +941,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.11.0:
resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==}
eslint@10.10.0:
resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -1071,8 +1071,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.10:
resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==}
ignore@7.0.9:
resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==}
engines: {node: '>= 4'}
import-meta-resolve@4.2.0:
@@ -1253,8 +1253,8 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rollup@4.63.5:
resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==}
rollup@4.63.4:
resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -1358,8 +1358,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typescript-eslint@8.70.1:
resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==}
typescript-eslint@8.70.0:
resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -1704,9 +1704,9 @@ snapshots:
'@esbuild/win32-x64@0.28.2':
optional: true
'@eslint-community/eslint-utils@4.10.1(eslint@10.11.0)':
'@eslint-community/eslint-utils@4.10.1(eslint@10.10.0)':
dependencies:
eslint: 10.11.0
eslint: 10.10.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -1933,79 +1933,79 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@rollup/rollup-android-arm-eabi@4.63.5':
'@rollup/rollup-android-arm-eabi@4.63.4':
optional: true
'@rollup/rollup-android-arm64@4.63.5':
'@rollup/rollup-android-arm64@4.63.4':
optional: true
'@rollup/rollup-darwin-arm64@4.63.5':
'@rollup/rollup-darwin-arm64@4.63.4':
optional: true
'@rollup/rollup-darwin-x64@4.63.5':
'@rollup/rollup-darwin-x64@4.63.4':
optional: true
'@rollup/rollup-freebsd-arm64@4.63.5':
'@rollup/rollup-freebsd-arm64@4.63.4':
optional: true
'@rollup/rollup-freebsd-x64@4.63.5':
'@rollup/rollup-freebsd-x64@4.63.4':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.63.5':
'@rollup/rollup-linux-arm-gnueabihf@4.63.4':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.63.5':
'@rollup/rollup-linux-arm-musleabihf@4.63.4':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.63.5':
'@rollup/rollup-linux-arm64-gnu@4.63.4':
optional: true
'@rollup/rollup-linux-arm64-musl@4.63.5':
'@rollup/rollup-linux-arm64-musl@4.63.4':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.63.5':
'@rollup/rollup-linux-loong64-gnu@4.63.4':
optional: true
'@rollup/rollup-linux-loong64-musl@4.63.5':
'@rollup/rollup-linux-loong64-musl@4.63.4':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.63.5':
'@rollup/rollup-linux-ppc64-gnu@4.63.4':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.63.5':
'@rollup/rollup-linux-ppc64-musl@4.63.4':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.63.5':
'@rollup/rollup-linux-riscv64-gnu@4.63.4':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.63.5':
'@rollup/rollup-linux-riscv64-musl@4.63.4':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.63.5':
'@rollup/rollup-linux-s390x-gnu@4.63.4':
optional: true
'@rollup/rollup-linux-x64-gnu@4.63.5':
'@rollup/rollup-linux-x64-gnu@4.63.4':
optional: true
'@rollup/rollup-linux-x64-musl@4.63.5':
'@rollup/rollup-linux-x64-musl@4.63.4':
optional: true
'@rollup/rollup-openbsd-x64@4.63.5':
'@rollup/rollup-openbsd-x64@4.63.4':
optional: true
'@rollup/rollup-openharmony-arm64@4.63.5':
'@rollup/rollup-openharmony-arm64@4.63.4':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.63.5':
'@rollup/rollup-win32-arm64-msvc@4.63.4':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.63.5':
'@rollup/rollup-win32-ia32-msvc@4.63.4':
optional: true
'@rollup/rollup-win32-x64-gnu@4.63.5':
'@rollup/rollup-win32-x64-gnu@4.63.4':
optional: true
'@rollup/rollup-win32-x64-msvc@4.63.5':
'@rollup/rollup-win32-x64-msvc@4.63.4':
optional: true
'@standard-schema/spec@1.1.0': {}
@@ -2026,72 +2026,72 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3)':
'@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0)(typescript@6.0.3))(eslint@10.10.0)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.70.1
'@typescript-eslint/type-utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.1
eslint: 10.11.0
ignore: 7.0.10
'@typescript-eslint/parser': 8.70.0(eslint@10.10.0)(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/type-utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.0
eslint: 10.10.0
ignore: 7.0.9
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
'@typescript-eslint/parser@8.70.0(eslint@10.10.0)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.70.1
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.1
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.0
debug: 4.4.3
eslint: 10.11.0
eslint: 10.10.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.70.1(typescript@6.0.3)':
'@typescript-eslint/project-service@8.70.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0
debug: 4.4.3
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.70.1':
'@typescript-eslint/scope-manager@8.70.0':
dependencies:
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/visitor-keys': 8.70.1
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/visitor-keys': 8.70.0
'@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)':
'@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
'@typescript-eslint/type-utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
'@typescript-eslint/type-utils@8.70.0(eslint@10.10.0)(typescript@6.0.3)':
dependencies:
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3)
debug: 4.4.3
eslint: 10.11.0
eslint: 10.10.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.70.1': {}
'@typescript-eslint/types@8.70.0': {}
'@typescript-eslint/typescript-estree@8.70.1(typescript@6.0.3)':
'@typescript-eslint/typescript-estree@8.70.0(typescript@6.0.3)':
dependencies:
'@typescript-eslint/project-service': 8.70.1(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/visitor-keys': 8.70.1
'@typescript-eslint/project-service': 8.70.0(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/visitor-keys': 8.70.0
debug: 4.4.3
minimatch: 10.2.6
semver: 7.8.5
@@ -2101,20 +2101,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
'@typescript-eslint/utils@8.70.0(eslint@10.10.0)(typescript@6.0.3)':
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0)
'@typescript-eslint/scope-manager': 8.70.1
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
eslint: 10.11.0
'@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0)
'@typescript-eslint/scope-manager': 8.70.0
'@typescript-eslint/types': 8.70.0
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
eslint: 10.10.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.70.1':
'@typescript-eslint/visitor-keys@8.70.0':
dependencies:
'@typescript-eslint/types': 8.70.1
'@typescript-eslint/types': 8.70.0
eslint-visitor-keys: 5.0.1
'@vitest/expect@4.1.11':
@@ -2186,7 +2186,7 @@ snapshots:
balanced-match@4.0.4: {}
brace-expansion@5.0.12:
brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
@@ -2282,9 +2282,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
eslint@10.11.0:
eslint@10.10.0:
dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0)
'@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5
'@eslint/config-helpers': 0.7.0
@@ -2429,7 +2429,7 @@ snapshots:
ignore@5.3.2: {}
ignore@7.0.10: {}
ignore@7.0.9: {}
import-meta-resolve@4.2.0: {}
@@ -2495,7 +2495,7 @@ snapshots:
minimatch@10.2.6:
dependencies:
brace-expansion: 5.0.12
brace-expansion: 5.0.9
mrmime@2.0.1: {}
@@ -2580,36 +2580,36 @@ snapshots:
reusify@1.1.0: {}
rollup@4.63.5:
rollup@4.63.4:
dependencies:
'@types/estree': 1.0.9
optionalDependencies:
'@napi-rs/lzma-linux-x64-gnu': 1.5.1
'@rollup/rollup-android-arm-eabi': 4.63.5
'@rollup/rollup-android-arm64': 4.63.5
'@rollup/rollup-darwin-arm64': 4.63.5
'@rollup/rollup-darwin-x64': 4.63.5
'@rollup/rollup-freebsd-arm64': 4.63.5
'@rollup/rollup-freebsd-x64': 4.63.5
'@rollup/rollup-linux-arm-gnueabihf': 4.63.5
'@rollup/rollup-linux-arm-musleabihf': 4.63.5
'@rollup/rollup-linux-arm64-gnu': 4.63.5
'@rollup/rollup-linux-arm64-musl': 4.63.5
'@rollup/rollup-linux-loong64-gnu': 4.63.5
'@rollup/rollup-linux-loong64-musl': 4.63.5
'@rollup/rollup-linux-ppc64-gnu': 4.63.5
'@rollup/rollup-linux-ppc64-musl': 4.63.5
'@rollup/rollup-linux-riscv64-gnu': 4.63.5
'@rollup/rollup-linux-riscv64-musl': 4.63.5
'@rollup/rollup-linux-s390x-gnu': 4.63.5
'@rollup/rollup-linux-x64-gnu': 4.63.5
'@rollup/rollup-linux-x64-musl': 4.63.5
'@rollup/rollup-openbsd-x64': 4.63.5
'@rollup/rollup-openharmony-arm64': 4.63.5
'@rollup/rollup-win32-arm64-msvc': 4.63.5
'@rollup/rollup-win32-ia32-msvc': 4.63.5
'@rollup/rollup-win32-x64-gnu': 4.63.5
'@rollup/rollup-win32-x64-msvc': 4.63.5
'@rollup/rollup-android-arm-eabi': 4.63.4
'@rollup/rollup-android-arm64': 4.63.4
'@rollup/rollup-darwin-arm64': 4.63.4
'@rollup/rollup-darwin-x64': 4.63.4
'@rollup/rollup-freebsd-arm64': 4.63.4
'@rollup/rollup-freebsd-x64': 4.63.4
'@rollup/rollup-linux-arm-gnueabihf': 4.63.4
'@rollup/rollup-linux-arm-musleabihf': 4.63.4
'@rollup/rollup-linux-arm64-gnu': 4.63.4
'@rollup/rollup-linux-arm64-musl': 4.63.4
'@rollup/rollup-linux-loong64-gnu': 4.63.4
'@rollup/rollup-linux-loong64-musl': 4.63.4
'@rollup/rollup-linux-ppc64-gnu': 4.63.4
'@rollup/rollup-linux-ppc64-musl': 4.63.4
'@rollup/rollup-linux-riscv64-gnu': 4.63.4
'@rollup/rollup-linux-riscv64-musl': 4.63.4
'@rollup/rollup-linux-s390x-gnu': 4.63.4
'@rollup/rollup-linux-x64-gnu': 4.63.4
'@rollup/rollup-linux-x64-musl': 4.63.4
'@rollup/rollup-openbsd-x64': 4.63.4
'@rollup/rollup-openharmony-arm64': 4.63.4
'@rollup/rollup-win32-arm64-msvc': 4.63.4
'@rollup/rollup-win32-ia32-msvc': 4.63.4
'@rollup/rollup-win32-x64-gnu': 4.63.4
'@rollup/rollup-win32-x64-msvc': 4.63.4
fsevents: 2.3.3
run-parallel@1.2.0:
@@ -2686,13 +2686,13 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
typescript-eslint@8.70.1(eslint@10.11.0)(typescript@6.0.3):
typescript-eslint@8.70.0(eslint@10.10.0)(typescript@6.0.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3))(eslint@10.11.0)(typescript@6.0.3)
'@typescript-eslint/parser': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.70.1(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.1(eslint@10.11.0)(typescript@6.0.3)
eslint: 10.11.0
'@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0)(typescript@6.0.3))(eslint@10.10.0)(typescript@6.0.3)
'@typescript-eslint/parser': 8.70.0(eslint@10.10.0)(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.10.0)(typescript@6.0.3)
eslint: 10.10.0
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -2711,7 +2711,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.7
postcss: 8.5.28
rollup: 4.63.5
rollup: 4.63.4
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 20.19.43
+1 -2
View File
@@ -13,7 +13,7 @@ artifacts:
- **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
- **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
- **Capabilities**: Identify which specs will be created or modified:
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Name each capability for a durable system behavior (for example, `user-auth`), not the work in this change (for example, `add-login-endpoint`). Choose a cohesive boundary that can own related requirements as the system evolves; avoid broad catch-all capabilities. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
- **Impact**: Affected code, APIs, dependencies, or systems.
@@ -94,7 +94,6 @@ artifacts:
- Each scenario: `#### Scenario: <name>` with WHEN/THEN format
- **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
- Every requirement MUST have at least one scenario.
- Keep each requirement's description (the text between `### Requirement:` and its first scenario) to 500 characters or fewer. `openspec validate` flags longer descriptions once they reach the main spec. This is an informational hint, not an error. When writing a new requirement, state one behavior per requirement: move examples and edge cases into scenarios, and split a requirement that covers several behaviors into separate `### Requirement:` blocks, each with its own scenarios. Under MODIFIED, keep the existing requirement block whole; never split, trim or rewrite existing text just to meet the length.
New capabilities only: the delta spec's first section is `## Purpose` -
one or two sentences (50+ characters, or `openspec validate --strict`
+3 -6
View File
@@ -11,12 +11,9 @@
## Capabilities
### New Capabilities
<!-- Capabilities being introduced. Name each capability for a cohesive system
behavior that can own related requirements as the system evolves. Do not name
implementation tasks or proposal sections. Avoid broad catch-all names. Use
kebab-case for path segments you introduce (e.g., user-auth or identity/user-auth)
that follow the project's existing spec organization. Each creates
specs/<capability-path>/spec.md. -->
<!-- Capabilities being introduced. Use kebab-case for path segments you introduce
(e.g., user-auth or identity/user-auth) that follow the project's existing
spec organization. Each creates specs/<capability-path>/spec.md. -->
- `<capability-path>`: <brief description of what this capability covers>
### Modified Capabilities
+2 -5
View File
@@ -55,7 +55,7 @@ In both branches, never create the root as a side effect: do not run `openspec i
This returns:
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status, source path, and source line
- Task list with status
- Dynamic instruction based on current state
- Optional `context`: current required project instruction input from the selected root
- Optional `operationGuidance`: current advisory guidance for apply
@@ -107,9 +107,7 @@ In both branches, never create the root as a side effect: do not run `openspec i
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Before editing, confirm the checkbox at the returned `sourcePath` and `line` still matches the task description; if it does not, rerun the apply instructions and use the refreshed location
- Mark the task complete at its returned `sourcePath` and `line`: `- [ ]` → `- [x]`
- Rerun the apply instructions and confirm that task is now done and progress changed
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
- Continue to next task
**Pause if:**
@@ -189,7 +187,6 @@ What would you like to do?
- When a task needs work beyond what the spec describes, surface the added scope and pause - never silently narrow, defer, or simplify away specified behavior
- Only mark a task `- [x]` when its specified behavior is fully implemented, not when it is partially done or deferred
- Use contextFiles from CLI output, don't assume specific file names
- Use each task's sourcePath and line to update its exact checkbox
- Do not use context or operation guidance as proof that a task is complete
- Apply relevant project context; report conflicts with controlling workflow inputs
- Consider every guidance entry; explain any inapplicable or conflicting advice
+1 -12
View File
@@ -146,21 +146,10 @@ In both branches, never create the root as a side effect: do not run `openspec i
Then run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) for change '<name>', passing the delta spec analysis and the fetched specs-rule snapshot from above, and wait for it to finish. The inline sync must reuse that snapshot without fetching `specs` instructions again. Do not delegate it to a background task — step 5 would move `changeRoot` out from under a sync that is still reading it, leaving the change archived and the main specs never updated. If your agent can only run it by delegation, delegate synchronously and wait for the result.
If the sync reports any stop or blocking condition, treat the sync as failed.
Stop the archive immediately. Do not perform the post-sync content comparison and do not move its `changeRoot`.
Nothing has moved, so the user can fix the blocking condition or re-run the sync.
After the sync writes each main spec, verify its structure against the canonical sync contract:
- A new main spec starts with a `# <capability> Specification` title. An existing main spec keeps its title exactly as it is.
- Preserve existing `## Purpose` sections completely untouched for established main specs.
- For a new main spec, copy the delta `## Purpose` verbatim. Warn only if the purpose text is shorter than standard validation expects. Do not regenerate or rewrite existing authored purpose. If no usable `## Purpose` is provided, use the existing TBD Purpose behavior and warning.
- Verify that no delta-style section headers (`## ADDED Requirements`, `## MODIFIED Requirements`, `## REMOVED Requirements`, `## RENAMED Requirements`) remain in the main spec, adhering strictly to the sync workflow formatting rules.
- Requirement blocks the sync wrote or changed use `### Requirement:` headings, and their scenarios use `#### Scenario:` headings, under the spec's `## Requirements` section. Leave content the delta does not mention exactly as it is.
Then re-run the comparison from the top of this step, including the explicitly retired, missing-spec case, against every capability that has a delta spec in `artifactPaths.specs.existingOutputPaths` — not only the ones the sync reports it touched. A successful sync leaves nothing left to apply, so each capability must now read as already synced:
- ADDED requirements present
- MODIFIED requirements carrying the scenario and description changes named in the delta, with their other scenarios intact
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty.
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match
- RENAMED requirements present under the new name and absent under the old one
If the sync failed, or any capability does not match, report what differs and stop — do not archive. Nothing has moved and `changeRoot` is intact, so the user can fix the mismatch or re-run the sync and start the archive again.
+1 -3
View File
@@ -206,8 +206,6 @@ In both branches, never create the root as a side effect: do not run `openspec i
a. **Sync included delta specs**:
- Run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge) only for changes with entries in `includedDeltas`, passing only the included delta paths and explicitly instructing it to ignore that change's `excludedDeltas`. Wait for it to finish.
- If the sync reports any stop or blocking condition, treat the sync as failed. Stop processing that change immediately. Before continuing to the next change, record this change's outcome as Failed in the batch results, including the sync blocking/error condition.
- Do not perform the post-sync content comparison and do not move its `changeRoot`; leave the change intact.
- For conflicts, apply in resolved order.
- Pass that change's fetched specs-rule snapshot into inline sync; inline
sync must reuse it without fetching instructions again
@@ -222,7 +220,7 @@ In both branches, never create the root as a side effect: do not run `openspec i
- Verify that main specs are updated:
- ADDED requirements present
- MODIFIED requirements carrying scenario and description changes named in the delta, with their other scenarios intact
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty.
- REMOVED requirements gone — and where this sync retired a capability (removed its last requirement, leaving `## Requirements` empty), its main spec deleted rather than left empty; a spec the sync deliberately kept and reported is also a match
- RENAMED requirements present under the new name and absent under the old one
- Do not verify delta specs in `excludedDeltas`; they are intentionally left unsynced.
- If sync failed or any capability does not match verification, report what differs and fail/skip moving that change's `changeRoot` — do not archive that change. `changeRoot` remains intact.
+1 -48
View File
@@ -16,11 +16,6 @@ import {
rerunUpdateWithUpgradedCli,
displayUpgradeCommand,
isSourceCheckout,
checkForCliUpdate,
getCliInstallInfo,
getCliUpdateCommand,
canSelfUpgrade,
buildVersionReportLines,
} from '../core/version-check.js';
import { ListCommand } from '../core/list.js';
import { ArchiveCommand, type ArchiveOptions } from '../core/archive.js';
@@ -174,41 +169,6 @@ program
// Global options
program.option('--no-color', 'Disable color output');
program
.command('version')
.description('Report the installed OpenSpec version and update availability')
.option('--json', 'Output as JSON')
.option('--check', 'Check the registry for a newer version')
.action(async (options: { json?: boolean; check?: boolean }) => {
const install = getCliInstallInfo();
const update = options.check ? await checkForCliUpdate() : undefined;
const command = update?.status === 'available' ? getCliUpdateCommand(install) : null;
const output = {
schemaVersion: 1,
version,
install,
...(update
? {
update: {
...update,
command,
canSelfUpgrade:
update.status === 'available'
? canSelfUpgrade(install.location, process.cwd())
: false,
},
}
: {}),
};
if (options.json) {
console.log(JSON.stringify(output, null, 2));
return;
}
console.log(buildVersionReportLines(version, install, update, command).join('\n'));
});
// Apply global flags and telemetry before any command runs
// Note: preAction receives (thisCommand, actionCommand) where:
// - thisCommand: the command where hook was added (root program)
@@ -395,17 +355,12 @@ program
.description('List items (changes by default). Use --specs to list specs.')
.option('--specs', 'List specs instead of changes')
.option('--changes', 'List changes explicitly (default)')
.option('--archived', 'Show only archived changes')
.option('--all', 'Show both active and archived changes')
.option('--sort <order>', 'Sort order: "recent" (default) or "name"', 'recent')
.option('--json', 'Output as JSON (for programmatic use)')
.option('--store <id>', STORE_OPTION_DESCRIPTION)
.addOption(hiddenStorePathOption())
.action(async (options?: { specs?: boolean; changes?: boolean; archived?: boolean; all?: boolean; sort?: string; json?: boolean; store?: string; storePath?: string }) => {
.action(async (options?: { specs?: boolean; changes?: boolean; sort?: string; json?: boolean; store?: string; storePath?: string }) => {
try {
if (options?.specs && (options.archived || options.all)) {
throw new Error('--archived and --all can only be used when listing changes.');
}
const root = await resolveRootForCommand(options ?? {}, {
json: options?.json,
failurePayload: options?.specs ? { specs: [], root: null } : { changes: [], root: null },
@@ -422,8 +377,6 @@ program
await listCommand.execute(root.path, mode, {
sort,
json: options?.json,
archived: options?.archived,
all: options?.all,
...(options?.json ? { root: toRootOutput(root) } : {}),
});
} catch (error) {
-1
View File
@@ -66,7 +66,6 @@ function filterSpec(spec: Spec, options: ShowOptions): Spec {
? [spec.requirements[requirementIndex]]
: spec.requirements
).map(req => ({
name: req.name,
text: req.text,
scenarios: includeScenarios ? req.scenarios : [],
}));
+10 -41
View File
@@ -211,15 +211,6 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc
);
console.log();
if (instructions.warnings) {
for (const warning of instructions.warnings) {
console.log('<warning>');
console.log(escapeEnvelopeTags(warning));
console.log('</warning>');
console.log();
}
}
// Artifacts skipped via skip_specs get no creation directive: emitting the
// task/template anyway would prompt an agent to write spec files that
// validate then rejects as conflicting with the marker.
@@ -351,23 +342,6 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc
// Apply Instructions Command
// -----------------------------------------------------------------------------
interface LocatedTask extends ParsedTask {
sourcePath: string;
line: number;
}
/** Adds one-based source locations to parsed tasks without changing task parsing. */
function parseLocatedTasks(content: string, sourcePath: string): LocatedTask[] {
const tasks: LocatedTask[] = [];
for (const [index, line] of content.split('\n').entries()) {
const [task] = parseTaskLines(line);
if (task) tasks.push({ ...task, sourcePath, line: index + 1 });
}
return tasks;
}
/**
* Turns parsed task lines into the listed task items.
*
@@ -379,7 +353,7 @@ function parseLocatedTasks(content: string, sourcePath: string): LocatedTask[] {
* what puts apply in its "nothing to work on" state, so a file of nothing but
* text-less checkboxes asks to be rewritten instead of being called done.
*/
function toTaskItems(parsed: LocatedTask[]): TaskItem[] {
function toTaskItems(parsed: ParsedTask[]): TaskItem[] {
const tasks: TaskItem[] = [];
for (const task of parsed) {
@@ -388,8 +362,6 @@ function toTaskItems(parsed: LocatedTask[]): TaskItem[] {
id: `${tasks.length + 1}`,
description: task.description,
done: task.done,
sourcePath: task.sourcePath,
line: task.line,
});
}
@@ -599,7 +571,7 @@ export async function generateApplyInstructions(
// Parse every concrete file matched by apply.tracks. A tracking path may be
// a glob owned by an artifact with any ID, so treating it as one literal
// path loses task evidence for valid custom schemas.
let parsedTasks: LocatedTask[] = [];
let parsedTasks: ParsedTask[] = [];
const unavailableTrackingFiles: Array<{ path: string; reason: string }> = [];
let tracksFileExists = false;
if (tracksFile) {
@@ -608,7 +580,7 @@ export async function generateApplyInstructions(
for (const tracksPath of tracksPaths) {
try {
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
parsedTasks.push(...parseLocatedTasks(tasksContent, tracksPath));
parsedTasks.push(...parseTaskLines(tasksContent));
} catch (error) {
const code = (error as NodeJS.ErrnoException)?.code;
const message = error instanceof Error ? error.message : String(error);
@@ -689,16 +661,13 @@ export async function generateApplyInstructions(
instruction += `\nTask completion is not verified because tracking evidence was unavailable:\n${unavailableDetails}`;
}
const warnings = [
...(context.warnings ?? []),
...(await collectApplyWarnings({
state,
schema,
changeDir,
changeName,
skippedArtifacts: context.skippedArtifacts,
})),
];
const warnings = await collectApplyWarnings({
state,
schema,
changeDir,
changeName,
skippedArtifacts: context.skippedArtifacts,
});
return {
changeName,
-2
View File
@@ -32,8 +32,6 @@ export interface TaskItem {
id: string;
description: string;
done: boolean;
sourcePath: string;
line: number;
}
export interface ApplyInstructions {
+1 -15
View File
@@ -13,7 +13,6 @@ import {
toRootOutput,
withStoreFlag,
isStoreSelectedRoot,
findDeclaringProjectRoot,
} from '../../core/root-selection.js';
import {
loadChangeContext,
@@ -92,14 +91,6 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
// `Next:` line, so a store-selected root can never carry `--store` in one
// and drop it from the other.
const storeOptions = isStoreSelectedRoot(root) ? { storeId: root.storeId } : {};
// A store holds planning artifacts only; the project declaring it is
// where implementation edits go (#2013).
const implementationRoot = isStoreSelectedRoot(root)
? findDeclaringProjectRoot(root.storeId)
: null;
const statusOptions = implementationRoot
? { ...storeOptions, implementationRoot }
: storeOptions;
// Single definition of "load one change's status" so the batch and
// single-change payloads can never drift apart.
@@ -109,7 +100,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
changeDir: getChangeDir(planningHome, changeName),
planningHome,
}),
statusOptions
storeOptions
);
// Handle no-changes case gracefully — status is informational,
@@ -251,11 +242,6 @@ export function printStatusText(status: ChangeStatus, options: PrintStatusTextOp
console.log(`Change: ${status.changeName}`);
console.log(`Schema: ${status.schemaName}`);
if (status.warnings) {
for (const warning of status.warnings) {
console.log(chalk.yellow(`Warning: ${warning}`));
}
}
if (status.changeRoot) {
console.log(`Change root: ${status.changeRoot}`);
}
+2 -23
View File
@@ -25,13 +25,7 @@ import {
type SpecUpdate,
} from './specs-apply.js';
import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
import {
METADATA_FILENAME,
formatUnknownChangeMetadataKeysMessage,
readRetireCapabilitiesMarker,
readSkipSpecsMarker,
readUnknownChangeMetadataKeys,
} from '../utils/change-metadata.js';
import { METADATA_FILENAME, readRetireCapabilitiesMarker, readSkipSpecsMarker } from '../utils/change-metadata.js';
import { confirmPrompt, isNonInteractivePromptError } from '../utils/interactive.js';
import { FileSystemUtils } from '../utils/file-system.js';
import { folderStyleNameProblem } from './id.js';
@@ -1425,15 +1419,6 @@ export class ArchiveCommand {
);
}
const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
const unknownMetadataWarning =
unknownMetadataKeys.length > 0
? formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys)
: undefined;
if (unknownMetadataWarning && !json) {
console.warn(chalk.yellow(unknownMetadataWarning));
}
const skipValidation = options.validate === false || options.noValidate === true;
// Validate specs and change before archiving
@@ -2315,13 +2300,7 @@ export class ArchiveCommand {
path: archivePath,
specsUpdated,
...(totals ? { totals } : {}),
...(specWarnings.length > 0 || unknownMetadataWarning
? {
warnings: unknownMetadataWarning
? [...specWarnings, unknownMetadataWarning]
: specWarnings,
}
: {}),
...(specWarnings.length > 0 ? { warnings: specWarnings } : {}),
};
} finally {
if (archiveClaim) await releaseArchiveClaim(archiveClaim, claimPath).catch(() => undefined);
+2 -31
View File
@@ -8,12 +8,7 @@ import {
resolveArtifactOutputPath,
resolveArtifactOutputs,
} from './outputs.js';
import {
formatUnknownChangeMetadataKeysMessage,
readChangeMetadata,
readUnknownChangeMetadataKeys,
resolveSchemaForChange,
} from '../../utils/change-metadata.js';
import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
import { FileSystemUtils } from '../../utils/file-system.js';
import {
buildActionContext,
@@ -64,8 +59,6 @@ export interface ChangeContext {
planningHome?: PlanningHome;
/** Parsed change metadata, when present */
metadata?: ChangeMetadata;
/** Non-fatal metadata diagnostics for text and JSON command surfaces */
warnings?: string[];
/**
* Artifact IDs counted as complete only because the change declares
* skip_specs, not because their files exist. Kept separate so status can
@@ -121,8 +114,6 @@ export interface ArtifactInstructions {
skipped?: boolean;
/** Present only when skipped: tells the consumer not to create the artifact */
warning?: string;
/** Non-fatal metadata diagnostics */
warnings?: string[];
}
/**
@@ -196,8 +187,6 @@ export interface ChangeStatus {
applyRequires: string[];
/** Status of each artifact */
artifacts: ArtifactStatus[];
/** Non-fatal metadata diagnostics */
warnings?: string[];
}
export interface ArtifactPathSummary {
@@ -284,11 +273,6 @@ export function loadChangeContext(
);
const metadata = readChangeMetadata(changeDir, projectRoot) ?? undefined;
const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
const warnings =
unknownMetadataKeys.length > 0
? [formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys)]
: [];
const resolvedSchemaName = resolveSchemaForChange(changeDir, schemaName, projectRoot, {
metadata: metadata ?? null,
projectConfig: options.projectConfig,
@@ -321,7 +305,6 @@ export function loadChangeContext(
projectRoot,
...(options.planningHome ? { planningHome: options.planningHome } : {}),
...(metadata ? { metadata } : {}),
...(warnings.length > 0 ? { warnings } : {}),
...(skippedArtifacts.size > 0 ? { skippedArtifacts } : {}),
};
}
@@ -415,7 +398,6 @@ export function generateInstructions(
context: configContext,
rules: configRules,
...(options.references !== undefined ? { references: options.references } : {}),
...(context.warnings ? { warnings: context.warnings } : {}),
...(context.skippedArtifacts?.has(artifact.id)
? { skipped: true, warning: SKIP_SPECS_INSTRUCTIONS_WARNING }
: {}),
@@ -473,7 +455,7 @@ function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[
*/
export function formatChangeStatus(
context: ChangeContext,
options: { storeId?: string; implementationRoot?: string } = {}
options: { storeId?: string } = {}
): ChangeStatus {
// Load schema to get apply phase configuration
const schema = resolveSchema(context.schemaName, context.projectRoot);
@@ -552,18 +534,7 @@ export function formatChangeStatus(
actionContext: buildActionContext({
projectRoot: context.projectRoot,
artifactIds,
...(options.storeId
? {
store: {
id: options.storeId,
...(options.implementationRoot
? { implementationRoot: options.implementationRoot }
: {}),
},
}
: {}),
}),
artifacts: artifactStatuses,
...(context.warnings ? { warnings: context.warnings } : {}),
};
}
-13
View File
@@ -20,19 +20,6 @@ export const InitiativeLinkSchema = z.object({
export type InitiativeLink = z.infer<typeof InitiativeLinkSchema>;
/** Top-level keys ChangeMetadataSchema recognizes. Anything else is ignored. */
export const CHANGE_METADATA_KNOWN_KEYS = [
'schema',
'created',
'goal',
'affected_areas',
'initiative',
'skip_specs',
'retire_capabilities',
] as const;
export type ChangeMetadataKnownKey = (typeof CHANGE_METADATA_KNOWN_KEYS)[number];
// Per-change metadata schema. The schema field is validated against available
// workflow schemas when metadata is read or written.
export const ChangeMetadataSchema = z.object({
+2 -41
View File
@@ -33,11 +33,6 @@ export interface ChangeNextStepsInput {
export interface ActionContextInput {
projectRoot: string;
artifactIds: string[];
/**
* Set when the root is a store: the store holds the planning artifacts,
* and `implementationRoot` is the project that declares it, if any.
*/
store?: { id: string; implementationRoot?: string };
}
export function summarizePlanningHome(
@@ -56,48 +51,14 @@ export function summarizePlanningHome(
}
export function buildActionContext(input: ActionContextInput): ActionContext {
const scope = editScope(input);
// Keys stay in the published contract order.
return {
mode: 'repo-local',
sourceOfTruth: 'repo',
planningArtifacts: input.artifactIds,
linkedContext: [],
allowedEditRoots: scope.allowedEditRoots,
requiresAffectedAreaSelection: false,
constraints: scope.constraints,
};
}
/**
* A store holds planning artifacts only. The CLI does not route tasks to
* repos, so it names the declaring project on the current path as the edit
* root and has the agent ask before going anywhere else (#2013).
*/
function editScope(input: ActionContextInput): Pick<ActionContext, 'allowedEditRoots' | 'constraints'> {
if (!input.store) {
return {
allowedEditRoots: [input.projectRoot],
constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
};
}
const planning = `Change artifacts live in store '${input.store.id}' (${input.projectRoot}).`;
const { implementationRoot } = input.store;
if (implementationRoot) {
return {
allowedEditRoots: [implementationRoot, input.projectRoot],
constraints: [
`${planning} Implementation edits go in ${implementationRoot}, the project on the current path that declares this store; ask the user before editing any other repository.`,
],
};
}
return {
allowedEditRoots: [input.projectRoot],
constraints: [
`${planning} OpenSpec could not determine which repository implements this change; ask the user which repository to edit, and make implementation edits there.`,
],
requiresAffectedAreaSelection: false,
constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
};
}
@@ -1,51 +0,0 @@
/**
* AtomCode Command Adapter
*
* Formats project commands for AtomCode.
* https://github.com/atomgit-atomcode/atomcode#custom-commands
*/
import path from 'path';
import { stringify } from 'yaml';
import type { CommandContent, ToolCommandAdapter } from '../types.js';
/** A workflow declares its invocation input with an `**Input**:` heading. */
const INPUT_HEADING = /^\*\*Input\*\*:/m;
/**
* AtomCode adapter for command generation.
* File path: .atomcode/commands/opsx-<id>.md
* Frontmatter: name, description, args
*
* AtomCode's custom-command parser reads name and args literally without YAML
* unquoting, so these controlled identifiers must stay unquoted.
* The command name matches the filename.
*
* `args` mirrors what the workflow actually accepts. AtomCode executes an
* `args: none` command straight from the slash menu, while `optional` completes
* to `/name ` and waits for a second Enter. Advertising arguments a workflow
* never reads would cost every user that extra keystroke, so only workflows
* carrying an `**Input**:` contract declare `optional` and receive $ARGUMENTS.
*/
export const atomcodeAdapter: ToolCommandAdapter = {
toolId: 'atomcode',
getFilePath(commandId: string): string {
return path.join('.atomcode', 'commands', `opsx-${commandId}.md`);
},
formatFile(content: CommandContent): string {
// Keep ordinary descriptions plain for the literal custom-command parser,
// while keeping special values valid YAML for frontmatter consumers.
const description = stringify({ description: content.description }, { lineWidth: 0, blockQuote: false });
const acceptsInput = INPUT_HEADING.test(content.body);
const argumentsBlock = acceptsInput ? '\n**Provided arguments**: $ARGUMENTS\n' : '';
return `---
name: opsx-${content.id}
${description}args: ${acceptsInput ? 'optional' : 'none'}
---
${argumentsBlock}
${content.body}
`;
},
};
@@ -1,31 +0,0 @@
/**
* Code Studio Command Adapter
*
* Formats commands for Syncfusion Code Studio following its .prompt.md specification.
*/
import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';
import { escapeYamlValue } from '../yaml.js';
/**
* Code Studio adapter for command generation.
* File path: .codestudio/prompts/opsx-<id>.prompt.md
* Frontmatter: description
*/
export const codeStudioAdapter: ToolCommandAdapter = {
toolId: 'codestudio',
getFilePath(commandId: string): string {
return path.join('.codestudio', 'prompts', `opsx-${commandId}.prompt.md`);
},
formatFile(content: CommandContent): string {
return `---
description: ${escapeYamlValue(content.description)}
---
${content.body}
`;
},
};
@@ -1,32 +0,0 @@
/**
* EasyCode Command Adapter
*
* Formats commands for OrionStarAI/EasyCode using its TOML command format.
* https://github.com/OrionStarAI/EasyCode
*/
import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';
import { escapeTomlBasicString, escapeTomlMultilineBasicString } from '../toml.js';
/**
* EasyCode adapter for command generation.
* File path: .easycode/commands/opsx/<id>.toml
*
* Format:
* description = "<basic-string>" single-line, backslash/quote-safe
* prompt = """<multiline-string>""" multiline, backslash/triple-quote-safe
*/
export const easycodeAdapter: ToolCommandAdapter = {
toolId: 'easycode',
getFilePath(commandId: string): string {
return path.join('.easycode', 'commands', 'opsx', `${commandId}.toml`);
},
formatFile(content: CommandContent): string {
const safeDesc = escapeTomlBasicString(content.description);
const safeBody = escapeTomlMultilineBasicString(content.body);
return `description = "${safeDesc}"\n\nprompt = """\n${safeBody}\n"""\n`;
},
};
+37 -1
View File
@@ -7,7 +7,43 @@
import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';
import { escapeTomlBasicString, escapeTomlMultilineBasicString } from '../toml.js';
/**
* Control characters (C0 except tab/newline/carriage return, plus DEL) are
* invalid inside TOML strings and must be written as escapes.
*/
const TOML_CONTROL_CHARS = new RegExp('[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]', 'g');
/**
* TOML basic strings are escape-active: a backslash or double quote in the
* value breaks the file if written raw. Newlines cannot appear in a
* single-line basic string at all, so they are escaped too.
*/
function escapeTomlBasicString(value: string): string {
return value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
/**
* Multiline basic strings keep raw newlines and tabs, but backslashes are
* still escape-active, any run of three quotes would end the string, and the
* same control characters are invalid as in single-line basic strings — a
* lone carriage return included (only LF and CRLF may appear raw; CRLF is
* normalized away so the emitted file is single-convention). Escapes are
* introduced after backslash-doubling so they are not re-doubled.
*/
function escapeTomlMultilineBasicString(value: string): string {
return value
.replace(/\r\n/g, '\n')
.replace(/\\/g, '\\\\')
.replace(/"""/g, '""\\"')
.replace(/\r/g, '\\r')
.replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
/**
* Gemini adapter for command generation.
@@ -1,35 +0,0 @@
/**
* GigaCode Command Adapter
*
* Formats commands for GigaCode using its Markdown custom command format.
* Project commands live in `.gigacode/commands/` and accept an optional
* `description` field in YAML frontmatter.
*
* @see https://gitverse.ru/docs/ai/ai-assistant-gigacode/gigacode-cli/commands
*/
import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';
import { escapeYamlValue } from '../yaml.js';
/**
* GigaCode adapter for command generation.
* File path: .gigacode/commands/opsx-<id>.md
* Format: Markdown with description frontmatter
*/
export const gigacodeAdapter: ToolCommandAdapter = {
toolId: 'gigacode',
getFilePath(commandId: string): string {
return path.join('.gigacode', 'commands', `opsx-${commandId}.md`);
},
formatFile(content: CommandContent): string {
return `---
description: ${escapeYamlValue(content.description)}
---
${content.body}
`;
},
};
@@ -6,24 +6,20 @@
export { amazonQAdapter } from './amazon-q.js';
export { antigravityAdapter } from './antigravity.js';
export { atomcodeAdapter } from './atomcode.js';
export { auggieAdapter } from './auggie.js';
export { bobAdapter } from './bob.js';
export { claudeAdapter } from './claude.js';
export { clineAdapter } from './cline.js';
export { commandCodeAdapter } from './command-code.js';
export { codebuddyAdapter } from './codebuddy.js';
export { codeStudioAdapter } from './codestudio.js';
export { continueAdapter } from './continue.js';
export { costrictAdapter } from './costrict.js';
export { crushAdapter } from './crush.js';
export { cursorAdapter } from './cursor.js';
export { devinAdapter } from './devin.js';
export { easycodeAdapter } from './easycode.js';
export { factoryAdapter } from './factory.js';
export { geminiAdapter } from './gemini.js';
export { githubCopilotAdapter } from './github-copilot.js';
export { gigacodeAdapter } from './gigacode.js';
export { iflowAdapter } from './iflow.js';
export { junieAdapter } from './junie.js';
export { kilocodeAdapter } from './kilocode.js';
-8
View File
@@ -8,7 +8,6 @@
import type { ToolCommandAdapter } from './types.js';
import { amazonQAdapter } from './adapters/amazon-q.js';
import { antigravityAdapter } from './adapters/antigravity.js';
import { atomcodeAdapter } from './adapters/atomcode.js';
import { auggieAdapter } from './adapters/auggie.js';
import { bobAdapter } from './adapters/bob.js';
import { claudeAdapter } from './adapters/claude.js';
@@ -16,16 +15,13 @@ import { clineAdapter } from './adapters/cline.js';
import { commandCodeAdapter } from './adapters/command-code.js';
import { devinAdapter } from './adapters/devin.js';
import { codebuddyAdapter } from './adapters/codebuddy.js';
import { codeStudioAdapter } from './adapters/codestudio.js';
import { continueAdapter } from './adapters/continue.js';
import { costrictAdapter } from './adapters/costrict.js';
import { crushAdapter } from './adapters/crush.js';
import { cursorAdapter } from './adapters/cursor.js';
import { easycodeAdapter } from './adapters/easycode.js';
import { factoryAdapter } from './adapters/factory.js';
import { geminiAdapter } from './adapters/gemini.js';
import { githubCopilotAdapter } from './adapters/github-copilot.js';
import { gigacodeAdapter } from './adapters/gigacode.js';
import { iflowAdapter } from './adapters/iflow.js';
import { junieAdapter } from './adapters/junie.js';
import { kilocodeAdapter } from './adapters/kilocode.js';
@@ -51,7 +47,6 @@ export class CommandAdapterRegistry {
static {
CommandAdapterRegistry.register(amazonQAdapter);
CommandAdapterRegistry.register(antigravityAdapter);
CommandAdapterRegistry.register(atomcodeAdapter);
CommandAdapterRegistry.register(auggieAdapter);
CommandAdapterRegistry.register(bobAdapter);
CommandAdapterRegistry.register(claudeAdapter);
@@ -59,16 +54,13 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(commandCodeAdapter);
CommandAdapterRegistry.register(devinAdapter);
CommandAdapterRegistry.register(codebuddyAdapter);
CommandAdapterRegistry.register(codeStudioAdapter);
CommandAdapterRegistry.register(continueAdapter);
CommandAdapterRegistry.register(costrictAdapter);
CommandAdapterRegistry.register(crushAdapter);
CommandAdapterRegistry.register(cursorAdapter);
CommandAdapterRegistry.register(easycodeAdapter);
CommandAdapterRegistry.register(factoryAdapter);
CommandAdapterRegistry.register(geminiAdapter);
CommandAdapterRegistry.register(githubCopilotAdapter);
CommandAdapterRegistry.register(gigacodeAdapter);
CommandAdapterRegistry.register(iflowAdapter);
CommandAdapterRegistry.register(junieAdapter);
CommandAdapterRegistry.register(kilocodeAdapter);
-41
View File
@@ -1,41 +0,0 @@
/**
* Shared TOML string escaping for command adapters.
*/
/**
* Control characters (C0 except tab/newline/carriage return, plus DEL) are
* invalid inside TOML strings and must be written as escapes.
*/
const TOML_CONTROL_CHARS = new RegExp('[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]', 'g');
/**
* TOML basic strings are escape-active: a backslash or double quote in the
* value breaks the file if written raw. Newlines cannot appear in a
* single-line basic string at all, so they are escaped too.
*/
export function escapeTomlBasicString(value: string): string {
return value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
/**
* Multiline basic strings keep raw newlines and tabs, but backslashes are
* still escape-active, any run of three quotes would end the string, and the
* same control characters are invalid as in single-line basic strings — a
* lone carriage return included (only LF and CRLF may appear raw; CRLF is
* normalized away so the emitted file is single-convention). Escapes are
* introduced after backslash-doubling so they are not re-doubled.
*/
export function escapeTomlMultilineBasicString(value: string): string {
return value
.replace(/\r\n/g, '\n')
.replace(/\\/g, '\\\\')
.replace(/"""/g, '""\\"')
.replace(/\r/g, '\\r')
.replace(TOML_CONTROL_CHARS, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
+1 -1
View File
@@ -19,7 +19,7 @@ export function resolveCommandSurfaceCapability(toolId: string): CommandSurfaceC
return 'adapter-backed';
}
if (toolId === 'codex' || toolId === 'warp') {
if (toolId === 'codex') {
return 'skills-invocable';
}
-19
View File
@@ -55,17 +55,6 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
},
],
},
{
name: 'version',
description: 'Report the installed OpenSpec version and update availability',
flags: [
COMMON_FLAGS.json,
{
name: 'check',
description: 'Check the registry for a newer version',
},
],
},
{
name: 'list',
description: 'List items (changes by default, or specs with --specs)',
@@ -78,14 +67,6 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
name: 'changes',
description: 'List changes explicitly (default)',
},
{
name: 'archived',
description: 'Show only archived changes',
},
{
name: 'all',
description: 'Show both active and archived changes',
},
{
name: 'sort',
description: 'Sort order: "recent" (default) or "name"',
@@ -220,10 +220,8 @@ export class ZshInstaller {
// Remove lines between markers (inclusive)
lines.splice(startIndex, endIndex - startIndex + 1);
// Install puts the block at the top of the file followed by one blank
// separator line; drop that line too so the file reads as it did before.
// Everything else, including blank lines the user had at the top, is left as is.
if (startIndex === 0 && lines.length > 0 && lines[0].trim() === '') {
// Remove trailing empty lines at the start if the markers were at the top
while (lines.length > 0 && lines[0].trim() === '') {
lines.shift();
}
+6 -6
View File
@@ -22,13 +22,13 @@ export function serializeConfig(config: Partial<ProjectConfig>): string {
} else {
// Context section with comments
lines.push('# Project context (optional)');
lines.push('# Add only constraints that should guide OpenSpec artifacts and workflows.');
lines.push('# Include constraints an agent cannot infer by reading the code.');
lines.push('# Keep general project documentation and discoverable codebase facts out.');
lines.push('# This is shown to AI when creating artifacts.');
lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.');
lines.push('# Example:');
lines.push('# context: |');
lines.push('# Designs and tasks must cover Windows, macOS, and Linux');
lines.push('# Write all artifacts in Spanish');
lines.push('# Tech stack: TypeScript, React, Node.js');
lines.push('# We use conventional commits');
lines.push('# Domain: e-commerce platform');
lines.push('');
}
@@ -39,7 +39,7 @@ export function serializeConfig(config: Partial<ProjectConfig>): string {
lines.push('# rules:');
lines.push('# proposal:');
lines.push('# - Keep proposals under 500 words');
lines.push('# - Always state what is out of scope');
lines.push('# - Always include a "Non-goals" section');
lines.push('# tasks:');
lines.push('# - Break tasks into chunks of max 2 hours');
lines.push('');
+1 -11
View File
@@ -40,37 +40,29 @@ export interface AIToolOption {
export const AI_TOOLS: AIToolOption[] = [
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq', requiresIdeRestart: true },
{ name: 'Amp', value: 'amp', available: true, successLabel: 'Amp', skillsDir: '.agents', detectionPaths: ['.amp', '.agents/skills'] },
// Antigravity moved workspace skills and workflows from `.agent` to the
// shared `.agents` root in v1.20.5. Detection keys off `.agent` and
// `.agents/workflows` rather than the bare `.agents` root: that root is
// shared with Codex, Zed, and the vendor-neutral target, so its presence
// alone says nothing about Antigravity.
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agents', legacySkillsDirs: ['.agent'], detectionPaths: ['.agent', '.agents/workflows'], requiresIdeRestart: true },
{ name: 'AtomCode', value: 'atomcode', available: true, successLabel: 'AtomCode', skillsDir: '.atomcode' },
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
{ name: 'IBM Bob', value: 'bob', available: true, successLabel: 'IBM Bob', skillsDir: '.bob' },
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline', requiresIdeRestart: true },
{ name: 'Command Code', value: 'command-code', available: true, successLabel: 'Command Code', skillsDir: '.commandcode' },
{ name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] },
{ name: 'DeepSeek Harness', value: 'dsh', available: true, successLabel: 'DeepSeek Harness', skillsDir: '.dsh' },
{ name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'], requiresIdeRestart: true },
{ name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
{ name: 'Code Studio', value: 'codestudio', available: true, successLabel: 'Code Studio', skillsDir: '.codestudio', requiresIdeRestart: true },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue', requiresIdeRestart: true },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec', requiresIdeRestart: true },
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor', requiresIdeRestart: true },
{ name: 'EasyCode', value: 'easycode', available: true, successLabel: 'EasyCode', skillsDir: '.easycode' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' },
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'], requiresIdeRestart: true },
{ name: 'GigaCode', value: 'gigacode', available: true, successLabel: 'GigaCode', skillsDir: '.gigacode' },
{ name: 'Grok Build', value: 'grok', available: true, successLabel: 'Grok Build', skillsDir: '.grok' },
{ name: 'GSD', value: 'gsd', available: true, successLabel: 'GSD', skillsDir: '.agents', detectionPaths: ['.gsd'] },
{ name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes', detectionPaths: ['.hermes', 'HERMES.md', '.hermes.md'], setupNote: "Hermes only loads skills from ~/.hermes/skills by default. Add this project's .hermes/skills directory to skills.external_dirs in ~/.hermes/config.yaml so Hermes picks up the generated OpenSpec skills." },
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie', requiresIdeRestart: true },
@@ -89,8 +81,6 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
{ name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo', requiresIdeRestart: true },
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true },
{ name: 'Veai', value: 'veai', available: true, successLabel: 'Veai', skillsDir: '.veai' },
{ name: 'Warp', value: 'warp', available: true, successLabel: 'Warp', skillsDir: '.warp', detectionPaths: ['.warp', 'WARP.md'] },
{ name: 'Zed Agent', value: 'zed', available: true, successLabel: 'Zed Agent', skillsDir: '.agents', detectionPaths: ['.zed', '.agents/skills'] },
{ name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
// Vendor-neutral target for assistants that read the shared `.agents` root.
+13 -39
View File
@@ -18,7 +18,6 @@ import {
storePointerProblem,
} from './project-config.js';
import { findRepoPlanningRootSync } from './planning-home.js';
import { resolveOpenSpecRoot } from './root-selection.js';
import { ANCHORED_OPENSPEC_DIRS, ensureDirectoryAnchor } from './openspec-root.js';
import { getSkillReferenceTransformer, getTransformerForTool, usesNaturalLanguageSkillReferences } from '../utils/command-references.js';
import {
@@ -189,7 +188,7 @@ export class InitCommand {
}
async execute(targetPath: string): Promise<void> {
const projectPath = FileSystemUtils.canonicalizeExistingPath(targetPath);
const projectPath = path.resolve(targetPath);
const openspecDir = OPENSPEC_DIR_NAME;
const openspecPath = path.join(projectPath, openspecDir);
@@ -204,7 +203,6 @@ export class InitCommand {
// finds the nearest ancestor root (so pointer-repo subdirectories
// refuse exactly where a normal command would resolve the pointer).
const guardRoot = findRepoPlanningRootSync(projectPath);
let integrationsOnly = false;
if (guardRoot) {
const { hasPlanningShape, pointer } = classifyOpenSpecDir(guardRoot);
if (!hasPlanningShape) {
@@ -216,36 +214,18 @@ export class InitCommand {
);
}
if (pointer.value !== undefined) {
if (path.resolve(guardRoot) !== projectPath) {
throw new Error(
`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
'Run openspec init from the pointer repo root to install integrations.'
);
}
// A valid pointer repo already has its planning root in the declared
// store. Init should still be able to install agent integrations in
// the code repo, without creating a second local planning root.
await resolveOpenSpecRoot({ startPath: projectPath });
integrationsOnly = true;
throw new Error(
`This repo's planning is externalized to store '${pointer.value}' (${pointer.filePath}). ` +
`Remove the store: line first to convert this repo to a local OpenSpec root.`
);
}
}
}
if (!integrationsOnly) {
await this.assertLanguageCanBeApplied(projectPath, openspecPath);
} else if (this.language) {
throw new Error(
'--language cannot update an external store through a pointer repo. ' +
'Run init in the store root, or edit the store config directly.'
);
}
await this.assertLanguageCanBeApplied(projectPath, openspecPath);
// Pointer repos keep their local planning files untouched. Normal init may
// still remove OpenSpec-managed artifacts from older layouts.
const deferredLegacyCleanup = integrationsOnly
? null
: await this.handleLegacyCleanup(projectPath, extendMode);
// Check for legacy artifacts and handle cleanup
const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
// Migrate OpenSpec-managed skills left in renamed tool directories
// (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
@@ -302,11 +282,8 @@ export class InitCommand {
// config.yaml exists so future non-interactive updates honor it.
const copilotDecision = await this.resolveCopilotCloudDecision(projectPath, validatedTools);
// Pointer repos only receive integrations. Their planning structure and
// config stay in the declared store.
if (!integrationsOnly) {
await this.createDirectoryStructure(openspecPath, extendMode);
}
// Create directory structure and config
await this.createDirectoryStructure(openspecPath, extendMode);
// Generate skills and commands for each tool
const results = await this.generateSkillsAndCommands(
@@ -321,16 +298,13 @@ export class InitCommand {
await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
}
// Create config.yaml if needed. A pointer repo already has the config that
// declares its store, so preserve it byte-for-byte.
const configStatus = integrationsOnly
? 'exists' as const
: await this.createConfig(openspecPath, extendMode);
// Create config.yaml if needed
const configStatus = await this.createConfig(openspecPath, extendMode);
// Persist an explicit Copilot cloud decision so `openspec update` (which
// never prompts) honors it. Best-effort: a config-write failure must not
// fail an otherwise-successful init.
if (!integrationsOnly && copilotDecision.persist !== undefined) {
if (copilotDecision.persist !== undefined) {
try {
await persistCopilotCloudOptIn(projectPath, copilotDecision.persist);
} catch {
+7 -11
View File
@@ -944,10 +944,10 @@ export function formatDetectionSummary(detection: LegacyDetectionResult): string
lines.push('as before.');
lines.push('');
// Section 1: Files to remove entirely
// Section 1: Files to remove (no user content to preserve)
if (removals.length > 0) {
lines.push(chalk.bold('Files to remove'));
lines.push(chalk.dim('These files will be deleted entirely. Back up any custom content before proceeding:'));
lines.push(chalk.dim('No user content to preserve:'));
for (const { path } of removals) {
lines.push(` • ${path}`);
}
@@ -1185,15 +1185,11 @@ export function formatProjectMdMigrationHint(): string {
lines.push(' • openspec/project.md');
lines.push(chalk.dim(' We won\'t delete this file. It may contain useful project context.'));
lines.push('');
lines.push(chalk.dim(' Ask your AI assistant:'));
lines.push(chalk.dim(' The new openspec/config.yaml has a "context:" section for planning'));
lines.push(chalk.dim(' context. This is included in every OpenSpec request and works more'));
lines.push(chalk.dim(' reliably than the old project.md approach.'));
lines.push('');
lines.push(chalk.dim(' Review openspec/project.md and migrate its useful content to'));
lines.push(chalk.dim(' openspec/config.yaml. Keep context concise: include only project-wide'));
lines.push(chalk.dim(' facts needed during artifact creation, apply, and archive. Move'));
lines.push(chalk.dim(' artifact-specific guidance into rules for the matching artifacts.'));
lines.push(chalk.dim(' Move guidance for apply or archive into the matching operations entry.'));
lines.push(chalk.dim(' Leave out generic, outdated, or verbose material. Do not delete project.md.'));
lines.push('');
lines.push(chalk.dim(' Review config.yaml, then delete project.md when ready.'));
lines.push(chalk.dim(' Review project.md, move any useful content to config.yaml\'s context'));
lines.push(chalk.dim(' section, then delete the file when ready.'));
return lines.join('\n');
}
+23 -55
View File
@@ -16,7 +16,6 @@ interface ChangeInfo {
completedTasks: number;
totalTasks: number;
lastModified: Date;
archived: boolean;
/** Set when the entry is a namespace folder rather than a change (#1846). */
nested?: string[];
}
@@ -25,8 +24,6 @@ interface ListOptions {
sort?: 'recent' | 'name';
json?: boolean;
root?: RootOutput;
archived?: boolean;
all?: boolean;
}
function isMissingPathError(error: unknown): boolean {
@@ -61,9 +58,8 @@ async function readChangeDirectoryEntries(changesDir: string): Promise<Dirent[]>
/**
* Get the most recent modification time of any file in a directory (recursive).
* Falls back to the directory's own mtime if no files are found.
* Archived links use their own mtime: moving a change can break relative targets.
*/
async function getLastModified(dirPath: string, archived: boolean = false): Promise<Date> {
async function getLastModified(dirPath: string): Promise<Date> {
let latest: Date | null = null;
async function walk(dir: string): Promise<void> {
@@ -74,9 +70,7 @@ async function getLastModified(dirPath: string, archived: boolean = false): Prom
if (entry.isDirectory()) {
await walk(fullPath);
} else {
const stat = archived && entry.isSymbolicLink()
? await fs.lstat(fullPath)
: await fs.stat(fullPath);
const stat = await fs.stat(fullPath);
if (latest === null || stat.mtime > latest) {
latest = stat.mtime;
}
@@ -125,34 +119,22 @@ function formatRelativeTime(date: Date): string {
export class ListCommand {
async execute(targetPath: string = '.', mode: 'changes' | 'specs' = 'changes', options: ListOptions = {}): Promise<void> {
const { sort = 'recent', json = false, root, archived = false, all = false } = options;
if (mode === 'specs' && (archived || all)) {
throw new Error('--archived and --all can only be used when listing changes.');
}
const { sort = 'recent', json = false, root } = options;
if (mode === 'changes') {
const changesDir = path.join(targetPath, 'openspec', 'changes');
const archiveDir = path.join(changesDir, 'archive');
const includeArchived = archived || all;
// Read the parent even for --archived: Windows can report ENOENT for
// changes/archive when changes is a file, hiding a malformed root.
// Get all directories in changes (excluding archive)
const entries = await readChangeDirectoryEntries(changesDir);
const activeDirs = !archived || all ? entries
const changeDirs = entries
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
.map(entry => ({ name: entry.name, parent: changesDir, archived: false })) : [];
const archiveEntries = includeArchived ? await readChangeDirectoryEntries(archiveDir) : [];
const archivedDirs = archiveEntries
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
.map(entry => ({ name: entry.name, parent: archiveDir, archived: true }));
const changeDirs = [...activeDirs, ...archivedDirs];
.map(entry => entry.name);
if (changeDirs.length === 0) {
if (json) {
console.log(JSON.stringify({ changes: [], ...(root ? { root } : {}) }, null, 2));
} else {
console.log(all ? 'No changes found.' : archived ? 'No archived changes found.' : 'No active changes found.');
console.log('No active changes found.');
}
return;
}
@@ -163,27 +145,21 @@ export class ListCommand {
// A directory that only wraps nested change directories is still listed -
// hiding it would hide a real change whenever the probe is wrong - but it
// is listed as what it is, so the nesting stops failing silently (#1846).
const nestedFindings = await findNestedChanges(
changesDir,
activeDirs.map((changeDir) => changeDir.name)
);
const nestedFindings = await findNestedChanges(changesDir, changeDirs);
const nestedByName = new Map<string, NestedChangeFinding>(
nestedFindings.map((finding) => [finding.name, finding])
);
for (const changeDir of changeDirs) {
const progress = await getTaskProgressForChange(changeDir.parent, changeDir.name, targetPath);
const changePath = path.join(changeDir.parent, changeDir.name);
const lastModified = await getLastModified(changePath, changeDir.archived);
const progress = await getTaskProgressForChange(changesDir, changeDir, targetPath);
const changePath = path.join(changesDir, changeDir);
const lastModified = await getLastModified(changePath);
changes.push({
name: changeDir.name,
name: changeDir,
completedTasks: progress.completed,
totalTasks: progress.total,
lastModified,
archived: changeDir.archived,
...(!changeDir.archived && nestedByName.has(changeDir.name)
? { nested: nestedByName.get(changeDir.name)!.nested }
: {})
...(nestedByName.has(changeDir) ? { nested: nestedByName.get(changeDir)!.nested } : {})
});
}
@@ -202,7 +178,6 @@ export class ListCommand {
totalTasks: c.totalTasks,
lastModified: c.lastModified.toISOString(),
status: c.totalTasks === 0 ? 'no-tasks' : c.completedTasks === c.totalTasks ? 'complete' : 'in-progress',
...(includeArchived ? { archived: c.archived } : {}),
...(c.nested ? { nested: c.nested } : {})
}));
// Additive: the entries keep their shape so existing consumers are
@@ -222,23 +197,16 @@ export class ListCommand {
}
// Display results
const groups = [
{ heading: 'Changes:', changes: changes.filter(change => !change.archived) },
{ heading: 'Archived Changes:', changes: changes.filter(change => change.archived) }
].filter(group => group.changes.length > 0);
for (const [index, group] of groups.entries()) {
if (index > 0) console.log('');
console.log(group.heading);
const padding = ' ';
const nameWidth = Math.max(...group.changes.map(c => c.name.length));
for (const change of group.changes) {
const paddedName = change.name.padEnd(nameWidth);
const status = change.nested
? 'not a change'
: formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
const timeAgo = formatRelativeTime(change.lastModified);
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
}
console.log('Changes:');
const padding = ' ';
const nameWidth = Math.max(...changes.map(c => c.name.length));
for (const change of changes) {
const paddedName = change.name.padEnd(nameWidth);
const status = change.nested
? 'not a change'
: formatTaskStatus({ total: change.totalTasks, completed: change.completedTasks });
const timeAgo = formatRelativeTime(change.lastModified);
console.log(`${padding}${paddedName} ${status.padEnd(12)} ${timeAgo}`);
}
for (const finding of nestedFindings) {
console.log('');
-5
View File
@@ -1,6 +1,5 @@
import { Spec, Change, Requirement, Scenario, Delta, DeltaOperation } from '../schemas/index.js';
import { buildCodeFenceMask, extractRequirementText, hasScenarioBody } from './requirement-text.js';
import { normalizeRequirementName, scenarioNameFromHeaderText } from './requirement-blocks.js';
export interface Section {
level: number;
@@ -161,9 +160,6 @@ export class MarkdownParser {
const scenarios = this.parseScenarios(child);
requirements.push({
// The name archive matches on, so a JSON reader can cite a requirement
// the way a MODIFIED or REMOVED header must.
name: normalizeRequirementName(child.title.replace(/^Requirement:\s*/i, '')),
text,
scenarios,
});
@@ -180,7 +176,6 @@ export class MarkdownParser {
// body is not a scenario; the delta counter applies the same rule.
if (hasScenarioBody(scenarioSection.content)) {
scenarios.push({
name: scenarioNameFromHeaderText(scenarioSection.title),
rawText: scenarioSection.content
});
}
+2 -10
View File
@@ -630,16 +630,8 @@ function scenarioHeaderAt(lines: string[], mask: boolean[], index: number): bool
* ATX-closed, one not) are not mistaken for a dropped scenario.
*/
function scenarioNameAt(line: string): string {
return scenarioNameFromHeaderText(line.replace(SCENARIO_HEADER, ''));
}
/**
* scenarioNameAt for header text whose leading `####` is already gone, as the
* section parser (MarkdownParser) holds it, so `show --json` names a scenario
* exactly as the MODIFIED loss check does.
*/
export function scenarioNameFromHeaderText(headerText: string): string {
return headerText
return line
.replace(SCENARIO_HEADER, '')
// Optional ATX closing sequence. CommonMark only treats a trailing `#` run
// as a close when it is preceded by a space or tab — not any Unicode space —
// so this uses `[ \t]`, not `\s`. A looser `\s` could strip a `#` run after
+3 -49
View File
@@ -247,54 +247,6 @@ function parseDeclarationList(raw: unknown): DeclarationEntry[] | undefined {
export const MAX_CONTEXT_SIZE = 50 * 1024; // 50KB hard limit, shared with the references index
/**
* Build the warning for an artifact whose `rules:` list is not an array of
* strings. Names the offending index and what YAML actually produced there, so
* a config that silently loses an entire rule set can be fixed without
* bisecting the list by hand. A bare `-` item containing an unquoted ": " is the
* common cause: YAML reads it as a mapping, so the hint points at quoting.
*/
function describeRulesShapeError(artifactId: string, rules: unknown): string {
const base = `Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`;
if (!Array.isArray(rules)) {
return `${base}. rules.${artifactId} is ${describeYamlType(rules)}`;
}
const bad = rules
.map((rule, index) => ({ rule, index }))
.filter(({ rule }) => typeof rule !== 'string');
if (bad.length === 0) {
return base;
}
// Name every offending index with its own shape, so a mixed list does not
// have to be re-bisected one item at a time.
const details = bad
.map(({ rule, index }) => `rules.${artifactId}[${index}] is ${describeYamlType(rule)}`)
.join('; ');
// A bare `-` item with an unquoted ": " is the common cause: YAML reads it as
// a mapping. Say so rather than leaving the reader to work out the quoting.
const hasMapping = bad.some(({ rule }) => rule !== null && typeof rule === 'object' && !Array.isArray(rule));
const hint = hasMapping
? ' — an unquoted ": " makes YAML read the item as a key/value pair; quote the whole scalar to keep it a string.'
: '';
return `${base}. ${details}${hint}`;
}
/** Name a YAML value's shape in a warning, e.g. "a mapping" or "a number". */
function describeYamlType(value: unknown): string {
if (value === null) return 'null';
if (Array.isArray(value)) return 'a nested list';
const type = typeof value;
if (type === 'object') return 'a mapping';
if (type === 'number' || type === 'boolean') return `a ${type}`;
return `a ${type}`;
}
/**
* Read and parse openspec/config.yaml from project root.
* Uses resilient parsing - validates each field independently using Zod safeParse.
@@ -389,7 +341,9 @@ export function readProjectConfig(projectRoot: string): ProjectConfig | null {
);
}
} else {
console.warn(describeRulesShapeError(artifactId, rules));
console.warn(
`Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`
);
}
}
+1 -3
View File
@@ -239,9 +239,7 @@ export function renderReferencedStoresSection(entries: ReferenceIndexEntry[]): s
* let hostile content forge instruction lines (slice 6.1 hardening).
*/
export function sanitizeInline(value: string, maxLength = 300): string {
const flattened = value
.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u206f]+/g, ' ')
.trim();
const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
}
-20
View File
@@ -484,26 +484,6 @@ export function isStoreSelectedRoot(
return root.storeId !== undefined;
}
/**
* The project on the current path whose `store:` pointer names `storeId`,
* as a canonical path (the root walk resolves aliases). Null when the
* nearest root is a real planning root or points at a different store.
*/
export function findDeclaringProjectRoot(
storeId: string,
startPath: string = process.cwd()
): string | null {
const nearestRoot = findQualifyingRootSync(startPath);
if (!nearestRoot) {
return null;
}
const { hasPlanningShape, pointer } = classifyOpenSpecDir(nearestRoot);
if (hasPlanningShape || pointer.value !== storeId) {
return null;
}
return nearestRoot;
}
/**
* Human-mode verification signal for a selected store. Written to stderr so
* raw-Markdown and agent-consumed stdout payloads stay clean.

Some files were not shown because too many files have changed in this diff Show More