mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ca9c1eb1 | ||
|
|
81c2f9fce3 | ||
|
|
cd4f9e4a5f | ||
|
|
cf2859a520 | ||
|
|
c879d13d5f | ||
|
|
e232080d09 | ||
|
|
781c7f9447 | ||
|
|
d1642cb58c | ||
|
|
a7f08b8a46 | ||
|
|
f197804a38 | ||
|
|
ded99e27de | ||
|
|
772819417a | ||
|
|
405d8b51ed | ||
|
|
e923d05c8f | ||
|
|
de4141f0fd | ||
|
|
ee3ca3821e | ||
|
|
56528ea454 | ||
|
|
3de7c72c26 | ||
|
|
297092cb25 | ||
|
|
c21d897261 | ||
|
|
070de01dfa | ||
|
|
5a360c2088 | ||
|
|
3c3e6e3d42 | ||
|
|
486cfeb8ca | ||
|
|
f7d426ab9d | ||
|
|
817cdb64be | ||
|
|
88692b3bb4 | ||
|
|
5fe58590ba | ||
|
|
9557b43aaf | ||
|
|
d28fb49c1c | ||
|
|
bda85565ef | ||
|
|
28f864351b | ||
|
|
9a40b58928 | ||
|
|
baad4494b4 | ||
|
|
e70dcc7c82 | ||
|
|
187298289d | ||
|
|
42671df890 | ||
|
|
e7a9512d7c | ||
|
|
fffe3d3850 | ||
|
|
0ff63dbae6 | ||
|
|
d4e1c77eba | ||
|
|
79b6aa9c98 |
@@ -1,2 +1,5 @@
|
||||
# Default code ownership
|
||||
* @Fission-AI/openspec-maintainers
|
||||
|
||||
# Route docs-lab changes to the docs owner for review
|
||||
/docs-lab/ @TabishB
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -184,6 +184,11 @@ 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
|
||||
@@ -210,6 +215,42 @@ 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
|
||||
|
||||
|
||||
@@ -1,5 +1,93 @@
|
||||
# @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
|
||||
|
||||
@@ -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>
|
||||
Tech stack: TypeScript, Node.js
|
||||
Domain: e-commerce platform
|
||||
Designs and tasks must cover Windows, macOS, and Linux
|
||||
Write all artifacts in Spanish
|
||||
</project_context>
|
||||
|
||||
<!-- From your config.yaml: rules for tasks -->
|
||||
@@ -74,18 +74,17 @@ The last column is exact, so a field reaches only the steps listed there. In par
|
||||
|
||||
### context
|
||||
|
||||
`context` is what the agent should know up front when planning a change, whether it's creating an artifact, applying tasks, or archiving:
|
||||
`context` is background the agent receives when it creates an artifact, applies tasks, or archives a change.
|
||||
|
||||
```yaml
|
||||
context: |
|
||||
We ship cross-platform; designs and tasks must cover Windows, macOS, and Linux
|
||||
Tech stack: TypeScript, Node.js, Commander.js
|
||||
We use conventional commits
|
||||
We ship cross-platform. Designs and tasks must cover Windows, macOS, and Linux
|
||||
Write all artifacts in Spanish
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
### rules
|
||||
|
||||
|
||||
@@ -162,3 +162,9 @@ 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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> The two-minute pass that catches wrong turns before they're code.
|
||||
|
||||
<!-- Skeleton: headings only. -->
|
||||
<!-- Partial draft: the plan-review sections are still headings only. -->
|
||||
|
||||
## The two-minute pass
|
||||
|
||||
@@ -13,3 +13,24 @@
|
||||
## 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.
|
||||
|
||||
@@ -15,4 +15,12 @@ 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
|
||||
|
||||
@@ -223,6 +223,23 @@ 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:
|
||||
|
||||
+157
-8
@@ -50,6 +50,7 @@ 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. |
|
||||
|
||||
@@ -77,6 +78,21 @@ 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 |
|
||||
@@ -88,6 +104,7 @@ With no `--tools`, init prompts you to pick tools in an interactive terminal. Ou
|
||||
| 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. |
|
||||
@@ -117,7 +134,7 @@ Restart your IDE for the new commands to take effect.
|
||||
**Exit codes**
|
||||
|
||||
- `0`: setup completed.
|
||||
- `1`: invalid `--tools` or `--profile` value, or a non-interactive run with no tools detected and no `--tools`.
|
||||
- `1`: invalid `--tools` or `--profile` value, a non-interactive run with no tools detected and no `--tools`, or an invalid store-only invocation.
|
||||
|
||||
## openspec update
|
||||
|
||||
@@ -409,12 +426,14 @@ Config updated. Run `openspec update` in your projects to apply.
|
||||
Lists changes, or specs with `--specs`.
|
||||
|
||||
```bash
|
||||
openspec list # changes, most recently modified first
|
||||
openspec list --specs # specs with requirement counts
|
||||
openspec list --json # machine-readable, includes the resolved root
|
||||
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
|
||||
```
|
||||
|
||||
Rows come from `openspec/changes/` and `openspec/specs/` under the resolved root. The `archive/` folder is skipped.
|
||||
Rows come from `openspec/changes/` and `openspec/specs/` under the resolved root. The default change listing skips `openspec/changes/archive/`.
|
||||
|
||||
**Options**
|
||||
|
||||
@@ -422,6 +441,8 @@ 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. |
|
||||
@@ -435,6 +456,16 @@ 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
|
||||
@@ -460,7 +491,9 @@ Specs:
|
||||
}
|
||||
```
|
||||
|
||||
An empty listing prints `No active changes found.` or `No specs found.` and still exits 0.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
@@ -541,9 +574,11 @@ 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"
|
||||
}
|
||||
]
|
||||
@@ -558,9 +593,11 @@ 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:
|
||||
A spec with `--json` lists its requirements with scenarios. Requirements and scenarios carry the same `name` fields as change JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -570,9 +607,11 @@ A spec with `--json` lists its requirements with scenarios:
|
||||
"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"
|
||||
}
|
||||
]
|
||||
@@ -604,7 +643,9 @@ 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). Specs list with requirement counts, largest first.
|
||||
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.
|
||||
|
||||
**Options**
|
||||
|
||||
@@ -623,11 +664,16 @@ 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
|
||||
@@ -639,6 +685,21 @@ 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.
|
||||
@@ -1290,6 +1351,8 @@ 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**
|
||||
@@ -2137,6 +2200,92 @@ 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,4 +59,7 @@ 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. The one exception is unknown top-level keys, which are ignored rather than rejected.
|
||||
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.
|
||||
|
||||
@@ -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's instructions |
|
||||
| `context` | string | No | Injected into every artifact, apply, and archive |
|
||||
| `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. The limit is 50KB, and a larger value is ignored with a warning.
|
||||
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.
|
||||
|
||||
### rules
|
||||
|
||||
@@ -77,14 +77,13 @@ A filled-in config.yaml:
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Tech stack: TypeScript, React, Node.js
|
||||
We use conventional commits
|
||||
Domain: e-commerce platform
|
||||
Designs and tasks must cover Windows, macOS, and Linux
|
||||
Write all artifacts in Spanish
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- Keep proposals under 500 words
|
||||
- Always include a "Non-goals" section
|
||||
- Always state what is out of scope
|
||||
tasks:
|
||||
- Break tasks into chunks of max 2 hours
|
||||
|
||||
|
||||
@@ -67,9 +67,12 @@ The template the agent receives as the output format ([templates/proposal.md](ht
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- 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. -->
|
||||
<!-- 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. -->
|
||||
- `<capability-path>`: <brief description of what this capability covers>
|
||||
|
||||
### Modified Capabilities
|
||||
@@ -98,7 +101,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`. 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`. 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.
|
||||
- **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.
|
||||
|
||||
@@ -205,6 +208,7 @@ 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`
|
||||
|
||||
@@ -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 touches only the tasks file, checking off each finished task (`- [ ]` to `- [x]`). |
|
||||
| **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. |
|
||||
| **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 via `openspec-sync-specs`. 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. When `openspec-sync-specs` is installed, it runs that workflow. Otherwise, it merges the delta specs into the main specs itself. 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
|
||||
|
||||
@@ -15,23 +15,31 @@ 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` |
|
||||
| Bob Shell | `bob` | `.bob/skills/` | `/openspec-apply-change` | `.bob/commands/` | `/opsx-apply` |
|
||||
| IBM Bob | `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` |
|
||||
@@ -47,6 +55,8 @@ 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 |
|
||||
@@ -54,14 +64,21 @@ 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. Gemini CLI takes
|
||||
`.toml`, Continue `.prompt`, Kiro and GitHub Copilot `.prompt.md`. The spelling you
|
||||
type is the same either way.
|
||||
- **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.
|
||||
|
||||
## 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
|
||||
@@ -69,8 +86,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 Codex, Zed Agent, and
|
||||
the `agents` target. OpenSpec writes that skill tree once while still writing
|
||||
- **Shared skills**: Antigravity shares `.agents/skills/` with Amp, Codex, Zed Agent,
|
||||
and the `agents` target. OpenSpec writes that skill tree once while still writing
|
||||
Antigravity commands to `.agents/workflows/`.
|
||||
|
||||
### Cline
|
||||
@@ -88,13 +105,26 @@ 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 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 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.
|
||||
- **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.
|
||||
@@ -114,6 +144,15 @@ 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
|
||||
@@ -127,6 +166,13 @@ 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,
|
||||
@@ -134,7 +180,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**: Antigravity, Codex, Zed Agent, and this target share
|
||||
- **Alongside other targets**: Amp, 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.
|
||||
|
||||
@@ -158,6 +158,19 @@ 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.
|
||||
|
||||
@@ -34,6 +34,22 @@ 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:
|
||||
|
||||
@@ -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" }`.
|
||||
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.
|
||||
|
||||
### 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" }`. `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" }`. 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.
|
||||
|
||||
`--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.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
|
||||
@@ -15,87 +15,98 @@
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems f;
|
||||
|
||||
pkgsFor =
|
||||
system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlays.default ];
|
||||
};
|
||||
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 = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs) lib;
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
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";
|
||||
};
|
||||
});
|
||||
default = pkgs.openspec;
|
||||
inherit (pkgs) openspec;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2025-12-29
|
||||
@@ -0,0 +1,31 @@
|
||||
## 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.
|
||||
@@ -0,0 +1,28 @@
|
||||
## 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
|
||||
@@ -0,0 +1,20 @@
|
||||
## 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.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-28
|
||||
@@ -0,0 +1,131 @@
|
||||
# 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`.
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,120 @@
|
||||
## 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
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-11
|
||||
@@ -0,0 +1,109 @@
|
||||
## 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 |
|
||||
@@ -0,0 +1,40 @@
|
||||
## 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`)
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# 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
|
||||
@@ -0,0 +1,24 @@
|
||||
## 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
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-15
|
||||
@@ -0,0 +1,86 @@
|
||||
## 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._
|
||||
@@ -0,0 +1,31 @@
|
||||
## 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
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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
|
||||
@@ -0,0 +1,30 @@
|
||||
## 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`
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-25
|
||||
@@ -0,0 +1,25 @@
|
||||
## 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.
|
||||
@@ -0,0 +1,12 @@
|
||||
## 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/`
|
||||
@@ -0,0 +1,6 @@
|
||||
## 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.
|
||||
@@ -56,6 +56,31 @@ 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,17 +183,6 @@ 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.
|
||||
|
||||
@@ -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:
|
||||
- `Change delivery + workflows`
|
||||
- `Change delivery only`
|
||||
- `Change workflows only`
|
||||
- `Delivery and workflows`
|
||||
- `Delivery only`
|
||||
- `Workflows only`
|
||||
- `Keep current settings (exit)`
|
||||
|
||||
#### Scenario: Delivery prompt marks current selection
|
||||
|
||||
@@ -231,6 +231,12 @@ 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.
|
||||
|
||||
@@ -45,6 +45,35 @@ 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.
|
||||
@@ -126,4 +155,3 @@ The dashboard SHALL display changes without tasks in a separate "Draft" section.
|
||||
|
||||
- **WHEN** multiple draft changes exist
|
||||
- **THEN** system sorts them alphabetically by name
|
||||
|
||||
|
||||
@@ -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** match using normalized headers: `normalize(header) = trim(header)`
|
||||
- **AND** compare headers with case-sensitive equality after normalization
|
||||
- **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
|
||||
|
||||
#### Scenario: Handling requirement renames
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ 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 a spec the sync deliberately kept and reported as verified too
|
||||
- **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** 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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "1.13.2",
|
||||
"version": "1.14.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
Generated
+188
-188
@@ -61,7 +61,7 @@ importers:
|
||||
version: 4.1.11(vitest@4.1.11)
|
||||
eslint:
|
||||
specifier: ^10.5.0
|
||||
version: 10.10.0
|
||||
version: 10.11.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.0(eslint@10.10.0)(typescript@6.0.3)
|
||||
version: 8.70.1(eslint@10.11.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.4':
|
||||
resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==}
|
||||
'@rollup/rollup-android-arm-eabi@4.63.5':
|
||||
resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.4':
|
||||
resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==}
|
||||
'@rollup/rollup-android-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.4':
|
||||
resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==}
|
||||
'@rollup/rollup-darwin-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.4':
|
||||
resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==}
|
||||
'@rollup/rollup-darwin-x64@4.63.5':
|
||||
resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.4':
|
||||
resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==}
|
||||
'@rollup/rollup-freebsd-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.4':
|
||||
resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==}
|
||||
'@rollup/rollup-freebsd-x64@4.63.5':
|
||||
resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.4':
|
||||
resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==}
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.5':
|
||||
resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.4':
|
||||
resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==}
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.5':
|
||||
resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==}
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.4':
|
||||
resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==}
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==}
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.4':
|
||||
resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==}
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==}
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.4':
|
||||
resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==}
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==}
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.4':
|
||||
resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==}
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==}
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==}
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.4':
|
||||
resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==}
|
||||
'@rollup/rollup-linux-x64-musl@4.63.5':
|
||||
resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.4':
|
||||
resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==}
|
||||
'@rollup/rollup-openbsd-x64@4.63.5':
|
||||
resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.4':
|
||||
resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==}
|
||||
'@rollup/rollup-openharmony-arm64@4.63.5':
|
||||
resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.4':
|
||||
resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==}
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.5':
|
||||
resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.4':
|
||||
resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==}
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.5':
|
||||
resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.4':
|
||||
resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==}
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.5':
|
||||
resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.4':
|
||||
resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==}
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.5':
|
||||
resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==}
|
||||
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.0':
|
||||
resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==}
|
||||
'@typescript-eslint/eslint-plugin@8.70.1':
|
||||
resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.70.0
|
||||
'@typescript-eslint/parser': ^8.70.1
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/parser@8.70.0':
|
||||
resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==}
|
||||
'@typescript-eslint/parser@8.70.1':
|
||||
resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==}
|
||||
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.0':
|
||||
resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==}
|
||||
'@typescript-eslint/project-service@8.70.1':
|
||||
resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==}
|
||||
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.0':
|
||||
resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==}
|
||||
'@typescript-eslint/scope-manager@8.70.1':
|
||||
resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.70.0':
|
||||
resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==}
|
||||
'@typescript-eslint/tsconfig-utils@8.70.1':
|
||||
resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==}
|
||||
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.0':
|
||||
resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==}
|
||||
'@typescript-eslint/type-utils@8.70.1':
|
||||
resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==}
|
||||
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.0':
|
||||
resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==}
|
||||
'@typescript-eslint/types@8.70.1':
|
||||
resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.70.0':
|
||||
resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==}
|
||||
'@typescript-eslint/typescript-estree@8.70.1':
|
||||
resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/utils@8.70.0':
|
||||
resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==}
|
||||
'@typescript-eslint/utils@8.70.1':
|
||||
resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==}
|
||||
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.0':
|
||||
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
|
||||
'@typescript-eslint/visitor-keys@8.70.1':
|
||||
resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==}
|
||||
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.9:
|
||||
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||
brace-expansion@5.0.12:
|
||||
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
||||
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.10.0:
|
||||
resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==}
|
||||
eslint@10.11.0:
|
||||
resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==}
|
||||
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.9:
|
||||
resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==}
|
||||
ignore@7.0.10:
|
||||
resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==}
|
||||
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.4:
|
||||
resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==}
|
||||
rollup@4.63.5:
|
||||
resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==}
|
||||
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.0:
|
||||
resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==}
|
||||
typescript-eslint@8.70.1:
|
||||
resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==}
|
||||
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.10.0)':
|
||||
'@eslint-community/eslint-utils@4.10.1(eslint@10.11.0)':
|
||||
dependencies:
|
||||
eslint: 10.10.0
|
||||
eslint: 10.11.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.4':
|
||||
'@rollup/rollup-android-arm-eabi@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.63.4':
|
||||
'@rollup/rollup-android-arm64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.63.4':
|
||||
'@rollup/rollup-darwin-arm64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.63.4':
|
||||
'@rollup/rollup-darwin-x64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.63.4':
|
||||
'@rollup/rollup-freebsd-arm64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.63.4':
|
||||
'@rollup/rollup-freebsd-x64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.4':
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.4':
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.4':
|
||||
'@rollup/rollup-linux-arm64-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.4':
|
||||
'@rollup/rollup-linux-arm64-musl@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.4':
|
||||
'@rollup/rollup-linux-loong64-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.4':
|
||||
'@rollup/rollup-linux-loong64-musl@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.4':
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.4':
|
||||
'@rollup/rollup-linux-ppc64-musl@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.4':
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.4':
|
||||
'@rollup/rollup-linux-riscv64-musl@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.4':
|
||||
'@rollup/rollup-linux-s390x-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.4':
|
||||
'@rollup/rollup-linux-x64-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.63.4':
|
||||
'@rollup/rollup-linux-x64-musl@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.63.4':
|
||||
'@rollup/rollup-openbsd-x64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.63.4':
|
||||
'@rollup/rollup-openharmony-arm64@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.4':
|
||||
'@rollup/rollup-win32-arm64-msvc@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.4':
|
||||
'@rollup/rollup-win32-ia32-msvc@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.4':
|
||||
'@rollup/rollup-win32-x64-gnu@4.63.5':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.4':
|
||||
'@rollup/rollup-win32-x64-msvc@4.63.5':
|
||||
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.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/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)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@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
|
||||
'@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
|
||||
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.0(eslint@10.10.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/parser@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@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
|
||||
'@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
|
||||
debug: 4.4.3
|
||||
eslint: 10.10.0
|
||||
eslint: 10.11.0
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.70.0(typescript@6.0.3)':
|
||||
'@typescript-eslint/project-service@8.70.1(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.70.0
|
||||
'@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
debug: 4.4.3
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.70.0':
|
||||
'@typescript-eslint/scope-manager@8.70.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.70.0
|
||||
'@typescript-eslint/visitor-keys': 8.70.0
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
'@typescript-eslint/visitor-keys': 8.70.1
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)':
|
||||
'@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)':
|
||||
dependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.70.0(eslint@10.10.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/type-utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@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)
|
||||
'@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)
|
||||
debug: 4.4.3
|
||||
eslint: 10.10.0
|
||||
eslint: 10.11.0
|
||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.70.0': {}
|
||||
'@typescript-eslint/types@8.70.1': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.70.0(typescript@6.0.3)':
|
||||
'@typescript-eslint/typescript-estree@8.70.1(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@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
|
||||
'@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
|
||||
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.0(eslint@10.10.0)(typescript@6.0.3)':
|
||||
'@typescript-eslint/utils@8.70.1(eslint@10.11.0)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@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
|
||||
'@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
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.70.0':
|
||||
'@typescript-eslint/visitor-keys@8.70.1':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.70.0
|
||||
'@typescript-eslint/types': 8.70.1
|
||||
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.9:
|
||||
brace-expansion@5.0.12:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
@@ -2282,9 +2282,9 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@10.10.0:
|
||||
eslint@10.11.0:
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0)
|
||||
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.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.9: {}
|
||||
ignore@7.0.10: {}
|
||||
|
||||
import-meta-resolve@4.2.0: {}
|
||||
|
||||
@@ -2495,7 +2495,7 @@ snapshots:
|
||||
|
||||
minimatch@10.2.6:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.9
|
||||
brace-expansion: 5.0.12
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
@@ -2580,36 +2580,36 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rollup@4.63.4:
|
||||
rollup@4.63.5:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
optionalDependencies:
|
||||
'@napi-rs/lzma-linux-x64-gnu': 1.5.1
|
||||
'@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
|
||||
'@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
|
||||
fsevents: 2.3.3
|
||||
|
||||
run-parallel@1.2.0:
|
||||
@@ -2686,13 +2686,13 @@ snapshots:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
typescript-eslint@8.70.0(eslint@10.10.0)(typescript@6.0.3):
|
||||
typescript-eslint@8.70.1(eslint@10.11.0)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@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-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: 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.4
|
||||
rollup: 4.63.5
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.43
|
||||
|
||||
@@ -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`. 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`. 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.
|
||||
- **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,6 +94,7 @@ 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`
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
<!-- 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. -->
|
||||
<!-- 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. -->
|
||||
- `<capability-path>`: <brief description of what this capability covers>
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
@@ -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
|
||||
- Task list with status, source path, and source line
|
||||
- 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,7 +107,9 @@ 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
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- 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
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
@@ -187,6 +189,7 @@ 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
|
||||
|
||||
@@ -146,10 +146,21 @@ 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; a spec the sync deliberately kept and reported is also a match
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -206,6 +206,8 @@ 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
|
||||
@@ -220,7 +222,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; a spec the sync deliberately kept and reported is also a match
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
+48
-1
@@ -16,6 +16,11 @@ 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';
|
||||
@@ -169,6 +174,41 @@ 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)
|
||||
@@ -355,12 +395,17 @@ 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; sort?: string; json?: boolean; store?: string; storePath?: string }) => {
|
||||
.action(async (options?: { specs?: boolean; changes?: boolean; archived?: boolean; all?: 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 },
|
||||
@@ -377,6 +422,8 @@ program
|
||||
await listCommand.execute(root.path, mode, {
|
||||
sort,
|
||||
json: options?.json,
|
||||
archived: options?.archived,
|
||||
all: options?.all,
|
||||
...(options?.json ? { root: toRootOutput(root) } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -66,6 +66,7 @@ function filterSpec(spec: Spec, options: ShowOptions): Spec {
|
||||
? [spec.requirements[requirementIndex]]
|
||||
: spec.requirements
|
||||
).map(req => ({
|
||||
name: req.name,
|
||||
text: req.text,
|
||||
scenarios: includeScenarios ? req.scenarios : [],
|
||||
}));
|
||||
|
||||
@@ -211,6 +211,15 @@ 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.
|
||||
@@ -342,6 +351,23 @@ 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.
|
||||
*
|
||||
@@ -353,7 +379,7 @@ export function printInstructionsText(instructions: ArtifactInstructions, isBloc
|
||||
* 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: ParsedTask[]): TaskItem[] {
|
||||
function toTaskItems(parsed: LocatedTask[]): TaskItem[] {
|
||||
const tasks: TaskItem[] = [];
|
||||
|
||||
for (const task of parsed) {
|
||||
@@ -362,6 +388,8 @@ function toTaskItems(parsed: ParsedTask[]): TaskItem[] {
|
||||
id: `${tasks.length + 1}`,
|
||||
description: task.description,
|
||||
done: task.done,
|
||||
sourcePath: task.sourcePath,
|
||||
line: task.line,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -571,7 +599,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: ParsedTask[] = [];
|
||||
let parsedTasks: LocatedTask[] = [];
|
||||
const unavailableTrackingFiles: Array<{ path: string; reason: string }> = [];
|
||||
let tracksFileExists = false;
|
||||
if (tracksFile) {
|
||||
@@ -580,7 +608,7 @@ export async function generateApplyInstructions(
|
||||
for (const tracksPath of tracksPaths) {
|
||||
try {
|
||||
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
|
||||
parsedTasks.push(...parseTaskLines(tasksContent));
|
||||
parsedTasks.push(...parseLocatedTasks(tasksContent, tracksPath));
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException)?.code;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -661,13 +689,16 @@ export async function generateApplyInstructions(
|
||||
instruction += `\nTask completion is not verified because tracking evidence was unavailable:\n${unavailableDetails}`;
|
||||
}
|
||||
|
||||
const warnings = await collectApplyWarnings({
|
||||
state,
|
||||
schema,
|
||||
changeDir,
|
||||
changeName,
|
||||
skippedArtifacts: context.skippedArtifacts,
|
||||
});
|
||||
const warnings = [
|
||||
...(context.warnings ?? []),
|
||||
...(await collectApplyWarnings({
|
||||
state,
|
||||
schema,
|
||||
changeDir,
|
||||
changeName,
|
||||
skippedArtifacts: context.skippedArtifacts,
|
||||
})),
|
||||
];
|
||||
|
||||
return {
|
||||
changeName,
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface TaskItem {
|
||||
id: string;
|
||||
description: string;
|
||||
done: boolean;
|
||||
sourcePath: string;
|
||||
line: number;
|
||||
}
|
||||
|
||||
export interface ApplyInstructions {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
toRootOutput,
|
||||
withStoreFlag,
|
||||
isStoreSelectedRoot,
|
||||
findDeclaringProjectRoot,
|
||||
} from '../../core/root-selection.js';
|
||||
import {
|
||||
loadChangeContext,
|
||||
@@ -91,6 +92,14 @@ 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.
|
||||
@@ -100,7 +109,7 @@ export async function statusCommand(options: StatusOptions): Promise<void> {
|
||||
changeDir: getChangeDir(planningHome, changeName),
|
||||
planningHome,
|
||||
}),
|
||||
storeOptions
|
||||
statusOptions
|
||||
);
|
||||
|
||||
// Handle no-changes case gracefully — status is informational,
|
||||
@@ -242,6 +251,11 @@ 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}`);
|
||||
}
|
||||
|
||||
+23
-2
@@ -25,7 +25,13 @@ import {
|
||||
type SpecUpdate,
|
||||
} from './specs-apply.js';
|
||||
import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
|
||||
import { METADATA_FILENAME, readRetireCapabilitiesMarker, readSkipSpecsMarker } from '../utils/change-metadata.js';
|
||||
import {
|
||||
METADATA_FILENAME,
|
||||
formatUnknownChangeMetadataKeysMessage,
|
||||
readRetireCapabilitiesMarker,
|
||||
readSkipSpecsMarker,
|
||||
readUnknownChangeMetadataKeys,
|
||||
} from '../utils/change-metadata.js';
|
||||
import { confirmPrompt, isNonInteractivePromptError } from '../utils/interactive.js';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { folderStyleNameProblem } from './id.js';
|
||||
@@ -1419,6 +1425,15 @@ 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
|
||||
@@ -2300,7 +2315,13 @@ export class ArchiveCommand {
|
||||
path: archivePath,
|
||||
specsUpdated,
|
||||
...(totals ? { totals } : {}),
|
||||
...(specWarnings.length > 0 ? { warnings: specWarnings } : {}),
|
||||
...(specWarnings.length > 0 || unknownMetadataWarning
|
||||
? {
|
||||
warnings: unknownMetadataWarning
|
||||
? [...specWarnings, unknownMetadataWarning]
|
||||
: specWarnings,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
} finally {
|
||||
if (archiveClaim) await releaseArchiveClaim(archiveClaim, claimPath).catch(() => undefined);
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
resolveArtifactOutputPath,
|
||||
resolveArtifactOutputs,
|
||||
} from './outputs.js';
|
||||
import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
|
||||
import {
|
||||
formatUnknownChangeMetadataKeysMessage,
|
||||
readChangeMetadata,
|
||||
readUnknownChangeMetadataKeys,
|
||||
resolveSchemaForChange,
|
||||
} from '../../utils/change-metadata.js';
|
||||
import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import {
|
||||
buildActionContext,
|
||||
@@ -59,6 +64,8 @@ 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
|
||||
@@ -114,6 +121,8 @@ 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[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,6 +196,8 @@ export interface ChangeStatus {
|
||||
applyRequires: string[];
|
||||
/** Status of each artifact */
|
||||
artifacts: ArtifactStatus[];
|
||||
/** Non-fatal metadata diagnostics */
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface ArtifactPathSummary {
|
||||
@@ -273,6 +284,11 @@ 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,
|
||||
@@ -305,6 +321,7 @@ export function loadChangeContext(
|
||||
projectRoot,
|
||||
...(options.planningHome ? { planningHome: options.planningHome } : {}),
|
||||
...(metadata ? { metadata } : {}),
|
||||
...(warnings.length > 0 ? { warnings } : {}),
|
||||
...(skippedArtifacts.size > 0 ? { skippedArtifacts } : {}),
|
||||
};
|
||||
}
|
||||
@@ -398,6 +415,7 @@ 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 }
|
||||
: {}),
|
||||
@@ -455,7 +473,7 @@ function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[
|
||||
*/
|
||||
export function formatChangeStatus(
|
||||
context: ChangeContext,
|
||||
options: { storeId?: string } = {}
|
||||
options: { storeId?: string; implementationRoot?: string } = {}
|
||||
): ChangeStatus {
|
||||
// Load schema to get apply phase configuration
|
||||
const schema = resolveSchema(context.schemaName, context.projectRoot);
|
||||
@@ -534,7 +552,18 @@ 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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,19 @@ 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({
|
||||
|
||||
@@ -33,6 +33,11 @@ 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(
|
||||
@@ -51,14 +56,48 @@ 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: [input.projectRoot],
|
||||
allowedEditRoots: scope.allowedEditRoots,
|
||||
requiresAffectedAreaSelection: false,
|
||||
constraints: ['Repo-local change artifacts and implementation edits are scoped to this project.'],
|
||||
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.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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`;
|
||||
},
|
||||
};
|
||||
@@ -7,43 +7,7 @@
|
||||
import path from 'path';
|
||||
import type { CommandContent, ToolCommandAdapter } from '../types.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')}`);
|
||||
}
|
||||
import { escapeTomlBasicString, escapeTomlMultilineBasicString } from '../toml.js';
|
||||
|
||||
/**
|
||||
* Gemini adapter for command generation.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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,20 +6,24 @@
|
||||
|
||||
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,6 +8,7 @@
|
||||
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';
|
||||
@@ -15,13 +16,16 @@ 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';
|
||||
@@ -47,6 +51,7 @@ export class CommandAdapterRegistry {
|
||||
static {
|
||||
CommandAdapterRegistry.register(amazonQAdapter);
|
||||
CommandAdapterRegistry.register(antigravityAdapter);
|
||||
CommandAdapterRegistry.register(atomcodeAdapter);
|
||||
CommandAdapterRegistry.register(auggieAdapter);
|
||||
CommandAdapterRegistry.register(bobAdapter);
|
||||
CommandAdapterRegistry.register(claudeAdapter);
|
||||
@@ -54,13 +59,16 @@ 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);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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')}`);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function resolveCommandSurfaceCapability(toolId: string): CommandSurfaceC
|
||||
return 'adapter-backed';
|
||||
}
|
||||
|
||||
if (toolId === 'codex') {
|
||||
if (toolId === 'codex' || toolId === 'warp') {
|
||||
return 'skills-invocable';
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,17 @@ 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)',
|
||||
@@ -67,6 +78,14 @@ 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,8 +220,10 @@ export class ZshInstaller {
|
||||
// Remove lines between markers (inclusive)
|
||||
lines.splice(startIndex, endIndex - startIndex + 1);
|
||||
|
||||
// Remove trailing empty lines at the start if the markers were at the top
|
||||
while (lines.length > 0 && lines[0].trim() === '') {
|
||||
// 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() === '') {
|
||||
lines.shift();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ export function serializeConfig(config: Partial<ProjectConfig>): string {
|
||||
} else {
|
||||
// Context section with comments
|
||||
lines.push('# Project context (optional)');
|
||||
lines.push('# This is shown to AI when creating artifacts.');
|
||||
lines.push('# Add your tech stack, conventions, style guides, domain knowledge, etc.');
|
||||
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('# Example:');
|
||||
lines.push('# context: |');
|
||||
lines.push('# Tech stack: TypeScript, React, Node.js');
|
||||
lines.push('# We use conventional commits');
|
||||
lines.push('# Domain: e-commerce platform');
|
||||
lines.push('# Designs and tasks must cover Windows, macOS, and Linux');
|
||||
lines.push('# Write all artifacts in Spanish');
|
||||
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 include a "Non-goals" section');
|
||||
lines.push('# - Always state what is out of scope');
|
||||
lines.push('# tasks:');
|
||||
lines.push('# - Break tasks into chunks of max 2 hours');
|
||||
lines.push('');
|
||||
|
||||
+11
-1
@@ -40,29 +40,37 @@ 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: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
|
||||
{ name: 'IBM Bob', value: 'bob', available: true, successLabel: 'IBM Bob', 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 },
|
||||
@@ -81,6 +89,8 @@ 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.
|
||||
|
||||
+39
-13
@@ -18,6 +18,7 @@ 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 {
|
||||
@@ -188,7 +189,7 @@ export class InitCommand {
|
||||
}
|
||||
|
||||
async execute(targetPath: string): Promise<void> {
|
||||
const projectPath = path.resolve(targetPath);
|
||||
const projectPath = FileSystemUtils.canonicalizeExistingPath(targetPath);
|
||||
const openspecDir = OPENSPEC_DIR_NAME;
|
||||
const openspecPath = path.join(projectPath, openspecDir);
|
||||
|
||||
@@ -203,6 +204,7 @@ 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) {
|
||||
@@ -214,18 +216,36 @@ export class InitCommand {
|
||||
);
|
||||
}
|
||||
if (pointer.value !== undefined) {
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.assertLanguageCanBeApplied(projectPath, openspecPath);
|
||||
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.'
|
||||
);
|
||||
}
|
||||
|
||||
// Check for legacy artifacts and handle cleanup
|
||||
const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
|
||||
// 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);
|
||||
|
||||
// Migrate OpenSpec-managed skills left in renamed tool directories
|
||||
// (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
|
||||
@@ -282,8 +302,11 @@ export class InitCommand {
|
||||
// config.yaml exists so future non-interactive updates honor it.
|
||||
const copilotDecision = await this.resolveCopilotCloudDecision(projectPath, validatedTools);
|
||||
|
||||
// Create directory structure and config
|
||||
await this.createDirectoryStructure(openspecPath, extendMode);
|
||||
// Pointer repos only receive integrations. Their planning structure and
|
||||
// config stay in the declared store.
|
||||
if (!integrationsOnly) {
|
||||
await this.createDirectoryStructure(openspecPath, extendMode);
|
||||
}
|
||||
|
||||
// Generate skills and commands for each tool
|
||||
const results = await this.generateSkillsAndCommands(
|
||||
@@ -298,13 +321,16 @@ export class InitCommand {
|
||||
await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
|
||||
}
|
||||
|
||||
// Create config.yaml if needed
|
||||
const configStatus = await this.createConfig(openspecPath, extendMode);
|
||||
// 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);
|
||||
|
||||
// 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 (copilotDecision.persist !== undefined) {
|
||||
if (!integrationsOnly && copilotDecision.persist !== undefined) {
|
||||
try {
|
||||
await persistCopilotCloudOptIn(projectPath, copilotDecision.persist);
|
||||
} catch {
|
||||
|
||||
@@ -944,10 +944,10 @@ export function formatDetectionSummary(detection: LegacyDetectionResult): string
|
||||
lines.push('as before.');
|
||||
lines.push('');
|
||||
|
||||
// Section 1: Files to remove (no user content to preserve)
|
||||
// Section 1: Files to remove entirely
|
||||
if (removals.length > 0) {
|
||||
lines.push(chalk.bold('Files to remove'));
|
||||
lines.push(chalk.dim('No user content to preserve:'));
|
||||
lines.push(chalk.dim('These files will be deleted entirely. Back up any custom content before proceeding:'));
|
||||
for (const { path } of removals) {
|
||||
lines.push(` • ${path}`);
|
||||
}
|
||||
@@ -1185,11 +1185,15 @@ 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(' 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(chalk.dim(' Ask your AI assistant:'));
|
||||
lines.push('');
|
||||
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.'));
|
||||
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.'));
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
+55
-23
@@ -16,6 +16,7 @@ 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[];
|
||||
}
|
||||
@@ -24,6 +25,8 @@ interface ListOptions {
|
||||
sort?: 'recent' | 'name';
|
||||
json?: boolean;
|
||||
root?: RootOutput;
|
||||
archived?: boolean;
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
function isMissingPathError(error: unknown): boolean {
|
||||
@@ -58,8 +61,9 @@ 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): Promise<Date> {
|
||||
async function getLastModified(dirPath: string, archived: boolean = false): Promise<Date> {
|
||||
let latest: Date | null = null;
|
||||
|
||||
async function walk(dir: string): Promise<void> {
|
||||
@@ -70,7 +74,9 @@ async function getLastModified(dirPath: string): Promise<Date> {
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else {
|
||||
const stat = await fs.stat(fullPath);
|
||||
const stat = archived && entry.isSymbolicLink()
|
||||
? await fs.lstat(fullPath)
|
||||
: await fs.stat(fullPath);
|
||||
if (latest === null || stat.mtime > latest) {
|
||||
latest = stat.mtime;
|
||||
}
|
||||
@@ -119,22 +125,34 @@ 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 } = options;
|
||||
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.');
|
||||
}
|
||||
|
||||
if (mode === 'changes') {
|
||||
const changesDir = path.join(targetPath, 'openspec', 'changes');
|
||||
const archiveDir = path.join(changesDir, 'archive');
|
||||
const includeArchived = archived || all;
|
||||
|
||||
// Get all directories in changes (excluding archive)
|
||||
// Read the parent even for --archived: Windows can report ENOENT for
|
||||
// changes/archive when changes is a file, hiding a malformed root.
|
||||
const entries = await readChangeDirectoryEntries(changesDir);
|
||||
const changeDirs = entries
|
||||
const activeDirs = !archived || all ? entries
|
||||
.filter(entry => entry.isDirectory() && entry.name !== 'archive')
|
||||
.map(entry => entry.name);
|
||||
.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];
|
||||
|
||||
if (changeDirs.length === 0) {
|
||||
if (json) {
|
||||
console.log(JSON.stringify({ changes: [], ...(root ? { root } : {}) }, null, 2));
|
||||
} else {
|
||||
console.log('No active changes found.');
|
||||
console.log(all ? 'No changes found.' : archived ? 'No archived changes found.' : 'No active changes found.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -145,21 +163,27 @@ 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, changeDirs);
|
||||
const nestedFindings = await findNestedChanges(
|
||||
changesDir,
|
||||
activeDirs.map((changeDir) => changeDir.name)
|
||||
);
|
||||
const nestedByName = new Map<string, NestedChangeFinding>(
|
||||
nestedFindings.map((finding) => [finding.name, finding])
|
||||
);
|
||||
|
||||
for (const changeDir of changeDirs) {
|
||||
const progress = await getTaskProgressForChange(changesDir, changeDir, targetPath);
|
||||
const changePath = path.join(changesDir, changeDir);
|
||||
const lastModified = await getLastModified(changePath);
|
||||
const progress = await getTaskProgressForChange(changeDir.parent, changeDir.name, targetPath);
|
||||
const changePath = path.join(changeDir.parent, changeDir.name);
|
||||
const lastModified = await getLastModified(changePath, changeDir.archived);
|
||||
changes.push({
|
||||
name: changeDir,
|
||||
name: changeDir.name,
|
||||
completedTasks: progress.completed,
|
||||
totalTasks: progress.total,
|
||||
lastModified,
|
||||
...(nestedByName.has(changeDir) ? { nested: nestedByName.get(changeDir)!.nested } : {})
|
||||
archived: changeDir.archived,
|
||||
...(!changeDir.archived && nestedByName.has(changeDir.name)
|
||||
? { nested: nestedByName.get(changeDir.name)!.nested }
|
||||
: {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,6 +202,7 @@ 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
|
||||
@@ -197,16 +222,23 @@ export class ListCommand {
|
||||
}
|
||||
|
||||
// Display results
|
||||
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}`);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
for (const finding of nestedFindings) {
|
||||
console.log('');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
@@ -160,6 +161,9 @@ 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,
|
||||
});
|
||||
@@ -176,6 +180,7 @@ 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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -630,8 +630,16 @@ 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 line
|
||||
.replace(SCENARIO_HEADER, '')
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -247,6 +247,54 @@ 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.
|
||||
@@ -341,9 +389,7 @@ export function readProjectConfig(projectRoot: string): ProjectConfig | null {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`Rules for '${artifactId}' must be an array of strings, ignoring this artifact's rules`
|
||||
);
|
||||
console.warn(describeRulesShapeError(artifactId, rules));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,9 @@ 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]+/g, ' ').trim();
|
||||
const flattened = value
|
||||
.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u206f]+/g, ' ')
|
||||
.trim();
|
||||
return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
|
||||
}
|
||||
|
||||
|
||||
@@ -484,6 +484,26 @@ 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.
|
||||
|
||||
@@ -2,10 +2,16 @@ import { z } from 'zod';
|
||||
import { VALIDATION_MESSAGES } from '../validation/constants.js';
|
||||
|
||||
export const ScenarioSchema = z.object({
|
||||
// Header text without `####`, the closing `#` run, and the `Scenario:`
|
||||
// prefix. Optional so objects built outside the parser still validate.
|
||||
name: z.string().optional(),
|
||||
rawText: z.string().min(1, VALIDATION_MESSAGES.SCENARIO_EMPTY),
|
||||
});
|
||||
|
||||
export const RequirementSchema = z.object({
|
||||
// Header text without `###` and the `Requirement:` prefix: the name archive
|
||||
// matches MODIFIED, REMOVED and RENAMED entries against.
|
||||
name: z.string().optional(),
|
||||
// SHALL/MUST body-keyword enforcement lives in the imperative validator
|
||||
// (Validator.applySpecRules), not here: the parser collapses the requirement
|
||||
// header into `text`, so a Zod refine on `text` cannot tell "keyword in header
|
||||
|
||||
@@ -74,7 +74,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
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
|
||||
- Task list with status, source path, and source line
|
||||
- Dynamic instruction based on current state
|
||||
- Optional \`context\`: current required project instruction input from the selected root
|
||||
- Optional \`operationGuidance\`: current advisory guidance for apply
|
||||
@@ -126,7 +126,9 @@ ${PROJECT_ROOT_GUARD}
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
|
||||
- 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
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
@@ -206,6 +208,7 @@ 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
|
||||
|
||||
@@ -158,12 +158,23 @@ ${PROJECT_ROOT_GUARD}
|
||||
form of main specs produced by this merge; do not use them as archive guidance,
|
||||
change CLI behavior, or copy the rule text into any output file.
|
||||
|
||||
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.
|
||||
Then ${optionalWorkflow('sync', 'run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge)', 'perform the delta-to-main-spec merge inline yourself (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; a spec the sync deliberately kept and reported is also a match
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -213,7 +224,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, run the \`openspec-sync-specs\` workflow inline (agent-driven)
|
||||
- If sync is requested, ${optionalWorkflow('sync', 'run the `openspec-sync-specs` workflow inline (agent-driven)', 'perform the delta-to-main-spec merge inline (agent-driven)')}
|
||||
- Never archive while a spec sync is still in flight — run the sync inline and verify the main specs before moving \`changeRoot\`
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
- Apply relevant runtime context and report conflicts; operation guidance remains advisory
|
||||
@@ -361,10 +372,21 @@ ${PROJECT_ROOT_GUARD}
|
||||
|
||||
Then ${SYNC_INLINE_HANDOFF} 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; a spec the sync deliberately kept and reported is also a match
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -220,6 +220,8 @@ ${PROJECT_ROOT_GUARD}
|
||||
|
||||
a. **Sync included delta specs**:
|
||||
- ${optionalWorkflow('sync', 'Run the `openspec-sync-specs` workflow inline (agent-driven intelligent merge)', 'Perform the delta-to-main-spec merge inline yourself (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
|
||||
@@ -234,7 +236,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
- 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; a spec the sync deliberately kept and reported is also a match
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -586,6 +588,8 @@ ${PROJECT_ROOT_GUARD}
|
||||
|
||||
a. **Sync included delta specs**:
|
||||
- ${SYNC_INLINE_HANDOFF} 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
|
||||
@@ -600,7 +604,7 @@ ${PROJECT_ROOT_GUARD}
|
||||
- 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; a spec the sync deliberately kept and reported is also a match
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -52,7 +52,7 @@ export const VALIDATION_MESSAGES = {
|
||||
'writes for a new capability, or a `TBD`/`TODO` marker left in its place). Replace it with what this ' +
|
||||
'capability is for, editing the main spec directly: a `## Purpose` in a delta is read only when the ' +
|
||||
'capability is created, so it cannot replace this one.',
|
||||
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Consider breaking it down.`,
|
||||
REQUIREMENT_TOO_LONG: `Requirement text is very long (>${MAX_REQUIREMENT_TEXT_LENGTH} characters). Move examples and edge cases into scenarios, or split it into separate requirements that each state one behavior.`,
|
||||
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
||||
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ import { FileSystemUtils } from '../../utils/file-system.js';
|
||||
import { discoverSpecFiles, findUnreadDeltaFiles, hasAnyFileUnder } from '../../utils/spec-discovery.js';
|
||||
import {
|
||||
METADATA_FILENAME,
|
||||
formatUnknownChangeMetadataKeysMessage,
|
||||
readSkipSpecsMarker,
|
||||
readUnknownChangeMetadataKeys,
|
||||
resolveSchemaForChange,
|
||||
} from '../../utils/change-metadata.js';
|
||||
import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
|
||||
@@ -491,6 +493,15 @@ export class Validator {
|
||||
issues.push({ level: 'ERROR', path: METADATA_FILENAME, message: this.formatInvalidMarkerMessage(marker.invalidReason) });
|
||||
}
|
||||
|
||||
const unknownMetadataKeys = readUnknownChangeMetadataKeys(changeDir);
|
||||
if (unknownMetadataKeys.length > 0) {
|
||||
issues.push({
|
||||
level: 'WARNING',
|
||||
path: METADATA_FILENAME,
|
||||
message: formatUnknownChangeMetadataKeysMessage(unknownMetadataKeys),
|
||||
});
|
||||
}
|
||||
|
||||
// ANY file under specs/ contradicts the marker - not just parsed deltas.
|
||||
// Headerless or stray files would be silently dropped at archive time (and
|
||||
// some still satisfy the artifact graph's specs/** glob) while the change
|
||||
|
||||
+134
-26
@@ -6,6 +6,7 @@ import { createRequire } from 'module';
|
||||
import chalk from 'chalk';
|
||||
import { isCiEnvironment } from '../utils/ci.js';
|
||||
import { isTelemetryOptedOutByEnv } from '../telemetry/opt-out.js';
|
||||
import { FileSystemUtils } from '../utils/file-system.js';
|
||||
import { getGlobalConfig, isGlobalConfigUnreadable } from './global-config.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -277,22 +278,43 @@ function fetchLatestVersion(): Promise<string | null> {
|
||||
});
|
||||
}
|
||||
|
||||
export type CliUpdateStatus = 'available' | 'current' | 'disabled' | 'offline';
|
||||
|
||||
export interface CliUpdateCheck {
|
||||
status: CliUpdateStatus;
|
||||
latest: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the published version when the installed CLI is behind it, otherwise
|
||||
* null. Never throws and never blocks for longer than the request timeout.
|
||||
* Checks the registry while preserving enough detail for callers to explain
|
||||
* why no newer version was reported. Never throws.
|
||||
*/
|
||||
export async function getAvailableCliUpdate(): Promise<string | null> {
|
||||
if (!isCheckEnabled()) return null;
|
||||
export async function checkForCliUpdate(): Promise<CliUpdateCheck> {
|
||||
if (!isCheckEnabled() || registryUrl() === null) {
|
||||
return { status: 'disabled', latest: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const latest = await fetchLatestVersion();
|
||||
if (!latest) return null;
|
||||
return compareVersions(latest, OPENSPEC_VERSION) > 0 ? latest : null;
|
||||
if (!latest) return { status: 'offline', latest: null };
|
||||
return {
|
||||
status: compareVersions(latest, OPENSPEC_VERSION) > 0 ? 'available' : 'current',
|
||||
latest,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
return { status: 'offline', latest: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the published version when the installed CLI is behind it, otherwise
|
||||
* null. Kept as the compatibility surface used by `openspec update`.
|
||||
*/
|
||||
export async function getAvailableCliUpdate(): Promise<string | null> {
|
||||
const result = await checkForCliUpdate();
|
||||
return result.status === 'available' ? result.latest : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory the running CLI was loaded from, or null when it cannot be
|
||||
* resolved. Shown in the upgrade hint so anyone who upgraded but still runs an
|
||||
@@ -326,8 +348,8 @@ export function isProjectLocalInstall(
|
||||
process.platform === 'win32' ? value.toLowerCase() : value;
|
||||
|
||||
try {
|
||||
let dir = path.resolve(projectPath);
|
||||
const target = normalize(installDir);
|
||||
let dir = FileSystemUtils.canonicalizeExistingPath(projectPath);
|
||||
const target = normalize(FileSystemUtils.canonicalizeExistingPath(installDir));
|
||||
|
||||
for (;;) {
|
||||
if (target.startsWith(normalize(path.join(dir, 'node_modules') + path.sep))) {
|
||||
@@ -468,29 +490,33 @@ export function isSourceCheckout(installDir: string | null): boolean {
|
||||
}
|
||||
|
||||
export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'yarn' | 'volta';
|
||||
export type InstallScope = 'global' | 'project' | 'temporary' | 'source';
|
||||
|
||||
export interface CliInstallInfo {
|
||||
location: string | null;
|
||||
packageManager: PackageManager | null;
|
||||
scope: InstallScope | null;
|
||||
}
|
||||
|
||||
function detectKnownPackageManager(installDir: string | null): PackageManager | null {
|
||||
const segments = (installDir ?? '').split(/[\\/]/).map((segment) => segment.toLowerCase());
|
||||
const has = (...names: string[]) => names.some((name) => segments.includes(name));
|
||||
|
||||
if (has('.volta') || (has('volta') && has('tools') && has('image'))) return 'volta';
|
||||
if (has('.bun', '_bunx', 'bun-cache')) return 'bun';
|
||||
if (has('_npx')) return 'npm';
|
||||
if (has('.pnpm', '.pnpm-global', 'pnpm-cache')) return 'pnpm';
|
||||
if (has('pnpm') && has('global', 'dlx', 'store')) return 'pnpm';
|
||||
if (has('.yarn') || (has('yarn') && has('global'))) return 'yarn';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The package manager that owns this copy, so the printed command is one the
|
||||
* user's setup will actually honor.
|
||||
*/
|
||||
export function detectPackageManager(installDir: string | null): PackageManager {
|
||||
// Lowercased because the Windows directories are capitalized and undotted:
|
||||
// %LOCALAPPDATA%\\Volta, \\Yarn\\Data, \\pnpm-cache.
|
||||
const segments = (installDir ?? '').split(/[\\/]/).map((segment) => segment.toLowerCase());
|
||||
const has = (...names: string[]) => names.some((name) => segments.includes(name));
|
||||
|
||||
// The undotted spelling exists for Windows (%LOCALAPPDATA%\Volta), whose
|
||||
// layout nests tools\image; require both segments so a user or project
|
||||
// directory merely named "volta" (even one with its own "tools" dir) does
|
||||
// not steal the install.
|
||||
if (has('.volta') || (has('volta') && has('tools') && has('image'))) return 'volta';
|
||||
if (has('.bun')) return 'bun';
|
||||
// These two need a corroborating segment: a directory merely named "pnpm" or
|
||||
// "yarn" (a user's home, a project) is not a global install of one.
|
||||
if (has('.pnpm-global', 'pnpm-cache')) return 'pnpm';
|
||||
if (has('pnpm') && has('global', 'dlx', 'store')) return 'pnpm';
|
||||
if (has('.yarn') || (has('yarn') && has('global'))) return 'yarn';
|
||||
return 'npm';
|
||||
return detectKnownPackageManager(installDir) ?? 'npm';
|
||||
}
|
||||
|
||||
const GLOBAL_UPGRADE_COMMANDS: Record<PackageManager, string> = {
|
||||
@@ -501,6 +527,88 @@ const GLOBAL_UPGRADE_COMMANDS: Record<PackageManager, string> = {
|
||||
volta: `volta install ${PACKAGE_NAME}@latest`,
|
||||
};
|
||||
|
||||
function detectGlobalPackageManager(installDir: string | null): PackageManager | null {
|
||||
if (isNpmGlobalInstall(installDir)) return 'npm';
|
||||
if (!installDir) return null;
|
||||
|
||||
const segments = installDir.split(/[\\/]/).map((segment) => segment.toLowerCase());
|
||||
const has = (...names: string[]) => names.some((name) => segments.includes(name));
|
||||
const hasSequence = (...names: string[]) =>
|
||||
segments.some((_, index) => names.every((name, offset) => segments[index + offset] === name));
|
||||
|
||||
if (hasSequence('.volta', 'tools', 'image') || hasSequence('volta', 'tools', 'image')) {
|
||||
return 'volta';
|
||||
}
|
||||
if (has('.pnpm-global') || hasSequence('pnpm', 'global')) return 'pnpm';
|
||||
if (hasSequence('yarn', 'global') || hasSequence('yarn', 'data', 'global')) return 'yarn';
|
||||
if (hasSequence('.bun', 'install', 'global')) return 'bun';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Describes the running copy without guessing when its owner is ambiguous. */
|
||||
export function getCliInstallInfo(
|
||||
installDir: string | null = getInstallDir(),
|
||||
projectPath: string = '.'
|
||||
): CliInstallInfo {
|
||||
if (!installDir) {
|
||||
return { location: null, packageManager: null, scope: null };
|
||||
}
|
||||
if (isSourceCheckout(installDir)) {
|
||||
return { location: installDir, packageManager: null, scope: 'source' };
|
||||
}
|
||||
if (isEphemeralRunnerInstall(installDir)) {
|
||||
return {
|
||||
location: installDir,
|
||||
packageManager: detectKnownPackageManager(installDir),
|
||||
scope: 'temporary',
|
||||
};
|
||||
}
|
||||
if (isProjectLocalInstall(installDir, projectPath)) {
|
||||
return {
|
||||
location: installDir,
|
||||
packageManager: detectKnownPackageManager(installDir),
|
||||
scope: 'project',
|
||||
};
|
||||
}
|
||||
|
||||
const packageManager = detectGlobalPackageManager(installDir);
|
||||
return {
|
||||
location: installDir,
|
||||
packageManager,
|
||||
scope: packageManager ? 'global' : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns a safe update command only for an install with known global ownership. */
|
||||
export function getCliUpdateCommand(install: CliInstallInfo): string | null {
|
||||
if (install.scope !== 'global' || install.packageManager === null) return null;
|
||||
return GLOBAL_UPGRADE_COMMANDS[install.packageManager];
|
||||
}
|
||||
|
||||
/** Builds the concise human-readable form of the version report. */
|
||||
export function buildVersionReportLines(
|
||||
version: string,
|
||||
install: CliInstallInfo,
|
||||
update?: CliUpdateCheck,
|
||||
command: string | null = null
|
||||
): string[] {
|
||||
const details = [install.packageManager, install.scope].filter(Boolean).join(', ');
|
||||
const lines = [`OpenSpec ${version}${details ? ` (${details})` : ''}`];
|
||||
if (!update) return lines;
|
||||
|
||||
if (update.status === 'available') {
|
||||
lines.push(`Update available: ${update.latest}`);
|
||||
if (command) lines.push(` ${command}`);
|
||||
} else if (update.status === 'current') {
|
||||
lines.push('OpenSpec is up to date.');
|
||||
} else if (update.status === 'disabled') {
|
||||
lines.push('Update check disabled.');
|
||||
} else {
|
||||
lines.push('Could not check for updates.');
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the hint, with the upgrade command chosen for how this copy of the CLI
|
||||
* was installed. Pure so every branch is assertable.
|
||||
|
||||
+74
-8
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
||||
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
||||
import { MarkdownParser } from './parsers/markdown-parser.js';
|
||||
import { discoverSpecFiles } from '../utils/spec-discovery.js';
|
||||
import { loadChangeContext, formatChangeStatus, type ChangeStatus } from './artifact-graph/index.js';
|
||||
|
||||
export class ViewCommand {
|
||||
async execute(targetPath: string = '.'): Promise<void> {
|
||||
@@ -37,6 +38,10 @@ export class ViewCommand {
|
||||
if (changesData.active.length > 0) {
|
||||
console.log(chalk.bold.cyan('\nActive Changes'));
|
||||
console.log('─'.repeat(60));
|
||||
const maxNameLength = Math.min(
|
||||
48,
|
||||
Math.max(30, ...changesData.active.map((change) => change.name.length))
|
||||
);
|
||||
changesData.active.forEach((change) => {
|
||||
const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
|
||||
const percentage =
|
||||
@@ -45,8 +50,12 @@ export class ViewCommand {
|
||||
: 0;
|
||||
|
||||
console.log(
|
||||
` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`
|
||||
` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(maxNameLength))} ${progressBar} ${chalk.dim(`${percentage}%`)}`
|
||||
);
|
||||
if (change.workflowStatus) {
|
||||
const { schemaName, artifacts } = change.workflowStatus;
|
||||
console.log(` ${chalk.dim(`└─ [${this.sanitizeWorkflowText(schemaName)}]`)} ${this.formatWorkflowArtifacts(artifacts)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,6 +68,15 @@ export class ViewCommand {
|
||||
});
|
||||
}
|
||||
|
||||
// Display archived changes
|
||||
if (changesData.archived.length > 0) {
|
||||
console.log(chalk.bold.gray('\nArchived Changes'));
|
||||
console.log('─'.repeat(60));
|
||||
changesData.archived.forEach((change) => {
|
||||
console.log(chalk.gray(` ◦ ${change.name}`));
|
||||
});
|
||||
}
|
||||
|
||||
// Display specifications
|
||||
if (specsData.length > 0) {
|
||||
console.log(chalk.bold.blue('\nSpecifications'));
|
||||
@@ -81,18 +99,34 @@ export class ViewCommand {
|
||||
|
||||
private async getChangesData(openspecDir: string): Promise<{
|
||||
draft: Array<{ name: string }>;
|
||||
active: Array<{ name: string; progress: { total: number; completed: number } }>;
|
||||
active: Array<{ name: string; progress: { total: number; completed: number }; workflowStatus?: ChangeStatus }>;
|
||||
completed: Array<{ name: string }>;
|
||||
archived: Array<{ name: string }>;
|
||||
}> {
|
||||
const changesDir = path.join(openspecDir, 'changes');
|
||||
const projectRoot = path.dirname(openspecDir);
|
||||
|
||||
if (!fs.existsSync(changesDir)) {
|
||||
return { draft: [], active: [], completed: [] };
|
||||
return { draft: [], active: [], completed: [], archived: [] };
|
||||
}
|
||||
|
||||
const draft: Array<{ name: string }> = [];
|
||||
const active: Array<{ name: string; progress: { total: number; completed: number } }> = [];
|
||||
const active: Array<{ name: string; progress: { total: number; completed: number }; workflowStatus?: ChangeStatus }> = [];
|
||||
const completed: Array<{ name: string }> = [];
|
||||
let archived: Array<{ name: string }> = [];
|
||||
|
||||
try {
|
||||
archived = fs.readdirSync(path.join(changesDir, 'archive'), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
||||
.map((entry) => ({ name: entry.name }));
|
||||
} catch (error) {
|
||||
// A missing archive, or an `archive` path that is a file, has no archived
|
||||
// changes to show; neither should break the rest of the dashboard.
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
||||
|
||||
@@ -108,7 +142,16 @@ export class ViewCommand {
|
||||
completed.push({ name: entry.name });
|
||||
} else {
|
||||
// Has tasks but not all complete
|
||||
active.push({ name: entry.name, progress });
|
||||
let workflowStatus: ChangeStatus | undefined;
|
||||
try {
|
||||
workflowStatus = formatChangeStatus(loadChangeContext(projectRoot, entry.name));
|
||||
} catch (error) {
|
||||
// Preserve task progress even when this change's workflow cannot be loaded.
|
||||
console.warn(chalk.yellow(this.sanitizeWorkflowText(
|
||||
`Could not load workflow status for "${entry.name}": ${error instanceof Error ? error.message : String(error)}`
|
||||
)));
|
||||
}
|
||||
active.push({ name: entry.name, progress, workflowStatus });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,8 +169,9 @@ export class ViewCommand {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
completed.sort((a, b) => a.name.localeCompare(b.name));
|
||||
archived.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return { draft, active, completed };
|
||||
return { draft, active, completed, archived };
|
||||
}
|
||||
|
||||
private async getSpecsData(openspecDir: string): Promise<Array<{ name: string; requirementCount: number }>> {
|
||||
@@ -156,7 +200,7 @@ export class ViewCommand {
|
||||
}
|
||||
|
||||
private displaySummary(
|
||||
changesData: { draft: any[]; active: any[]; completed: any[] },
|
||||
changesData: { draft: any[]; active: any[]; completed: any[]; archived: any[] },
|
||||
specsData: any[]
|
||||
): void {
|
||||
const totalChanges =
|
||||
@@ -189,6 +233,7 @@ export class ViewCommand {
|
||||
` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`
|
||||
);
|
||||
console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
|
||||
console.log(` ${chalk.gray('●')} Archived Changes: ${chalk.bold(changesData.archived.length)}`);
|
||||
|
||||
if (totalTasks > 0) {
|
||||
const overallProgress = Math.round((completedTasks / totalTasks) * 100);
|
||||
@@ -198,6 +243,27 @@ export class ViewCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeWorkflowText(value: string): string {
|
||||
// Metadata may contain terminal controls; mask them before adding our own colors.
|
||||
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, '?');
|
||||
}
|
||||
|
||||
private formatWorkflowArtifacts(artifacts: ChangeStatus['artifacts']): string {
|
||||
return artifacts.map((artifact) => {
|
||||
const id = this.sanitizeWorkflowText(artifact.id);
|
||||
switch (artifact.status) {
|
||||
case 'done':
|
||||
return `${id}${chalk.green('✓')}`;
|
||||
case 'ready':
|
||||
return `${id}${chalk.cyan('→')}`;
|
||||
case 'skipped':
|
||||
return chalk.dim(`${id} (skipped)`);
|
||||
case 'blocked':
|
||||
return chalk.dim(id);
|
||||
}
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
private createProgressBar(completed: number, total: number, width: number = 20): string {
|
||||
if (total === 0) return chalk.dim('─'.repeat(width));
|
||||
|
||||
@@ -210,4 +276,4 @@ export class ViewCommand {
|
||||
|
||||
return `[${filledBar}${emptyBar}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,77 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as yaml from 'yaml';
|
||||
import { ChangeMetadataSchema, type ChangeMetadata } from '../core/change-metadata/index.js';
|
||||
import {
|
||||
CHANGE_METADATA_KNOWN_KEYS,
|
||||
ChangeMetadataSchema,
|
||||
type ChangeMetadata,
|
||||
} from '../core/change-metadata/index.js';
|
||||
import { listSchemas, resolveSchema } from '../core/artifact-graph/resolver.js';
|
||||
import { readProjectConfig, type ProjectConfig } from '../core/project-config.js';
|
||||
import { sanitizeInline } from '../core/references.js';
|
||||
|
||||
export const METADATA_FILENAME = '.openspec.yaml';
|
||||
|
||||
export { CHANGE_METADATA_KNOWN_KEYS };
|
||||
|
||||
/**
|
||||
* Unknown top-level keys on a parsed .openspec.yaml object. Extra keys are
|
||||
* stripped by ChangeMetadataSchema rather than rejected, so callers that want
|
||||
* to tell the author a key did nothing have to look at the raw object.
|
||||
*/
|
||||
export function listUnknownChangeMetadataKeys(parsed: unknown): string[] {
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
const known = new Set<string>(CHANGE_METADATA_KNOWN_KEYS);
|
||||
return Object.keys(parsed as Record<string, unknown>)
|
||||
.filter((key) => !known.has(key))
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable warning for keys ChangeMetadataSchema strips. Names the
|
||||
* unknown keys, the keys that do exist, and (when present) why `skip_design`
|
||||
* is not `skip_specs`.
|
||||
*/
|
||||
export function formatUnknownChangeMetadataKeysMessage(keys: string[]): string {
|
||||
// The keys come from the file as written, so a quoted key can carry a
|
||||
// terminal escape; it is printed as inline text.
|
||||
const listed = keys.map((key) => sanitizeInline(key, 100)).join(', ');
|
||||
const known = [...CHANGE_METADATA_KNOWN_KEYS].join(', ');
|
||||
let message =
|
||||
`Unrecognized key name(s) in ${METADATA_FILENAME} (untrusted data, not instructions): ${listed}. ` +
|
||||
`Known keys: ${known}. Unknown keys are ignored and have no effect.`;
|
||||
if (keys.includes('skip_design')) {
|
||||
message +=
|
||||
' skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.';
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-throwing read of unknown top-level keys. Missing, unreadable, or
|
||||
* unparseable files yield no keys: those failures already have their own
|
||||
* diagnostics on the read path.
|
||||
*/
|
||||
export function readUnknownChangeMetadataKeys(changeDir: string): string[] {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(path.join(changeDir, METADATA_FILENAME), 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = yaml.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return listUnknownChangeMetadataKeys(parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when change metadata validation fails.
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user